Skip to content

Commit

Permalink
Create CSS accounts.
Browse files Browse the repository at this point in the history
  • Loading branch information
RubenVerborgh committed Sep 27, 2023
1 parent a642b28 commit 79f2d76
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 3 deletions.
107 changes: 104 additions & 3 deletions copy-pods-to-css.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env node
#!/usr/bin/env node --no-warnings
import assert from 'node:assert';
import { resolve } from 'node:path';
import { opendir, readFile } from 'node:fs/promises';

const expectedArgs = {
'nss/config.json': 'path to the NSS configuration file',
'https://css.pod/': 'URL to the running CSS server instance',
'[email protected]': 'e-mail pattern to generate CSS usernames',
};

main(...process.argv).catch(console.error);
Expand All @@ -23,11 +25,14 @@ async function main(bin, script, ...args) {

// Copies the pods and accounts from NSS disk storage
// to a CSS instance and associated disk storage
async function copyNssPodsToCSS(nssConfigPath) {
async function copyNssPodsToCSS(nssConfigPath, cssUrl, emailPattern) {
print('1️⃣ NSS: Read pod configurations from disk');
const nss = await readNssConfig(nssConfigPath);
const pods = await readPodConfigs(nss.dbPath);
print(`Found ${pods.length} pods`);

print(`2️⃣ CSS: Create ${pods.length} accounts with pods via HTTP`);
const accounts = await createAccounts(pods, cssUrl, emailPattern);
print(`Created ${Object.keys(accounts).length} accounts`);
}

// Reads the configuration of an NSS instance
Expand Down Expand Up @@ -68,6 +73,102 @@ async function readPodConfig(configPath) {
return pod;
}

// Creates a CSS account and pod for each of the NSS pods
async function createAccounts(pods, cssUrl, emailPattern) {
const accounts = {};
const emailDomain = emailPattern.replace(/.*@+/, '');
const { account: { create } } = await getAccountControls(cssUrl);
for (const pod of pods) {
try {
const account = await createAccount(create, pod.username, emailDomain);
accounts[account.id] = { ...pod, ...account };
}
catch { /* Skip unsuccessful accounts */ }
}
return accounts;
}

// Creates a CSS account with a login and pod
async function createAccount(creationUrl, name, emailDomain) {
const checks = { account: false, login: false, pod: false };

try {
// Create and obtain a new empty account
const { resource, cookie } = await cssPost(creationUrl);
const { controls } = await cssGet(resource, cookie);
const [, id] = /account\/([^/]+)\/$/.exec(resource);
checks.account = true;

// Create a login to the account with a temporary password
const password = generateRandomPassword();
// We have to generate a new e-mail address per pod,
// since NSS does not perform e-mail validation on sign-up.
// As such, there exists a security risk in which
// an attacker registers a bogus pod with someone else's e-mail,
// in an attempt to gain access to all pods under that e-mail.
const email = `${name}@${emailDomain}`;
await cssPost(controls.password.create, { email, password }, cookie);
checks.login = true;

// Create a pod under the account
await cssPost(controls.account.pod, { name }, cookie);
checks.pod = true;

return { id, email, cookie, controls };
}
finally {
assert(printChecks(name, checks), 'Could not create account');
}
}

// Retrieves the CSS hypermedia controls for the account API
async function getAccountControls(cssUrl) {
try {
const body = await cssGet(new URL('.account/', cssUrl));
assert.equal(body.version, '0.5', 'Unsupported CSS account API');
return body.controls;
}
catch (cause) {
throw new Error(`Could not access CSS configuration at ${cssUrl}`, { cause });
}
}

// Retrieves JSON from CSS via an authenticated HTTP request
async function cssGet(url, cookie = '') {
return cssFetch(url, {}, cookie);
}

// Posts JSON to CSS via an authenticated HTTP request
async function cssPost(url, body = {}, cookie = '') {
return cssFetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
}, cookie);
}

// Performs an authenticated HTTP request on CSS
async function cssFetch(url, options = {}, cookie = '') {
const response = await fetch(url, {
...options,
headers: {
...options.headers,
accept: 'application/json',
cookie: `css-account=${cookie}`,
},
});
const json = await response.json();
if (response.status !== 200)
throw new Error(json.message);
return json;
}

// Generates a random password
function generateRandomPassword(length = 32) {
return new Array(length).fill(0).map(() =>
String.fromCharCode(65 + Math.floor(58 * Math.random()))).join('');
}

// Prints a message to the console
function print(message) {
process.stdout.write(`${message}\n`);
Expand Down
41 changes: 41 additions & 0 deletions css-file-subdomain-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"@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/static-root.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/default.json",
"css:config/identity/interaction/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/pod.json",
"css:config/storage/middleware/default.json",
"css:config/util/auxiliary/acl.json",
"css:config/util/identifiers/subdomain.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, uses WAC for authorization, and subdomains for pods."
}
]
}

0 comments on commit 79f2d76

Please sign in to comment.