Skip to content

Commit 498bb0e

Browse files
authored
Merge pull request #729 from PROCEED-Labs/folder-permission-management
Folder Permission Management
2 parents 8ca3a39 + 81c96a6 commit 498bb0e

25 files changed

Lines changed: 894 additions & 477 deletions

File tree

src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/folder-permissions.tsx

Lines changed: 409 additions & 0 deletions
Large diffs are not rendered by default.

src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ import RolePermissions from './rolePermissions';
1111
import RoleMembers from './role-members';
1212
import { AuthenticatedUser } from '@/lib/data/user-schema';
1313
import SpaceLink from '@/components/space-link';
14-
import { getFolderById } from '@/lib/data/db/folders';
14+
import { getFolderById, getFolders } from '@/lib/data/db/folders';
15+
import FolderPermissions from './folder-permissions';
1516

1617
const Page = async (props: { params: Promise<{ roleId: string; environmentId: string }> }) => {
1718
const params = await props.params;
1819

1920
const { roleId, environmentId } = params;
2021

2122
const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId);
22-
const role = await getRoleWithMembersById(roleId, ability);
23+
const role = await getRoleWithMembersById(roleId, ability, true);
2324
// if (role && !ability.can('manage', toCaslResource('Role', role))) return <UnauthorizedFallback />;
2425
if (!ability.can('admin', 'All')) return <UnauthorizedFallback />;
2526

@@ -40,6 +41,8 @@ const Page = async (props: { params: Promise<{ roleId: string; environmentId: st
4041
</Content>
4142
);
4243

44+
const folders = await getFolders(environmentId);
45+
4346
const usersInRole = role.members;
4447
const roleUserSet = new Set(usersInRole.map((member) => member.id));
4548

@@ -63,6 +66,11 @@ const Page = async (props: { params: Promise<{ roleId: string; environmentId: st
6366
label: 'Permissions',
6467
children: <RolePermissions role={role} />,
6568
},
69+
{
70+
key: 'folder-permissions',
71+
label: 'Folder Permissions',
72+
children: <FolderPermissions role={role} folders={folders} />,
73+
},
6674
];
6775

6876
if (role.name !== '@everyone' && role.name !== '@guest') {

src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/role-members.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ const AddUserModal: FC<{
6565
/* ---- */
6666
users={usersNotInRole}
6767
loading={loading}
68-
columns={(clearSelected, _, selectedRowKeys) => [
68+
columns={(clearSelected, _) => [
6969
{
7070
dataIndex: 'id',
71-
render: (id, user) => (
71+
render: (_, user) => (
7272
<Tooltip placement="top" title="Add to role">
7373
<Button
7474
icon={<PlusOutlined />}

src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/role-permissions-helper.ts

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import Ability from '@/lib/ability/abilityHelper';
2+
import { ResourceActionType, ResourceType } from '@/lib/ability/caslAbility';
3+
import {
4+
ResourceActionsMapping,
5+
permissionIdentifiersToNumber,
6+
} from '@/lib/authorization/permissionHelpers';
7+
import { Role } from '@/lib/data/role-schema';
8+
import { Fragment } from 'react/jsx-runtime';
9+
import { Divider, Form, Row, Space, Switch, Typography } from 'antd';
10+
import { useAbilityStore } from '@/lib/abilityStore';
11+
12+
export function switchChecked(
13+
permissions: Role['permissions'] | undefined,
14+
resources: keyof Role['permissions'] | (keyof Role['permissions'])[],
15+
action: ResourceActionType,
16+
) {
17+
if (!permissions) return false;
18+
19+
const resourceInPermissions =
20+
typeof resources === 'string'
21+
? resources in permissions
22+
: resources.every((res) => res in permissions);
23+
if (!resourceInPermissions) return false;
24+
25+
for (const resource of typeof resources === 'string' ? [resources] : resources) {
26+
const permissionNumber = permissions[resource]!;
27+
28+
if (action === 'admin' && permissionNumber !== ResourceActionsMapping.admin) return false;
29+
// If admin is checked all other permissions are checked
30+
else if (action !== 'admin' && permissionNumber === ResourceActionsMapping.admin) continue;
31+
32+
// bit check
33+
if (!(ResourceActionsMapping[action] & permissionNumber)) return false;
34+
}
35+
36+
return true;
37+
}
38+
39+
function switchDisabled(
40+
permissions: Role['permissions'] | undefined,
41+
resources: keyof Role['permissions'] | (keyof Role['permissions'])[],
42+
action: ResourceActionType,
43+
ability: Ability,
44+
) {
45+
if (action === 'admin' && !ability.can('admin', resources)) return true;
46+
47+
if (!permissions) return false;
48+
49+
const resourceInPermissions =
50+
typeof resources === 'string'
51+
? resources in permissions
52+
: resources.every((res) => res in permissions);
53+
if (!resourceInPermissions) return false;
54+
55+
for (const resource of typeof resources === 'string' ? [resources] : resources) {
56+
const permissionNumber = permissions[resource]!;
57+
if (permissionNumber === ResourceActionsMapping.admin && action !== 'admin') return true;
58+
}
59+
60+
return false;
61+
}
62+
63+
export type ResourceFormEntries = Record<ResourceType, Record<ResourceActionType, boolean>>;
64+
65+
export function formDataToPermissions(values: ResourceFormEntries) {
66+
return Object.fromEntries(
67+
Object.entries(values).map(([resource, actions]) => [
68+
resource,
69+
permissionIdentifiersToNumber(
70+
Object.entries(actions)
71+
.filter(([_, enabled]) => enabled)
72+
.map(([action]) => action as ResourceActionType),
73+
),
74+
]),
75+
) as Record<ResourceType, number>;
76+
}
77+
78+
type PermissionCategory = {
79+
key: string;
80+
title: string;
81+
resource: ResourceType;
82+
permissions: {
83+
key: string;
84+
title: string;
85+
description: string;
86+
permission: ResourceActionType;
87+
}[];
88+
};
89+
90+
export function permissionsToFormData(
91+
options: PermissionCategory[],
92+
permissions: Role['permissions'],
93+
) {
94+
return Object.fromEntries(
95+
options.map((entry) => [
96+
entry.resource,
97+
Object.fromEntries(
98+
entry.permissions.map(({ permission }) => [
99+
permission,
100+
switchChecked(permissions, entry.resource, permission),
101+
]),
102+
),
103+
]),
104+
);
105+
}
106+
107+
export const ResourcePermissionInputs: React.FC<{
108+
pathPrefix?: (string | number)[];
109+
options: PermissionCategory[];
110+
permissions: Role['permissions'];
111+
}> = ({ pathPrefix = [], options, permissions }) => {
112+
const ability = useAbilityStore((store) => store.ability);
113+
114+
return (
115+
<>
116+
{options.map((permissionCategory) => (
117+
<Fragment key={permissionCategory.key}>
118+
<Typography.Title type="secondary" level={5}>
119+
{permissionCategory.title}
120+
</Typography.Title>
121+
{permissionCategory.permissions.map((permission, idx) => (
122+
<Fragment key={permission.key}>
123+
<Row align="top" justify="space-between" wrap={false}>
124+
<Space orientation="vertical" size={0}>
125+
<Typography.Text strong>{permission.title}</Typography.Text>
126+
<Typography.Text type="secondary">{permission.description}</Typography.Text>
127+
</Space>
128+
<Form.Item
129+
valuePropName="checked"
130+
name={[...pathPrefix, permissionCategory.resource, permission.permission]}
131+
>
132+
<Switch
133+
disabled={switchDisabled(
134+
permissions,
135+
permissionCategory.resource,
136+
permission.permission,
137+
ability,
138+
)}
139+
/>
140+
</Form.Item>
141+
</Row>
142+
{idx < permissionCategory.permissions.length - 1 && (
143+
<Divider style={{ marginTop: '10px', marginBottom: '10px' }} />
144+
)}
145+
</Fragment>
146+
))}
147+
<br />
148+
</Fragment>
149+
))}
150+
</>
151+
);
152+
};

0 commit comments

Comments
 (0)