add configurable club login domains in RMS software
This commit is contained in:
@@ -173,12 +173,25 @@ test("plugin-based auth methods support link and OTP flows", async (t) => {
|
||||
assert.ok(adminVerify.accessToken, "admin access token expected");
|
||||
const adminHeaders = { Authorization: `Bearer ${adminVerify.accessToken}` };
|
||||
|
||||
let forbiddenDomainError = null;
|
||||
try {
|
||||
await requestJson(baseUrl, "/v1/auth/request-access", {
|
||||
method: "POST",
|
||||
body: { email: "outsider@gmail.com" }
|
||||
});
|
||||
} catch (error) {
|
||||
forbiddenDomainError = error;
|
||||
}
|
||||
assert.ok(forbiddenDomainError, "non-club domains should be rejected immediately");
|
||||
assert.equal(forbiddenDomainError.status, 403);
|
||||
assert.equal(forbiddenDomainError.payload && forbiddenDomainError.payload.error && forbiddenDomainError.payload.error.code, "auth.email.domain_forbidden");
|
||||
|
||||
const guestStart = await requestJson(baseUrl, "/v1/auth/request-access", {
|
||||
method: "POST",
|
||||
body: { email: "guest@example.com" }
|
||||
body: { email: "guest@oevsv.at" }
|
||||
});
|
||||
const outbox2 = await readOutbox(dataDir);
|
||||
const guestVerifyMail = [...outbox2].reverse().find((entry) => entry.to === "guest@example.com");
|
||||
const guestVerifyMail = [...outbox2].reverse().find((entry) => entry.to === "guest@oevsv.at");
|
||||
assert.ok(guestVerifyMail, "guest verify challenge must be written");
|
||||
|
||||
let guestVerify;
|
||||
@@ -187,7 +200,7 @@ test("plugin-based auth methods support link and OTP flows", async (t) => {
|
||||
assert.ok(guestCode, "guest otp code must be parsable");
|
||||
guestVerify = await requestJson(baseUrl, "/v1/auth/verify-email", {
|
||||
method: "POST",
|
||||
body: { email: "guest@example.com", code: guestCode }
|
||||
body: { email: "guest@oevsv.at", code: guestCode }
|
||||
});
|
||||
} else {
|
||||
const guestVerifyToken = extractTokenFromText(guestVerifyMail.text);
|
||||
@@ -200,7 +213,7 @@ test("plugin-based auth methods support link and OTP flows", async (t) => {
|
||||
assert.equal(guestVerify.approved, false, "external domain should not auto-approve");
|
||||
|
||||
const approvals = await requestJson(baseUrl, "/v1/approvals", { headers: adminHeaders });
|
||||
const pending = (approvals.approvals || []).find((entry) => entry.email === "guest@example.com" && entry.status === "pending");
|
||||
const pending = (approvals.approvals || []).find((entry) => entry.email === "guest@oevsv.at" && entry.status === "pending");
|
||||
assert.ok(pending, "approval request for external user expected");
|
||||
|
||||
await requestJson(baseUrl, `/v1/approvals/${encodeURIComponent(pending.id)}/approve`, {
|
||||
@@ -210,7 +223,7 @@ test("plugin-based auth methods support link and OTP flows", async (t) => {
|
||||
});
|
||||
|
||||
const users = await requestJson(baseUrl, "/v1/admin/users", { headers: adminHeaders });
|
||||
const guest = (users.users || []).find((entry) => entry.email === "guest@example.com");
|
||||
const guest = (users.users || []).find((entry) => entry.email === "guest@oevsv.at");
|
||||
assert.ok(guest, "guest user must exist");
|
||||
|
||||
await requestJson(baseUrl, `/v1/admin/users/${encodeURIComponent(guest.id)}/auth-methods`, {
|
||||
@@ -224,14 +237,14 @@ test("plugin-based auth methods support link and OTP flows", async (t) => {
|
||||
|
||||
const otpStart = await requestJson(baseUrl, "/v1/auth/request-access", {
|
||||
method: "POST",
|
||||
body: { email: "guest@example.com", method: "otp-email" }
|
||||
body: { email: "guest@oevsv.at", method: "otp-email" }
|
||||
});
|
||||
assert.equal(otpStart.challengeType, "otp", "OTP challenge should be selected");
|
||||
|
||||
const outbox3 = await readOutbox(dataDir);
|
||||
const guestOtpMail = [...outbox3]
|
||||
.reverse()
|
||||
.find((entry) => entry.to === "guest@example.com" && /Code lautet:\s*\d{6}/.test(entry.text));
|
||||
.find((entry) => entry.to === "guest@oevsv.at" && /Code lautet:\s*\d{6}/.test(entry.text));
|
||||
assert.ok(guestOtpMail, "guest OTP mail must be written");
|
||||
const otpMatch = /Code lautet:\s*(\d{6})/.exec(guestOtpMail.text);
|
||||
assert.ok(otpMatch, "otp code must be parsable");
|
||||
@@ -239,10 +252,10 @@ test("plugin-based auth methods support link and OTP flows", async (t) => {
|
||||
const otpVerify = await requestJson(baseUrl, "/v1/auth/verify-email", {
|
||||
method: "POST",
|
||||
body: {
|
||||
email: "guest@example.com",
|
||||
code: otpMatch[1]
|
||||
}
|
||||
});
|
||||
email: "guest@oevsv.at",
|
||||
code: otpMatch[1]
|
||||
}
|
||||
});
|
||||
assert.ok(otpVerify.accessToken, "guest OTP login should return token");
|
||||
|
||||
if (server.exitCode !== null && server.exitCode !== 0) {
|
||||
@@ -346,6 +359,80 @@ test("maintenance mode keeps admin active and blocks non-admin login", async (t)
|
||||
}
|
||||
});
|
||||
|
||||
test("admin can configure allowed login domains", async (t) => {
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "rms-auth-domains-"));
|
||||
const port = randomPort();
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
|
||||
const server = spawn(process.execPath, ["server/index.js"], {
|
||||
cwd: rootDir,
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
DATA_DIR: dataDir,
|
||||
ADMIN_EMAILS: "admin@arcg.at",
|
||||
PRIMARY_EMAIL_DOMAIN: "arcg.at",
|
||||
PUBLIC_BASE_URL: baseUrl
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
});
|
||||
|
||||
let serverStdErr = "";
|
||||
server.stderr.on("data", (chunk) => {
|
||||
serverStdErr += String(chunk);
|
||||
});
|
||||
|
||||
t.after(async () => {
|
||||
if (!server.killed) {
|
||||
server.kill("SIGTERM");
|
||||
}
|
||||
await fs.rm(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
await waitForServer(baseUrl);
|
||||
|
||||
const adminVerify = await requestAccessAndVerify(baseUrl, dataDir, "admin@arcg.at");
|
||||
const adminHeaders = { Authorization: `Bearer ${adminVerify.accessToken}` };
|
||||
|
||||
const initial = await requestJson(baseUrl, "/v1/admin/login-domains", { headers: adminHeaders });
|
||||
assert.equal(initial.ok, true);
|
||||
assert.ok(Array.isArray(initial.domains));
|
||||
assert.ok(initial.domains.includes("arcg.at"));
|
||||
assert.ok(initial.domains.includes("oevsv.at"));
|
||||
|
||||
const update = await requestJson(baseUrl, "/v1/admin/login-domains", {
|
||||
method: "PUT",
|
||||
headers: adminHeaders,
|
||||
body: { domains: ["arcg.at"] }
|
||||
});
|
||||
assert.equal(update.ok, true);
|
||||
assert.deepEqual(update.domains, ["arcg.at"]);
|
||||
|
||||
let forbidden = null;
|
||||
try {
|
||||
await requestJson(baseUrl, "/v1/auth/request-access", {
|
||||
method: "POST",
|
||||
body: { email: "guest@oevsv.at" }
|
||||
});
|
||||
} catch (error) {
|
||||
forbidden = error;
|
||||
}
|
||||
assert.ok(forbidden, "oevsv domain should be blocked after policy update");
|
||||
assert.equal(forbidden.status, 403);
|
||||
assert.equal(forbidden.payload && forbidden.payload.error && forbidden.payload.error.code, "auth.email.domain_forbidden");
|
||||
|
||||
const allowed = await requestJson(baseUrl, "/v1/auth/request-access", {
|
||||
method: "POST",
|
||||
body: { email: "member@arcg.at" }
|
||||
});
|
||||
assert.equal(allowed.ok, true);
|
||||
|
||||
if (server.exitCode !== null && server.exitCode !== 0) {
|
||||
throw new Error(`Server exited unexpectedly: ${server.exitCode}\n${serverStdErr}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("approver role can process external approvals without admin role", async (t) => {
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "rms-approver-test-"));
|
||||
@@ -417,11 +504,11 @@ test("approver role can process external approvals without admin role", async (t
|
||||
body: {}
|
||||
});
|
||||
|
||||
const externalVerify = await requestAccessAndVerify(baseUrl, dataDir, "remote@outside.example");
|
||||
const externalVerify = await requestAccessAndVerify(baseUrl, dataDir, "remote@oevsv.at");
|
||||
assert.equal(externalVerify.approved, false, "external user should require approval");
|
||||
|
||||
const approvalsForApprover = await requestJson(baseUrl, "/v1/approvals", { headers: approverHeaders });
|
||||
const pending = (approvalsForApprover.approvals || []).find((entry) => entry.email === "remote@outside.example" && entry.status === "pending");
|
||||
const pending = (approvalsForApprover.approvals || []).find((entry) => entry.email === "remote@oevsv.at" && entry.status === "pending");
|
||||
assert.ok(pending, "approver should see pending external request");
|
||||
|
||||
await requestJson(baseUrl, `/v1/approvals/${encodeURIComponent(pending.id)}/approve`, {
|
||||
@@ -430,7 +517,7 @@ test("approver role can process external approvals without admin role", async (t
|
||||
body: {}
|
||||
});
|
||||
|
||||
const externalLogin = await requestAccessAndVerify(baseUrl, dataDir, "remote@outside.example");
|
||||
const externalLogin = await requestAccessAndVerify(baseUrl, dataDir, "remote@oevsv.at");
|
||||
assert.ok(externalLogin.accessToken, "external user should be able to login after approver decision");
|
||||
|
||||
const operatorVerify = await requestAccessAndVerify(baseUrl, dataDir, "operator@arcg.at");
|
||||
@@ -482,7 +569,7 @@ test("rejected external user can request approval again via request-approval flo
|
||||
const adminVerify = await requestAccessAndVerify(baseUrl, dataDir, "admin@arcg.at");
|
||||
const adminHeaders = { Authorization: `Bearer ${adminVerify.accessToken}` };
|
||||
|
||||
const externalEmail = "blocked@external.example";
|
||||
const externalEmail = "blocked@oevsv.at";
|
||||
const externalVerify = await requestAccessAndVerify(baseUrl, dataDir, externalEmail);
|
||||
assert.equal(externalVerify.approved, false, "external user should require approval");
|
||||
|
||||
@@ -564,7 +651,7 @@ test("auth error codes remain stable for denied/not-approved/maintenance", async
|
||||
const adminVerify = await requestAccessAndVerify(baseUrl, dataDir, "admin@arcg.at");
|
||||
const adminHeaders = { Authorization: `Bearer ${adminVerify.accessToken}` };
|
||||
|
||||
const externalEmail = "codes@external.example";
|
||||
const externalEmail = "codes@oevsv.at";
|
||||
const externalVerify = await requestAccessAndVerify(baseUrl, dataDir, externalEmail);
|
||||
assert.equal(externalVerify.approved, false);
|
||||
|
||||
@@ -1013,7 +1100,7 @@ test("approvals list exposes account status and supports repeated decisions", as
|
||||
const adminVerify = await requestAccessAndVerify(baseUrl, dataDir, "admin@arcg.at");
|
||||
const adminHeaders = { Authorization: `Bearer ${adminVerify.accessToken}` };
|
||||
|
||||
const externalEmail = "statuscheck@outside.example";
|
||||
const externalEmail = "statuscheck@oevsv.at";
|
||||
const verify = await requestAccessAndVerify(baseUrl, dataDir, externalEmail);
|
||||
assert.equal(verify.approved, false);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user