Files
Termix/src/backend/tests/database/repositories/rbac-access-repository.test.ts
T

339 lines
10 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { RbacAccessRepository } from "../../../database/repositories/rbac-access-repository.js";
describe("RbacAccessRepository", () => {
let adapter: TestSqliteDatabase | null = null;
const activeAccessTime = "2026-06-26T12:00:00.000Z";
afterEach(async () => {
if (adapter) {
await adapter.close();
adapter = null;
}
});
async function createRepository(
onWrite?: () => void | Promise<void>,
): Promise<RbacAccessRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
await adapter.exec(`
INSERT INTO users (id, username, password_hash, is_admin, is_oidc)
VALUES
('admin', 'admin', 'hash', 1, 0),
('user-1', 'alice', 'hash', 0, 0),
('owner-1', 'owner', 'hash', 0, 0);
INSERT INTO roles (id, name, display_name, is_system)
VALUES (7, 'ops', 'Operations', 0);
INSERT INTO ssh_credentials (id, user_id, name, username, auth_type) VALUES
(123, 'admin', 'cred-123', 'root', 'password'),
(124, 'admin', 'cred-124', 'root', 'password'),
(125, 'admin', 'cred-125', 'root', 'password'),
(126, 'admin', 'cred-126', 'root', 'password');
INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) VALUES
(43, 'admin', 'host-43', '10.0.0.43', 22, 'root', 'password');
INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) VALUES
(44, 'admin', 'host-44', '10.0.0.45', 22, 'root', 'password');
INSERT INTO ssh_data (
id, user_id, name, ip, port, username, credential_id, folder, tags, auth_type)
VALUES (42, 'owner-1', 'prod', '10.0.0.42', 22, 'root', 123, 'servers', 'linux', 'password');
INSERT INTO host_access (
id, host_id, user_id, role_id, granted_by, permission_level, expires_at, created_at
)
VALUES
(1, 42, 'user-1', NULL, 'admin', 'view', NULL, '2026-06-26T00:00:00.000Z'),
(2, 42, NULL, 7, 'admin', 'view', '2026-06-27T00:00:00.000Z', '2026-06-26T01:00:00.000Z'),
(5, 44, 'user-1', NULL, 'admin', 'view', '2026-06-25T00:00:00.000Z', '2026-06-24T00:00:00.000Z');
INSERT INTO shared_host_secrets (
id, host_access_id, target_user_id, protocol, source_type, original_credential_id, encrypted_username, encrypted_auth_type
)
VALUES
(8, 2, 'user-1', 'ssh', 'credential', 123, 'enc-user', 'enc-auth'),
(9, 2, 'user-1', 'rdp', 'inline', NULL, 'enc-rdp-user', 'direct');
`);
return new RbacAccessRepository(context, onWrite);
}
it("lists host access with user and role target metadata", async () => {
const repo = await createRepository();
const accessList = await repo.listHostAccess(42);
expect(accessList).toMatchObject([
{
id: 2,
targetType: "role",
userId: null,
roleId: 7,
username: null,
roleName: "ops",
roleDisplayName: "Operations",
grantedByUsername: "admin",
},
{
id: 1,
targetType: "user",
userId: "user-1",
roleId: null,
username: "alice",
roleName: null,
roleDisplayName: null,
grantedByUsername: "admin",
},
]);
});
it("lists shared hosts for direct and role access", async () => {
const repo = await createRepository();
const sharedHosts = await repo.listSharedHosts(
"user-1",
[7],
activeAccessTime,
);
expect(sharedHosts).toMatchObject([
{
id: 42,
name: "prod",
ip: "10.0.0.42",
ownerUsername: "owner",
permissionLevel: "view",
},
{
id: 42,
name: "prod",
ip: "10.0.0.42",
ownerUsername: "owner",
permissionLevel: "view",
},
]);
});
it("lists visible host access entries for host list access checks", async () => {
const repo = await createRepository();
await expect(
repo.listVisibleHostAccessEntries("user-1", [7], activeAccessTime),
).resolves.toEqual([
{
hostId: 42,
permissionLevel: "view",
expiresAt: "2026-06-27T00:00:00.000Z",
},
{
hostId: 42,
permissionLevel: "view",
expiresAt: null,
},
]);
});
it("lists role host access credential sources for role assignment", async () => {
const repo = await createRepository();
await expect(repo.listRoleHostAccessCredentialSources(7)).resolves.toEqual([
{
hostAccessId: 2,
credentialId: 123,
hostId: 42,
hostOwnerId: "owner-1",
},
]);
});
it("finds shared secrets per protocol and host access owner", async () => {
const repo = await createRepository();
await expect(
repo.findSharedSecretForHostUserProtocol(42, "user-1", "ssh"),
).resolves.toMatchObject({
id: 8,
hostAccessId: 2,
protocol: "ssh",
sourceType: "credential",
originalCredentialId: 123,
targetUserId: "user-1",
encryptedUsername: "enc-user",
encryptedAuthType: "enc-auth",
});
await expect(
repo.findSharedSecretForHostUserProtocol(42, "user-1", "rdp"),
).resolves.toMatchObject({
id: 9,
protocol: "rdp",
sourceType: "inline",
originalCredentialId: null,
});
await expect(
repo.findSharedSecretForHostUserProtocol(42, "user-1", "vnc"),
).resolves.toBeNull();
await expect(
repo.findSharedSecretForHostUserProtocol(99, "user-1", "ssh"),
).resolves.toBeNull();
await expect(repo.findHostAccessOwnerId(2)).resolves.toBe("owner-1");
await expect(repo.findHostAccessOwnerId(999)).resolves.toBeNull();
});
it("lists active grants, finds grants by id and updates grant level/expiry", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
const grants = await repo.listActiveHostAccessGrants(42, activeAccessTime);
expect(grants.map((grant) => grant.id).sort()).toEqual([1, 2]);
// Host 44's only grant expired before activeAccessTime.
await expect(
repo.listActiveHostAccessGrants(44, activeAccessTime),
).resolves.toEqual([]);
await expect(repo.findHostAccessById(1, 42)).resolves.toMatchObject({
id: 1,
hostId: 42,
permissionLevel: "view",
});
await expect(repo.findHostAccessById(1, 99)).resolves.toBeNull();
await expect(
repo.updateHostAccessGrant(1, 42, {
permissionLevel: "manage",
expiresAt: "2026-07-01T00:00:00.000Z",
}),
).resolves.toBe(true);
await expect(repo.findHostAccessById(1, 42)).resolves.toMatchObject({
permissionLevel: "manage",
expiresAt: "2026-07-01T00:00:00.000Z",
});
await expect(
repo.updateHostAccessGrant(999, 42, { permissionLevel: "view" }),
).resolves.toBe(false);
expect(writeCount).toBe(1);
});
it("upserts host access and updates overrides", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
const updated = await repo.upsertHostAccess({
hostId: 42,
targetType: "user",
targetUserId: "user-1",
grantedBy: "admin",
permissionLevel: "view",
expiresAt: "2026-06-28T00:00:00.000Z",
});
expect(updated).toEqual({ id: 1, created: false });
expect(
(await repo.listHostAccess(42)).find((row) => row.id === 1),
).toMatchObject({
expiresAt: "2026-06-28T00:00:00.000Z",
});
const created = await repo.upsertHostAccess({
hostId: 43,
targetType: "role",
targetRoleId: 7,
grantedBy: "admin",
permissionLevel: "view",
expiresAt: null,
});
expect(created.created).toBe(true);
const directAccess = await repo.findDirectHostAccess(42, "user-1");
expect(directAccess?.id).toBe(1);
await repo.touchHostAccess(1, "2026-06-26T03:00:00.000Z");
expect(
(await repo.findDirectHostAccess(42, "user-1"))?.lastAccessedAt,
).toBe("2026-06-26T03:00:00.000Z");
await repo.revokeHostAccess(1, 42);
expect(await repo.findDirectHostAccess(42, "user-1")).toBeNull();
expect(writeCount).toBe(4);
});
it("finds active host access and deletes expired host access", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
expect(
await repo.findActiveHostAccess(
42,
"user-1",
[7],
"2026-06-26T12:00:00.000Z",
),
).toMatchObject({ id: 1 });
expect(
await repo.findActiveHostAccess(
44,
"user-1",
[],
"2026-06-26T12:00:00.000Z",
),
).toBeNull();
expect(await repo.deleteExpiredHostAccess("2026-06-26T12:00:00.000Z")).toBe(
1,
);
expect(await repo.findDirectHostAccess(44, "user-1")).toBeNull();
expect(writeCount).toBe(1);
});
it("deletes host access for a host and only saves when rows changed", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
expect(await repo.deleteHostAccessForHost(42)).toBe(2);
expect(await repo.listHostAccess(42)).toEqual([]);
expect(writeCount).toBe(1);
expect(await repo.deleteHostAccessForHost(42)).toBe(0);
expect(writeCount).toBe(1);
});
it("deletes host access for multiple hosts", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
expect(await repo.deleteHostAccessForHosts([])).toBe(0);
expect(writeCount).toBe(0);
expect(await repo.deleteHostAccessForHosts([42, 44])).toBe(3);
expect(await repo.listHostAccess(42)).toEqual([]);
expect(await repo.findDirectHostAccess(44, "user-1")).toBeNull();
expect(writeCount).toBe(1);
});
it("deletes host access that references a user directly or as grantor", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
expect(await repo.deleteHostAccessForUserReferences("admin")).toBe(3);
expect(await repo.listHostAccess(42)).toEqual([]);
expect(await repo.findDirectHostAccess(44, "user-1")).toBeNull();
expect(writeCount).toBe(1);
expect(await repo.deleteHostAccessForUserReferences("admin")).toBe(0);
expect(writeCount).toBe(1);
});
});