diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx index ecb12404a6..ef1f89b7b7 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx @@ -1,5 +1,4 @@ import { type Interval, IntervalSelect } from "@/app/(app)/apis/[apiId]/select"; -import type { NestedPermissions } from "@/app/(app)/authorization/roles/[roleId]/tree"; import { StackedColumnChart } from "@/components/dashboard/charts"; import { PageContent } from "@/components/page-content"; import { Badge } from "@/components/ui/badge"; @@ -226,36 +225,12 @@ export default async function APIKeyDetailPage(props: { } }); - const roleTee = key.workspace.roles.map((role) => { - const nested: NestedPermissions = {}; - for (const permission of key.workspace.permissions) { - let n = nested; - const parts = permission.name.split("."); - for (let i = 0; i < parts.length; i++) { - const p = parts[i]; - if (!(p in n)) { - n[p] = { - id: permission.id, - name: permission.name, - description: permission.description, - checked: role.permissions.some((p) => p.permissionId === permission.id), - part: p, - permissions: {}, - path: parts.slice(0, i).join("."), - }; - } - n = n[p].permissions; - } - } - const data = { + const rolesList = key.workspace.roles.map((role) => { + return { id: role.id, name: role.name, - description: role.description, - keyId: key.id, - active: key.roles.some((keyRole) => keyRole.roleId === role.id), - nestedPermissions: nested, + isActive: key.roles.some((keyRole) => keyRole.roleId === role.id), }; - return data; }); return ( @@ -372,7 +347,7 @@ export default async function APIKeyDetailPage(props: { - + diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx index 7764394a1a..9c2fe2a77c 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx @@ -1,83 +1,84 @@ "use client"; -import { Tree } from "@/app/(app)/authorization/roles/[roleId]/tree"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { cn } from "@/lib/utils"; -import { Button } from "@unkey/ui"; -import { useEffect, useState } from "react"; - -export type NestedPermission = { - id: string; - checked: boolean; - description: string | null; - name: string; - part: string; - path: string; - permissions: NestedPermissions; -}; -export type NestedPermissions = Record; +import { Checkbox } from "@/components/ui/checkbox"; +import { trpc } from "@/lib/trpc/client"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; export type Role = { id: string; name: string; - keyId: string; - active: boolean; - description: string | null; - nestedPermissions: NestedPermissions; + isActive: boolean; }; + type PermissionTreeProps = { roles: Role[]; + keyId: string; }; -export function PermissionList({ roles }: PermissionTreeProps) { - const [activeRoleId, setActiveRoleId] = useState( - roles.length > 0 ? roles[0].id : null, - ); - const [key, setKey] = useState(0); - - // biome-ignore lint/correctness/useExhaustiveDependencies: Force rerender on role change - useEffect(() => { - // Update the key when activeRoleId changes to force a complete re-render - setKey((prev) => prev + 1); - }, [activeRoleId]); +export function PermissionList({ roles, keyId }: PermissionTreeProps) { + const router = useRouter(); + const connectRole = trpc.rbac.connectRoleToKey.useMutation({ + onMutate: () => { + toast.loading("Connecting role to key"); + }, + onSuccess: () => { + toast.dismiss(); + toast.success("Role connected to key"); + router.refresh(); + }, + onError: (error) => { + toast.dismiss(); + toast.error(error.message); + }, + }); - const activeRole = roles.find((r) => r.id === activeRoleId); + const disconnectRole = trpc.rbac.disconnectRoleFromKey.useMutation({ + onMutate: () => { + toast.loading("Disconnecting role from key"); + }, + onSuccess: () => { + toast.dismiss(); + toast.success("Role disconnected from key"); + router.refresh(); + }, + onError: (error) => { + toast.dismiss(); + toast.error(error.message); + }, + }); return (
- Permissions & Roles - Connect roles with permissions to control access -
-
-
- {roles.map((role) => ( - - ))} -
+ Roles + Manage roles for this key
- {activeRole && activeRoleId && ( - - )} +
+ {roles.map((role) => ( +
+ { + if (checked) { + connectRole.mutate({ keyId: keyId, roleId: role.id }); + } else { + disconnectRole.mutate({ keyId: keyId, roleId: role.id }); + } + }} + /> +
+ {role.name} +
+
+ ))} +
); diff --git a/apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx b/apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx index e722be8e5b..575673aa4c 100644 --- a/apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx +++ b/apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx @@ -14,7 +14,7 @@ export const RootKeysFilter = ({ ({ - label: rootKey.name ?? "", + label: getRootKeyLabel(rootKey), // Format the root key label when empty and contains no name. value: rootKey.id, checked: false, id: index, @@ -32,3 +32,15 @@ export const RootKeysFilter = ({ /> ); }; + +// Format the root key label when rootkey contains no name. Splits and formats into 'unkey_1234...5678' +// This is to avoid displaying the full id in the filter. +const getRootKeyLabel = (rootKey: { id: string; name: string | null }) => { + const id = rootKey.id; + const prefix = id.substring(0, id.indexOf("_") + 1); + const obfuscatedMiddle = + id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : ""; + const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5); + const lastFour = id.substring(id.length - 4); + return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour; +}; diff --git a/apps/docs/apis/features/authorization/api-key-screen.png b/apps/docs/apis/features/authorization/api-key-screen.png new file mode 100644 index 0000000000..b272058865 Binary files /dev/null and b/apps/docs/apis/features/authorization/api-key-screen.png differ diff --git a/apps/docs/apis/features/authorization/api-keys-navigation.png b/apps/docs/apis/features/authorization/api-keys-navigation.png new file mode 100644 index 0000000000..cf9497bff1 Binary files /dev/null and b/apps/docs/apis/features/authorization/api-keys-navigation.png differ diff --git a/apps/docs/apis/features/authorization/example.mdx b/apps/docs/apis/features/authorization/example.mdx index 0181bbb345..5fb84adfae 100644 --- a/apps/docs/apis/features/authorization/example.mdx +++ b/apps/docs/apis/features/authorization/example.mdx @@ -76,28 +76,33 @@ Now that we have permissions and roles in place, we can connect them to keys. 1. In the sidebar, click on one of your APIs -2. Then click on `Keys` in the tabs +2. In the breakcrumb navigation on the top click Reqests and then keys + + Breadcrumb Navigation + + 3. Select one of your existing keys by clicking on it -4. Go to the `Permissions` tab +4. Scroll down to the `Roles` section if not visible -You should now be on `/app/keys/key_auth_???/key_???/permissions` +You should now be on `/app/keys/key_auth_???/key_???` - Unconnected roles and permissions + Unconnected roles and permissions - You can connect a role to your key by clicking on the checkbox in the graph. + You can connect a role to your key by clicking on the checkbox. Let's give this key the `dns.manager` and `read-only` roles. + A toast message should come up in the lower corner when the action has been completed. - Connected roles and permissions + Unconnected roles and permissions -As you can see, now the key is connected to the following permissions: -`domain.dns.create_record`, `domain.dns.read_record`, `domain.dns.update_record`, `domain.dns.delete_record`, `domain.create_domain`, `domain.read_domain` +As you can see, now the key now contains 2 `roles` and 5 `permissions` shown just above the Roles section: + diff --git a/apps/docs/apis/features/authorization/role-add-example.png b/apps/docs/apis/features/authorization/role-add-example.png new file mode 100644 index 0000000000..fba15ab4c1 Binary files /dev/null and b/apps/docs/apis/features/authorization/role-add-example.png differ diff --git a/apps/docs/apis/features/authorization/roles-and-permissions.mdx b/apps/docs/apis/features/authorization/roles-and-permissions.mdx index 51a65f5716..546bab3a96 100644 --- a/apps/docs/apis/features/authorization/roles-and-permissions.mdx +++ b/apps/docs/apis/features/authorization/roles-and-permissions.mdx @@ -90,28 +90,32 @@ A checked box means the role will grant the permission to keys. ## Connecting roles to keys 1. In the sidebar, click on one of your APIs -2. Then click on `Keys` in the tabs +2. In the breakcrumb navigation on the top click Reqests and then keys + + Breadcrumb Navigation + 3. Select one of your existing keys by clicking on it -4. Go to the `Permissions` tab +4. Scroll down to the `Roles` section if not visible -You should now be on `/app/keys/key_auth_???/key_???/permissions` +You should now be on `/app/keys/key_auth_???/key_???` - Unconnected roles and permissions + Unconnected roles and permissions - You can connect a role to your key by clicking on the checkbox in the graph. + You can connect a role to your key by clicking on the checkbox. Let's give this key the `dns.manager` and `read-only` roles. + A toast message should come up in the lower corner when the action has been completed. - Connected roles and permissions + Unconnected roles and permissions -As you can see, now the key is connected to the following permissions: -`domain.dns.create_record`, `domain.dns.read_record`, `domain.dns.update_record`, `domain.dns.delete_record`, `domain.create_domain`, `domain.read_domain` +As you can see, now the key now contains 2 `roles` and 5 `permissions` shown just above the Roles section: +