|
| 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