Skip to content

Commit

Permalink
feat: track permission changes
Browse files Browse the repository at this point in the history
  • Loading branch information
gasp committed Jul 27, 2024
1 parent e85736e commit 8c244a8
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
createUserGroup,
createUserGroupReset,
} from "../../../../../state/usergroups/actions";
import { GroupPermissions } from "./GroupPermissions";
import { GroupPermissions } from "../editPermissions/GroupPermissions";

export const EditGroup = () => {
const dispatch = useDispatch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import React from "react";
import { Checkbox, Popper } from "@material-ui/core";

import { PermissionDTO } from "../../../../../generated";
import { computeNewPermission } from "./permission.utils";

interface IProps {
permission: PermissionDTO;
onChange: (permission: PermissionDTO) => void;
thisGroup: string;
thisGroupId: string;
}

export const AclPermissionCheckbox = ({
permission,
onChange,
thisGroup,
thisGroupId,
}: IProps) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
Expand All @@ -21,8 +22,12 @@ export const AclPermissionCheckbox = ({
<>
<Checkbox
aria-describedby={id}
checked={!!permission.userGroupIds.find((group) => group === thisGroup)}
onChange={() => console.log}
checked={
!!permission.userGroupIds.find((group) => group === thisGroupId)
}
onChange={(_ev, val) =>
onChange(computeNewPermission(thisGroupId, permission, val))
}
name={permission.id.toString()}
onMouseEnter={(event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
Expand All @@ -45,6 +50,8 @@ export const AclPermissionCheckbox = ({
}}
>
{permission.name || "unknown"}
{/**- TODO REMOVE DEBUG */}
&bull;{permission.userGroupIds.join(",")} {thisGroupId}
</span>
</Popper>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,15 @@ import React, { useMemo } from "react";
import { PermissionDTO } from "../../../../../generated";
import { AclPermissionCheckbox } from "./AclPermissionCheckbox";
import classes from "./AclTable.module.scss";
import { Crud, groupPermissions } from "./permission.utils";

interface IProps {
permissions: PermissionDTO[];
userGroupId: string;
onChange: (permission: PermissionDTO) => void;
}

enum Crud {
CREATE = "create",
READ = "read",
UPDATE = "update",
DELETE = "delete",
}

export const groupPermissions = (
permissions: PermissionDTO[]
): Map<string, Record<Crud, PermissionDTO>> => {
let permissionNames = new Map();
for (let i = 0; i < permissions.length; i++) {
const matches =
permissions[i].name &&
/([a-z]+)\.(create|read|update|delete)$/.exec(permissions[i].name || "");
// no match: skip
if (!matches) continue;
const [, key, access] = matches;

if (!!permissionNames.get(key)?.[access]) {
throw new Error(`duplicate permission ${key}.${access}`);
}

permissionNames.set(key, {
...permissionNames.get(key),
[access]: permissions[i],
});
}
return permissionNames;
};

export const AclTable = ({ permissions, userGroupId }: IProps) => {
export const AclTable = ({ permissions, userGroupId, onChange }: IProps) => {
const crudPermissions = useMemo(() => {
return groupPermissions(permissions);
}, [permissions]);
Expand Down Expand Up @@ -69,12 +40,12 @@ export const AclTable = ({ permissions, userGroupId }: IProps) => {
<td key={index}>
<AclPermissionCheckbox
permission={crudPermission[access]}
onChange={() => console.log}
thisGroup={userGroupId}
onChange={onChange}
thisGroupId={userGroupId}
/>
</td>
) : (
<td className={classes.empty}>
<td key={index} className={classes.empty}>
<span></span>
</td>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";

import { PermissionDTO } from "../../../../../generated";
import { PermissionCheckbox } from "./PermissionCheckbox";

interface IProps {
permissions: PermissionDTO[];
thisGroupId: string;
onChange: (permission: PermissionDTO) => void;
}
export const AreaAccess = ({ permissions, thisGroupId, onChange }: IProps) => {
return (
<ul>
{permissions
.filter(
(perm: PermissionDTO) => perm.name && /\.access$/.test(perm.name)
)
.map((perm, index) => (
<li key={index}>
<PermissionCheckbox
permission={perm}
onChange={onChange}
thisGroupId={thisGroupId}
/>
</li>
))}
</ul>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { useDispatch, useSelector } from "react-redux";

import { getAllPermissions } from "../../../../../state/permissions/actions";
import { IState } from "../../../../../types";
import { UserGroupDTO, PermissionDTO } from "../../../../../generated";
import { UserGroupDTO } from "../../../../../generated";
import InfoBox from "../../../infoBox/InfoBox";

import { PermissionCheckbox } from "./PermissionCheckbox";
import { AclTable } from "./AclTable";
import { GroupPermissionsEditor } from "./GroupPermissionsEditor";

interface IProps {
userGroupId: Pick<UserGroupDTO, "code"> | string;
Expand All @@ -31,29 +29,11 @@ export const GroupPermissions = ({ userGroupId }: IProps) => {
);
if (!permissionsState.hasSucceeded || !permissionsState.data) return <>...</>;
if (!permissionsState.data.length) return <>no permissions</>;

return (
<>
<h2>Areas access</h2>
<ul>
{permissionsState.data
.filter(
(perm: PermissionDTO) => perm.name && /\.access$/.test(perm.name)
)
.map((perm, index) => (
<li key={index}>
<PermissionCheckbox
permission={perm}
onChange={() => console.log}
thisGroup={userGroupId as string}
/>
</li>
))}
</ul>
<h2>Access-control list</h2>
<AclTable
permissions={permissionsState.data}
userGroupId={userGroupId as string}
/>
</>
<GroupPermissionsEditor
permissions={permissionsState.data}
thisGroupId={userGroupId as string}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useEffect, useState } from "react";

import { PermissionDTO } from "../../../../../generated";
import { AclTable } from "./AclTable";
import { AreaAccess } from "./AreaAccess";

interface IProps {
permissions: PermissionDTO[];
thisGroupId: string;
}
export const GroupPermissionsEditor = ({
permissions,
thisGroupId,
}: IProps) => {
const [permissionsStack, setPermissionsStack] = useState<PermissionDTO[]>([]);
const [permissionsState, setPermissionsState] = useState<PermissionDTO[]>([]);

useEffect(() => {
setPermissionsState(permissions);
}, [permissions]);

const handleChange = (newPermission: PermissionDTO) => {
const otherPermissions = permissionsStack.filter(
({ id }) => id !== newPermission.id
);
setPermissionsStack([...otherPermissions, newPermission]);
const newState = permissionsState.map((perm) =>
perm.id === newPermission.id ? newPermission : perm
);
setPermissionsState(newState);
};

return (
<>
<h2>Areas access</h2>
<AreaAccess
permissions={permissionsState}
thisGroupId={thisGroupId}
onChange={handleChange}
/>

<h2>Access-control list</h2>
<AclTable
permissions={permissionsState}
userGroupId={thisGroupId}
onChange={handleChange}
/>
{permissionsStack.length && (
<p>
Editing permissions: {permissionsStack.map(({ id }) => id).join(",")}
</p>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import React from "react";

import { PermissionDTO } from "../../../../../generated";
import { Checkbox, FormControlLabel } from "@material-ui/core";
import { PermissionDTO } from "../../../../../generated";
import { computeNewPermission } from "./permission.utils";

interface IProps {
permission: PermissionDTO;
onChange: (permission: PermissionDTO) => void;
thisGroup: string;
thisGroupId: string;
}

export const PermissionCheckbox = ({
permission,
onChange,
thisGroup,
thisGroupId,
}: IProps) => {
return (
<FormControlLabel
control={
<Checkbox
checked={
!!permission.userGroupIds.find((group) => group === thisGroup)
!!permission.userGroupIds.find((group) => group === thisGroupId)
}
onChange={(_ev, val) =>
onChange(computeNewPermission(thisGroupId, permission, val))
}
onChange={() => console.log}
name={permission.id.toString()}
/>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PermissionDTO } from "../../../../../generated";
import { groupPermissions } from "./AclTable";
import { computeNewPermission, groupPermissions } from "./permission.utils";

describe("groupPermissions", () => {
it("should ignore non-crud actions and badly formatted permission names", () => {
Expand Down Expand Up @@ -57,3 +57,32 @@ describe("groupPermissions", () => {
expect(() => groupPermissions(doublePermissions)).toThrow();
});
});

describe("computeNewPermission", () => {
const samplePermission: PermissionDTO = {
description: "",
id: 163,
name: "laboratories.access",
userGroupIds: ["foo", "bar"],
};
it("should remove a group to permission", () => {
expect(
computeNewPermission("bar", { ...samplePermission }, false).userGroupIds
).toEqual(["foo"]);
});
it("should add a group to permission", () => {
expect(
computeNewPermission("con", { ...samplePermission }, true).userGroupIds
).toEqual(["foo", "bar", "con"]);
});
it("should leave a group as it (do not add twice a group)", () => {
expect(
computeNewPermission("bar", { ...samplePermission }, true).userGroupIds
).toEqual(["foo", "bar"]);
});
it("should leave a group as it (can't remove a non-existant group)", () => {
expect(
computeNewPermission("baz", { ...samplePermission }, false).userGroupIds
).toEqual(["foo", "bar"]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { PermissionDTO } from "../../../../../generated";

export enum Crud {
CREATE = "create",
READ = "read",
UPDATE = "update",
DELETE = "delete",
}

export const groupPermissions = (
permissions: PermissionDTO[]
): Map<string, Record<Crud, PermissionDTO>> => {
let permissionNames = new Map();
for (let i = 0; i < permissions.length; i++) {
const matches =
permissions[i].name &&
/([a-z]+)\.(create|read|update|delete)$/.exec(permissions[i].name || "");
// no match: skip
if (!matches) continue;
const [, key, access] = matches;

if (!!permissionNames.get(key)?.[access]) {
throw new Error(`duplicate permission ${key}.${access}`);
}

permissionNames.set(key, {
...permissionNames.get(key),
[access]: permissions[i],
});
}
return permissionNames;
};

export const computeNewPermission = (
thisGroupId: string,
permission: PermissionDTO,
val: boolean
): PermissionDTO => {
const getUserGroups = () => {
if (val) {
if (permission.userGroupIds.includes(thisGroupId))
return permission.userGroupIds;
return [...permission.userGroupIds, thisGroupId];
}
let userGroupIds: string[] = [];
for (let index = 0; index < permission.userGroupIds.length; index++) {
if (permission.userGroupIds[index] !== thisGroupId)
userGroupIds = [...userGroupIds, permission.userGroupIds[index]];
}
return userGroupIds;
};

return {
...permission,
userGroupIds: getUserGroups(),
};
};
5 changes: 4 additions & 1 deletion src/mockServer/fixtures/permissionDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const permissionDTO: PermissionDTO[] = Array.from(permissionList).map(
name: permissionName,
id: i,
description: "",
userGroupIds: ["admin", userGroupsDTO[i % userGroupsDTO.length].code],
userGroupIds: [
userGroupsDTO[0].code, //admin
userGroupsDTO[(i + 1) % (userGroupsDTO.length - 1)].code, //others
],
})
);

0 comments on commit 8c244a8

Please sign in to comment.