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:
Joachim Van Herwegen
2022-11-18 14:01:06 +01:00
parent 6ad5c0c797
commit c46d01d3d7
28 changed files with 220 additions and 276 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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:

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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.