Skip to content

Commit b1abf30

Browse files
committed
feat: Create authorizer that handles simple policy checks
This prevents calling the expensive authorizer when not needed.
1 parent f982805 commit b1abf30

5 files changed

Lines changed: 443 additions & 3 deletions

File tree

packages/uma/config/policies/authorizers/default.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,17 @@
5050
}
5151
],
5252
"fallback": {
53-
"@id": "urn:uma:default:OdrlAuthorizer",
54-
"@type": "OdrlAuthorizer",
53+
"@id": "urn:uma:default:SimpleOdrlAuthorizer",
54+
"@type": "SimpleOdrlAuthorizer",
5555
"policies": {
5656
"@id": "urn:uma:default:RulesStorage",
5757
"@type": "FileBackupUCRulesStorage",
5858
"filePath": { "@id": "urn:uma:variables:backupFilePath" }
59+
},
60+
"authorizer": {
61+
"@id": "urn:uma:default:OdrlAuthorizer",
62+
"@type": "OdrlAuthorizer",
63+
"policies": { "@id": "urn:uma:default:RulesStorage" }
5964
}
6065
},
6166
"registrationStore": { "@id": "urn:uma:default:ResourceRegistrationStore" }

packages/uma/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export * from './policies/authorizers/AllAuthorizer';
3333
export * from './policies/authorizers/NamespacedAuthorizer';
3434
export * from './policies/authorizers/NoneAuthorizer';
3535
export * from './policies/authorizers/OdrlAuthorizer';
36+
export * from './policies/authorizers/SimpleOdrlAuthorizer';
3637
export * from './policies/authorizers/WebIdAuthorizer';
3738

3839
// Contracts
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { NamedNode } from '@rdfjs/types';
2+
import { getLoggerFor } from 'global-logger-factory';
3+
import { DataFactory as DF, Quad_Subject, Store } from 'n3';
4+
import { ODRL } from 'odrl-evaluator';
5+
import { CLIENTID, WEBID } from '../../credentials/Claims';
6+
import { ClaimSet } from '../../credentials/ClaimSet';
7+
import { UCRulesStorage } from '../../ucp/storage/UCRulesStorage';
8+
import { Permission } from '../../views/Permission';
9+
import { Authorizer } from './Authorizer';
10+
11+
const ANONYMOUS = DF.namedNode('urn:solidlab:uma:id:anonymous');
12+
13+
// TODO: Mostly copied from ODRL Authorizer.
14+
// One change: append and write are mapped to modify.
15+
// Should be handled by RS.
16+
const scopeCssToOdrl: Map<string, string> = new Map();
17+
scopeCssToOdrl.set('urn:example:css:modes:read','http://www.w3.org/ns/odrl/2/read');
18+
scopeCssToOdrl.set('urn:example:css:modes:append','http://www.w3.org/ns/odrl/2/modify');
19+
scopeCssToOdrl.set('urn:example:css:modes:create','http://www.w3.org/ns/odrl/2/create');
20+
scopeCssToOdrl.set('urn:example:css:modes:delete','http://www.w3.org/ns/odrl/2/delete');
21+
scopeCssToOdrl.set('urn:example:css:modes:write','http://www.w3.org/ns/odrl/2/modify');
22+
23+
const dateComparators: NodeJS.Dict<(a: Date, b: Date) => boolean> = {
24+
[ODRL.lt]: (a: Date, b: Date) => a < b,
25+
[ODRL.lteq]: (a: Date, b: Date) => a <= b,
26+
[ODRL.eq]: (a: Date, b: Date) => a === b,
27+
[ODRL.gt]: (a: Date, b: Date) => a > b,
28+
[ODRL.gteq]: (a: Date, b: Date) => a >= b,
29+
};
30+
31+
/**
32+
* A simple authorizer that can handle basic ODRL policies with direct permissions and prohibitions,
33+
* without any complex constraints or inheritance.
34+
* If a request doesn't match any permission or prohibition
35+
* in the policies it evaluates, it falls back to a provided authorizer.
36+
*/
37+
export class SimpleOdrlAuthorizer implements Authorizer {
38+
protected readonly logger = getLoggerFor(this);
39+
40+
public constructor(
41+
protected readonly policies: UCRulesStorage,
42+
protected readonly authorizer: Authorizer,
43+
) {}
44+
45+
public async permissions(claims: ClaimSet, query?: Permission[]): Promise<Permission[]> {
46+
if (!query) {
47+
return this.authorizer.permissions(claims, query);
48+
}
49+
50+
const store = await this.policies.getStore();
51+
52+
let permissions: Permission[] = [];
53+
for (const { resource_id, resource_scopes } of query) {
54+
for (const scope of resource_scopes) {
55+
const result = this.getPermissions(store, claims, resource_id, scope);
56+
if (!result) {
57+
// Too difficult to handle internally so need to call complete authorizer
58+
return this.authorizer.permissions(claims, query);
59+
}
60+
61+
permissions.push(...result);
62+
}
63+
}
64+
return permissions;
65+
}
66+
67+
protected getPermissions(policies: Store, claims: ClaimSet, resource: string, scope: string):
68+
Permission[] | undefined {
69+
this.logger.info(`Evaluating Request ${scope}, ${resource} with claims ${JSON.stringify(claims)}`);
70+
const targets = [ DF.namedNode(resource), ...policies.getObjects(resource, ODRL.terms.partOf, null)];
71+
let rules = targets.flatMap(target => policies.getSubjects(ODRL.terms.target, target, null));
72+
if (rules.length === 0) {
73+
this.logger.warn('Rejecting request because no rules with a matching target or asset collection were found');
74+
return [];
75+
}
76+
77+
let revertScopeToCssMode = scope.startsWith('urn:example:css:modes:');
78+
const oldScope = scope;
79+
if (revertScopeToCssMode) {
80+
scope = scopeCssToOdrl.get(scope) ?? scope;
81+
}
82+
83+
// Note that this does not catch refined actions or superclasses of actions
84+
rules = rules.filter(rule => policies.has(DF.quad(rule, ODRL.terms.action, DF.namedNode(scope))));
85+
if (rules.length === 0) {
86+
this.logger.warn('Rejecting request because no rules with a matching action were found');
87+
return [];
88+
}
89+
90+
let user = claims[WEBID];
91+
let assignees: NamedNode[] = [ ANONYMOUS ];
92+
if (typeof user === 'string') {
93+
const userNode = DF.namedNode(user);
94+
assignees.push(userNode);
95+
assignees.push(...(policies.getObjects(user, ODRL.terms.partOf, null) as NamedNode[]));
96+
}
97+
rules = rules.filter(rule => {
98+
const ruleAssignees = policies.getObjects(rule, ODRL.terms.assignee, null);
99+
if (ruleAssignees.length === 0) {
100+
// Public access
101+
return true;
102+
}
103+
return ruleAssignees.some(ruleAssignee => assignees.some(assignee => assignee.equals(ruleAssignee)));
104+
});
105+
this.logger.warn('Rejecting request because no rules with a matching assignee or party collection were found');
106+
if (rules.length === 0) {
107+
return [];
108+
}
109+
110+
// Check simple constraints
111+
const validRules: Quad_Subject[] = [];
112+
for (const rule of rules) {
113+
const constraintResponse = this.validateConstraints(rule, policies, claims);
114+
if (constraintResponse === true) {
115+
validRules.push(rule);
116+
} else if (constraintResponse === undefined) {
117+
return;
118+
}
119+
}
120+
if (validRules.length === 0) {
121+
this.logger.warn('Rejecting request because no rules with fulfilled constraints were found');
122+
return [];
123+
}
124+
125+
const predicates = validRules.map(rule => policies.getPredicates(null, rule, null));
126+
for (const rulePredicates of predicates) {
127+
if (rulePredicates.length === 0) {
128+
return;
129+
}
130+
if (rulePredicates.some(predicate => predicate.equals(ODRL.terms.prohibition))) {
131+
this.logger.warn('Rejecting request because only matching prohibitions were found');
132+
return [];
133+
}
134+
// This implies we have an unsupported type of rule
135+
if (!rulePredicates.some(predicate => predicate.equals(ODRL.terms.permission))) {
136+
return;
137+
}
138+
}
139+
140+
return [{
141+
resource_id: resource,
142+
resource_scopes: [ oldScope ],
143+
}];
144+
}
145+
146+
// TODO: 3 modes: valid, not valid, too complicated
147+
/**
148+
* Determines if all constraints for the given rule are valid.
149+
* Returns true if all constraints are valid, false if any constraint is not valid,
150+
* and undefined if any constraint is too complex to evaluate.
151+
* Only supports purpose (for client ID) and dateTime constraints.
152+
*/
153+
protected validateConstraints(rule: Quad_Subject, policies: Store, claims: ClaimSet): boolean | undefined {
154+
const constraints = policies.getObjects(rule, ODRL.terms.constraint, null).map(constraint => ({
155+
leftOperand: policies.getObjects(constraint, ODRL.terms.leftOperand, null)[0],
156+
operator: policies.getObjects(constraint, ODRL.terms.operator, null)[0],
157+
rightOperand: policies.getObjects(constraint, ODRL.terms.rightOperand, null)[0],
158+
}));
159+
// If any of these are undefined this is too complex to handle here
160+
if (constraints.some(({ leftOperand, operator, rightOperand }) => !leftOperand || !operator || !rightOperand)) {
161+
return;
162+
}
163+
for (const constraint of constraints) {
164+
// Return undefined if any of these are too complex or unknown
165+
// TODO: because of weird hack described in OdrlAuthorizer, needs to change to term that makes more sense
166+
if (constraint.leftOperand.equals(ODRL.terms.purpose)) {
167+
if (!constraint.operator.equals(ODRL.terms.eq)) {
168+
return false;
169+
}
170+
const clientId = claims[CLIENTID];
171+
if (typeof clientId !== 'string' || constraint.rightOperand.value !== clientId) {
172+
return false;
173+
}
174+
} else if (constraint.leftOperand.equals(ODRL.terms.dateTime)) {
175+
const comparisonDate = new Date(constraint.rightOperand.value);
176+
const comparator = dateComparators[constraint.operator.value];
177+
if (!comparator) {
178+
return false;
179+
}
180+
if (!comparator(new Date(), comparisonDate)) {
181+
return false;
182+
}
183+
} else {
184+
// Unsupported constraint
185+
return;
186+
}
187+
}
188+
return true;
189+
}
190+
}

0 commit comments

Comments
 (0)