From f7742cffefa2875442798a478a5d7a9960dbfa7e Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Wed, 5 Oct 2022 15:43:58 +0200 Subject: [PATCH 01/15] fix: Add missing parameter to `sparql-file-storage.json` configuration --- config/sparql-file-storage.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config/sparql-file-storage.json b/config/sparql-file-storage.json index 8e0a2c8bb..567c8a710 100644 --- a/config/sparql-file-storage.json +++ b/config/sparql-file-storage.json @@ -78,6 +78,7 @@ { "@id": "urn:solid-server:default:SparqlResourceStore", "@type": "RepresentationConvertingStore", + "metadataStrategy":{ "@id": "urn:solid-server:default:MetadataStrategy" }, "options_inConverter": { "@id": "urn:solid-server:default:RepresentationConverter" }, "options_inType": "internal/quads", "source": { From 6fecd713ddb2eae56ba5a4d30376088118edfb3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Oct 2022 00:45:04 +0000 Subject: [PATCH 02/15] chore(deps): bump actions/checkout from 2 to 3.1.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.1.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3.1.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/cth-test.yml | 2 +- .github/workflows/docker.yml | 4 ++-- .github/workflows/mkdocs.yml | 4 ++-- .github/workflows/npm-test.yml | 10 +++++----- .github/workflows/typedocs.yml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cth-test.yml b/.github/workflows/cth-test.yml index 48a12cf2a..77119e1d2 100644 --- a/.github/workflows/cth-test.yml +++ b/.github/workflows/cth-test.yml @@ -42,7 +42,7 @@ jobs: with: node-version: 16.x - name: Check out the project - uses: actions/checkout@v3 + uses: actions/checkout@v3.1.0 with: ref: ${{ inputs.branch || github.ref }} - name: Install dependencies and run build scripts diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 56c9e8d0b..d37cc422d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,7 +21,7 @@ jobs: tags: ${{ steps.meta-main.outputs.tags || steps.meta-version.outputs.tags }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v3.1.0 - if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main') name: Docker meta edge and version tag id: meta-main @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v3.1.0 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index d3efb1d8c..2d5f2a1e3 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -21,7 +21,7 @@ jobs: outputs: major: ${{ steps.tagged_version.outputs.major || steps.current_version.ouputs.major }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - uses: actions/setup-node@v3 with: node-version: '16.x' @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest needs: mkdocs-prep steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - uses: actions/setup-python@v4 with: python-version: 3.x diff --git a/.github/workflows/npm-test.yml b/.github/workflows/npm-test.yml index edacc2651..fefc8b59f 100644 --- a/.github/workflows/npm-test.yml +++ b/.github/workflows/npm-test.yml @@ -7,7 +7,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.1.0 - uses: actions/setup-node@v3 with: node-version: '16.x' @@ -38,7 +38,7 @@ jobs: - name: Ensure line endings are consistent run: git config --global core.autocrlf input - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v3.1.0 - name: Install dependencies and run build scripts run: npm ci - name: Type-check tests @@ -81,7 +81,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v3.1.0 - name: Install dependencies and run build scripts run: npm ci - name: Run integration tests @@ -105,7 +105,7 @@ jobs: - name: Ensure line endings are consistent run: git config --global core.autocrlf input - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v3.1.0 - name: Install dependencies and run build scripts run: npm ci - name: Run integration tests @@ -127,7 +127,7 @@ jobs: with: node-version: '16.x' - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v3.1.0 - name: Install dependencies and run build scripts run: npm ci - name: Run deploy tests diff --git a/.github/workflows/typedocs.yml b/.github/workflows/typedocs.yml index fef711f51..a91fa23a9 100644 --- a/.github/workflows/typedocs.yml +++ b/.github/workflows/typedocs.yml @@ -7,7 +7,7 @@ jobs: # Build typedocs and publish them to the GH page runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.1.0 - uses: actions/setup-node@v3 with: node-version: '16.x' From 37dabe12442bbfc17edc7847f4b07696fdcfec30 Mon Sep 17 00:00:00 2001 From: Jasper Vaneessen Date: Mon, 10 Oct 2022 10:11:00 +0200 Subject: [PATCH 03/15] ci: add stale action for issues * ci: add stale action for issues * ci: add stale action for issues * ci: rephrase messages * ci: set issue stale after 60 days * ci: stale action debug mode --- .github/workflows/stale.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..9dc11c15f --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,29 @@ +name: 'Stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +permissions: + issues: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v5 + with: + debug-only: true + stale-issue-label: 🏚️ abandoned + stale-issue-message: > + This issue has been automatically marked as `🏚️ abandoned` + because it has not had recent activity. It will be closed if no + further activity occurs. + close-issue-message: > + Closing stale issue. If this issue is still relevant, please reopen it. + days-before-issue-stale: 60 + days-before-close: 30 + days-before-pr-stale: -1 + operations-per-run: 700 + exempt-issue-labels: "🐛 bug,☀️ enhancement,📚 documentation,➕ feature,🐌 performance, + ➕ test,📝 task,:ant: worker threads,👩🏾‍💻 developer experience" + exempt-all-assignees: true From 6f1305020b871952e3266c0cc3c9f190309b17bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 00:57:14 +0000 Subject: [PATCH 04/15] chore(deps): bump actions/stale from 5 to 6 Bumps [actions/stale](https://github.com/actions/stale) from 5 to 6. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 9dc11c15f..160053c67 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v5 + - uses: actions/stale@v6 with: debug-only: true stale-issue-label: 🏚️ abandoned From e1af8ee66ef21ac9e0fac4379d7836b3338b8343 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 13 Oct 2022 15:06:50 +0200 Subject: [PATCH 05/15] fix: Fix incorrect config import --- config/util/auxiliary/no-acl.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/util/auxiliary/no-acl.json b/config/util/auxiliary/no-acl.json index aec657d8c..1fab7c356 100644 --- a/config/util/auxiliary/no-acl.json +++ b/config/util/auxiliary/no-acl.json @@ -1,9 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld", "import": [ - "files-scs:config/util/auxiliary/strategies/meta.json" + "css:config/util/auxiliary/strategies/meta.json" ], - "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld", "@graph": [ { "comment": "This will contain references to all the auxiliary strategies (such as the acl one) if they are needed.", From 09afebbc8435c46e869d90904708c44b621dd8e6 Mon Sep 17 00:00:00 2001 From: Thomas Dupont Date: Thu, 27 Oct 2022 16:04:37 +0200 Subject: [PATCH 06/15] ci: fix cth version on PR --- .github/workflows/cth-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cth-test.yml b/.github/workflows/cth-test.yml index 77119e1d2..cdd2a492d 100644 --- a/.github/workflows/cth-test.yml +++ b/.github/workflows/cth-test.yml @@ -65,7 +65,7 @@ jobs: -v "$(pwd)"/reports/css:/reports --env-file=./test/deploy/conformance.env --network="host" - solidproject/conformance-test-harness + solidproject/conformance-test-harness:${{ inputs.version }} --skip-teardown --output=/reports --target=https://github.com/solid/conformance-test-harness/css From d690cc7ed02e2b0ab95fa20b8934ac31c44f8566 Mon Sep 17 00:00:00 2001 From: Thomas Dupont Date: Thu, 27 Oct 2022 15:29:28 +0200 Subject: [PATCH 07/15] feat: add support for key namespacePrefixes in a RedisLocker instance --- src/util/locking/RedisLocker.ts | 18 +++++++++++++----- test/unit/util/locking/RedisLocker.test.ts | 11 ++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/util/locking/RedisLocker.ts b/src/util/locking/RedisLocker.ts index d2bff1bfc..3e61444ba 100644 --- a/src/util/locking/RedisLocker.ts +++ b/src/util/locking/RedisLocker.ts @@ -50,11 +50,19 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab private readonly redisRw: RedisReadWriteLock; private readonly redisLock: RedisResourceLock; private readonly attemptSettings: Required; + private readonly namespacePrefix: string; private finalized = false; - public constructor(redisClient = '127.0.0.1:6379', attemptSettings: AttemptSettings = {}) { + /** + * Creates a new RedisClient + * @param redisClient - Redis connection string of a standalone Redis node + * @param attemptSettings - Override default AttemptSettings + * @param namespacePrefix - Override default namespacePrefixes (used to prefix keys in Redis) + */ + public constructor(redisClient = '127.0.0.1:6379', attemptSettings: AttemptSettings = {}, namespacePrefix = '') { this.redis = this.createRedisClient(redisClient); this.attemptSettings = { ...attemptDefaults, ...attemptSettings }; + this.namespacePrefix = namespacePrefix; // Register lua scripts for (const [ name, script ] of Object.entries(REDIS_LUA_SCRIPTS)) { @@ -94,7 +102,7 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab * @returns A scoped Redis key that allows cleanup afterwards without affecting other keys. */ private getReadWriteKey(identifier: ResourceIdentifier): string { - return `${PREFIX_RW}${identifier.path}`; + return `${this.namespacePrefix}${PREFIX_RW}${identifier.path}`; } /** @@ -103,7 +111,7 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab * @returns A scoped Redis key that allows cleanup afterwards without affecting other keys. */ private getResourceKey(identifier: ResourceIdentifier): string { - return `${PREFIX_LOCK}${identifier.path}`; + return `${this.namespacePrefix}${PREFIX_LOCK}${identifier.path}`; } /* ReadWriteLocker methods */ @@ -199,12 +207,12 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab * Remove any lock still open */ private async clearLocks(): Promise { - const keysRw = await this.redisRw.keys(`${PREFIX_RW}*`); + const keysRw = await this.redisRw.keys(`${this.namespacePrefix}${PREFIX_RW}*`); if (keysRw.length > 0) { await this.redisRw.del(...keysRw); } - const keysLock = await this.redisLock.keys(`${PREFIX_LOCK}*`); + const keysLock = await this.redisLock.keys(`${this.namespacePrefix}${PREFIX_LOCK}*`); if (keysLock.length > 0) { await this.redisLock.del(...keysLock); } diff --git a/test/unit/util/locking/RedisLocker.test.ts b/test/unit/util/locking/RedisLocker.test.ts index 7df0f6a6d..d4c70f122 100644 --- a/test/unit/util/locking/RedisLocker.test.ts +++ b/test/unit/util/locking/RedisLocker.test.ts @@ -101,6 +101,15 @@ const redis: jest.Mocked = { jest.mock('ioredis', (): any => jest.fn().mockImplementation((): Redis => redis)); describe('A RedisLocker', (): void => { + it('will generate keys with the given namespacePrefix.', async(): Promise => { + const identifier = { path: 'http://test.com/resource' }; + const lockerPrefixed = new RedisLocker('6379', {}, 'MY_PREFIX'); + await lockerPrefixed.acquire(identifier); + const allLocksPrefixed = Object.keys(store.internal).every((key): boolean => key.startsWith('MY_PREFIX')); + await lockerPrefixed.release(identifier); + expect(allLocksPrefixed).toBeTruthy(); + }); + describe('with Read-Write logic', (): void => { const resource1 = { path: 'http://test.com/resource' }; const resource2 = { path: 'http://test.com/resource2' }; @@ -392,8 +401,8 @@ describe('A RedisLocker', (): void => { const emitter = new EventEmitter(); const promise = locker.withWriteLock(resource1, (): any => new Promise((resolve): any => emitter.on('release', resolve))); - await redis.releaseWriteLock(`__RW__${resource1.path}`); await flushPromises(); + await redis.releaseWriteLock(`__RW__${resource1.path}`); emitter.emit('release'); await expect(promise).rejects.toThrow('Redis operation error detected (value was null).'); }); From ef48660b482cb954e5b08e9c3799acfdba6d6f23 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 31 Oct 2022 16:55:35 +0100 Subject: [PATCH 08/15] fix: Return correct status code when deleting non-existent resource --- config/ldp/modes/default.json | 12 ++- .../permissions/DeleteParentExtractor.ts | 44 +++++++++ src/index.ts | 1 + test/integration/PermissionTable.test.ts | 3 +- .../permissions/DeleteParentExtractor.test.ts | 94 +++++++++++++++++++ 5 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 src/authorization/permissions/DeleteParentExtractor.ts create mode 100644 test/unit/authorization/permissions/DeleteParentExtractor.test.ts diff --git a/config/ldp/modes/default.json b/config/ldp/modes/default.json index e2606fb7c..3b74b0061 100644 --- a/config/ldp/modes/default.json +++ b/config/ldp/modes/default.json @@ -19,9 +19,15 @@ "@id": "urn:solid-server:default:PatchModesExtractor" }, { - "comment": "Extract access modes based on the HTTP method.", - "@type": "MethodModesExtractor", - "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" } + "comment": "Requires read permissions on parent container when deleting non-existent resource for correct status code.", + "@type": "DeleteParentExtractor", + "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }, + "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }, + "source": { + "comment": "Extract access modes based on the HTTP method.", + "@type": "MethodModesExtractor", + "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" } + } }, { "@type": "StaticThrowHandler", diff --git a/src/authorization/permissions/DeleteParentExtractor.ts b/src/authorization/permissions/DeleteParentExtractor.ts new file mode 100644 index 000000000..57ae454ce --- /dev/null +++ b/src/authorization/permissions/DeleteParentExtractor.ts @@ -0,0 +1,44 @@ +import type { Operation } from '../../http/Operation'; +import type { ResourceSet } from '../../storage/ResourceSet'; +import type { IdentifierStrategy } from '../../util/identifiers/IdentifierStrategy'; +import { ModesExtractor } from './ModesExtractor'; +import type { AccessMap } from './Permissions'; +import { AccessMode } from './Permissions'; + +/** + * In case a resource is being deleted but does not exist, + * the server response code depends on the access modes the agent has on the parent container. + * In case the agent has read access on the parent container, a 404 should be returned, + * otherwise it should be 401/403. + * + * This class adds support for this by requiring read access on the parent container + * in case the target resource does not exist. + */ +export class DeleteParentExtractor extends ModesExtractor { + private readonly source: ModesExtractor; + private readonly resourceSet: ResourceSet; + private readonly identifierStrategy: IdentifierStrategy; + + public constructor(source: ModesExtractor, resourceSet: ResourceSet, identifierStrategy: IdentifierStrategy) { + super(); + this.source = source; + this.resourceSet = resourceSet; + this.identifierStrategy = identifierStrategy; + } + + public async canHandle(operation: Operation): Promise { + await this.source.canHandle(operation); + } + + public async handle(operation: Operation): Promise { + const accessMap = await this.source.handle(operation); + const { target } = operation; + if (accessMap.get(target)?.has(AccessMode.delete) && + !this.identifierStrategy.isRootContainer(target) && + !await this.resourceSet.hasResource(target)) { + const parent = this.identifierStrategy.getParentContainer(target); + accessMap.add(parent, new Set([ AccessMode.read ])); + } + return accessMap; + } +} diff --git a/src/index.ts b/src/index.ts index 16acd154d..895f2114e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ export * from './authorization/access/AgentGroupAccessChecker'; // Authorization/Permissions export * from './authorization/permissions/AclPermission'; +export * from './authorization/permissions/DeleteParentExtractor'; export * from './authorization/permissions/IntermediateCreateExtractor'; export * from './authorization/permissions/ModesExtractor'; export * from './authorization/permissions/MethodModesExtractor'; diff --git a/test/integration/PermissionTable.test.ts b/test/integration/PermissionTable.test.ts index 3d5d39ca3..269793723 100644 --- a/test/integration/PermissionTable.test.ts +++ b/test/integration/PermissionTable.test.ts @@ -104,8 +104,7 @@ const table: [string, string, AM[], AM[] | undefined, string, string, number, nu [ '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 ], - // We throw a 404 instead of 401 since we don't yet check if the parent container has read permissions - // [ 'DELETE', 'C/R', [ AM.write ], undefined, '', '', 205, 401 ], + [ '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 ], diff --git a/test/unit/authorization/permissions/DeleteParentExtractor.test.ts b/test/unit/authorization/permissions/DeleteParentExtractor.test.ts new file mode 100644 index 000000000..04ed9658a --- /dev/null +++ b/test/unit/authorization/permissions/DeleteParentExtractor.test.ts @@ -0,0 +1,94 @@ +import { DeleteParentExtractor } from '../../../../src/authorization/permissions/DeleteParentExtractor'; +import type { ModesExtractor } from '../../../../src/authorization/permissions/ModesExtractor'; +import type { AccessMap } from '../../../../src/authorization/permissions/Permissions'; +import { AccessMode } from '../../../../src/authorization/permissions/Permissions'; +import type { Operation } from '../../../../src/http/Operation'; +import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation'; +import type { ResourceSet } from '../../../../src/storage/ResourceSet'; +import type { IdentifierStrategy } from '../../../../src/util/identifiers/IdentifierStrategy'; +import { IdentifierSetMultiMap } from '../../../../src/util/map/IdentifierMap'; + +describe('A DeleteParentExtractor', (): void => { + const baseUrl = 'http://example.com/'; + const resource = 'http://example.com/foo'; + let operation: Operation; + let sourceMap: AccessMap; + let source: jest.Mocked; + let resourceSet: jest.Mocked; + let identifierStrategy: jest.Mocked; + let extractor: DeleteParentExtractor; + + beforeEach(async(): Promise => { + operation = { + target: { path: resource }, + method: 'DELETE', + preferences: {}, + body: new BasicRepresentation(), + }; + + sourceMap = new IdentifierSetMultiMap(); + + source = { + canHandle: jest.fn(), + handle: jest.fn().mockResolvedValue(sourceMap), + } as any; + + resourceSet = { + hasResource: jest.fn().mockResolvedValue(true), + }; + + identifierStrategy = { + isRootContainer: jest.fn().mockReturnValue(false), + getParentContainer: jest.fn().mockReturnValue({ path: baseUrl }), + } as any; + + extractor = new DeleteParentExtractor(source, resourceSet, identifierStrategy); + }); + + it('supports input its source supports.', async(): Promise => { + await expect(extractor.canHandle(operation)).resolves.toBeUndefined(); + + source.canHandle.mockRejectedValue(new Error('bad data')); + await expect(extractor.canHandle(operation)).rejects.toThrow('bad data'); + }); + + it('adds read permission requirements if all conditions are met.', async(): Promise => { + sourceMap.add({ path: resource }, AccessMode.delete); + identifierStrategy.isRootContainer.mockReturnValue(false); + resourceSet.hasResource.mockResolvedValue(false); + + const resultMap = await extractor.handle(operation); + expect([ ...resultMap.entries() ]).toHaveLength(2); + expect(resultMap.get({ path: baseUrl })).toContain(AccessMode.read); + }); + + it('does not change the results if no delete access is required.', async(): Promise => { + sourceMap.add({ path: resource }, AccessMode.read); + identifierStrategy.isRootContainer.mockReturnValue(false); + resourceSet.hasResource.mockResolvedValue(false); + + const resultMap = await extractor.handle(operation); + expect([ ...resultMap.entries() ]).toHaveLength(1); + expect(resultMap.get({ path: baseUrl })).toBeUndefined(); + }); + + it('does not change the results if the target is the root container.', async(): Promise => { + sourceMap.add({ path: resource }, AccessMode.delete); + identifierStrategy.isRootContainer.mockReturnValue(true); + resourceSet.hasResource.mockResolvedValue(false); + + const resultMap = await extractor.handle(operation); + expect([ ...resultMap.entries() ]).toHaveLength(1); + expect(resultMap.get({ path: baseUrl })).toBeUndefined(); + }); + + it('does not change the results if the target exists.', async(): Promise => { + sourceMap.add({ path: resource }, AccessMode.delete); + identifierStrategy.isRootContainer.mockReturnValue(false); + resourceSet.hasResource.mockResolvedValue(true); + + const resultMap = await extractor.handle(operation); + expect([ ...resultMap.entries() ]).toHaveLength(1); + expect(resultMap.get({ path: baseUrl })).toBeUndefined(); + }); +}); From 79fa83a07ab6dded5c7d601dd7b165fa9178ef26 Mon Sep 17 00:00:00 2001 From: Arthur Joppart <38424924+BelgianNoise@users.noreply.github.com> Date: Wed, 2 Nov 2022 10:48:30 +0100 Subject: [PATCH 09/15] feat: add additional redis settings to redis locker * feat: add additional redis settings to redis locker * fix: unfinished doc --- src/util/locking/RedisLocker.ts | 26 +++++++++++++++++----- test/unit/util/locking/RedisLocker.test.ts | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/util/locking/RedisLocker.ts b/src/util/locking/RedisLocker.ts index 3e61444ba..d28f4cb45 100644 --- a/src/util/locking/RedisLocker.ts +++ b/src/util/locking/RedisLocker.ts @@ -16,6 +16,17 @@ const attemptDefaults: Required = { retryCount: -1, retryDelay: const PREFIX_RW = '__RW__'; const PREFIX_LOCK = '__L__'; +export interface RedisSettings { + /* Override default namespacePrefixes (used to prefix keys in Redis) */ + namespacePrefix: string; + /* Username used for AUTH on the Redis server */ + username?: string; + /* Password used for AUTH on the Redis server */ + password?: string; + /* The number of the database to use */ + db?: number; +} + /** * A Redis Locker that can be used as both: * * a Read Write Locker that uses a (single) Redis server to store the locks and counts. @@ -57,10 +68,15 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab * Creates a new RedisClient * @param redisClient - Redis connection string of a standalone Redis node * @param attemptSettings - Override default AttemptSettings - * @param namespacePrefix - Override default namespacePrefixes (used to prefix keys in Redis) + * @param redisSettings - Addition settings used to create the Redis client or to interact with the Redis server */ - public constructor(redisClient = '127.0.0.1:6379', attemptSettings: AttemptSettings = {}, namespacePrefix = '') { - this.redis = this.createRedisClient(redisClient); + public constructor( + redisClient = '127.0.0.1:6379', + attemptSettings: AttemptSettings = {}, + redisSettings: RedisSettings = { namespacePrefix: '' }, + ) { + const { namespacePrefix, ...options } = redisSettings; + this.redis = this.createRedisClient(redisClient, options); this.attemptSettings = { ...attemptDefaults, ...attemptSettings }; this.namespacePrefix = namespacePrefix; @@ -78,7 +94,7 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab * @param redisClientString - A string that contains either a host address and a * port number like '127.0.0.1:6379' or just a port number like '6379'. */ - private createRedisClient(redisClientString: string): Redis { + private createRedisClient(redisClientString: string, options: Omit): Redis { if (redisClientString.length > 0) { // Check if port number or ip with port number // Definitely not perfect, but configuring this is only for experienced users @@ -90,7 +106,7 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab } const port = Number(match[2]); const host = match[1]; - return new Redis(port, host); + return new Redis(port, host, options); } throw new Error(`Empty redisClientString provided!\n Please provide a port number like '6379' or a host address and a port number like '127.0.0.1:6379'`); diff --git a/test/unit/util/locking/RedisLocker.test.ts b/test/unit/util/locking/RedisLocker.test.ts index d4c70f122..786337eda 100644 --- a/test/unit/util/locking/RedisLocker.test.ts +++ b/test/unit/util/locking/RedisLocker.test.ts @@ -103,7 +103,7 @@ jest.mock('ioredis', (): any => jest.fn().mockImplementation((): Redis => redis) describe('A RedisLocker', (): void => { it('will generate keys with the given namespacePrefix.', async(): Promise => { const identifier = { path: 'http://test.com/resource' }; - const lockerPrefixed = new RedisLocker('6379', {}, 'MY_PREFIX'); + const lockerPrefixed = new RedisLocker('6379', {}, { namespacePrefix: 'MY_PREFIX' }); await lockerPrefixed.acquire(identifier); const allLocksPrefixed = Object.keys(store.internal).every((key): boolean => key.startsWith('MY_PREFIX')); await lockerPrefixed.release(identifier); From 68ee9648e1c78684708f026d9f4b22ee1cc66790 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 7 Oct 2022 11:21:35 +0200 Subject: [PATCH 10/15] fix: Require create permission for empty PATCH bodies --- config/ldp/modes/default.json | 38 ++++++------ .../permissions/CreateModesExtractor.ts | 34 +++++++++++ src/index.ts | 1 + test/integration/PermissionTable.test.ts | 11 ++-- .../permissions/CreateModesExtractor.test.ts | 59 +++++++++++++++++++ 5 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 src/authorization/permissions/CreateModesExtractor.ts create mode 100644 test/unit/authorization/permissions/CreateModesExtractor.test.ts diff --git a/config/ldp/modes/default.json b/config/ldp/modes/default.json index 3b74b0061..71c97a253 100644 --- a/config/ldp/modes/default.json +++ b/config/ldp/modes/default.json @@ -37,24 +37,28 @@ }, { "@id": "urn:solid-server:default:PatchModesExtractor", - "@type": "MethodFilterHandler", - "methods": [ "PATCH" ], + "@type": "CreateModesExtractor", + "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }, "source": { - "@type": "WaterfallHandler", - "handlers": [ - { - "@type": "N3PatchModesExtractor", - "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" } - }, - { - "@type": "SparqlUpdateModesExtractor", - "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" } - }, - { - "@type": "StaticThrowHandler", - "error": { "@type": "UnsupportedMediaTypeHttpError" } - } - ] + "@type": "MethodFilterHandler", + "methods": [ "PATCH" ], + "source": { + "@type": "WaterfallHandler", + "handlers": [ + { + "@type": "N3PatchModesExtractor", + "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" } + }, + { + "@type": "SparqlUpdateModesExtractor", + "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" } + }, + { + "@type": "StaticThrowHandler", + "error": { "@type": "UnsupportedMediaTypeHttpError" } + } + ] + } } } ] diff --git a/src/authorization/permissions/CreateModesExtractor.ts b/src/authorization/permissions/CreateModesExtractor.ts new file mode 100644 index 000000000..5048eabe6 --- /dev/null +++ b/src/authorization/permissions/CreateModesExtractor.ts @@ -0,0 +1,34 @@ +import type { Operation } from '../../http/Operation'; +import type { ResourceSet } from '../../storage/ResourceSet'; +import { ModesExtractor } from './ModesExtractor'; +import type { AccessMap } from './Permissions'; +import { AccessMode } from './Permissions'; + +/** + * Adds the `create` access mode to the result of the source in case the target resource does not exist. + */ +export class CreateModesExtractor extends ModesExtractor { + private readonly source: ModesExtractor; + private readonly resourceSet: ResourceSet; + + public constructor(source: ModesExtractor, resourceSet: ResourceSet) { + super(); + this.source = source; + this.resourceSet = resourceSet; + } + + public async canHandle(operation: Operation): Promise { + await this.source.canHandle(operation); + } + + public async handle(operation: Operation): Promise { + const accessMap = await this.source.handle(operation); + + if (!accessMap.hasEntry(operation.target, AccessMode.create) && + !await this.resourceSet.hasResource(operation.target)) { + accessMap.add(operation.target, AccessMode.create); + } + + return accessMap; + } +} diff --git a/src/index.ts b/src/index.ts index 895f2114e..d38f5c906 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ export * from './authorization/access/AgentGroupAccessChecker'; // Authorization/Permissions export * from './authorization/permissions/AclPermission'; +export * from './authorization/permissions/CreateModesExtractor'; export * from './authorization/permissions/DeleteParentExtractor'; export * from './authorization/permissions/IntermediateCreateExtractor'; export * from './authorization/permissions/ModesExtractor'; diff --git a/test/integration/PermissionTable.test.ts b/test/integration/PermissionTable.test.ts index 269793723..8b3cf1919 100644 --- a/test/integration/PermissionTable.test.ts +++ b/test/integration/PermissionTable.test.ts @@ -85,17 +85,20 @@ const table: [string, string, AM[], AM[] | undefined, string, string, number, nu [ 'PUT', 'C/R', [ AM.write ], undefined, '', TXT, 205, 201 ], [ 'PUT', 'C/R', [ AM.append ], [ AM.write ], '', TXT, 205, 201 ], + // All PATCH operations with read permissions return 401 instead of 404 if the target does not exist. + // This is a consequence of PATCH always creating a resource in case it does not exist. + // https://solidproject.org/TR/2021/protocol-20211217#n3-patch + // "Start from the RDF dataset in the target document, + // or an empty RDF dataset if the target resource does not exist yet." [ 'PATCH', 'C/R', [], undefined, DELETE, N3, 401, 401 ], - [ 'PATCH', 'C/R', [], [ AM.read ], DELETE, N3, 401, 404 ], + [ 'PATCH', 'C/R', [], [ AM.read ], DELETE, N3, 401, 401 ], [ '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 currently return 409 instead of 404 in case a PATCH has no inserts and C/R does not exist. - // This is an agreed upon deviation from the original table - [ 'PATCH', 'C/R', [], [ AM.read, AM.write ], DELETE, N3, 205, 409 ], + [ 'PATCH', 'C/R', [], [ AM.read, AM.write ], DELETE, N3, 205, 401 ], [ 'DELETE', 'C/R', [], undefined, '', '', 401, 401 ], [ 'DELETE', 'C/R', [], [ AM.read ], '', '', 401, 404 ], diff --git a/test/unit/authorization/permissions/CreateModesExtractor.test.ts b/test/unit/authorization/permissions/CreateModesExtractor.test.ts new file mode 100644 index 000000000..7e375b885 --- /dev/null +++ b/test/unit/authorization/permissions/CreateModesExtractor.test.ts @@ -0,0 +1,59 @@ +import { CreateModesExtractor } from '../../../../src/authorization/permissions/CreateModesExtractor'; +import type { ModesExtractor } from '../../../../src/authorization/permissions/ModesExtractor'; +import type { AccessMap } from '../../../../src/authorization/permissions/Permissions'; +import { AccessMode } from '../../../../src/authorization/permissions/Permissions'; +import type { Operation } from '../../../../src/http/Operation'; +import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation'; +import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier'; +import type { ResourceSet } from '../../../../src/storage/ResourceSet'; +import { IdentifierSetMultiMap } from '../../../../src/util/map/IdentifierMap'; +import { compareMaps } from '../../../util/Util'; + +describe('A CreateModesExtractor', (): void => { + const target: ResourceIdentifier = { path: 'http://example.com/foo' }; + let operation: Operation; + let result: AccessMap; + let resourceSet: jest.Mocked; + let source: jest.Mocked; + let extractor: CreateModesExtractor; + + beforeEach(async(): Promise => { + operation = { + method: 'PATCH', + target, + body: new BasicRepresentation(), + preferences: {}, + }; + + result = new IdentifierSetMultiMap([[ target, AccessMode.read ]]); + + resourceSet = { + hasResource: jest.fn().mockResolvedValue(true), + }; + + source = { + canHandle: jest.fn(), + handle: jest.fn().mockResolvedValue(result), + } as any; + + extractor = new CreateModesExtractor(source, resourceSet); + }); + + it('checks if the source can handle the input.', async(): Promise => { + await expect(extractor.canHandle(operation)).resolves.toBeUndefined(); + + source.canHandle.mockRejectedValue(new Error('bad data')); + await expect(extractor.canHandle(operation)).rejects.toThrow('bad data'); + }); + + it('does nothing if the resource exists.', async(): Promise => { + await expect(extractor.handle(operation)).resolves.toBe(result); + compareMaps(result, new IdentifierSetMultiMap([[ target, AccessMode.read ]])); + }); + + it('adds the create mode if the resource does not exist.', async(): Promise => { + resourceSet.hasResource.mockResolvedValue(false); + await expect(extractor.handle(operation)).resolves.toBe(result); + compareMaps(result, new IdentifierSetMultiMap([[ target, AccessMode.read ], [ target, AccessMode.create ]])); + }); +}); From 3fe4656a7155d53a61f5b24d8f551ffc7345adb7 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 3 Nov 2022 09:25:31 +0100 Subject: [PATCH 11/15] chore: Update dependency vulnerability --- package-lock.json | 54 ++++++----------------------------------------- 1 file changed, 6 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index a013e844c..d69740fb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3244,18 +3244,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", @@ -8307,18 +8295,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -11867,9 +11843,9 @@ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -18066,15 +18042,6 @@ "requires": { "argparse": "^2.0.1" } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } } } }, @@ -21568,15 +21535,6 @@ "p-locate": "^5.0.0" } }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -24692,9 +24650,9 @@ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } From 1a07de7c9da8c19b3406b9b843bdcb36c14e98a6 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 3 Nov 2022 09:52:03 +0100 Subject: [PATCH 12/15] chore(release): Release version 5.1.0 of the npm package --- CHANGELOG.md | 20 ++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0adc54ce..7e2518252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to this project will be documented in this file. +## [5.1.0](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v5.0.0...v5.1.0) (2022-11-03) + +### Features + +* add additional redis settings to redis locker ([79fa83a](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/79fa83a07ab6dded5c7d601dd7b165fa9178ef26)) +* add support for key namespacePrefixes in a RedisLocker instance ([d690cc7](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/d690cc7ed02e2b0ab95fa20b8934ac31c44f8566)) +* Allow JSON-LD contexts to be stored locally ([b0924bf](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/b0924bf168500070a287cef300047f874deebe0c)) +* Allow multiple configurations to be used during startup ([e050f8b](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/e050f8be93b7ea9596fdaf250de9f0bf32fa4fd8)) + +### Fixes + +* Require create permission for empty PATCH bodies ([68ee964](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/68ee9648e1c78684708f026d9f4b22ee1cc66790)) +* Return correct status code when deleting non-existent resource ([ef48660](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/ef48660b482cb954e5b08e9c3799acfdba6d6f23)) +* Fix incorrect config import ([e1af8ee](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/e1af8ee66ef21ac9e0fac4379d7836b3338b8343)) +* Add missing parameter to `sparql-file-storage.json` configuration ([f7742cf](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/f7742cffefa2875442798a478a5d7a9960dbfa7e)) +* Always render OIDC errors correctly ([7884348](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/7884348c2f4e8f646f38cf987c58a8a47135facd)) +* Clarify application consent screen. ([7987824](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/79878240682b2f7e335db4edd4c6a4ed8044a170)) +* Prevent websockets from being used with worker threads ([327ce74](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/327ce7409ac3f62f8213bd8e300b3726fa848efb)) +* update metadata documentation ([abbf3dd](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/abbf3ddeef1494a84aa8e7293108ef63f47ac2d9)) + ## [5.0.0](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v4.0.1...v5.0.0) (2022-08-08) ### Features diff --git a/package-lock.json b/package-lock.json index d69740fb8..28ac7e1e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solid/community-server", - "version": "5.0.0", + "version": "5.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@solid/community-server", - "version": "5.0.0", + "version": "5.1.0", "license": "MIT", "dependencies": { "@comunica/context-entries": "^2.2.0", diff --git a/package.json b/package.json index 4f1f804d9..685940577 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solid/community-server", - "version": "5.0.0", + "version": "5.1.0", "description": "Community Solid Server: an open and modular implementation of the Solid specifications", "keywords": [ "solid", From eab17fcd2351384bd03ceccf944b458302203f07 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 3 Nov 2022 09:59:21 +0100 Subject: [PATCH 13/15] docs: Update v5.1.0 release notes --- RELEASE_NOTES.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d6cd84546..e604cdc36 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,12 @@ # Community Solid Server release notes +## v5.1.0 + +### New features + +- The `--config` CLI parameter now accepts multiple configuration paths, which will be combined. +- The `RedisLocker` now accepts more configuration parameters. + ## v5.0.0 ### New features From 7fafd646fc685c4908b51c3e3ae2738937967368 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 4 Nov 2022 09:55:04 +0100 Subject: [PATCH 14/15] test: Make sure quota test succeeds if tmp folder does not exist --- test/integration/Quota.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/Quota.test.ts b/test/integration/Quota.test.ts index a9e579c63..81b429db5 100644 --- a/test/integration/Quota.test.ts +++ b/test/integration/Quota.test.ts @@ -2,7 +2,7 @@ import { promises as fsPromises } from 'fs'; import type { Stats } from 'fs'; import fetch from 'cross-fetch'; import type { Response } from 'cross-fetch'; -import { pathExists } from 'fs-extra'; +import { ensureDir, pathExists } from 'fs-extra'; import { joinFilePath, joinUrl } from '../../src'; import type { App } from '../../src'; import { getPort } from '../util/Util'; @@ -73,7 +73,7 @@ describe('A quota server', (): void => { beforeAll(async(): Promise => { // We want to use an empty folder as on APFS/Mac folder sizes vary a lot const tempFolder = getTestFolder('quota-temp'); - await fsPromises.mkdir(tempFolder); + await ensureDir(tempFolder); folderSizeTest = await fsPromises.stat(tempFolder); await removeFolder(tempFolder); }); From 60718a123d201d64315d03d5cd7977479d6cdc7f Mon Sep 17 00:00:00 2001 From: Samu Lang Date: Mon, 7 Nov 2022 08:45:52 +0100 Subject: [PATCH 15/15] fix: Expose Www-Authenticate via CORS To support reactive authentication where clients inspect the challenge to choose an authentication mechanism. --- config/http/middleware/handlers/cors.json | 3 ++- test/integration/Middleware.test.ts | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/config/http/middleware/handlers/cors.json b/config/http/middleware/handlers/cors.json index 4f4d90911..25d31d4fa 100644 --- a/config/http/middleware/handlers/cors.json +++ b/config/http/middleware/handlers/cors.json @@ -26,7 +26,8 @@ "Link", "Location", "Updates-Via", - "WAC-Allow" + "WAC-Allow", + "Www-Authenticate" ] } ] diff --git a/test/integration/Middleware.test.ts b/test/integration/Middleware.test.ts index 5e58d7cef..d4a257b0c 100644 --- a/test/integration/Middleware.test.ts +++ b/test/integration/Middleware.test.ts @@ -133,6 +133,12 @@ describe('An http server with middleware', (): void => { expect(splitCommaSeparated(exposed)).toContain('Updates-Via'); }); + it('exposes the Www-Authenticate header via CORS.', async(): Promise => { + const res = await request(server).get('/').expect(200); + const exposed = res.header['access-control-expose-headers']; + expect(splitCommaSeparated(exposed)).toContain('Www-Authenticate'); + }); + it('sends incoming requests to the handler.', async(): Promise => { const response = request(server).get('/').set('Host', 'test.com'); expect(response).toBeDefined();