@loopback/authorization
Advanced tools
Comparing version 0.1.2 to 0.2.0
@@ -6,2 +6,13 @@ # Change Log | ||
# [0.2.0](https://github.com/strongloop/loopback-next/compare/@loopback/authorization@0.1.2...@loopback/authorization@0.2.0) (2019-08-19) | ||
### Features | ||
* **authorization:** add options to improve decision making ([ce59488](https://github.com/strongloop/loopback-next/commit/ce59488)) | ||
## [0.1.2](https://github.com/strongloop/loopback-next/compare/@loopback/authorization@0.1.1...@loopback/authorization@0.1.2) (2019-08-15) | ||
@@ -8,0 +19,0 @@ |
@@ -6,11 +6,21 @@ "use strict"; | ||
// License text available at https://opensource.org/licenses/MIT | ||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; | ||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); | ||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; | ||
return c > 3 && r && Object.defineProperty(target, key, r), r; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const core_1 = require("@loopback/core"); | ||
const authorize_interceptor_1 = require("./authorize-interceptor"); | ||
class AuthorizationComponent { | ||
const keys_1 = require("./keys"); | ||
let AuthorizationComponent = class AuthorizationComponent { | ||
constructor() { | ||
this.bindings = [core_1.createBindingFromClass(authorize_interceptor_1.AuthorizationInterceptor)]; | ||
} | ||
} | ||
}; | ||
AuthorizationComponent = __decorate([ | ||
core_1.bind({ tags: { [core_1.ContextTags.KEY]: keys_1.AuthorizationBindings.COMPONENT.key } }) | ||
], AuthorizationComponent); | ||
exports.AuthorizationComponent = AuthorizationComponent; | ||
//# sourceMappingURL=authorization-component.js.map |
import { Interceptor, InvocationContext, Next, Provider } from '@loopback/context'; | ||
import { Authorizer } from './types'; | ||
import { AuthorizationOptions, Authorizer } from './types'; | ||
export declare class AuthorizationInterceptor implements Provider<Interceptor> { | ||
private authorizers; | ||
constructor(authorizers: Authorizer[]); | ||
private options; | ||
constructor(authorizers: Authorizer[], options?: AuthorizationOptions); | ||
value(): Interceptor; | ||
intercept(invocationCtx: InvocationContext, next: Next): Promise<any>; | ||
} |
@@ -19,2 +19,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const authentication_1 = require("@loopback/authentication"); | ||
const context_1 = require("@loopback/context"); | ||
@@ -25,7 +26,8 @@ const debugFactory = require("debug"); | ||
const types_1 = require("./types"); | ||
const authentication_1 = require("@loopback/authentication"); | ||
const debug = debugFactory('loopback:authorization:interceptor'); | ||
let AuthorizationInterceptor = class AuthorizationInterceptor { | ||
constructor(authorizers) { | ||
constructor(authorizers, options = {}) { | ||
this.authorizers = authorizers; | ||
this.options = Object.assign({ defaultDecision: types_1.AuthorizationDecision.DENY, precedence: types_1.AuthorizationDecision.DENY }, options); | ||
debug('Authorization options', this.options); | ||
} | ||
@@ -58,7 +60,15 @@ value() { | ||
let authorizers = await loadAuthorizers(invocationCtx, metadata.voters || []); | ||
let finalDecision = this.options.defaultDecision; | ||
authorizers = authorizers.concat(this.authorizers); | ||
for (const fn of authorizers) { | ||
const decision = await fn(authorizationCtx, metadata); | ||
debug('Decision', decision); | ||
// Reset the final decision if an explicit Deny or Allow is voted | ||
if (decision && decision !== types_1.AuthorizationDecision.ABSTAIN) { | ||
finalDecision = decision; | ||
} | ||
// we can add another interceptor to process the error | ||
if (decision === types_1.AuthorizationDecision.DENY) { | ||
if (decision === types_1.AuthorizationDecision.DENY && | ||
this.options.precedence === types_1.AuthorizationDecision.DENY) { | ||
debug('Access denied'); | ||
const error = new types_1.AuthorizationError('Access denied'); | ||
@@ -68,3 +78,15 @@ error.statusCode = 401; | ||
} | ||
if (decision === types_1.AuthorizationDecision.ALLOW && | ||
this.options.precedence === types_1.AuthorizationDecision.ALLOW) { | ||
debug('Access allowed'); | ||
break; | ||
} | ||
} | ||
debug('Final decision', finalDecision); | ||
// Handle the final decision | ||
if (finalDecision === types_1.AuthorizationDecision.DENY) { | ||
const error = new types_1.AuthorizationError('Access denied'); | ||
error.statusCode = 401; | ||
throw error; | ||
} | ||
return next(); | ||
@@ -76,3 +98,4 @@ } | ||
__param(0, context_1.inject(context_1.filterByTag(keys_1.AuthorizationTags.AUTHORIZER))), | ||
__metadata("design:paramtypes", [Array]) | ||
__param(1, context_1.config({ fromBinding: keys_1.AuthorizationBindings.COMPONENT })), | ||
__metadata("design:paramtypes", [Array, Object]) | ||
], AuthorizationInterceptor); | ||
@@ -79,0 +102,0 @@ exports.AuthorizationInterceptor = AuthorizationInterceptor; |
@@ -0,1 +1,4 @@ | ||
import { BindingKey } from '@loopback/core'; | ||
import { AuthorizationComponent } from './authorization-component'; | ||
import { AuthorizationMetadata } from './types'; | ||
/** | ||
@@ -5,3 +8,4 @@ * Binding keys used by authorization component. | ||
export declare namespace AuthorizationBindings { | ||
const METADATA = "authorization.operationMetadata"; | ||
const METADATA: BindingKey<AuthorizationMetadata>; | ||
const COMPONENT: BindingKey<AuthorizationComponent>; | ||
} | ||
@@ -8,0 +12,0 @@ /** |
@@ -7,2 +7,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const core_1 = require("@loopback/core"); | ||
/** | ||
@@ -13,3 +14,4 @@ * Binding keys used by authorization component. | ||
(function (AuthorizationBindings) { | ||
AuthorizationBindings.METADATA = 'authorization.operationMetadata'; | ||
AuthorizationBindings.METADATA = core_1.BindingKey.create('authorization.operationMetadata'); | ||
AuthorizationBindings.COMPONENT = core_1.BindingKey.create(`${core_1.CoreBindings.COMPONENTS}.AuthorizationComponent`); | ||
})(AuthorizationBindings = exports.AuthorizationBindings || (exports.AuthorizationBindings = {})); | ||
@@ -16,0 +18,0 @@ /** |
@@ -220,1 +220,16 @@ import { BindingAddress, InvocationContext } from '@loopback/context'; | ||
} | ||
export interface AuthorizationOptions { | ||
/** | ||
* Default decision if all authorizers vote for ABSTAIN | ||
* If not set, default to `AuthorizationDecision.DENY` | ||
*/ | ||
defaultDecision?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW; | ||
/** | ||
* Controls if Allow/Deny vote takes precedence and override other votes. | ||
* If not set, default to `AuthorizationDecision.DENY`. | ||
* | ||
* Once a vote matches the `precedence`, it becomes the final decision. The | ||
* rest of votes will be skipped. | ||
*/ | ||
precedence?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW; | ||
} |
{ | ||
"name": "@loopback/authorization", | ||
"version": "0.1.2", | ||
"version": "0.2.0", | ||
"description": "A LoopBack component for authorization support.", | ||
@@ -25,10 +25,10 @@ "engines": { | ||
"dependencies": { | ||
"@loopback/context": "^1.21.3", | ||
"@loopback/core": "^1.9.2", | ||
"@loopback/context": "^1.21.4", | ||
"@loopback/core": "^1.9.3", | ||
"debug": "^4.1.1" | ||
}, | ||
"devDependencies": { | ||
"@loopback/authentication": "^2.1.10", | ||
"@loopback/build": "^2.0.7", | ||
"@loopback/testlab": "^1.7.3", | ||
"@loopback/authentication": "^2.1.11", | ||
"@loopback/build": "^2.0.8", | ||
"@loopback/testlab": "^1.7.4", | ||
"@types/debug": "^4.1.4", | ||
@@ -55,3 +55,3 @@ "@types/node": "10.14.15", | ||
}, | ||
"gitHead": "535f77559c5d8e8500a6378bc2ea1e569beb8bb6" | ||
"gitHead": "e17358d04cf9986fc692fdea37b582111932551d" | ||
} |
@@ -45,2 +45,50 @@ # @loopback/authorization | ||
## Decision matrix | ||
The final decision is controlled by voting results from authorizers and options | ||
for the authorization component. | ||
The following table illustrates the decision matrix with 3 voters and | ||
corresponding options. | ||
| Vote #1 | Vote # 2 | Vote #3 | Options | Final Decision | | ||
| ------- | -------- | ------- | ------------------------ | -------------- | | ||
| Deny | Deny | Deny | **any** | Deny | | ||
| Allow | Allow | Allow | **any** | Allow | | ||
| Abstain | Allow | Abstain | **any** | Allow | | ||
| Abstain | Deny | Abstain | **any** | Deny | | ||
| Deny | Allow | Abstain | {precedence: Deny} | Deny | | ||
| Deny | Allow | Abstain | {precedence: Allow} | Allow | | ||
| Allow | Abstain | Deny | {precedence: Deny} | Deny | | ||
| Allow | Abstain | Deny | {precedence: Allow} | Allow | | ||
| Abstain | Abstain | Abstain | {defaultDecision: Deny} | Deny | | ||
| Abstain | Abstain | Abstain | {defaultDecision: Allow} | Allow | | ||
The `options` is described as follows: | ||
```ts | ||
export interface AuthorizationOptions { | ||
/** | ||
* Default decision if all authorizers vote for ABSTAIN | ||
*/ | ||
defaultDecision?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW; | ||
/** | ||
* Controls if Allow/Deny vote takes precedence and override other votes | ||
*/ | ||
precedence?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW; | ||
} | ||
``` | ||
The authorization component can be configured with options: | ||
```ts | ||
const options: AuthorizationOptions = { | ||
precedence: AuthorizationDecisions.DENY; | ||
defaultDecision: AuthorizationDecisions.DENY; | ||
} | ||
const binding = app.component(AuthorizationComponent); | ||
app.configure(binding.key).to(options); | ||
``` | ||
## Installation | ||
@@ -47,0 +95,0 @@ |
@@ -6,7 +6,14 @@ // Copyright IBM Corp. 2018. All Rights Reserved. | ||
import {Component, createBindingFromClass} from '@loopback/core'; | ||
import { | ||
bind, | ||
Component, | ||
ContextTags, | ||
createBindingFromClass, | ||
} from '@loopback/core'; | ||
import {AuthorizationInterceptor} from './authorize-interceptor'; | ||
import {AuthorizationBindings} from './keys'; | ||
@bind({tags: {[ContextTags.KEY]: AuthorizationBindings.COMPONENT.key}}) | ||
export class AuthorizationComponent implements Component { | ||
bindings = [createBindingFromClass(AuthorizationInterceptor)]; | ||
} |
@@ -6,2 +6,3 @@ // Copyright IBM Corp. 2019. All Rights Reserved. | ||
import {AuthenticationBindings, UserProfile} from '@loopback/authentication'; | ||
import { | ||
@@ -11,2 +12,4 @@ asGlobalInterceptor, | ||
BindingAddress, | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
config, | ||
Context, | ||
@@ -22,11 +25,11 @@ filterByTag, | ||
import {getAuthorizationMetadata} from './decorators/authorize'; | ||
import {AuthorizationTags} from './keys'; | ||
import {AuthorizationBindings, AuthorizationTags} from './keys'; | ||
import { | ||
AuthorizationContext, | ||
AuthorizationDecision, | ||
AuthorizationError, | ||
AuthorizationOptions, | ||
Authorizer, | ||
AuthorizationError, | ||
Principal, | ||
} from './types'; | ||
import {AuthenticationBindings, UserProfile} from '@loopback/authentication'; | ||
@@ -37,6 +40,17 @@ const debug = debugFactory('loopback:authorization:interceptor'); | ||
export class AuthorizationInterceptor implements Provider<Interceptor> { | ||
private options: AuthorizationOptions; | ||
constructor( | ||
@inject(filterByTag(AuthorizationTags.AUTHORIZER)) | ||
private authorizers: Authorizer[], | ||
) {} | ||
@config({fromBinding: AuthorizationBindings.COMPONENT}) | ||
options: AuthorizationOptions = {}, | ||
) { | ||
this.options = { | ||
defaultDecision: AuthorizationDecision.DENY, | ||
precedence: AuthorizationDecision.DENY, | ||
...options, | ||
}; | ||
debug('Authorization options', this.options); | ||
} | ||
@@ -84,7 +98,17 @@ value(): Interceptor { | ||
let finalDecision = this.options.defaultDecision; | ||
authorizers = authorizers.concat(this.authorizers); | ||
for (const fn of authorizers) { | ||
const decision = await fn(authorizationCtx, metadata); | ||
debug('Decision', decision); | ||
// Reset the final decision if an explicit Deny or Allow is voted | ||
if (decision && decision !== AuthorizationDecision.ABSTAIN) { | ||
finalDecision = decision; | ||
} | ||
// we can add another interceptor to process the error | ||
if (decision === AuthorizationDecision.DENY) { | ||
if ( | ||
decision === AuthorizationDecision.DENY && | ||
this.options.precedence === AuthorizationDecision.DENY | ||
) { | ||
debug('Access denied'); | ||
const error = new AuthorizationError('Access denied'); | ||
@@ -94,3 +118,17 @@ error.statusCode = 401; | ||
} | ||
if ( | ||
decision === AuthorizationDecision.ALLOW && | ||
this.options.precedence === AuthorizationDecision.ALLOW | ||
) { | ||
debug('Access allowed'); | ||
break; | ||
} | ||
} | ||
debug('Final decision', finalDecision); | ||
// Handle the final decision | ||
if (finalDecision === AuthorizationDecision.DENY) { | ||
const error = new AuthorizationError('Access denied'); | ||
error.statusCode = 401; | ||
throw error; | ||
} | ||
return next(); | ||
@@ -97,0 +135,0 @@ } |
@@ -6,2 +6,6 @@ // Copyright IBM Corp. 2018. All Rights Reserved. | ||
import {BindingKey, CoreBindings} from '@loopback/core'; | ||
import {AuthorizationComponent} from './authorization-component'; | ||
import {AuthorizationMetadata} from './types'; | ||
/** | ||
@@ -11,3 +15,9 @@ * Binding keys used by authorization component. | ||
export namespace AuthorizationBindings { | ||
export const METADATA = 'authorization.operationMetadata'; | ||
export const METADATA = BindingKey.create<AuthorizationMetadata>( | ||
'authorization.operationMetadata', | ||
); | ||
export const COMPONENT = BindingKey.create<AuthorizationComponent>( | ||
`${CoreBindings.COMPONENTS}.AuthorizationComponent`, | ||
); | ||
} | ||
@@ -14,0 +24,0 @@ |
@@ -255,1 +255,17 @@ // Copyright IBM Corp. 2018. All Rights Reserved. | ||
} | ||
export interface AuthorizationOptions { | ||
/** | ||
* Default decision if all authorizers vote for ABSTAIN | ||
* If not set, default to `AuthorizationDecision.DENY` | ||
*/ | ||
defaultDecision?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW; | ||
/** | ||
* Controls if Allow/Deny vote takes precedence and override other votes. | ||
* If not set, default to `AuthorizationDecision.DENY`. | ||
* | ||
* Once a vote matches the `precedence`, it becomes the final decision. The | ||
* rest of votes will be skipped. | ||
*/ | ||
precedence?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
63890
1385
167
1
Updated@loopback/context@^1.21.4
Updated@loopback/core@^1.9.3