-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add support for agentGroup ACL rules
Co-Authored-By: Ludovico Granata <Ludogranata@gmail.com>
- Loading branch information
Showing
6 changed files
with
121 additions
and
2 deletions.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
config/ldp/authorization/authorizers/access-checkers/agent-group.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" } | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/authorization/access-checkers/AgentGroupAccessChecker.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
test/unit/authorization/access-checkers/AgentGroupAccessChecker.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |