11import { createVocabulary , DC , getLoggerFor , RDF } from '@solid/community-server' ;
2- import { ODRL , UconRequest , UCRulesStorage } from '@solidlab/ucp' ;
3- import { generate_uuid } from 'koreografeye' ;
2+ import { basicPolicy , ODRL , UCPPolicy , UCRulesStorage } from '@solidlab/ucp' ;
43import { DataFactory , Literal , NamedNode , Quad_Subject , Store } from 'n3' ;
54import { ODRLEngineMultipleSteps , ODRLEvaluator } from 'odrl-evaluator'
65import { WEBID } from '../../credentials/Claims' ;
@@ -11,13 +10,30 @@ import { Authorizer } from './Authorizer';
1110
1211const { quad, namedNode, literal} = DataFactory
1312
13+ /**
14+ * Permission evaluation is performed as follows:
15+ *
16+ * 1. Conversion of Permission queries to ODRL Requests.
17+ * - A translation is performed to transform CSS actions to ODRL actions.
18+ * - One ODRL Request per Action and target Resource.
19+ *
20+ * 2. ODRL Evaluator performs ODRL Evaluation
21+ * - No policy selection is performed (all policies are inserted rather than all relevant).
22+ * - No conflict resolution strategy is present (Prohibition policies are ignored).
23+ * - No duties are checked.
24+ *
25+ * 3. Conversion from ODRL Policy Compliance Reports to Permissions
26+ * - Selecting the ODRL actions from Active Permission Reports
27+ * - Translation from ODRL actions to CSS actions
28+ */
1429export class OdrlAuthorizer implements Authorizer {
1530 protected readonly logger = getLoggerFor ( this ) ;
1631 private readonly odrlEvaluator : ODRLEvaluator ;
1732
1833 /**
1934 * Creates a OdrlAuthorizer enforcing policies using ODRL with the ODRL Evaluator.
2035 *
36+ *
2137 * @param policies - A store containing the ODRL policy rules.
2238 */
2339 constructor (
@@ -35,79 +51,66 @@ export class OdrlAuthorizer implements Authorizer {
3551 return [ ] ;
3652 }
3753
38- const requests : UconRequest [ ] = [ ] ;
39- for ( const { resource_id, resource_scopes} of query ) {
40-
41- if ( ! resource_id ) {
42- this . logger . warn ( 'The OdrlAuthorizer can only calculate permissions for explicit resources.' ) ;
43- continue ;
44- }
45-
46- // ODRL can only handle odrl actions
47- requests . push ( {
48- subject : typeof claims [ WEBID ] === 'string' ? claims [ WEBID ] : 'urn:solidlab:uma:id:anonymous' ,
49- resource : resource_id ,
50- action : resource_scopes ? transformActionsCssToOdrl ( resource_scopes ) : [ "http://www.w3.org/ns/odrl/2/use" ] ,
51- claims
52- } ) ;
53- }
54- const permissions : Permission [ ] = await Promise . all ( requests . map (
55- async ( request ) => {
56- const scopes_permitted = [ ] ;
57-
58- // prepare sotw
59- const sotw = new Store ( ) ;
60-
61- sotw . add ( quad ( namedNode ( 'http://example.com/request/currentTime' ) , namedNode ( 'http://purl.org/dc/terms/issued' ) , literal ( new Date ( ) . toISOString ( ) , namedNode ( "http://www.w3.org/2001/XMLSchema#dateTime" ) ) ) ) ;
62-
63- // prepare policy
64- const policyStore = ( await this . policies . getStore ( ) )
54+ // key value store for building the permissions to be granted on a resource
55+ const grantedPermissions : { [ key : string ] : string [ ] } = { } ;
6556
57+ // prepare policy
58+ const policyStore = ( await this . policies . getStore ( ) )
6659
67- for ( const action of request . action ) {
68- // prepare request
69- const req = new Store ( ) ;
70- const requestNode = generate_uuid ( ) ;
71- const permissionNode = generate_uuid ( ) ;
72- req . add ( quad ( requestNode , RDF . terms . type , ODRL . terms . Request ) ) ;
73- req . add ( quad ( requestNode , ODRL . terms . permission , permissionNode ) ) ;
60+ // prepare sotw
61+ const sotw = new Store ( ) ;
62+ sotw . add ( quad ( namedNode ( 'http://example.com/request/currentTime' ) , namedNode ( 'http://purl.org/dc/terms/issued' ) , literal ( new Date ( ) . toISOString ( ) , namedNode ( "http://www.w3.org/2001/XMLSchema#dateTime" ) ) ) ) ;
7463
75- req . add ( quad ( permissionNode , RDF . terms . type , ODRL . terms . Permission ) ) ;
76- req . add ( quad ( permissionNode , ODRL . terms . assignee , namedNode ( request . subject ) ) ) ;
77- req . add ( quad ( permissionNode , ODRL . terms . target , namedNode ( request . resource ) ) ) ;
78- req . add ( quad ( permissionNode , ODRL . terms . action , namedNode ( action ) ) ) ;
64+ const subject = typeof claims [ WEBID ] === 'string' ? claims [ WEBID ] : 'urn:solidlab:uma:id:anonymous' ;
7965
80- // evaluate policies
81- const reports = await this . odrlEvaluator . evaluate ( [ ...policyStore ] , [ ...req ] , [ ...sotw ] ) ;
82- const reportStore = new Store ( reports )
8366
84- // TODO: handle multiple reports -> possible to be generated
85- // NOTE: current strategy, add all actions of active reports generated by the request
86- // fetch active and attempted
87- // console.log(new Writer().quadsToString([...req]))
67+ for ( const { resource_id, resource_scopes} of query ) {
68+ if ( ! resource_id ) {
69+ this . logger . warn ( 'The OdrlAuthorizer can only calculate permissions for explicit resources.' ) ;
70+ continue ;
71+ }
8872
89- const PolicyReportNodes = reportStore . getSubjects ( RDF . type , CR . PolicyReport , null ) ;
90- for ( const policyReportNode of PolicyReportNodes ) {
91- const policyReport = parseComplianceReport ( policyReportNode , reportStore )
92- // console.log(new Writer().quadsToString([...policyStore]))
93- // console.log(new Writer().quadsToString([...sotw]))
94- // console.log(new Writer().quadsToString(reports))
95- // console.log(policyReport)
96- if ( policyReport . ruleReport . activationState === ActivationState . Active &&
97- policyReport . ruleReport . type === RuleReportType . PermissionReport ) {
98- scopes_permitted . push ( action )
73+ grantedPermissions [ resource_id ] = [ ] ;
74+ const actions = resource_scopes ? transformActionsCssToOdrl ( resource_scopes ) : [ "http://www.w3.org/ns/odrl/2/use" ]
75+ for ( const action of actions ) {
76+ this . logger . info ( `Evaluating Request [S R AR]: [${ subject } ${ resource_id } ${ action } ]` ) ;
77+ const requestPolicy : UCPPolicy = {
78+ type : ODRL . Request ,
79+ rules : [
80+ {
81+ action : action ,
82+ resource : resource_id ,
83+ requestingParty : subject
9984 }
100- }
101-
85+ ]
10286 }
103- // extract allowed scopes
104- return {
105- resource_id : request . resource ,
106- resource_scopes : transformActionsOdrlToCss ( scopes_permitted )
87+ const requestStore = basicPolicy ( requestPolicy ) . representation
88+ // evaluate policies
89+ const reports = await this . odrlEvaluator . evaluate (
90+ [ ...policyStore ] ,
91+ [ ...requestStore ] ,
92+ [ ...sotw ] ) ;
93+ const reportStore = new Store ( reports ) ;
94+
95+ // TODO: handle multiple reports -> possible to be generated
96+ // NOTE: current strategy, add all actions of active reports generated by the request
97+ // fetch active and attempted
98+ const PolicyReportNodes = reportStore . getSubjects ( RDF . type , CR . PolicyReport , null ) ;
99+ for ( const policyReportNode of PolicyReportNodes ) {
100+ const policyReport = parseComplianceReport ( policyReportNode , reportStore )
101+ if ( policyReport . ruleReport [ 0 ] . activationState === ActivationState . Active &&
102+ policyReport . ruleReport [ 0 ] . type === RuleReportType . PermissionReport ) {
103+ grantedPermissions [ resource_id ] . push ( action ) ;
104+ }
107105 }
108106 }
109- ) ) ;
110- // console.log(permissions)
107+ }
108+ const permissions : Permission [ ] = [ ]
109+ Object . keys ( grantedPermissions ) . forEach (
110+ resource_id => permissions . push ( {
111+ resource_id,
112+ resource_scopes : transformActionsOdrlToCss ( grantedPermissions [ resource_id ] )
113+ } ) ) ;
111114 return permissions ;
112115 }
113116
@@ -125,14 +128,21 @@ scopeCssToOdrl.set('urn:example:css:modes:write','http://www.w3.org/ns/odrl/2/wr
125128
126129const scopeOdrlToCss : Map < string , string > = new Map ( Array . from ( scopeCssToOdrl , entry => [ entry [ 1 ] , entry [ 0 ] ] ) ) ;
127130
131+ /**
132+ * Transform the Actions enforced by the Community Solid Server to equivalent ODRL Actions
133+ * @param actions
134+ */
128135function transformActionsCssToOdrl ( actions : string [ ] ) : string [ ] {
129136 // scopes come from UmaClient.ts -> see CSS package
130137
131138 // in UMAPermissionReader, only the last part of the URN will be used, divided by a colon
132139 // again, see CSS package
133140 return actions . map ( action => scopeCssToOdrl . get ( action ) ! ) ;
134141}
135-
142+ /**
143+ * Transform ODRL Actions to equivalent Actions enforced by the Community Solid Server
144+ * @param actions
145+ */
136146function transformActionsOdrlToCss ( actions : string [ ] ) : string [ ] {
137147 const cssActions = [ ]
138148 for ( const action of actions ) {
@@ -149,13 +159,14 @@ type PolicyReport = {
149159 created : Literal ;
150160 request : NamedNode ;
151161 policy : NamedNode ;
152- ruleReport : RuleReport ;
162+ ruleReport : RuleReport [ ] ;
153163}
154164type RuleReport = {
155165 id : NamedNode ;
156166 type : RuleReportType ;
157167 activationState : ActivationState
158- // TODO: others
168+ rule : NamedNode ;
169+ requestedRule : NamedNode ;
159170 premiseReport : PremiseReport [ ]
160171}
161172
@@ -189,35 +200,58 @@ enum ActivationState {
189200 Active = 'http://example.com/report/temp/Active' ,
190201 Inactive = 'http://example.com/report/temp/Inactive' ,
191202}
192- function parseComplianceReport ( identifier : Quad_Subject , store : Store ) :PolicyReport {
193- const exists = store . getQuads ( identifier , RDF . type , CR . PolicyReport , null ) . length === 1 ;
203+
204+ /**
205+ * Parses an ODRL Compliance Report Model into a {@link PolicyReport}.
206+ * @param identifier
207+ * @param store
208+ */
209+ function parseComplianceReport ( identifier : Quad_Subject , store : Store ) : PolicyReport {
210+ const exists = store . getQuads ( identifier , RDF . type , CR . PolicyReport , null ) . length === 1 ;
194211 if ( ! exists ) { throw Error ( `No Policy Report found with: ${ identifier } .` ) ; }
195- const ruleReportNode = store . getQuads ( identifier , CR . ruleReport , null , null ) [ 0 ] . object as NamedNode ;
196- const premises :PremiseReport [ ] = [ ] ;
197- const premiseNodes = store . getObjects ( ruleReportNode , CR . premiseReport , null ) as NamedNode [ ] ;
198- for ( const premiseNode of premiseNodes ) {
199- premises . push ( {
200- id : premiseNode ,
201- type : store . getObjects ( premiseNode , RDF . type , null ) [ 0 ] . value as PremiseReportType ,
202- premiseReport :[ ] , // Note: won't get nested premises this way
203- satisfactionState : store . getObjects ( premiseNode , CR . satisfactionState , null ) [ 0 ] . value as SatisfactionState
204- } )
205- }
206- const ruleReport :RuleReport = {
207- id : ruleReportNode ,
208- type : store . getObjects ( ruleReportNode , RDF . type , null ) [ 0 ] . value as RuleReportType ,
209- activationState : store . getObjects ( ruleReportNode , CR . activationState , null ) [ 0 ] . value as ActivationState ,
210- premiseReport : premises
211- }
212+ const ruleReportNodes = store . getObjects ( identifier , CR . ruleReport , null ) as NamedNode [ ] ;
213+
212214 return {
213215 id : identifier as NamedNode ,
214216 created : store . getObjects ( identifier , DC . namespace + "created" , null ) [ 0 ] as Literal ,
215217 policy : store . getObjects ( identifier , CR . policy , null ) [ 0 ] as NamedNode ,
216218 request : store . getObjects ( identifier , CR . policyRequest , null ) [ 0 ] as NamedNode ,
217- ruleReport : ruleReport
219+ ruleReport : ruleReportNodes . map ( ruleReportNode => parseRuleReport ( ruleReportNode , store ) )
218220 }
219221}
220222
223+ /**
224+ * Parses Rule Reports from a Compliance Report, including its premises
225+ * @param identifier
226+ * @param store
227+ */
228+ function parseRuleReport ( identifier : Quad_Subject , store : Store ) : RuleReport {
229+ const premiseNodes = store . getObjects ( identifier , CR . premiseReport , null ) as NamedNode [ ] ;
230+ return {
231+ id : identifier as NamedNode ,
232+ type : store . getObjects ( identifier , RDF . type , null ) [ 0 ] . value as RuleReportType ,
233+ activationState : store . getObjects ( identifier , CR . activationState , null ) [ 0 ] . value as ActivationState ,
234+ requestedRule : store . getObjects ( identifier , CR . ruleRequest , null ) [ 0 ] as NamedNode ,
235+ rule : store . getObjects ( identifier , CR . rule , null ) [ 0 ] as NamedNode ,
236+ premiseReport : premiseNodes . map ( ( prem ) => parsePremiseReport ( prem , store ) )
237+ }
238+ }
239+
240+ /**
241+ * Parses Premise Reports, including premises of a Premise Report itself.
242+ * Note that if for some reason there are circular premise reports, this will result into an infinite loop
243+ * @param identifier
244+ * @param store
245+ */
246+ function parsePremiseReport ( identifier : Quad_Subject , store : Store ) : PremiseReport {
247+ const nestedPremises = store . getObjects ( identifier , CR . PremiseReport , null ) as NamedNode [ ] ;
248+ return {
249+ id : identifier as NamedNode ,
250+ type : store . getObjects ( identifier , RDF . type , null ) [ 0 ] . value as PremiseReportType ,
251+ premiseReport : nestedPremises . map ( ( prem ) => parsePremiseReport ( prem , store ) ) ,
252+ satisfactionState : store . getObjects ( identifier , CR . satisfactionState , null ) [ 0 ] . value as SatisfactionState
253+ }
254+ }
221255const CR = createVocabulary ( 'http://example.com/report/temp/' ,
222256 'PolicyReport' ,
223257 'RuleReport' ,
0 commit comments