mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Remove agent/user permission differentiation
This was only used for the WAC-Allow header and greatly simplifies how we use permissions.
This commit is contained in:
@@ -19,12 +19,12 @@ import { ACL } from '../util/Vocabularies';
|
||||
import { getAccessControlledResources } from './AcpUtil';
|
||||
import type { PermissionReaderInput } from './PermissionReader';
|
||||
import { PermissionReader } from './PermissionReader';
|
||||
import type { AclPermission } from './permissions/AclPermission';
|
||||
import { AclMode } from './permissions/AclPermission';
|
||||
import type { AclPermissionSet } from './permissions/AclPermissionSet';
|
||||
import { AclMode } from './permissions/AclPermissionSet';
|
||||
import { AccessMode } from './permissions/Permissions';
|
||||
import type { PermissionMap, PermissionSet } from './permissions/Permissions';
|
||||
|
||||
const modesMap: Record<string, Readonly<(keyof AclPermission)[]>> = {
|
||||
const modesMap: Record<string, Readonly<(keyof AclPermissionSet)[]>> = {
|
||||
[ACL.Read]: [ AccessMode.read ],
|
||||
[ACL.Write]: [ AccessMode.append, AccessMode.write ],
|
||||
[ACL.Append]: [ AccessMode.append ],
|
||||
@@ -85,15 +85,11 @@ export class AcpReader extends PermissionReader {
|
||||
}
|
||||
const modes = allowAccessModes(policies, context);
|
||||
|
||||
// We don't do a separate ACP run for public and agent credentials
|
||||
// as that is only relevant for the WAC-Allow header.
|
||||
// All permissions are put in the `agent` field of the PermissionSet,
|
||||
// as the actual field used does not matter for authorization.
|
||||
const permissionSet: PermissionSet = { agent: {}};
|
||||
for (const mode of modes) {
|
||||
if (mode in modesMap) {
|
||||
for (const permission of modesMap[mode]) {
|
||||
permissionSet.agent![permission as AccessMode] = true;
|
||||
const permissionSet: PermissionSet = { };
|
||||
for (const aclMode of modes) {
|
||||
if (aclMode in modesMap) {
|
||||
for (const mode of modesMap[aclMode]) {
|
||||
permissionSet[mode as AccessMode] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { IdentifierMap } from '../util/map/IdentifierMap';
|
||||
import type { PermissionReaderInput } from './PermissionReader';
|
||||
import { PermissionReader } from './PermissionReader';
|
||||
import { permissionSetKeys } from './permissions/Permissions';
|
||||
import type { Permission, PermissionMap, PermissionSet } from './permissions/Permissions';
|
||||
import type { PermissionSet, PermissionMap } from './permissions/Permissions';
|
||||
|
||||
/**
|
||||
* PermissionReader which sets all permissions to true or false
|
||||
* independently of the identifier and requested permissions.
|
||||
*/
|
||||
export class AllStaticReader extends PermissionReader {
|
||||
private readonly permissions: Permission;
|
||||
private readonly permissionSet: PermissionSet;
|
||||
|
||||
public constructor(allow: boolean) {
|
||||
super();
|
||||
this.permissions = Object.freeze({
|
||||
this.permissionSet = Object.freeze({
|
||||
read: allow,
|
||||
write: allow,
|
||||
append: allow,
|
||||
@@ -24,18 +23,9 @@ export class AllStaticReader extends PermissionReader {
|
||||
|
||||
public async handle({ requestedModes }: PermissionReaderInput): Promise<PermissionMap> {
|
||||
const availablePermissions = new IdentifierMap<PermissionSet>();
|
||||
const permissions = this.createPermissions();
|
||||
for (const [ identifier ] of requestedModes) {
|
||||
availablePermissions.set(identifier, permissions);
|
||||
availablePermissions.set(identifier, this.permissionSet);
|
||||
}
|
||||
return availablePermissions;
|
||||
}
|
||||
|
||||
private createPermissions(): PermissionSet {
|
||||
const result: PermissionSet = {};
|
||||
for (const group of permissionSetKeys) {
|
||||
result[group] = this.permissions;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { MapEntry } from '../util/map/MapUtil';
|
||||
import { modify } from '../util/map/MapUtil';
|
||||
import type { PermissionReaderInput } from './PermissionReader';
|
||||
import { PermissionReader } from './PermissionReader';
|
||||
import { AclMode } from './permissions/AclPermission';
|
||||
import type { AclPermission } from './permissions/AclPermission';
|
||||
import type { AccessMap, AccessMode, PermissionMap, PermissionSet } from './permissions/Permissions';
|
||||
import { AclMode } from './permissions/AclPermissionSet';
|
||||
import type { AclPermissionSet } from './permissions/AclPermissionSet';
|
||||
import type { AccessMap, AccessMode, PermissionSet, PermissionMap } from './permissions/Permissions';
|
||||
|
||||
/**
|
||||
* Determines the permission for authorization resources (such as ACL or ACR).
|
||||
@@ -64,17 +64,13 @@ export class AuthAuxiliaryReader extends PermissionReader {
|
||||
* Updates the permissions for an authorization resource
|
||||
* by interpreting the Control access mode as allowing full access.
|
||||
*/
|
||||
protected interpretControl(identifier: ResourceIdentifier, permissionSet: PermissionSet = {}): PermissionSet {
|
||||
const authSet: PermissionSet = {};
|
||||
for (const [ group, permissions ] of Object.entries(permissionSet) as [ keyof PermissionSet, AclPermission ][]) {
|
||||
const { control } = permissions;
|
||||
authSet[group] = {
|
||||
read: control,
|
||||
append: control,
|
||||
write: control,
|
||||
control,
|
||||
} as AclPermission;
|
||||
}
|
||||
return authSet;
|
||||
protected interpretControl(identifier: ResourceIdentifier, permissionSet: AclPermissionSet = {}): PermissionSet {
|
||||
const { control } = permissionSet;
|
||||
return {
|
||||
read: control,
|
||||
append: control,
|
||||
write: control,
|
||||
control,
|
||||
} as AclPermissionSet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { filter } from '../util/IterableUtil';
|
||||
import { IdentifierMap } from '../util/map/IdentifierMap';
|
||||
import type { PermissionReaderInput } from './PermissionReader';
|
||||
import { PermissionReader } from './PermissionReader';
|
||||
import type { AclPermission } from './permissions/AclPermission';
|
||||
import type { AclPermissionSet } from './permissions/AclPermissionSet';
|
||||
import type { PermissionMap } from './permissions/Permissions';
|
||||
|
||||
/**
|
||||
@@ -51,14 +51,14 @@ export class OwnerPermissionReader extends PermissionReader {
|
||||
for (const auth of auths) {
|
||||
if (this.identifierStrategy.contains(podBaseUrl, auth, true)) {
|
||||
this.logger.debug(`Granting Control permissions to owner on ${auth.path}`);
|
||||
result.set(auth, { agent: {
|
||||
result.set(auth, {
|
||||
read: true,
|
||||
write: true,
|
||||
append: true,
|
||||
create: true,
|
||||
delete: true,
|
||||
control: true,
|
||||
} as AclPermission });
|
||||
} as AclPermissionSet);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { MapEntry } from '../util/map/MapUtil';
|
||||
import { modify } from '../util/map/MapUtil';
|
||||
import type { PermissionReaderInput } from './PermissionReader';
|
||||
import { PermissionReader } from './PermissionReader';
|
||||
import type { PermissionMap, Permission, PermissionSet, AccessMap } from './permissions/Permissions';
|
||||
import type { PermissionMap, PermissionSet, AccessMap } from './permissions/Permissions';
|
||||
import { AccessMode } from './permissions/Permissions';
|
||||
|
||||
/**
|
||||
@@ -80,20 +80,16 @@ export class ParentContainerReader extends PermissionReader {
|
||||
private addContainerPermissions(resourceSet?: PermissionSet, containerSet?: PermissionSet): PermissionSet {
|
||||
resourceSet = resourceSet ?? {};
|
||||
containerSet = containerSet ?? {};
|
||||
// Already copying the `permissionSet` here since the loop only iterates over the container entries.
|
||||
// It is possible `resourceSet` contains a key that `containerSet` does not contain.
|
||||
const resultSet: PermissionSet = { ...resourceSet };
|
||||
for (const [ group, containerPerms ] of Object.entries(containerSet) as [ keyof PermissionSet, Permission ][]) {
|
||||
resultSet[group] = this.interpretContainerPermission(resourceSet[group] ?? {}, containerPerms);
|
||||
}
|
||||
return resultSet;
|
||||
|
||||
return this.interpretContainerPermission(resourceSet, containerSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the create and delete permissions for the given resource permissions
|
||||
* based on those of its parent container.
|
||||
*/
|
||||
private interpretContainerPermission(resourcePermission: Permission, containerPermission: Permission): Permission {
|
||||
private interpretContainerPermission(resourcePermission: PermissionSet, containerPermission: PermissionSet):
|
||||
PermissionSet {
|
||||
const mergedPermission = { ...resourcePermission };
|
||||
|
||||
// https://solidproject.org/TR/2021/wac-20210711:
|
||||
|
||||
@@ -38,12 +38,12 @@ export class PermissionBasedAuthorizer extends Authorizer {
|
||||
for (const [ identifier, modes ] of requestedModes.entrySets()) {
|
||||
const modeString = [ ...modes ].join(',');
|
||||
this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modeString} permissions for ${identifier.path}`);
|
||||
const permissions = availablePermissions.get(identifier) ?? {};
|
||||
const permissionSet = availablePermissions.get(identifier) ?? {};
|
||||
for (const mode of modes) {
|
||||
try {
|
||||
this.requireModePermission(credentials, permissions, mode);
|
||||
this.requireModePermission(credentials, permissionSet, mode);
|
||||
} catch (error: unknown) {
|
||||
await this.reportAccessError(identifier, modes, permissions, error);
|
||||
await this.reportAccessError(identifier, modes, permissionSet, error);
|
||||
}
|
||||
}
|
||||
this.logger.debug(`${JSON.stringify(credentials)} has ${modeString} permissions for ${identifier.path}`);
|
||||
@@ -58,8 +58,8 @@ export class PermissionBasedAuthorizer extends Authorizer {
|
||||
* Otherwise, deny access based on existing grounds.
|
||||
*/
|
||||
private async reportAccessError(identifier: ResourceIdentifier, modes: ReadonlySet<AccessMode>,
|
||||
permissions: PermissionSet, cause: unknown): Promise<never> {
|
||||
const exposeExistence = this.hasModePermission(permissions, AccessMode.read);
|
||||
permissionSet: PermissionSet, cause: unknown): Promise<never> {
|
||||
const exposeExistence = permissionSet[AccessMode.read];
|
||||
if (exposeExistence && !modes.has(AccessMode.create) && !await this.resourceSet.hasResource(identifier)) {
|
||||
throw new NotFoundHttpError();
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export class PermissionBasedAuthorizer extends Authorizer {
|
||||
* @param mode - Which mode is requested.
|
||||
*/
|
||||
private requireModePermission(credentials: Credentials, permissionSet: PermissionSet, mode: AccessMode): void {
|
||||
if (!this.hasModePermission(permissionSet, mode)) {
|
||||
if (!permissionSet[mode]) {
|
||||
if (this.isAuthenticated(credentials)) {
|
||||
this.logger.warn(`Agent ${credentials.agent!.webId} has no ${mode} permissions`);
|
||||
throw new ForbiddenHttpError();
|
||||
@@ -90,18 +90,6 @@ export class PermissionBasedAuthorizer extends Authorizer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if one of the Permissions in the PermissionSet grants permission to use the given mode.
|
||||
*/
|
||||
private hasModePermission(permissionSet: PermissionSet, mode: AccessMode): boolean {
|
||||
for (const permissions of Object.values(permissionSet)) {
|
||||
if (permissions[mode]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the agent is authenticated (logged in) or not (public/anonymous).
|
||||
* @param credentials - Credentials to check.
|
||||
|
||||
@@ -2,7 +2,7 @@ import { UnionHandler } from '../util/handlers/UnionHandler';
|
||||
import { IdentifierMap } from '../util/map/IdentifierMap';
|
||||
import { getDefault } from '../util/map/MapUtil';
|
||||
import type { PermissionReader } from './PermissionReader';
|
||||
import type { Permission, PermissionMap, PermissionSet } from './permissions/Permissions';
|
||||
import type { PermissionMap, PermissionSet } from './permissions/Permissions';
|
||||
|
||||
/**
|
||||
* Combines the results of multiple PermissionReaders.
|
||||
@@ -26,22 +26,16 @@ export class UnionPermissionReader extends UnionHandler<PermissionReader> {
|
||||
*/
|
||||
private mergePermissionMaps(permissionMap: PermissionMap, result: PermissionMap): void {
|
||||
for (const [ identifier, permissionSet ] of permissionMap) {
|
||||
for (const [ credential, permission ] of Object.entries(permissionSet) as [keyof PermissionSet, Permission][]) {
|
||||
const resultSet = getDefault(result, identifier, (): PermissionSet => ({}));
|
||||
resultSet[credential] = this.mergePermissions(permission, resultSet[credential]);
|
||||
}
|
||||
const resultSet = getDefault(result, identifier, (): PermissionSet => ({}));
|
||||
result.set(identifier, this.mergePermissions(permissionSet, resultSet));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given permissions to the result object according to the combination rules of the class.
|
||||
*/
|
||||
private mergePermissions(permissions?: Permission, result: Permission = {}): Permission {
|
||||
if (!permissions) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const [ key, value ] of Object.entries(permissions) as [ keyof Permission, boolean | undefined ][]) {
|
||||
private mergePermissions(permissions: PermissionSet, result: PermissionSet): PermissionSet {
|
||||
for (const [ key, value ] of Object.entries(permissions) as [ keyof PermissionSet, boolean | undefined ][]) {
|
||||
if (typeof value !== 'undefined' && result[key] !== false) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ import { ACL, RDF } from '../util/Vocabularies';
|
||||
import type { AccessChecker } from './access/AccessChecker';
|
||||
import type { PermissionReaderInput } from './PermissionReader';
|
||||
import { PermissionReader } from './PermissionReader';
|
||||
import type { AclPermission } from './permissions/AclPermission';
|
||||
import { AclMode } from './permissions/AclPermission';
|
||||
import type { AclPermissionSet } from './permissions/AclPermissionSet';
|
||||
import { AclMode } from './permissions/AclPermissionSet';
|
||||
import type { PermissionMap } from './permissions/Permissions';
|
||||
import { AccessMode } from './permissions/Permissions';
|
||||
|
||||
// Maps WebACL-specific modes to generic access modes.
|
||||
const modesMap: Record<string, Readonly<(keyof AclPermission)[]>> = {
|
||||
const modesMap: Record<string, Readonly<(keyof AclPermissionSet)[]>> = {
|
||||
[ACL.Read]: [ AccessMode.read ],
|
||||
[ACL.Write]: [ AccessMode.append, AccessMode.write ],
|
||||
[ACL.Append]: [ AccessMode.append ],
|
||||
@@ -81,14 +81,9 @@ export class WebAclReader extends PermissionReader {
|
||||
Promise<PermissionMap> {
|
||||
const result: PermissionMap = new IdentifierMap();
|
||||
for (const [ store, aclIdentifiers ] of aclMap) {
|
||||
// WebACL requires knowledge of both the public and agent-specific permissions for the WAC-Allow header.
|
||||
const publicPermissions = await this.determinePermissions(store, {});
|
||||
const agentPermissions = credentials.agent ? await this.determinePermissions(store, credentials) : {};
|
||||
const permissionSet = await this.determinePermissions(store, credentials);
|
||||
for (const identifier of aclIdentifiers) {
|
||||
result.set(identifier, {
|
||||
public: publicPermissions,
|
||||
agent: agentPermissions,
|
||||
});
|
||||
result.set(identifier, permissionSet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +95,8 @@ export class WebAclReader extends PermissionReader {
|
||||
* @param acl - Store containing all relevant authorization triples.
|
||||
* @param credentials - Credentials to find the permissions for.
|
||||
*/
|
||||
private async determinePermissions(acl: Store, credentials: Credentials): Promise<AclPermission> {
|
||||
const aclPermissions: AclPermission = {};
|
||||
private async determinePermissions(acl: Store, credentials: Credentials): Promise<AclPermissionSet> {
|
||||
const aclPermissions: AclPermissionSet = {};
|
||||
|
||||
// Apply all ACL rules
|
||||
const aclRules = acl.getSubjects(RDF.type, ACL.Authorization, null);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Permission } from './Permissions';
|
||||
import type { PermissionSet } from './Permissions';
|
||||
|
||||
export enum AclMode {
|
||||
control = 'control',
|
||||
}
|
||||
|
||||
// Adds a control field to the permissions to specify this WAC-specific value
|
||||
export type AclPermission = Permission & {
|
||||
export type AclPermissionSet = PermissionSet & {
|
||||
[mode in AclMode]?: boolean;
|
||||
};
|
||||
@@ -19,21 +19,7 @@ export type AccessMap = IdentifierSetMultiMap<AccessMode>;
|
||||
/**
|
||||
* A data interface indicating which permissions are required (based on the context).
|
||||
*/
|
||||
export type Permission = Partial<Record<AccessMode, boolean>>;
|
||||
|
||||
/**
|
||||
* The keys that can be used in a {@link PermissionSet};
|
||||
*/
|
||||
export const permissionSetKeys = [ 'public', 'agent' ] as const;
|
||||
|
||||
/**
|
||||
* Contains the public permissions and those specific for the agent.
|
||||
* There is no good reason to subdivide permissions per type of credentials
|
||||
* since credentials are a combination of multiple factors.
|
||||
* The only reason is the WAC-Allow header which requires this subdivision,
|
||||
* which is why we make that same division here.
|
||||
*/
|
||||
export type PermissionSet = Partial<Record<typeof permissionSetKeys[number], Permission>>;
|
||||
export type PermissionSet = Partial<Record<AccessMode, boolean>>;
|
||||
|
||||
/**
|
||||
* PermissionSet per identifier.
|
||||
|
||||
Reference in New Issue
Block a user