mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 16:07:07 +00:00
Merge pull request #478 from drifterza/fix/issue-453-self-service-preauth-keys
This commit is contained in:
@@ -6,16 +6,18 @@ import type { Route } from "./+types/overview";
|
|||||||
|
|
||||||
export async function authKeysAction({ request, context }: Route.ActionArgs) {
|
export async function authKeysAction({ request, context }: Route.ActionArgs) {
|
||||||
const session = await context.sessions.auth(request);
|
const session = await context.sessions.auth(request);
|
||||||
const check = await context.sessions.check(request, Capabilities.generate_authkeys);
|
const api = context.hsApi.getRuntimeClient(session.api_key);
|
||||||
|
|
||||||
if (!check) {
|
const canGenerateAny = await context.sessions.check(request, Capabilities.generate_authkeys);
|
||||||
|
const canGenerateOwn = await context.sessions.check(request, Capabilities.generate_own_authkeys);
|
||||||
|
|
||||||
|
if (!canGenerateAny && !canGenerateOwn) {
|
||||||
throw data("You do not have permission to manage pre-auth keys", {
|
throw data("You do not have permission to manage pre-auth keys", {
|
||||||
status: 403,
|
status: 403,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const api = context.hsApi.getRuntimeClient(session.api_key);
|
|
||||||
const action = formData.get("action_id")?.toString();
|
const action = formData.get("action_id")?.toString();
|
||||||
if (!action) {
|
if (!action) {
|
||||||
throw data("Missing `action_id` in the form data.", {
|
throw data("Missing `action_id` in the form data.", {
|
||||||
@@ -38,6 +40,20 @@ export async function authKeysAction({ request, context }: Route.ActionArgs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only allow self-service users to create keys for themselves
|
||||||
|
if (!canGenerateAny && canGenerateOwn && user) {
|
||||||
|
const [targetUser] = await api.getUsers(user);
|
||||||
|
if (!targetUser) {
|
||||||
|
return data("User not found.", { status: 404 });
|
||||||
|
}
|
||||||
|
const targetSubject = targetUser.providerId?.split("/").pop();
|
||||||
|
if (targetSubject !== session.user.subject) {
|
||||||
|
throw data("You can only create pre-auth keys for your own user", {
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const expiry = formData.get("expiry")?.toString();
|
const expiry = formData.get("expiry")?.toString();
|
||||||
if (!expiry) {
|
if (!expiry) {
|
||||||
return data("Missing `expiry` in the form data.", {
|
return data("Missing `expiry` in the form data.", {
|
||||||
@@ -88,6 +104,20 @@ export async function authKeysAction({ request, context }: Route.ActionArgs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only allow self-service users to expire their own keys
|
||||||
|
if (!canGenerateAny && canGenerateOwn) {
|
||||||
|
const [targetUser] = await api.getUsers(user);
|
||||||
|
if (!targetUser) {
|
||||||
|
return data("User not found.", { status: 404 });
|
||||||
|
}
|
||||||
|
const targetSubject = targetUser.providerId?.split("/").pop();
|
||||||
|
if (targetSubject !== session.user.subject) {
|
||||||
|
throw data("You can only expire pre-auth keys for your own user", {
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await api.expirePreAuthKey(user, key);
|
await api.expirePreAuthKey(user, key);
|
||||||
return data("Pre-auth key expired");
|
return data("Pre-auth key expired");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,29 @@ import toast from "~/utils/toast";
|
|||||||
interface AddAuthKeyProps {
|
interface AddAuthKeyProps {
|
||||||
users: User[];
|
users: User[];
|
||||||
url: string;
|
url: string;
|
||||||
|
selfServiceOnly: boolean;
|
||||||
|
currentSubject: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddAuthKey({ users, url }: AddAuthKeyProps) {
|
function findCurrentUser(users: User[], subject: string): User | undefined {
|
||||||
|
return users.find((u) => u.providerId?.split("/").pop() === subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddAuthKey({
|
||||||
|
users,
|
||||||
|
url,
|
||||||
|
selfServiceOnly,
|
||||||
|
currentSubject,
|
||||||
|
}: AddAuthKeyProps) {
|
||||||
const fetcher = useFetcher();
|
const fetcher = useFetcher();
|
||||||
const submittingRef = useRef(false);
|
const submittingRef = useRef(false);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [reusable, setReusable] = useState(false);
|
const [reusable, setReusable] = useState(false);
|
||||||
const [ephemeral, setEphemeral] = useState(false);
|
const [ephemeral, setEphemeral] = useState(false);
|
||||||
const [tagOnly, setTagOnly] = useState(false);
|
const [tagOnly, setTagOnly] = useState(false);
|
||||||
const [userId, setUserId] = useState<Key | null>(users[0]?.id);
|
const currentUser = selfServiceOnly ? findCurrentUser(users, currentSubject) : null;
|
||||||
|
const availableUsers = selfServiceOnly && currentUser ? [currentUser] : users;
|
||||||
|
const [userId, setUserId] = useState<Key | null>(availableUsers[0]?.id);
|
||||||
const [tags, setTags] = useState("");
|
const [tags, setTags] = useState("");
|
||||||
|
|
||||||
const createdKey = fetcher.data?.success ? fetcher.data.key : null;
|
const createdKey = fetcher.data?.success ? fetcher.data.key : null;
|
||||||
@@ -41,7 +54,7 @@ export default function AddAuthKey({ users, url }: AddAuthKeyProps) {
|
|||||||
setReusable(false);
|
setReusable(false);
|
||||||
setEphemeral(false);
|
setEphemeral(false);
|
||||||
setTagOnly(false);
|
setTagOnly(false);
|
||||||
setUserId(users[0]?.id);
|
setUserId(availableUsers[0]?.id);
|
||||||
setTags("");
|
setTags("");
|
||||||
fetcher.data = undefined;
|
fetcher.data = undefined;
|
||||||
}
|
}
|
||||||
@@ -107,30 +120,38 @@ export default function AddAuthKey({ users, url }: AddAuthKeyProps) {
|
|||||||
>
|
>
|
||||||
<Dialog.Title>Generate auth key</Dialog.Title>
|
<Dialog.Title>Generate auth key</Dialog.Title>
|
||||||
|
|
||||||
<div className="mb-4 flex items-center justify-between gap-2">
|
{!selfServiceOnly && (
|
||||||
<div>
|
<div className="mb-4 flex items-center justify-between gap-2">
|
||||||
<Dialog.Text className="font-semibold">Tag-only key</Dialog.Text>
|
<div>
|
||||||
<Dialog.Text className="text-sm">
|
<Dialog.Text className="font-semibold">Tag-only key</Dialog.Text>
|
||||||
Create a key owned by ACL tags instead of a user.
|
<Dialog.Text className="text-sm">
|
||||||
</Dialog.Text>
|
Create a key owned by ACL tags instead of a user.
|
||||||
|
</Dialog.Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
defaultSelected={tagOnly}
|
||||||
|
label="Tag-only"
|
||||||
|
onChange={() => setTagOnly(!tagOnly)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
)}
|
||||||
defaultSelected={tagOnly}
|
|
||||||
label="Tag-only"
|
|
||||||
onChange={() => setTagOnly(!tagOnly)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!tagOnly && (
|
{!tagOnly && (
|
||||||
<Select
|
<Select
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
description="Machines will belong to this user when they authenticate."
|
description={
|
||||||
|
selfServiceOnly
|
||||||
|
? "You can only create keys for your own user."
|
||||||
|
: "Machines will belong to this user when they authenticate."
|
||||||
|
}
|
||||||
|
isDisabled={selfServiceOnly}
|
||||||
isRequired
|
isRequired
|
||||||
label="User"
|
label="User"
|
||||||
onSelectionChange={(value) => setUserId(value)}
|
onSelectionChange={(value) => setUserId(value)}
|
||||||
placeholder="Select a user"
|
placeholder="Select a user"
|
||||||
|
selectedKey={userId}
|
||||||
>
|
>
|
||||||
{users.map((user) => (
|
{availableUsers.map((user) => (
|
||||||
<Select.Item key={user.id}>
|
<Select.Item key={user.id}>
|
||||||
{user.name || user.displayName || user.email || user.id}
|
{user.name || user.displayName || user.email || user.id}
|
||||||
</Select.Item>
|
</Select.Item>
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import { FileKey2 } from "lucide-react";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Link as RemixLink } from "react-router";
|
import { Link as RemixLink } from "react-router";
|
||||||
|
|
||||||
|
import type { PreAuthKey } from "~/types";
|
||||||
|
import type { User } from "~/types/User";
|
||||||
|
|
||||||
import Code from "~/components/Code";
|
import Code from "~/components/Code";
|
||||||
import Link from "~/components/Link";
|
import Link from "~/components/Link";
|
||||||
import Notice from "~/components/Notice";
|
import Notice from "~/components/Notice";
|
||||||
import Select from "~/components/Select";
|
import Select from "~/components/Select";
|
||||||
import TableList from "~/components/TableList";
|
import TableList from "~/components/TableList";
|
||||||
import { Capabilities } from "~/server/web/roles";
|
import { Capabilities } from "~/server/web/roles";
|
||||||
import type { PreAuthKey } from "~/types";
|
|
||||||
import type { User } from "~/types/User";
|
|
||||||
import log from "~/utils/log";
|
import log from "~/utils/log";
|
||||||
import { filterUsersWithValidIds, getUserDisplayName } from "~/utils/user";
|
import { filterUsersWithValidIds, getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
@@ -82,11 +83,16 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
.map(({ user, error }) => ({ user, error }));
|
.map(({ user, error }) => ({ user, error }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canGenerateAny = await context.sessions.check(request, Capabilities.generate_authkeys);
|
||||||
|
const canGenerateOwn = await context.sessions.check(request, Capabilities.generate_own_authkeys);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
keys,
|
keys,
|
||||||
missing,
|
missing,
|
||||||
users,
|
users,
|
||||||
access: await context.sessions.check(request, Capabilities.generate_authkeys),
|
access: canGenerateAny || canGenerateOwn,
|
||||||
|
selfServiceOnly: !canGenerateAny && canGenerateOwn,
|
||||||
|
currentSubject: session.user.subject,
|
||||||
url: context.config.headscale.public_url ?? context.config.headscale.url,
|
url: context.config.headscale.public_url ?? context.config.headscale.url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -95,7 +101,7 @@ export const action = authKeysAction;
|
|||||||
|
|
||||||
type Status = "all" | "active" | "expired" | "reusable" | "ephemeral";
|
type Status = "all" | "active" | "expired" | "reusable" | "ephemeral";
|
||||||
export default function Page({
|
export default function Page({
|
||||||
loaderData: { keys, missing, users, url, access },
|
loaderData: { keys, missing, users, url, access, selfServiceOnly, currentSubject },
|
||||||
}: Route.ComponentProps) {
|
}: Route.ComponentProps) {
|
||||||
const [selectedUser, setSelectedUser] = useState("__headplane_all");
|
const [selectedUser, setSelectedUser] = useState("__headplane_all");
|
||||||
const [status, setStatus] = useState<Status>("active");
|
const [status, setStatus] = useState<Status>("active");
|
||||||
@@ -193,7 +199,12 @@ export default function Page({
|
|||||||
Tailscale documentation
|
Tailscale documentation
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<AddAuthKey url={url} users={users} />
|
<AddAuthKey
|
||||||
|
currentSubject={currentSubject}
|
||||||
|
selfServiceOnly={selfServiceOnly}
|
||||||
|
url={url}
|
||||||
|
users={users}
|
||||||
|
/>
|
||||||
<div className="mt-4 flex items-center gap-4">
|
<div className="mt-4 flex items-center gap-4">
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
+112
-108
@@ -1,144 +1,148 @@
|
|||||||
export type Capabilities = (typeof Capabilities)[keyof typeof Capabilities];
|
export type Capabilities = (typeof Capabilities)[keyof typeof Capabilities];
|
||||||
export const Capabilities = {
|
export const Capabilities = {
|
||||||
// Can access the admin console
|
// Can access the admin console
|
||||||
ui_access: 1 << 0,
|
ui_access: 1 << 0,
|
||||||
|
|
||||||
// Read tailnet policy file (unimplemented)
|
// Read tailnet policy file (unimplemented)
|
||||||
read_policy: 1 << 1,
|
read_policy: 1 << 1,
|
||||||
|
|
||||||
// Write tailnet policy file (unimplemented)
|
// Write tailnet policy file (unimplemented)
|
||||||
write_policy: 1 << 2,
|
write_policy: 1 << 2,
|
||||||
|
|
||||||
// Read network configurations
|
// Read network configurations
|
||||||
read_network: 1 << 3,
|
read_network: 1 << 3,
|
||||||
|
|
||||||
// Write network configurations, for example, enable MagicDNS, split DNS,
|
// Write network configurations, for example, enable MagicDNS, split DNS,
|
||||||
// make subnet, or allow a node to be an exit node, enable HTTPS
|
// make subnet, or allow a node to be an exit node, enable HTTPS
|
||||||
write_network: 1 << 4,
|
write_network: 1 << 4,
|
||||||
|
|
||||||
// Read feature configuration (unimplemented)
|
// Read feature configuration (unimplemented)
|
||||||
read_feature: 1 << 5,
|
read_feature: 1 << 5,
|
||||||
|
|
||||||
// Write feature configuration, for example, enable Taildrop (unimplemented)
|
// Write feature configuration, for example, enable Taildrop (unimplemented)
|
||||||
write_feature: 1 << 6,
|
write_feature: 1 << 6,
|
||||||
|
|
||||||
// Configure user & group provisioning
|
// Configure user & group provisioning
|
||||||
configure_iam: 1 << 7,
|
configure_iam: 1 << 7,
|
||||||
|
|
||||||
// Read machines, for example, see machine names and status
|
// Read machines, for example, see machine names and status
|
||||||
read_machines: 1 << 8,
|
read_machines: 1 << 8,
|
||||||
|
|
||||||
// Write machines, for example, approve, rename, and remove machines
|
// Write machines, for example, approve, rename, and remove machines
|
||||||
write_machines: 1 << 9,
|
write_machines: 1 << 9,
|
||||||
|
|
||||||
// Read users and user roles
|
// Read users and user roles
|
||||||
read_users: 1 << 10,
|
read_users: 1 << 10,
|
||||||
|
|
||||||
// Write users and user roles, for example, remove users,
|
// Write users and user roles, for example, remove users,
|
||||||
// approve users, make Admin
|
// approve users, make Admin
|
||||||
write_users: 1 << 11,
|
write_users: 1 << 11,
|
||||||
|
|
||||||
// Can generate authkeys (unimplemented)
|
// Can generate authkeys for any user
|
||||||
generate_authkeys: 1 << 12,
|
generate_authkeys: 1 << 12,
|
||||||
|
|
||||||
// Can use any tag (without being tag owner) (unimplemented)
|
// Can generate authkeys for own user only
|
||||||
use_tags: 1 << 13,
|
generate_own_authkeys: 1 << 16,
|
||||||
|
|
||||||
// Write tailnet name (unimplemented)
|
// Can use any tag (without being tag owner) (unimplemented)
|
||||||
write_tailnet: 1 << 14,
|
use_tags: 1 << 13,
|
||||||
|
|
||||||
// Owner flag
|
// Write tailnet name (unimplemented)
|
||||||
owner: 1 << 15,
|
write_tailnet: 1 << 14,
|
||||||
|
|
||||||
|
// Owner flag
|
||||||
|
owner: 1 << 15,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type Roles = [keyof typeof Roles];
|
export type Roles = [keyof typeof Roles];
|
||||||
export const Roles = {
|
export const Roles = {
|
||||||
owner:
|
owner:
|
||||||
Capabilities.ui_access |
|
Capabilities.ui_access |
|
||||||
Capabilities.read_policy |
|
Capabilities.read_policy |
|
||||||
Capabilities.write_policy |
|
Capabilities.write_policy |
|
||||||
Capabilities.read_network |
|
Capabilities.read_network |
|
||||||
Capabilities.write_network |
|
Capabilities.write_network |
|
||||||
Capabilities.read_feature |
|
Capabilities.read_feature |
|
||||||
Capabilities.write_feature |
|
Capabilities.write_feature |
|
||||||
Capabilities.configure_iam |
|
Capabilities.configure_iam |
|
||||||
Capabilities.read_machines |
|
Capabilities.read_machines |
|
||||||
Capabilities.write_machines |
|
Capabilities.write_machines |
|
||||||
Capabilities.read_users |
|
Capabilities.read_users |
|
||||||
Capabilities.write_users |
|
Capabilities.write_users |
|
||||||
Capabilities.generate_authkeys |
|
Capabilities.generate_authkeys |
|
||||||
Capabilities.use_tags |
|
Capabilities.use_tags |
|
||||||
Capabilities.write_tailnet |
|
Capabilities.write_tailnet |
|
||||||
Capabilities.owner,
|
Capabilities.owner,
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
Capabilities.ui_access |
|
Capabilities.ui_access |
|
||||||
Capabilities.read_policy |
|
Capabilities.read_policy |
|
||||||
Capabilities.write_policy |
|
Capabilities.write_policy |
|
||||||
Capabilities.read_network |
|
Capabilities.read_network |
|
||||||
Capabilities.write_network |
|
Capabilities.write_network |
|
||||||
Capabilities.read_feature |
|
Capabilities.read_feature |
|
||||||
Capabilities.write_feature |
|
Capabilities.write_feature |
|
||||||
Capabilities.configure_iam |
|
Capabilities.configure_iam |
|
||||||
Capabilities.read_machines |
|
Capabilities.read_machines |
|
||||||
Capabilities.write_machines |
|
Capabilities.write_machines |
|
||||||
Capabilities.read_users |
|
Capabilities.read_users |
|
||||||
Capabilities.write_users |
|
Capabilities.write_users |
|
||||||
Capabilities.generate_authkeys |
|
Capabilities.generate_authkeys |
|
||||||
Capabilities.use_tags |
|
Capabilities.use_tags |
|
||||||
Capabilities.write_tailnet,
|
Capabilities.write_tailnet,
|
||||||
|
|
||||||
network_admin:
|
network_admin:
|
||||||
Capabilities.ui_access |
|
Capabilities.ui_access |
|
||||||
Capabilities.read_policy |
|
Capabilities.read_policy |
|
||||||
Capabilities.write_policy |
|
Capabilities.write_policy |
|
||||||
Capabilities.read_network |
|
Capabilities.read_network |
|
||||||
Capabilities.write_network |
|
Capabilities.write_network |
|
||||||
Capabilities.read_feature |
|
Capabilities.read_feature |
|
||||||
Capabilities.read_machines |
|
Capabilities.read_machines |
|
||||||
Capabilities.read_users |
|
Capabilities.read_users |
|
||||||
Capabilities.generate_authkeys |
|
Capabilities.generate_authkeys |
|
||||||
Capabilities.use_tags |
|
Capabilities.use_tags |
|
||||||
Capabilities.write_tailnet,
|
Capabilities.write_tailnet,
|
||||||
|
|
||||||
it_admin:
|
it_admin:
|
||||||
Capabilities.ui_access |
|
Capabilities.ui_access |
|
||||||
Capabilities.read_policy |
|
Capabilities.read_policy |
|
||||||
Capabilities.read_network |
|
Capabilities.read_network |
|
||||||
Capabilities.read_feature |
|
Capabilities.read_feature |
|
||||||
Capabilities.write_feature |
|
Capabilities.write_feature |
|
||||||
Capabilities.configure_iam |
|
Capabilities.configure_iam |
|
||||||
Capabilities.read_machines |
|
Capabilities.read_machines |
|
||||||
Capabilities.write_machines |
|
Capabilities.write_machines |
|
||||||
Capabilities.read_users |
|
Capabilities.read_users |
|
||||||
Capabilities.write_users |
|
Capabilities.write_users |
|
||||||
Capabilities.generate_authkeys,
|
Capabilities.generate_authkeys,
|
||||||
|
|
||||||
auditor:
|
auditor:
|
||||||
Capabilities.ui_access |
|
Capabilities.ui_access |
|
||||||
Capabilities.read_policy |
|
Capabilities.read_policy |
|
||||||
Capabilities.read_network |
|
Capabilities.read_network |
|
||||||
Capabilities.read_feature |
|
Capabilities.read_feature |
|
||||||
Capabilities.read_machines |
|
Capabilities.read_machines |
|
||||||
Capabilities.read_users,
|
Capabilities.read_users |
|
||||||
|
Capabilities.generate_own_authkeys,
|
||||||
|
|
||||||
// Default role for new users with 0 capabilities on the UI side of things
|
// Default role for new users with 0 capabilities on the UI side of things
|
||||||
member: 0,
|
member: 0,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type Role = keyof typeof Roles;
|
export type Role = keyof typeof Roles;
|
||||||
export type Capability = keyof typeof Capabilities;
|
export type Capability = keyof typeof Capabilities;
|
||||||
export function hasCapability(role: Role, capability: Capability): boolean {
|
export function hasCapability(role: Role, capability: Capability): boolean {
|
||||||
return (Roles[role] & Capabilities[capability]) !== 0;
|
return (Roles[role] & Capabilities[capability]) !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoleFromCapabilities(capabilities: Capabilities): Role {
|
export function getRoleFromCapabilities(capabilities: Capabilities): Role {
|
||||||
const iterable = Roles as Record<string, Capabilities>;
|
const iterable = Roles as Record<string, Capabilities>;
|
||||||
for (const role in iterable) {
|
for (const role in iterable) {
|
||||||
if (iterable[role] === capabilities) {
|
if (iterable[role] === capabilities) {
|
||||||
return role as Role;
|
return role as Role;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'member';
|
return "member";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { Capabilities, hasCapability, Roles } from "~/server/web/roles";
|
||||||
|
|
||||||
|
describe("Self-service pre-auth keys", () => {
|
||||||
|
describe("Capabilities", () => {
|
||||||
|
test("generate_own_authkeys capability exists", () => {
|
||||||
|
expect(Capabilities.generate_own_authkeys).toBeDefined();
|
||||||
|
expect(typeof Capabilities.generate_own_authkeys).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("generate_own_authkeys is distinct from generate_authkeys", () => {
|
||||||
|
expect(Capabilities.generate_own_authkeys).not.toBe(Capabilities.generate_authkeys);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Auditor role", () => {
|
||||||
|
test("auditor has generate_own_authkeys", () => {
|
||||||
|
expect(hasCapability("auditor", "generate_own_authkeys")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("auditor does not have generate_authkeys", () => {
|
||||||
|
expect(hasCapability("auditor", "generate_authkeys")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("auditor has read permissions", () => {
|
||||||
|
expect(hasCapability("auditor", "read_machines")).toBe(true);
|
||||||
|
expect(hasCapability("auditor", "read_users")).toBe(true);
|
||||||
|
expect(hasCapability("auditor", "read_policy")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Admin roles retain full access", () => {
|
||||||
|
test("owner has generate_authkeys", () => {
|
||||||
|
expect(hasCapability("owner", "generate_authkeys")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("admin has generate_authkeys", () => {
|
||||||
|
expect(hasCapability("admin", "generate_authkeys")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it_admin has generate_authkeys", () => {
|
||||||
|
expect(hasCapability("it_admin", "generate_authkeys")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("network_admin has generate_authkeys", () => {
|
||||||
|
expect(hasCapability("network_admin", "generate_authkeys")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Member role", () => {
|
||||||
|
test("member has no capabilities", () => {
|
||||||
|
expect(Roles.member).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("member does not have generate_own_authkeys", () => {
|
||||||
|
expect(hasCapability("member", "generate_own_authkeys")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("providerId subject extraction", () => {
|
||||||
|
function extractSubject(providerId: string | undefined): string | undefined {
|
||||||
|
return providerId?.split("/").pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
test("extracts subject from oidc providerId", () => {
|
||||||
|
expect(extractSubject("oidc/abc123")).toBe("abc123");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("extracts subject from nested providerId", () => {
|
||||||
|
expect(extractSubject("provider/tenant/user123")).toBe("user123");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles single component providerId", () => {
|
||||||
|
expect(extractSubject("subject")).toBe("subject");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined for undefined providerId", () => {
|
||||||
|
expect(extractSubject(undefined)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user