Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
113 lines
4.4 KiB
JavaScript
113 lines
4.4 KiB
JavaScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
// auth.mjs reads its file paths from env at module load, so point them at a
|
|
// throwaway dir before importing.
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-auth-'));
|
|
process.env.ALGA_APPLIANCE_TOKEN_FILE = path.join(tmp, 'setup-token');
|
|
process.env.ALGA_APPLIANCE_ADMIN_CREDENTIAL_FILE = path.join(tmp, 'admin-ui-credential.json');
|
|
process.env.ALGA_APPLIANCE_SESSION_SECRET_FILE = path.join(tmp, 'session-secret');
|
|
|
|
const auth = await import('../auth.mjs');
|
|
|
|
function fakeReq(cookie) {
|
|
return { headers: cookie ? { cookie } : {} };
|
|
}
|
|
|
|
test('generateToken yields five groups of four digits', () => {
|
|
assert.match(auth.generateToken(), /^\d{4}-\d{4}-\d{4}-\d{4}-\d{4}$/);
|
|
});
|
|
|
|
test('writeSetupToken is world-readable so the pod can read it', () => {
|
|
auth.writeSetupToken('1111-2222-3333-4444-5555');
|
|
const mode = fs.statSync(process.env.ALGA_APPLIANCE_TOKEN_FILE).mode & 0o777;
|
|
assert.equal(mode, 0o644);
|
|
assert.equal(auth.readSetupToken(), '1111-2222-3333-4444-5555');
|
|
});
|
|
|
|
test('tokensMatch compares against the stored token', () => {
|
|
auth.writeSetupToken('1111-2222-3333-4444-5555');
|
|
assert.equal(auth.tokensMatch('1111-2222-3333-4444-5555'), true);
|
|
assert.equal(auth.tokensMatch(' 1111-2222-3333-4444-5555 '), true); // trims
|
|
assert.equal(auth.tokensMatch('0000-0000-0000-0000-0000'), false);
|
|
assert.equal(auth.tokensMatch(''), false);
|
|
});
|
|
|
|
test('credential lifecycle: unset -> configured -> verify -> clear', () => {
|
|
auth.clearCredential();
|
|
assert.equal(auth.getCredentialState().status, 'unset');
|
|
assert.equal(auth.verifyPassword('whatever'), false);
|
|
|
|
auth.setPassword('Str0ng!Pass');
|
|
assert.equal(auth.getCredentialState().status, 'configured');
|
|
assert.equal(auth.verifyPassword('Str0ng!Pass'), true);
|
|
assert.equal(auth.verifyPassword('wrong'), false);
|
|
|
|
// The stored credential must never contain the plaintext password.
|
|
const stored = fs.readFileSync(process.env.ALGA_APPLIANCE_ADMIN_CREDENTIAL_FILE, 'utf8');
|
|
assert.doesNotMatch(stored, /Str0ng!Pass/);
|
|
|
|
auth.clearCredential();
|
|
assert.equal(auth.getCredentialState().status, 'unset');
|
|
});
|
|
|
|
test('session token verifies, rejects tampering, and honours expiry', () => {
|
|
const token = auth.createSessionToken(3600);
|
|
assert.equal(auth.verifySessionToken(token), true);
|
|
|
|
const tampered = `${token.slice(0, -1)}${token.endsWith('a') ? 'b' : 'a'}`;
|
|
assert.equal(auth.verifySessionToken(tampered), false);
|
|
|
|
assert.equal(auth.verifySessionToken(auth.createSessionToken(-1)), false); // already expired
|
|
assert.equal(auth.verifySessionToken('garbage'), false);
|
|
assert.equal(auth.verifySessionToken(''), false);
|
|
});
|
|
|
|
test('rotateSessionSecret invalidates previously issued tokens', () => {
|
|
const token = auth.createSessionToken(3600);
|
|
assert.equal(auth.verifySessionToken(token), true);
|
|
auth.rotateSessionSecret();
|
|
assert.equal(auth.verifySessionToken(token), false);
|
|
});
|
|
|
|
test('isAuthenticated reads the session cookie', () => {
|
|
const cookie = auth.sessionCookieHeader(3600).split(';')[0]; // "alga_appliance_session=..."
|
|
assert.equal(auth.isAuthenticated(fakeReq(cookie)), true);
|
|
assert.equal(auth.isAuthenticated(fakeReq('other=1')), false);
|
|
assert.equal(auth.isAuthenticated(fakeReq()), false);
|
|
});
|
|
|
|
test('sessionCookieHeader sets HttpOnly + SameSite=Strict', () => {
|
|
const header = auth.sessionCookieHeader(3600);
|
|
assert.match(header, /HttpOnly/);
|
|
assert.match(header, /SameSite=Strict/);
|
|
assert.match(header, /Path=\//);
|
|
});
|
|
|
|
test('authPhase reflects credential + session state', () => {
|
|
auth.clearCredential();
|
|
assert.equal(auth.authPhase(fakeReq()), 'needs-token');
|
|
auth.setPassword('Str0ng!Pass');
|
|
assert.equal(auth.authPhase(fakeReq()), 'needs-password');
|
|
const cookie = auth.sessionCookieHeader(3600).split(';')[0];
|
|
assert.equal(auth.authPhase(fakeReq(cookie)), 'authenticated');
|
|
auth.clearCredential();
|
|
});
|
|
|
|
test('lockout engages after repeated failures and clears on success', () => {
|
|
auth._resetLockoutState();
|
|
const ip = '203.0.113.7';
|
|
assert.equal(auth.checkLockout(ip).locked, false);
|
|
for (let i = 0; i < 5; i += 1) auth.registerFailure(ip);
|
|
assert.equal(auth.checkLockout(ip).locked, true);
|
|
assert.ok(auth.checkLockout(ip).retryAfterMs > 0);
|
|
|
|
auth._resetLockoutState();
|
|
for (let i = 0; i < 4; i += 1) auth.registerFailure(ip);
|
|
auth.registerSuccess(ip);
|
|
assert.equal(auth.checkLockout(ip).locked, false);
|
|
});
|