Beans

The extension produces three @Singleton beans that share two gRPC channels, one to the authorizer and one to the directory:

@Inject TopazClient topaz;           // authorizer(), directory(), blocking()
@Inject AuthorizerClient authorizer; // the same instance as topaz.authorizer()
@Inject DirectoryClient directory;   // the same instance as topaz.directory()

The channels are Vert.x gRPC client channels on the Quarkus event loop. Generated stubs keep their proto packages (aserto.authorizer.v2, aserto.directory.reader.v3, …​) and come from the quarkus-topaz-grpc artifact.

API

All methods return Mutiny types. Each client also exposes its raw Mutiny stubs for requests the convenience methods do not cover. Packages named runtime are internal.

Method Purpose

AuthorizerClient

isAllowed(identity, policyPath, decision, resource)

One decision as Uni<Boolean>; see isAllowed fails closed.

is(identity, policyPath, decisions, resource) / is(IsRequest)

Several decisions as an IsResponse.

decisionTree(…​), query(…​), compile(…​)

The other authorizer calls.

listPolicies(), getPolicy(id)

The loaded policy modules.

info()

Topaz version information.

stub()

The raw MutinyAuthorizerStub.

DirectoryClient

check(objectType, objectId, relation, subjectType, subjectId)

Whether the subject has the relation or permission on the object.

checks(List<CheckRequest>)

Several checks in one call; results in request order.

getObject, setObject, deleteObject

Objects.

getRelation, setRelation, deleteRelation

Relations.

getManifest(), setManifest(yaml), deleteManifest()

The directory manifest as YAML; chunking is handled for you.

importData(Multi<ImportRequest>)

Streams an import and returns an ImportSummary of counts and errors.

export(ExportRequest)

Streams the directory contents.

reader(), writer(), model(), importer(), exporter()

The raw Mutiny stubs.

Identity contexts

IdentityContexts builds the IdentityContext passed to authorizer calls:

Factory Caller

none()

Anonymous.

subject(String)

A subject (user key, email, …​) that Topaz resolves through the directory.

jwt(String)

A JWT whose sub claim Topaz resolves through the directory. Topaz does not validate the token.

manual(String)

An identity passed to the policy as is, without directory resolution.

For subject and jwt, the identity must exist in the directory as an identity object with an identifier relation to a user; otherwise Topaz answers NOT_FOUND (error E30017). The quick start’s manifest and data show that shape.

Topaz does not validate the token passed to jwt(…​): it does not check the signature, it parses the token and resolves its sub claim through the directory. Authenticate the request in your application (e.g. quarkus-oidc) before forwarding the token; pass the raw token only so Topaz can read its subject.

isAllowed fails closed

isAllowed asks for one named decision and returns true only when the response contains that decision with value true. A decision the policy does not produce is false.

Errors are never turned into a boolean. A gRPC failure, an unknown policy path (Topaz answers INVALID_ARGUMENT), an unresolvable identity or an expired deadline fails the Uni with io.grpc.StatusRuntimeException. Handle it where the decision is enforced, for example by answering 403:

return topaz.authorizer().isAllowed(subject(user), policy, "allowed", Map.of("id", id))
        .onFailure(StatusRuntimeException.class).recoverWithItem(false);

Deadlines

quarkus.topaz.deadline, or quarkus.topaz.authorizer.deadline / quarkus.topaz.directory.deadline, sets a per-call deadline. It starts when a Uni or Multi is subscribed to, so each retry gets a fresh deadline. The deadline bounds the whole call, streams (manifest, import, export) included, as a gRPC withDeadlineAfter deadline. In addition the client fails a call that receives nothing for one deadline period with DEADLINE_EXCEEDED, so a stalled connection cannot hang the convenience methods. For a Multi this guard is an inactivity timeout between items, not a first-item timeout. A refused connection fails immediately with UNAVAILABLE.

The raw stubs (reader(), writer(), model(), importer(), exporter() and the authorizer stub()) carry only the gRPC deadline and are not protected against a stalled handshake: add your own ifNoItem() or use the convenience methods. Timeout failures are emitted on a Mutiny timer thread, not on the caller’s Vert.x context; recover with emitOn if the recovery needs the context.

Without a deadline, calls have no time limit, except that the health check and the Dev UI give up after 10 seconds.

Connection security

quarkus.topaz.plain-text=true gives plaintext channels and wins over any TLS configuration. Otherwise TLS is configured per service through quarkus.topaz.authorizer.tls-configuration-name or quarkus.topaz.directory.tls-configuration-name, falling back to quarkus.topaz.tls-configuration-name, each naming a configuration of the Quarkus TLS registry. With no name set, the registry’s default TLS configuration is used when it defines a trust store or trust-all; otherwise the channel is plaintext. plain-text=false requires TLS: with no named configuration and no usable registry default, the channel uses TLS with the JVM default trust store; it never falls back to plaintext. A TLS configuration name the registry does not know fails startup.

quarkus.topaz.api-key (or the per-service api-key) is sent as authorization: basic <key> on every call. quarkus.topaz.tenant-id adds an aserto-tenant-id header for legacy Aserto deployments; Topaz ignores it.

Blocking facade

topaz.blocking() exposes authorizer() and directory() with the same methods returning plain values. It shares the channels of the reactive client. Each call waits at most the configured deadline plus one second (so the gRPC deadline fires first), or 30 seconds without one. Failures, including an exceeded wait, are thrown as io.grpc.StatusRuntimeException (DEADLINE_EXCEEDED for a timeout). Call it from worker threads only, never from the event loop.

boolean allowed = topaz.blocking().authorizer()
        .isAllowed(IdentityContexts.subject("rick@the-citadel.com"), "todoApp.DELETE.todos.__id", "allowed",
                Map.of("id", "todo-1"));

Building a client outside CDI

TopazClient.builder() works without a running Quarkus application, for example in a test JVM seeding data into a native binary or to reach a second Topaz:

try (TopazClient client = TopazClient.builder()
        .authorizer("localhost", 8282)
        .directory("localhost", 9292)
        .plainText(true)
        .apiKey("...")      // optional
        .build()) {
    boolean owner = client.blocking().directory().check("todo", "todo-1", "owner", "user", "rick@the-citadel.com");
}

The builder also takes tenantId, deadline, tlsConfiguration, extra interceptors, an existing vertx, or ready-made authorizerChannel / directoryChannel. Without a vertx, the client creates its own Vert.x instance. close() closes the channels and the Vert.x instance it created, never ones that were passed in.