diff --git a/README.md b/README.md index 901b9c9cd..a7b7e9074 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ npm start -- # add parameters if needed ``` ### 📦 Running via Docker -Docker allows you to run the server without having Node.js installed. Images are built on each tagged version and hosted on [Docker Hub](https://hub.docker.com/r/solidproject/community-server). +Docker allows you to run the server without having Node.js installed. Images are built on each tagged version and hosted on [Docker Hub](https://hub.docker.com/r/solidproject/community-server). ```shell # Clone the repo to get access to the configs @@ -110,6 +110,7 @@ to some commonly used settings: | `--sparqlEndpoint, -s` | | URL of the SPARQL endpoint, when using a quadstore-based configuration. | | `--showStackTrace, -t` | false | Enables detailed logging on error pages. | | `--podConfigJson` | `./pod-config.json` | Path to the file that keeps track of dynamic Pod configurations. | +| `--seededPodConfigJson` | | Path to the file that keeps track of seeded Pod configurations. | | `--mainModulePath, -m` | | Path from where Components.js will start its lookup when initializing configurations. ### 🧶 Custom configurations diff --git a/config/README.md b/config/README.md index 6dca90572..2320a4958 100644 --- a/config/README.md +++ b/config/README.md @@ -10,7 +10,7 @@ it is always possible to not choose any of them and create your own custom versi # How to use The easiest way to create a new config is by creating a JSON-LD file -that imports one option from every component subfolder +that imports one option from every component subfolder (such as either `allow-all.json` or `webacl.json` from `ldp/authorization`). In case none of the available options suffice, there are 2 other ways to handle this: diff --git a/config/app/init/base/init.json b/config/app/init/base/init.json index 0036fdbe8..5972f3b45 100644 --- a/config/app/init/base/init.json +++ b/config/app/init/base/init.json @@ -4,6 +4,7 @@ "files-scs:config/app/init/initializers/base-url.json", "files-scs:config/app/init/initializers/logger.json", "files-scs:config/app/init/initializers/server.json", + "files-scs:config/app/init/initializers/seeded-pod.json", "files-scs:config/app/init/initializers/version.json" ], "@graph": [ @@ -15,6 +16,7 @@ { "@id": "urn:solid-server:default:LoggerInitializer" }, { "@id": "urn:solid-server:default:BaseUrlVerifier" }, { "@id": "urn:solid-server:default:ParallelInitializer" }, + { "@id": "urn:solid-server:default:SeededPodInitializer" }, { "@id": "urn:solid-server:default:ServerInitializer" }, { "@id": "urn:solid-server:default:ModuleVersionVerifier" } ] diff --git a/config/app/init/initializers/seeded-pod.json b/config/app/init/initializers/seeded-pod.json new file mode 100644 index 000000000..6ca116ec5 --- /dev/null +++ b/config/app/init/initializers/seeded-pod.json @@ -0,0 +1,23 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^3.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Separate manager from the RegistrationHandler in case registration is disabled.", + "@id": "urn:solid-server:default:SeededPodRegistrationManager", + "@type": "RegistrationManager", + "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "args_webIdSuffix": "/profile/card#me", + "args_identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" }, + "args_ownershipValidator": { "@id": "urn:solid-server:auth:password:OwnershipValidator" }, + "args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }, + "args_podManager": { "@id": "urn:solid-server:default:PodManager" } + }, + { + "comment": "Initializer that instantiates all the seeded accounts and pods.", + "@id": "urn:solid-server:default:SeededPodInitializer", + "@type": "SeededPodInitializer", + "registrationManager": { "@id": "urn:solid-server:default:SeededPodRegistrationManager" }, + "configFilePath": { "@id": "urn:solid-server:default:variable:seededPodConfigJson" }, + } + ] +} diff --git a/config/app/variables/cli/cli.json b/config/app/variables/cli/cli.json index 63ac5fa4f..b5129f5f0 100644 --- a/config/app/variables/cli/cli.json +++ b/config/app/variables/cli/cli.json @@ -57,6 +57,11 @@ "requiresArg": true, "type": "string", "describe": "Path to the file that keeps track of dynamic Pod configurations." + }, + "seededPodConfigJson": { + "requiresArg": true, + "type": "string", + "describe": "Path to the file that will be used to seed pods." } }, "options": { diff --git a/config/app/variables/resolver/resolver.json b/config/app/variables/resolver/resolver.json index 9f5e2e749..46dcf812f 100644 --- a/config/app/variables/resolver/resolver.json +++ b/config/app/variables/resolver/resolver.json @@ -52,12 +52,19 @@ } }, { - "CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:AssetPathResolver", + "CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:podConfigJson", "CombinedSettingsResolver:_computers_value": { "@type": "AssetPathExtractor", "key": "podConfigJson", "defaultPath": "./pod-config.json" } + }, + { + "CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:seededPodConfigJson", + "CombinedSettingsResolver:_computers_value": { + "@type": "AssetPathExtractor", + "key": "seededPodConfigJson" + } } ] } diff --git a/config/util/variables/default.json b/config/util/variables/default.json index 09f34c94c..3fa7671d5 100644 --- a/config/util/variables/default.json +++ b/config/util/variables/default.json @@ -36,6 +36,11 @@ "comment": "Path to the JSON file used to store configuration for dynamic pods.", "@id": "urn:solid-server:default:variable:podConfigJson", "@type": "Variable" + }, + { + "comment": "Path to the JSON file used to seed pods.", + "@id": "urn:solid-server:default:variable:seededPodConfigJson", + "@type": "Variable" } ] } diff --git a/guides/seeding-pods.md b/guides/seeding-pods.md new file mode 100644 index 000000000..2c9837c55 --- /dev/null +++ b/guides/seeding-pods.md @@ -0,0 +1,20 @@ +# How to seed Accounts and Pods +If you need to seed accounts and pods, set the `--seededPodConfigJson` option to a file such as `./seeded-pod-config.json` to set your desired accounts and pods. The contents of `./seeded-pod-config.json` (or whatever file name you choose) should be a JSON array whose entries are objects which include +`podName`, `email`, and `password`. For example: +```json + [ + { + "podName": "example", + "email": "hello@example.com", + "password": "abc123" + } + ] +``` + +You may optionally specify other parameters accepted by the `register` method of [RegistrationManager](https://github.com/solid/community-server/blob/3b353affb1f0919fdcb66172364234eb59c2e3f6/src/identity/interaction/email-password/util/RegistrationManager.ts#L173). For example: + +To use a pre-existing wedId: +```json + createWebId: false, + webId: "https://pod.inrupt.com/example/profile/card#me" +``` diff --git a/src/index.ts b/src/index.ts index 86f2cf068..8a8830fdc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -207,6 +207,7 @@ export * from './init/ConfigPodInitializer'; export * from './init/ContainerInitializer'; export * from './init/Initializer'; export * from './init/LoggerInitializer'; +export * from './init/SeededPodInitializer'; export * from './init/ServerInitializer'; export * from './init/ModuleVersionVerifier'; diff --git a/src/init/SeededPodInitializer.ts b/src/init/SeededPodInitializer.ts new file mode 100644 index 000000000..2c54d4d7b --- /dev/null +++ b/src/init/SeededPodInitializer.ts @@ -0,0 +1,52 @@ +import { promises as fsPromises } from 'fs'; +import type { RegistrationManager } from '../identity/interaction/email-password/util/RegistrationManager'; +import { getLoggerFor } from '../logging/LogUtil'; +import { Initializer } from './Initializer'; + +/** + * Uses a {@link RegistrationManager} to initialize accounts and pods + * for all seeded pods. Reads the pod settings from seededPodConfigJson. + */ +export class SeededPodInitializer extends Initializer { + protected readonly logger = getLoggerFor(this); + + private readonly registrationManager: RegistrationManager; + private readonly configFilePath: string | null; + + public constructor(registrationManager: RegistrationManager, configFilePath: string | null) { + super(); + this.registrationManager = registrationManager; + this.configFilePath = configFilePath; + } + + public async handle(): Promise { + if (!this.configFilePath) { + return; + } + const configText = await fsPromises.readFile(this.configFilePath, 'utf8'); + const configuration: NodeJS.Dict[] = JSON.parse(configText); + + let count = 0; + for await (const input of configuration) { + const config = { + confirmPassword: input.password, + createPod: true, + createWebId: true, + register: true, + ...input, + }; + + this.logger.info(`Initializing pod ${input.podName}`); + + // Validate the input JSON + const validated = this.registrationManager.validateInput(config, true); + this.logger.debug(`Validated input: ${JSON.stringify(validated)}`); + + // Register and/or create a pod as requested. Potentially does nothing if all booleans are false. + await this.registrationManager.register(validated, true); + this.logger.info(`Initialized seeded pod and account for "${input.podName}".`); + count += 1; + } + this.logger.info(`Initialized ${count} seeded pods.`); + } +} diff --git a/src/init/variables/extractors/AssetPathExtractor.ts b/src/init/variables/extractors/AssetPathExtractor.ts index 7c14e5760..0e6ffca90 100644 --- a/src/init/variables/extractors/AssetPathExtractor.ts +++ b/src/init/variables/extractors/AssetPathExtractor.ts @@ -18,9 +18,14 @@ export class AssetPathExtractor extends SettingsExtractor { public async handle(args: Settings): Promise { const path = args[this.key] ?? this.defaultPath; - if (typeof path !== 'string') { - throw new Error(`Invalid ${this.key} argument`); + if (path) { + if (typeof path !== 'string') { + throw new Error(`Invalid ${this.key} argument`); + } + + return resolveAssetPath(path); } - return resolveAssetPath(path); + + return null; } } diff --git a/test/integration/Config.ts b/test/integration/Config.ts index 796890f80..2d22630ec 100644 --- a/test/integration/Config.ts +++ b/test/integration/Config.ts @@ -48,5 +48,6 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record { getTestConfigPath('server-without-auth.json'), getDefaultVariables(port, 'https://example.pod/'), ) as App; + await app.start(); }); diff --git a/test/integration/config/ldp-with-auth.json b/test/integration/config/ldp-with-auth.json index 4ef09bd4d..5a1caf497 100644 --- a/test/integration/config/ldp-with-auth.json +++ b/test/integration/config/ldp-with-auth.json @@ -10,6 +10,8 @@ "files-scs:config/http/static/default.json", "files-scs:config/identity/access/public.json", "files-scs:config/identity/handler/default.json", + "files-scs:config/identity/ownership/token.json", + "files-scs:config/identity/pod/static.json", "files-scs:config/ldp/authentication/debug-auth-header.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", diff --git a/test/integration/config/run-with-redlock.json b/test/integration/config/run-with-redlock.json index ae8164828..d639e7819 100644 --- a/test/integration/config/run-with-redlock.json +++ b/test/integration/config/run-with-redlock.json @@ -8,6 +8,9 @@ "files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/handler/account-store/default.json", + "files-scs:config/identity/ownership/unsafe-no-check.json", + "files-scs:config/identity/pod/static.json", "files-scs:config/ldp/authentication/debug-auth-header.json", "files-scs:config/ldp/authorization/allow-all.json", "files-scs:config/ldp/handler/default.json", diff --git a/test/integration/config/server-without-auth.json b/test/integration/config/server-without-auth.json index 5b9bdbf12..25212c2cc 100644 --- a/test/integration/config/server-without-auth.json +++ b/test/integration/config/server-without-auth.json @@ -8,6 +8,9 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/handler/account-store/default.json", + "files-scs:config/identity/ownership/unsafe-no-check.json", + "files-scs:config/identity/pod/static.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/allow-all.json", "files-scs:config/ldp/handler/default.json", diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index 4ed64b7be..2c41aa1c0 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -62,6 +62,7 @@ describe('AppRunner', (): void => { 'urn:solid-server:default:variable:rootFilePath': '/var/cwd/', 'urn:solid-server:default:variable:showStackTrace': false, 'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json', + 'urn:solid-server:default:variable:seededPodConfigJson': '/var/cwd/seeded-pod-config.json', }; const createdApp = await new AppRunner().create( { @@ -99,6 +100,7 @@ describe('AppRunner', (): void => { 'urn:solid-server:default:variable:rootFilePath': '/var/cwd/', 'urn:solid-server:default:variable:showStackTrace': false, 'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json', + 'urn:solid-server:default:variable:seededPodConfigJson': '/var/cwd/seeded-pod-config.json', }; await new AppRunner().run( { @@ -166,6 +168,7 @@ describe('AppRunner', (): void => { '-s', 'http://localhost:5000/sparql', '-t', '--podConfigJson', '/different-path.json', + '--seededPodConfigJson', '/different-path.json', ]; process.argv = argvParameters; diff --git a/test/unit/init/SeededPodInitializer.test.ts b/test/unit/init/SeededPodInitializer.test.ts new file mode 100644 index 000000000..fdbe2a71e --- /dev/null +++ b/test/unit/init/SeededPodInitializer.test.ts @@ -0,0 +1,47 @@ +import { promises as fsPromises } from 'fs'; +import type { RegistrationManager } from '../../../src/identity/interaction/email-password/util/RegistrationManager'; +import { SeededPodInitializer } from '../../../src/init/SeededPodInitializer'; +import { mockFs } from '../../util/Util'; + +jest.mock('fs'); + +describe('A SeededPodInitializer', (): void => { + const dummyConfig = JSON.stringify([ + { + podName: 'example', + email: 'hello@example.com', + password: 'abc123', + }, + { + podName: 'example2', + email: 'hello2@example.com', + password: '123abc', + }, + ]); + let registrationManager: RegistrationManager; + let configFilePath: string | null; + + beforeEach(async(): Promise => { + configFilePath = './seeded-pod-config.json'; + registrationManager = { + validateInput: jest.fn((input): any => input), + register: jest.fn(), + } as any; + + mockFs('/'); + await fsPromises.writeFile(configFilePath, dummyConfig); + }); + + it('does not generate any accounts or pods if no config file is specified.', async(): Promise => { + configFilePath = null; + await new SeededPodInitializer(registrationManager, configFilePath).handle(); + expect(registrationManager.validateInput).not.toHaveBeenCalled(); + expect(registrationManager.register).not.toHaveBeenCalled(); + }); + + it('generates an account and a pod for every entry in the seeded pod configuration.', async(): Promise => { + await new SeededPodInitializer(registrationManager, configFilePath).handle(); + expect(registrationManager.validateInput).toHaveBeenCalledTimes(2); + expect(registrationManager.register).toHaveBeenCalledTimes(2); + }); +}); diff --git a/test/unit/init/variables/extractors/AssetPathExtractor.test.ts b/test/unit/init/variables/extractors/AssetPathExtractor.test.ts index 352d5d5d8..4b57f792b 100644 --- a/test/unit/init/variables/extractors/AssetPathExtractor.test.ts +++ b/test/unit/init/variables/extractors/AssetPathExtractor.test.ts @@ -25,4 +25,9 @@ describe('An AssetPathExtractor', (): void => { resolver = new AssetPathExtractor('path', '/root'); await expect(resolver.handle({ otherPath: '/var/data' })).resolves.toBe('/root'); }); + + it('returns null if not default value or default is provided.', async(): Promise => { + resolver = new AssetPathExtractor('path'); + await expect(resolver.handle({ otherPath: '/var/data' })).resolves.toBeNull(); + }); });