diff --git a/src/authorization/AcpReader.ts b/src/authorization/AcpReader.ts index 5a3365888..3d1d6d7fe 100644 --- a/src/authorization/AcpReader.ts +++ b/src/authorization/AcpReader.ts @@ -58,7 +58,7 @@ export class AcpReader extends PermissionReader { const permissionMap: PermissionMap = new IdentifierMap(); // Resolves the targets sequentially so the `resourceCache` can be filled and reused - for (const target of requestedModes.keys()) { + for (const target of requestedModes.distinctKeys()) { permissionMap.set(target, await this.extractPermissions(target, credentials, resourceCache)); } return permissionMap; diff --git a/test/integration/PermissionTable.test.ts b/test/integration/PermissionTable.test.ts index 3d5d39ca3..10960c94c 100644 --- a/test/integration/PermissionTable.test.ts +++ b/test/integration/PermissionTable.test.ts @@ -9,6 +9,7 @@ import { TEXT_TURTLE } from '../../src/util/ContentTypes'; import { ConflictHttpError } from '../../src/util/errors/ConflictHttpError'; import { ensureTrailingSlash, joinUrl } from '../../src/util/PathUtil'; import { AclHelper } from '../util/AclHelper'; +import { AcpHelper } from '../util/AcpHelper'; import { getPort } from '../util/Util'; import { getDefaultVariables, @@ -121,25 +122,74 @@ function toPermission(modes: AM[]): AclPermission { return Object.fromEntries(modes.map((mode): [AM, boolean] => [ mode, true ])); } +async function setWebAclPermissions(store: ResourceStore, target: string, permissions: AclPermission, + childPermissions: AclPermission): Promise { + const aclHelper = new AclHelper(store); + await aclHelper.setSimpleAcl(target, [ + { permissions, agentClass: 'agent', accessTo: true }, + { permissions: childPermissions, agentClass: 'agent', default: true }, + ]); +} + +async function setAcpPermissions(store: ResourceStore, target: string, permissions: AclPermission, + childPermissions: AclPermission): Promise { + const acpHelper = new AcpHelper(store); + const publicMatcher = acpHelper.createMatcher({ publicAgent: true }); + const policies = [ acpHelper.createPolicy({ + // Casting from enum to strings + allow: Object.keys(permissions) as any, + anyOf: [ publicMatcher ], + }) ]; + const memberPolicies = [ acpHelper.createPolicy({ + allow: Object.keys(childPermissions) as any, + anyOf: [ publicMatcher ], + }) ]; + await acpHelper.setAcp(target, acpHelper.createAcr({ + resource: target, + policies, + memberPolicies, + })); +} + const port = getPort('PermissionTable'); const baseUrl = `http://localhost:${port}/`; -const rootFilePath = getTestFolder('permissionTable'); -const stores: [string, any][] = [ - [ 'in-memory storage', { - storeConfig: 'storage/backend/memory.json', - teardown: jest.fn(), - }], - [ 'on-disk storage', { - storeConfig: 'storage/backend/file.json', - teardown: async(): Promise => removeFolder(rootFilePath), - }], -]; +type AuthFunctionType = (store: ResourceStore, target: string, + permissions: AclPermission, childPermissions: AclPermission) => Promise; -describe.each(stores)('A request on a server with %s', (name, { storeConfig, teardown }): void => { +const rootFilePath = getTestFolder('permissionTable'); +const stores: [string, string, { configs: string[]; authFunction: AuthFunctionType; teardown: () => Promise }][] = + [ + [ 'WebACL', + 'in-memory storage', { + configs: [ 'ldp/authorization/webacl.json', 'util/auxiliary/acl.json', 'storage/backend/memory.json' ], + authFunction: setWebAclPermissions, + teardown: jest.fn(), + }], + [ 'WebACL', + 'on-disk storage', { + configs: [ 'ldp/authorization/webacl.json', 'util/auxiliary/acl.json', 'storage/backend/file.json' ], + authFunction: setWebAclPermissions, + teardown: async(): Promise => removeFolder(rootFilePath), + }], + [ 'ACP', + 'in-memory storage', { + configs: [ 'ldp/authorization/acp.json', 'util/auxiliary/acr.json', 'storage/backend/memory.json' ], + authFunction: setAcpPermissions, + teardown: jest.fn(), + }], + [ 'ACP', + 'on-disk storage', { + configs: [ 'ldp/authorization/acp.json', 'util/auxiliary/acr.json', 'storage/backend/file.json' ], + authFunction: setAcpPermissions, + teardown: async(): Promise => removeFolder(rootFilePath), + }], + ]; + +describe.each(stores)('A request on a server with %s authorization and %s', (auth, name, + { configs, authFunction, teardown }): void => { let app: App; let store: ResourceStore; - let aclHelper: AclHelper; beforeAll(async(): Promise => { const variables = { @@ -151,25 +201,14 @@ describe.each(stores)('A request on a server with %s', (name, { storeConfig, tea const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', [ - getPresetConfigPath(storeConfig), - getTestConfigPath('ldp-with-auth.json'), + ...configs.map(getPresetConfigPath), + getTestConfigPath('permission-table.json'), ], variables, ) as Record; ({ app, store } = instances); await app.start(); - - // Create test helper for manipulating acl - aclHelper = new AclHelper(store); - - // Set the root acl file to allow everything - await aclHelper.setSimpleAcl(baseUrl, { - permissions: { read: true, write: true, append: true, control: true }, - agentClass: 'agent', - accessTo: true, - default: true, - }); }); afterAll(async(): Promise => { @@ -201,11 +240,10 @@ describe.each(stores)('A request on a server with %s', (name, { storeConfig, tea } } - await aclHelper.setSimpleAcl(parent, [ - // In case we are targeting C/ we assume everything is allowed by the parent - { permissions: toPermission(parent === root ? allModes : cPerm), agentClass: 'agent', accessTo: true }, - { permissions: toPermission(parent === root ? cPerm : crPerm), agentClass: 'agent', default: true }, - ]); + await authFunction(store, + parent, + toPermission(parent === root ? allModes : cPerm), + toPermission(parent === root ? cPerm : crPerm)); // Set up fetch parameters init = { method }; diff --git a/test/integration/config/permission-table.json b/test/integration/config/permission-table.json new file mode 100644 index 000000000..3959dc4c5 --- /dev/null +++ b/test/integration/config/permission-table.json @@ -0,0 +1,52 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld", + "import": [ + "css:config/app/main/default.json", + "css:config/app/init/default.json", + "css:config/app/setup/disabled.json", + + "css:config/http/handler/simple.json", + "css:config/http/middleware/no-websockets.json", + "css:config/http/server-factory/no-websockets.json", + "css:config/http/static/default.json", + "css:config/identity/access/public.json", + + "css:config/identity/handler/default.json", + "css:config/identity/ownership/token.json", + "css:config/identity/pod/static.json", + + "css:config/ldp/authentication/debug-auth-header.json", + + "css:config/ldp/handler/default.json", + "css:config/ldp/metadata-parser/default.json", + "css:config/ldp/metadata-writer/default.json", + "css:config/ldp/modes/default.json", + + "css:config/storage/key-value/memory.json", + "css:config/storage/middleware/default.json", + + "css:config/util/identifiers/suffix.json", + "css:config/util/index/default.json", + "css:config/util/logging/winston.json", + "css:config/util/representation-conversion/default.json", + "css:config/util/resource-locker/memory.json", + "css:config/util/variables/default.json" + ], + "@graph": [ + { + "comment": "An HTTP server with only the LDP handler as HttpHandler and an unsecure authenticator.", + "@id": "urn:solid-server:test:Instances", + "@type": "RecordObject", + "record": [ + { + "RecordObject:_record_key": "app", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:App" } + }, + { + "RecordObject:_record_key": "store", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore" } + } + ] + } + ] +}