From 23a85a29000bd9dd5a443f446bd7d5bb609c37f9 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 28 Mar 2018 11:22:06 -0700 Subject: [PATCH] feat: add authorization component --- packages/authorization/.npmrc | 1 + packages/authorization/LICENSE | 25 ++++++++ packages/authorization/README.md | 59 +++++++++++++++++++ packages/authorization/docs.json | 12 ++++ packages/authorization/index.d.ts | 6 ++ packages/authorization/index.js | 6 ++ packages/authorization/index.ts | 8 +++ packages/authorization/package.json | 47 +++++++++++++++ .../src/authorization-component.ts | 21 +++++++ .../authorization/src/decorators/authorize.ts | 50 ++++++++++++++++ packages/authorization/src/index.ts | 13 ++++ packages/authorization/src/keys.ts | 20 +++++++ .../src/providers/authorization-metadata.ts | 31 ++++++++++ .../authorization/src/providers/authorize.ts | 33 +++++++++++ packages/authorization/src/types.ts | 43 ++++++++++++++ .../test/unit/authorize-decorator.test.ts | 48 +++++++++++++++ packages/authorization/tsconfig.build.json | 8 +++ 17 files changed, 431 insertions(+) create mode 100644 packages/authorization/.npmrc create mode 100644 packages/authorization/LICENSE create mode 100644 packages/authorization/README.md create mode 100644 packages/authorization/docs.json create mode 100644 packages/authorization/index.d.ts create mode 100644 packages/authorization/index.js create mode 100644 packages/authorization/index.ts create mode 100644 packages/authorization/package.json create mode 100644 packages/authorization/src/authorization-component.ts create mode 100644 packages/authorization/src/decorators/authorize.ts create mode 100644 packages/authorization/src/index.ts create mode 100644 packages/authorization/src/keys.ts create mode 100644 packages/authorization/src/providers/authorization-metadata.ts create mode 100644 packages/authorization/src/providers/authorize.ts create mode 100644 packages/authorization/src/types.ts create mode 100644 packages/authorization/test/unit/authorize-decorator.test.ts create mode 100644 packages/authorization/tsconfig.build.json diff --git a/packages/authorization/.npmrc b/packages/authorization/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/authorization/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/authorization/LICENSE b/packages/authorization/LICENSE new file mode 100644 index 000000000000..af99ad048201 --- /dev/null +++ b/packages/authorization/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2018. All Rights Reserved. +Node module: @loopback/authorization +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/authorization/README.md b/packages/authorization/README.md new file mode 100644 index 000000000000..89f33b063826 --- /dev/null +++ b/packages/authorization/README.md @@ -0,0 +1,59 @@ +# @loopback/authorization + +A LoopBack 4 component for authorization support. + +**This is a reference implementation showing how to implement an authorization +component, it is not production ready.** + +## Overview + + +## Installation + +```shell +npm install --save @loopback/authorization +``` + +## Basic use + +Start by decorating your controller methods with `@authorize` to require the +request to be authenticated. + +In this example, we make the user profile available via dependency injection +using a key available from `@loopback/authorization` package. + +```ts +import {inject} from '@loopback/context'; +import {authorize} from '@loopback/authorization'; +import {get} from '@loopback/rest'; + +export class MyController { + + @authorize({allow: ['ADMIN']}) + @get('/number-of-views') + numOfViews(): number { + return 100; + } +} +``` + +## Related resources + + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/authorization/docs.json b/packages/authorization/docs.json new file mode 100644 index 000000000000..0ce80ef8469d --- /dev/null +++ b/packages/authorization/docs.json @@ -0,0 +1,12 @@ +{ + "content": [ + "index.ts", + "src/index.ts", + "src/decorators/authorize.ts", + "src/providers/authorization-metadata.ts", + "src/providers/authorize.ts", + "src/authorization-component.ts", + "src/keys.ts" + ], + "codeSectionDepth": 4 +} diff --git a/packages/authorization/index.d.ts b/packages/authorization/index.d.ts new file mode 100644 index 000000000000..026b9d7c9ffc --- /dev/null +++ b/packages/authorization/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/authorization/index.js b/packages/authorization/index.js new file mode 100644 index 000000000000..28622def8c06 --- /dev/null +++ b/packages/authorization/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/packages/authorization/index.ts b/packages/authorization/index.ts new file mode 100644 index 000000000000..f81c156a7f3f --- /dev/null +++ b/packages/authorization/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/authorization/package.json b/packages/authorization/package.json new file mode 100644 index 000000000000..a148b5291d97 --- /dev/null +++ b/packages/authorization/package.json @@ -0,0 +1,47 @@ +{ + "name": "@loopback/authorization", + "version": "0.1.0", + "description": "A LoopBack component for authorization support.", + "engines": { + "node": ">=8" + }, + "scripts": { + "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", + "build": "lb-tsc es2017", + "build:apidocs": "lb-apidocs", + "clean": "lb-clean loopback-authorization*.tgz dist package api-docs", + "integration": "lb-mocha \"DIST/test/integration/**/*.js\"", + "prepublishOnly": "npm run build && npm run build:apidocs", + "pretest": "npm run build", + "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/integration/**/*.js\" \"DIST/test/acceptance/**/*.js\"", + "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-authorization*.tgz && tree package && npm run clean" + }, + "author": "IBM", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "@loopback/context": "^0.5.3", + "@loopback/core": "^0.4.3" + }, + "devDependencies": { + "@loopback/build": "^0.4.2", + "@loopback/testlab": "^0.5.3" + }, + "keywords": [ + "LoopBack", + "Authorization" + ], + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist/src", + "api-docs", + "src" + ], + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/authorization/src/authorization-component.ts b/packages/authorization/src/authorization-component.ts new file mode 100644 index 000000000000..11f7560ab2ba --- /dev/null +++ b/packages/authorization/src/authorization-component.ts @@ -0,0 +1,21 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {AuthorizationBindings} from './keys'; +import {Component, ProviderMap} from '@loopback/core'; +import {AuthorizationProvider} from './providers/authorize'; +import {AuthorizationMetadataProvider} from './providers/authorization-metadata'; + +export class AuthenticationComponent implements Component { + providers?: ProviderMap; + + constructor() { + this.providers = { + [AuthorizationBindings.AUTHORIZE_ACTION.key]: AuthorizationProvider, + [AuthorizationBindings.AUTHORIZE_METADATA + .key]: AuthorizationMetadataProvider, + }; + } +} diff --git a/packages/authorization/src/decorators/authorize.ts b/packages/authorization/src/decorators/authorize.ts new file mode 100644 index 000000000000..af52214664c0 --- /dev/null +++ b/packages/authorization/src/decorators/authorize.ts @@ -0,0 +1,50 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + MetadataInspector, + Constructor, + MethodDecoratorFactory, +} from '@loopback/context'; +import {AuthorizationBindings} from '../keys'; +import {AuthorizationMetadata} from '../types'; + +/** + * Mark a controller method as requiring authorized user. + * + * @param spec Authorization metadata + */ +export function authorize(spec: AuthorizationMetadata) { + return MethodDecoratorFactory.createDecorator( + AuthorizationBindings.METADATA, + spec, + ); +} + +export namespace authorize { + export const allowAll = () => authorize({allow: ['$everyone']}); + export const allowAuthenticated = () => + authorize({allow: ['$authenticated']}); + export const denyAll = () => authorize({deny: ['$everyone']}); + export const denyUnauthenticated = () => + authorize({deny: ['$anonymous', '$unauthenticated']}); +} + +/** + * Fetch authorization metadata stored by `@authorize` decorator. + * + * @param controllerClass Target controller + * @param methodName Target method + */ +export function getAuthorizeMetadata( + controllerClass: Constructor<{}>, + methodName: string, +): AuthorizationMetadata | undefined { + return MetadataInspector.getMethodMetadata( + AuthorizationBindings.METADATA, + controllerClass.prototype, + methodName, + ); +} diff --git a/packages/authorization/src/index.ts b/packages/authorization/src/index.ts new file mode 100644 index 000000000000..ab2249b1d931 --- /dev/null +++ b/packages/authorization/src/index.ts @@ -0,0 +1,13 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './types'; +export * from './authorization-component'; +export * from './decorators/authorize'; +export * from './keys'; + +// internals for tests +export * from './providers/authorization-metadata'; +export * from './providers/authorize'; diff --git a/packages/authorization/src/keys.ts b/packages/authorization/src/keys.ts new file mode 100644 index 000000000000..0696646aba06 --- /dev/null +++ b/packages/authorization/src/keys.ts @@ -0,0 +1,20 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingKey} from '@loopback/context'; +import {AuthorizationMetadata, AuthorizeFn} from './types'; + +/** + * Binding keys used by this component. + */ +export namespace AuthorizationBindings { + export const AUTHORIZE_ACTION = BindingKey.create( + 'authorization.actions.authorize', + ); + export const AUTHORIZE_METADATA = BindingKey.create( + 'authorization.operationMetadata', + ); + export const METADATA = 'authorization.operationMetadata'; +} diff --git a/packages/authorization/src/providers/authorization-metadata.ts b/packages/authorization/src/providers/authorization-metadata.ts new file mode 100644 index 000000000000..faa9836e6842 --- /dev/null +++ b/packages/authorization/src/providers/authorization-metadata.ts @@ -0,0 +1,31 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {CoreBindings} from '@loopback/core'; +import {Constructor, Provider, inject} from '@loopback/context'; +import {getAuthorizeMetadata} from '../decorators/authorize'; +import {AuthorizationMetadata} from '../types'; + +/** + * @description Provides authorization metadata of a controller method + * @example `context.bind('authorization.meta') + * .toProvider(AuthMetadataProvider)` + */ +export class AuthorizationMetadataProvider + implements Provider { + constructor( + @inject(CoreBindings.CONTROLLER_CLASS) + private readonly controllerClass: Constructor<{}>, + @inject(CoreBindings.CONTROLLER_METHOD_NAME) + private readonly methodName: string, + ) {} + + /** + * @returns AuthorizationMetadata + */ + value(): AuthorizationMetadata | undefined { + return getAuthorizeMetadata(this.controllerClass, this.methodName); + } +} diff --git a/packages/authorization/src/providers/authorize.ts b/packages/authorization/src/providers/authorize.ts new file mode 100644 index 000000000000..a80cdfa7dd46 --- /dev/null +++ b/packages/authorization/src/providers/authorize.ts @@ -0,0 +1,33 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Provider} from '@loopback/context'; +import { + AuthorizationMetadata, + AuthorizeFn, + SecurityContext, + AuthorizationDecision, +} from '../types'; + +/** + * @description Provider of a function which authenticates + * @example `context.bind('authentication_key') + * .toProvider(AuthorizationProvider)` + */ +export class AuthorizationProvider implements Provider { + constructor() {} + + /** + * @returns authenticateFn + */ + value(): AuthorizeFn { + return async ( + securityContext: SecurityContext, + metadata: AuthorizationMetadata, + ) => { + return AuthorizationDecision.ALLOW; + }; + } +} diff --git a/packages/authorization/src/types.ts b/packages/authorization/src/types.ts new file mode 100644 index 000000000000..944d7e905bbb --- /dev/null +++ b/packages/authorization/src/types.ts @@ -0,0 +1,43 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * Authorization metadata stored via Reflection API + */ +export interface AuthorizationMetadata { + allow?: string[]; + deny?: string[]; + scopes?: string[]; +} + +export enum AuthorizationDecision { + ALLOW = 'Allow', + DENY = 'Deny', + AUDIT = 'Audit', +} + +export interface Principal { + name: string; + type: string; + // tslint:disable-next-line:no-any + [attribute: string]: any; +} + +export interface Role { + name: string; + type: string; + // tslint:disable-next-line:no-any + [attribute: string]: any; +} + +export interface SecurityContext { + principals: Principal[]; + roles: Role[]; +} + +export type AuthorizeFn = ( + caller: SecurityContext, + target: AuthorizationMetadata, +) => Promise; diff --git a/packages/authorization/test/unit/authorize-decorator.test.ts b/packages/authorization/test/unit/authorize-decorator.test.ts new file mode 100644 index 000000000000..3a2c7006f7c4 --- /dev/null +++ b/packages/authorization/test/unit/authorize-decorator.test.ts @@ -0,0 +1,48 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/authorization +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {authorize, getAuthorizeMetadata} from '../..'; + +describe('Authentication', () => { + describe('@authorize decorator', () => { + it('can add authorize metadata to target method', () => { + class TestClass { + @authorize({allow: ['ADMIN'], scopes: ['secret.read']}) + getSecret() {} + } + + const metaData = getAuthorizeMetadata(TestClass, 'getSecret'); + expect(metaData).to.eql({ + allow: ['ADMIN'], + scopes: ['secret.read'], + }); + }); + + it('can add allowAll to target method', () => { + class TestClass { + @authorize.allowAll() + getSecret() {} + } + + const metaData = getAuthorizeMetadata(TestClass, 'getSecret'); + expect(metaData).to.eql({ + allow: ['$everyone'], + }); + }); + + it('can add denyAll to target method', () => { + class TestClass { + @authorize.denyAll() + getSecret() {} + } + + const metaData = getAuthorizeMetadata(TestClass, 'getSecret'); + expect(metaData).to.eql({ + deny: ['$everyone'], + }); + }); + }); +}); diff --git a/packages/authorization/tsconfig.build.json b/packages/authorization/tsconfig.build.json new file mode 100644 index 000000000000..3ffcd508d23e --- /dev/null +++ b/packages/authorization/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +}