Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Always grant control permissions to pod owners
- Loading branch information
Showing
11 changed files
with
216 additions
and
14 deletions.
There are no files selected for viewing
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": "Allows pod owners to always edit permissions on the data.", | ||
"@id": "urn:solid-server:default:OwnershipAuthorizer", | ||
"@type": "OwnershipAuthorizer", | ||
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" } | ||
} | ||
] | ||
} |
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": "This authorizer will be used to prevent external access to containers used for internal storage.", | ||
"@id": "urn:solid-server:default:PathBasedAuthorizer", | ||
"@type": "PathBasedAuthorizer", | ||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" } | ||
} | ||
] | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import type { AccountSettings, AccountStore } from '../identity/interaction/email-password/storage/AccountStore'; | ||
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; | ||
import type { Authorization } from './Authorization'; | ||
import type { AuthorizerArgs } from './Authorizer'; | ||
import { Authorizer } from './Authorizer'; | ||
|
||
/** | ||
* Authorizer used to make sure that owners always will be able to change the permissions on data stored in their pod. | ||
*/ | ||
export class OwnershipAuthorizer extends Authorizer { | ||
private readonly accountStore: AccountStore; | ||
|
||
public constructor(accountStore: AccountStore) { | ||
super(); | ||
this.accountStore = accountStore; | ||
} | ||
|
||
public async canHandle({ credentials, identifier, permissions }: AuthorizerArgs): Promise<void> { | ||
if (Object.entries(permissions).some(([ name, value ]): boolean => value && name !== 'control')) { | ||
throw new NotImplementedHttpError('Only control permissions are supported.'); | ||
} | ||
if (!credentials.webId) { | ||
throw new NotImplementedHttpError('Only authenticated requests are supported.'); | ||
} | ||
let settings: AccountSettings; | ||
try { | ||
settings = await this.accountStore.getSettings(credentials.webId); | ||
} catch { | ||
throw new NotImplementedHttpError('Only requests by registered WebIDs are supported.'); | ||
} | ||
if (!settings.podBaseUrl || !identifier.path.startsWith(settings.podBaseUrl)) { | ||
throw new NotImplementedHttpError('Only requests targeting the pod registered to this WebID are supported.'); | ||
} | ||
} | ||
|
||
public async handle(): Promise<Authorization> { | ||
// If all checks in the canHandle function pass permission is always granted | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
return { addMetadata(): void {} }; | ||
} | ||
} |
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import type { Credentials } from '../../../src/authentication/Credentials'; | ||
import { OwnershipAuthorizer } from '../../../src/authorization/OwnershipAuthorizer'; | ||
import type { | ||
AccountSettings, | ||
AccountStore, | ||
} from '../../../src/identity/interaction/email-password/storage/AccountStore'; | ||
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; | ||
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; | ||
|
||
describe('An OwnershipAuthorizer', (): void => { | ||
const owner = 'http://test.com/alice/profile/card#me'; | ||
const podBaseUrl = 'http://test.com/alice/'; | ||
let credentials: Credentials; | ||
let identifier: ResourceIdentifier; | ||
let permissions: PermissionSet; | ||
let settings: AccountSettings; | ||
let accountStore: jest.Mocked<AccountStore>; | ||
let authorizer: OwnershipAuthorizer; | ||
|
||
beforeEach(async(): Promise<void> => { | ||
credentials = {}; | ||
|
||
identifier = { path: 'http://test.com/random/location/' }; | ||
|
||
permissions = { | ||
read: false, | ||
write: false, | ||
append: false, | ||
control: false, | ||
}; | ||
|
||
settings = { | ||
useIdp: true, | ||
podBaseUrl, | ||
}; | ||
|
||
accountStore = { | ||
getSettings: jest.fn(async(webId: string): Promise<AccountSettings> => { | ||
if (webId === owner) { | ||
return settings; | ||
} | ||
throw new Error('No account'); | ||
}), | ||
} as any; | ||
|
||
authorizer = new OwnershipAuthorizer(accountStore); | ||
}); | ||
|
||
it('only handles control permission requests.', async(): Promise<void> => { | ||
permissions.read = true; | ||
await expect(authorizer.canHandle({ credentials, identifier, permissions })) | ||
.rejects.toThrow('Only control permissions are supported.'); | ||
}); | ||
|
||
it('requires WebID credentials.', async(): Promise<void> => { | ||
permissions.control = true; | ||
await expect(authorizer.canHandle({ credentials, identifier, permissions })) | ||
.rejects.toThrow('Only authenticated requests are supported.'); | ||
}); | ||
|
||
it('requires the WebID to have an account.', async(): Promise<void> => { | ||
permissions.control = true; | ||
credentials.webId = 'http://test.com/someone/else'; | ||
await expect(authorizer.canHandle({ credentials, identifier, permissions })) | ||
.rejects.toThrow('Only requests by registered WebIDs are supported.'); | ||
}); | ||
|
||
it('requires the registered account to have a pod.', async(): Promise<void> => { | ||
delete settings.podBaseUrl; | ||
permissions.control = true; | ||
credentials.webId = owner; | ||
await expect(authorizer.canHandle({ credentials, identifier, permissions })) | ||
.rejects.toThrow('Only requests targeting the pod registered to this WebID are supported.'); | ||
}); | ||
|
||
it('requires the target identifier to be part of the pod.', async(): Promise<void> => { | ||
permissions.control = true; | ||
credentials.webId = owner; | ||
await expect(authorizer.canHandle({ credentials, identifier, permissions })) | ||
.rejects.toThrow('Only requests targeting the pod registered to this WebID are supported.'); | ||
}); | ||
|
||
it('can handle all request passing the above checks.', async(): Promise<void> => { | ||
permissions.control = true; | ||
credentials.webId = owner; | ||
identifier.path = podBaseUrl; | ||
await expect(authorizer.canHandle({ credentials, identifier, permissions })) | ||
.resolves.toBeUndefined(); | ||
}); | ||
|
||
it('allows all requests that passed the canHandle checks.', async(): Promise<void> => { | ||
const prom = authorizer.handle(); | ||
await expect(prom).resolves.toEqual(expect.objectContaining({ addMetadata: expect.any(Function) })); | ||
expect((await prom).addMetadata(null as any)).toBeUndefined(); | ||
}); | ||
}); |
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