Enterprise Drivers
OPA Driver
Authorisation decisions and policy management with Open Policy Agent in your Kipchak API.
Introduction
Note: This driver is only available with Kipchak Enterprise.
An Open Policy Agent driver for Kipchak: ask OPA for authorisation decisions, and manage its policies and data, from your Kipchak API.
- Decisions:
allow()for yes/no checks that deny on anything but an explicittrue, andevaluate()for structured results (reasons, obligations, filtered fields). - Policies and data: load Rego modules and push the data they use (roles, grants, tenant settings).
- Queries and partial evaluation: ad-hoc Rego queries, and
compile()for turning policies into data filters. - Production settings: bearer tokens, TLS and client certificates, short timeouts, retries for connection failures only, multiple named OPA servers.
Built on the Kipchak HTTP driver. Talks to OPA's REST API (opa run --server); tested against OPA 1.21.
Installation
composer require kipchak/driver-opa
Initialise it in drivers/drivers.php, after the HTTP driver:
use Kipchak\Driver\OPA\OPA;
OPA::initialise($container);
Configuration
Create config/kipchak.opa.php (the package ships a full sample.config.php):
return [
'connections' => [
'default' => [
'url' => env('OPA_URL', 'http://opa:8181'),
'token' => env('OPA_TOKEN', ''),
'timeout' => 2.0,
'connect_timeout' => 1.0,
],
],
];
| Key | Default | Meaning |
|---|---|---|
url | required | OPA's base URL, http:// or https:// |
token | none | Bearer token, for OPA started with --authentication=token |
timeout | 2.0 | Seconds for the whole request. Decisions sit on the request path, so keep it short. |
connect_timeout | 1.0 | Seconds to establish the connection |
retries | 0 | Extra attempts after a connection failure. OPA's own errors are never retried. |
retry_delay_ms | 50 | Pause between attempts |
strict_builtin_errors | false | Raise builtin errors (e.g. to_number("abc")) as exceptions instead of leaving the rule undefined |
tls.ca | system CAs | CA file to verify OPA's certificate |
tls.verify | true | Set false only for local development |
tls.cert, tls.key | none | Client certificate and key, for OPA started with --authentication=tls |
Add more entries under connections for more OPA servers, and fetch them by name: OPA::get('tenant-policies').
Usage
Decisions
use Kipchak\Driver\OPA\OPA;
$allowed = OPA::get()->allow('httpapi/authz/allow', [
'user' => $userId,
'method' => $request->getMethod(),
'path' => $request->getUri()->getPath(),
]);
allow() is true only when the rule's result is exactly true. An undefined rule (a policy that is not loaded, a typo in the path), false, null, "true" or 1 all deny.
Paths can be written with slashes (httpapi/authz/allow) or as Rego references (httpapi.authz.allow or data.httpapi.authz.allow). Use slashes when a key contains a dot: tenants/acme.com/plan.
For more than yes or no, evaluate the package or a rule that returns an object:
$decision = OPA::get()->evaluate('httpapi/authz', $input);
$decision->defined; // false when OPA has nothing at that path
$decision->result; // e.g. ['allow' => false, 'reasons' => ['unknown user']]
$decision->allowed(); // the same strict check as allow()
$decision->decisionId; // when OPA's decision logging is on
An empty input array is sent as an empty JSON object, so input.user is simply undefined rather than an error.
When OPA is unavailable
allow() and evaluate() throw rather than guess. For authorisation, deny:
use Kipchak\Driver\OPA\Exception\OPAException;
try {
$allowed = OPA::get()->allow('httpapi/authz/allow', $input);
} catch (OPAException $exception) {
$logger->error('Policy check failed', ['error' => $exception->getMessage()]);
$allowed = false;
}
OPAConnectionException (a subclass) means OPA could not be reached. Other OPAExceptions carry OPA's error: $exception->opaCode (invalid_parameter, resource_not_found, unauthorized, internal_error, ...), $exception->httpStatus, and $exception->errors with each error's code, message and location.
Diagnostics
use Kipchak\Driver\OPA\Request\EvaluationOptions;
$decision = OPA::get()->evaluate('httpapi/authz/allow', $input, new EvaluationOptions(
metrics: true, // $decision->metrics
provenance: true, // $decision->provenance (OPA version, bundle revisions)
explain: 'notes', // notes, fails, full or debug; $decision->explanation
));
Leave these off in production paths.
Policies
$opa = OPA::get();
$opa->putPolicy('httpapi/authz.rego', file_get_contents(__DIR__ . '/policies/authz.rego'));
$policy = $opa->getPolicy('httpapi/authz.rego'); // ->id, ->raw, ->ast, ->package()
$all = $opa->listPolicies();
$opa->deletePolicy('httpapi/authz.rego');
A module that does not compile throws an OPAException with code invalid_parameter whose message ends with the file, line and column.
In production most teams ship policies to OPA as bundles instead; these methods suit tests, tooling and small deployments.
Data
$opa->putData('roles', ['alice' => ['admin'], 'bob' => ['viewer']]);
$opa->patchData('roles', [['op' => 'add', 'path' => '/carol', 'value' => ['viewer']]]); // JSON Patch
$roles = $opa->getData('roles')->result;
$opa->deleteData('roles/carol');
Writing to the root of the data tree is refused.
Queries and partial evaluation
$rows = $opa->query('data.roles[user] = roles');
// [['user' => 'alice', 'roles' => ['admin']], ['user' => 'bob', 'roles' => ['viewer']]]
$residual = $opa->compile('data.httpapi.authz.allow == true', ['user' => 'bob'], ['input.path']);
// $residual['queries']: the conditions left on input.path, to translate into a database filter
Health
OPA::get()->health(); // OPA is up
OPA::get()->health(bundles: true); // ...and has activated its bundles
health() never throws.
Source
The source code for this driver is hosted internally.