feat: Add support for initializing a server with a root pod

This commit is contained in:
Joachim Van Herwegen 2023-10-06 15:10:07 +02:00
parent 764b392b2b
commit 864dd7c2e0
8 changed files with 220 additions and 0 deletions

View File

@ -46,6 +46,7 @@ The following changes pertain to the imports in the default configs:
- There is a new `identity/oidc` import set that needs to be added to each config.
Options are `default.json` and `disabled.json`.
- There is a new `static-root.json` import option for `app/init`, setting a static page for the root container.
- There is a new `initialize-root-pod.json` import option for `app/init`, initializing a pod in the root.
- There are more `identity/handler` options to finetune account management availability.
- There is a new set of imports `storage/location` to determine where the root storage of the server is located.
- The `app/setup`and `identity/registration` imports have been removed.

View File

@ -11,6 +11,9 @@ Contains a list of initializer that need to be run when starting the server.
* *initialize-prefilled-root*: Similar to `initialize-root` but adds an index page to the root container.
* *initialize-intro*: Similar to `initialize-prefilled-root` but adds an index page
specific to the memory-based server of the default configuration.
* *initialize-root-pod*: Initializes the server with a pod in the root.
Email and password have not yet been set and need to be defined in the base configuration.
See `file-root-pod.json` for an example.
* *static-root*: Shows a static introduction page at the server root. This is not a Solid resource.
## Main

View File

@ -0,0 +1,17 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/init/default.json",
"css:config/app/init/initializers/root-pod.json"
],
"@graph": [
{
"comment": "Initializes the root container resource.",
"@id": "urn:solid-server:default:PrimaryParallelInitializer",
"@type": "ParallelHandler",
"handlers": [
{ "@id": "urn:solid-server:default:RootInitializer" }
]
}
]
}

View File

@ -0,0 +1,21 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Makes sure the root container exists and contains the necessary resources.",
"@id": "urn:solid-server:default:RootInitializer",
"@type": "ConditionalHandler",
"storageKey": "rootInitialized",
"storageValue": true,
"handleStorage": true,
"storage": { "@id": "urn:solid-server:default:SetupStorage" },
"source": {
"@id": "urn:solid-server:default:RootPodInitializer",
"@type": "AccountInitializer",
"accountStore": { "@id": "urn:solid-server:default:AccountStore" },
"passwordStore": { "@id": "urn:solid-server:default:PasswordStore" },
"podCreator": { "@id": "urn:solid-server:default:PodCreator" }
}
}
]
}

51
config/file-root-pod.json Normal file
View File

@ -0,0 +1,51 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root-pod.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",
"css:config/http/server-factory/http.json",
"css:config/http/static/default.json",
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/no-accounts.json",
"css:config/identity/oidc/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
"css:config/ldp/metadata-parser/default.json",
"css:config/ldp/metadata-writer/default.json",
"css:config/ldp/modes/default.json",
"css:config/storage/backend/file.json",
"css:config/storage/key-value/resource-store.json",
"css:config/storage/location/root.json",
"css:config/storage/middleware/default.json",
"css:config/util/auxiliary/acl.json",
"css:config/util/identifiers/suffix.json",
"css:config/util/index/default.json",
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/file.json",
"css:config/util/variables/default.json"
],
"@graph": [
{
"comment": [
"A Solid server that stores its resources on disk and uses WAC for authorization.",
"A pod will be created in the root with the email/password login defined here.",
"It is advised to immediately change this password after starting the server."
]
},
{
"@id": "urn:solid-server:default:RootPodInitializer",
"@type": "AccountInitializer",
"email": "test@example.com",
"password": "secret!"
}
]
}

View File

@ -0,0 +1,70 @@
import { Initializer } from '../init/Initializer';
import { getLoggerFor } from '../logging/LogUtil';
import type { AccountStore } from './interaction/account/util/AccountStore';
import type { PasswordStore } from './interaction/password/util/PasswordStore';
import type { PodCreator } from './interaction/pod/util/PodCreator';
export interface AccountInitializerArgs {
/**
* Creates the accounts.
*/
accountStore: AccountStore;
/**
* Adds the login methods.
*/
passwordStore: PasswordStore;
/**
* Creates the pods.
*/
podCreator: PodCreator;
/**
* Email address for the account login.
*/
email: string;
/**
* Password for the account login.
*/
password: string;
/**
* Name to use for the pod. If undefined the pod will be made in the root of the server.
*/
name?: string;
}
/**
* Initializes an account with email/password login and a pod with the provided name.
*/
export class AccountInitializer extends Initializer {
protected readonly logger = getLoggerFor(this);
private readonly accountStore: AccountStore;
private readonly passwordStore: PasswordStore;
private readonly podCreator: PodCreator;
private email: string | undefined;
private password: string | undefined;
private readonly name: string | undefined;
public constructor(args: AccountInitializerArgs) {
super();
this.accountStore = args.accountStore;
this.passwordStore = args.passwordStore;
this.podCreator = args.podCreator;
this.email = args.email;
this.password = args.password;
this.name = args.name;
}
public async handle(): Promise<void> {
this.logger.info(`Creating account for ${this.email}`);
const accountId = await this.accountStore.create();
const id = await this.passwordStore.create(this.email!, accountId, this.password!);
await this.passwordStore.confirmVerification(id);
this.logger.info(`Creating pod ${this.name ? `with name ${this.name}` : 'at the root'}`);
await this.podCreator.handleSafe({ accountId, name: this.name });
// Not really necessary but don't want to keep passwords in memory if not required
delete this.email;
delete this.password;
}
}

View File

@ -258,7 +258,9 @@ export * from './identity/storage/PassthroughAdapterFactory';
export * from './identity/storage/WebIdAdapterFactory';
// Identity
export * from './identity/AccountInitializer';
export * from './identity/IdentityProviderHttpHandler';
export * from './identity/IdentityUtil';
export * from './identity/OidcHttpHandler';
// Init/Cluster

View File

@ -0,0 +1,55 @@
import { AccountInitializer } from '../../../src/identity/AccountInitializer';
import { AccountStore } from '../../../src/identity/interaction/account/util/AccountStore';
import { PasswordStore } from '../../../src/identity/interaction/password/util/PasswordStore';
import { PodCreator } from '../../../src/identity/interaction/pod/util/PodCreator';
describe('An AccountInitializer', (): void => {
const email = 'email@example.com';
const password = 'password!';
let accountStore: jest.Mocked<AccountStore>;
let passwordStore: jest.Mocked<PasswordStore>;
let podCreator: jest.Mocked<PodCreator>;
let initializer: AccountInitializer;
beforeEach(async(): Promise<void> => {
accountStore = {
create: jest.fn().mockResolvedValue('account-id'),
} satisfies Partial<AccountStore> as any;
passwordStore = {
create: jest.fn().mockResolvedValue('password-id'),
confirmVerification: jest.fn(),
} satisfies Partial<PasswordStore> as any;
podCreator = {
handleSafe: jest.fn(),
} satisfies Partial<PodCreator> as any;
initializer = new AccountInitializer({
accountStore, passwordStore, podCreator, email, password,
});
});
it('creates the account/login/pod.', async(): Promise<void> => {
await expect(initializer.handle()).resolves.toBeUndefined();
expect(accountStore.create).toHaveBeenCalledTimes(1);
expect(passwordStore.create).toHaveBeenCalledTimes(1);
expect(passwordStore.create).toHaveBeenLastCalledWith(email, 'account-id', password);
expect(passwordStore.confirmVerification).toHaveBeenCalledTimes(1);
expect(podCreator.handleSafe).toHaveBeenCalledTimes(1);
expect(podCreator.handleSafe).toHaveBeenLastCalledWith({ accountId: 'account-id' });
});
it('can create a pod with a name.', async(): Promise<void> => {
initializer = new AccountInitializer({
accountStore, passwordStore, podCreator, email, password, name: 'name',
});
await expect(initializer.handle()).resolves.toBeUndefined();
expect(accountStore.create).toHaveBeenCalledTimes(1);
expect(passwordStore.create).toHaveBeenCalledTimes(1);
expect(passwordStore.create).toHaveBeenLastCalledWith(email, 'account-id', password);
expect(passwordStore.confirmVerification).toHaveBeenCalledTimes(1);
expect(podCreator.handleSafe).toHaveBeenCalledTimes(1);
expect(podCreator.handleSafe).toHaveBeenLastCalledWith({ accountId: 'account-id', name: 'name' });
});
});