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..b6abf52545b9 --- /dev/null +++ b/packages/authorization/README.md @@ -0,0 +1,56 @@ +# @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..9f7202bc028f --- /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.12.4", + "@loopback/core": "^0.11.4" + }, + "devDependencies": { + "@loopback/build": "^0.3.3", + "@loopback/testlab": "^0.4.1" + }, + "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..4331ef48c590 --- /dev/null +++ b/packages/authorization/src/authorization-component.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 {AuthorizationBindings} from './keys'; +import {Component, ProviderMap} from '@loopback/core'; +import {AuthorizationProvider} from './providers/authorize'; +import {AuthMetadataProvider} from './providers/authorization-metadata'; + +export class AuthenticationComponent implements Component { + providers?: ProviderMap; + + constructor() { + this.providers = { + [AuthorizationBindings.AUTHORIZE_ACTION]: AuthorizationProvider, + [AuthorizationBindings.METADATA]: AuthMetadataProvider, + }; + } +} diff --git a/packages/authorization/src/decorators/authorize.ts b/packages/authorization/src/decorators/authorize.ts new file mode 100644 index 000000000000..cb377babe9da --- /dev/null +++ b/packages/authorization/src/decorators/authorize.ts @@ -0,0 +1,58 @@ +// 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'; + +/** + * Authorization metadata stored via Reflection API + */ +export interface AuthorizationMetadata { + allow?: string[]; + deny?: string[]; + scopes?: string[]; +} + +/** + * 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..b304cacbb89a --- /dev/null +++ b/packages/authorization/src/index.ts @@ -0,0 +1,12 @@ +// 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 './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..8ba282e0c3bf --- /dev/null +++ b/packages/authorization/src/keys.ts @@ -0,0 +1,12 @@ +// 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 + +/** + * Binding keys used by this component. + */ +export namespace AuthorizationBindings { + export const AUTHORIZE_ACTION = 'authorization.actions.authorize'; + 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..4559d62ef355 --- /dev/null +++ b/packages/authorization/src/providers/authorization-metadata.ts @@ -0,0 +1,33 @@ +// 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 { + AuthorizationMetadata, + getAuthorizeMetadata, +} from '../decorators/authorize'; + +/** + * @description Provides authorization metadata of a controller method + * @example `context.bind('authorization.meta') + * .toProvider(AuthMetadataProvider)` + */ +export class AuthMetadataProvider + 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..110be5d97c63 --- /dev/null +++ b/packages/authorization/src/providers/authorize.ts @@ -0,0 +1,58 @@ +// 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} from '..'; + +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; + +/** + * @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/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"] +}