Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ The following changes are relevant for v3 custom configs that replaced certain f
### Interface changes
These changes are relevant if you wrote custom modules for the server that depend on existing interfaces.
- The output of `parseContentType` in `HeaderUtil` was changed to include parameters.
- `PermissionReader`s take an additional `modes` parameter as input.
- The `ResourceStore` function `resourceExists` has been renamed to `hasResource`
and has been moved to a separate `ResourceSet` interface.
- Several `ModesExtractor`s `PermissionBasedAuthorizer` now take a `ResourceSet` as constructor parameter.
Comment thread
joachimvh marked this conversation as resolved.

## v3.0.0
### New features
Expand Down
1 change: 1 addition & 0 deletions config/http/middleware/handlers/cors.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"DELETE"
],
"options_credentials": true,
"options_preflightContinue": true,
"options_exposedHeaders": [
"Accept-Patch",
"ETag",
Expand Down
3 changes: 2 additions & 1 deletion config/ldp/handler/components/authorizer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
{
"comment": "Matches requested permissions with those available.",
"@id": "urn:solid-server:default:Authorizer",
"@type": "PermissionBasedAuthorizer"
"@type": "PermissionBasedAuthorizer",
"resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }
}
]
}
8 changes: 6 additions & 2 deletions config/ldp/handler/components/operation-handler.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
"@id": "urn:solid-server:default:OperationHandler",
"@type": "WaterfallHandler",
"handlers": [
{
"@type": "OptionsOperationHandler",
"resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }
},
{
"@type": "GetOperationHandler",
"store": { "@id": "urn:solid-server:default:ResourceStore" },
"store": { "@id": "urn:solid-server:default:ResourceStore" }
},
{
"@type": "PostOperationHandler",
Expand All @@ -23,7 +27,7 @@
},
{
"@type": "HeadOperationHandler",
"store": { "@id": "urn:solid-server:default:ResourceStore" },
"store": { "@id": "urn:solid-server:default:ResourceStore" }
},
{
"@type": "PatchOperationHandler",
Expand Down
13 changes: 10 additions & 3 deletions config/ldp/modes/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
},
{
"comment": "Extract access modes based on the HTTP method.",
"@type": "MethodModesExtractor"
"@type": "MethodModesExtractor",
"resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }
},
{
"@type": "StaticThrowHandler",
Expand All @@ -27,8 +28,14 @@
"source": {
"@type": "WaterfallHandler",
"handlers": [
{ "@type": "N3PatchModesExtractor" },
{ "@type": "SparqlUpdateModesExtractor" },
{
"@type": "N3PatchModesExtractor",
"resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }
},
{
"@type": "SparqlUpdateModesExtractor",
"resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }
},
{
"@type": "StaticThrowHandler",
"error": { "@type": "UnsupportedMediaTypeHttpError" }
Expand Down
6 changes: 6 additions & 0 deletions config/storage/middleware/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
"files-scs:config/storage/middleware/stores/patching.json"
],
"@graph": [
{
"comment": "A cache to prevent duplicate existence checks on resources.",
"@id": "urn:solid-server:default:CachedResourceSet",
"@type": "CachedResourceSet",
"source": { "@id": "urn:solid-server:default:ResourceStore" }
},
{
"comment": "Sets up a stack of utility stores used by most instances.",
"@id": "urn:solid-server:default:ResourceStore",
Expand Down
32 changes: 30 additions & 2 deletions src/authorization/PermissionBasedAuthorizer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { CredentialSet } from '../authentication/Credentials';
import { getLoggerFor } from '../logging/LogUtil';
import type { ResourceSet } from '../storage/ResourceSet';
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
import type { AuthorizerInput } from './Authorizer';
import { Authorizer } from './Authorizer';
import type { AccessMode, PermissionSet } from './permissions/Permissions';
import type { PermissionSet } from './permissions/Permissions';
import { AccessMode } from './permissions/Permissions';

/**
* Authorizer that bases its decision on the output it gets from its PermissionReader.
Expand All @@ -15,14 +18,39 @@ import type { AccessMode, PermissionSet } from './permissions/Permissions';
export class PermissionBasedAuthorizer extends Authorizer {
protected readonly logger = getLoggerFor(this);

private readonly resourceSet: ResourceSet;

/**
* The existence of the target resource determines the output status code for certain situations.
* The provided {@link ResourceSet} will be used for that.
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
*/
public constructor(resourceSet: ResourceSet) {
Comment thread
joachimvh marked this conversation as resolved.
super();
this.resourceSet = resourceSet;
}

public async handle(input: AuthorizerInput): Promise<void> {
const { credentials, modes, identifier, permissionSet } = input;

const modeString = [ ...modes ].join(',');
this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modeString} permissions for ${identifier.path}`);

// Ensure all required modes are within the agent's permissions.
for (const mode of modes) {
Comment thread
joachimvh marked this conversation as resolved.
this.requireModePermission(credentials, permissionSet, mode);
try {
this.requireModePermission(credentials, permissionSet, mode);
} catch (error: unknown) {
// If we know the operation will return a 404 regardless (= resource does not exist and is not being created),
// and the agent is allowed to know about its existence (= the agent has Read permissions),
// then immediately send the 404 here, as it makes any other agent permissions irrelevant.
const exposeExistence = this.hasModePermission(permissionSet, AccessMode.read);
if (exposeExistence && !modes.has(AccessMode.create) && !await this.resourceSet.hasResource(identifier)) {
throw new NotFoundHttpError();
}
// Otherwise, deny access based on existing grounds.
throw error;
Comment thread
joachimvh marked this conversation as resolved.
}
}
this.logger.debug(`${JSON.stringify(credentials)} has ${modeString} permissions for ${identifier.path}`);
}
Expand Down
8 changes: 7 additions & 1 deletion src/authorization/PermissionReader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CredentialSet } from '../authentication/Credentials';
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
import { AsyncHandler } from '../util/handlers/AsyncHandler';
import type { PermissionSet } from './permissions/Permissions';
import type { AccessMode, PermissionSet } from './permissions/Permissions';

export interface PermissionReaderInput {
/**
Expand All @@ -12,6 +12,12 @@ export interface PermissionReaderInput {
* Identifier of the resource that will be read/modified.
*/
identifier: ResourceIdentifier;
/**
* This is the minimum set of access modes the output needs to contain,
* allowing the handler to limit its search space to this set.
* However, non-exhaustive information about other access modes can still be returned.
*/
modes: Set<AccessMode>;
Comment thread
RubenVerborgh marked this conversation as resolved.
}

/**
Expand Down
Loading