A Node.js client for interacting with the Topaz Directory and Authorizer APIs.
The Directory APIs can be used to get, set, or delete object instances, relation instances, and manifests. They can also be used to check whether a user has a permission or relation on an object instance.
type ServiceConfig = {
url?: string;
tenantId?: string;
apiKey?: string;
caFile?: string;
insecure?: boolean;
};
export type DirectoryConfig = ServiceConfig & {
reader?: ServiceConfig;
writer?: ServiceConfig;
importer?: ServiceConfig;
exporter?: ServiceConfig;
model?: ServiceConfig;
};You can initialize a directory client as follows:
import { Directory } from "topaz-node";
const directoryClient = new Directory({
url: 'localhost:9292',
caFile: `${process.env.HOME}/.local/share/topaz/certs/grpc-ca.crt`
});Parameters:
url: Hostname:port of directory service (required)apiKey: API key for directory service (required if using hosted directory)tenantId: Aserto tenant ID (required if using hosted directory)caFile: Path to the directory CA file (optional)insecure: Skip server certificate and domain verification (optional, defaults tofalse)reader: ServiceConfig for the reader client (optional)writer: ServiceConfig for the writer client (optional)importer: ServiceConfig for the importer client (optional)exporter: ServiceConfig for the exporter client (optional)model: ServiceConfig for the model client (optional)
Define a writer client that uses the same credentials but connects to localhost:9393. All other services will have the default configuration:
import { Directory } from "topaz-node";
const directoryClient = new Directory({
url: 'localhost:9292',
tenantId: '1234',
apiKey: 'my-api-key',
writer: {
url: 'localhost:9393'
}
});Get an object instance with the specified type and id.
const user = await directoryClient.object({ objectType: 'user', objectId: 'euang@acmecorp.com' });Handle a specific Directory Error:
import { NotFoundError } from "topaz-node";
try {
await directoryClient.object({
objectType: "user",
objectId: "euang@acmecorp.com",
});
} catch (error) {
if (error instanceof NotFoundError) {
// handle the case where the object was not found
}
throw error;
}Get a relation of a certain type between a subject and an object.
const identity = 'euang@acmecorp.com';
const relation = await directoryClient.relation({
subjectType: 'user',
subjectId: 'euang@acmecorp.com',
relation: 'identifier',
objectType: 'identity',
objectId: identity
});Get all relations of a certain type for a given object.
const relations = await directoryClient.relations({
subjectType: 'subject-type',
relation: 'relation-name',
objectType: 'object-type',
objectId: 'object-id',
});Create an object instance with the specified fields.
const user = await directoryClient.setObject({
object: {
type: "user",
id: "test-object",
properties: {
displayName: "test object"
}
}
});Create a relation with a specified name between two objects.
const relation = await directoryClient.setRelation({
subjectId: 'subjectId',
subjectType: 'subjectType',
relation: 'relationName',
objectType: 'objectType',
objectId: 'objectId',
});Delete an object instance with the specified type and key.
await directoryClient.deleteObject({ objectType: 'user', objectId: 'euang@acmecorp.com' });Delete a relation between two objects.
await directoryClient.deleteRelation({
subjectType: 'subjectType',
subjectId: 'subjectId',
relation: 'relationName',
objectType: 'objectType',
objectId: 'objectId',
});You can evaluate graph queries over the directory to determine whether a subject (e.g., user) has a permission or a relation to an object instance.
Check that a user object with the key euang@acmecorp.com has the read permission in the admin group:
const check = await directoryClient.check({
subjectId: 'euang@acmecorp.com',
subjectType: 'user',
relation: 'read',
objectType: 'group',
objectId: 'admin',
});const identity = 'euang@acmecorp.com';
const relation = await directoryClient.relation(
{
subjectType: 'user',
objectType: 'identity',
objectId: identity,
relation: 'identifier',
subjectId: 'euang@acmecorp.com'
}
);
if (!relation) {
throw new Error(`No relations found for identity ${identity}`)
};
const user = await directoryClient.object(
{ objectId: relation.subjectId, objectType: relation.subjectType }
);You can get, set, or delete the manifest
await directoryClient.getManifest();await directoryClient.setManifest(`
# yaml-language-server: $schema=https://www.topaz.sh/schema/manifest.json
---
### model ###
model:
version: 3
### object type definitions ###
types:
### display_name: User ###
user:
relations:
### display_name: user#manager ###
manager: user
### display_name: Identity ###
identity:
relations:
### display_name: identity#identifier ###
identifier: user
### display_name: Group ###
group:
relations:
### display_name: group#member ###
member: user
permissions:
read: member
`);await directoryClient.deleteManifest();import { ImportMsgCase, ImportOpCode, createImportRequest } from "topaz-node"
const importRequest = createImportRequest([
{
opCode: ImportOpCode.SET,
msg: {
case: ImportMsgCase.OBJECT,
value: {
id: "import-user",
type: "user",
properties: { foo: "bar" },
displayName: "name1",
},
},
},
{
opCode: ImportOpCode.SET,
msg: {
case: ImportMsgCase.OBJECT,
value: {
id: "import-group",
type: "group",
properties: {},
displayName: "name2",
},
},
},
{
opCode: ImportOpCode.SET,
msg: {
case: ImportMsgCase.RELATION,
value: {
subjectId: "import-user",
subjectType: "user",
objectId: "import-group",
objectType: "group",
relation: "member",
},
},
},
]);
const resp = await directoryClient.import(importRequest);
await (readAsyncIterable(resp))const response = await readAsyncIterable(
await directoryClient.export({ options: "DATA" })
)// passing custom headers to a request
const user = await directoryClient.object(
{
objectType: "user",
objectId: "euang@acmecorp.com",
},
{
headers: {
customKey: "customValue",
},
}
);Use Protocol Buffers to serialize data.
import { GetObjectsResponseSchema, toJson } from "topaz-node";
const objects = await directoryClient.objects({objectType: "user"});
const json = toJson(GetObjectsResponseSchema, objects)interface Authorizer {
config: AuthorizerConfig,
};
type AuthorizerConfig = {
authorizerServiceUrl?: string;
tenantId?: string;
authorizerApiKey?: string;
token?: string;
caFile?: string;
insecure?: boolean;
};const authClient = new Authorizer({
authorizerServiceUrl: "localhost:8282",
caFile: `${process.env.HOME}/.local/share/topaz/certs/grpc-ca.crt`
});authorizerServiceUrl: Hostname:port of authorizer service (required)authorizerApiKey: API key for authorizer service (required if using hosted authorizer)tenantId: Aserto tenant ID (required if using hosted authorizer)caFile: Path to the authorizer CA file (optional)insecure: Skip server certificate and domain verification (optional, defaults tofalse)
import {
Authorizer,
identityContext,
policyContext,
policyInstance,
} from "topaz-node";
const authClient = new Authorizer(
{
authorizerServiceUrl: "localhost:8282",
caFile: `${process.env.HOME}/.local/share/topaz/certs/grpc-ca.crt`
},
);
authClient
.Is({
identityContext: {
identity: "rick@the-citadel.com",
type: IdentityType.SUB,
},
policyInstance: {
name: "rebac",
},
policyContext: {
path: "rebac.check",
decisions: ["allowed"],
},
resourceContext: {
object_type: "group",
object_id: "evil_genius",
relation: "member",
},
})// Is
// (method) Authorizer.Is(params: IsRequest, options?: CallOptions): Promise<IsResponse>
await authClient
.Is({
identityContext: {
identity: "rick@the-citadel.com",
type: IdentityType.SUB,
},
policyInstance: {
name: "todo",
},
policyContext: {
path: "todoApp.POST.todos",
decisions: ["allowed"],
},
resourceContext: {
ownerID: "fd1614d3-c39a-4781-b7bd-8b96f5a5100d",
},
})
// Query
// (method) Authorizer.Query(params: QueryRequest, options?: CallOptions): Promise<JsonObject>
await authClient
.Query({
identityContext: {
identity: "rick@the-citadel.com",
type: IdentityType.SUB,
},
policyInstance: {
name: "todo",
},
policyContext: {
path: "todoApp.POST.todos",
decisions: ["allowed"],
},
resourceContext: {
ownerID: "fd1614d3-c39a-4781-b7bd-8b96f5a5100d",
},
query: "x = data",
})
// DecisionTree
// (method) Authorizer.DecisionTree(params: DecisionTreeRequest, options?: CallOptions): Promise<{
// path: Path;
// pathRoot: string;
// }>
await authClient
.DecisionTree({
identityContext: {
identity: "rick@the-citadel.com",
type: IdentityType.SUB,
},
policyInstance: {
name: "todo",
},
policyContext: {
path: "todoApp.POST.todos",
decisions: ["allowed"],
},
resourceContext: {
ownerID: "fd1614d3-c39a-4781-b7bd-8b96f5a5100d",
},
})
// ListPolicies
// (method) Authorizer.ListPolicies(params: PlainMessage<ListPoliciesRequest>, options?: CallOptions): Promise<Module[]>
await authClient.ListPolicies({ policyInstance: { name: "todo" } })await authClient.ListPolicies(
{ policyInstance: { name: "todo" } }
{ headers: { customKey: "customValue" } }
);