Zanzibar connector
quarkus-topaz-zanzibar connects quarkus-zanzibar to Topaz.
quarkus-zanzibar offers annotations such as @FGARelation and @FGAPathObject. They guard REST endpoints with
relationship checks. quarkus-zanzibar also defines a RelationshipManager interface that stores and checks the
relationships. This artifact supplies a RelationshipManager backed by the Topaz directory, so Topaz answers the
annotations. It is a candidate for upstreaming as a quarkiverse connector.
|
The artifact enables the quarkus-zanzibar filter, and |
Installation
The artifact brings quarkus-zanzibar and quarkus-topaz with it. quarkus-topaz holds the client and the Dev
Service.
<dependency>
<groupId>io.quarkiverse.topaz</groupId>
<artifactId>quarkus-topaz-zanzibar</artifactId>
<version>0</version>
</dependency>
The quarkus-zanzibar filter needs a REST extension, such as quarkus-rest or RESTEasy.
quarkus.zanzibar.filter.enabled defaults to true. While it is true, the build fails without a REST extension.
The relationship manager
The RelationshipManager bean calls the Topaz directory through the DirectoryClient:
| Method | Directory call | Behaviour |
|---|---|---|
|
Reader |
Answers whether the relationship holds. These all answer |
|
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 |
Ignores a relation that does not exist. Not atomic,
like |
The connector never sets the subject relation. 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. The first source that gives a type wins:
-
The principal name, when
quarkus.zanzibar.extract-user-type-from-nameistrue, which is the default. The separatorquarkus.zanzibar.user-type-separatordefaults to:. When it splits the name into exactly two parts, quarkus-zanzibar reads the name astype:id. The first part is the subject type and the second the subject id. -
The role of the caller, when
quarkus.zanzibar.extract-user-type-from-rolesistrueand the identity has exactly one role. The property defaults totrue. -
@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 then checks the quarkus.zanzibar.filter.unauthenticated-user when that
property is set. Otherwise it 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
If you turn role extraction off without |
quarkus.topaz.zanzibar.default-user-type defaults to user. It 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 relation or a permission such as can_delete, because Topaz Check evaluates both.
Walkthrough
The todo application of the integration-tests module in this repository 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>. The delete endpoint 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 topaz route policy secures the other endpoints of the application. Its application.properties therefore sets
quarkus.zanzibar.filter.deny-unannotated-resource-methods=false. Its embedded users have no roles and no : in
their names. So the application 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 two limits ends the wait. The filter then answers 500. The limits are these:
-
The client deadline
quarkus.topaz.deadline, unset by default. It fails the call withDEADLINE_EXCEEDED. -
quarkus.zanzibar.filter.timeout, 5s by default. It fails the call 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 for a blank user type from quarkus-zanzibar. This
happens on the |
The full reference is in Configuration reference.