mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/6.0.0
This commit is contained in:
commit
3328f8dea6
4
.github/workflows/cth-test.yml
vendored
4
.github/workflows/cth-test.yml
vendored
@ -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
|
||||
@ -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
|
||||
|
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@ -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
|
||||
|
4
.github/workflows/mkdocs.yml
vendored
4
.github/workflows/mkdocs.yml
vendored
@ -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
|
||||
|
10
.github/workflows/npm-test.yml
vendored
10
.github/workflows/npm-test.yml
vendored
@ -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
|
||||
|
29
.github/workflows/stale.yml
vendored
Normal file
29
.github/workflows/stale.yml
vendored
Normal file
@ -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@v6
|
||||
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
|
2
.github/workflows/typedocs.yml
vendored
2
.github/workflows/typedocs.yml
vendored
@ -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'
|
||||
|
20
CHANGELOG.md
20
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
|
||||
|
@ -51,6 +51,13 @@ These changes are relevant if you wrote custom modules for the server that depen
|
||||
- `CredentialSet` was replaced by a single `Credentials` interface.
|
||||
This impacts all authentication and authorization related classes.
|
||||
|
||||
## 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
|
||||
|
@ -26,7 +26,8 @@
|
||||
"Link",
|
||||
"Location",
|
||||
"Updates-Via",
|
||||
"WAC-Allow"
|
||||
"WAC-Allow",
|
||||
"Www-Authenticate"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -19,9 +19,15 @@
|
||||
"@id": "urn:solid-server:default:PatchModesExtractor"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@ -31,6 +37,9 @@
|
||||
},
|
||||
{
|
||||
"@id": "urn:solid-server:default:PatchModesExtractor",
|
||||
"@type": "CreateModesExtractor",
|
||||
"resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" },
|
||||
"source": {
|
||||
"@type": "MethodFilterHandler",
|
||||
"methods": [ "PATCH" ],
|
||||
"source": {
|
||||
@ -51,5 +60,6 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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.",
|
||||
|
58
package-lock.json
generated
58
package-lock.json
generated
@ -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",
|
||||
@ -3245,18 +3245,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",
|
||||
@ -8331,18 +8319,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",
|
||||
@ -11891,9 +11867,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"
|
||||
},
|
||||
@ -18098,15 +18074,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -21621,15 +21588,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",
|
||||
@ -24745,9 +24703,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"
|
||||
}
|
||||
|
@ -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",
|
||||
|
34
src/authorization/permissions/CreateModesExtractor.ts
Normal file
34
src/authorization/permissions/CreateModesExtractor.ts
Normal file
@ -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<void> {
|
||||
await this.source.canHandle(operation);
|
||||
}
|
||||
|
||||
public async handle(operation: Operation): Promise<AccessMap> {
|
||||
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;
|
||||
}
|
||||
}
|
44
src/authorization/permissions/DeleteParentExtractor.ts
Normal file
44
src/authorization/permissions/DeleteParentExtractor.ts
Normal file
@ -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<void> {
|
||||
await this.source.canHandle(operation);
|
||||
}
|
||||
|
||||
public async handle(operation: Operation): Promise<AccessMap> {
|
||||
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;
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@ 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';
|
||||
export * from './authorization/permissions/MethodModesExtractor';
|
||||
|
@ -17,6 +17,17 @@ const attemptDefaults: Required<AttemptSettings> = { 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.
|
||||
@ -51,11 +62,24 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab
|
||||
private readonly redisRw: RedisReadWriteLock;
|
||||
private readonly redisLock: RedisResourceLock;
|
||||
private readonly attemptSettings: Required<AttemptSettings>;
|
||||
private readonly namespacePrefix: string;
|
||||
private finalized = false;
|
||||
|
||||
public constructor(redisClient = '127.0.0.1:6379', attemptSettings: AttemptSettings = {}) {
|
||||
this.redis = this.createRedisClient(redisClient);
|
||||
/**
|
||||
* Creates a new RedisClient
|
||||
* @param redisClient - Redis connection string of a standalone Redis node
|
||||
* @param attemptSettings - Override default AttemptSettings
|
||||
* @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 = {},
|
||||
redisSettings: RedisSettings = { namespacePrefix: '' },
|
||||
) {
|
||||
const { namespacePrefix, ...options } = redisSettings;
|
||||
this.redis = this.createRedisClient(redisClient, options);
|
||||
this.attemptSettings = { ...attemptDefaults, ...attemptSettings };
|
||||
this.namespacePrefix = namespacePrefix;
|
||||
|
||||
// Register lua scripts
|
||||
for (const [ name, script ] of Object.entries(REDIS_LUA_SCRIPTS)) {
|
||||
@ -71,7 +95,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<RedisSettings, 'namespacePrefix'>): 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
|
||||
@ -83,7 +107,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'`);
|
||||
@ -95,7 +119,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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,7 +128,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 */
|
||||
@ -200,12 +224,12 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab
|
||||
* Remove any lock still open
|
||||
*/
|
||||
private async clearLocks(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
@ -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<void> => {
|
||||
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<void> => {
|
||||
const response = request(server).get('/').set('Host', 'test.com');
|
||||
expect(response).toBeDefined();
|
||||
|
@ -86,17 +86,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 ],
|
||||
@ -105,8 +108,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 ],
|
||||
|
||||
|
@ -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<void> => {
|
||||
// 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);
|
||||
});
|
||||
|
@ -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<ResourceSet>;
|
||||
let source: jest.Mocked<ModesExtractor>;
|
||||
let extractor: CreateModesExtractor;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = {
|
||||
method: 'PATCH',
|
||||
target,
|
||||
body: new BasicRepresentation(),
|
||||
preferences: {},
|
||||
};
|
||||
|
||||
result = new IdentifierSetMultiMap<AccessMode>([[ 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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
resourceSet.hasResource.mockResolvedValue(false);
|
||||
await expect(extractor.handle(operation)).resolves.toBe(result);
|
||||
compareMaps(result, new IdentifierSetMultiMap([[ target, AccessMode.read ], [ target, AccessMode.create ]]));
|
||||
});
|
||||
});
|
@ -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<ModesExtractor>;
|
||||
let resourceSet: jest.Mocked<ResourceSet>;
|
||||
let identifierStrategy: jest.Mocked<IdentifierStrategy>;
|
||||
let extractor: DeleteParentExtractor;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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();
|
||||
});
|
||||
});
|
@ -101,6 +101,15 @@ const redis: jest.Mocked<Redis & RedisResourceLock & RedisReadWriteLock> = {
|
||||
jest.mock('ioredis', (): any => jest.fn().mockImplementation((): Redis => redis));
|
||||
|
||||
describe('A RedisLocker', (): void => {
|
||||
it('will generate keys with the given namespacePrefix.', async(): Promise<void> => {
|
||||
const identifier = { path: 'http://test.com/resource' };
|
||||
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);
|
||||
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<void>((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).');
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user