Skip to content

Commit

Permalink
feat: Add support for agentGroup ACL rules
Browse files Browse the repository at this point in the history
Co-Authored-By: Ludovico Granata <Ludogranata@gmail.com>
  • Loading branch information
2 people authored and joachimvh committed Aug 19, 2021
1 parent dc847d6 commit 1ee8352
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 4 deletions.
@@ -0,0 +1,10 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"@id": "urn:solid-server:default:AgentGroupAccessChecker",
"@type": "AgentGroupAccessChecker",
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" }
}
]
}
6 changes: 4 additions & 2 deletions config/ldp/authorization/authorizers/acl.json
Expand Up @@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/ldp/authorization/authorizers/access-checkers/agent.json",
"files-scs:config/ldp/authorization/authorizers/access-checkers/agentClass.json"
"files-scs:config/ldp/authorization/authorizers/access-checkers/agentClass.json",
"files-scs:config/ldp/authorization/authorizers/access-checkers/agentGroup.json"
],
"@graph": [
{
Expand All @@ -21,7 +22,8 @@
"@type": "BooleanHandler",
"handlers": [
{ "@id": "urn:solid-server:default:AgentAccessChecker" },
{ "@id": "urn:solid-server:default:AgentClassAccessChecker" }
{ "@id": "urn:solid-server:default:AgentClassAccessChecker" },
{ "@id": "urn:solid-server:default:AgentGroupAccessChecker" }
]
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/authorization/WebAclAuthorizer.ts
Expand Up @@ -31,8 +31,9 @@ const modesMap: Record<string, keyof PermissionSet> = {

/**
* Handles most web access control predicates such as
* `acl:mode`, `acl:agentClass`, `acl:agent`, `acl:default` and `acl:accessTo`.
* Does not support `acl:agentGroup`, `acl:origin` and `acl:trustedApp` yet.
* `acl:mode`, `acl:agentClass`, `acl:agent`, `acl:default`,
* `acl:accessTo` and `acl:agentGroup`.
* Does not support `acl:origin` and `acl:trustedApp` yet.
*/
export class WebAclAuthorizer extends Authorizer {
protected readonly logger = getLoggerFor(this);
Expand Down
49 changes: 49 additions & 0 deletions src/authorization/access-checkers/AgentGroupAccessChecker.ts
@@ -0,0 +1,49 @@
import type { Term } from 'n3';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
import { fetchDataset } from '../../util/FetchUtil';
import { promiseSome } from '../../util/PromiseUtil';
import { readableToQuads } from '../../util/StreamUtil';
import { ACL, VCARD } from '../../util/Vocabularies';
import type { AccessCheckerArgs } from './AccessChecker';
import { AccessChecker } from './AccessChecker';

/**
* Checks if the given WebID belongs to a group that has access.
*/
export class AgentGroupAccessChecker extends AccessChecker {
private readonly converter: RepresentationConverter;

public constructor(converter: RepresentationConverter) {
super();
this.converter = converter;
}

public async handle({ acl, rule, credentials }: AccessCheckerArgs): Promise<boolean> {
if (typeof credentials.webId === 'string') {
const { webId } = credentials;
const groups = acl.getObjects(rule, ACL.terms.agentGroup, null);

return await promiseSome(groups.map(async(group: Term): Promise<boolean> =>
this.isMemberOfGroup(webId, group)));
}
return false;
}

/**
* Checks if the given agent is member of a given vCard group.
* @param webId - WebID of the agent that needs access.
* @param group - URL of the vCard group that needs to be checked.
*
* @returns If the agent is member of the given vCard group.
*/
private async isMemberOfGroup(webId: string, group: Term): Promise<boolean> {
const groupDocument: ResourceIdentifier = { path: /^[^#]*/u.exec(group.value)![0] };

// Fetch the required vCard group file
const dataset = await fetchDataset(groupDocument.path, this.converter);

const quads = await readableToQuads(dataset.data);
return quads.countQuads(group, VCARD.terms.hasMember, webId, null) !== 0;
}
}
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -21,6 +21,7 @@ export * from './authorization/WebAclAuthorizer';
export * from './authorization/access-checkers/AccessChecker';
export * from './authorization/access-checkers/AgentAccessChecker';
export * from './authorization/access-checkers/AgentClassAccessChecker';
export * from './authorization/access-checkers/AgentGroupAccessChecker';

// Identity/Configuration
export * from './identity/configuration/IdentityProviderFactory';
Expand Down
5 changes: 5 additions & 0 deletions src/util/Vocabularies.ts
Expand Up @@ -59,6 +59,7 @@ export const ACL = createUriAndTermNamespace('http://www.w3.org/ns/auth/acl#',
'accessTo',
'agent',
'agentClass',
'agentGroup',
'AuthenticatedAgent',
'Authorization',
'default',
Expand Down Expand Up @@ -144,6 +145,10 @@ export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
'preferredNamespacePrefix',
);

export const VCARD = createUriAndTermNamespace('http://www.w3.org/2006/vcard/ns#',
'hasMember',
);

export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#',
'dateTime',
'integer',
Expand Down
@@ -0,0 +1,50 @@
import { DataFactory, Store } from 'n3';
import type { AccessCheckerArgs } from '../../../../src/authorization/access-checkers/AccessChecker';
import { AgentGroupAccessChecker } from '../../../../src/authorization/access-checkers/AgentGroupAccessChecker';
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
import type { Representation } from '../../../../src/ldp/representation/Representation';
import type { RepresentationConverter } from '../../../../src/storage/conversion/RepresentationConverter';
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import * as fetchUtil from '../../../../src/util/FetchUtil';
import { ACL, VCARD } from '../../../../src/util/Vocabularies';
const { namedNode, quad } = DataFactory;

describe('An AgentGroupAccessChecker', (): void => {
const webId = 'http://test.com/alice/profile/card#me';
const groupId = 'http://test.com/group';
const acl = new Store();
acl.addQuad(namedNode('groupMatch'), ACL.terms.agentGroup, namedNode(groupId));
acl.addQuad(namedNode('noMatch'), ACL.terms.agentGroup, namedNode('badGroup'));
let fetchMock: jest.SpyInstance;
let representation: Representation;
const converter: RepresentationConverter = {} as any;
let checker: AgentGroupAccessChecker;

beforeEach(async(): Promise<void> => {
const groupQuads = [ quad(namedNode(groupId), VCARD.terms.hasMember, namedNode(webId)) ];
representation = new BasicRepresentation(groupQuads, INTERNAL_QUADS, false);
fetchMock = jest.spyOn(fetchUtil, 'fetchDataset');
fetchMock.mockResolvedValue(representation);

checker = new AgentGroupAccessChecker(converter);
});

it('can handle all requests.', async(): Promise<void> => {
await expect(checker.canHandle(null as any)).resolves.toBeUndefined();
});

it('returns true if the WebID is a valid group member.', async(): Promise<void> => {
const input: AccessCheckerArgs = { acl, rule: namedNode('groupMatch'), credentials: { webId }};
await expect(checker.handle(input)).resolves.toBe(true);
});

it('returns false if the WebID is not a valid group member.', async(): Promise<void> => {
const input: AccessCheckerArgs = { acl, rule: namedNode('noMatch'), credentials: { webId }};
await expect(checker.handle(input)).resolves.toBe(false);
});

it('returns false if there are no WebID credentials.', async(): Promise<void> => {
const input: AccessCheckerArgs = { acl, rule: namedNode('groupMatch'), credentials: {}};
await expect(checker.handle(input)).resolves.toBe(false);
});
});

0 comments on commit 1ee8352

Please sign in to comment.