diff --git a/docs/software-development-kits/javascript/express.mdx b/docs/software-development-kits/javascript/express.mdx index d130301..ac84469 100644 --- a/docs/software-development-kits/javascript/express.mdx +++ b/docs/software-development-kits/javascript/express.mdx @@ -14,16 +14,16 @@ package. This SDK is open source and can be found on [GitHub](https://github.com/aserto-dev/aserto-node). -This package provides four capabilities: +This package provides multiple capabilities: -1. `jwtAuthz`: middleware that sits on a route, and validates a request to authorize access to that route. -2. `displayStateMap`: middleware that adds an endpoint for returning the display state map for a service, based on its authorization policy. -3. `is`: a function that can be called to make a decision about a user's access to a resource based on a policy. -4. `ds`: an object containing the `object` and `relation` functions, which can be called to retrieve an object or relation, respectively, from the directory. - -The first three capabilities call out to an authorizer service, which must be configured as part of the `options` map passed in. - -The fourth calls out to a directory service. +1. `Middleware` - Provides 2 implementations: `Authz` and `Check` middlewares that sits on a route, and validates a request to authorize access to that route. +2. `Authorizer` - Authorizer Client that provides functions to facilitate comunication with an Authorizer v2 service. +3. `DirectoryServiceV3` - Directory Client that provides functions to facilitate comunication with an Directory v3 service. +4. `DirectoryServiceV2` - Directory Client that provides functions to facilitate comunication with an Directory v2 service. +5. `displayStateMap`: middleware that adds an endpoint for returning the display state map for a service, based on its authorization policy. +6. `is`: a function that can be called to make a decision about a user's access to a resource based on a policy. +7. `jwtAuthz`(deprecated(replaced by `Middleware.Authz()`)): middleware that sits on a route, and validates a request to authorize access to that route. +8. `ds`(deprecated): an object containing the `object` and `relation` functions, which can be called to retrieve an object or relation, respectively, from the directory. ## Installation @@ -39,234 +39,443 @@ Using [yarn](https://yarnpkg.com): yarn add @aserto/aserto-node ``` -:::note -`express@^4.0.0` is a peer dependency. Make sure it is installed in your project. -::: - ## Usage -> Note: the `authorizerServiceUrl` option that is used throughout is no longer a URL, but the option name is retained for backward-compatibility. It is now expected to be a hostname that exposes a gRPC binding. Any "https://" prefix is stripped out of the value provided. +## Authorizer -### jwtAuthz middleware +### Authorizer Client +```ts +interface Authorizer { + config: AuthorizerConfig, + channelCredentials: ChannelCredentials = credentials.createSsl() +}; -`jwtAuthz` is an Express-compatible middleware that you can place in the dispatch pipeline of a route. +type AuthorizerConfig = { + authorizerServiceUrl?: string; + tenantId?: string; + authorizerApiKey?: string; + authorizerCertFile?: string; +}; +``` -You can use the jwtAuthz function together with [express-jwt](https://github.com/auth0/express-jwt) to both validate a JWT and make sure it has the correct permissions to call an endpoint. -```javascript -const jwt = require('express-jwt'); -const { jwtAuthz } = require('aserto-node'); +### Topaz +```ts +import { getSSLCredentials } from "@aserto/aserto-node"; -const options = { - authorizerServiceUrl: 'localhost:8282', // required - must pass a valid host:port - policyRoot: 'mycars' // required - must be a string representing the policy root (the first component of the policy module name) -}; +const sslCredentials = getSSLCredentials(`${process.env.HOME}/.config/topaz/certs/grpc-ca.crt`) -app.get('/users/:id', - jwt({ secret: 'shared_secret' }), - jwtAuthz(options), - function(req, res) { ... }); +const authClient = new Authorizer({ + authorizerServiceUrl: "localhost:8282", +}, sslCredentials); ``` -By default, `jwtAuthz` derives the policy file name and resource key from the Express route path. To override this behavior, two optional parameters are available. - -#### arguments +#### Example: +```ts +import { + Authorizer, + getSSLCredentials, + identityContext, + policyContext, + policyInstance, +} from "@aserto/aserto-node"; + +const authClient = new Authorizer( + { + authorizerServiceUrl: "localhost:8282", + }, + getSSLCredentials(`${process.env.HOME}/.config/topaz/certs/grpc-ca.crt`) +); -`jwtAuthz(options[, packageName[, resourceMap]])`: +authClient + .Is({ + identityContext: identityContext( + "rick@the-citadel.com", + "IDENTITY_TYPE_SUB" + ), + policyInstance: policyInstance("rebac", "rebac"), + policyContext: policyContext("rebac.check", ["allowed"]), + resourceContext: { + object_type: "group", + object_id: "evil_genius", + relation: "member", + }, + }) +``` -- `options`: a javascript map containing at least `{ authorizerServiceUrl, policyRoot, authorizerCertCAFile }` -- `packageName`: a string representing the policy package name (optional) -- `resourceMap`: a map of key/value pairs to use as the resource context for evaluation (optional) +### Methods +```ts +// Is +await authClient + .Is({ + identityContext: identityContext( + "morty@the-citadel.com", + "IDENTITY_TYPE_SUB" + ), + policyInstance: policyInstance("todo", "todo"), + policyContext: policyContext("todoApp.POST.todos", ["allowed"]), + resourceContext: { + ownerID: "fd1614d3-c39a-4781-b7bd-8b96f5a5100d", + }, + }) -#### options argument +// Query +await authClient + .Is({ + identityContext: identityContext( + "morty@the-citadel.com", + "IDENTITY_TYPE_SUB" + ), + policyInstance: policyInstance("todo", "todo"), + policyContext: policyContext("todoApp.POST.todos", ["allowed"]), + resourceContext: { + ownerID: "fd1614d3-c39a-4781-b7bd-8b96f5a5100d", + }, + query: "x = data", + }) -- `authorizerServiceUrl`: hostname:port of authorizer service (_required_) -- `policyRoot`: Policy root (_required_) -- `authorizerCertCAFile`: location on the filesystem of the CA certificate that signed the Topaz authorizer self-signed certificate. See the "Certificates" section for more information. -- `disableTlsValidation`: ignore TLS certificate validation when creating a TLS connection to the authorizer. Defaults to false. -- `failWithError`: When set to `true`, will forward errors to `next` instead of ending the response directly. -- `useAuthorizationHeader`: When set to `true`, will forward the Authorization header to the authorizer. The authorizer will crack open the JWT and use that as the identity context. Defaults to `true`. -- `identityHeader`: the name of the header from which to extract the `identity` field to pass into the authorize call. This only happens if `useAuthorizationHeader` is false. Defaults to 'identity'. -- `customUserKey`: The property name to check for the subject key. By default, permissions are checked against `req.user`, but you can change it to be `req.myCustomUserKey` with this option. Defaults to `user`. -- `customSubjectKey`: The property name to check for the subject. By default, permissions are checked against `user.sub`, but you can change it to be `user.myCustomSubjectKey` with this option. Defaults to `sub`. -#### packageName argument +// DecisionTree +await authClient + .DecisionTree({ + identityContext: identityContext( + "morty@the-citadel.com", + "IDENTITY_TYPE_SUB" + ), + policyInstance: policyInstance("todo", "todo"), + policyContext: policyContext("todoApp.POST.todos", ["allowed"]), + resourceContext: { + ownerID: "fd1614d3-c39a-4781-b7bd-8b96f5a5100d", + }, + }) -By convention, Topaz policy package names are of the form `policyRoot.METHOD.path`. By default, the package name will be inferred from the policy name, HTTP method, and route path: -- `GET /api/users` --> `policyRoot.GET.api.users` -- `POST /api/users/:id` --> `policyRoot.POST.api.users.__id` +// ListPolicies +await authClient + .ListPolicies({ policyInstance: policyInstance("todo", "todo") }) +``` -Passing in the `packageName` parameter into the `jwtAuthz()` function will override this behavior. +### Middleware -#### resourceMap argument +:::note +`express@^4.0.0` is a peer dependency for the Middleware. Make sure it is installed in your project. +::: -By default, the resource map will be req.params. For example, if the route path is `/api/users/:id`, the resource will be `{ 'id': 'value-of-id' }`. +When authorization middleware is configured and attached to a server, it examines incoming requests, extracts authorization parameters like the caller's identity, calls the Aserto authorizers, and rejects messages if their access is denied. + +`failWithError`: When set to `true`, will forward errors to `next` instead of ending the response directly. + +```ts +interface Middleware { + client: Authorizer; + policy: Policy; + resourceMapper?: ResourceMapper; + identityMapper?: IdentityMapper; + policyMapper?: PolicyMapper; + failWithError?: boolean; +} + +type Policy = { + root: string; + name?: string; + instanceLabel?: string; + decision?: string; + path?: string; +}; -Passing in the `resourceMap` parameter into the `jwtAuthz()` function will override this behavior. +type CheckOptions = { + object?: { + id?: string; + type?: string; + idMapper?: StringMapper; + mapper?: ObjectMapper; + }; + relation?: { + name?: string; + mapper?: StringMapper; + }; + subject?: { + type?: string; + mapper?: IdentityMapper; + }; +}; -### displayStateMap middleware +type ResourceMapper = + | ResourceContext + | ((req?: Request) => Promise); -Use the displayStateMap middleware to set up an endpoint that returns the display state map to a caller. The endpoint is named `__displaystatemap` by default, but can be overridden in `options`. +type IdentityMapper = (req?: Request) => Promise; +type PolicyMapper = (req?: Request) => Promise; +``` -```javascript -const { displayStateMap } = require('aserto-node'); +#### Methods -const options = { - authorizerServiceUrl: 'localhost:8282', // required - must pass a valid host:port - policyRoot: 'policy' // required - must be a string representing the policy root (the first component of the policy module name) -}; -app.use(displayStateMap(options)); +```ts +function Authz() +function Check(options: CheckOptions) ``` -#### arguments +#### Examples +```ts +const app: express.Application = express(); + + +// Standard REST +const restMw = new Middleware({ + client: client, + policy: { + name: 'todo', + instanceLabel: 'todo', + root: 'todoApp', + }, + resourceMapper: async (req: express.Request) => { + if (!req.params?.id) { + return {}; + } -`displayStateMap(options)` + const todo = await store.get(req.params.id); + return { ownerID: todo.OwnerID }; + }, +}) -#### options argument +app.get("/todos", checkJwt, restMw.Authz(), server.list.bind(server)); +app.post("/todos", checkJwt, restMw.Authz(), server.create.bind(server)); +app.put("/todos/:id", checkJwt, restMw.Authz(), server.update.bind(server)); -- `authorizerServiceUrl`: hostname:port of authorizer service (_required_) -- `policyRoot`: Policy root (_required_) -- `authorizerCertCAFile`: location on the filesystem of the CA certificate that signed the Topaz authorizer self-signed certificate. See the "Certificates" section for more information. -- `disableTlsValidation`: ignore TLS certificate validation when creating a TLS connection to the authorizer. Defaults to false. -- `endpointPath`: display state map endpoint path, defaults to `/__displaystatemap`. -- `failWithError`: When set to `true`, will forward errors to `next` instead of ending the response directly. Defaults to `false`. -- `useAuthorizationHeader`: When set to `true`, will forward the Authorization header to the authorizer. The authorizer will crack open the JWT and use that as the identity context. Defaults to `true`. -- `identityHeader`: the name of the header from which to extract the `identity` field to pass into the displayStateMap call. This only happens if `useAuthorizationHeader` is false. Defaults to 'identity'. -- `customUserKey`: The property name to check for the subject key. By default, permissions are checked against `req.user`, but you can change it to be `req.myCustomUserKey` with this option. Defaults to `user`. -- `customSubjectKey`: The property name to check for the subject. By default, permissions are checked against `user.sub`, but you can change it to be `user.myCustomSubjectKey` with this option. Defaults to `sub`. -### 'is' function +// Check +const rebacMw = new Middleware({ + client: authClient, + policy: { + name: 'policy-rebac', + instanceLabel: 'policy-rebac', + root: 'rebac', + } +}) + +// Only users that are in the `evil_genius` group are allowed to delete todos. +app.delete("/todos/:id", checkJwt, rebacMw.Check({ + object: { + type: "group", + id: "evil_genius" + }, + relation: { + name: "member", + } +}) +``` -While `jwtAuthz` is meant to be used as dispatch middleware for a route, `is` provides an explicit mechanism for calling the Topaz authorizer. +#### Mappers -Use the `is` function to call the authorizer with a `decision`, policy, and resource, and get a boolean `true` or `false` response. The `decision` is a named value in the policy: the string `allowed` is used by convention. Examples: `is('allowed')`, `is('enabled')`, `is('visible')`, etc. +##### Identity +To determine the identity of the user, the middleware can be configured to use a JWT token or a claim using the `IdentityMapper`. -```javascript -const { is } = require('aserto-node'); +```ts +// use the identity type sub +import { SubIdentityMapper } from "@aserto/aserto-node"; -const options = { - authorizerServiceUrl: 'localhost:8282', // required - must pass a valid host:port - policyRoot: 'policy' // required - must be a string representing the policy root (the first component of the policy module name) -}; +const restMw = new Middleware({ + client: authClient, + policy: policy, + identityMapper: SubIdentityMapper, +}) -app.get('/users/:id', async function(req, res) { - try { - const allowed = await is('allowed', req, options); - if (allowed) { - ... - } else { - res.status(403).send("Unauthorized"); - } - } catch (e) { - res.status(500).send(e.message); - } -}); +// use the jwt type sub from a custom header +import { JWTIdentityMapper } from "@aserto/aserto-node"; + +const restMw = new Middleware({ + client: authClient, + policy: policy, + identityMapper: JWTIdentityMapper("my-header");, +}) ``` -#### arguments +```ts +// use the manual identity type +import { ManualIdentityMapper } from "@aserto/aserto-node"; -`is(decision, req, options[, packageName[, resourceMap]])`: +const restMw = new Middleware({ + client: authClient, + policy: policy, + identityMapper: ManualIdentityMapper("my-identity");, +}) +``` -- `decision`: a string representing the name of the decision - typically `allowed` (_required_) -- `req`: Express request object (_required_) -- `options`: a javascript map containing at least `{ authorizerServiceUrl, policyRoot, authorizerCertCAFile }` (_required_) -- `packageName`: a string representing the package name for the the policy (optional) -- `resourceMap`: a map of key/value pairs to use as the resource context for evaluation (optional) +The whole identity resolution can be overwritten by providing a custom function. +```ts +import { identityContext } from "@aserto/aserto-node"; + +const restMw = new Middleware({ + client: authClient, + policy: policy, + identityMapper: async () => { + return identityContext('test', 'IDENTITY_TYPE_SUB') + }, +}) +``` -#### decision argument +### Policy -This is simply a string that is correlates to a decision referenced in the policy: for example, `allowed`, `enabled`, etc. +The authorization policy's ID and the decision to be evaluated are specified when creating authorization Middleware, but the policy path is often derived from the URL or method being called. -#### req argument +By default, the policy path is derived from the URL path. -The Express request object. +To provide custom logic, use a PolicyMapper. For example: -#### options argument +```ts +import { policyContext } from "@aserto/aserto-node"; -- `authorizerServiceUrl`: hostname:port of authorizer service (_required_) -- `policyRoot`: Policy root (_required_) -- `authorizerCertCAFile`: location on the filesystem of the CA certificate that signed the Topaz authorizer self-signed certificate. See the "Certificates" section for more information. -- `disableTlsValidation`: ignore TLS certificate validation when creating a TLS connection to the authorizer. Defaults to false. -- `useAuthorizationHeader`: When set to `true`, will forward the Authorization header to the authorizer. The authorizer will crack open the JWT and use that as the identity context. Defaults to `true`. -- `identityHeader`: the name of the header from which to extract the `identity` field to pass into the `authorize` call. This only happens if `useAuthorizationHeader` is false. Defaults to 'identity'. -- `customUserKey`: The property name to check for the subject key. By default, permissions are checked against `req.user`, but you can change it to be `req.myCustomUserKey` with this option. Defaults to `user`. -- `customSubjectKey`: The property name to check for the subject. By default, permissions are checked against `user.sub`, but you can change it to be `user.myCustomSubjectKey` with this option. Defaults to `sub`. +const restMw = new Middleware({ + client: authClient, + policy: policy, + policyMapper: async () => { + return policyContext('path', ['decision']) + } +}) +``` -#### packageName argument +#### Resource +A resource can be any structured data that the authorization policy uses to evaluate decisions. By default, the request params are included in the ResourceContext. -By default, `is` will follow the same heuristic behavior as `jwtAuthz` - it will infer the packge name from the policy name, HTTP method, and route path. If provided, the `packageName` argument will override this and specify a policy package to use. +This behavior can be overwritten by providing a custom function: -By convention, Topaz Rego policies are named in the form `policyRoot.METHOD.path`. Following the node.js idiom, you can also pass it in as `policyRoot/METHOD/path`, and the path can contain the Express parameter syntax. +```ts +const restMw = new Middleware({ + client: authClient, + policy: policy, + resourceMapper: async () => { + return { customKey: "customValue" }; + }, +}) +``` -For example, passing in `policyRoot/GET/api/users/:id` will resolve to a policy called `policyRoot.GET.api.users.__id`. +#### Mappers -#### resourceMap argument +##### Resource -By default, `is` follows the same behavior as `jwtAuthz` in that resource map will be `req.params`. For example, if the route path is `/api/users/:id`, the resource will be `{ 'id': 'value-of-id' }`. +```ts +// provides a custom resource context, +type ResourceMapper = + | ResourceContext + | ((req?: Request) => Promise); -Passing in the `resourceMap` parameter into the `is()` function will override this behavior. +// examples +async (req: Request) => { return { customKey: req.params.id } }; +// or just a plain resource context +{ customKey: "customValue" } +``` -## Certificates +##### Identity -The Topaz [authorizer](/docs/authorizer-guide/overview) exposes SSL-only endpoints. In order for a Node.js policy to properly communicate with the authorizer, TLS certificates must be verified. +```ts +type IdentityMapper = (req?: Request) => Promise; -In order for the `aserto-node` package to perform the TLS handshake, it needs to verify the TLS certificate of the Topaz authorizer using the certificate of the CA that signed it - which was placed in `$HOME/.config/topaz/certs/grpc-ca.crt`. Therefore, in order for this middleware to work successfully, either the `authorizerCertCAFile` must be set to the correct path for the CA cert file, or the `disableTlsValidation` flag must be set to `true`. +// You can also use the built-in policyContext function to create a identity context and pass it as the mapper response +identityContext = (value: string, type: keyof IdentityTypeMap) -Furthermore, when packaging a policy for deployment (e.g. in a Docker container) which uses `aserto-node` to communicate with an authorizer that has a self-signed TLS certificate, you must copy this CA certificate into the container as part of the Docker build (typically performed in the Dockerfile). When you do that, you'll need to override the `authorizerCertCAFile` option that is passed into any of the API calls defined above with the location of this cert file. +IdentityTypeMap { + IDENTITY_TYPE_UNKNOWN: 0; + IDENTITY_TYPE_NONE: 1; + IDENTITY_TYPE_SUB: 2; + IDENTITY_TYPE_JWT: 3; +} -Alternately, to ignore TLS certificate validation when creating a TLS connection to the authorizer, you can set the `disableTlsValidation` option to `true` and avoid TLS certificate validation. This option is **not recommended for production**. +// example +identityContext("morty@the-citadel.com", "IDENTITY_TYPE_SUB") +``` + +##### Policy + +```ts +type PolicyMapper = (req?: Request) => Promise; + + +// You can also use the built-in policyContext function to create a policy context and pass it as the mapper response +policyContext = (policyPath: string, decisionsList: Array = ["allowed"]) + +// Example +policyContext("todoApp.POST.todos", ["allowed"]) +``` ## Directory -The Directory APIs can be used to get or set object instances and relation instances. They can also be used to check whether a user has a permission or relation on an object instance. +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. ### Directory Client +```ts +type ServiceConfig = { + url?: string; + tenantId?: string; + apiKey?: string; + caFile?: string; + rejectUnauthorized?: boolean; +}; + +type DirectoryV3Config = ServiceConfig & { + reader?: ServiceConfig; + writer?: ServiceConfig; + importer?: ServiceConfig; + exporter?: ServiceConfig; + model?: ServiceConfig; +}; +``` + You can initialize a directory client as follows: ```typescript -import { ds } from "@aserto/aserto-node"; +import { DirectoryServiceV3 } from "@aserto/aserto-node"; -const directoryClient = ds({ +const directoryClient = DirectoryServiceV3({ url: 'localhost:9292', - tenantId: '1234', - apiKey: 'my-api-key', + caFile: `${process.env.HOME}/.config/topaz/certs/grpc-ca.crt` }); - `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) - `rejectUnauthorized`: reject clients with invalid certificates. Defaults to `true`. +- `reader`: ServiceConfig for the reader client(optional) +- `writer`: ServiceConfig for the writer client(option) +- `importer`: ServiceConfig for the importer client(option) +- `exporter`: ServiceConfig for the exporter client(option) +- `model`: ServiceConfig for the model client(option) +``` + +#### Example +Define a writer client that uses the same credentials but connects to localhost:9393. All other services will have the default configuration +```ts +import { DirectoryServiceV3 } from "@aserto/aserto-node"; + +const directoryClient = DirectoryServiceV3({ + url: 'localhost:9292', + writer: { + url: 'localhost:9393' + } +}); ``` ### Getting objects and relations #### 'object' function -`object({ type: "type-name", key: "object-key" })`: +`object({ objectType: "type-name", objectId: "object-id" })`: -Get an object instance with the type `type-name` and the key `object-key`. For example: +Get an object instance with the type `type-name` and the id `object-id`. For example: ```typescript -const user = await directoryClient.object({ type: 'user', key: 'euang@acmecorp.com' }); +const user = await directoryClient.object({ objectType: 'user', objectId: 'euang@acmecorp.com' }); ``` #### 'relation' function ```typescript relation({ - subject: { - type: 'subject-type', - }, - object: { - type: 'object-type', - key: 'object-key' - }, - relation: { - name: 'relation-name', - objectType: 'object-type' - } + subjectType: 'subject-type', + objectType: 'object-type', + objectId: 'object-id', + relation: 'relation-name', }) ``` @@ -274,21 +483,13 @@ Get an array of relations of a certain type for an object instance. For example: ```typescript const identity = 'euang@acmecorp.com'; -const relations = await directoryClient.relation( - { - subject: { - type: 'user', - }, - object: { - type: 'identity', - key: identity - }, - relation: { - name: 'identifier', - objectType: 'identity' - } - } -); +const relations = await directoryClient.relation({ + subjectType: 'user', + objectType: 'identity', + objectId: identity + relation: 'identifier', + objectType: 'identity' +}); ``` ### Setting objects and relations @@ -300,11 +501,11 @@ const relations = await directoryClient.relation( Create an object instance with the specified fields. For example: ```typescript -user = directoryClient.setObject( +const user = await directoryClient.setObject( { object: { type: "user", - key: "test-object", + id: "test-object", properties: { displayName: "test object" } @@ -315,60 +516,45 @@ user = directoryClient.setObject( #### 'setRelation' function -`setRelation({ subject: ObjectIdentifier, relation: String, object: ObjectIdentifier })`: +`setRelation({ relation: Relation })`: Create a relation with a specified name between two objects. For example: ```typescript -const relation = await directoryClient.setRelation( - { - subject: { - key: 'subjectKey', - type: 'subjectType', - }, - relation: 'relationName', - object: { - type: 'objectType', - key: 'objectKey', - }, - } -); +const relation = await directoryClient.setRelation({ + subjectId: 'subjectId', + subjectType: 'subjectType', + relation: 'relationName', + objectType: 'objectType', + objectId: 'objectId', +}); ``` #### 'deleteObject' function -`deleteObject({ type: "type-name", key: "object-key" })`: +`deleteObject({ objectType: "type-name", objectId: "object-id", withRelations: false })`: Deletes an object instance with the specified type and key. For example: ```typescript -await directoryClient.deleteObject({ type: 'user', key: 'euang@acmecorp.com' }); +await directoryClient.deleteObject({ objectType: 'user', objectId: 'euang@acmecorp.com' }); ``` #### 'deleteRelation' function -`deleteRelation({ subject: ObjectIdentifier, relation: RelationIdentifier, object: ObjectIdentifier })`: +`deleteRelation({ objectType: string, objectId: string, relation: string, subjectType: string, subjectId: string, subjectRelation: string })`: Delete a relation: ```typescript -await directoryClient.deleteRelation( - { - subject: { - key: 'subjectKey', - type: 'subjectType', - }, - relation: { - name: 'relationName', - object_type: 'objectType', - }, - object: { - type: 'objectType', - key: 'objectKey', - }, - } -); +await directoryClient.deleteRelation({ + subjectType: 'subjectType', + subjectId: 'subjectId', + relation: 'relationName', + objectType: 'objectType', + objectId: 'objectId', +}); ``` ### Checking permissions and relations @@ -377,51 +563,34 @@ You can evaluate graph queries over the directory, to determine whether a subjec #### 'checkPermission' function -`checkPermission({ subject: ObjectIdentifier, permission: PermissionIdentifier, object: ObjectIdentifier })`: +`checkPermission({ objectType: string, objectId: string, permission: string, subjectType: string, subjectId: string, trace: boolean })`: Check that an `user` object with the key `euang@acmecorp.com` has the `read` permission in the `admin` group: ```typescript -const check = await directoryClient.checkPermission( - { - subject: { - key: 'euang@acmecorp.com', - type: 'user', - }, - permission: { - name: 'read', - }, - object: { - type: 'group', - key: 'admin', - }, - } -); +const check = await directoryClient.checkPermission({ + subjectId: 'euang@acmecorp.com', + subjectType: 'user', + permission: 'read', + objectType: 'group', + objectId: 'admin', +}); ``` #### 'checkRelation' function -`checkPermission({ subject: ObjectIdentifier, permission: PermissionIdentifier, object: ObjectIdentifier })`: +`checkRelation({ objectType: string, objectId: string, relation: string, subjectType: string, subjectId: string, trace: boolean })`: Check that `euang@acmecorp.com` has an `identifier` relation to an object with key `euang@acmecorp.com` and type `identity`: ```typescript -const check = directoryClient.checkPermission( - { - subject: { - key: 'euang@acmecorp.com', - type: 'user', - }, - relation: { - name: "identifier", - object_type: "identity" - }, - object: { - type: 'identity', - key: 'euang@acmecorp.com', - }, - } -); +const check = directoryClient.checkRelation({ + subjectId: 'euang@acmecorp.com', + subjectType: 'user', + name: 'identifier', + objectType: 'identity', + objectId: 'euang@acmecorp.com', +}); ``` ### Example @@ -430,23 +599,246 @@ const check = directoryClient.checkPermission( const identity = 'euang@acmecorp.com'; const relation = await directoryClient.relation( { - subject: { - type: 'user', - }, - object: { - type: 'identity', - key: identity - }, - relation: { - name: 'identifier', - objectType: 'identity' - } + subjectType: 'user', + objectType: 'identity', + objectId: identity, + relation: 'identifier', + subjectId: 'euang@acmecorp.com' } ); -if (!relation || relation.length === 0) { - throw new Error(`No relations found for identity ${identity}`, ) +if (!relation) { + throw new Error(`No relations found for identity ${identity}`) +}; + +const user = await directoryClient.object( + { objectId: relation.subjectId, objectType: relation.subjectType } +); +``` + +### Manifest + +You can get, set, or delete the manifest + +#### 'getManifest' function + +```ts +await directoryClient.getManifest(); +``` + +#### 'setManifest' function + +```ts +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 +`); +``` + +#### 'deleteManifest' function + +```ts +await directoryClient.deleteManifest(); +``` + +### Import + +```ts +const objectCase = "object" as const; +const relationCase = "relation" as const; + +const importRequest = createAsyncIterable([ + { + msg: { + case: objectCase, + value: { + id: "import-user", + type: "user", + properties: {}, + displayName: "name1", + }, + }, + }, + { + msg: { + case: objectCase, + value: { + id: "import-group", + type: "group", + properties: {}, + displayName: "name2", + }, + }, + }, + { + msg: { + case: relationCase, + value: { + subjectId: "import-user", + subjectType: "user", + objectId: "import-group", + objectType: "group", + relation: "member", + }, + }, + }, +]); + +const resp = await directoryClient.import(importRequest); +await (readAsyncIterable(resp)) +``` + +### Export + +```ts +const response = await readAsyncIterable( + await directoryClient.export({ options: "all" }) +) +``` + +### displayStateMap middleware + +Use the displayStateMap middleware to set up an endpoint that returns the display state map to a caller. The endpoint is named `__displaystatemap` by default, but can be overridden in `options`. + +```javascript +const { displayStateMap } = require('@aserto/aserto-node'); + +const options = { + authorizerServiceUrl: 'localhost:8282', // required - must pass a valid host:port + policyRoot: 'policy' // required - must be a string representing the policy root (the first component of the policy module name) +}; +app.use(displayStateMap(options)); +``` + +#### arguments + +`displayStateMap(options)` + +#### options argument + +- `authorizerServiceUrl`: hostname:port of authorizer service (_required_) +- `policyRoot`: Policy root (_required_) +- `instanceName`: instance name (_required_ if using hosted authorizer) +- `instanceLabel`: instance label (_required_ if using hosted authorizer) +- `authorizerApiKey`: API key for authorizer service (_required_ if using hosted authorizer) +- `tenantId`: Aserto tenant ID (_required_ if using hosted authorizer) +- `authorizerCertFile`: location on the filesystem of the CA certificate that signed the Aserto authorizer self-signed certificate. See the "Certificates" section for more information. +- `disableTlsValidation`: ignore TLS certificate validation when creating a TLS connection to the authorizer. Defaults to false. +- `endpointPath`: display state map endpoint path, defaults to `/__displaystatemap`. +- `failWithError`: When set to `true`, will forward errors to `next` instead of ending the response directly. Defaults to `false`. +- `useAuthorizationHeader`: When set to `true`, will forward the Authorization header to the authorizer. The authorizer will crack open the JWT and use that as the identity context. Defaults to `true`. +- `identityHeader`: the name of the header from which to extract the `identity` field to pass into the displayStateMap call. This only happens if `useAuthorizationHeader` is false. Defaults to 'identity'. +- `customUserKey`: The property name to check for the subject key. By default, permissions are checked against `req.user`, but you can change it to be `req.myCustomUserKey` with this option. Defaults to `user`. +- `customSubjectKey`: The property name to check for the subject. By default, permissions are checked against `user.sub`, but you can change it to be `user.myCustomSubjectKey` with this option. Defaults to `sub`. + +### 'is' function + +`is` provides an explicit mechanism for calling the Aserto authorizer. + +Use the `is` function to call the authorizer with a `decision`, policy, and resource, and get a boolean `true` or `false` response. The `decision` is a named value in the policy: the string `allowed` is used by convention. Examples: `is('allowed')`, `is('enabled')`, `is('visible')`, etc. + +```javascript +const { is } = require('@aserto/aserto-node'); + +const options = { + authorizerServiceUrl: 'localhost:8282', // required - must pass a valid host:port + policyRoot: 'policy' // required - must be a string representing the policy root (the first component of the policy module name) }; -const user = await directoryClient.object(relation[0].subject); +app.get('/users/:id', async function(req, res) { + try { + const allowed = await is('allowed', req, options); + if (allowed) { + ... + } else { + res.status(403).send("Unauthorized"); + } + } catch (e) { + res.status(500).send(e.message); + } +}); ``` + +#### arguments + +`is(decision, req, options[, packageName[, resourceMap]])`: + +- `decision`: a string representing the name of the decision - typically `allowed` (_required_) +- `req`: Express request object (_required_) +- `options`: a javascript map containing at least `{ authorizerServiceUrl, policyRoot }` as well as `authorizerApiKey` and `tenantId` for the hosted authorizer (_required_) +- `packageName`: a string representing the package name for the the policy (optional) +- `resourceMap`: a map of key/value pairs to use as the resource context for evaluation (optional) + +#### decision argument + +This is simply a string that is correlates to a decision referenced in the policy: for example, `allowed`, `enabled`, etc. + +#### req argument + +The Express request object. + +#### options argument + +- `authorizerServiceUrl`: hostname:port of authorizer service (_required_) +- `policyRoot`: Policy root (_required_) +- `instanceName`: instance name (_required_ if using hosted authorizer) +- `instanceLabel`: instance label (_required_ if using hosted authorizer) +- `authorizerApiKey`: API key for authorizer service (_required_ if using hosted authorizer) +- `tenantId`: Aserto tenant ID (_required_ if using hosted authorizer) +- `authorizerCertFile`: location on the filesystem of the CA certificate that signed the Aserto authorizer self-signed certificate. See the "Certificates" section for more information. +- `disableTlsValidation`: ignore TLS certificate validation when creating a TLS connection to the authorizer. Defaults to false. +- `useAuthorizationHeader`: When set to `true`, will forward the Authorization header to the authorizer. The authorizer will crack open the JWT and use that as the identity context. Defaults to `true`. +- `identityHeader`: the name of the header from which to extract the `identity` field to pass into the `authorize` call. This only happens if `useAuthorizationHeader` is false. Defaults to 'identity'. +- `customUserKey`: The property name to check for the subject key. By default, permissions are checked against `req.user`, but you can change it to be `req.myCustomUserKey` with this option. Defaults to `user`. +- `customSubjectKey`: The property name to check for the subject. By default, permissions are checked against `user.sub`, but you can change it to be `user.myCustomSubjectKey` with this option. Defaults to `sub`. + +#### packageName argument + +By convention, Aserto policy package names are of the form `policyRoot.METHOD.path`. By default, the package name will be inferred from the policy name, HTTP method, and route path: + +- `GET /api/users` --> `policyRoot.GET.api.users` +- `POST /api/users/:id` --> `policyRoot.POST.api.users.__id` + +Passing in the `packageName` parameter into the `is()` function will override this behavior. + +#### resourceMapper argument + +By default, the resource map will be req.params. For example, if the route path is `/api/users/:id`, the resource will be `{ 'id': 'value-of-id' }`. + +Passing in the `resourceMapper` parameter into the `is()` function will override this behavior. + + +## Certificates + +The Topaz [authorizer](/docs/authorizer-guide/overview) exposes SSL-only endpoints. In order for a Node.js policy to properly communicate with the authorizer, TLS certificates must be verified. + +In order for the `aserto-node` package to perform the TLS handshake, it needs to verify the TLS certificate of the Topaz authorizer using the certificate of the CA that signed it - which was placed in `$HOME/.config/topaz/certs/grpc-ca.crt`. Therefore, in order for this middleware to work successfully, either the `authorizerCertCAFile` must be set to the correct path for the CA cert file, or the `disableTlsValidation` flag must be set to `true`. + +Furthermore, when packaging a policy for deployment (e.g. in a Docker container) which uses `aserto-node` to communicate with an authorizer that has a self-signed TLS certificate, you must copy this CA certificate into the container as part of the Docker build (typically performed in the Dockerfile). When you do that, you'll need to override the `authorizerCertCAFile` option that is passed into any of the API calls defined above with the location of this cert file. + +Alternately, to ignore TLS certificate validation when creating a TLS connection to the authorizer, you can set the `disableTlsValidation` option to `true` and avoid TLS certificate validation. This option is **not recommended for production**.