Skip to content

Commit

Permalink
make check middleware configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
gimmyxd committed Oct 26, 2023
1 parent 0f5e9cf commit 5234d8f
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 79 deletions.
24 changes: 12 additions & 12 deletions lib/authorizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ interface AuthorizerConfig {
authorizerCertFile?: string;
}
export class Authorizer {
Client: AuthorizerClient;
Metadata: Metadata;
client: AuthorizerClient;
metadata: Metadata;
constructor(
config: AuthorizerConfig,
channelCredentials: ChannelCredentials = credentials.createSsl()
Expand All @@ -50,8 +50,8 @@ export class Authorizer {
metadata.add("authorization", `basic ${config.authorizerApiKey}`);
config.tenantId && metadata.add("aserto-tenant-id", config.tenantId);

this.Metadata = metadata;
this.Client = new AuthorizerClient(url, channelCredentials);
this.metadata = metadata;
this.client = new AuthorizerClient(url, channelCredentials);
}

async Is({
Expand All @@ -74,9 +74,9 @@ export class Authorizer {

return new Promise((resolve, reject) => {
try {
this.Client.is(
this.client.is(
request,
this.Metadata,
this.metadata,
(err: ServiceError, response: IsResponse) => {
if (err) {
const message = `'is' returned error: ${err.message}`;
Expand Down Expand Up @@ -123,9 +123,9 @@ export class Authorizer {

return new Promise((resolve, reject) => {
try {
this.Client.query(
this.client.query(
request,
this.Metadata,
this.metadata,
(err: ServiceError, response: QueryResponse) => {
if (err) {
const message = `'query' returned error: ${err.message}`;
Expand Down Expand Up @@ -178,9 +178,9 @@ export class Authorizer {

return new Promise((resolve, reject) => {
try {
this.Client.decisionTree(
this.client.decisionTree(
request,
this.Metadata,
this.metadata,
(err: ServiceError, response: DecisionTreeResponse) => {
if (err) {
const message = `'decisionTree' returned error: ${err.message}`;
Expand Down Expand Up @@ -217,9 +217,9 @@ export class Authorizer {

return new Promise((resolve, reject) => {
try {
this.Client.listPolicies(
this.client.listPolicies(
request,
this.Metadata,
this.metadata,
(err: ServiceError, response: ListPoliciesResponse) => {
if (err) {
const message = `'listPolicies' returned error: ${err.message}`;
Expand Down
46 changes: 42 additions & 4 deletions lib/authorizer/mapper/resource/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,48 @@ import { Request } from "express";
import { CheckOptions } from "../../middleware";
import { CheckResourceContext } from "../../model/resourceContext";

export default (options: CheckOptions, req: Request): CheckResourceContext => {
export default async (
options: CheckOptions,
req: Request
): Promise<CheckResourceContext> => {
const [objectId, objectType] = await object(options, req);
const rel = await relation(options, req);
return {
object_key: options.objectKey || req.params.id || "",
relation: options.relation,
object_type: options.objectType,
object_id: objectId,
object_type: objectType,
relation: rel,
subject_type: options.subject?.type || "user",
};
};

const relation = async (
options: CheckOptions,
req: Request
): Promise<string> => {
let name = options.relation?.name;
if (options.relation?.mapper) {
name = await options.relation.mapper(req);
}

return name || "";
};

const object = async (
options: CheckOptions,
req: Request
): Promise<[string, string]> => {
let objectId = options.object?.id;
let objectType = options.object?.type;

if (options.object?.idMapper) {
objectId = await options.object.idMapper(req);
}

if (options.object?.mapper) {
const obj = await options.object.mapper(req);
objectId = obj.objectId;
objectType = obj.objectType;
}

return [objectId || "", objectType || ""];
};
126 changes: 70 additions & 56 deletions lib/authorizer/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NextFunction, Request, Response } from "express";
import { IdentityContext } from "@aserto/node-authorizer/pkg/aserto/authorizer/v2/api/identity_context_pb";
import { PolicyContext } from "@aserto/node-authorizer/pkg/aserto/authorizer/v2/api/policy_context_pb";
import { PolicyInstance } from "@aserto/node-authorizer/pkg/aserto/authorizer/v2/api/policy_instance_pb";

import { errorHandler } from "../errorHandler";
import { log } from "../log";
import { Authorizer } from "./index";
import JWTIdentityMapper from "./mapper/identity/jwt";
import PolicyPathMapper from "./mapper/policy/path";
Expand All @@ -22,9 +22,20 @@ type Policy = {
};

export type CheckOptions = {
objectKey: string;
objectType: string;
relation: string;
object?: {
id?: string;
type?: string;
idMapper?: StringMapper;
mapper?: ObjectMapper;
};
relation?: {
name?: string;
mapper?: StringMapper;
};
subject?: {
type?: string;
mapper?: IdentityMapper;
};
};

export type ResourceMapper =
Expand All @@ -34,12 +45,17 @@ export type ResourceMapper =
export type IdentityMapper = (req: Request) => Promise<IdentityContext>;
export type PolicyMapper = (req: Request) => Promise<PolicyContext>;

type ObjectMapper = (
req: Request
) => Promise<{ objectId: string; objectType: string }>;
type StringMapper = (req: Request) => Promise<string>;

export class Middleware {
Client: Authorizer;
Policy: Policy;
ResourceMapper?: ResourceMapper;
IdentityMapper?: IdentityMapper;
PolicyMapper?: PolicyMapper;
client: Authorizer;
policy: Policy;
resourceMapper?: ResourceMapper;
identityMapper?: IdentityMapper;
policyMapper?: PolicyMapper;
constructor({
client,
policy,
Expand All @@ -53,11 +69,23 @@ export class Middleware {
identityMapper?: IdentityMapper;
policyMapper?: PolicyMapper;
}) {
this.Client = client;
this.Policy = policy;
this.ResourceMapper = resourceMapper;
this.IdentityMapper = identityMapper;
this.PolicyMapper = policyMapper;
this.client = client;
this.policy = policy;
this.resourceMapper = resourceMapper;
this.identityMapper = identityMapper;
this.policyMapper = policyMapper;
}

private policyInstance(): PolicyInstance | undefined {
return this.policy.name && this.policy.instanceLabel
? policyInstance(this.policy.name, this.policy.instanceLabel)
: undefined;
}

private async identityContext(req: Request): Promise<IdentityContext> {
return this.identityMapper
? this.identityMapper(req)
: JWTIdentityMapper(req);
}

// Check Middleware
Expand All @@ -66,45 +94,35 @@ export class Middleware {
const error = errorHandler(next, true);

const callAuthorizer = async () => {
const identityCtx: IdentityContext = this.IdentityMapper
? await this.IdentityMapper(req)
: JWTIdentityMapper(req);

const policyCtx = this.PolicyMapper
? await this.PolicyMapper(req)
const policyCtx = this.policyMapper
? await this.policyMapper(req)
: policyContext("rebac.check", ["allowed"]);

let resourceContext: ResourceContext = checkResourceMapper(
let resourceContext: ResourceContext = await checkResourceMapper(
options,
req
);
if (typeof this.ResourceMapper === "function") {
if (typeof this.resourceMapper === "function") {
resourceContext = {
...resourceContext,
...(await this.ResourceMapper(req)),
...(await this.resourceMapper(req)),
};
} else {
resourceContext = { ...resourceContext, ...this.ResourceMapper };
resourceContext = { ...resourceContext, ...this.resourceMapper };
}

const policyInst =
this.Policy.name && this.Policy.instanceLabel
? policyInstance(this.Policy.name, this.Policy.instanceLabel)
: undefined;

return this.Client.Is({
identityContext: identityCtx,
return this.client.Is({
identityContext: await this.identityContext(req),
policyContext: policyCtx,
policyInstance: policyInst,
policyInstance: this.policyInstance(),
resourceContext: resourceContext,
});
};
try {
const allowed = await callAuthorizer();
log(`Requested evaluated with: ${allowed}`);
return allowed
? next()
: error(res, `Forbidden by policy ${this.Policy.root}`);
: error(res, `Forbidden by policy ${this.policy.root}`);
} catch (err) {
error(res, err as string);
}
Expand All @@ -117,41 +135,37 @@ export class Middleware {
const error = errorHandler(next, true);

const callAuthorizer = async () => {
const identityCtx: IdentityContext = this.IdentityMapper
? await this.IdentityMapper(req)
: JWTIdentityMapper(req);

const policyCtx = this.PolicyMapper
? await this.PolicyMapper(req)
: PolicyPathMapper(this.Policy.root, req);

const resourceContext = this.ResourceMapper
? typeof this.ResourceMapper === "function"
? await this.ResourceMapper(req)
: this.ResourceMapper
const policyCtx = this.policyMapper
? await this.policyMapper(req)
: PolicyPathMapper(this.policy.root, req);

const resourceContext = this.resourceMapper
? typeof this.resourceMapper === "function"
? await this.resourceMapper(req)
: this.resourceMapper
: ResourceParamsMapper(req);

const policyInst =
this.Policy.name && this.Policy.instanceLabel
? policyInstance(this.Policy.name, this.Policy.instanceLabel)
: undefined;

return this.Client.Is({
identityContext: identityCtx,
return this.client.Is({
identityContext: await this.identityContext(req),
policyContext: policyCtx,
policyInstance: policyInst,
policyInstance: this.policyInstance(),
resourceContext: resourceContext,
});
};
try {
const allowed = await callAuthorizer();
log(`Requested evaluated with: ${allowed}`);
return allowed
? next()
: error(res, `Forbidden by policy ${this.Policy.root}`);
: error(res, `Forbidden by policy ${this.policy.root}`);
} catch (err) {
error(res, err as string);
}
};
}
}

export const ObjectIDFromVar = (key: string) => {
return async (req: Request) => {
return req.params?.[key];
};
};
3 changes: 2 additions & 1 deletion lib/authorizer/model/resourceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export type ResourceContext = {
export type CheckResourceContext = {
relation: string;
object_type: string;
object_key: string;
object_id: string;
subject_type: string;
};
6 changes: 3 additions & 3 deletions lib/ds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
Struct,
} from "@bufbuild/protobuf";

interface Config {
export interface DirectoryConfig {
url?: string;
tenantId?: string;
apiKey?: string;
Expand Down Expand Up @@ -90,7 +90,7 @@ export class Directory {
ReaderClient: PromiseClient<typeof Reader>;
WriterClient: PromiseClient<typeof Writer>;

constructor(config: Config) {
constructor(config: DirectoryConfig) {
const setHeader = (
req:
| UnaryRequest<AnyMessage, AnyMessage>
Expand Down Expand Up @@ -322,6 +322,6 @@ function handleError(error: unknown, method: string) {
}
}

export const ds = (config: Config): Directory => {
export const ds = (config: DirectoryConfig): Directory => {
return new Directory(config);
};
11 changes: 8 additions & 3 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Authorizer, authz } from "./authorizer";
import AnonymousIdentityMapper from "./authorizer/mapper/identity/anonymous";
import JWTIdentityMapper from "./authorizer/mapper/identity/jwt";
import SubIdentityMapper from "./authorizer/mapper/identity/sub";
import { Middleware } from "./authorizer/middleware";
import PolicyPathMapper from "./authorizer/mapper/policy/path";
import { Middleware, ObjectIDFromVar } from "./authorizer/middleware";
import { displayStateMap } from "./displayStateMap";
import { Directory, ds } from "./ds";
import { Directory, DirectoryConfig, ds } from "./ds";
import { is } from "./is";
import { jwtAuthz } from "./jwtAuthz";
import { AuthzOptions, jwtAuthz } from "./jwtAuthz";
export {
is,
jwtAuthz,
Expand All @@ -19,4 +20,8 @@ export {
SubIdentityMapper,
JWTIdentityMapper,
AnonymousIdentityMapper,
DirectoryConfig,
AuthzOptions,
PolicyPathMapper,
ObjectIDFromVar,
};

0 comments on commit 5234d8f

Please sign in to comment.