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 |
|---|---|
|
|
|
One decision as |
|
Several decisions as an |
|
The other authorizer calls. |
|
The loaded policy modules. |
|
Topaz version information. |
|
The raw |
|
|
|
Whether the subject has the relation or permission on the object. |
|
Several checks in one call; results in request order. |
|
Objects. |
|
Relations. |
|
The directory manifest as YAML; chunking is handled for you. |
|
Streams an import and returns an |
|
Streams the directory contents. |
|
The raw Mutiny stubs. |
Identity contexts
IdentityContexts builds the IdentityContext passed to authorizer calls:
| Factory | Caller |
|---|---|
|
Anonymous. |
|
A subject (user key, email, …) that Topaz resolves through the directory. |
|
A JWT whose |
|
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.