Add OIDC sign-in, group-derived roles and declarative instance configuration
Workspaces, group mappings and sign-in methods now come from a configuration file synchronized at startup; workspaces dropped from it are disabled, never deleted. Instance administrators (a new role) see and help every workspace and may delete disabled ones; workspace administrators and employees are derived from the user's groups at every SSO sign-in or administrator edit, and the employee permission set belongs to the workspace. Self-registration is gone: users come from SSO (only when their groups map) or from the administrator. Break-glass bootstrap accounts get an HMAC-derived hourly password that all replicas agree on. First SSO sign-in links a matching email contact to the user. The demo runs on the same model with a generated per-start password. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
e37d583179
commit
4764f3545f
68 files changed
+7220
-964
No files matched your search
@@ -1,4 +1,4 @@
|
||||
const {openSetup}=require('./support/browser.cjs');
|
||||
const {openSetup,isolatedUser,signIn,demoPassword}=require('./support/browser.cjs');
|
||||
|
||||
const assert=require('node:assert/strict');
|
||||
const fs=require('node:fs');
|
||||
@@ -13,9 +13,9 @@ fs.mkdirSync('test-results',{recursive:true});
|
||||
const save=async(name='Save changes')=>{await dialog().getByRole('button',{name,exact:true}).click();await dialog().waitFor({state:'hidden'});};
|
||||
const checked=()=>page.waitForFunction(()=>{const a=Alpine.$data(document.body);return a.generationResult && !a.generationLoading && a.generationResult.key===JSON.stringify(a.generationPayload());});
|
||||
try {
|
||||
await page.goto(base);await page.locator('.demo-note').waitFor();await page.getByRole('status',{name:'Demo data retention',exact:true}).waitFor();assert.equal(await page.locator('.demo-banner button').count(),0);assert.match(await page.locator('.demo-protection').innerText(),/left untouched/);assert.match(await page.locator('.demo-note').innerText(),/alice[\s\S]*all four workspaces[\s\S]*bob[\s\S]*carol/);
|
||||
await page.goto(base);await page.locator('.demo-note').waitFor();await page.getByRole('status',{name:'Demo data retention',exact:true}).waitFor();assert.equal(await page.locator('.demo-banner button').count(),0);assert.match(await page.locator('.demo-protection').innerText(),/left untouched/);for(const [name,role] of [['admin','Instance administrator'],['alice','Workspace administrator'],['bob','Employee']])assert.match(await page.locator(`[data-demo-account=${name}]`).innerText(),new RegExp(role));assert.equal((await page.locator('[data-demo-password]').innerText()).trim(),await demoPassword());
|
||||
await page.screenshot({path:'test-results/demo-login.png',fullPage:true});
|
||||
await page.getByLabel('Username',{exact:true}).fill('alice');await page.getByLabel('Password',{exact:true}).fill('meguri-demo-only');await page.getByRole('button',{name:'Sign in',exact:true}).click();await page.waitForFunction(()=>Alpine.$data(document.body).workspace);
|
||||
await page.getByLabel('Username',{exact:true}).fill('alice');await page.getByLabel('Password',{exact:true}).fill(await require('./support/browser.cjs').demoPassword());await page.getByRole('button',{name:'Sign in',exact:true}).click();await page.waitForFunction(()=>Alpine.$data(document.body).workspace);
|
||||
const demo=await page.evaluate(()=>{const a=Alpine.$data(document.body);return {matrix:a.directoryState.workspaces.find(t=>t.name==='Acme Engineering'),support:a.directoryState.workspaces.find(t=>t.name==='Customer Support')};});
|
||||
assert(demo.matrix && demo.support);assert.notEqual(demo.matrix.id,demo.support.id);
|
||||
await page.getByLabel('Workspace',{exact:true}).selectOption(demo.matrix.id);await openSetup(page,'Variables & rules');await checked();
|
||||
@@ -40,11 +40,12 @@ fs.mkdirSync('test-results',{recursive:true});
|
||||
await openSetup(page,'Escalations & routing');await page.screenshot({path:'test-results/support-demo.png',fullPage:true});
|
||||
// Distinct members cannot read the other example's team, even with its URL.
|
||||
for(const [username,denied] of [['bob',demo.support],['dave',demo.support]]){
|
||||
const ctx=await browser.newContext();const api=ctx.request;const login=await api.post(base+'/api/login',{headers:{Origin:base},data:{username,password:'meguri-demo-only'}});assert.equal(login.status(),200);const response=await api.get(`${base}/api/workspaces/${denied.id}`);assert.equal(response.status(),404);await ctx.close();
|
||||
const ctx=await browser.newContext();const api=ctx.request;const login=await api.post(base+'/api/login',{headers:{Origin:base},data:{username,password:await require('./support/browser.cjs').demoPassword()}});assert.equal(login.status(),200);const response=await api.get(`${base}/api/workspaces/${denied.id}`);assert.equal(response.status(),404);await ctx.close();
|
||||
}
|
||||
// Mutating UI checks use a separate account, leaving the examples untouched.
|
||||
await page.getByRole('button',{name:'Sign out',exact:true}).click();await page.getByRole('button',{name:'New here? Create an account',exact:true}).click();const unique=Date.now();await page.getByLabel('Username',{exact:true}).fill('matrix-'+unique);await page.getByLabel('Display name',{exact:true}).fill('Matrix tester');await page.getByLabel('Password',{exact:true}).fill('local-browser-test-password');await page.getByRole('button',{name:'Create account',exact:true}).click();
|
||||
await page.getByRole('button',{name:'New workspace',exact:true}).click();await dialog().getByLabel('Name',{exact:true}).fill('Matrix '+unique);await save();
|
||||
// Mutating UI checks use Sandbox 5 and a separate administrator, leaving the demo examples untouched.
|
||||
await page.getByRole('button',{name:'Sign out',exact:true}).click();
|
||||
const tester=await isolatedUser({sandbox:5,prefix:'matrix',display:'Matrix tester'});
|
||||
await signIn(page,tester.username,tester.password);await page.getByLabel('Workspace',{exact:true}).waitFor();
|
||||
await openSetup(page,'Variables & rules');
|
||||
await page.evaluate(()=>{const a=Alpine.$data(document.body),projects={id:crypto.randomUUID(),name:'projects',node:a.newNode('list')},responsible={id:crypto.randomUUID(),name:'responsible',node:a.newNode('dictionary')};for(const name of ['A','B']){projects.node.entries.push(a.newNode('text',name));const entry=a.newNode('list',null,name);entry.entries=['alice@example.com','bob@example.com'].map(email=>a.newNode('person',email));responsible.node.entries.push(entry);}const iteration={id:crypto.randomUUID(),name:'Project',source:{kind:'variable',variable:projects.id,path:[]}},block=a.newBlock('rotation');block.fields.name={kind:'item',iteration:iteration.id,path:[],suffix:' on call'};block.fields.participants={kind:'lookup',variable:responsible.id,key:{kind:'item',iteration:iteration.id,path:[]},path:[]};a.generationDraft.variables.push(projects,responsible);a.generationDraft.rules.push({id:crypto.randomUUID(),name:'Project rotations',enabled:true,iterations:[iteration],when:null,blocks:[block]});a.generationChanged();});await checked();
|
||||
assert.equal(await page.evaluate(()=>Alpine.$data(document.body).generationCount('create')),2);
|
||||
|
||||
Reference in new issue
Block a user