As a separate service¶
A policy decision point that runs apart from the app: every service of a company asks the same one, over the network, and the policies are deployed and audited in one place.
How the dependency and the middleware work: Authorization. The engines that run in the process, with no network: Casbin and Cedar.
| Service | Model | For |
|---|---|---|
| Open Policy Agent | Policies in Rego, any model | One policy language for the whole infrastructure (Kubernetes, gateways, services) |
| OpenFGA, SpiceDB | Relationships, as Google's Zanzibar | Permissions that follow relationships: shared documents, organisations, teams |
| Cerbos | Policies in YAML, RBAC and ABAC | Readable policies with tests of their own |
What changes from an engine in the process:
- Latency: each check is a request over the network, a fraction of a millisecond next to the service, against a few microseconds in the process. Ask once per request, not once per item of a list.
- Failures: when the service doesn't answer, the request is refused, never let through. An
enforcer raises
PolicyUnavailable, and the request gets a 503 (Authorization unavailable), a WebSocket 1013 (try again later), counted asunavailablein thefastapi_alcazaba.authzmetric. Each call has a deadline, and a circuit breaker stops asking a service that is down, so that requests don't each wait for it. - Async: a call over the network can't block the event loop.
FastAPIAuthzandAuthzMiddlewareawait anenforcethat is a coroutine function.
Open Policy Agent has an enforcer of its own, OPAEnforcer. For another service, an
enforcer is a class with an async enforce, which raises PolicyUnavailable when the service
doesn't answer:
import httpx2 as httpx
from fastapi_alcazaba.authz import FastAPIAuthz, PolicyUnavailable
from fastapi_alcazaba.outbound import ResilientTransport
class PolicyService:
def __init__(self, url: str) -> None:
# A deadline for each call, and a circuit breaker for the service.
self.client = httpx.AsyncClient(base_url=url, transport=ResilientTransport(timeout=0.5))
async def enforce(self, subject: str, resource: str, action: str) -> bool:
try:
response = await self.client.post(
"/check", json={"subject": subject, "resource": resource, "action": action}
)
except httpx.TransportError as exc:
raise PolicyUnavailable(f"the policy service didn't answer: {exc!r}") from exc
if response.status_code >= 500:
raise PolicyUnavailable(f"the policy service answered {response.status_code}")
response.raise_for_status() # a 4xx is a mistake to fix: a 500
return response.json()["allowed"] is True # only true allows
enforcer = PolicyService("http://policy-service:8080") # enforcer.client.aclose() at shutdown
authz = FastAPIAuthz(enforcer, subject=current_username)