Skip to content

AuthZen PDP client#4177

Draft
stevenvegt wants to merge 4 commits into4144-1-scope-parsing-and-configfrom
4144-2-authzen-client
Draft

AuthZen PDP client#4177
stevenvegt wants to merge 4 commits into4144-1-scope-parsing-and-configfrom
4144-2-authzen-client

Conversation

@stevenvegt
Copy link
Copy Markdown
Member

@stevenvegt stevenvegt commented Apr 13, 2026

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 decrease total coverage on 4144-1-scope-parsing-and-config by 0.03%.

Modified Files with Diff Coverage (1)

RatingFile% DiffUncovered Line #s
New file Coverage rating: B
policy/authzen/client.go88.5%54-55, 58-59, 84-85
Total88.5%
🤖 Increase coverage with AI coding...

In the `4144-2-authzen-client` branch, add test coverage for this new code:

- `policy/authzen/client.go` -- Lines 54-55, 58-59, and 84-85

🚦 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
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

1 participant