Skip to content

Commit 0a8e321

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

5 files changed

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

0 commit comments

Comments
 (0)