fix: Prevent accidental nested storages

This commit is contained in:
Joachim Van Herwegen 2022-08-24 13:28:48 +02:00
parent cf74ce3d2a
commit 4d9d1b90b0
19 changed files with 75 additions and 65 deletions

View File

@ -19,7 +19,8 @@ The `@context` needs to be updated to
The following changes pertain to the imports in the default configs:
- ...
- All default configurations which had setup disabled have been updated to also disable registration.
This is done to prevent configurations with accidental nested storage containers.
The following changes are relevant for v5 custom configs that replaced certain features.

View File

@ -3,13 +3,14 @@
"@graph": [
{
"comment": "Handles everything related to the first-time server setup.",
"@id": "urn:solid-server:default:SetupHttpHandler",
"@id": "urn:solid-server:default:SetupParsingHandler",
"@type": "ParsingHttpHandler",
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"args_metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"args_operationHandler": {
"@id": "urn:solid-server:default:SetupHttpHandler",
"@type": "SetupHttpHandler",
"args_handler": {
"@type": "SetupHandler",

View File

@ -17,7 +17,7 @@
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"args_allowedMethods": [ "*" ],
"args_allowedPathNames": [ "/setup" ],
"args_handler": { "@id": "urn:solid-server:default:SetupHttpHandler" }
"args_handler": { "@id": "urn:solid-server:default:SetupParsingHandler" }
}
}
]

View File

@ -22,7 +22,7 @@
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"args_allowedMethods": [ "*" ],
"args_allowedPathNames": [ "/setup" ],
"args_handler": { "@id": "urn:solid-server:default:SetupHttpHandler" }
"args_handler": { "@id": "urn:solid-server:default:SetupParsingHandler" }
}
]
}

View File

@ -34,7 +34,7 @@
],
"@graph": [
{
"comment": "A single-pod server that stores its resources in memory."
"comment": "A Solid server that stores its resources in memory."
}
]
}

View File

@ -14,7 +14,7 @@
"css:config/identity/handler/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@ -34,7 +34,7 @@
],
"@graph": [
{
"comment": "A single-pod server that stores its resources on disk."
"comment": "A Solid server that stores its resources on disk."
}
]
}

View File

@ -30,6 +30,12 @@
"HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:RegistrationRoute" }
}
]
},
{
"comment": "Root access is disabled when registration is enabled to prevent nested storage containers.",
"@id": "urn:solid-server:default:SetupHttpHandler",
"@type": "SetupHttpHandler",
"allowRootPod": false
}
]
}

View File

@ -34,7 +34,7 @@
],
"@graph": [
{
"comment": "A single-pod server that stores its resources in memory with support for subdomain identifiers."
"comment": "A Solid server that stores its resources in memory with support for subdomain identifiers."
}
]
}

View File

@ -14,7 +14,7 @@
"css:config/identity/handler/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@ -14,7 +14,7 @@
"css:config/identity/handler/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@ -35,7 +35,7 @@
"@graph": [
{
"comment": [
"A single-pod server that stores its resources in a SPARQL endpoint.",
"A Solid server that stores its resources in a SPARQL endpoint.",
"This server only supports RDF data. For this reason it can not use its resource store for internal key/value storage."
]
}

View File

@ -13,7 +13,7 @@ The links here assume the server is hosted at `http://localhost:3000/`.
## Registering an account
To register an account, you can go to `http://localhost:3000/idp/register/` if this feature is enabled,
which it is on all configurations we provide.
which it is on most configurations we provide.
Currently our registration page ties 3 features together on the same page:
* Creating an account on the server.

View File

@ -110,12 +110,12 @@ export class RegistrationManager {
* * Only create a root pod when allowed.
*
* @param input - Input parameters for the registration procedure.
* @param allowRoot - If creating a pod in the root container should be allowed.
* @param allowRootPod - If creating a pod in the root container should be allowed.
*
* @returns A cleaned up version of the input parameters.
* Only (trimmed) parameters that are relevant to the registration procedure will be retained.
*/
public validateInput(input: NodeJS.Dict<unknown>, allowRoot = false): RegistrationParams {
public validateInput(input: NodeJS.Dict<unknown>, allowRootPod: boolean): RegistrationParams {
const {
email, password, confirmPassword, webId, podName, register, createPod, createWebId, template, rootPod,
} = input;
@ -135,7 +135,7 @@ export class RegistrationManager {
rootPod: Boolean(rootPod),
};
assert(validated.register || validated.createPod, 'Please register for a WebID or create a Pod.');
assert(allowRoot || !validated.rootPod, 'Creating a root pod is not supported.');
assert(allowRootPod || !validated.rootPod, 'Creating a root pod is not supported.');
// Parse WebID
if (!validated.createWebId) {
@ -171,7 +171,7 @@ export class RegistrationManager {
* * Ownership will be verified when the WebID is provided.
* * When registering and creating a pod, the base URL will be used as oidcIssuer value.
*/
public async register(input: RegistrationParams, allowRoot = false): Promise<RegistrationResponse> {
public async register(input: RegistrationParams, allowRootPod: boolean): Promise<RegistrationResponse> {
// This is only used when createWebId and/or createPod are true
let podBaseUrl: ResourceIdentifier | undefined;
if (input.createPod) {
@ -213,7 +213,7 @@ export class RegistrationManager {
try {
// Only allow overwrite for root pods
await this.podManager.createPod(podBaseUrl!, podSettings, allowRoot);
await this.podManager.createPod(podBaseUrl!, podSettings, allowRootPod);
} catch (error: unknown) {
await this.accountStore.deleteAccount(input.email);
throw error;

View File

@ -33,6 +33,12 @@ export interface SetupHttpHandlerArgs {
* Renders the main view.
*/
templateEngine: TemplateEngine;
/**
* Determines if pods can be created in the root of the server.
* Relevant to make sure there are no nested storages if registration is enabled for example.
* Defaults to `true`.
*/
allowRootPod?: boolean;
}
/**
@ -53,6 +59,7 @@ export class SetupHttpHandler extends OperationHttpHandler {
private readonly storageKey: string;
private readonly storage: KeyValueStorage<string, boolean>;
private readonly templateEngine: TemplateEngine;
private readonly allowRootPod: boolean;
public constructor(args: SetupHttpHandlerArgs) {
super();
@ -62,6 +69,7 @@ export class SetupHttpHandler extends OperationHttpHandler {
this.storageKey = args.storageKey;
this.storage = args.storage;
this.templateEngine = args.templateEngine;
this.allowRootPod = args.allowRootPod ?? true;
}
public async handle({ operation }: OperationHttpHandlerInput): Promise<ResponseDescription> {
@ -76,7 +84,7 @@ export class SetupHttpHandler extends OperationHttpHandler {
* Returns the HTML representation of the setup page.
*/
private async handleGet(operation: Operation): Promise<ResponseDescription> {
const result = await this.templateEngine.handleSafe({ contents: {}});
const result = await this.templateEngine.handleSafe({ contents: { allowRootPod: this.allowRootPod }});
const representation = new BasicRepresentation(result, operation.target, TEXT_HTML);
return new OkResponseDescription(representation.metadata, representation.data);
}

View File

@ -56,29 +56,15 @@
<label>
<input type="checkbox" id="createPod" name="createPod"<%
if (isBlankForm || prefilled.createPod) { %> checked<% } %>>
Create a new Pod with my WebID as owner<% if (!locals.allowRoot) { %>.<% } %>
Create a new Pod with my WebID as owner<% if (locals.allowRootPod) { %> in the root<% } %>.
</label>
<ol id="createPodForm">
<% if (locals.allowRoot) { %>
<li class="radio">
<label>
<input type="radio" id="rootPodOn" name="rootPod" value="on"<%
if (isBlankForm || prefilled.rootPod) { %> checked<% } %>>
…in the root.
</label>
</li>
<li class="radio">
<label>
<input type="radio" id="rootPodOff" name="rootPod" value=""<%
if (!isBlankForm && !prefilled.rootPod) { %> checked<% } %>>
…in its own namespace.
</label>
</li>
<% } %>
<% if (!locals.allowRootPod) { %>
<li id="podNameForm">
<label for="podName">Pod name:</label>
<input id="podName" type="text" name="podName" value="<%= prefilled.podName || '' %>">
</li>
<% } %>
</ol>
</li>
</ol>
@ -124,7 +110,7 @@
[
'mainForm',
'createWebIdOn', 'createWebIdOff', 'createWebIdForm', 'existingWebIdForm', 'webId',
'createPod', 'createPodForm', 'rootPodOn', 'rootPodOff', 'podNameForm', 'podName',
'createPod', 'createPodForm', 'podNameForm', 'podName',
'register', 'passwordForm',
].forEach(registerElement);
@ -133,7 +119,6 @@
createWebIdForm: () => elements.createWebIdOn.checked,
existingWebIdForm: () => elements.createWebIdOff.checked,
createPodForm: () => elements.createPod.checked,
podNameForm: () => !elements.rootPodOn.checked,
};
// Ensures that the only relevant input fields are visible and enabled
@ -158,7 +143,6 @@
webId.focus();
break;
case elements.createPod:
case elements.rootPodOff:
elements.podName.focus();
break;
}

View File

@ -3,7 +3,7 @@
<form method="post" id="mainForm">
<p class="error" id="error"></p>
<%- include('./register-partial.html.ejs', { allowRoot: false }) %>
<%- include('./register-partial.html.ejs', { allowRootPod: false }) %>
<p class="actions"><button type="submit" name="submit">Sign up</button></p>
</form>

View File

@ -12,12 +12,13 @@
<ol>
<li class="checkbox">
<label>
<input type="checkbox" checked disabled>
<input type="checkbox" <% if (!allowRootPod) { %> checked <% } %> disabled>
Enable account registration.
</label>
<p>
You can disable account registration
by <a href="https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/config/identity/README.md">changing the configuration</a>.
This can only be changed in the configuration.
See the <a href="https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/config/README.md">general configuration documentation</a>
and the <a href="https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/config/identity/README.md">identity specific options</a> to find out how.
</p>
</li>
<li class="checkbox">
@ -31,7 +32,7 @@
</li>
<li class="checkbox" id="initializeForm">
<label>
<input type="checkbox" id="initialize" name="initialize">
<input type="checkbox" id="initialize" name="initialize" <% if (!allowRootPod) { %> disabled <% } %>>
Expose a public root Pod.
</label>
<p>
@ -39,6 +40,8 @@
<br>
You typically only want to choose this
for rapid testing and development.
<br>
This requires registration to be disabled.
</p>
</li>
</ol>
@ -48,7 +51,7 @@
<legend>Sign up</legend>
<%-
include('../identity/email-password/register-partial.html.ejs', {
allowRoot: true,
allowRootPod: allowRootPod,
})
%>
</fieldset>
@ -64,6 +67,13 @@
Object.assign(visibilityConditions, {
registrationForm: () => elements.registration.checked,
initializeForm: () => !elements.registration.checked,
});
// Assigning elements to `visibilityConditions` causes the `disabled` to be removed.
// We don't want this for the initialize form.
const registration = document.getElementById('registration');
registration.addEventListener('change', () => {
const initializeForm = document.getElementById('initializeForm');
initializeForm.classList[elements.registration.checked ? 'add' : 'remove']('hidden');
});
</script>

View File

@ -65,57 +65,57 @@ describe('A RegistrationManager', (): void => {
describe('validating data', (): void => {
it('errors on invalid emails.', async(): Promise<void> => {
let input: any = { email: undefined };
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid e-mail address.');
expect((): any => manager.validateInput(input, false)).toThrow('Please enter a valid e-mail address.');
input = { email: '' };
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid e-mail address.');
expect((): any => manager.validateInput(input, false)).toThrow('Please enter a valid e-mail address.');
input = { email: 'invalidEmail' };
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid e-mail address.');
expect((): any => manager.validateInput(input, false)).toThrow('Please enter a valid e-mail address.');
});
it('errors on invalid passwords.', async(): Promise<void> => {
const input: any = { email, webId, password, confirmPassword: 'bad' };
expect((): any => manager.validateInput(input)).toThrow('Your password and confirmation did not match.');
expect((): any => manager.validateInput(input, false)).toThrow('Your password and confirmation did not match.');
});
it('errors on missing passwords.', async(): Promise<void> => {
const input: any = { email, webId };
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
expect((): any => manager.validateInput(input, false)).toThrow('Please enter a password.');
});
it('errors when setting rootPod to true when not allowed.', async(): Promise<void> => {
const input = { email, password, confirmPassword, createWebId, rootPod };
expect((): any => manager.validateInput(input)).toThrow('Creating a root pod is not supported.');
expect((): any => manager.validateInput(input, false)).toThrow('Creating a root pod is not supported.');
});
it('errors when a required WebID is not valid.', async(): Promise<void> => {
let input: any = { email, password, confirmPassword, register, webId: undefined };
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid WebID.');
expect((): any => manager.validateInput(input, false)).toThrow('Please enter a valid WebID.');
input = { email, password, confirmPassword, register, webId: '' };
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid WebID.');
expect((): any => manager.validateInput(input, false)).toThrow('Please enter a valid WebID.');
});
it('errors on invalid pod names when required.', async(): Promise<void> => {
let input: any = { email, webId, password, confirmPassword, createPod, podName: undefined };
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
expect((): any => manager.validateInput(input, false)).toThrow('Please specify a Pod name.');
input = { email, webId, password, confirmPassword, createPod, podName: ' ' };
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
expect((): any => manager.validateInput(input, false)).toThrow('Please specify a Pod name.');
input = { email, webId, password, confirmPassword, createWebId };
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
expect((): any => manager.validateInput(input, false)).toThrow('Please specify a Pod name.');
});
it('errors when no option is chosen.', async(): Promise<void> => {
const input = { email, webId, password, confirmPassword };
expect((): any => manager.validateInput(input)).toThrow('Please register for a WebID or create a Pod.');
expect((): any => manager.validateInput(input, false)).toThrow('Please register for a WebID or create a Pod.');
});
it('adds the template parameter if there is one.', async(): Promise<void> => {
const input = { email, webId, password, confirmPassword, podName, template: 'template', createPod };
expect(manager.validateInput(input)).toEqual({
expect(manager.validateInput(input, false)).toEqual({
email,
webId,
password,
@ -146,12 +146,12 @@ describe('A RegistrationManager', (): void => {
register,
createPod,
};
expect(manager.validateInput(input)).toEqual({
expect(manager.validateInput(input, false)).toEqual({
email, password: ' a ', podName, template: 'template', createWebId, register, createPod, rootPod: false,
});
input = { email, webId: ` ${webId} `, password: ' a ', confirmPassword: ' a ', register: true };
expect(manager.validateInput(input)).toEqual({
expect(manager.validateInput(input, false)).toEqual({
email, webId, password: ' a ', createWebId: false, register, createPod: false, rootPod: false,
});
});
@ -160,7 +160,7 @@ describe('A RegistrationManager', (): void => {
describe('handling data', (): void => {
it('can register a user.', async(): Promise<void> => {
const params: any = { email, webId, password, register, createPod: false, createWebId: false };
await expect(manager.register(params)).resolves.toEqual({
await expect(manager.register(params, false)).resolves.toEqual({
email,
webId,
oidcIssuer: baseUrl,
@ -184,7 +184,7 @@ describe('A RegistrationManager', (): void => {
it('can create a pod.', async(): Promise<void> => {
const params: any = { email, webId, password, podName, createPod, createWebId: false, register: false };
await expect(manager.register(params)).resolves.toEqual({
await expect(manager.register(params, false)).resolves.toEqual({
email,
webId,
oidcIssuer: baseUrl,
@ -211,7 +211,7 @@ describe('A RegistrationManager', (): void => {
it('adds an oidcIssuer to the data when doing both IDP registration and pod creation.', async(): Promise<void> => {
const params: any = { email, webId, password, confirmPassword, podName, register, createPod, createWebId: false };
podSettings.oidcIssuer = baseUrl;
await expect(manager.register(params)).resolves.toEqual({
await expect(manager.register(params, false)).resolves.toEqual({
email,
webId,
oidcIssuer: baseUrl,
@ -240,7 +240,7 @@ describe('A RegistrationManager', (): void => {
const params: any = { email, webId, password, confirmPassword, podName, register, createPod };
podSettings.oidcIssuer = baseUrl;
(podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
await expect(manager.register(params)).rejects.toThrow('pod error');
await expect(manager.register(params, false)).rejects.toThrow('pod error');
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
@ -263,7 +263,7 @@ describe('A RegistrationManager', (): void => {
podSettings.webId = generatedWebID;
podSettings.oidcIssuer = baseUrl;
await expect(manager.register(params)).resolves.toEqual({
await expect(manager.register(params, false)).resolves.toEqual({
email,
webId: generatedWebID,
oidcIssuer: baseUrl,