93 lines
6.7 KiB
TypeScript
93 lines
6.7 KiB
TypeScript
|
|
import { readFile, writeFile } from 'node:fs/promises';
|
||
|
|
|
||
|
|
type Fields = Record<string, unknown>;
|
||
|
|
type Method = [string, Fields, string];
|
||
|
|
type Result = { methodResponses: Method[] };
|
||
|
|
|
||
|
|
export async function bootstrap(runtime: string, store: Fields, fixture: { issuer: string; origins: string[]; management: number }): Promise<void> {
|
||
|
|
const token = (await readFile(`${runtime}/bootstrap.pat`, 'utf8')).trim();
|
||
|
|
async function idp(path: string, body?: Fields, method = 'POST'): Promise<Fields> {
|
||
|
|
const response = await fetch(`${fixture.issuer}${path}`, {
|
||
|
|
method, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
|
|
body: body ? JSON.stringify(body) : undefined,
|
||
|
|
});
|
||
|
|
const value = await response.json() as Fields;
|
||
|
|
if (!response.ok) throw new Error(`${path}: ${JSON.stringify(value)}`);
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
async function mail(method: string, args: Fields): Promise<Fields> {
|
||
|
|
const response = await fetch(`http://localhost:${fixture.management}/jmap/`, {
|
||
|
|
method: 'POST', headers: { Authorization: `Basic ${Buffer.from('synthetic-admin:synthetic-admin').toString('base64')}`, 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ using: ['urn:stalwart:jmap'], methodCalls: [[method, args, 't']] }),
|
||
|
|
});
|
||
|
|
const value = await response.json() as Result;
|
||
|
|
const result = value.methodResponses[0];
|
||
|
|
if (result[0] === 'error' || result[1].notCreated || result[1].notUpdated) throw new Error(`${method}: ${JSON.stringify(value)}`);
|
||
|
|
return result[1];
|
||
|
|
}
|
||
|
|
async function create(name: string, value: Fields): Promise<string> {
|
||
|
|
const result = await mail(`x:${name}/set`, { create: { test: value } });
|
||
|
|
return (result.created as Record<string, { id: string }>).test.id;
|
||
|
|
}
|
||
|
|
async function singleton(name: string, value: Fields): Promise<void> {
|
||
|
|
await mail(`x:${name}/set`, { update: { singleton: value } });
|
||
|
|
}
|
||
|
|
const project = await idp('/management/v1/projects', { name: 'Mail keys', projectRoleAssertion: true, projectRoleCheck: true, hasProjectCheck: true });
|
||
|
|
const projectId = String(project.id);
|
||
|
|
await idp(`/management/v1/projects/${projectId}/roles`, { roleKey: 'mail_user', displayName: 'Mail user', group: 'Mail' });
|
||
|
|
const app = await idp(`/management/v1/projects/${projectId}/apps/oidc`, {
|
||
|
|
name: 'Mail keys', redirectUris: fixture.origins.map(origin => `${origin}/account/oauth/callback`),
|
||
|
|
responseTypes: ['OIDC_RESPONSE_TYPE_CODE'], grantTypes: ['OIDC_GRANT_TYPE_AUTHORIZATION_CODE'],
|
||
|
|
appType: 'OIDC_APP_TYPE_USER_AGENT', authMethodType: 'OIDC_AUTH_METHOD_TYPE_NONE',
|
||
|
|
postLogoutRedirectUris: fixture.origins.map(origin => `${origin}/account/oauth/logout`),
|
||
|
|
version: 'OIDC_VERSION_1_0', devMode: true, accessTokenType: 'OIDC_TOKEN_TYPE_BEARER',
|
||
|
|
idTokenUserinfoAssertion: true,
|
||
|
|
});
|
||
|
|
const users: Record<string, { id: string; password: string }> = {};
|
||
|
|
for (const name of ['alice', 'bob']) {
|
||
|
|
const password = `Synthetic-${name}-42!`;
|
||
|
|
const user = await idp('/management/v1/users/human', {
|
||
|
|
userName: `${name}@example.test`, profile: { firstName: name, lastName: 'Synthetic', preferredLanguage: 'en' },
|
||
|
|
email: { email: `${name}@example.test`, isEmailVerified: true },
|
||
|
|
initialPassword: password,
|
||
|
|
});
|
||
|
|
const id = String(user.userId);
|
||
|
|
await idp(`/management/v1/users/${id}/password`, { password, noChangeRequired: true });
|
||
|
|
await idp(`/management/v1/users/${id}/grants`, { projectId, roleKeys: ['mail_user'] });
|
||
|
|
users[name] = { id, password };
|
||
|
|
}
|
||
|
|
const domain = await create('Domain', { name: 'example.test', certificateManagement: { '@type': 'Manual' }, dkimManagement: { '@type': 'Manual' }, dnsManagement: { '@type': 'Manual' } });
|
||
|
|
const authDomain = await create('Domain', { name: 'auth.example.test', certificateManagement: { '@type': 'Manual' }, dkimManagement: { '@type': 'Manual' }, dnsManagement: { '@type': 'Manual' } });
|
||
|
|
const protocolPerms = ['authenticate', 'emailSend', 'emailReceive', 'imapAuthenticate', 'imapAppend', 'imapCapability', 'imapId',
|
||
|
|
'imapCopy', 'imapMove', 'imapCreate', 'imapDelete', 'imapEnable', 'imapExpunge', 'imapFetch', 'imapIdle',
|
||
|
|
'imapList', 'imapLsub', 'imapNamespace', 'imapRename', 'imapSearch', 'imapSort', 'imapSelect', 'imapExamine',
|
||
|
|
'imapStatus', 'imapStore', 'imapSubscribe', 'imapThread'];
|
||
|
|
const permissions = Object.fromEntries([...protocolPerms, 'authenticateWithAlias', 'sysAppPasswordGet', 'sysAppPasswordCreate', 'sysAppPasswordUpdate', 'sysAppPasswordDestroy', 'sysAppPasswordQuery'].map(name => [name, true]));
|
||
|
|
for (const [name, user] of Object.entries(users)) {
|
||
|
|
await create('Account', { '@type': 'User', name, domainId: domain,
|
||
|
|
aliases: { '0': { name: user.id, domainId: authDomain } }, credentials: {},
|
||
|
|
roles: { '@type': 'User' }, permissions: { '@type': 'Replace', enabledPermissions: permissions, disabledPermissions: {} },
|
||
|
|
memberGroupIds: {}, encryptionAtRest: { '@type': 'Disabled' }, quotas: {},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const directory = await create('Directory', { '@type': 'Oidc', description: 'Synthetic Zitadel',
|
||
|
|
issuerUrl: fixture.issuer, requireAudience: projectId, requireScopes: {},
|
||
|
|
claimUsername: 'sub', usernameDomain: 'auth.example.test', claimName: 'sub', claimGroups: null,
|
||
|
|
});
|
||
|
|
await singleton('Authentication', { directoryId: directory });
|
||
|
|
await singleton('DataStore', store);
|
||
|
|
await singleton('SearchStore', store);
|
||
|
|
await singleton('BlobStore', { '@type': 'Default' });
|
||
|
|
await singleton('Http', { useXForwarded: true });
|
||
|
|
await singleton('SystemSettings', { defaultHostname: 'mail.example.test', defaultDomainId: domain });
|
||
|
|
await create('Certificate', { certificate: { '@type': 'File', filePath: '/fixture/tls.crt' },
|
||
|
|
privateKey: { '@type': 'File', filePath: '/fixture/tls.key' } });
|
||
|
|
await create('NetworkListener', { name: 'http', bind: { '0.0.0.0:18882': true }, protocol: 'http', useTls: false, tlsImplicit: false });
|
||
|
|
await create('NetworkListener', { name: 'imap', bind: { '0.0.0.0:18143': true }, protocol: 'imap', useTls: true, tlsImplicit: true });
|
||
|
|
await create('NetworkListener', { name: 'submission', bind: { '0.0.0.0:18587': true }, protocol: 'smtp', useTls: true, tlsImplicit: true });
|
||
|
|
await create('NetworkListener', { name: 'account', bind: { '127.0.0.1:8081': true }, protocol: 'http', useTls: false, tlsImplicit: false });
|
||
|
|
await create('Tracer', { '@type': 'Stdout', buffered: false, ansi: false, multiline: false, enable: true, level: 'info', events: {}, eventsPolicy: 'exclude' });
|
||
|
|
await writeFile(`${runtime}/config.json`, JSON.stringify({ issuer: fixture.issuer, clientId: app.clientId, scope: `openid profile email urn:zitadel:iam:org:project:id:${projectId}:aud` }));
|
||
|
|
await writeFile(`${runtime}/users.json`, JSON.stringify(users));
|
||
|
|
console.log('Synthetic Zitadel users and least-privilege Stalwart accounts configured.');
|
||
|
|
}
|