mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
339 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|