diff --git a/packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts index 1f08c6f5..b7709050 100644 --- a/packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts @@ -55,6 +55,7 @@ export class SimpleOdrlAuthorizer implements Authorizer { let permissions: Permission[] = []; for (const { resource_id, resource_scopes } of query) { + const allowedScopes: string[] = []; for (const scope of resource_scopes) { const result = this.getPermissions(store, claims, resource_id, scope); if (!result) { @@ -62,14 +63,20 @@ export class SimpleOdrlAuthorizer implements Authorizer { return this.authorizer.permissions(claims, query); } - permissions.push(...result); + allowedScopes.push(...result); + } + if (allowedScopes.length > 0) { + permissions.push({ + resource_id, + resource_scopes: allowedScopes, + }); } } return permissions; } protected getPermissions(policies: ReadOnlyStore, claims: ClaimSet, resource: string, scope: string): - Permission[] | undefined { + string[] | undefined { this.logger.info(`Evaluating Request ${scope}, ${resource} with claims ${JSON.stringify(claims)}`); const targets = [ DF.namedNode(resource), ...policies.getObjects(resource, ODRL.terms.partOf, null)]; let rules = targets.flatMap(target => policies.getSubjects(ODRL.terms.target, target, null)); @@ -145,10 +152,7 @@ export class SimpleOdrlAuthorizer implements Authorizer { } } - return [{ - resource_id: resource, - resource_scopes: [ oldScope ], - }]; + return [ oldScope ]; } // TODO: 3 modes: valid, not valid, too complicated diff --git a/packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts b/packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts index 67b29622..019a44e4 100644 --- a/packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts +++ b/packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts @@ -243,16 +243,17 @@ describe('SimpleOdrlAuthorizer', () => { const scope2 = 'urn:example:css:modes:write'; const odrlScope2 = 'http://www.w3.org/ns/odrl/2/modify'; const multiQuery: Permission[] = [ - { resource_id: resource, resource_scopes: [scope] }, + { resource_id: resource, resource_scopes: [scope, odrlScope] }, { resource_id: resource2, resource_scopes: [scope2] }, ]; addRule({}); + addRule({ action: odrlScope }); addRule({ target: resource2, action: odrlScope2 }); const result = await authorizer.permissions({}, multiQuery); expect(result).toEqual([ - { resource_id: resource, resource_scopes: [scope] }, + { resource_id: resource, resource_scopes: [scope, odrlScope] }, { resource_id: resource2, resource_scopes: [scope2] }, ]); expect(fallback.permissions).not.toHaveBeenCalled(); diff --git a/test/integration/Partial.test.ts b/test/integration/Partial.test.ts index 9b0a1595..cd325807 100644 --- a/test/integration/Partial.test.ts +++ b/test/integration/Partial.test.ts @@ -1,5 +1,6 @@ import { App } from '@solid/community-server'; import { setGlobalLoggerFactory, WinstonLoggerFactory } from 'global-logger-factory'; +import { decodeJwt } from 'jose'; import { createServer, Server } from 'node:http'; import path from 'node:path'; import { getPorts, instantiateFromConfig } from '../util/ServerUtil'; @@ -18,10 +19,12 @@ interface UmaConfig { describe('A server with partial results enabled', (): void => { const owner = 'http://example.com/alice#me'; const user = `http://example.com/bob`; + const aliceContainer = `http://localhost:${rsPort}/alice/`; const aliceResource = `http://localhost:${rsPort}/alice/data`; const bobResource = `http://localhost:${rsPort}/bob/data`; const readScope = 'http://www.w3.org/ns/odrl/2/read'; - const writeScope = 'http://www.w3.org/ns/odrl/2/write'; + const createScope = 'http://www.w3.org/ns/odrl/2/create'; + const modifyScope = 'http://www.w3.org/ns/odrl/2/modify'; let umaApp: App; let rsServer: Server; let umaConfig: UmaConfig; @@ -77,6 +80,20 @@ describe('A server with partial results enabled', (): void => { pat = `${patJson.token_type} ${patJson.access_token}`; let resourceRegistrationResponse = await fetch(umaConfig.resource_registration_endpoint, { + method: 'POST', + headers: { + Authorization: pat, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + name: aliceContainer, + resource_scopes: [ createScope, modifyScope ], + }), + }); + expect(resourceRegistrationResponse.status).toBe(201); + + resourceRegistrationResponse = await fetch(umaConfig.resource_registration_endpoint, { method: 'POST', headers: { Authorization: pat, @@ -85,7 +102,7 @@ describe('A server with partial results enabled', (): void => { }, body: JSON.stringify({ name: aliceResource, - resource_scopes: [ readScope, writeScope ], + resource_scopes: [ createScope, modifyScope ], }), }); expect(resourceRegistrationResponse.status).toBe(201); @@ -99,7 +116,7 @@ describe('A server with partial results enabled', (): void => { }, body: JSON.stringify({ name: bobResource, - resource_scopes: [ readScope, writeScope ], + resource_scopes: [ readScope, modifyScope ], }), }); expect(resourceRegistrationResponse.status).toBe(201); @@ -187,7 +204,7 @@ describe('A server with partial results enabled', (): void => { odrl:assignee <${owner}> ; odrl:assigner <${owner}> ; odrl:action odrl:create, odrl:modify ; - odrl:target , + odrl:target <${aliceContainer}> , <${aliceResource}> . ex:userPermission a odrl:Permission ; @@ -208,6 +225,44 @@ describe('A server with partial results enabled', (): void => { expect(response.status).toBe(200); }); + it('returns all requested scopes when possible.', async(): Promise => { + const response = await fetch(umaConfig.permission_endpoint, { + method: 'POST', + headers: { + Authorization: pat, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify([ + { + resource_id: aliceResource, + resource_scopes: [ createScope, modifyScope ], + }, + { + resource_id: aliceContainer, + resource_scopes: [ createScope, modifyScope ], + } + ]), + }); + expect(response.status).toBe(201); + const { ticket } = await response.json() as { ticket: string }; + + const token = await getToken(ticket, umaConfig.token_endpoint, owner); + expect((token as unknown as { partial?: boolean }).partial).toBeUndefined(); + + const decodedToken = decodeJwt(token.access_token); + expect(decodedToken.permissions).toEqual([ + { + resource_id: aliceResource, + resource_scopes: [createScope, modifyScope], + }, + { + resource_id: aliceContainer, + resource_scopes: [ createScope, modifyScope ], + } + ]); + }); + it('returns partial=true for mixed namespaces when not all scopes are granted.', async(): Promise => { const permissionResponse = await fetch(umaConfig.permission_endpoint, { method: 'POST', @@ -219,7 +274,7 @@ describe('A server with partial results enabled', (): void => { body: JSON.stringify([ { resource_id: aliceResource, - resource_scopes: [ readScope, writeScope ], + resource_scopes: [ readScope, modifyScope ], }, { resource_id: bobResource,