diff --git a/.github/workflows/forms-flow-admin-cd.yml b/.github/workflows/forms-flow-admin-cd.yml index d99fd6bc9..31ec73cb9 100644 --- a/.github/workflows/forms-flow-admin-cd.yml +++ b/.github/workflows/forms-flow-admin-cd.yml @@ -6,6 +6,7 @@ on: - main - develop - release/* + - feature/FWF-3316-permission-matrix paths: - "forms-flow-admin/**" - "VERSION" diff --git a/.github/workflows/forms-flow-integration-cd.yml b/.github/workflows/forms-flow-integration-cd.yml index 92e8da83a..53f8ddcb6 100644 --- a/.github/workflows/forms-flow-integration-cd.yml +++ b/.github/workflows/forms-flow-integration-cd.yml @@ -6,6 +6,7 @@ on: - main - develop - release/* + - feature/FWF-3316-permission-matrix paths: - "forms-flow-integration/**" - "VERSION" diff --git a/.github/workflows/forms-flow-nav.cd.yml b/.github/workflows/forms-flow-nav.cd.yml index d157d4fa0..c9ad5b775 100644 --- a/.github/workflows/forms-flow-nav.cd.yml +++ b/.github/workflows/forms-flow-nav.cd.yml @@ -6,6 +6,7 @@ on: - main - develop - release/* + - feature/FWF-3316-permission-matrix paths: - "forms-flow-nav/**" - "VERSION" diff --git a/.github/workflows/forms-flow-service.yml b/.github/workflows/forms-flow-service.yml index 9bf4596a3..8f6ede2a8 100644 --- a/.github/workflows/forms-flow-service.yml +++ b/.github/workflows/forms-flow-service.yml @@ -6,6 +6,7 @@ on: - main - develop - release/* + - feature/FWF-3316-permission-matrix paths: - "forms-flow-service/**" - "VERSION" diff --git a/.github/workflows/forms-flow-theme.yml b/.github/workflows/forms-flow-theme.yml index 33037713e..6f5dfa948 100644 --- a/.github/workflows/forms-flow-theme.yml +++ b/.github/workflows/forms-flow-theme.yml @@ -6,6 +6,7 @@ on: - main - develop - release/* + - feature/FWF-3316-permission-matrix paths: - "forms-flow-theme/**" - "VERSION" diff --git a/VERSION b/VERSION index fb5d1e208..07abcadc7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v6.1.0-alpha +v6.1.0-rbac-alpha diff --git a/forms-flow-admin/src/components/AccessDenied/AccessDenied.js b/forms-flow-admin/src/components/AccessDenied/AccessDenied.js new file mode 100644 index 000000000..b0478f8c9 --- /dev/null +++ b/forms-flow-admin/src/components/AccessDenied/AccessDenied.js @@ -0,0 +1,46 @@ +// AccessDeniedIcon.js +import React from 'react'; + +const AccessDeniedIcon = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default AccessDeniedIcon; diff --git a/forms-flow-admin/src/components/AccessDenied/accessDenied.scss b/forms-flow-admin/src/components/AccessDenied/accessDenied.scss new file mode 100644 index 000000000..aac677799 --- /dev/null +++ b/forms-flow-admin/src/components/AccessDenied/accessDenied.scss @@ -0,0 +1,12 @@ +.access-denied-text { + font-family: Arial, Helvetica, sans-serif; + font-size: 2rem; + color: var(--ff-black); + opacity: 1; + } + .access-denied{ + font-family: Arial, Helvetica, sans-serif; + font-size: 1rem; + color: var(--ff-black); + opacity: 1; + } \ No newline at end of file diff --git a/forms-flow-admin/src/components/AccessDenied/index.js b/forms-flow-admin/src/components/AccessDenied/index.js new file mode 100644 index 000000000..fa0e4a3c1 --- /dev/null +++ b/forms-flow-admin/src/components/AccessDenied/index.js @@ -0,0 +1,45 @@ +import React from "react"; +import AccessDeniedIcon from "./AccessDenied.js"; +import './accessDenied.scss'; +import { useTranslation } from "react-i18next"; +import { BASE_ROUTE } from "../../constants/index"; +import { useHistory } from "react-router-dom"; + + +const AccessDenied = ({ userRoles }) => { + const { t } = useTranslation(); + const history = useHistory(); + + const handleLogout = () => { + const kcInstance = kcServiceInstance(); + kcInstance.userLogout(); + }; + + const handleReturn = () => { + history.push(BASE_ROUTE); + }; + + const showReturnToLogin = userRoles?.length === 0; + const showReturnToHome = userRoles?.length > 0; + + return ( +
+ +

{t("Access Denied")}

+ {t("You don't have permission to access this page.")} + {t("Please contact your administrator or try again later.")} + {showReturnToLogin && ( + + )} + {showReturnToHome && ( + + )} +
+ ); +}; + +export default AccessDenied; diff --git a/forms-flow-admin/src/components/roles/roles.tsx b/forms-flow-admin/src/components/roles/roles.tsx index eb86e468d..5f62c3cda 100644 --- a/forms-flow-admin/src/components/roles/roles.tsx +++ b/forms-flow-admin/src/components/roles/roles.tsx @@ -7,7 +7,12 @@ import paginationFactory from "react-bootstrap-table2-paginator"; import Form from "react-bootstrap/Form"; import Button from "react-bootstrap/Button"; import { fetchUsers } from "../../services/users"; -import { CreateRole, DeleteRole, UpdateRole } from "../../services/roles"; +import { + CreateRole, + DeleteRole, + UpdateRole, + fetchPermissions, +} from "../../services/roles"; import Modal from "react-bootstrap/Modal"; import Loading from "../loading"; import DropdownButton from "react-bootstrap/DropdownButton"; @@ -21,6 +26,7 @@ import { } from "../../constants"; import { DEFAULT_ROLES } from "../../constants"; +import {removingTenantId} from "../../utils/utils.js"; const Roles = React.memo((props: any) => { const { t } = useTranslation(); const { tenantId } = useParams(); @@ -36,13 +42,18 @@ const Roles = React.memo((props: any) => { const [showRoleModal, setShowRoleModal] = useState(false); const [showEditRoleModal, setShowEditRoleModal] = useState(false); const [loading, setLoading] = React.useState(false); - const [payload, setPayload] = React.useState({ name: "", description: "" }); + const [payload, setPayload] = React.useState({ + name: "", + description: "", + permissions: [], + }); // Toggle for Delete Confirm modal const [showConfirmDelete, setShowConfirmDelete] = React.useState(false); const initialRoleType = { name: "", id: "", description: "", + permissions: [], }; const [deleteCandidate, setDeleteCandidate] = React.useState(initialRoleType); const [selectedRoleIdentifier, setSelectedRoleIdentifier] = @@ -50,30 +61,60 @@ const Roles = React.memo((props: any) => { const [editCandidate, setEditCandidate] = React.useState(initialRoleType); const [disabled, setDisabled] = React.useState(true); const [search, setSerach] = React.useState(""); + const [permission, setPermission] = React.useState([]); const filterList = (filterTerm, List) => { - let roleList = List.filter((role) => { + let roleList = removingTenantId(List,tenantId); + let newRoleList = roleList.filter((role) => { return role.name.toLowerCase().search(filterTerm.toLowerCase()) !== -1; }); - return roleList; + return newRoleList; }; React.useEffect(() => { - setDisabled(!(payload.name?.trim() && payload.description?.trim())); + setDisabled( + !( + payload.name?.trim() && + payload.description?.trim() && + payload.permissions.length !== 0 + ) + ); }, [payload]); React.useEffect(() => { setDisabled( - !(editCandidate.name?.trim() && editCandidate.description?.trim()) + !( + editCandidate.name?.trim() && + editCandidate.description?.trim() && + editCandidate.permissions.length !== 0 + ) ); }, [editCandidate]); React.useEffect(() => { + let updatedRoles = props.roles; + if (search) { - return setRoles(filterList(search, props.roles)); + updatedRoles = filterList(search, updatedRoles); } - setRoles(props.roles); - }, [props.roles]); + + if (updatedRoles.length > 0) { + updatedRoles = removingTenantId(updatedRoles,tenantId); + } + + setRoles(updatedRoles); + }, [props.roles, search]); + + React.useEffect(() => { + fetchPermissions( + (data) => { + setPermission(data); + }, + (err) => { + setError(err); + } + ); + }, []); const handlFilter = (e) => { setSerach(e.target.value); @@ -103,9 +144,39 @@ const Roles = React.memo((props: any) => { const handleChangeDescription = (e) => { setPayload({ ...payload, description: e.target.value }); }; + const handlePermissionCheck = ( + permissionName: string, + dependsOn: string[] + ) => { + let updatedPermissions: string[] = [...payload.permissions]; + const isChecked = updatedPermissions.includes(permissionName); + + if (!isChecked) { + updatedPermissions.push(permissionName); + dependsOn.forEach((dependency) => { + if (!updatedPermissions.includes(dependency)) { + updatedPermissions.push(dependency); + } + }); + } else { + updatedPermissions = updatedPermissions.filter( + (permission) => permission !== permissionName + ); + dependsOn.forEach((dependency) => { + updatedPermissions = updatedPermissions.filter( + (permission) => permission !== dependency + ); + }); + } + setPayload({ ...payload, permissions: updatedPermissions }); + }; const validateRolePayload = (payload) => { - return !(payload.name === "" || payload.description === ""); + return !( + payload.name === "" || + payload.description === "" || + payload.permissions.length === 0 + ); }; //check regex exept _ - const hasSpecialCharacters = (text) => { @@ -121,21 +192,21 @@ const Roles = React.memo((props: any) => { if (!validateRolePayload(payload)) { return; } - if (KEYCLOAK_ENABLE_CLIENT_AUTH) { - if (hasSpecialCharacters(payload.name)) { - toast.error( - t("Role names cannot contain special characters except _ , -") - ); - return; - } - } else { - if (hasSpecialCharacterswithslash(payload.name)) { - toast.error( - t("Role names cannot contain special characters except _ , - , / ") - ); - return; - } + // if (KEYCLOAK_ENABLE_CLIENT_AUTH) { + // if (hasSpecialCharacters(payload.name)) { + // toast.error( + // t("Role names cannot contain special characters except _ , -") + // ); + // return; + // } + // } else { + if (hasSpecialCharacterswithslash(payload.name)) { + toast.error( + t("Role names cannot contain special characters except _ , - , / ") + ); + return; } + // } setDisabled(true); CreateRole( payload, @@ -155,21 +226,21 @@ const Roles = React.memo((props: any) => { if (!validateRolePayload(editCandidate)) { return; } - if (KEYCLOAK_ENABLE_CLIENT_AUTH) { - if (hasSpecialCharacters(editCandidate.name)) { - toast.error( - t("Role names cannot contain special characters except _ , -") - ); - return; - } - } else { - if (hasSpecialCharacterswithslash(editCandidate.name)) { - toast.error( - t("Role names cannot contain special characters except _ , - , / ") - ); - return; - } + // if (KEYCLOAK_ENABLE_CLIENT_AUTH) { + // if (hasSpecialCharacters(editCandidate.name)) { + // toast.error( + // t("Role names cannot contain special characters except _ , -") + // ); + // return; + // } + // } else { + if (hasSpecialCharacterswithslash(editCandidate.name)) { + toast.error( + t("Role names cannot contain special characters except _ , - , / ") + ); + return; } + // } setDisabled(true); UpdateRole( selectedRoleIdentifier, @@ -186,7 +257,6 @@ const Roles = React.memo((props: any) => { } ); }; - // handlers for user list popover const handleClick = (event, rowData) => { setShow(!show); @@ -216,10 +286,37 @@ const Roles = React.memo((props: any) => { setEditCandidate({ ...editCandidate, description: e.target.value }); }; + const handleEditPermissionCheck = ( + permissionName: string, + dependsOn: string[] + ) => { + let updatedPermissions: string[] = [...editCandidate.permissions]; + const isChecked = updatedPermissions.includes(permissionName); + + if (!isChecked) { + updatedPermissions.push(permissionName); + dependsOn.forEach((dependency) => { + if (!updatedPermissions.includes(dependency)) { + updatedPermissions.push(dependency); + } + }); + } else { + updatedPermissions = updatedPermissions.filter( + (permission) => permission !== permissionName + ); + dependsOn.forEach((dependency) => { + updatedPermissions = updatedPermissions.filter( + (permission) => permission !== dependency + ); + }); + } + setEditCandidate({ ...editCandidate, permissions: updatedPermissions }); + }; + // handlers for role create/edit modal const handleCloseRoleModal = () => { setShowRoleModal(false); - setPayload({ name: "", description: "" }); + setPayload({ name: "", description: "", permissions: [] }); }; const handleShowRoleModal = () => setShowRoleModal(true); const handleCloseEditRoleModal = () => { @@ -258,8 +355,16 @@ const Roles = React.memo((props: any) => { return DEFAULT_ROLES.includes(role); } }; - // Delete confirmation + + + const clearSearch = () => { + setSerach(""); + let updatedRoleName = removingTenantId(props.roles,tenantId); + setRoles(updatedRoleName); + }; + + // Delete confirmation const confirmDelete = () => (
@@ -294,111 +399,180 @@ const Roles = React.memo((props: any) => { const showCreateModal = () => (
- - {t("Create Role")} - - - - - {t("Role Name")} - - * - - - {t("Description")} - - * - - - - - - - + {t("Permissions")} + + * +
+ {permission.map((permission) => ( +
+ + handlePermissionCheck( + permission.name, + permission.depends_on + ) + } + aria-label={t(permission.description)} + /> +
+ ))} +
+ + + + + +
); const showEditModal = () => (
- - {t("Edit Role")} - - - - - {t("Role Name")} - - * - - - {t("Description")} - - * - - - - - - - + {t("Permissions")} + + * +
+ {permission.map((permission) => ( +
+ + handleEditPermissionCheck( + permission.name, + permission.depends_on + ) + } + aria-label={t(permission.description)} + /> +
+ ))} +
+ + + + + +
); @@ -541,11 +715,12 @@ const Roles = React.memo((props: any) => { className="fa fa-pencil me-4" style={{ color: "#7E7E7F", cursor: "pointer" }} onClick={() => { - setSelectedRoleIdentifier( - KEYCLOAK_ENABLE_CLIENT_AUTH ? rowData.name : rowData.id - ); + setSelectedRoleIdentifier(rowData.id); setEditCandidate(rowData); handleShowEditRoleModal(); + // setSelectedRoleIdentifier( + // KEYCLOAK_ENABLE_CLIENT_AUTH ? rowData.name : rowData.id + // ); }} data-testid="admin-roles-edit-icon" /> @@ -581,10 +756,7 @@ const Roles = React.memo((props: any) => { {search.length > 0 && (