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 explicit true, and evaluate() 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,
        ],
    ],
];
KeyDefaultMeaning
urlrequiredOPA's base URL, http:// or https://
tokennoneBearer token, for OPA started with --authentication=token
timeout2.0Seconds for the whole request. Decisions sit on the request path, so keep it short.
connect_timeout1.0Seconds to establish the connection
retries0Extra attempts after a connection failure. OPA's own errors are never retried.
retry_delay_ms50Pause between attempts
strict_builtin_errorsfalseRaise builtin errors (e.g. to_number("abc")) as exceptions instead of leaving the rule undefined
tls.casystem CAsCA file to verify OPA's certificate
tls.verifytrueSet false only for local development
tls.cert, tls.keynoneClient 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.

Previous
Kafka