Skip to content

AuthZen PDP client#4177

Merged
stevenvegt merged 5 commits intofeature/4144-mixed-scopesfrom
4144-2-authzen-client
Apr 24, 2026
Merged

AuthZen PDP client#4177
stevenvegt merged 5 commits intofeature/4144-mixed-scopesfrom
4144-2-authzen-client

Conversation

@stevenvegt
Copy link
Copy Markdown
Member

@stevenvegt stevenvegt commented Apr 13, 2026

Parent PRD

#4144

Summary

New HTTP client for the AuthZen Access Evaluations batch API (POST /access/v1/evaluations). Provides the types and the transport layer for dynamic scope policy evaluation. The server-side token flow in #4179 uses this client to evaluate requested scopes against an external PDP.

What changed

New package policy/authzen:

  • types.go — AuthZen request/response types matching the AuthZen spec batch format
  • client.go — Thin HTTP client with a single Evaluate method returning map[string]bool (scope → decision)
  • client_test.go — Unit tests using httptest.NewServer

Batch format: top-level subject / action / context are shared defaults, and an evaluations array contains per-scope resource overrides. This avoids repeating shared fields for every scope.

Error handling:

  • HTTP non-2xx → error with status code, PDP error body truncated to 200 chars (prevents log injection)
  • Network errors / cancelled context → wrapped error
  • Malformed JSON / evaluation count mismatch → error
  • Duplicate resource IDs in request → early error before HTTP call (client-side invariant check)

How to review

Start with types.go — verify the AuthZen request/response shape against the spec (the batch format uses top-level defaults + per-evaluation overrides).

Then client.go — single Evaluate method:

  • Sends JSON with Content-Type + Accept headers
  • Reads the response through the caller-provided HTTPRequestDoer (StrictHTTPClient is used in wiring for response body limits / timeout)
  • Validates evaluation count matches request
  • Returns map[string]bool keyed by Resource.ID

Tests (client_test.go) — 8 scenarios covering the full surface: success, partial denial, HTTP 500, unreachable endpoint, cancelled context, count mismatch, malformed response, duplicate resource IDs.

Deviations from spec

  • Request type changed from the original spec. The original spec had Resource []Resource, implying a flat batch with multiple resources. During planning we verified this didn't match the AuthZen batch API. Corrected to Evaluations []Evaluation with per-evaluation overrides, matching the spec's top-level-defaults-plus-overrides pattern.
  • Claim extraction removed from this PR. Originally the spec included an ExtractSubjectProperties helper. Moved to Server-side multi-scope flow & scope policy evaluation #4179 where the credential map's shape is concrete. This client is transport-only — it takes a pre-built EvaluationsRequest from the caller.
  • NewClient signature simplified — no timeout parameter. Timeout is the responsibility of the injected HTTPRequestDoer (wired with StrictHTTPClient in Server-side multi-scope flow & scope policy evaluation #4179).
  • Added at self-review: duplicate resource ID precondition check, error body truncation, Accept: application/json header.
  • subject.type in the PRD example was updated from "token_request" to "organization" (see PRD comment). The AuthZen client itself is agnostic — the caller sets the type.

Dependencies

Depends on: #4176 (foundation PR, provides the base branch).

Review order: Review #4176 first for the foundation; this PR can then be reviewed independently as it's a self-contained new package with no cross-cutting changes.

Design context

Acceptance Criteria

  • AuthZen client sends correctly formatted batch evaluation requests (per AuthZen spec)
  • Response parsed into scope → decision map
  • Timeout and error handling works correctly
  • Evaluation count mismatch detected and reported
  • Duplicate resource IDs in request detected (added during self-review)
  • Error response body truncated to prevent log injection (added during self-review)
  • Unit tests with HTTP mock cover success, partial denial, errors
Original implementation spec (used during AI-assisted development)

Parent PRD

#4144

Implementation Spec

Overview

New HTTP client for the AuthZen batch Access Evaluations API (POST /access/v1/evaluations). This client is used by the server-side token flow (PR #4179) when scope_policy: "dynamic" to evaluate requested scopes against an external PDP.

Key files to create/modify

  • policy/authzen/client.go — New HTTP client
  • policy/authzen/types.go — Request/response types
  • policy/authzen/client_test.go — Unit tests

Design decisions

  • Batch format follows the AuthZen spec: top-level subject/action/context are shared defaults, the evaluations array contains per-scope resource overrides. This avoids repeating the same subject/action/context for every scope.
  • Claim extraction deferred to PR Server-side multi-scope flow & scope policy evaluation #4179: The ExtractSubjectProperties helper depends on how the credential map from PEX evaluation maps to the organization bucket. This becomes clear in the server-side flow where the actual credential data is available. The AuthZen client takes a pre-built EvaluationsRequest — it doesn't need to know about credentials.
  • map[string]bool return type: Denial reasons from the AuthZen response are logged for debugging but not exposed in the return value. Rego-produced reasons are typically terse and few. A richer return type can be added later if operators need it.

AuthZen request/response examples

Request (POST /access/v1/evaluations):

{
  "subject": {
    "type": "token_request",
    "id": "did:web:hospital.example.com",
    "properties": {
      "organization": {
        "id": "did:web:hospital.example.com",
        "name": "Hospital B.V.",
        "ura": "12345678"
      }
    }
  },
  "action": { "name": "request_scope" },
  "context": { "policy": "urn:nuts:medication-overview" },
  "evaluations": [
    { "resource": { "type": "scope", "id": "urn:nuts:medication-overview" } },
    { "resource": { "type": "scope", "id": "patient/Observation.read" } },
    { "resource": { "type": "scope", "id": "launch/patient" } }
  ]
}

Response:

{
  "evaluations": [
    { "decision": true },
    { "decision": true },
    { "decision": false, "context": { "reason": "scope not permitted by policy" } }
  ]
}

Go types

type EvaluationsRequest struct {
    Subject     Subject           `json:"subject"`
    Action      Action            `json:"action"`
    Context     EvaluationContext `json:"context"`
    Evaluations []Evaluation      `json:"evaluations"`
}

type Evaluation struct {
    Resource Resource `json:"resource"`
}

type Subject struct {
    Type       string            `json:"type"`
    ID         string            `json:"id"`
    Properties SubjectProperties `json:"properties"`
}

type SubjectProperties struct {
    Client       map[string]any `json:"client,omitempty"`
    Organization map[string]any `json:"organization,omitempty"`
    User         map[string]any `json:"user,omitempty"`
}

type Action struct {
    Name string `json:"name"`
}

type Resource struct {
    Type string `json:"type"`
    ID   string `json:"id"`
}

type EvaluationContext struct {
    Policy string `json:"policy"`
}

type EvaluationsResponse struct {
    Evaluations []EvaluationResult `json:"evaluations"`
}

type EvaluationResult struct {
    Decision bool                    `json:"decision"`
    Context  *EvaluationResultContext `json:"context,omitempty"`
}

type EvaluationResultContext struct {
    Reason string `json:"reason,omitempty"`
}

Client implementation

type Client struct {
    endpoint   string
    httpClient core.HTTPRequestDoer
}

func NewClient(endpoint string, httpClient core.HTTPRequestDoer) *Client

// Evaluate sends a batch evaluation request for the given scopes.
// Returns a map of scope → decision (true = granted, false = denied).
func (c *Client) Evaluate(ctx context.Context, req EvaluationsRequest) (map[string]bool, error)

The Evaluate method:

  1. Serializes the request to JSON.
  2. POSTs to {endpoint}/access/v1/evaluations.
  3. Parses the response.
  4. Maps each evaluation result back to the corresponding scope (by index).
  5. Returns a map[string]bool for easy lookup.

Error handling

  • HTTP errors (non-2xx) → return error with status code
  • Network errors / timeouts → return error (caller maps to 503)
  • Malformed response → return error
  • Evaluation count mismatch (response has fewer evaluations than scopes) → return error

Testing

  • Successful evaluation: batch request → scope→decision map
  • Partial denial: some scopes approved, some denied
  • HTTP error: PDP returns 500 → error
  • Timeout: PDP unreachable → error
  • Evaluation count mismatch: response has fewer evaluations than request → error
  • Malformed response: invalid JSON → error
  • Use httptest.NewServer for HTTP mocking

Acceptance Criteria

  • AuthZen client sends correctly formatted batch evaluation requests (per AuthZen spec)
  • Response parsed into scope → decision map
  • Timeout and error handling works correctly
  • Evaluation count mismatch detected and reported
  • Duplicate resource IDs in request detected (added during self-review)
  • Error response body truncated to prevent log injection (added during self-review)
  • Unit tests with HTTP mock cover success, partial denial, errors

@qltysh
Copy link
Copy Markdown

qltysh Bot commented Apr 13, 2026

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on feature/4144-mixed-scopes by 0.01%.

Modified Files with Diff Coverage (1)

RatingFile% DiffUncovered Line #s
New Coverage rating: A
policy/authzen/client.go91.3%52-53, 56-57
Total91.3%
🤖 Increase coverage with AI coding...
In the `4144-2-authzen-client` branch, add test coverage for this new code:

- `policy/authzen/client.go` -- Lines 52-53 and 56-57

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

stevenvegt and others added 4 commits April 14, 2026 08:52
Implements the HTTP client for the AuthZen Access Evaluations API
(POST /access/v1/evaluations). Request uses AuthZen batch format:
shared subject/action/context with per-scope evaluations array.
Returns scope→decision map.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests cover: partial denial, HTTP 500, PDP unreachable, context
cancellation/timeout, evaluation count mismatch, malformed response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Truncate PDP error body in error messages (prevent log injection)
- Validate duplicate resource IDs before sending request
- Add Accept: application/json header
- Add package doc comment
- Fix require.NoError inside httptest handler (capture request, assert outside)
- Rename context cancellation test for accuracy
- Add duplicate resource ID test
- Response body limiting delegated to StrictHTTPClient (caller responsibility)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@stevenvegt stevenvegt force-pushed the 4144-2-authzen-client branch from 7c44b2a to 4f08bcf Compare April 14, 2026 07:46
@stevenvegt stevenvegt marked this pull request as ready for review April 16, 2026 14:38
@stevenvegt stevenvegt mentioned this pull request Apr 16, 2026
20 tasks
Comment thread policy/authzen/client.go
Comment thread policy/authzen/client.go Outdated
Comment thread policy/authzen/client.go Outdated
Comment thread policy/authzen/client.go Outdated
- Adopt core.TestResponseCode for status validation; drops bespoke
  status check and error-body truncation (HttpError message omits the
  body, so no log-injection risk).
- Wrap PDP error as "authzen: PDP call failed" to disambiguate from
  the AS server in mixed log output.
- Replace duplicate-resource-ID comment with the actual rationale:
  AuthZen correlates request/response by index, so duplicate IDs would
  collapse map[string]bool decisions silently.
- Clarify NewClient godoc: httpClient must enforce timeouts, TLS, and
  body size limits (use http/client.StrictHTTPClient in production).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qltysh
Copy link
Copy Markdown

qltysh Bot commented Apr 21, 2026

All good ✅

Base automatically changed from 4144-1-scope-parsing-and-config to feature/4144-mixed-scopes April 24, 2026 12:33
@stevenvegt stevenvegt merged commit e0bcb9c into feature/4144-mixed-scopes Apr 24, 2026
3 of 4 checks passed
@stevenvegt stevenvegt deleted the 4144-2-authzen-client branch April 24, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support mixed OAuth2 scopes with configurable scope policy

2 participants