mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add support for agentGroup ACL rules
Co-Authored-By: Ludovico Granata <Ludogranata@gmail.com>
This commit is contained in:
parent
16ebfb329f
commit
401923b792
@ -0,0 +1,11 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Checks if the agent belongs to a group that has access.",
|
||||
"@id": "urn:solid-server:default:AgentGroupAccessChecker",
|
||||
"@type": "AgentGroupAccessChecker",
|
||||
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" }
|
||||
}
|
||||
]
|
||||
}
|
@ -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/agent-class.json"
|
||||
"files-scs:config/ldp/authorization/authorizers/access-checkers/agent-class.json",
|
||||
"files-scs:config/ldp/authorization/authorizers/access-checkers/agent-group.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
50
src/authorization/access-checkers/AgentGroupAccessChecker.ts
Normal file
50
src/authorization/access-checkers/AgentGroupAccessChecker.ts
Normal file
@ -0,0 +1,50 @@
|
||||
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.
|
||||
* Implements the behaviour of groups from the WAC specification.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -59,6 +59,7 @@ export const ACL = createUriAndTermNamespace('http://www.w3.org/ns/auth/acl#',
|
||||
'accessTo',
|
||||
'agent',
|
||||
'agentClass',
|
||||
'agentGroup',
|
||||
'AuthenticatedAgent',
|
||||
'Authorization',
|
||||
'default',
|
||||
@ -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',
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user