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

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. The results keep the request order.

getObject, setObject, deleteObject

Objects.

getRelation, setRelation, deleteRelation

Relations.

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

The directory manifest as YAML. The client handles the chunking.

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 that the authorizer calls take:

Factory Caller

none()

Anonymous.

subject(String)

A subject, such as a user key or an 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 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.