Client
Beans
The extension produces three @Singleton beans. They 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. The generated stubs come from the
quarkus-topaz-grpc artifact. They keep their proto packages, such as aserto.authorizer.v2 and
aserto.directory.reader.v3.
API
All methods return Mutiny types. Each client also exposes its raw Mutiny stubs. Use them for requests that 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. The results keep the request order. |
|
Objects. |
|
Relations. |
|
The directory manifest as YAML. The client handles the chunking. |
|
Streams an import and returns an |
|
Streams the directory contents. |
|
The raw Mutiny stubs. |
Identity contexts
IdentityContexts builds the IdentityContext that the authorizer calls take:
| Factory | Caller |
|---|---|
|
Anonymous. |
|
A subject, such as a user key or an email, that Topaz resolves through the directory. |
|
A JWT whose |
|
An identity that Topaz passes to the policy as is, without directory resolution. |
For subject and jwt, the identity must exist in the directory as an identity object. That object needs an
identifier relation to a user. Otherwise Topaz answers NOT_FOUND with error E30017. The manifest and data of
the quick start show that shape.
Topaz does not validate the token that you pass 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 before you forward
the token, for example with quarkus-oidc. Pass the raw token only so that Topaz can read its subject.
isAllowed fails closed
isAllowed asks for one named decision. It returns true only when the response contains that decision with value
true. A decision that the policy does not produce counts as false.
isAllowed never turns an error into a boolean. Each of these errors fails the Uni with
io.grpc.StatusRuntimeException:
-
A gRPC failure.
-
An unknown policy path. Topaz answers it with
INVALID_ARGUMENT. -
An identity that Topaz cannot resolve.
-
An expired deadline.
Handle the exception where you enforce the decision, 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 sets a per-call deadline for both services. quarkus.topaz.authorizer.deadline and
quarkus.topaz.directory.deadline override it for one service. The deadline starts when a subscriber subscribes to
the Uni or Multi, so each retry gets a fresh deadline. The client applies it as a gRPC withDeadlineAfter
deadline that bounds the whole call. This includes the streams of the manifest, import and export calls.
The client also guards against a stalled connection. It fails a call with DEADLINE_EXCEEDED when the call receives
nothing for one deadline period. This guard keeps a stalled connection from hanging the convenience methods. For a
Multi, the guard is an inactivity timeout between items, not a first-item timeout. A refused connection fails at
once with UNAVAILABLE.
The raw stubs carry only the gRPC deadline. These stubs are reader(), writer(), model(), importer(),
exporter() and the authorizer stub(). The client does not guard them against a stalled handshake. Add your own
ifNoItem(), or use the convenience methods.
Mutiny emits a timeout failure on a Mutiny timer thread, not on the Vert.x context of the calling thread. Recover with
emitOn if the recovery needs that context.
Without a deadline, calls have no time limit. The health check and the Dev UI still give up after 10 seconds.
Connection security
quarkus.topaz.plain-text=true gives plaintext channels and wins over any TLS configuration.
Otherwise each service takes its TLS configuration by name from the Quarkus TLS registry. The client reads the name
from quarkus.topaz.authorizer.tls-configuration-name or quarkus.topaz.directory.tls-configuration-name. Without a
per-service name, it falls back to quarkus.topaz.tls-configuration-name.
With no name set, the client looks at the default TLS configuration of the registry. It uses that configuration when it defines a trust store or trust-all. Otherwise the channel is plaintext.
plain-text=false requires TLS. Without a named configuration or a usable registry default, the channel uses TLS
with the default JVM trust store. It never falls back to plaintext. A TLS configuration name that the registry
does not know fails startup.
The client sends quarkus.topaz.api-key as authorization: basic <key> on every call. A per-service api-key
overrides it for one service. quarkus.topaz.tenant-id adds an aserto-tenant-id header for legacy Aserto
deployments. Topaz ignores this header.
Blocking facade
topaz.blocking() exposes authorizer() and directory(). They have the same methods as the reactive clients and
return plain values. The facade shares the channels of the reactive client. Call it from worker threads only, never
from the event loop.
Each call waits at most the configured deadline plus one second. The extra second lets the gRPC deadline fire first.
Without a deadline, a call waits at most 30 seconds. The facade throws every failure as
io.grpc.StatusRuntimeException, an exceeded wait included. A timeout carries the status DEADLINE_EXCEEDED.
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. A test JVM can use it to seed data into a native
binary. An application can use it 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 and extra interceptors. It also takes an existing
vertx, or a ready-made authorizerChannel and directoryChannel. Without a vertx, the client creates its own
Vert.x instance. close() closes the channels and the Vert.x instance that the client created. It never closes the
ones you passed in.