Skip to content

Commit 7034bb0

Browse files
committed
feat(authorization): add support for default authorization metadata
Implements #3681
1 parent 0debabe commit 7034bb0

File tree

3 files changed

+80
-10
lines changed

3 files changed

+80
-10
lines changed

packages/authorization/src/__tests__/acceptance/authorization.options.acceptance.ts

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,29 +72,58 @@ const matrix3: DecisionMatrix = [
7272
[{defaultDecision: Allow}, [Abstain, Abstain, Abstain], Allow],
7373
];
7474

75+
const matrix4: DecisionMatrix = [
76+
[{defaultMetadata: {}}, [Abstain, Abstain, Abstain], Deny],
77+
[
78+
{defaultMetadata: {}, defaultDecision: Deny},
79+
[Abstain, Abstain, Abstain],
80+
Deny,
81+
],
82+
[
83+
{defaultMetadata: {}, defaultDecision: Allow},
84+
[Abstain, Abstain, Abstain],
85+
Allow,
86+
],
87+
];
88+
89+
// Decisions controlled by options.defaultDecision
90+
const matrix5: DecisionMatrix = [
91+
[{}, [Abstain, Abstain, Abstain], Allow],
92+
[{defaultDecision: Deny}, [Abstain, Abstain, Abstain], Allow],
93+
[{defaultDecision: Allow}, [Abstain, Abstain, Abstain], Allow],
94+
];
95+
7596
describe('Authorization', () => {
7697
let app: Application;
7798
let controller: OrderController;
7899
let reqCtx: Context;
79100

80101
it('always use explicit decisions', async () => {
81-
await runTest(matrix1);
102+
await testCancelOrder(matrix1);
82103
});
83104

84105
it('honors decisions and options.precedence', async () => {
85-
await runTest(matrix2);
106+
await testCancelOrder(matrix2);
86107
});
87108

88109
it('honors decisions and options.defaultDecision', async () => {
89-
await runTest(matrix3);
110+
await testCancelOrder(matrix3);
111+
});
112+
113+
it('honors decisions with options.defaultMetadata', async () => {
114+
await testPlaceOrder(matrix4);
90115
});
91116

92-
async function runTest(matrix: DecisionMatrix) {
117+
it('honors decisions without options.defaultMetadata', async () => {
118+
await testPlaceOrder(matrix5);
119+
});
120+
121+
async function testCancelOrder(matrix: DecisionMatrix) {
93122
let index = 0;
94123
for (const row of matrix) {
95124
givenRequestContext();
96125
setupAuthorization(row[0], ...row[1]);
97-
const finalDecision = await run();
126+
const finalDecision = await cancelOrder();
98127
const expectedDecision = row[2];
99128
expect(`${index}:${finalDecision}`).to.equal(
100129
`${index}:${expectedDecision}`,
@@ -103,10 +132,35 @@ describe('Authorization', () => {
103132
}
104133
}
105134

106-
async function run() {
135+
async function testPlaceOrder(matrix: DecisionMatrix) {
136+
let index = 0;
137+
for (const row of matrix) {
138+
givenRequestContext();
139+
setupAuthorization(row[0], ...row[1]);
140+
const finalDecision = await placeOrder();
141+
const expectedDecision = row[2];
142+
expect(`${index}:${finalDecision}`).to.equal(
143+
`${index}:${expectedDecision}`,
144+
);
145+
index++;
146+
}
147+
}
148+
149+
async function cancelOrder() {
150+
let finalDecision = Deny;
151+
try {
152+
await invokeMethod(controller, 'cancelOrder', reqCtx, ['order-01']);
153+
finalDecision = Allow;
154+
} catch (err) {
155+
finalDecision = Deny;
156+
}
157+
return finalDecision;
158+
}
159+
160+
async function placeOrder() {
107161
let finalDecision = Deny;
108162
try {
109-
await invokeMethod(controller, 'handleOrder', reqCtx, ['order-01']);
163+
await invokeMethod(controller, 'placeOrder', reqCtx, ['prod-01', 10]);
110164
finalDecision = Allow;
111165
} catch (err) {
112166
finalDecision = Deny;
@@ -116,9 +170,15 @@ describe('Authorization', () => {
116170

117171
class OrderController {
118172
@authorize({})
119-
async handleOrder(orderId: string) {
173+
async cancelOrder(orderId: string) {
120174
return orderId;
121175
}
176+
177+
// This method is not decorated with `@authorize`. The decision depends on
178+
// authorizationOptions.defaultMetadata
179+
async placeOrder(productId: string, quantity: number) {
180+
return '001';
181+
}
122182
}
123183

124184
function setupAuthorization(

packages/authorization/src/authorize-interceptor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,16 @@ export class AuthorizationInterceptor implements Provider<Interceptor> {
6060

6161
async intercept(invocationCtx: InvocationContext, next: Next) {
6262
const description = debug.enabled ? invocationCtx.description : '';
63-
const metadata = getAuthorizationMetadata(
63+
let metadata = getAuthorizationMetadata(
6464
invocationCtx.target,
6565
invocationCtx.methodName,
6666
);
6767
if (!metadata) {
68-
debug('No authorization metadata is found %s', description);
68+
debug('No authorization metadata is found for %s', description);
69+
}
70+
metadata = metadata || this.options.defaultMetadata;
71+
if (!metadata) {
72+
debug('Authorization is skipped for %s', description);
6973
const result = await next();
7074
return result;
7175
}

packages/authorization/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,10 @@ export interface AuthorizationOptions {
177177
* rest of votes will be skipped.
178178
*/
179179
precedence?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW;
180+
/**
181+
* Default authorization metadata if a method is not decorated with `@authorize`.
182+
* If not set, no authorization will be enforced for those methods that are
183+
* not associated with authorization metadata.
184+
*/
185+
defaultMetadata?: AuthorizationMetadata;
180186
}

0 commit comments

Comments
 (0)