mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Prevent accidental nested storages
This commit is contained in:
parent
cf74ce3d2a
commit
4d9d1b90b0
@ -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.
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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."
|
||||
]
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user