Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -372,7 +347,7 @@ export default async function APIKeyDetailPage(props: {
</div>
<RBACButtons permissions={key.workspace.permissions} />
</div>
<PermissionList roles={roleTee} />
<PermissionList roles={rolesList} keyId={key.id} />
</div>
</div>
</PageContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, NestedPermission>;
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<string | null>(
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 (
<Card>
<CardHeader className="pb-0">
<div className="mb-2">
<CardTitle>Permissions & Roles</CardTitle>
<CardDescription>Connect roles with permissions to control access</CardDescription>
</div>
<div className="border-b border-gray-6">
<div className="flex flex-wrap -mb-px">
{roles.map((role) => (
<Button
variant="ghost"
key={role.id}
onClick={() => setActiveRoleId(role.id)}
className={cn(
"rounded-none rounded-t inline-flex items-center px-4 py-2 text-sm font-medium border-b-2 transition-colors outline-none focus:ring-0",
activeRoleId === role.id
? "border-primary text-primary"
: "border-transparent text-gray-11 hover:text-gray-12 hover:border-gray-7",
)}
>
{role.name}
{role.active && <span className="ml-2 h-2 w-2 rounded-full bg-success-9" />}
</Button>
))}
</div>
<CardTitle>Roles</CardTitle>
<CardDescription>Manage roles for this key</CardDescription>
</div>
</CardHeader>

<CardContent className="pt-6">
{activeRole && activeRoleId && (
<Tree
key={`tree-${key}-${activeRoleId}`}
nestedPermissions={activeRole.nestedPermissions}
role={{ id: activeRoleId }}
/>
)}
<div className="space-y-1">
{roles.map((role) => (
<div
key={role.id}
className="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-2"
>
<Checkbox
checked={role.isActive}
onCheckedChange={(checked) => {
if (checked) {
connectRole.mutate({ keyId: keyId, roleId: role.id });
} else {
disconnectRole.mutate({ keyId: keyId, roleId: role.id });
}
}}
/>
<div className="flex flex-col">
<span className="text-sm font-medium">{role.name}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const RootKeysFilter = ({
<FilterCheckbox
showScroll
options={(rootKeys ?? []).map((rootKey, index) => ({
label: rootKey.name ?? "<EMPTY>",
label: getRootKeyLabel(rootKey), // Format the root key label when empty and contains no name.
value: rootKey.id,
checked: false,
id: index,
Expand All @@ -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;
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 13 additions & 8 deletions apps/docs/apis/features/authorization/example.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,28 +76,33 @@ Now that we have permissions and roles in place, we can connect them to keys.
<Tab title="Dashboard">

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
<Frame>
<img src="/apis/features/authorization/api-keys-navigation.png" alt="Breadcrumb Navigation"/>
</Frame>

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_???`

<Frame>
<img src="/apis/features/authorization/connections.png" alt="Unconnected roles and permissions"/>
<img src="/apis/features/authorization/api-key-screen.png" alt="Unconnected roles and permissions"/>
</Frame>


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.


<Frame>
<img src="/apis/features/authorization/connections-connected.png" alt="Connected roles and permissions"/>
<img src="/apis/features/authorization/role-add-example.png" alt="Unconnected roles and permissions"/>
</Frame>

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:



</Tab>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 12 additions & 8 deletions apps/docs/apis/features/authorization/roles-and-permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Frame>
<img src="/apis/features/authorization/api-keys-navigation.png" alt="Breadcrumb Navigation"/>
</Frame>
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_???`

<Frame>
<img src="/apis/features/authorization/connections.png" alt="Unconnected roles and permissions"/>
<img src="/apis/features/authorization/api-key-screen.png" alt="Unconnected roles and permissions"/>
</Frame>


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.


<Frame>
<img src="/apis/features/authorization/connections-connected.png" alt="Connected roles and permissions"/>
<img src="/apis/features/authorization/role-add-example.png" alt="Unconnected roles and permissions"/>
</Frame>

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:




Expand Down
Loading