Skip to content

Commit

Permalink
chore(authorization): more refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Aug 2, 2019
1 parent 82090ea commit 997eb86
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import {
authorize,
Authorizer,
} from '../..';
import {AuthorizationTags} from '../../keys';

describe('Authorization', () => {
let app: Application;
let controller: OrderController;
let reqCtx: Context;
let events: string[];

before(givenApplication);
before(givenApplicationAndAuthorizer);
beforeEach(givenRequestContext);

it('allows placeOrder for everyone', async () => {
Expand Down Expand Up @@ -100,14 +101,14 @@ describe('Authorization', () => {
}
}

function givenApplication() {
function givenApplicationAndAuthorizer() {
app = new Application();
app.component(AuthorizationComponent);
app.bind('casbin.enforcer').toDynamicValue(createEnforcer);
app
.bind('authorizationProviders.casbin-provider')
.toProvider(CasbinAuthorizationProvider)
.tag('authorizationProvider');
.tag(AuthorizationTags.AUTHORIZER);
}

function givenRequestContext(user = {name: 'alice'}) {
Expand All @@ -127,22 +128,17 @@ describe('Authorization', () => {
* @returns authenticateFn
*/
value(): Authorizer {
return async (
authzCtx: AuthorizationContext,
metadata: AuthorizationMetadata,
) => {
return this.authorize(authzCtx, metadata);
};
return this.authorize.bind(this);
}

async authorize(
authzCtx: AuthorizationContext,
authorizationCtx: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
events.push(authzCtx.resource);
events.push(authorizationCtx.resource);
const request: AuthorizationRequest = {
subject: authzCtx.principals[0].name,
object: metadata.resource || authzCtx.resource,
subject: authorizationCtx.principals[0].name,
object: metadata.resource || authorizationCtx.resource,
action: (metadata.scopes && metadata.scopes[0]) || 'execute',
};
const allow = await this.enforcer.enforce(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import {
Authorizer,
EVERYONE,
} from '../..';
import {AuthorizationTags} from '../../keys';

describe('Authorization', () => {
let app: Application;
let controller: OrderController;
let reqCtx: Context;
let events: string[];

before(givenApplication);
before(givenApplicationAndAuthorizer);
beforeEach(givenRequestContext);

it('allows placeOrder for everyone', async () => {
Expand Down Expand Up @@ -73,13 +74,13 @@ describe('Authorization', () => {
}
}

function givenApplication() {
function givenApplicationAndAuthorizer() {
app = new Application();
app.component(AuthorizationComponent);
app
.bind('authorizationProviders.my-provider')
.toProvider(MyAuthorizationProvider)
.tag('authorizationProvider');
.tag(AuthorizationTags.AUTHORIZER);
}

function givenRequestContext() {
Expand All @@ -93,21 +94,17 @@ describe('Authorization', () => {
* Provider of a function which authenticates
*/
class MyAuthorizationProvider implements Provider<Authorizer> {
constructor() {}

/**
* @returns authenticateFn
*/
value(): Authorizer {
return async (
context: AuthorizationContext,
metadata: AuthorizationMetadata,
) => {
return this.authorize(context, metadata);
};
return this.authorize.bind(this);
}

authorize(context: AuthorizationContext, metadata: AuthorizationMetadata) {
async authorize(
context: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
events.push(context.resource);
if (
context.resource === 'OrderController.prototype.cancelOrder' &&
Expand Down
92 changes: 61 additions & 31 deletions packages/authorization/src/authorize-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,85 @@
import {
asGlobalInterceptor,
bind,
BindingAddress,
Context,
filterByTag,
inject,
Interceptor,
InvocationContext,
Next,
Provider,
} from '@loopback/context';
import * as debugFactory from 'debug';
import {getAuthorizeMetadata} from './decorators/authorize';
import {AuthorizationTags} from './keys';
import {AuthorizationContext, AuthorizationDecision, Authorizer} from './types';

const debug = debugFactory('loopback:authorization:interceptor');

@bind(asGlobalInterceptor('authorization'))
export class AuthorizationInterceptor implements Provider<Interceptor> {
constructor(
@inject(filterByTag('authorizationProvider'))
private authorizationFunctions: Authorizer[],
@inject(filterByTag(AuthorizationTags.AUTHORIZER))
private authorizers: Authorizer[],
) {}

value(): Interceptor {
return async (invocationCtx, next) => {
const description = debug.enabled ? invocationCtx.description : '';
const metadata = getAuthorizeMetadata(
invocationCtx.target,
invocationCtx.methodName,
);
if (!metadata) {
debug('No authorization metadata is found %s', description);
return await next();
}
debug('Authorization metadata for %s', description, metadata);
const user = await invocationCtx.get<{name: string}>('current.user', {
optional: true,
});
debug('Current user', user);
const authCtx: AuthorizationContext = {
principals: user ? [{name: user.name, type: 'USER'}] : [],
roles: [],
scopes: [],
resource: invocationCtx.targetName,
invocationContext: invocationCtx,
};
debug('Security context for %s', description, authCtx);
for (const fn of this.authorizationFunctions) {
const decision = await fn(authCtx, metadata);
if (decision === AuthorizationDecision.DENY) {
throw new Error('Access denied');
}
}
return this.intercept.bind(this);
}

async intercept(invocationCtx: InvocationContext, next: Next) {
const description = debug.enabled ? invocationCtx.description : '';
const metadata = getAuthorizeMetadata(
invocationCtx.target,
invocationCtx.methodName,
);
if (!metadata) {
debug('No authorization metadata is found %s', description);
return await next();
}
debug('Authorization metadata for %s', description, metadata);
const user = await invocationCtx.get<{name: string}>('current.user', {
optional: true,
});
debug('Current user', user);
const authorizationCtx: AuthorizationContext = {
principals: user ? [{name: user.name, type: 'USER'}] : [],
roles: [],
scopes: [],
resource: invocationCtx.targetName,
invocationContext: invocationCtx,
};
debug('Security context for %s', description, authorizationCtx);
let authorizers = await loadAuthorizers(
invocationCtx,
metadata.voters || [],
);
authorizers = authorizers.concat(this.authorizers);
for (const fn of authorizers) {
const decision = await fn(authorizationCtx, metadata);
if (decision === AuthorizationDecision.DENY) {
throw new Error('Access denied');
}
}
return await next();
}
}

async function loadAuthorizers(
ctx: Context,
authorizers: (Authorizer | BindingAddress<Authorizer>)[],
) {
const authorizerFunctions: Authorizer[] = [];
const bindings = ctx.findByTag<Authorizer>(AuthorizationTags.AUTHORIZER);
authorizers = authorizers.concat(bindings.map(b => b.key));
for (const keyOrFn of authorizers) {
if (typeof keyOrFn === 'function') {
authorizerFunctions.push(keyOrFn);
} else {
const fn = await ctx.get(keyOrFn);
authorizerFunctions.push(fn);
}
}
return authorizerFunctions;
}
7 changes: 4 additions & 3 deletions packages/authorization/src/decorators/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import {
import {
AUTHENTICATED,
AuthorizationMetadata,
Authorizer,
EVERYONE,
UNAUTHENTICATED,
Voter,
} from '../types';

export const AUTHORIZATION_METHOD_KEY = MetadataAccessor.create<
Expand Down Expand Up @@ -147,8 +147,9 @@ export namespace authorize {
* Shortcut to configure voters
* @param voters
*/
export const vote = (...voters: (Voter | BindingAddress<Voter>)[]) =>
authorize({voters});
export const vote = (
...voters: (Authorizer | BindingAddress<Authorizer>)[]
) => authorize({voters});

/**
* Allows all
Expand Down
12 changes: 11 additions & 1 deletion packages/authorization/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@
// License text available at https://opensource.org/licenses/MIT

/**
* Binding keys used by this component.
* Binding keys used by authorization component.
*/
export namespace AuthorizationBindings {
export const METADATA = 'authorization.operationMetadata';
}

/**
* Binding tags used by authorization component
*/
export namespace AuthorizationTags {
/**
* A tag for authorizers
*/
export const AUTHORIZER = 'authorizer';
}
49 changes: 19 additions & 30 deletions packages/authorization/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,25 @@ export const UNAUTHENTICATED = '$unauthenticated';
export const ANONYMOUS = '$anonymous';

/**
* Voting decision for the authorization decision
* Decisions for authorization
*/
export enum VotingDecision {
export enum AuthorizationDecision {
/**
* Access allowed
*/
ALLOW = 'Allow',
/**
* Access denied
*/
DENY = 'Deny',
/**
* No decision
*/
ABSTAIN = 'Abstain',
}

/**
* A voter function
*/
export type Voter = (
authorizationCtx: AuthorizationContext,
) => Promise<VotingDecision>;

/**
* Authorization metadata stored via Reflection API
* Authorization metadata supplied via `@authorize` decorator
*/
export interface AuthorizationMetadata {
/**
Expand All @@ -41,10 +43,11 @@ export interface AuthorizationMetadata {
* Roles that are denied access
*/
deniedRoles?: string[];

/**
* Voters that help make the authorization decision
*/
voters?: (Voter | BindingAddress<Voter>)[];
voters?: (Authorizer | BindingAddress<Authorizer>)[];

/**
* Name of the resource, default to the method name
Expand All @@ -56,24 +59,6 @@ export interface AuthorizationMetadata {
scopes?: string[];
}

/**
* Decisions for authorization
*/
export enum AuthorizationDecision {
/**
* Access allowed
*/
ALLOW = 'Allow',
/**
* Access denied
*/
DENY = 'Deny',
/**
* No decision
*/
ABSTAIN = 'Abstain',
}

/**
* Represent a user, an application, or a device
*/
Expand All @@ -87,7 +72,7 @@ export interface Principal {
*/
type: string;

// organization
// organization/realm/domain/tenant
// team/group

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -212,6 +197,10 @@ export type Authorizer =
* Inspired by https://github.com/casbin/node-casbin
*/
export interface AuthorizationRequest {
/**
* The domain (realm/tenant)
*/
domain?: string;
/**
* The requestor that wants to access a resource.
*/
Expand Down

0 comments on commit 997eb86

Please sign in to comment.