quarkus-topaz-zanzibar connects quarkus-zanzibar to Topaz.
quarkus-zanzibar offers annotations such as @FGARelation and @FGAPathObject that guard REST endpoints with
relationship checks, and a RelationshipManager interface that stores and checks the relationships. This artifact
supplies the RelationshipManager, backed by the Topaz Directory, so the annotations are answered by Topaz. It is a
candidate for upstreaming as a quarkiverse connector.
|
Adding the artifact enables quarkus-zanzibar’s filter, and |
Installation
The artifact brings quarkus-topaz (the client and the Dev Service) and quarkus-zanzibar with it.
<dependency>
<groupId>org.sitenetsoft</groupId>
<artifactId>quarkus-topaz-zanzibar</artifactId>
<version>0.2.0-SNAPSHOT</version>
</dependency>
quarkus-zanzibar’s filter needs a REST extension: add quarkus-rest (or RESTEasy). While
quarkus.zanzibar.filter.enabled is true (the default), the build fails without one.
The relationship manager
The RelationshipManager bean calls the Topaz Directory through the DirectoryClient:
| Method | Directory call | Behaviour |
|---|---|---|
|
Reader |
Answers whether the relationship holds. A relation or permission that does not exist, an
unknown object type and a |
|
Writer |
Idempotent: setting an existing relation succeeds. Not atomic: the first error stops the list and propagates, and the elements before it stay set. |
|
Writer |
A relation that does not exist is ignored. Not atomic,
like |
The subject relation is never set. Errors propagate as io.grpc.StatusRuntimeException and never turn into true.
User identity
quarkus-zanzibar turns the authenticated caller into the subject of the check. It resolves the subject type from three sources, in this fixed order, and the first one that gives a type wins:
-
The principal name, when
quarkus.zanzibar.extract-user-type-from-nameistrue(the default). A name that the separator (quarkus.zanzibar.user-type-separator,:by default) splits into exactly two parts is read astype:id: the first part is the subject type and the second the subject id. -
The caller’s role, when
quarkus.zanzibar.extract-user-type-from-rolesistrue(the default) and the identity has exactly one role. -
@FGAUserTypeon the resource method or class.
Unless the name was split, the subject id is the whole principal name. When no source gives a type, quarkus-zanzibar
treats the caller as having no user: it checks the quarkus.zanzibar.filter.unauthenticated-user instead when that is
set, and otherwise denies with 403.
Topaz needs a directory object of the resolved type and id, so the caller must exist in the directory; otherwise checks
answer false.
|
Turn both extractions off and put
Turning role extraction off without |
quarkus.topaz.zanzibar.default-user-type (user by default) fills the subject type only when quarkus-zanzibar hands
the connector a blank one. The filter does that on one path: the unauthenticated-user fallback on an endpoint without
@FGAUserType. The property also applies to a Relationship with a blank user type that your code passes to the
RelationshipManager. It does not give an authenticated caller a type.
Relations and permissions
@FGARelation can name a permission such as can_delete as well as a relation, because Topaz Check evaluates both.
Walkthrough
The todo application of this repository’s integration-tests module uses the manifest in
integration-tests/src/main/resources/topaz/manifest.yaml:
types:
user: {}
todo:
relations:
owner: user
permissions:
can_delete: owner
FgaTodoResource guards its endpoints with the annotations. The class sets the subject type, and the owner may read:
@Path("/fga/todos")
@ApplicationScoped
@FGAUserType("user")
public class FgaTodoResource {
@GET
@Path("/{id}")
@FGAPathObject(param = "id", type = "todo")
@FGARelation("owner")
public Map<String, String> get(@PathParam("id") String id) { ... }
@FGAPathObject takes the object id from the path parameter id; the object type is todo. The check is
todo:<id>#owner@user:<principal>. Deleting names the permission:
@DELETE
@Path("/{id}")
@FGAPathObject(param = "id", type = "todo")
@FGARelation("can_delete")
public Response delete(@PathParam("id") String id) { ... }
Sharing is a write through the RelationshipManager. Only an owner may share, so the endpoint requires owner like
get; the method then sets or removes the relation:
@PUT
@Path("/{id}/owners/{user}")
@FGAPathObject(param = "id", type = "todo")
@FGARelation("owner")
public Uni<Response> share(@PathParam("id") String id, @PathParam("user") String user) {
return manager.add(List.of(Relationship.of("todo", id, "owner", "user", user)))
.map(v -> Response.noContent().build());
}
unshare is the same with manager.remove.
|
|
The application’s other endpoints are secured by the topaz route policy, so its application.properties sets
quarkus.zanzibar.filter.deny-unannotated-resource-methods=false. Its embedded users have no roles and no : in their
names, so it leaves both user-type extractions at their defaults.
Errors
-
An anonymous caller, and a caller that is unknown to the Directory, get 403 on a checked endpoint. quarkus-zanzibar denies; it does not challenge with 401.
-
When Topaz refuses the connection, the check fails fast with
UNAVAILABLEand the filter answers 500. -
When Topaz accepts the connection and never answers, the shorter of the client deadline (
quarkus.topaz.deadline, unset by default) andquarkus.zanzibar.filter.timeout(5s by default) ends the wait first, and the filter answers 500. The client deadline fails the call withDEADLINE_EXCEEDED; the filter timeout fails it with aTimeoutException.
Configuration
The connector has one property. All other settings, quarkus.zanzibar.*, belong to quarkus-zanzibar; see its
documentation.
| Property | Meaning |
|---|---|
|
The subject type used when quarkus-zanzibar hands over a blank user type:
on the |
The full reference is in Configuration reference.