-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: Create permission table to automate tests
- Loading branch information
Showing
3 changed files
with
264 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
import fetch from 'cross-fetch'; | ||
import { v4 } from 'uuid'; | ||
import type { AclPermission } from '../../src/authorization/permissions/AclPermission'; | ||
import { AccessMode as AM } from '../../src/authorization/permissions/Permissions'; | ||
import { BasicRepresentation } from '../../src/http/representation/BasicRepresentation'; | ||
import type { App } from '../../src/init/App'; | ||
import type { ResourceStore } from '../../src/storage/ResourceStore'; | ||
import { TEXT_TURTLE } from '../../src/util/ContentTypes'; | ||
import { ensureTrailingSlash, joinUrl } from '../../src/util/PathUtil'; | ||
import { AclHelper } from '../util/AclHelper'; | ||
import { getPort } from '../util/Util'; | ||
import { | ||
getDefaultVariables, | ||
getPresetConfigPath, | ||
getTestConfigPath, | ||
getTestFolder, | ||
instantiateFromConfig, removeFolder, | ||
} from './Config'; | ||
|
||
const DEFAULT_BODY = `@prefix solid: <http://www.w3.org/ns/solid/terms#>. | ||
@prefix ex: <http://www.example.org/terms#>. | ||
ex:custom ex:givenName "Claudia".`; | ||
|
||
const INSERT = `@prefix solid: <http://www.w3.org/ns/solid/terms#>. | ||
@prefix ex: <http://www.example.org/terms#>. | ||
_:patch a solid:InsertDeletePatch; | ||
solid:inserts { ex:custom ex:givenName "Alex". }.`; | ||
|
||
const DELETE = `@prefix solid: <http://www.w3.org/ns/solid/terms#>. | ||
@prefix ex: <http://www.example.org/terms#>. | ||
_:rename a solid:InsertDeletePatch; | ||
solid:deletes { ex:custom ex:givenName "Claudia". }.`; | ||
|
||
const N3 = 'text/n3'; | ||
const TXT = 'text/plain'; | ||
|
||
const allModes = [ AM.read, AM.append, AM.create, AM.write, AM.delete ]; | ||
|
||
// Based on https://github.com/solid/specification/issues/14#issuecomment-683480525 | ||
// Columns: method, target, C/ permissions, C/R permissions, body, content-type, target exists, target does not exist | ||
// `undefined` implies C/R inherits the permissions of C/ | ||
// For PUT/PATCH/DELETE we return 205 instead of 200/204 | ||
/* eslint-disable no-multi-spaces */ | ||
const table: [string, string, AM[], AM[] | undefined, string, string, number, number][] = [ | ||
// We currently handle OPTIONS before authorization | ||
// [ 'OPTIONS', 'C/R', [], undefined, '', '', 401, 401 ], | ||
// [ 'OPTIONS', 'C/R', [], [ AM.read ], '', '', 200, 404 ], | ||
// [ 'OPTIONS', 'C/R', [ AM.read ], undefined, '', '', 200, 404 ], | ||
|
||
[ 'HEAD', 'C/R', [], undefined, '', '', 401, 401 ], | ||
[ 'HEAD', 'C/R', [], [ AM.read ], '', '', 200, 404 ], | ||
[ 'HEAD', 'C/R', [ AM.read ], undefined, '', '', 200, 404 ], | ||
|
||
[ 'GET', 'C/R', [], undefined, '', '', 401, 401 ], | ||
[ 'GET', 'C/R', [], [ AM.read ], '', '', 200, 404 ], | ||
[ 'GET', 'C/R', [ AM.read ], undefined, '', '', 200, 404 ], | ||
// For the 404, we only check if C/R has read permissions, not C/ | ||
// [ 'GET', 'C/R', [ AM.read ], [ AM.write ], '', '', 401, 404 ], | ||
|
||
[ 'POST', 'C/', [], undefined, '', TXT, 401, 401 ], | ||
[ 'POST', 'C/', [], [ AM.read ], '', TXT, 401, 401 ], | ||
[ 'POST', 'C/', [ AM.append ], undefined, '', TXT, 201, 201 ], | ||
[ 'POST', 'C/', [ AM.append ], [ AM.read ], '', TXT, 201, 201 ], | ||
[ 'POST', 'C/', [ AM.read, AM.append ], undefined, '', TXT, 201, 201 ], | ||
[ 'POST', 'C/', [ AM.read, AM.append ], [ AM.read ], '', TXT, 201, 201 ], | ||
|
||
[ 'PUT', 'C/', [], undefined, '', N3, 401, 401 ], | ||
[ 'PUT', 'C/', [ AM.read ], undefined, '', N3, 401, 401 ], | ||
[ 'PUT', 'C/', [ AM.write ], undefined, '', N3, 205, 201 ], | ||
|
||
[ 'PUT', 'C/R', [], undefined, '', TXT, 401, 401 ], | ||
[ 'PUT', 'C/R', [], [ AM.read ], '', TXT, 401, 401 ], | ||
[ 'PUT', 'C/R', [], [ AM.append ], '', TXT, 401, 401 ], | ||
[ 'PUT', 'C/R', [], [ AM.write ], '', TXT, 205, 401 ], | ||
[ 'PUT', 'C/R', [ AM.read ], undefined, '', TXT, 401, 401 ], | ||
[ 'PUT', 'C/R', [ AM.append ], undefined, '', TXT, 401, 401 ], | ||
[ 'PUT', 'C/R', [ AM.write ], undefined, '', TXT, 205, 201 ], | ||
[ 'PUT', 'C/R', [ AM.append ], [ AM.write ], '', TXT, 205, 201 ], | ||
|
||
[ 'PATCH', 'C/R', [], undefined, DELETE, N3, 401, 401 ], | ||
// We don't return 404 yet in case a PATCH has no inserts and C/R does not exist | ||
// [ 'PATCH', 'C/R', [], [ AM.read ], DELETE, N3, 401, 404 ], | ||
[ 'PATCH', 'C/R', [], [ AM.append ], INSERT, N3, 205, 401 ], | ||
[ 'PATCH', 'C/R', [], [ AM.append ], DELETE, N3, 401, 401 ], | ||
[ 'PATCH', 'C/R', [], [ AM.write ], INSERT, N3, 205, 401 ], | ||
[ 'PATCH', 'C/R', [], [ AM.write ], DELETE, N3, 401, 401 ], | ||
[ 'PATCH', 'C/R', [ AM.append ], [ AM.write ], INSERT, N3, 205, 201 ], | ||
[ 'PATCH', 'C/R', [ AM.append ], [ AM.write ], DELETE, N3, 401, 401 ], | ||
// We don't return 404 yet in case a PATCH has no inserts and C/R does not exist | ||
// [ 'PATCH', 'C/R', [], [ AM.read, AM.write ], DELETE, N3, 205, 404 ], | ||
|
||
[ 'DELETE', 'C/R', [], undefined, '', '', 401, 401 ], | ||
[ 'DELETE', 'C/R', [], [ AM.read ], '', '', 401, 404 ], | ||
[ 'DELETE', 'C/R', [], [ AM.append ], '', '', 401, 401 ], | ||
[ 'DELETE', 'C/R', [], [ AM.write ], '', '', 401, 401 ], | ||
[ 'DELETE', 'C/R', [ AM.read ], undefined, '', '', 401, 404 ], | ||
[ 'DELETE', 'C/R', [ AM.append ], undefined, '', '', 401, 401 ], | ||
[ 'DELETE', 'C/R', [ AM.append ], [ AM.read ], '', '', 401, 404 ], | ||
// Not sure why 401 is suggested here instead of 404? | ||
// [ 'DELETE', 'C/R', [ AM.write ], undefined, '', '', 205, 401 ], | ||
[ 'DELETE', 'C/R', [ AM.write ], [ AM.read ], '', '', 401, 404 ], | ||
[ 'DELETE', 'C/R', [ AM.write ], [ AM.append ], '', '', 401, 401 ], | ||
|
||
[ 'DELETE', 'C/', [], undefined, '', '', 401, 401 ], | ||
[ 'DELETE', 'C/', [ AM.read ], undefined, '', '', 401, 404 ], | ||
[ 'DELETE', 'C/', [ AM.append ], undefined, '', '', 401, 401 ], | ||
// You need read permissions to delete a container? | ||
// [ 'DELETE', 'C/', [ AM.write ], undefined, '', '', 401, 401 ], | ||
[ 'DELETE', 'C/', [ AM.read, AM.write ], undefined, '', '', 205, 404 ], | ||
]; | ||
/* eslint-enable no-multi-spaces */ | ||
|
||
function toPermission(modes: AM[]): AclPermission { | ||
return Object.fromEntries(modes.map((mode): [AM, boolean] => [ mode, true ])); | ||
} | ||
|
||
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<void> => removeFolder(rootFilePath), | ||
}], | ||
]; | ||
|
||
describe.each(stores)('A request on a server with %s', (name, { storeConfig, teardown }): void => { | ||
let app: App; | ||
let store: ResourceStore; | ||
let aclHelper: AclHelper; | ||
|
||
beforeAll(async(): Promise<void> => { | ||
const variables = { | ||
...getDefaultVariables(port, baseUrl), | ||
'urn:solid-server:default:variable:rootFilePath': rootFilePath, | ||
}; | ||
|
||
// Create and start the server | ||
const instances = await instantiateFromConfig( | ||
'urn:solid-server:test:Instances', | ||
[ | ||
getPresetConfigPath(storeConfig), | ||
getTestConfigPath('ldp-with-auth.json'), | ||
], | ||
variables, | ||
) as Record<string, any>; | ||
({ 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<void> => { | ||
await teardown(); | ||
await app.stop(); | ||
}); | ||
|
||
describe.each(table)('%s %s with permissions C/: %s and C/R: %s.', (...entry): void => { | ||
const [ method, target, cPerm, crPermTemp, body, contentType, existsCode, notExistsCode ] = entry; | ||
const crPerm = crPermTemp ?? cPerm; | ||
const id = v4(); | ||
const root = ensureTrailingSlash(joinUrl(baseUrl, id)); | ||
const container = ensureTrailingSlash(joinUrl(root, 'container/')); | ||
const resource = joinUrl(container, 'resource'); | ||
const targetingContainer = target !== 'C/R'; | ||
const targetUrl = targetingContainer ? container : resource; | ||
let init: RequestInit; | ||
|
||
beforeEach(async(): Promise<void> => { | ||
// POST is special as the request targets the container but we care about the generated resource | ||
const parent = targetingContainer && method !== 'POST' ? root : container; | ||
|
||
// Create C/ and set up permissions | ||
await store.setRepresentation({ path: parent }, new BasicRepresentation([], TEXT_TURTLE)); | ||
|
||
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 }, | ||
]); | ||
|
||
// Set up fetch parameters | ||
init = { method }; | ||
if (contentType && contentType.length > 0) { | ||
init.body = body; | ||
init.headers = { 'content-type': contentType }; | ||
} | ||
}); | ||
|
||
it('target does not exist.', async(): Promise<void> => { | ||
const response = await fetch(targetUrl, init); | ||
expect(response.status).toBe(notExistsCode); | ||
}); | ||
|
||
it('target exists.', async(): Promise<void> => { | ||
await store.setRepresentation({ path: targetUrl }, new BasicRepresentation(DEFAULT_BODY, TEXT_TURTLE)); | ||
const response = await fetch(targetUrl, init); | ||
expect(response.status).toBe(existsCode); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters