diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1e200b5c5..3c2abfe7e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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. diff --git a/config/app/setup/handlers/setup.json b/config/app/setup/handlers/setup.json index 624dd6479..847444ef1 100644 --- a/config/app/setup/handlers/setup.json +++ b/config/app/setup/handlers/setup.json @@ -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", diff --git a/config/app/setup/optional.json b/config/app/setup/optional.json index d3d4bcff1..e4e566528 100644 --- a/config/app/setup/optional.json +++ b/config/app/setup/optional.json @@ -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" } } } ] diff --git a/config/app/setup/required.json b/config/app/setup/required.json index e793e4b68..769d68a28 100644 --- a/config/app/setup/required.json +++ b/config/app/setup/required.json @@ -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" } } ] } diff --git a/config/default.json b/config/default.json index c1ddb4d00..43ebb0d47 100644 --- a/config/default.json +++ b/config/default.json @@ -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." } ] } diff --git a/config/file-no-setup.json b/config/file-no-setup.json index 7aba1ade0..69f4d167d 100644 --- a/config/file-no-setup.json +++ b/config/file-no-setup.json @@ -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", diff --git a/config/file.json b/config/file.json index 1c7c5b7be..59ba4f720 100644 --- a/config/file.json +++ b/config/file.json @@ -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." } ] } diff --git a/config/identity/registration/enabled.json b/config/identity/registration/enabled.json index 2071e3c45..7e0bb04a2 100644 --- a/config/identity/registration/enabled.json +++ b/config/identity/registration/enabled.json @@ -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 } ] } diff --git a/config/memory-subdomains.json b/config/memory-subdomains.json index 80ab274f2..eb802f774 100644 --- a/config/memory-subdomains.json +++ b/config/memory-subdomains.json @@ -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." } ] } diff --git a/config/path-routing.json b/config/path-routing.json index 78afc4000..7921a5da7 100644 --- a/config/path-routing.json +++ b/config/path-routing.json @@ -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", diff --git a/config/sparql-endpoint-no-setup.json b/config/sparql-endpoint-no-setup.json index fcce94f13..442e54a63 100644 --- a/config/sparql-endpoint-no-setup.json +++ b/config/sparql-endpoint-no-setup.json @@ -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", diff --git a/config/sparql-endpoint.json b/config/sparql-endpoint.json index a340825d6..139cd41b8 100644 --- a/config/sparql-endpoint.json +++ b/config/sparql-endpoint.json @@ -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." ] } diff --git a/documentation/markdown/usage/identity-provider.md b/documentation/markdown/usage/identity-provider.md index ed2721398..8dee1d359 100644 --- a/documentation/markdown/usage/identity-provider.md +++ b/documentation/markdown/usage/identity-provider.md @@ -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. diff --git a/src/identity/interaction/email-password/util/RegistrationManager.ts b/src/identity/interaction/email-password/util/RegistrationManager.ts index 623d20bf4..1abc9908d 100644 --- a/src/identity/interaction/email-password/util/RegistrationManager.ts +++ b/src/identity/interaction/email-password/util/RegistrationManager.ts @@ -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, allowRoot = false): RegistrationParams { + public validateInput(input: NodeJS.Dict, 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 { + public async register(input: RegistrationParams, allowRootPod: boolean): Promise { // 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; diff --git a/src/init/setup/SetupHttpHandler.ts b/src/init/setup/SetupHttpHandler.ts index 33c714305..3c8cbbb51 100644 --- a/src/init/setup/SetupHttpHandler.ts +++ b/src/init/setup/SetupHttpHandler.ts @@ -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; 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 { @@ -76,7 +84,7 @@ export class SetupHttpHandler extends OperationHttpHandler { * Returns the HTML representation of the setup page. */ private async handleGet(operation: Operation): Promise { - 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); } diff --git a/templates/identity/email-password/register-partial.html.ejs b/templates/identity/email-password/register-partial.html.ejs index 815bb3f08..748d0223a 100644 --- a/templates/identity/email-password/register-partial.html.ejs +++ b/templates/identity/email-password/register-partial.html.ejs @@ -56,29 +56,15 @@
    - <% if (locals.allowRoot) { %> -
  1. - -
  2. -
  3. - -
  4. - <% } %> + <% if (!locals.allowRootPod) { %>
  5. + <% } %>
@@ -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; } diff --git a/templates/identity/email-password/register.html.ejs b/templates/identity/email-password/register.html.ejs index 40944d84b..b88e6b5ec 100644 --- a/templates/identity/email-password/register.html.ejs +++ b/templates/identity/email-password/register.html.ejs @@ -3,7 +3,7 @@

- <%- include('./register-partial.html.ejs', { allowRoot: false }) %> + <%- include('./register-partial.html.ejs', { allowRootPod: false }) %>

diff --git a/templates/setup/input-partial.html.ejs b/templates/setup/input-partial.html.ejs index 03cef1958..ddbbe8728 100644 --- a/templates/setup/input-partial.html.ejs +++ b/templates/setup/input-partial.html.ejs @@ -12,12 +12,13 @@
  1. - You can disable account registration - by changing the configuration. + This can only be changed in the configuration. + See the general configuration documentation + and the identity specific options to find out how.

  2. @@ -31,7 +32,7 @@
  3. @@ -39,6 +40,8 @@
    You typically only want to choose this for rapid testing and development. +
    + This requires registration to be disabled.

@@ -48,7 +51,7 @@ Sign up <%- include('../identity/email-password/register-partial.html.ejs', { - allowRoot: true, + allowRootPod: allowRootPod, }) %> @@ -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'); }); diff --git a/test/unit/identity/interaction/email-password/util/RegistrationManager.test.ts b/test/unit/identity/interaction/email-password/util/RegistrationManager.test.ts index be4244de3..c72150aec 100644 --- a/test/unit/identity/interaction/email-password/util/RegistrationManager.test.ts +++ b/test/unit/identity/interaction/email-password/util/RegistrationManager.test.ts @@ -65,57 +65,57 @@ describe('A RegistrationManager', (): void => { describe('validating data', (): void => { it('errors on invalid emails.', async(): Promise => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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,