欢迎光临
我们一直在努力

Authn & Authz for Microservice: 08 — OPA (PDP)

📦 GitHub: https://github.com/geekchow/micro-service-auth

OPA is the Policy Decision Point: it receives authorization input from Kong and returns allow or deny.

What OPA Is

OPA stands for Open Policy Agent.

In this project, OPA is the PDP (Policy Decision Point) — see 01 — Concepts for the full IdP / PEP / PDP glossary.

OPA does one job:

  • receive a structured input document
  • evaluate it against Rego policy rules
  • return a decision: allow or deny

OPA does not authenticate users. That is Keycloak’s role. OPA does not enforce the decision at the edge. That is Kong’s role. OPA only decides.

Why This Project Needs OPA

Authorization logic could have been written directly inside:

  • Kong plugin code
  • banking-api-service Java code

But that would couple policy to enforcement or business logic.

Using OPA as a dedicated PDP gives three concrete benefits:

  • Policy stays separate from gateway and service code.
  • Policy can be tested independently with rego test.
  • Policy can change without rewriting Kong plugins or banking-api-service.
  • In this PoC:

    • Keycloak proves who the user is.
    • Kong enforces at the edge (PEP).
    • OPA decides whether the action is allowed (PDP).
    • banking-api-service adds defense in depth (resource server).

    Where OPA Sits In The Architecture

    #mermaid-svg-LMsYcDIJ651tAplh{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LMsYcDIJ651tAplh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LMsYcDIJ651tAplh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LMsYcDIJ651tAplh .error-icon{fill:#552222;}#mermaid-svg-LMsYcDIJ651tAplh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LMsYcDIJ651tAplh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LMsYcDIJ651tAplh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LMsYcDIJ651tAplh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LMsYcDIJ651tAplh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LMsYcDIJ651tAplh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LMsYcDIJ651tAplh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LMsYcDIJ651tAplh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LMsYcDIJ651tAplh .marker.cross{stroke:#333333;}#mermaid-svg-LMsYcDIJ651tAplh svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LMsYcDIJ651tAplh p{margin:0;}#mermaid-svg-LMsYcDIJ651tAplh .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-LMsYcDIJ651tAplh .cluster-label text{fill:#333;}#mermaid-svg-LMsYcDIJ651tAplh .cluster-label span{color:#333;}#mermaid-svg-LMsYcDIJ651tAplh .cluster-label span p{background-color:transparent;}#mermaid-svg-LMsYcDIJ651tAplh .label text,#mermaid-svg-LMsYcDIJ651tAplh span{fill:#333;color:#333;}#mermaid-svg-LMsYcDIJ651tAplh .node rect,#mermaid-svg-LMsYcDIJ651tAplh .node circle,#mermaid-svg-LMsYcDIJ651tAplh .node ellipse,#mermaid-svg-LMsYcDIJ651tAplh .node polygon,#mermaid-svg-LMsYcDIJ651tAplh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LMsYcDIJ651tAplh .rough-node .label text,#mermaid-svg-LMsYcDIJ651tAplh .node .label text,#mermaid-svg-LMsYcDIJ651tAplh .image-shape .label,#mermaid-svg-LMsYcDIJ651tAplh .icon-shape .label{text-anchor:middle;}#mermaid-svg-LMsYcDIJ651tAplh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LMsYcDIJ651tAplh .rough-node .label,#mermaid-svg-LMsYcDIJ651tAplh .node .label,#mermaid-svg-LMsYcDIJ651tAplh .image-shape .label,#mermaid-svg-LMsYcDIJ651tAplh .icon-shape .label{text-align:center;}#mermaid-svg-LMsYcDIJ651tAplh .node.clickable{cursor:pointer;}#mermaid-svg-LMsYcDIJ651tAplh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LMsYcDIJ651tAplh .arrowheadPath{fill:#333333;}#mermaid-svg-LMsYcDIJ651tAplh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LMsYcDIJ651tAplh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LMsYcDIJ651tAplh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LMsYcDIJ651tAplh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LMsYcDIJ651tAplh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LMsYcDIJ651tAplh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LMsYcDIJ651tAplh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LMsYcDIJ651tAplh .cluster text{fill:#333;}#mermaid-svg-LMsYcDIJ651tAplh .cluster span{color:#333;}#mermaid-svg-LMsYcDIJ651tAplh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LMsYcDIJ651tAplh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LMsYcDIJ651tAplh rect.text{fill:none;stroke-width:0;}#mermaid-svg-LMsYcDIJ651tAplh .icon-shape,#mermaid-svg-LMsYcDIJ651tAplh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LMsYcDIJ651tAplh .icon-shape p,#mermaid-svg-LMsYcDIJ651tAplh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LMsYcDIJ651tAplh .icon-shape .label rect,#mermaid-svg-LMsYcDIJ651tAplh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LMsYcDIJ651tAplh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LMsYcDIJ651tAplh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LMsYcDIJ651tAplh :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    identity and token activity

    allow or deny

    business response

    Client

    Kong

    Keycloak

    OPA

    banking-api-service

    OPA sits between Kong and the upstream banking-api-service. Kong calls OPA synchronously on every request, waits for the decision, then either forwards the request or returns 403.

    The Input → Policy → Result Model

    OPA is a general-purpose engine. It does not know what a bank account is by itself. It only knows:

  • what input it received
  • what rules were written in Rego
  • #mermaid-svg-ysHpr1pg5gALjfSP{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ysHpr1pg5gALjfSP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ysHpr1pg5gALjfSP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ysHpr1pg5gALjfSP .error-icon{fill:#552222;}#mermaid-svg-ysHpr1pg5gALjfSP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ysHpr1pg5gALjfSP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ysHpr1pg5gALjfSP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ysHpr1pg5gALjfSP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ysHpr1pg5gALjfSP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ysHpr1pg5gALjfSP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ysHpr1pg5gALjfSP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ysHpr1pg5gALjfSP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ysHpr1pg5gALjfSP .marker.cross{stroke:#333333;}#mermaid-svg-ysHpr1pg5gALjfSP svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ysHpr1pg5gALjfSP p{margin:0;}#mermaid-svg-ysHpr1pg5gALjfSP .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ysHpr1pg5gALjfSP .cluster-label text{fill:#333;}#mermaid-svg-ysHpr1pg5gALjfSP .cluster-label span{color:#333;}#mermaid-svg-ysHpr1pg5gALjfSP .cluster-label span p{background-color:transparent;}#mermaid-svg-ysHpr1pg5gALjfSP .label text,#mermaid-svg-ysHpr1pg5gALjfSP span{fill:#333;color:#333;}#mermaid-svg-ysHpr1pg5gALjfSP .node rect,#mermaid-svg-ysHpr1pg5gALjfSP .node circle,#mermaid-svg-ysHpr1pg5gALjfSP .node ellipse,#mermaid-svg-ysHpr1pg5gALjfSP .node polygon,#mermaid-svg-ysHpr1pg5gALjfSP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ysHpr1pg5gALjfSP .rough-node .label text,#mermaid-svg-ysHpr1pg5gALjfSP .node .label text,#mermaid-svg-ysHpr1pg5gALjfSP .image-shape .label,#mermaid-svg-ysHpr1pg5gALjfSP .icon-shape .label{text-anchor:middle;}#mermaid-svg-ysHpr1pg5gALjfSP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ysHpr1pg5gALjfSP .rough-node .label,#mermaid-svg-ysHpr1pg5gALjfSP .node .label,#mermaid-svg-ysHpr1pg5gALjfSP .image-shape .label,#mermaid-svg-ysHpr1pg5gALjfSP .icon-shape .label{text-align:center;}#mermaid-svg-ysHpr1pg5gALjfSP .node.clickable{cursor:pointer;}#mermaid-svg-ysHpr1pg5gALjfSP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ysHpr1pg5gALjfSP .arrowheadPath{fill:#333333;}#mermaid-svg-ysHpr1pg5gALjfSP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ysHpr1pg5gALjfSP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ysHpr1pg5gALjfSP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ysHpr1pg5gALjfSP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ysHpr1pg5gALjfSP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ysHpr1pg5gALjfSP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ysHpr1pg5gALjfSP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ysHpr1pg5gALjfSP .cluster text{fill:#333;}#mermaid-svg-ysHpr1pg5gALjfSP .cluster span{color:#333;}#mermaid-svg-ysHpr1pg5gALjfSP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ysHpr1pg5gALjfSP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ysHpr1pg5gALjfSP rect.text{fill:none;stroke-width:0;}#mermaid-svg-ysHpr1pg5gALjfSP .icon-shape,#mermaid-svg-ysHpr1pg5gALjfSP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ysHpr1pg5gALjfSP .icon-shape p,#mermaid-svg-ysHpr1pg5gALjfSP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ysHpr1pg5gALjfSP .icon-shape .label rect,#mermaid-svg-ysHpr1pg5gALjfSP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ysHpr1pg5gALjfSP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ysHpr1pg5gALjfSP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ysHpr1pg5gALjfSP :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    Input JSON

    Rego Policy

    result: true or false

    In this repo:

    • Kong constructs and sends the input
    • banking_authz.rego defines the rules
    • OPA returns result: true or result: false

    Rego Basics

    OPA policies are written in Rego, a declarative language.

    You describe what must be true for access to be granted, rather than writing step-by-step imperative logic.

    In practice the rules in this repo read as:

    • allow if the route is a read route and role is ops-admin
    • allow if the route is a read route, role is customer, and the requested account is in the token’s account_ids
    • deny everything else

    Deny By Default

    The most important line in the policy is:

    default allow := false

    Unless one of the allow rules matches, the answer is deny. This is safer than enumerating deny rules — a missing deny rule can never accidentally open access.

    The Actual Policy In This Repo

    File: infra/opa/policies/banking_authz.rego

    package banking_authz

    default allow := false

    allow {
    read_only_account_request
    input.role == "ops-admin"
    }

    allow {
    read_only_account_request
    input.role == "customer"
    input.customer_id != ""
    account_ids := object.get(input, "account_ids", [])
    account_ids[_] == input.account_id
    }

    read_only_account_request {
    input.method == "GET"
    regex.match("^/api/accounts/[^/]+(?:/transactions)?$", input.path)
    }

    Policy Walkthrough

    package banking_authz

    Places the rules in the banking_authz package. Kong queries OPA at:

    http://opa:8181/v1/data/banking_authz/allow

    So banking_authz is the package and allow is the decision being queried.

    default allow := false

    Everything is denied unless one of the allow rules below matches.

    First allow Rule — ops-admin

    allow {
    read_only_account_request
    input.role == "ops-admin"
    }

    Meaning:

    • the request must be a valid read route (via helper)
    • the caller must have role ops-admin
    • no account ownership check — ops-admin may read any account

    Second allow Rule — customer

    allow {
    read_only_account_request
    input.role == "customer"
    input.customer_id != ""
    account_ids := object.get(input, "account_ids", [])
    account_ids[_] == input.account_id
    }

    Meaning:

    • the request must be a valid read route
    • the caller must have role customer
    • the token must carry a non-empty customer_id
    • the requested account_id must appear in the token’s account_ids list

    This is the core customer ownership check.

    Helper Rule: read_only_account_request

    read_only_account_request {
    input.method == "GET"
    regex.match("^/api/accounts/[^/]+(?:/transactions)?$", input.path)
    }

    Meaning:

    • only GET requests pass this gate
    • only two path shapes are permitted:
      • /api/accounts/{accountId}
      • /api/accounts/{accountId}/transactions

    This prevents future non-read routes from being accidentally allowed by the same allow rules.

    What Input OPA Receives

    OPA does not read HTTP requests directly. Kong constructs a structured input document and POSTs it as JSON.

    The fields OPA actually consumes in the policy are:

    FieldTypeUsed by
    method string read_only_account_request
    path string read_only_account_request
    role string both allow rules
    customer_id string customer allow rule
    account_ids array of strings customer allow rule
    account_id string customer allow rule

    username is included in the input but not consumed by policy rules in this version.

    See 14 — Request & Response Details for the full claim catalog.

    Example input document for alice:

    {
    "input": {
    "method": "GET",
    "path": "/api/accounts/A-1001",
    "account_id": "A-1001",
    "customer_id": "C-1001",
    "account_ids": ["A-1001"],
    "role": "customer",
    "username": "alice"
    }
    }

    How Kong Sends Input To OPA

    Kong calls the OPA REST API via the opa-authz plugin. The URL comes from infra/kong/kong.yml:

    plugins:
    name: opaauthz
    config:
    opa_url: http://opa:8181/v1/data/banking_authz/allow

    The plugin handler (infra/kong/plugins/opa-authz/handler.lua) constructs the input body:

    local request_body = cjson.encode({
    input = {
    method = kong.request.get_method(),
    path = kong.request.get_path(),
    account_id = account_id,
    customer_id = claim_value(claims.customer_id),
    account_ids = claim_values(claims.account_ids),
    role = effective_role(claims),
    username = claims.preferred_username,
    },
    })

    OPA receives a clean authorization input extracted from the validated JWT, not the raw HTTP request.

    What OPA Returns

    If the policy allows:

    { "result": true }

    If the policy denies:

    { "result": false }

    Kong maps that to behavior:

    • result: true → forward request upstream to banking-api-service
    • result: false → return 403 Forbidden to the client

    OPA Request Flow In This PoC

    banking-api-service

    OPA

    Keycloak

    Kong

    Client

    banking-api-service

    OPA

    Keycloak

    Kong

    Client

    #mermaid-svg-JxVcqMPRruIPue9m{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JxVcqMPRruIPue9m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JxVcqMPRruIPue9m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JxVcqMPRruIPue9m .error-icon{fill:#552222;}#mermaid-svg-JxVcqMPRruIPue9m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JxVcqMPRruIPue9m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JxVcqMPRruIPue9m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JxVcqMPRruIPue9m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JxVcqMPRruIPue9m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JxVcqMPRruIPue9m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JxVcqMPRruIPue9m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JxVcqMPRruIPue9m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JxVcqMPRruIPue9m .marker.cross{stroke:#333333;}#mermaid-svg-JxVcqMPRruIPue9m svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JxVcqMPRruIPue9m p{margin:0;}#mermaid-svg-JxVcqMPRruIPue9m .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JxVcqMPRruIPue9m text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JxVcqMPRruIPue9m .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JxVcqMPRruIPue9m .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JxVcqMPRruIPue9m .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JxVcqMPRruIPue9m .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JxVcqMPRruIPue9m #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JxVcqMPRruIPue9m .sequenceNumber{fill:white;}#mermaid-svg-JxVcqMPRruIPue9m #sequencenumber{fill:#333;}#mermaid-svg-JxVcqMPRruIPue9m #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JxVcqMPRruIPue9m .messageText{fill:#333;stroke:none;}#mermaid-svg-JxVcqMPRruIPue9m .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JxVcqMPRruIPue9m .labelText,#mermaid-svg-JxVcqMPRruIPue9m .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JxVcqMPRruIPue9m .loopText,#mermaid-svg-JxVcqMPRruIPue9m .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JxVcqMPRruIPue9m .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JxVcqMPRruIPue9m .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JxVcqMPRruIPue9m .noteText,#mermaid-svg-JxVcqMPRruIPue9m .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JxVcqMPRruIPue9m .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JxVcqMPRruIPue9m .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JxVcqMPRruIPue9m .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JxVcqMPRruIPue9m .actorPopupMenu{position:absolute;}#mermaid-svg-JxVcqMPRruIPue9m .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JxVcqMPRruIPue9m .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JxVcqMPRruIPue9m .actor-man circle,#mermaid-svg-JxVcqMPRruIPue9m line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JxVcqMPRruIPue9m :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    alt

    [result false]

    [result true]

    GET /api/accounts/A-1001 + JWT

    Introspect token

    active true

    POST policy input JSON

    Evaluate Rego rules

    result true or false

    403 Forbidden

    forward request

    banking response

    200 response

    How OPA Is Run In Docker Compose

    opa:
    image: openpolicyagent/opa:0.68.0
    command: ["run", "–server", "–addr=0.0.0.0:8181", "/policies"]
    ports:
    "8181:8181"
    volumes:
    ./infra/opa/policies:/policies:ro

    • OPA runs as a standalone HTTP server on port 8181
    • Policies are mounted read-only from infra/opa/policies
    • OPA is not compiled into any Java service — it is a separate container

    This separation means policy can be updated, tested, and reloaded independently of Kong or banking-api-service.

    The Tests

    File: infra/opa/policies/banking_authz_test.rego

    package banking_authz_test

    import data.banking_authz

    Allow: ops-admin reads an account

    test_ops_admin_is_allowed {
    banking_authz.allow with input as {
    "method": "GET",
    "path": "/api/accounts/A-1001",
    "role": "ops-admin",
    "account_id": "A-1001",
    "customer_id": "C-9999",
    }
    }

    ops-admin may read any account endpoint regardless of customer ownership.

    Allow: customer reads their own account

    test_customer_can_access_owned_account {
    banking_authz.allow with input as {
    "method": "GET",
    "path": "/api/accounts/A-1001",
    "role": "customer",
    "account_id": "A-1001",
    "customer_id": "C-1001",
    "account_ids": ["A-1001"],
    }
    }

    alice (customer C-1001) may read account A-1001 when it appears in her account_ids.

    Allow: customer reads their own transactions

    test_customer_can_access_owned_account_transactions {
    banking_authz.allow with input as {
    "method": "GET",
    "path": "/api/accounts/A-1001/transactions",
    "role": "customer",
    "account_id": "A-1001",
    "customer_id": "C-1001",
    "account_ids": ["A-1001"],
    }
    }

    The transactions sub-resource is also permitted for an owned account.

    Deny cases

    The test file proves denial for all of the following:

    TestWhat it proves
    test_customer_cannot_access_other_account account_ids does not contain the requested account
    test_customer_without_claimed_account_is_denied account_ids is empty
    test_customer_without_customer_id_is_denied customer_id field is absent
    test_ops_admin_post_account_is_denied POST fails read_only_account_request
    test_customer_subresource_path_is_denied /cards path not matched by regex
    test_other_roles_are_denied role auditor matches neither allow rule

    Negative tests matter as much as positive ones: a policy is only trustworthy if you also prove what it denies.

    Practical Examples

    alice Reading Her Own Account

    Input:

    {
    "input": {
    "method": "GET",
    "path": "/api/accounts/A-1001",
    "account_id": "A-1001",
    "customer_id": "C-1001",
    "account_ids": ["A-1001"],
    "role": "customer",
    "username": "alice"
    }
    }

    Result: allow

    Reason:

    • GET + matching path → read_only_account_request passes
    • role == "customer"
    • customer_id is non-empty
    • A-1001 is in account_ids

    alice Attempting Another Customer’s Account

    Input:

    {
    "input": {
    "method": "GET",
    "path": "/api/accounts/A-2001",
    "account_id": "A-2001",
    "customer_id": "C-1001",
    "account_ids": ["A-1001"],
    "role": "customer",
    "username": "alice"
    }
    }

    Result: deny

    Reason:

    • A-2001 is not in alice’s account_ids (["A-1001"])

    ops-admin Reading Any Account

    Input:

    {
    "input": {
    "method": "GET",
    "path": "/api/accounts/A-2001",
    "account_id": "A-2001",
    "customer_id": "C-9999",
    "role": "ops-admin"
    }
    }

    Result: allow

    Reason:

    • ops-admin rule does not check account ownership

    POST Request (Denied For Both Roles)

    Input:

    {
    "input": {
    "method": "POST",
    "path": "/api/accounts/A-1001",
    "role": "ops-admin",
    "account_id": "A-1001",
    "customer_id": "C-9999"
    }
    }

    Result: deny

    Reason:

    • read_only_account_request fails because method != "GET"

    Unsupported Sub-Resource Path

    Input:

    {
    "input": {
    "method": "GET",
    "path": "/api/accounts/A-1001/cards",
    "role": "customer",
    "account_id": "A-1001",
    "customer_id": "C-1001",
    "account_ids": ["A-1001"]
    }
    }

    Result: deny

    Reason:

    • regex only matches /api/accounts/{id} and /api/accounts/{id}/transactions
    • /cards does not match

    What OPA Does Not Do

    OPA is powerful, but it has clear limits in this PoC:

    • does not authenticate users (that is Keycloak)
    • does not issue JWTs (that is Keycloak)
    • does not introspect tokens itself in this request path (that is the Kong plugin)
    • does not validate JWT signatures itself here (the Kong plugin decodes the payload after introspection)
    • does not serve banking data (that is banking-api-service)

    OPA depends on Kong to provide trustworthy, well-formed input. If the input is wrong, the decision is wrong.

    Mental Model

  • Keycloak says who the user is.
  • Kong verifies the token is active and extracts claims.
  • Kong sends structured input to OPA.
  • OPA evaluates Rego rules and returns result: true or result: false.
  • Kong enforces that decision at the edge.
  • banking-api-service validates the JWT again before returning banking data.

  • 📚 返回专栏目录

    赞(0)
    未经允许不得转载:171主机测评 » Authn & Authz for Microservice: 08 — OPA (PDP)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址