From d443266424aaa5f3c3cecff0365bbbd10f845528 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Fri, 11 Oct 2024 09:20:35 +0200 Subject: [PATCH 01/40] remove permission from branch selector --- frontend/app/src/components/branch-selector.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/app/src/components/branch-selector.tsx b/frontend/app/src/components/branch-selector.tsx index 29521dd781..dc5405898f 100644 --- a/frontend/app/src/components/branch-selector.tsx +++ b/frontend/app/src/components/branch-selector.tsx @@ -1,17 +1,17 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { QSP } from "@/config/qsp"; import { Branch } from "@/generated/graphql"; -import { usePermission } from "@/hooks/usePermission"; import { branchesState, currentBranchAtom } from "@/state/atoms/branches.atom"; import { branchesToSelectOptions } from "@/utils/branches"; import { Icon } from "@iconify-icon/react"; import { useAtomValue } from "jotai/index"; -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { StringParam, useQueryParam } from "use-query-params"; import { ComboboxItem } from "@/components/ui/combobox"; import { Command, CommandEmpty, CommandInput, CommandList } from "@/components/ui/command"; import graphqlClient from "@/graphql/graphqlClientApollo"; +import { useAuth } from "@/hooks/useAuth"; import { constructPath } from "@/utils/fetch"; import { useSetAtom } from "jotai"; import { Button, ButtonWithTooltip, LinkButton } from "./buttons/button-primitive"; @@ -162,13 +162,13 @@ function BranchOption({ branch, onChange }: { branch: Branch; onChange: () => vo } export const BranchFormTriggerButton = ({ setOpen }: { setOpen: (open: boolean) => void }) => { - const permission = usePermission(); + const { isAuthenticated } = useAuth(); return ( { if (e.key === "Enter") { From 508805daa1bb416d2f1ea7c1f46518bcb6732104 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Fri, 11 Oct 2024 09:20:54 +0200 Subject: [PATCH 02/40] check permission for object details and items --- .../form/object-create-form-trigger.tsx | 15 ++---- .../app/src/graphql/graphqlClientApollo.tsx | 4 ++ .../queries/objects/getObjectDetails.ts | 12 +++++ .../graphql/queries/objects/getObjectItems.ts | 11 +++++ frontend/app/src/hooks/useObjectDetails.ts | 14 +++++- frontend/app/src/hooks/useObjectItems.ts | 14 +++++- .../app/src/pages/objects/object-details.tsx | 16 +++++-- .../screens/errors/unauthorized-screen.tsx | 25 ++++++++++ .../groups/groups-manager-trigger-button.tsx | 7 ++- .../action-buttons/details-buttons.tsx | 13 +++--- .../object-item-details-paginated.tsx | 17 ++++--- .../object-items/object-items-paginated.tsx | 30 ++++++------ .../app/src/screens/objects/object-header.tsx | 8 +++- .../src/screens/proposed-changes/items.tsx | 1 + frontend/app/src/utils/permissions.ts | 46 +++++++++++++++++++ 15 files changed, 183 insertions(+), 50 deletions(-) create mode 100644 frontend/app/src/screens/errors/unauthorized-screen.tsx create mode 100644 frontend/app/src/utils/permissions.ts diff --git a/frontend/app/src/components/form/object-create-form-trigger.tsx b/frontend/app/src/components/form/object-create-form-trigger.tsx index 43d168ce42..6a4704210b 100644 --- a/frontend/app/src/components/form/object-create-form-trigger.tsx +++ b/frontend/app/src/components/form/object-create-form-trigger.tsx @@ -1,10 +1,8 @@ import SlideOver, { SlideOverTitle } from "@/components/display/slide-over"; import ObjectForm from "@/components/form/object-form"; -import { ACCOUNT_GENERIC_OBJECT, ARTIFACT_OBJECT } from "@/config/constants"; +import { ARTIFACT_OBJECT } from "@/config/constants"; import graphqlClient from "@/graphql/graphqlClientApollo"; -import { usePermission } from "@/hooks/usePermission"; import { IModelSchema } from "@/state/atoms/schema.atom"; -import { isGeneric } from "@/utils/common"; import { Icon } from "@iconify-icon/react"; import { useState } from "react"; import { Button, ButtonProps } from "../buttons/button-primitive"; @@ -19,22 +17,17 @@ export const ObjectCreateFormTrigger = ({ schema, onSuccess, isLoading, + permission, ...props }: ObjectCreateFormTriggerProps) => { - const permission = usePermission(); - const [showCreateDrawer, setShowCreateDrawer] = useState(false); if (schema.kind === ARTIFACT_OBJECT) { return null; } - const isAccount: boolean = - schema.kind === ACCOUNT_GENERIC_OBJECT || - (!isGeneric(schema) && !!schema.inherit_from?.includes(ACCOUNT_GENERIC_OBJECT)); - - const isAllowed = isAccount ? permission.isAdmin.allow : permission.write.allow; - const tooltipMessage = isAccount ? permission.isAdmin.message : permission.isAdmin.message; + const isAllowed = permission.create.allow; + const tooltipMessage = permission.create.message; return ( <> diff --git a/frontend/app/src/graphql/graphqlClientApollo.tsx b/frontend/app/src/graphql/graphqlClientApollo.tsx index 5f1b917cd7..65dcc41919 100644 --- a/frontend/app/src/graphql/graphqlClientApollo.tsx +++ b/frontend/app/src/graphql/graphqlClientApollo.tsx @@ -124,6 +124,10 @@ export const errorLink = onError(({ graphQLErrors, operation, forward }) => { forward(operation); }); } + case 403: { + // Do not display alert on unauthorized errors + return; + } default: const { processErrorMessage } = operation.getContext(); diff --git a/frontend/app/src/graphql/queries/objects/getObjectDetails.ts b/frontend/app/src/graphql/queries/objects/getObjectDetails.ts index b88c5b6447..f6d1fccfa9 100644 --- a/frontend/app/src/graphql/queries/objects/getObjectDetails.ts +++ b/frontend/app/src/graphql/queries/objects/getObjectDetails.ts @@ -101,6 +101,18 @@ query {{kind}} { {{/if}} } } + + permissions{ + edges{ + node{ + kind + view + create + update + delete + } + } + } } {{#if taskKind}} diff --git a/frontend/app/src/graphql/queries/objects/getObjectItems.ts b/frontend/app/src/graphql/queries/objects/getObjectItems.ts index df5f300440..e04c4deb1d 100644 --- a/frontend/app/src/graphql/queries/objects/getObjectItems.ts +++ b/frontend/app/src/graphql/queries/objects/getObjectItems.ts @@ -45,6 +45,17 @@ query {{kind}} ( {{/each}} } } + permissions{ + edges{ + node{ + kind + view + create + update + delete + } + } + } } } `); diff --git a/frontend/app/src/hooks/useObjectDetails.ts b/frontend/app/src/hooks/useObjectDetails.ts index be4a6c7f39..6da4f8a65e 100644 --- a/frontend/app/src/hooks/useObjectDetails.ts +++ b/frontend/app/src/hooks/useObjectDetails.ts @@ -4,6 +4,7 @@ import useQuery from "@/hooks/useQuery"; import { IModelSchema, genericsState } from "@/state/atoms/schema.atom"; import { isGeneric } from "@/utils/common"; import { getSchemaObjectColumns, getTabs } from "@/utils/getSchemaObjectColumns"; +import { getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { useAtomValue } from "jotai"; @@ -34,8 +35,19 @@ export const useObjectDetails = (schema: IModelSchema, objectId: string) => { "query { ok }" ); - return useQuery(query, { + const apolloQuery = useQuery(query, { skip: !schema, notifyOnNetworkStatusChange: true, }); + + const permission = getPermission( + schema?.kind && + apolloQuery?.data && + apolloQuery?.data[schema?.kind]?.permissions?.edges[0]?.node + ); + + return { + ...apolloQuery, + permission, + }; }; diff --git a/frontend/app/src/hooks/useObjectItems.ts b/frontend/app/src/hooks/useObjectItems.ts index b8b41e5f5d..662e99816c 100644 --- a/frontend/app/src/hooks/useObjectItems.ts +++ b/frontend/app/src/hooks/useObjectItems.ts @@ -5,6 +5,7 @@ import { Filter } from "@/hooks/useFilters"; import useQuery from "@/hooks/useQuery"; import { IModelSchema, genericsState, profilesAtom, schemaState } from "@/state/atoms/schema.atom"; import { getObjectAttributes, getObjectRelationships } from "@/utils/getSchemaObjectColumns"; +import { getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { useAtomValue } from "jotai"; @@ -63,5 +64,16 @@ export const useObjectItems = (schema?: IModelSchema, filters?: Array) = ${getQuery(schema, filters)} `; - return useQuery(query, { notifyOnNetworkStatusChange: true, skip: !schema }); + const apolloQuery = useQuery(query, { notifyOnNetworkStatusChange: true, skip: !schema }); + + const permission = getPermission( + schema?.kind && + apolloQuery?.data && + apolloQuery?.data[schema?.kind]?.permissions?.edges[0]?.node + ); + + return { + ...apolloQuery, + permission, + }; }; diff --git a/frontend/app/src/pages/objects/object-details.tsx b/frontend/app/src/pages/objects/object-details.tsx index 994901e7d1..508b9d9c47 100644 --- a/frontend/app/src/pages/objects/object-details.tsx +++ b/frontend/app/src/pages/objects/object-details.tsx @@ -3,6 +3,7 @@ import { TASK_OBJECT } from "@/config/constants"; import { useObjectDetails } from "@/hooks/useObjectDetails"; import ErrorScreen from "@/screens/errors/error-screen"; import NoDataFound from "@/screens/errors/no-data-found"; +import UnauthorizedScreen from "@/screens/errors/unauthorized-screen"; import Content from "@/screens/layout/content"; import LoadingScreen from "@/screens/loading-screen/loading-screen"; import ObjectItemDetails from "@/screens/object-item-details/object-item-details-paginated"; @@ -25,16 +26,20 @@ export function ObjectDetailsPage() { if (!objectid) return ; - const { data, networkStatus, error } = useObjectDetails(schema, objectid); - - if (error) { - return ; - } + const { data, networkStatus, error, permission } = useObjectDetails(schema, objectid); if (networkStatus === NetworkStatus.loading) { return ; } + if (!permission.view.isAllowed) { + return ; + } + + if (error) { + return ; + } + const objectDetailsData = schema && data && data[schema.kind!]?.edges[0]?.node; if (!objectDetailsData) { @@ -51,6 +56,7 @@ export function ObjectDetailsPage() { diff --git a/frontend/app/src/screens/errors/unauthorized-screen.tsx b/frontend/app/src/screens/errors/unauthorized-screen.tsx new file mode 100644 index 0000000000..105bbbe0e6 --- /dev/null +++ b/frontend/app/src/screens/errors/unauthorized-screen.tsx @@ -0,0 +1,25 @@ +import { classNames } from "@/utils/common"; +import { Icon } from "@iconify-icon/react"; +import { ReactElement } from "react"; + +type tUnauthorized = { + className?: string; + message?: string; + icon?: ReactElement; + hideIcon?: boolean; +}; + +const DEFAULT_MESSAGE = "Sorry, you are not authorized to access this view."; + +export default function UnauthorizedScreen({ className, message, icon, hideIcon }: tUnauthorized) { + return ( +
+ {!hideIcon && ( +
+ {icon || } +
+ )} +
{message ?? DEFAULT_MESSAGE}
+
+ ); +} diff --git a/frontend/app/src/screens/groups/groups-manager-trigger-button.tsx b/frontend/app/src/screens/groups/groups-manager-trigger-button.tsx index 87bcbc064a..c96fd74855 100644 --- a/frontend/app/src/screens/groups/groups-manager-trigger-button.tsx +++ b/frontend/app/src/screens/groups/groups-manager-trigger-button.tsx @@ -1,7 +1,6 @@ import { ButtonProps, ButtonWithTooltip } from "@/components/buttons/button-primitive"; import SlideOver, { SlideOverTitle } from "@/components/display/slide-over"; import { useObjectDetails } from "@/hooks/useObjectDetails"; -import { usePermission } from "@/hooks/usePermission"; import { GroupsManager, GroupsManagerProps } from "@/screens/groups/groups-manager"; import { Icon } from "@iconify-icon/react"; import { useState } from "react"; @@ -10,10 +9,10 @@ type GroupsManagerTriggerProps = ButtonProps & GroupsManagerProps; export const GroupsManagerTriggerButton = ({ schema, + permission, objectId, ...props }: GroupsManagerTriggerProps) => { - const permission = usePermission(); const [isManageGroupsDrawerOpen, setIsManageGroupsDrawerOpen] = useState(false); const { data } = useObjectDetails(schema, objectId); @@ -23,9 +22,9 @@ export const GroupsManagerTriggerButton = ({ return ( <> setIsManageGroupsDrawerOpen(true)} variant="outline" size="square" diff --git a/frontend/app/src/screens/object-item-details/action-buttons/details-buttons.tsx b/frontend/app/src/screens/object-item-details/action-buttons/details-buttons.tsx index 92e26d445f..6697f03f13 100644 --- a/frontend/app/src/screens/object-item-details/action-buttons/details-buttons.tsx +++ b/frontend/app/src/screens/object-item-details/action-buttons/details-buttons.tsx @@ -3,7 +3,6 @@ import SlideOver, { SlideOverTitle } from "@/components/display/slide-over"; import ModalDeleteObject from "@/components/modals/modal-delete-object"; import { ARTIFACT_DEFINITION_OBJECT, GENERIC_REPOSITORY_KIND } from "@/config/constants"; import graphqlClient from "@/graphql/graphqlClientApollo"; -import { usePermission } from "@/hooks/usePermission"; import { Generate } from "@/screens/artifacts/generate"; import { GroupsManagerTriggerButton } from "@/screens/groups/groups-manager-trigger-button"; import ObjectItemEditComponent from "@/screens/object-item-edit/object-item-edit-paginated"; @@ -19,8 +18,7 @@ type DetailsButtonsProps = { objectDetailsData: any; }; -export function DetailsButtons({ schema, objectDetailsData }: DetailsButtonsProps) { - const permission = usePermission(); +export function DetailsButtons({ schema, objectDetailsData, permission }: DetailsButtonsProps) { const location = useLocation(); const { objectid } = useParams(); const navigate = useNavigate(); @@ -36,9 +34,9 @@ export function DetailsButtons({ schema, objectDetailsData }: DetailsButtonsProp {schema.kind === ARTIFACT_DEFINITION_OBJECT && } setShowEditModal(true)} data-testid="edit-button" > @@ -48,6 +46,7 @@ export function DetailsButtons({ schema, objectDetailsData }: DetailsButtonsProp {!schema.kind?.match(/Core.*Group/g)?.length && ( // Hide group buttons on group list view @@ -58,9 +57,9 @@ export function DetailsButtons({ schema, objectDetailsData }: DetailsButtonsProp )} } + rightItems={ + + } /> )} @@ -151,9 +156,9 @@ export default function ObjectItemDetails({
{attribute.label}
{ setMetaEditFieldDetails({ type: "attribute", diff --git a/frontend/app/src/screens/object-items/object-items-paginated.tsx b/frontend/app/src/screens/object-items/object-items-paginated.tsx index b696d09236..3366878ca7 100644 --- a/frontend/app/src/screens/object-items/object-items-paginated.tsx +++ b/frontend/app/src/screens/object-items/object-items-paginated.tsx @@ -12,7 +12,6 @@ import { } from "@/config/constants"; import useFilters, { Filter } from "@/hooks/useFilters"; import { useObjectItems } from "@/hooks/useObjectItems"; -import { usePermission } from "@/hooks/usePermission"; import { useTitle } from "@/hooks/useTitle"; import ErrorScreen from "@/screens/errors/error-screen"; import NoDataFound from "@/screens/errors/no-data-found"; @@ -25,6 +24,7 @@ import { getSchemaObjectColumns } from "@/utils/getSchemaObjectColumns"; import { Icon } from "@iconify-icon/react"; import { useState } from "react"; import { Navigate } from "react-router-dom"; +import UnauthorizedScreen from "../errors/unauthorized-screen"; type ObjectItemsProps = { schema: IModelSchema; @@ -39,7 +39,6 @@ export default function ObjectItems({ preventBlock, preventLinks, }: ObjectItemsProps) { - const permission = usePermission(); const [filters, setFilters] = useFilters(); const [rowToDelete, setRowToDelete] = useState(); @@ -58,7 +57,7 @@ export default function ObjectItems({ // Get all the needed columns (attributes + relationships) const columns = getSchemaObjectColumns({ schema: schema, forListView: true }); - const { loading, error, data = {}, refetch } = useObjectItems(schema, filters); + const { loading, error, data = {}, refetch, permission } = useObjectItems(schema, filters); const result = data && schema?.kind ? (data[kindFilter?.value || schema?.kind] ?? {}) : {}; @@ -95,14 +94,12 @@ export default function ObjectItems({ const debouncedHandleSearch = debounce(handleSearch, 500); - if (error) { - return ; + if (!permission.view.isAllowed) { + return ; } - const currentPermission = permissions?.edges[0]?.node; - - if (currentPermission?.view !== "ALLOW") { - // return ; + if (error) { + return ; } return ( @@ -122,13 +119,18 @@ export default function ObjectItems({ - +
{loading && !rows && } {/* TODO: use new Table component for list */} - {rows && ( + {!loading && rows && (
@@ -168,9 +170,9 @@ export default function ObjectItems({ { setRowToDelete(row); diff --git a/frontend/app/src/screens/objects/object-header.tsx b/frontend/app/src/screens/objects/object-header.tsx index ba6827dd4c..827def1dff 100644 --- a/frontend/app/src/screens/objects/object-header.tsx +++ b/frontend/app/src/screens/objects/object-header.tsx @@ -33,6 +33,12 @@ const ObjectItemsHeader = ({ schema }: ObjectHeaderProps) => { const schemaKind = kindFilter?.value || (schema.kind as string); const isProfile = schema.namespace === "Profile" || schemaKind === PROFILE_KIND; const breadcrumbModelLabel = isProfile ? "All Profiles" : schema.label || schema.name; + const { count, permissions } = data ? data[schemaKind] : {}; + const currentPermission = permissions?.edges[0]?.node; + + if (currentPermission?.view !== "ALLOW") { + return null; + } return ( {

{breadcrumbModelLabel}

- {loading && !error ? "..." : data?.[schemaKind]?.count} + {loading && !error ? "..." : count} } diff --git a/frontend/app/src/screens/proposed-changes/items.tsx b/frontend/app/src/screens/proposed-changes/items.tsx index 623e3489b1..3c7149d862 100644 --- a/frontend/app/src/screens/proposed-changes/items.tsx +++ b/frontend/app/src/screens/proposed-changes/items.tsx @@ -59,6 +59,7 @@ export const ProposedChangesPage = () => { notifyOnNetworkStatusChange: true, } ); + const [deleteProposedChange, { loading: isDeleteLoading }] = useMutation(DELETE_PROPOSED_CHANGE); if (error) { diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts new file mode 100644 index 0000000000..fe92505f9c --- /dev/null +++ b/frontend/app/src/utils/permissions.ts @@ -0,0 +1,46 @@ +export type tPermission = { + view: string; + create: string; + update: string; + delete: string; +}; + +export type uiPermission = { + view: { + isAllowed: boolean; + message: string; + }; + create: { + isAllowed: boolean; + message: string; + }; + update: { + isAllowed: boolean; + message: string; + }; + delete: { + isAllowed: boolean; + message: string; + }; +}; + +export function getPermission(permission: tPermission): uiPermission { + return { + view: { + isAllowed: permission?.view === "ALLOW", + message: "You can't access this view.", + }, + create: { + isAllowed: permission?.create === "ALLOW", + message: "You can't create this object.", + }, + update: { + isAllowed: permission?.update === "ALLOW", + message: "You can't update this object.", + }, + delete: { + isAllowed: permission?.delete === "ALLOW", + message: "You can't delete this object.", + }, + }; +} From 5cc5d1b19e4fd448eec29fd536cd0915a4ca6db7 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Fri, 11 Oct 2024 14:46:44 +0200 Subject: [PATCH 03/40] update types --- .../app/src/components/form/object-create-form-trigger.tsx | 4 +++- frontend/app/src/utils/permissions.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/app/src/components/form/object-create-form-trigger.tsx b/frontend/app/src/components/form/object-create-form-trigger.tsx index 6a4704210b..ebb0843f62 100644 --- a/frontend/app/src/components/form/object-create-form-trigger.tsx +++ b/frontend/app/src/components/form/object-create-form-trigger.tsx @@ -3,6 +3,7 @@ import ObjectForm from "@/components/form/object-form"; import { ARTIFACT_OBJECT } from "@/config/constants"; import graphqlClient from "@/graphql/graphqlClientApollo"; import { IModelSchema } from "@/state/atoms/schema.atom"; +import { Permission } from "@/utils/permissions"; import { Icon } from "@iconify-icon/react"; import { useState } from "react"; import { Button, ButtonProps } from "../buttons/button-primitive"; @@ -11,6 +12,7 @@ import { Tooltip } from "../ui/tooltip"; interface ObjectCreateFormTriggerProps extends ButtonProps { schema: IModelSchema; onSuccess?: (newObject: any) => void; + permission: Permission; } export const ObjectCreateFormTrigger = ({ @@ -26,7 +28,7 @@ export const ObjectCreateFormTrigger = ({ return null; } - const isAllowed = permission.create.allow; + const isAllowed = permission.create.isAllowed; const tooltipMessage = permission.create.message; return ( diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts index fe92505f9c..8976147b64 100644 --- a/frontend/app/src/utils/permissions.ts +++ b/frontend/app/src/utils/permissions.ts @@ -1,11 +1,11 @@ -export type tPermission = { +export type PermissionProps = { view: string; create: string; update: string; delete: string; }; -export type uiPermission = { +export type Permission = { view: { isAllowed: boolean; message: string; @@ -24,7 +24,7 @@ export type uiPermission = { }; }; -export function getPermission(permission: tPermission): uiPermission { +export function getPermission(permission: PermissionProps): Permission { return { view: { isAllowed: permission?.view === "ALLOW", From 3e3d0692c1f359a3de1be426e5b432427ff38a30 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Fri, 11 Oct 2024 17:37:54 +0200 Subject: [PATCH 04/40] update mock data --- frontend/app/tests/mocks/data/devices.ts | 14 ++++++++ .../app/tests/mocks/data/graphqlQueries.ts | 26 ++++++++++++++ frontend/app/tests/mocks/data/permissions.ts | 15 ++++++++ frontend/app/tests/mocks/data/task.ts | 34 +++++++++++++------ frontend/app/tests/mocks/data/task_1.ts | 14 ++++++++ frontend/app/tests/mocks/data/task_2.ts | 14 ++++++++ frontend/app/tests/mocks/data/task_3.ts | 14 ++++++++ frontend/app/tests/mocks/data/task_4.ts | 14 ++++++++ frontend/app/tests/mocks/data/task_5.ts | 14 ++++++++ frontend/app/tests/mocks/data/task_6.ts | 14 ++++++++ frontend/app/tests/mocks/data/task_7.ts | 14 ++++++++ 11 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 frontend/app/tests/mocks/data/permissions.ts diff --git a/frontend/app/tests/mocks/data/devices.ts b/frontend/app/tests/mocks/data/devices.ts index f3ab414537..f3fd80cba9 100644 --- a/frontend/app/tests/mocks/data/devices.ts +++ b/frontend/app/tests/mocks/data/devices.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const deviceDetailsMocksId = "bd3110b9-5923-45e9-b643-776b8151c074"; export const deviceSiteMocksId = "06c3ab9e-535e-41af-bf4b-ec9134cc4353"; export const deviceSiteOwnerMocksId = "1790adb9-7030-259c-35c7-d8e28044d715"; @@ -1348,6 +1350,17 @@ query InfraDevice { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } InfrahubTask(related_node__ids: ["${deviceDetailsMocksId}"]) { count @@ -1564,6 +1577,7 @@ export const deviceDetailsMocksData = { __typename: "EdgedInfraDevice", }, ], + permissions: permissionsAllow, __typename: "PaginatedInfraDevice", }, }; diff --git a/frontend/app/tests/mocks/data/graphqlQueries.ts b/frontend/app/tests/mocks/data/graphqlQueries.ts index 3d29b9ad5f..0d8d94457e 100644 --- a/frontend/app/tests/mocks/data/graphqlQueries.ts +++ b/frontend/app/tests/mocks/data/graphqlQueries.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const graphqlQueriesMocksQuery = ` query CoreGraphQLQuery($offset: Int, $limit: Int) { CoreGraphQLQuery(offset: $offset,limit: $limit) { @@ -29,6 +31,17 @@ query CoreGraphQLQuery($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -64,6 +77,17 @@ query CoreGraphQLQuery($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -603,6 +627,7 @@ export const graphqlQueriesMocksData = { __typename: "EdgedGraphQLQuery", }, ], + permissions: permissionsAllow, __typename: "PaginatedGraphQLQuery", }, }; @@ -1091,6 +1116,7 @@ export const graphqlQueriesMocksDataDeleted = { __typename: "EdgedGraphQLQuery", }, ], + permissions: permissionsAllow, __typename: "PaginatedGraphQLQuery", }, }; diff --git a/frontend/app/tests/mocks/data/permissions.ts b/frontend/app/tests/mocks/data/permissions.ts new file mode 100644 index 0000000000..f4a2bc33f6 --- /dev/null +++ b/frontend/app/tests/mocks/data/permissions.ts @@ -0,0 +1,15 @@ +export const permissionsAllow = { + edges: [ + { + node: { + kind: "InfraDevice", + view: "ALLOW", + create: "ALLOW", + update: "ALLOW", + delete: "ALLOW", + __typename: "ObjectPermission", + }, + __typename: "ObjectPermissionNode", + }, + ], +}; diff --git a/frontend/app/tests/mocks/data/task.ts b/frontend/app/tests/mocks/data/task.ts index 0f7a092a62..80468d28b7 100644 --- a/frontend/app/tests/mocks/data/task.ts +++ b/frontend/app/tests/mocks/data/task.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const taskMocksSchema = [ { id: "8a4e2579-c300-48e1-b703-022bf6d224df", @@ -145,17 +147,26 @@ query TestTask { id display_label __typename - - name { - value - } - description { - value - } - completed { - value - } - + name { + value + } + description { + value + } + completed { + value + } + } + } + permissions { + edges { + node { + kind + view + create + update + delete + } } } } @@ -178,6 +189,7 @@ export const taskMocksData = { __typename: "EdgedTestTask", }, ], + permissions: permissionsAllow, __typename: "PaginatedTestTask", }, }; diff --git a/frontend/app/tests/mocks/data/task_1.ts b/frontend/app/tests/mocks/data/task_1.ts index ac59a2e61c..d0d501be3f 100644 --- a/frontend/app/tests/mocks/data/task_1.ts +++ b/frontend/app/tests/mocks/data/task_1.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const taskMocksSchema = [ { id: "8a4e2579-c300-48e1-b703-022bf6d224df", @@ -131,6 +133,17 @@ query TestTask($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -139,6 +152,7 @@ export const taskMocksData = { TestTask: { count: 0, edges: [], + permissions: permissionsAllow, __typename: "PaginatedTestTask", }, }; diff --git a/frontend/app/tests/mocks/data/task_2.ts b/frontend/app/tests/mocks/data/task_2.ts index 9505c038a8..072375a821 100644 --- a/frontend/app/tests/mocks/data/task_2.ts +++ b/frontend/app/tests/mocks/data/task_2.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const taskMocksSchema = [ { id: "8a4e2579-c300-48e1-b703-022bf6d224df", @@ -128,6 +130,17 @@ query TestTask($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -136,6 +149,7 @@ export const taskMocksData = { TestTask: { count: 0, edges: [], + permissions: permissionsAllow, __typename: "PaginatedTestTask", }, }; diff --git a/frontend/app/tests/mocks/data/task_3.ts b/frontend/app/tests/mocks/data/task_3.ts index 4e5ec31285..ebea9ac185 100644 --- a/frontend/app/tests/mocks/data/task_3.ts +++ b/frontend/app/tests/mocks/data/task_3.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const taskMocksSchema = [ { id: "8a4e2579-c300-48e1-b703-022bf6d224df", @@ -128,6 +130,17 @@ query TestTask($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -136,6 +149,7 @@ export const taskMocksData = { TestTask: { count: 0, edges: [], + permissions: permissionsAllow, __typename: "PaginatedTestTask", }, }; diff --git a/frontend/app/tests/mocks/data/task_4.ts b/frontend/app/tests/mocks/data/task_4.ts index cccc684268..2e24344119 100644 --- a/frontend/app/tests/mocks/data/task_4.ts +++ b/frontend/app/tests/mocks/data/task_4.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const taskMocksSchema = [ { id: "8a4e2579-c300-48e1-b703-022bf6d224df", @@ -50,6 +52,17 @@ query TestTask($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -58,6 +71,7 @@ export const taskMocksData = { TestTask: { count: 0, edges: [], + permissions: permissionsAllow, __typename: "PaginatedTestTask", }, }; diff --git a/frontend/app/tests/mocks/data/task_5.ts b/frontend/app/tests/mocks/data/task_5.ts index 58035d389c..56486cd0f0 100644 --- a/frontend/app/tests/mocks/data/task_5.ts +++ b/frontend/app/tests/mocks/data/task_5.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const taskMocksSchema = [ { id: "8a4e2579-c300-48e1-b703-022bf6d224df", @@ -45,6 +47,17 @@ query TestTask($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -53,6 +66,7 @@ export const taskMocksData = { TestTask: { count: 0, edges: [], + permissions: permissionsAllow, __typename: "PaginatedTestTask", }, }; diff --git a/frontend/app/tests/mocks/data/task_6.ts b/frontend/app/tests/mocks/data/task_6.ts index 27cbcd1986..ba6d9b50a3 100644 --- a/frontend/app/tests/mocks/data/task_6.ts +++ b/frontend/app/tests/mocks/data/task_6.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const taskMocksSchema = [ { id: "8a4e2579-c300-48e1-b703-022bf6d224df", @@ -45,6 +47,17 @@ query TestTask($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -53,6 +66,7 @@ export const taskMocksData = { TestTask: { count: 0, edges: [], + permissions: permissionsAllow, __typename: "PaginatedTestTask", }, }; diff --git a/frontend/app/tests/mocks/data/task_7.ts b/frontend/app/tests/mocks/data/task_7.ts index 9cb3efb261..e4e2a0dbb6 100644 --- a/frontend/app/tests/mocks/data/task_7.ts +++ b/frontend/app/tests/mocks/data/task_7.ts @@ -1,3 +1,5 @@ +import { permissionsAllow } from "./permissions"; + export const taskMocksSchema = [ { id: "8a4e2579-c300-48e1-b703-022bf6d224df", @@ -46,6 +48,17 @@ query TestTask($offset: Int, $limit: Int) { } } } + permissions { + edges { + node { + kind + view + create + update + delete + } + } + } } } `; @@ -54,6 +67,7 @@ export const taskMocksData = { TestTask: { count: 0, edges: [], + permissions: permissionsAllow, __typename: "PaginatedTestTask", }, }; From c73f6159f32b4648395c081d002558561757f8f7 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sat, 12 Oct 2024 17:33:21 +0200 Subject: [PATCH 05/40] fix loading state for items --- .../app/src/screens/object-items/object-items-paginated.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/src/screens/object-items/object-items-paginated.tsx b/frontend/app/src/screens/object-items/object-items-paginated.tsx index 3366878ca7..830f495d96 100644 --- a/frontend/app/src/screens/object-items/object-items-paginated.tsx +++ b/frontend/app/src/screens/object-items/object-items-paginated.tsx @@ -94,7 +94,7 @@ export default function ObjectItems({ const debouncedHandleSearch = debounce(handleSearch, 500); - if (!permission.view.isAllowed) { + if (!loading && !permission.view.isAllowed) { return ; } From be762a093e0cb73e7f21ea7a7b606d67fcacbdfd Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sat, 12 Oct 2024 17:38:02 +0200 Subject: [PATCH 06/40] update test for role-management (will be improved) --- .../src/screens/role-management/accounts.tsx | 9 +++++-- .../e2e/permissions/accounts-object.spec.ts | 27 ------------------- .../e2e/permissions/role-management.spec.ts | 11 ++++++++ 3 files changed, 18 insertions(+), 29 deletions(-) delete mode 100644 frontend/app/tests/e2e/permissions/accounts-object.spec.ts create mode 100644 frontend/app/tests/e2e/permissions/role-management.spec.ts diff --git a/frontend/app/src/screens/role-management/accounts.tsx b/frontend/app/src/screens/role-management/accounts.tsx index f31e53d9cf..4ca5624507 100644 --- a/frontend/app/src/screens/role-management/accounts.tsx +++ b/frontend/app/src/screens/role-management/accounts.tsx @@ -4,7 +4,7 @@ import { Pill } from "@/components/display/pill"; import SlideOver, { SlideOverTitle } from "@/components/display/slide-over"; import ObjectForm from "@/components/form/object-form"; import ModalDeleteObject from "@/components/modals/modal-delete-object"; -import { Table, tRow, tRowValue } from "@/components/table/table"; +import { Table, tRowValue } from "@/components/table/table"; import { Pagination } from "@/components/ui/pagination"; import { ACCOUNT_GENERIC_OBJECT, ACCOUNT_OBJECT } from "@/config/constants"; import graphqlClient from "@/graphql/graphqlClientApollo"; @@ -97,7 +97,12 @@ function Accounts() {
{/* Search input + filter button */}
-
diff --git a/frontend/app/tests/e2e/permissions/accounts-object.spec.ts b/frontend/app/tests/e2e/permissions/accounts-object.spec.ts deleted file mode 100644 index de0343bb7a..0000000000 --- a/frontend/app/tests/e2e/permissions/accounts-object.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expect, test } from "@playwright/test"; -import { ACCOUNT_STATE_PATH } from "../../constants"; - -test.describe("/objects/CoreGenericAccount - Admin permissions", () => { - test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); - - test("should be allowed to add accounts", async ({ page }) => { - await page.goto("/objects/CoreGenericAccount"); - await expect(page.getByTestId("create-object-button")).toBeEnabled(); - }); -}); - -test.describe("/objects/CoreGenericAccount - Read write permissions", () => { - test.use({ storageState: ACCOUNT_STATE_PATH.READ_WRITE }); - test("should not be allowed to add accounts", async ({ page }) => { - await page.goto("/objects/CoreGenericAccount"); - await expect(page.getByTestId("create-object-button")).not.toBeEnabled(); - }); -}); - -test.describe("/objects/CoreGenericAccount - Read only permissions", () => { - test.use({ storageState: ACCOUNT_STATE_PATH.READ_ONLY }); - test("should not be allowed to add accounts", async ({ page }) => { - await page.goto("/objects/CoreGenericAccount"); - await expect(page.getByTestId("create-object-button")).not.toBeEnabled(); - }); -}); diff --git a/frontend/app/tests/e2e/permissions/role-management.spec.ts b/frontend/app/tests/e2e/permissions/role-management.spec.ts new file mode 100644 index 0000000000..12fe2b9497 --- /dev/null +++ b/frontend/app/tests/e2e/permissions/role-management.spec.ts @@ -0,0 +1,11 @@ +import { expect, test } from "@playwright/test"; +import { ACCOUNT_STATE_PATH } from "../../constants"; + +test.describe("Role Management - Admin", () => { + test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); + + test("should be allowed to add accounts", async ({ page }) => { + await page.goto("/role-management"); + await expect(page.getByTestId("create-object-button")).toBeEnabled(); + }); +}); From fce0d1b1af4a993eecf2d56df78cfb5fdb0f360b Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sat, 12 Oct 2024 18:07:50 +0200 Subject: [PATCH 07/40] skip anonymous test for now --- frontend/app/tests/e2e/objects/object-details.spec.ts | 2 +- frontend/app/tests/e2e/objects/object-list.spec.ts | 2 +- frontend/app/tests/e2e/objects/object-relationships.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/tests/e2e/objects/object-details.spec.ts b/frontend/app/tests/e2e/objects/object-details.spec.ts index fbd08decb4..1175cc8368 100644 --- a/frontend/app/tests/e2e/objects/object-details.spec.ts +++ b/frontend/app/tests/e2e/objects/object-details.spec.ts @@ -11,7 +11,7 @@ test.describe("/objects/:objectKind/:objectid", () => { }); test.describe("when not logged in", () => { - test("should not be able to edit object", async ({ page }) => { + test.skip("should not be able to edit object", async ({ page }) => { await page.goto("/objects/InfraBGPSession"); await expect(page.getByText("Just a moment")).not.toBeVisible(); await page.getByRole("cell", { name: "EXTERNAL" }).first().click(); diff --git a/frontend/app/tests/e2e/objects/object-list.spec.ts b/frontend/app/tests/e2e/objects/object-list.spec.ts index 104b247046..860cd545ec 100644 --- a/frontend/app/tests/e2e/objects/object-list.spec.ts +++ b/frontend/app/tests/e2e/objects/object-list.spec.ts @@ -33,7 +33,7 @@ test.describe("/objects/:objectKind", () => { }); test.describe("when not logged in", () => { - test("should not be able to create a new object", async ({ page }) => { + test.skip("should not be able to create a new object", async ({ page }) => { await page.goto("/objects/BuiltinTag"); await expect(page.getByRole("heading", { name: "Tag" })).toBeVisible(); diff --git a/frontend/app/tests/e2e/objects/object-relationships.spec.ts b/frontend/app/tests/e2e/objects/object-relationships.spec.ts index e5889f6e18..2155a647b3 100644 --- a/frontend/app/tests/e2e/objects/object-relationships.spec.ts +++ b/frontend/app/tests/e2e/objects/object-relationships.spec.ts @@ -15,7 +15,7 @@ test.describe("/objects/:objectKind/:objectid - relationship tab", () => { }); test.describe("when not logged in", () => { - test("should not be able to edit relationship", async ({ page }) => { + test.skip("should not be able to edit relationship", async ({ page }) => { await test.step("Navigate to relationship tab of an object", async () => { await page.goto("/objects/InfraPlatform"); await page.getByRole("link", { name: "Cisco IOS", exact: true }).click(); From 7c6a8268469cd9904dc04157cc4e2d58ead6fd43 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sat, 12 Oct 2024 18:44:39 +0200 Subject: [PATCH 08/40] use admin auth for artifacts --- frontend/app/tests/e2e/objects/artifact.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/tests/e2e/objects/artifact.spec.ts b/frontend/app/tests/e2e/objects/artifact.spec.ts index 9cb6f1baa7..e98f385a8a 100644 --- a/frontend/app/tests/e2e/objects/artifact.spec.ts +++ b/frontend/app/tests/e2e/objects/artifact.spec.ts @@ -2,6 +2,8 @@ import { expect, test } from "@playwright/test"; import { ACCOUNT_STATE_PATH } from "../../constants"; test.describe("/objects/CoreArtifact - Artifact page", () => { + test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); + test.describe.configure({ mode: "serial" }); test.beforeEach(async function ({ page }) { @@ -30,8 +32,6 @@ test.describe("/objects/CoreArtifact - Artifact page", () => { }); test.describe("when logged in", async () => { - test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); - test("should not be able to create a new artifact", async ({ page }) => { await page.goto("/objects/CoreArtifact"); await expect(page.getByRole("heading", { name: "Artifact" })).toBeVisible(); From 49edb005d855bc4048ba908d1b2f25076ef6416e Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sat, 12 Oct 2024 20:08:07 +0200 Subject: [PATCH 09/40] fix artifacts details view with object permission --- .../src/screens/artifacts/object-item-details-paginated.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx index 311f2bbe74..390d98b5b6 100644 --- a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx +++ b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx @@ -32,6 +32,7 @@ import { getSchemaObjectColumns, getTabs, } from "@/utils/getSchemaObjectColumns"; +import { getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { LockClosedIcon, RectangleGroupIcon } from "@heroicons/react/24/outline"; import { Icon } from "@iconify-icon/react"; @@ -105,6 +106,10 @@ export default function ArtifactsDetails() { const objectDetailsData = data[schemaData.kind]?.edges[0]?.node; + const permission = getPermission( + schemaData.kind && data && data[schemaData?.kind]?.permissions?.edges[0]?.node + ); + const tabs = [ { label: schemaData?.label, @@ -150,6 +155,7 @@ export default function ArtifactsDetails() { From 93223fb8070bbad891a776da287a5d144a4ec90c Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sun, 13 Oct 2024 09:52:12 +0200 Subject: [PATCH 10/40] fix filter test --- frontend/app/tests/e2e/objects/object-filters.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/app/tests/e2e/objects/object-filters.spec.ts b/frontend/app/tests/e2e/objects/object-filters.spec.ts index fddc0669c5..8c4eb74dfa 100644 --- a/frontend/app/tests/e2e/objects/object-filters.spec.ts +++ b/frontend/app/tests/e2e/objects/object-filters.spec.ts @@ -1,6 +1,9 @@ import { expect, test } from "@playwright/test"; +import { ACCOUNT_STATE_PATH } from "../../constants"; test.describe("Object filters", () => { + test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); + test.beforeEach(async function ({ page }) { page.on("response", async (response) => { if (response.status() === 500) { @@ -12,6 +15,7 @@ test.describe("Object filters", () => { test("should filter the objects list", async ({ page }) => { await test.step("access objects list and verify initial state", async () => { await page.goto("/objects/InfraDevice"); + await expect(page.getByText("Just a moment")).not.toBeVisible(); await expect(page.getByTestId("object-items")).toContainText("Filters: 0"); await expect(page.getByTestId("object-items")).toContainText("Showing 1 to 10 of 30 results"); }); @@ -94,6 +98,7 @@ test.describe("Object filters", () => { test("should correctly filter from a kind", async ({ page }) => { await page.goto("/objects/InfraInterface"); + await expect(page.getByText("Just a moment")).not.toBeVisible(); await page.getByTestId("apply-filters").click(); await test.step("profiles selector should not be visible", async () => { From e10ec9d19b3a731a196d28899f9ea7ae44cc013a Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sun, 13 Oct 2024 09:54:54 +0200 Subject: [PATCH 11/40] add kind for filter in use object items for permission --- frontend/app/src/hooks/useObjectItems.ts | 8 ++++++-- .../src/screens/object-items/object-items-paginated.tsx | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/app/src/hooks/useObjectItems.ts b/frontend/app/src/hooks/useObjectItems.ts index 662e99816c..6431410121 100644 --- a/frontend/app/src/hooks/useObjectItems.ts +++ b/frontend/app/src/hooks/useObjectItems.ts @@ -59,7 +59,11 @@ const getQuery = (schema?: IModelSchema, filters?: Array) => { }); }; -export const useObjectItems = (schema?: IModelSchema, filters?: Array) => { +export const useObjectItems = ( + schema?: IModelSchema, + filters?: Array, + kindFilter?: string +) => { const query = gql` ${getQuery(schema, filters)} `; @@ -69,7 +73,7 @@ export const useObjectItems = (schema?: IModelSchema, filters?: Array) = const permission = getPermission( schema?.kind && apolloQuery?.data && - apolloQuery?.data[schema?.kind]?.permissions?.edges[0]?.node + apolloQuery?.data[kindFilter || schema?.kind]?.permissions?.edges[0]?.node ); return { diff --git a/frontend/app/src/screens/object-items/object-items-paginated.tsx b/frontend/app/src/screens/object-items/object-items-paginated.tsx index 830f495d96..3cc3670f55 100644 --- a/frontend/app/src/screens/object-items/object-items-paginated.tsx +++ b/frontend/app/src/screens/object-items/object-items-paginated.tsx @@ -57,7 +57,13 @@ export default function ObjectItems({ // Get all the needed columns (attributes + relationships) const columns = getSchemaObjectColumns({ schema: schema, forListView: true }); - const { loading, error, data = {}, refetch, permission } = useObjectItems(schema, filters); + const { + loading, + error, + data = {}, + refetch, + permission, + } = useObjectItems(schema, filters, kindFilter?.value); const result = data && schema?.kind ? (data[kindFilter?.value || schema?.kind] ?? {}) : {}; From 4b7c939723eb33b5914c881d3f732657a66ce329 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sun, 13 Oct 2024 09:56:39 +0200 Subject: [PATCH 12/40] fix auth for object list --- .../app/tests/e2e/objects/object-list.spec.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/frontend/app/tests/e2e/objects/object-list.spec.ts b/frontend/app/tests/e2e/objects/object-list.spec.ts index 860cd545ec..20936a74ad 100644 --- a/frontend/app/tests/e2e/objects/object-list.spec.ts +++ b/frontend/app/tests/e2e/objects/object-list.spec.ts @@ -10,28 +10,6 @@ test.describe("/objects/:objectKind", () => { }); }); - test("should display 'kind' column on when the object is a generic", async ({ page }) => { - await page.goto("/objects/CoreGroup"); - await expect(page.locator("thead")).toContainText("Kind"); - }); - - test("should display default column when a relationship schema has no attributes/relationship", async ({ - page, - }) => { - await page.goto("/objects/CoreStandardGroup"); - await page.getByTestId("object-items").getByRole("link", { name: "arista_devices" }).click(); - await page.getByText("Members").click(); - await expect(page.getByRole("columnheader", { name: "Type" })).toBeVisible(); - await expect(page.getByRole("columnheader", { name: "Name" })).toBeVisible(); - }); - - test("clicking on a relationship value redirects to its details page", async ({ page }) => { - await page.goto("/objects/InfraDevice"); - await page.getByRole("link", { name: "Juniper JunOS" }).first().click(); - await expect(page.getByText("NameJuniper JunOS")).toBeVisible(); - expect(page.url()).toContain("/objects/InfraPlatform/"); - }); - test.describe("when not logged in", () => { test.skip("should not be able to create a new object", async ({ page }) => { await page.goto("/objects/BuiltinTag"); @@ -44,7 +22,7 @@ test.describe("/objects/:objectKind", () => { await expect(page.getByRole("row", { name: "blue" }).getByRole("button")).toBeDisabled(); }); - test("should be able to open object details in a new tab", async ({ page, context }) => { + test.skip("should be able to open object details in a new tab", async ({ page, context }) => { await page.goto("/objects/BuiltinTag"); // When @@ -63,6 +41,28 @@ test.describe("/objects/:objectKind", () => { test.describe("when logged in as Admin", () => { test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); + test("should display 'kind' column on when the object is a generic", async ({ page }) => { + await page.goto("/objects/CoreGroup"); + await expect(page.locator("thead")).toContainText("Kind"); + }); + + test("should display default column when a relationship schema has no attributes/relationship", async ({ + page, + }) => { + await page.goto("/objects/CoreStandardGroup"); + await page.getByTestId("object-items").getByRole("link", { name: "arista_devices" }).click(); + await page.getByText("Members").click(); + await expect(page.getByRole("columnheader", { name: "Type" })).toBeVisible(); + await expect(page.getByRole("columnheader", { name: "Name" })).toBeVisible(); + }); + + test("clicking on a relationship value redirects to its details page", async ({ page }) => { + await page.goto("/objects/InfraDevice"); + await page.getByRole("link", { name: "Juniper JunOS" }).first().click(); + await expect(page.getByText("NameJuniper JunOS")).toBeVisible(); + expect(page.url()).toContain("/objects/InfraPlatform/"); + }); + test("should be able to create a new object", async ({ page }) => { await page.goto("/objects/BuiltinTag"); From 171720c0cd53d67333ae94c58450d9eec19d254e Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sun, 13 Oct 2024 09:57:19 +0200 Subject: [PATCH 13/40] fix auth --- frontend/app/tests/e2e/objects/object-list-search.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app/tests/e2e/objects/object-list-search.spec.ts b/frontend/app/tests/e2e/objects/object-list-search.spec.ts index aa01215b39..289ef73a29 100644 --- a/frontend/app/tests/e2e/objects/object-list-search.spec.ts +++ b/frontend/app/tests/e2e/objects/object-list-search.spec.ts @@ -1,9 +1,12 @@ import { expect, test } from "@playwright/test"; +import { ACCOUNT_STATE_PATH } from "../../constants"; const OBJECT_NAME = "atl1-core1"; const SEARCH = "atl"; test.describe("Object list search", async () => { + test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); + test.beforeEach(async function ({ page }) { page.on("response", async (response) => { if (response.status() === 500) { From 8b139b6398af0d7b44177ba6d6eb1e5e5358a274 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Sun, 13 Oct 2024 10:19:55 +0200 Subject: [PATCH 14/40] skip anonymous profile test --- frontend/app/tests/e2e/profile/profile.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/tests/e2e/profile/profile.spec.ts b/frontend/app/tests/e2e/profile/profile.spec.ts index 65756b541e..30ad7d30cd 100644 --- a/frontend/app/tests/e2e/profile/profile.spec.ts +++ b/frontend/app/tests/e2e/profile/profile.spec.ts @@ -11,7 +11,7 @@ test.describe("/profile", () => { }); test.describe("when not logged in", () => { - test("should see 'Login' and no user avatar on header", async ({ page }) => { + test.skip("should see 'Login' and no user avatar on header", async ({ page }) => { await page.goto("/"); await expect(page.getByTestId("unauthenticated-menu-trigger")).toBeVisible(); From fec8db102cadca73028d0358045a36cdb7555fc5 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Mon, 14 Oct 2024 07:55:27 +0200 Subject: [PATCH 15/40] skip profiles --- .../e2e/objects/profiles/profiles.spec.ts | 24 ++++++++++--------- .../app/tests/e2e/profile/profile.spec.ts | 6 ++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/frontend/app/tests/e2e/objects/profiles/profiles.spec.ts b/frontend/app/tests/e2e/objects/profiles/profiles.spec.ts index 0471f9ccaf..7d9e23964f 100644 --- a/frontend/app/tests/e2e/objects/profiles/profiles.spec.ts +++ b/frontend/app/tests/e2e/objects/profiles/profiles.spec.ts @@ -16,7 +16,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test("should create a new profile successfully", async ({ page }) => { + test.skip("should create a new profile successfully", async ({ page }) => { await test.step("Navigate to CoreProfile page", async () => { await page.goto("/objects/CoreProfile"); await expect(page.getByRole("heading")).toContainText("Profile"); @@ -39,7 +39,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test("access the created profile, view its data, and edit it", async ({ page }) => { + test.skip("access the created profile, view its data, and edit it", async ({ page }) => { await test.step("Navigate to CoreProfile page", async () => { await page.goto("/objects/CoreProfile"); await expect(page.getByRole("heading")).toContainText("Profile"); @@ -56,7 +56,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test("create an object with a profile", async ({ page }) => { + test.skip("create an object with a profile", async ({ page }) => { await test.step("Navigate to object creation page", async () => { await page.goto("/objects/BuiltinTag"); await page.getByTestId("create-object-button").click(); @@ -106,7 +106,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test("edit a used profile and verify the changes reflect in an object using it", async ({ + test.skip("edit a used profile and verify the changes reflect in an object using it", async ({ page, }) => { await test.step("Navigate to an used profile", async () => { @@ -129,7 +129,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test("edit profile of tag without touching any other field", async ({ page }) => { + test.skip("edit profile of tag without touching any other field", async ({ page }) => { await test.step("got to edit form of tag", async () => { await page.goto("/objects/BuiltinTag"); await page.getByRole("link", { name: "tag with profile" }).click(); @@ -146,7 +146,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { await expect(page.getByText("Description-")).toBeVisible(); }); - test("delete the profile and reset object attribute value", async ({ page }) => { + test.skip("delete the profile and reset object attribute value", async ({ page }) => { await test.step("Navigate to CoreProfile page", async () => { await page.goto("/objects/CoreProfile"); }); @@ -188,7 +188,9 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi }); }); - test("should verify the form fields for a new profile for interface L2", async ({ page }) => { + test.skip("should verify the form fields for a new profile for interface L2", async ({ + page, + }) => { await test.step("access Interface L2 form", async () => { await page.goto("/objects/CoreProfile"); @@ -222,7 +224,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi }); }); - test("should create a new profile successfully for interface L2", async ({ page }) => { + test.skip("should create a new profile successfully for interface L2", async ({ page }) => { await test.step("access Interface L2 form", async () => { await page.goto("/objects/CoreProfile"); await page.getByTestId("create-object-button").click(); @@ -242,7 +244,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi }); }); - test("should create a new profile successfully for generic interface", async ({ page }) => { + test.skip("should create a new profile successfully for generic interface", async ({ page }) => { await test.step("access Interface form", async () => { await page.goto("/objects/CoreProfile"); await page.getByTestId("create-object-button").click(); @@ -260,7 +262,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi }); }); - test("should verify profile values after creation", async ({ page }) => { + test.skip("should verify profile values after creation", async ({ page }) => { await page.goto("/objects/CoreProfile"); await page.getByRole("link", { name: PROFILE_NAME }).click(); await expect(page.locator("dl").getByText(PROFILE_NAME)).toBeVisible(); @@ -276,7 +278,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi await expect(page.getByText("Provisioning")).toBeVisible(); }); - test("should verify the available profiles in the object form", async ({ page }) => { + test.skip("should verify the available profiles in the object form", async ({ page }) => { await page.goto("/objects/InfraInterface"); await page.getByTestId("create-object-button").click(); await page.getByLabel("Select an object type").click(); diff --git a/frontend/app/tests/e2e/profile/profile.spec.ts b/frontend/app/tests/e2e/profile/profile.spec.ts index 30ad7d30cd..09ab13f48e 100644 --- a/frontend/app/tests/e2e/profile/profile.spec.ts +++ b/frontend/app/tests/e2e/profile/profile.spec.ts @@ -22,7 +22,7 @@ test.describe("/profile", () => { test.describe("when logged in as admin account", () => { test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); - test("should access the profile page", async ({ page }) => { + test.skip("should access the profile page", async ({ page }) => { await test.step("go to profile page", async () => { await page.goto("/"); await page.getByTestId("authenticated-menu-trigger").click(); @@ -41,7 +41,7 @@ test.describe("/profile", () => { test.describe("when logged in as read-write account", () => { test.use({ storageState: ACCOUNT_STATE_PATH.READ_WRITE }); - test("should access the profile page", async ({ page }) => { + test.skip("should access the profile page", async ({ page }) => { await test.step("go to profile page", async () => { await page.goto("/"); await page.getByTestId("authenticated-menu-trigger").click(); @@ -61,7 +61,7 @@ test.describe("/profile", () => { test.describe("when logged in as read-only account", () => { test.use({ storageState: ACCOUNT_STATE_PATH.READ_ONLY }); - test("should access the profile page", async ({ page }) => { + test.skip("should access the profile page", async ({ page }) => { await test.step("go to profile page", async () => { await page.goto("/"); await page.getByTestId("authenticated-menu-trigger").click(); From 6383f745c7ec4b16b8765141102315940184ca3e Mon Sep 17 00:00:00 2001 From: pa-lem Date: Mon, 14 Oct 2024 08:03:32 +0200 Subject: [PATCH 16/40] use auth in hierarchical view test --- frontend/app/tests/e2e/objects/object-hierarchical.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app/tests/e2e/objects/object-hierarchical.spec.ts b/frontend/app/tests/e2e/objects/object-hierarchical.spec.ts index e7a482e59d..7ec0098ea6 100644 --- a/frontend/app/tests/e2e/objects/object-hierarchical.spec.ts +++ b/frontend/app/tests/e2e/objects/object-hierarchical.spec.ts @@ -1,6 +1,9 @@ import { expect, test } from "@playwright/test"; +import { ACCOUNT_STATE_PATH } from "../../constants"; test.describe("Object hierarchical view", () => { + test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); + test("should display correctly", async ({ page }) => { await test.step("view tree and list for a hierarchical model", async () => { await page.goto("/objects/LocationGeneric"); From 677467bbf1b61c641325edc2521122c7f9e904b0 Mon Sep 17 00:00:00 2001 From: bilalabbad Date: Mon, 14 Oct 2024 10:13:20 +0200 Subject: [PATCH 17/40] fix search e2e --- frontend/app/tests/e2e/search.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app/tests/e2e/search.spec.ts b/frontend/app/tests/e2e/search.spec.ts index b4edc2056b..ed6b47c2b5 100644 --- a/frontend/app/tests/e2e/search.spec.ts +++ b/frontend/app/tests/e2e/search.spec.ts @@ -1,6 +1,9 @@ import { expect, test } from "@playwright/test"; +import { ACCOUNT_STATE_PATH } from "../constants"; test.describe("when searching an object", () => { + test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); + test.beforeEach(async function ({ page }) { page.on("response", async (response) => { if (response.status() === 500) { From 1cd5acda265c4061d4c730beee6ceb54c5f8e596 Mon Sep 17 00:00:00 2001 From: bilalabbad Date: Mon, 14 Oct 2024 10:43:38 +0200 Subject: [PATCH 18/40] fix token.spec.ts --- frontend/app/src/hooks/useObjectItems.ts | 15 ++++-- .../object-item-details-paginated.tsx | 2 + .../src/screens/user-profile/tab-profile.tsx | 9 +++- frontend/app/src/utils/permissions.ts | 46 ++++++++++++------- 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/frontend/app/src/hooks/useObjectItems.ts b/frontend/app/src/hooks/useObjectItems.ts index 6431410121..734246253c 100644 --- a/frontend/app/src/hooks/useObjectItems.ts +++ b/frontend/app/src/hooks/useObjectItems.ts @@ -5,7 +5,7 @@ import { Filter } from "@/hooks/useFilters"; import useQuery from "@/hooks/useQuery"; import { IModelSchema, genericsState, profilesAtom, schemaState } from "@/state/atoms/schema.atom"; import { getObjectAttributes, getObjectRelationships } from "@/utils/getSchemaObjectColumns"; -import { getPermission } from "@/utils/permissions"; +import { PERMISSION_ALLOW, getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { useAtomValue } from "jotai"; @@ -70,12 +70,17 @@ export const useObjectItems = ( const apolloQuery = useQuery(query, { notifyOnNetworkStatusChange: true, skip: !schema }); - const permission = getPermission( - schema?.kind && - apolloQuery?.data && - apolloQuery?.data[kindFilter || schema?.kind]?.permissions?.edges[0]?.node + const currentKind = kindFilter || schema?.kind; + const hasPermission = !!( + currentKind && + apolloQuery?.data && + apolloQuery?.data[currentKind]?.permissions ); + const permission = hasPermission + ? getPermission(apolloQuery?.data[currentKind].permissions) + : PERMISSION_ALLOW; + return { ...apolloQuery, permission, diff --git a/frontend/app/src/screens/object-item-details/object-item-details-paginated.tsx b/frontend/app/src/screens/object-item-details/object-item-details-paginated.tsx index fc6dbcf9d7..5469406cf8 100644 --- a/frontend/app/src/screens/object-item-details/object-item-details-paginated.tsx +++ b/frontend/app/src/screens/object-item-details/object-item-details-paginated.tsx @@ -23,6 +23,7 @@ import { getObjectTabs, getTabs, } from "@/utils/getSchemaObjectColumns"; +import { Permission } from "@/utils/permissions"; import { LockClosedIcon } from "@heroicons/react/24/outline"; import { Icon } from "@iconify-icon/react"; import { useAtom } from "jotai"; @@ -40,6 +41,7 @@ type ObjectDetailsProps = { objectDetailsData: any; taskData?: Object; hideHeaders?: boolean; + permission: Permission; }; export default function ObjectItemDetails({ diff --git a/frontend/app/src/screens/user-profile/tab-profile.tsx b/frontend/app/src/screens/user-profile/tab-profile.tsx index 9806d33936..48329313fd 100644 --- a/frontend/app/src/screens/user-profile/tab-profile.tsx +++ b/frontend/app/src/screens/user-profile/tab-profile.tsx @@ -20,7 +20,7 @@ export default function TabProfile() { const tokenData = parseJwt(localToken); const accountId = tokenData?.sub; - const { data, error, networkStatus } = useObjectDetails(schema, accountId); + const { data, error, networkStatus, permission } = useObjectDetails(schema, accountId); const objectDetailsData = schema && data && data[schema.kind!]?.edges[0]?.node; @@ -43,7 +43,12 @@ export default function TabProfile() { return ( - + ); diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts index 8976147b64..404618ab6b 100644 --- a/frontend/app/src/utils/permissions.ts +++ b/frontend/app/src/utils/permissions.ts @@ -1,3 +1,5 @@ +import { IModelSchema } from "@/state/atoms/schema.atom"; + export type PermissionProps = { view: string; create: string; @@ -5,23 +7,20 @@ export type PermissionProps = { delete: string; }; +export type PermissionAction = + | { + isAllowed: true; + } + | { + isAllowed: false; + message: string; + }; + export type Permission = { - view: { - isAllowed: boolean; - message: string; - }; - create: { - isAllowed: boolean; - message: string; - }; - update: { - isAllowed: boolean; - message: string; - }; - delete: { - isAllowed: boolean; - message: string; - }; + view: PermissionAction; + create: PermissionAction; + update: PermissionAction; + delete: PermissionAction; }; export function getPermission(permission: PermissionProps): Permission { @@ -44,3 +43,18 @@ export function getPermission(permission: PermissionProps): Permission { }, }; } + +export const PERMISSION_ALLOW: Permission = { + view: { + isAllowed: true, + }, + create: { + isAllowed: true, + }, + update: { + isAllowed: true, + }, + delete: { + isAllowed: true, + }, +}; From c56c0da971fe756b1931a00d3992432f691614c8 Mon Sep 17 00:00:00 2001 From: bilalabbad Date: Mon, 14 Oct 2024 11:09:25 +0200 Subject: [PATCH 19/40] fix profile --- frontend/app/src/hooks/useObjectDetails.ts | 13 ++++++------ frontend/app/src/hooks/useObjectItems.ts | 10 ++++++--- frontend/app/src/utils/permissions.ts | 21 ++++++++++++++----- .../app/tests/e2e/profile/profile.spec.ts | 8 +++---- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/frontend/app/src/hooks/useObjectDetails.ts b/frontend/app/src/hooks/useObjectDetails.ts index 6da4f8a65e..d342f43a3f 100644 --- a/frontend/app/src/hooks/useObjectDetails.ts +++ b/frontend/app/src/hooks/useObjectDetails.ts @@ -4,7 +4,7 @@ import useQuery from "@/hooks/useQuery"; import { IModelSchema, genericsState } from "@/state/atoms/schema.atom"; import { isGeneric } from "@/utils/common"; import { getSchemaObjectColumns, getTabs } from "@/utils/getSchemaObjectColumns"; -import { getPermission } from "@/utils/permissions"; +import { PERMISSION_ALLOW, getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { useAtomValue } from "jotai"; @@ -40,11 +40,12 @@ export const useObjectDetails = (schema: IModelSchema, objectId: string) => { notifyOnNetworkStatusChange: true, }); - const permission = getPermission( - schema?.kind && - apolloQuery?.data && - apolloQuery?.data[schema?.kind]?.permissions?.edges[0]?.node - ); + const permissionData = + schema?.kind && apolloQuery?.data?.[schema.kind]?.permissions?.edges[0]?.node + ? apolloQuery.data[schema.kind].permissions.edges[0].node + : null; + + const permission = permissionData ? getPermission(permissionData) : PERMISSION_ALLOW; return { ...apolloQuery, diff --git a/frontend/app/src/hooks/useObjectItems.ts b/frontend/app/src/hooks/useObjectItems.ts index 734246253c..4929eb08eb 100644 --- a/frontend/app/src/hooks/useObjectItems.ts +++ b/frontend/app/src/hooks/useObjectItems.ts @@ -77,9 +77,13 @@ export const useObjectItems = ( apolloQuery?.data[currentKind]?.permissions ); - const permission = hasPermission - ? getPermission(apolloQuery?.data[currentKind].permissions) - : PERMISSION_ALLOW; + const permissionData = hasPermission + ? apolloQuery.data[currentKind].permissions?.edges + ? apolloQuery.data[currentKind].permissions.edges + : apolloQuery.data[currentKind].permissions + : null; + + const permission = permissionData ? getPermission(permissionData) : PERMISSION_ALLOW; return { ...apolloQuery, diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts index 404618ab6b..447bfa5b50 100644 --- a/frontend/app/src/utils/permissions.ts +++ b/frontend/app/src/utils/permissions.ts @@ -23,22 +23,33 @@ export type Permission = { delete: PermissionAction; }; -export function getPermission(permission: PermissionProps): Permission { +export function getPermission( + permission: PermissionProps | Array<{ node: PermissionProps }> +): Permission { + const isPermissionArray = Array.isArray(permission); return { view: { - isAllowed: permission?.view === "ALLOW", + isAllowed: isPermissionArray + ? permission.some(({ node }) => node.view === "ALLOW") + : permission.view === "ALLOW", message: "You can't access this view.", }, create: { - isAllowed: permission?.create === "ALLOW", + isAllowed: isPermissionArray + ? permission.some(({ node }) => node.create === "ALLOW") + : permission.create === "ALLOW", message: "You can't create this object.", }, update: { - isAllowed: permission?.update === "ALLOW", + isAllowed: isPermissionArray + ? permission.some(({ node }) => node.update === "ALLOW") + : permission.update === "ALLOW", message: "You can't update this object.", }, delete: { - isAllowed: permission?.delete === "ALLOW", + isAllowed: isPermissionArray + ? permission.some(({ node }) => node.delete === "ALLOW") + : permission.delete === "ALLOW", message: "You can't delete this object.", }, }; diff --git a/frontend/app/tests/e2e/profile/profile.spec.ts b/frontend/app/tests/e2e/profile/profile.spec.ts index 09ab13f48e..65756b541e 100644 --- a/frontend/app/tests/e2e/profile/profile.spec.ts +++ b/frontend/app/tests/e2e/profile/profile.spec.ts @@ -11,7 +11,7 @@ test.describe("/profile", () => { }); test.describe("when not logged in", () => { - test.skip("should see 'Login' and no user avatar on header", async ({ page }) => { + test("should see 'Login' and no user avatar on header", async ({ page }) => { await page.goto("/"); await expect(page.getByTestId("unauthenticated-menu-trigger")).toBeVisible(); @@ -22,7 +22,7 @@ test.describe("/profile", () => { test.describe("when logged in as admin account", () => { test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); - test.skip("should access the profile page", async ({ page }) => { + test("should access the profile page", async ({ page }) => { await test.step("go to profile page", async () => { await page.goto("/"); await page.getByTestId("authenticated-menu-trigger").click(); @@ -41,7 +41,7 @@ test.describe("/profile", () => { test.describe("when logged in as read-write account", () => { test.use({ storageState: ACCOUNT_STATE_PATH.READ_WRITE }); - test.skip("should access the profile page", async ({ page }) => { + test("should access the profile page", async ({ page }) => { await test.step("go to profile page", async () => { await page.goto("/"); await page.getByTestId("authenticated-menu-trigger").click(); @@ -61,7 +61,7 @@ test.describe("/profile", () => { test.describe("when logged in as read-only account", () => { test.use({ storageState: ACCOUNT_STATE_PATH.READ_ONLY }); - test.skip("should access the profile page", async ({ page }) => { + test("should access the profile page", async ({ page }) => { await test.step("go to profile page", async () => { await page.goto("/"); await page.getByTestId("authenticated-menu-trigger").click(); From bb439f375551237ec69b03d47f9a89903f118b2f Mon Sep 17 00:00:00 2001 From: bilalabbad Date: Mon, 14 Oct 2024 11:27:36 +0200 Subject: [PATCH 20/40] permission for profile --- .../queries/objects/getObjectDetails.ts | 20 +++++++++------- .../graphql/queries/objects/getObjectItems.ts | 21 +++++++++------- frontend/app/src/hooks/useObjectDetails.ts | 2 ++ frontend/app/src/hooks/useObjectItems.ts | 3 +++ .../e2e/objects/profiles/profiles.spec.ts | 24 +++++++++---------- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/frontend/app/src/graphql/queries/objects/getObjectDetails.ts b/frontend/app/src/graphql/queries/objects/getObjectDetails.ts index f6d1fccfa9..05456508be 100644 --- a/frontend/app/src/graphql/queries/objects/getObjectDetails.ts +++ b/frontend/app/src/graphql/queries/objects/getObjectDetails.ts @@ -102,17 +102,19 @@ query {{kind}} { } } - permissions{ - edges{ - node{ - kind - view - create - update - delete + {{#if hasPermissions}} + permissions { + edges { + node { + kind + view + create + update + delete + } } } - } + {{/if}} } {{#if taskKind}} diff --git a/frontend/app/src/graphql/queries/objects/getObjectItems.ts b/frontend/app/src/graphql/queries/objects/getObjectItems.ts index e04c4deb1d..48cc43a16f 100644 --- a/frontend/app/src/graphql/queries/objects/getObjectItems.ts +++ b/frontend/app/src/graphql/queries/objects/getObjectItems.ts @@ -45,17 +45,20 @@ query {{kind}} ( {{/each}} } } - permissions{ - edges{ - node{ - kind - view - create - update - delete + + {{#if hasPermissions}} + permissions { + edges { + node { + kind + view + create + update + delete + } } } - } + {{/if}} } } `); diff --git a/frontend/app/src/hooks/useObjectDetails.ts b/frontend/app/src/hooks/useObjectDetails.ts index d342f43a3f..8d3e103cff 100644 --- a/frontend/app/src/hooks/useObjectDetails.ts +++ b/frontend/app/src/hooks/useObjectDetails.ts @@ -15,6 +15,7 @@ export const useObjectDetails = (schema: IModelSchema, objectId: string) => { const relationshipsTabs = getTabs(schema); const columns = getSchemaObjectColumns({ schema }); + const isProfileSchema = schema.namespace === "Profile"; const query = gql( schema ? getObjectDetailsPaginated({ @@ -29,6 +30,7 @@ export const useObjectDetails = (schema: IModelSchema, objectId: string) => { schema?.kind !== PROFILE_KIND && !isGeneric(schema) && schema?.generate_profile, + hasPermissions: !isProfileSchema, }) : // Empty query to make the gql parsing work // TODO: Find another solution for queries while loading schema diff --git a/frontend/app/src/hooks/useObjectItems.ts b/frontend/app/src/hooks/useObjectItems.ts index 4929eb08eb..439a67dba1 100644 --- a/frontend/app/src/hooks/useObjectItems.ts +++ b/frontend/app/src/hooks/useObjectItems.ts @@ -51,11 +51,14 @@ const getQuery = (schema?: IModelSchema, filters?: Array) => { const relationships = getObjectRelationships({ schema, forListView: true }); + const isProfileSchema = schema.namespace === "Profile"; + return getObjectItemsPaginated({ kind: kindFilterSchema?.kind || schema.kind, attributes, relationships, filters: filtersString, + hasPermissions: !isProfileSchema, }); }; diff --git a/frontend/app/tests/e2e/objects/profiles/profiles.spec.ts b/frontend/app/tests/e2e/objects/profiles/profiles.spec.ts index 7d9e23964f..0471f9ccaf 100644 --- a/frontend/app/tests/e2e/objects/profiles/profiles.spec.ts +++ b/frontend/app/tests/e2e/objects/profiles/profiles.spec.ts @@ -16,7 +16,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test.skip("should create a new profile successfully", async ({ page }) => { + test("should create a new profile successfully", async ({ page }) => { await test.step("Navigate to CoreProfile page", async () => { await page.goto("/objects/CoreProfile"); await expect(page.getByRole("heading")).toContainText("Profile"); @@ -39,7 +39,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test.skip("access the created profile, view its data, and edit it", async ({ page }) => { + test("access the created profile, view its data, and edit it", async ({ page }) => { await test.step("Navigate to CoreProfile page", async () => { await page.goto("/objects/CoreProfile"); await expect(page.getByRole("heading")).toContainText("Profile"); @@ -56,7 +56,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test.skip("create an object with a profile", async ({ page }) => { + test("create an object with a profile", async ({ page }) => { await test.step("Navigate to object creation page", async () => { await page.goto("/objects/BuiltinTag"); await page.getByTestId("create-object-button").click(); @@ -106,7 +106,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test.skip("edit a used profile and verify the changes reflect in an object using it", async ({ + test("edit a used profile and verify the changes reflect in an object using it", async ({ page, }) => { await test.step("Navigate to an used profile", async () => { @@ -129,7 +129,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { }); }); - test.skip("edit profile of tag without touching any other field", async ({ page }) => { + test("edit profile of tag without touching any other field", async ({ page }) => { await test.step("got to edit form of tag", async () => { await page.goto("/objects/BuiltinTag"); await page.getByRole("link", { name: "tag with profile" }).click(); @@ -146,7 +146,7 @@ test.describe("/objects/CoreProfile - Profiles page", () => { await expect(page.getByText("Description-")).toBeVisible(); }); - test.skip("delete the profile and reset object attribute value", async ({ page }) => { + test("delete the profile and reset object attribute value", async ({ page }) => { await test.step("Navigate to CoreProfile page", async () => { await page.goto("/objects/CoreProfile"); }); @@ -188,9 +188,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi }); }); - test.skip("should verify the form fields for a new profile for interface L2", async ({ - page, - }) => { + test("should verify the form fields for a new profile for interface L2", async ({ page }) => { await test.step("access Interface L2 form", async () => { await page.goto("/objects/CoreProfile"); @@ -224,7 +222,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi }); }); - test.skip("should create a new profile successfully for interface L2", async ({ page }) => { + test("should create a new profile successfully for interface L2", async ({ page }) => { await test.step("access Interface L2 form", async () => { await page.goto("/objects/CoreProfile"); await page.getByTestId("create-object-button").click(); @@ -244,7 +242,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi }); }); - test.skip("should create a new profile successfully for generic interface", async ({ page }) => { + test("should create a new profile successfully for generic interface", async ({ page }) => { await test.step("access Interface form", async () => { await page.goto("/objects/CoreProfile"); await page.getByTestId("create-object-button").click(); @@ -262,7 +260,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi }); }); - test.skip("should verify profile values after creation", async ({ page }) => { + test("should verify profile values after creation", async ({ page }) => { await page.goto("/objects/CoreProfile"); await page.getByRole("link", { name: PROFILE_NAME }).click(); await expect(page.locator("dl").getByText(PROFILE_NAME)).toBeVisible(); @@ -278,7 +276,7 @@ test.describe("/objects/CoreProfile - Profile for Interface L2 and fields verifi await expect(page.getByText("Provisioning")).toBeVisible(); }); - test.skip("should verify the available profiles in the object form", async ({ page }) => { + test("should verify the available profiles in the object form", async ({ page }) => { await page.goto("/objects/InfraInterface"); await page.getByTestId("create-object-button").click(); await page.getByLabel("Select an object type").click(); From 103f4b24be5768ebb4a9439e55dc9b6d24b811cf Mon Sep 17 00:00:00 2001 From: pa-lem Date: Mon, 14 Oct 2024 16:28:55 +0200 Subject: [PATCH 21/40] add flag for artifact --- .../app/src/screens/artifacts/object-item-details-paginated.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx index 390d98b5b6..b544143721 100644 --- a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx +++ b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx @@ -77,6 +77,7 @@ export default function ArtifactsDetails() { columns, relationshipsTabs, objectid, + hasPermissions: true, }) : // Empty query to make the gql parsing work // TODO: Find another solution for queries while loading schema @@ -106,6 +107,7 @@ export default function ArtifactsDetails() { const objectDetailsData = data[schemaData.kind]?.edges[0]?.node; + console.log("data: ", data); const permission = getPermission( schemaData.kind && data && data[schemaData?.kind]?.permissions?.edges[0]?.node ); From cc1a1fc84bc657dda2d3d39647355c4f276a748f Mon Sep 17 00:00:00 2001 From: pa-lem Date: Mon, 14 Oct 2024 16:38:55 +0200 Subject: [PATCH 22/40] remove log --- .../app/src/screens/artifacts/object-item-details-paginated.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx index b544143721..c2b608f0ba 100644 --- a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx +++ b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx @@ -107,7 +107,6 @@ export default function ArtifactsDetails() { const objectDetailsData = data[schemaData.kind]?.edges[0]?.node; - console.log("data: ", data); const permission = getPermission( schemaData.kind && data && data[schemaData?.kind]?.permissions?.edges[0]?.node ); From 0120950436c8dbf892f90304a2038b6ebd359ba9 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Mon, 14 Oct 2024 17:34:41 +0200 Subject: [PATCH 23/40] fix unauth test --- .../e2e/objects/object-relationships.spec.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/app/tests/e2e/objects/object-relationships.spec.ts b/frontend/app/tests/e2e/objects/object-relationships.spec.ts index 2155a647b3..6fa520aa2b 100644 --- a/frontend/app/tests/e2e/objects/object-relationships.spec.ts +++ b/frontend/app/tests/e2e/objects/object-relationships.spec.ts @@ -116,16 +116,16 @@ test.describe("/objects/:objectKind/:objectid - relationship tab", () => { .click(); await expect(page.getByTestId("relationship-row").first()).toBeVisible(); }); - }); - test("clicking on a relationship value redirects to its details page", async ({ page }) => { - await test.step("Navigate to relationship tab of an object", async () => { - await page.goto("/objects/InfraPlatform"); - await page.getByRole("link", { name: "Cisco IOS", exact: true }).click(); - await page.getByText("Devices10").click(); + test("clicking on a relationship value redirects to its details page", async ({ page }) => { + await test.step("Navigate to relationship tab of an object", async () => { + await page.goto("/objects/InfraPlatform"); + await page.getByRole("link", { name: "Cisco IOS", exact: true }).click(); + await page.getByText("Devices10").click(); + }); + await page.getByRole("link", { name: "atl1", exact: true }).first().click(); + await expect(page.getByText("Nameatl1")).toBeVisible(); + expect(page.url()).toContain("/objects/LocationSite/"); }); - await page.getByRole("link", { name: "atl1", exact: true }).first().click(); - await expect(page.getByText("Nameatl1")).toBeVisible(); - expect(page.url()).toContain("/objects/LocationSite/"); }); }); From 6639f3a8246348da2de58cf14537b6ac0f414692 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Tue, 15 Oct 2024 11:58:27 +0200 Subject: [PATCH 24/40] update permissions structure to not provide a message if it's allowed + relationship example --- .../action-buttons/relationships-buttons.tsx | 13 ++-- frontend/app/src/utils/permissions.ts | 75 ++++++++++++------- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/frontend/app/src/screens/object-item-details/action-buttons/relationships-buttons.tsx b/frontend/app/src/screens/object-item-details/action-buttons/relationships-buttons.tsx index f8fd1c6e9a..f85250089c 100644 --- a/frontend/app/src/screens/object-item-details/action-buttons/relationships-buttons.tsx +++ b/frontend/app/src/screens/object-item-details/action-buttons/relationships-buttons.tsx @@ -6,9 +6,9 @@ import { ALERT_TYPES, Alert } from "@/components/ui/alert"; import { QSP } from "@/config/qsp"; import graphqlClient from "@/graphql/graphqlClientApollo"; import { ADD_RELATIONSHIP } from "@/graphql/mutations/relationships/addRelationship"; -import { usePermission } from "@/hooks/usePermission"; import { useMutation } from "@/hooks/useQuery"; import { genericsState, schemaState } from "@/state/atoms/schema.atom"; +import { Permission } from "@/utils/permissions"; import { Icon } from "@iconify-icon/react"; import { useAtomValue } from "jotai"; import { useState } from "react"; @@ -16,8 +16,11 @@ import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { StringParam, useQueryParam } from "use-query-params"; -export function RelationshipsButtons() { - const permission = usePermission(); +interface RelationshipsButtonsProps { + permission: Permission; +} + +export function RelationshipsButtons({ permission }: RelationshipsButtonsProps) { const { objectKind, objectid } = useParams(); const [addRelationship] = useMutation(ADD_RELATIONSHIP); const generics = useAtomValue(genericsState); @@ -89,9 +92,9 @@ export function RelationshipsButtons() { return ( <> setShowAddDrawer(true)} data-testid="open-relationship-form-button" > diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts index 447bfa5b50..0e3e7895b0 100644 --- a/frontend/app/src/utils/permissions.ts +++ b/frontend/app/src/utils/permissions.ts @@ -1,5 +1,3 @@ -import { IModelSchema } from "@/state/atoms/schema.atom"; - export type PermissionProps = { view: string; create: string; @@ -27,31 +25,56 @@ export function getPermission( permission: PermissionProps | Array<{ node: PermissionProps }> ): Permission { const isPermissionArray = Array.isArray(permission); + + const isViewAllowed = isPermissionArray + ? permission.some(({ node }) => node.view === "ALLOW") + : permission.view === "ALLOW"; + + const isCreateAllowed = isPermissionArray + ? permission.some(({ node }) => node.create === "ALLOW") + : permission.create === "ALLOW"; + + const isUpdateAllowed = isPermissionArray + ? permission.some(({ node }) => node.update === "ALLOW") + : permission.update === "ALLOW"; + + const isDeleteAllowed = isPermissionArray + ? permission.some(({ node }) => node.delete === "ALLOW") + : permission.delete === "ALLOW"; + return { - view: { - isAllowed: isPermissionArray - ? permission.some(({ node }) => node.view === "ALLOW") - : permission.view === "ALLOW", - message: "You can't access this view.", - }, - create: { - isAllowed: isPermissionArray - ? permission.some(({ node }) => node.create === "ALLOW") - : permission.create === "ALLOW", - message: "You can't create this object.", - }, - update: { - isAllowed: isPermissionArray - ? permission.some(({ node }) => node.update === "ALLOW") - : permission.update === "ALLOW", - message: "You can't update this object.", - }, - delete: { - isAllowed: isPermissionArray - ? permission.some(({ node }) => node.delete === "ALLOW") - : permission.delete === "ALLOW", - message: "You can't delete this object.", - }, + view: isViewAllowed + ? { + isAllowed: true, + } + : { + isAllowed: false, + message: "You can't access this view", + }, + create: isCreateAllowed + ? { + isAllowed: true, + } + : { + isAllowed: false, + message: "You can't create this view", + }, + update: isUpdateAllowed + ? { + isAllowed: true, + } + : { + isAllowed: false, + message: "You can't access this view", + }, + delete: isDeleteAllowed + ? { + isAllowed: true, + } + : { + isAllowed: false, + message: "You can't create this view", + }, }; } From d6bd31d17f5bdc2ffca8c99ec74041f5f453050a Mon Sep 17 00:00:00 2001 From: pa-lem Date: Tue, 15 Oct 2024 14:22:02 +0200 Subject: [PATCH 25/40] add message from api on 403 --- .../app/src/screens/object-items/object-items-paginated.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/app/src/screens/object-items/object-items-paginated.tsx b/frontend/app/src/screens/object-items/object-items-paginated.tsx index 3cc3670f55..6d646f0ce7 100644 --- a/frontend/app/src/screens/object-items/object-items-paginated.tsx +++ b/frontend/app/src/screens/object-items/object-items-paginated.tsx @@ -105,6 +105,12 @@ export default function ObjectItems({ } if (error) { + if (error.networkError?.statusCode === 403) { + const { message } = error.networkError?.result?.errors?.[0] ?? {}; + + return ; + } + return ; } From 9157a6dea037ad353519e88277c1f4031520d67e Mon Sep 17 00:00:00 2001 From: pa-lem Date: Tue, 15 Oct 2024 14:27:10 +0200 Subject: [PATCH 26/40] update message --- frontend/app/src/utils/permissions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts index 0e3e7895b0..16d49da340 100644 --- a/frontend/app/src/utils/permissions.ts +++ b/frontend/app/src/utils/permissions.ts @@ -73,7 +73,7 @@ export function getPermission( } : { isAllowed: false, - message: "You can't create this view", + message: "You can't delete this view", }, }; } From a41f3315283755584c4e2ecf1620e62e55c8d496 Mon Sep 17 00:00:00 2001 From: pa-lem Date: Tue, 15 Oct 2024 14:29:38 +0200 Subject: [PATCH 27/40] update message --- frontend/app/src/utils/permissions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts index 16d49da340..636ad9f240 100644 --- a/frontend/app/src/utils/permissions.ts +++ b/frontend/app/src/utils/permissions.ts @@ -65,7 +65,7 @@ export function getPermission( } : { isAllowed: false, - message: "You can't access this view", + message: "You can't update this view", }, delete: isDeleteAllowed ? { From 83d96cce11f4f92417e51c420495cea42434b3fb Mon Sep 17 00:00:00 2001 From: pa-lem Date: Wed, 16 Oct 2024 13:44:54 +0200 Subject: [PATCH 28/40] start update messages and use new logic with current branch --- frontend/app/src/hooks/useObjectDetails.ts | 10 ++- frontend/app/src/hooks/useObjectItems.ts | 7 +- .../object-item-details-paginated.tsx | 7 +- .../app/src/screens/objects/object-header.tsx | 2 +- frontend/app/src/utils/permissions.ts | 67 ++++++++++++------- 5 files changed, 63 insertions(+), 30 deletions(-) diff --git a/frontend/app/src/hooks/useObjectDetails.ts b/frontend/app/src/hooks/useObjectDetails.ts index 8d3e103cff..ac7bebf43f 100644 --- a/frontend/app/src/hooks/useObjectDetails.ts +++ b/frontend/app/src/hooks/useObjectDetails.ts @@ -1,6 +1,7 @@ import { PROFILE_KIND, TASK_OBJECT } from "@/config/constants"; import { getObjectDetailsPaginated } from "@/graphql/queries/objects/getObjectDetails"; import useQuery from "@/hooks/useQuery"; +import { currentBranchAtom } from "@/state/atoms/branches.atom"; import { IModelSchema, genericsState } from "@/state/atoms/schema.atom"; import { isGeneric } from "@/utils/common"; import { getSchemaObjectColumns, getTabs } from "@/utils/getSchemaObjectColumns"; @@ -9,6 +10,7 @@ import { gql } from "@apollo/client"; import { useAtomValue } from "jotai"; export const useObjectDetails = (schema: IModelSchema, objectId: string) => { + const currentBranch = useAtomValue(currentBranchAtom); const generics = useAtomValue(genericsState); const profileGenericSchema = generics.find((s) => s.kind === PROFILE_KIND); @@ -43,11 +45,13 @@ export const useObjectDetails = (schema: IModelSchema, objectId: string) => { }); const permissionData = - schema?.kind && apolloQuery?.data?.[schema.kind]?.permissions?.edges[0]?.node - ? apolloQuery.data[schema.kind].permissions.edges[0].node + schema?.kind && apolloQuery?.data?.[schema.kind]?.permissions?.edges + ? apolloQuery.data[schema.kind].permissions.edges : null; - const permission = permissionData ? getPermission(permissionData) : PERMISSION_ALLOW; + const permission = permissionData + ? getPermission(permissionData, currentBranch) + : PERMISSION_ALLOW; return { ...apolloQuery, diff --git a/frontend/app/src/hooks/useObjectItems.ts b/frontend/app/src/hooks/useObjectItems.ts index 439a67dba1..3748735a6d 100644 --- a/frontend/app/src/hooks/useObjectItems.ts +++ b/frontend/app/src/hooks/useObjectItems.ts @@ -3,6 +3,7 @@ import { getTokens } from "@/graphql/queries/accounts/getTokens"; import { getObjectItemsPaginated } from "@/graphql/queries/objects/getObjectItems"; import { Filter } from "@/hooks/useFilters"; import useQuery from "@/hooks/useQuery"; +import { currentBranchAtom } from "@/state/atoms/branches.atom"; import { IModelSchema, genericsState, profilesAtom, schemaState } from "@/state/atoms/schema.atom"; import { getObjectAttributes, getObjectRelationships } from "@/utils/getSchemaObjectColumns"; import { PERMISSION_ALLOW, getPermission } from "@/utils/permissions"; @@ -67,6 +68,8 @@ export const useObjectItems = ( filters?: Array, kindFilter?: string ) => { + const currentBranch = useAtomValue(currentBranchAtom); + const query = gql` ${getQuery(schema, filters)} `; @@ -86,7 +89,9 @@ export const useObjectItems = ( : apolloQuery.data[currentKind].permissions : null; - const permission = permissionData ? getPermission(permissionData) : PERMISSION_ALLOW; + const permission = permissionData + ? getPermission(permissionData, currentBranch) + : PERMISSION_ALLOW; return { ...apolloQuery, diff --git a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx index c2b608f0ba..5b69888fdd 100644 --- a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx +++ b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx @@ -19,6 +19,7 @@ import LoadingScreen from "@/screens/loading-screen/loading-screen"; import RelationshipDetails from "@/screens/object-item-details/relationship-details-paginated"; import { RelationshipsDetails } from "@/screens/object-item-details/relationships-details-paginated"; import ObjectItemMetaEdit from "@/screens/object-item-meta-edit/object-item-meta-edit"; +import { currentBranchAtom } from "@/state/atoms/branches.atom"; import { showMetaEditState } from "@/state/atoms/metaEditFieldDetails.atom"; import { genericsState, schemaState } from "@/state/atoms/schema.atom"; import { schemaKindNameState } from "@/state/atoms/schemaKindName.atom"; @@ -36,13 +37,14 @@ import { getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { LockClosedIcon, RectangleGroupIcon } from "@heroicons/react/24/outline"; import { Icon } from "@iconify-icon/react"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { Link, Navigate, useParams } from "react-router-dom"; import { StringParam, useQueryParam } from "use-query-params"; export default function ArtifactsDetails() { const { objectid } = useParams(); + const currentBranch = useAtomValue(currentBranchAtom); const [qspTab] = useQueryParam(QSP.TAB, StringParam); const auth = useAuth(); const [showMetaEditModal, setShowMetaEditModal] = useAtom(showMetaEditState); @@ -108,7 +110,8 @@ export default function ArtifactsDetails() { const objectDetailsData = data[schemaData.kind]?.edges[0]?.node; const permission = getPermission( - schemaData.kind && data && data[schemaData?.kind]?.permissions?.edges[0]?.node + schemaData.kind && data && data[schemaData?.kind]?.permissions?.edges, + currentBranch ); const tabs = [ diff --git a/frontend/app/src/screens/objects/object-header.tsx b/frontend/app/src/screens/objects/object-header.tsx index 827def1dff..154c8c821d 100644 --- a/frontend/app/src/screens/objects/object-header.tsx +++ b/frontend/app/src/screens/objects/object-header.tsx @@ -34,7 +34,7 @@ const ObjectItemsHeader = ({ schema }: ObjectHeaderProps) => { const isProfile = schema.namespace === "Profile" || schemaKind === PROFILE_KIND; const breadcrumbModelLabel = isProfile ? "All Profiles" : schema.label || schema.name; const { count, permissions } = data ? data[schemaKind] : {}; - const currentPermission = permissions?.edges[0]?.node; + const currentPermission = permissions?.edges; if (currentPermission?.view !== "ALLOW") { return null; diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts index 636ad9f240..78f74d6699 100644 --- a/frontend/app/src/utils/permissions.ts +++ b/frontend/app/src/utils/permissions.ts @@ -1,8 +1,28 @@ -export type PermissionProps = { - view: string; - create: string; - update: string; - delete: string; +import { Branch } from "@/generated/graphql"; + +type Action = "view" | "create" | "update" | "delete"; + +type Decision = "DENY" | "ALLOW_ALL" | "ALLOW_DEFAULT" | "ALLOW_OTHER"; + +const getAllowedValue = (decision: Decision, isOnDefaultBranch: boolean) => { + if (decision === "ALLOW_ALL") return true; + if (decision === "ALLOW_DEFAULT" && isOnDefaultBranch) return true; + if (decision === "ALLOW_OTHER" && !isOnDefaultBranch) return true; + return false; +}; + +const getMessage = (decision: Decision, action: string) => { + switch (decision) { + case "DENY": { + return `You can't ${action} this object.`; + } + case "ALLOW_DEFAULT": { + return `You can't ${action} this object in this branch. Switch to the default branch.`; + } + case "ALLOW_OTHER": { + return `You can't ${action} this object in the default branch. Switch to another one.`; + } + } }; export type PermissionAction = @@ -22,25 +42,26 @@ export type Permission = { }; export function getPermission( - permission: PermissionProps | Array<{ node: PermissionProps }> + permission: Array<{ node: Record }>, + currentBranch: Branch | null ): Permission { - const isPermissionArray = Array.isArray(permission); + const isOnDefaultBranch = !!currentBranch?.is_default; - const isViewAllowed = isPermissionArray - ? permission.some(({ node }) => node.view === "ALLOW") - : permission.view === "ALLOW"; + const isViewAllowed = permission.find(({ node }) => + getAllowedValue(node.view, isOnDefaultBranch) + ); - const isCreateAllowed = isPermissionArray - ? permission.some(({ node }) => node.create === "ALLOW") - : permission.create === "ALLOW"; + const isCreateAllowed = permission.find(({ node }) => + getAllowedValue(node.create, isOnDefaultBranch) + ); - const isUpdateAllowed = isPermissionArray - ? permission.some(({ node }) => node.update === "ALLOW") - : permission.update === "ALLOW"; + const isUpdateAllowed = permission.find(({ node }) => + getAllowedValue(node.update, isOnDefaultBranch) + ); - const isDeleteAllowed = isPermissionArray - ? permission.some(({ node }) => node.delete === "ALLOW") - : permission.delete === "ALLOW"; + const isDeleteAllowed = permission.find(({ node }) => + getAllowedValue(node.delete, isOnDefaultBranch) + ); return { view: isViewAllowed @@ -49,7 +70,7 @@ export function getPermission( } : { isAllowed: false, - message: "You can't access this view", + message: "You can't access this object.", }, create: isCreateAllowed ? { @@ -57,7 +78,7 @@ export function getPermission( } : { isAllowed: false, - message: "You can't create this view", + message: "You can't create this object", }, update: isUpdateAllowed ? { @@ -65,7 +86,7 @@ export function getPermission( } : { isAllowed: false, - message: "You can't update this view", + message: "You can't update this object", }, delete: isDeleteAllowed ? { @@ -73,7 +94,7 @@ export function getPermission( } : { isAllowed: false, - message: "You can't delete this view", + message: "You can't delete this object", }, }; } From a4e9dbd3c2619b121565bb3e3bb2510fea5b7209 Mon Sep 17 00:00:00 2001 From: bilalabbad Date: Wed, 16 Oct 2024 15:02:06 +0200 Subject: [PATCH 29/40] update types + renaming --- frontend/app/src/hooks/useObjectDetails.ts | 8 +- frontend/app/src/hooks/useObjectItems.ts | 9 +- .../object-item-details-paginated.tsx | 7 +- .../app/src/screens/objects/object-header.tsx | 5 +- .../app/src/screens/role-management/types.ts | 9 + frontend/app/src/utils/permissions.ts | 161 +++++++----------- 6 files changed, 82 insertions(+), 117 deletions(-) create mode 100644 frontend/app/src/screens/role-management/types.ts diff --git a/frontend/app/src/hooks/useObjectDetails.ts b/frontend/app/src/hooks/useObjectDetails.ts index ac7bebf43f..bdd564043e 100644 --- a/frontend/app/src/hooks/useObjectDetails.ts +++ b/frontend/app/src/hooks/useObjectDetails.ts @@ -1,16 +1,14 @@ import { PROFILE_KIND, TASK_OBJECT } from "@/config/constants"; import { getObjectDetailsPaginated } from "@/graphql/queries/objects/getObjectDetails"; import useQuery from "@/hooks/useQuery"; -import { currentBranchAtom } from "@/state/atoms/branches.atom"; import { IModelSchema, genericsState } from "@/state/atoms/schema.atom"; import { isGeneric } from "@/utils/common"; import { getSchemaObjectColumns, getTabs } from "@/utils/getSchemaObjectColumns"; -import { PERMISSION_ALLOW, getPermission } from "@/utils/permissions"; +import { getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { useAtomValue } from "jotai"; export const useObjectDetails = (schema: IModelSchema, objectId: string) => { - const currentBranch = useAtomValue(currentBranchAtom); const generics = useAtomValue(genericsState); const profileGenericSchema = generics.find((s) => s.kind === PROFILE_KIND); @@ -49,9 +47,7 @@ export const useObjectDetails = (schema: IModelSchema, objectId: string) => { ? apolloQuery.data[schema.kind].permissions.edges : null; - const permission = permissionData - ? getPermission(permissionData, currentBranch) - : PERMISSION_ALLOW; + const permission = getPermission(permissionData); return { ...apolloQuery, diff --git a/frontend/app/src/hooks/useObjectItems.ts b/frontend/app/src/hooks/useObjectItems.ts index 3748735a6d..4d95dab098 100644 --- a/frontend/app/src/hooks/useObjectItems.ts +++ b/frontend/app/src/hooks/useObjectItems.ts @@ -3,10 +3,9 @@ import { getTokens } from "@/graphql/queries/accounts/getTokens"; import { getObjectItemsPaginated } from "@/graphql/queries/objects/getObjectItems"; import { Filter } from "@/hooks/useFilters"; import useQuery from "@/hooks/useQuery"; -import { currentBranchAtom } from "@/state/atoms/branches.atom"; import { IModelSchema, genericsState, profilesAtom, schemaState } from "@/state/atoms/schema.atom"; import { getObjectAttributes, getObjectRelationships } from "@/utils/getSchemaObjectColumns"; -import { PERMISSION_ALLOW, getPermission } from "@/utils/permissions"; +import { getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { useAtomValue } from "jotai"; @@ -68,8 +67,6 @@ export const useObjectItems = ( filters?: Array, kindFilter?: string ) => { - const currentBranch = useAtomValue(currentBranchAtom); - const query = gql` ${getQuery(schema, filters)} `; @@ -89,9 +86,7 @@ export const useObjectItems = ( : apolloQuery.data[currentKind].permissions : null; - const permission = permissionData - ? getPermission(permissionData, currentBranch) - : PERMISSION_ALLOW; + const permission = getPermission(permissionData); return { ...apolloQuery, diff --git a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx index 5b69888fdd..54380004ab 100644 --- a/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx +++ b/frontend/app/src/screens/artifacts/object-item-details-paginated.tsx @@ -19,7 +19,6 @@ import LoadingScreen from "@/screens/loading-screen/loading-screen"; import RelationshipDetails from "@/screens/object-item-details/relationship-details-paginated"; import { RelationshipsDetails } from "@/screens/object-item-details/relationships-details-paginated"; import ObjectItemMetaEdit from "@/screens/object-item-meta-edit/object-item-meta-edit"; -import { currentBranchAtom } from "@/state/atoms/branches.atom"; import { showMetaEditState } from "@/state/atoms/metaEditFieldDetails.atom"; import { genericsState, schemaState } from "@/state/atoms/schema.atom"; import { schemaKindNameState } from "@/state/atoms/schemaKindName.atom"; @@ -37,14 +36,13 @@ import { getPermission } from "@/utils/permissions"; import { gql } from "@apollo/client"; import { LockClosedIcon, RectangleGroupIcon } from "@heroicons/react/24/outline"; import { Icon } from "@iconify-icon/react"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtom } from "jotai"; import { Link, Navigate, useParams } from "react-router-dom"; import { StringParam, useQueryParam } from "use-query-params"; export default function ArtifactsDetails() { const { objectid } = useParams(); - const currentBranch = useAtomValue(currentBranchAtom); const [qspTab] = useQueryParam(QSP.TAB, StringParam); const auth = useAuth(); const [showMetaEditModal, setShowMetaEditModal] = useAtom(showMetaEditState); @@ -110,8 +108,7 @@ export default function ArtifactsDetails() { const objectDetailsData = data[schemaData.kind]?.edges[0]?.node; const permission = getPermission( - schemaData.kind && data && data[schemaData?.kind]?.permissions?.edges, - currentBranch + schemaData.kind && data && data[schemaData?.kind]?.permissions?.edges ); const tabs = [ diff --git a/frontend/app/src/screens/objects/object-header.tsx b/frontend/app/src/screens/objects/object-header.tsx index 154c8c821d..2fef9c275c 100644 --- a/frontend/app/src/screens/objects/object-header.tsx +++ b/frontend/app/src/screens/objects/object-header.tsx @@ -9,6 +9,7 @@ import { useObjectItems } from "@/hooks/useObjectItems"; import Content from "@/screens/layout/content"; import { IModelSchema } from "@/state/atoms/schema.atom"; import { constructPath } from "@/utils/fetch"; +import { getPermission } from "@/utils/permissions"; import { Icon } from "@iconify-icon/react"; import { Link } from "react-router-dom"; @@ -34,9 +35,9 @@ const ObjectItemsHeader = ({ schema }: ObjectHeaderProps) => { const isProfile = schema.namespace === "Profile" || schemaKind === PROFILE_KIND; const breadcrumbModelLabel = isProfile ? "All Profiles" : schema.label || schema.name; const { count, permissions } = data ? data[schemaKind] : {}; - const currentPermission = permissions?.edges; + const currentPermission = getPermission(permissions?.edges); - if (currentPermission?.view !== "ALLOW") { + if (!currentPermission.view.isAllowed) { return null; } diff --git a/frontend/app/src/screens/role-management/types.ts b/frontend/app/src/screens/role-management/types.ts new file mode 100644 index 0000000000..666dc5f551 --- /dev/null +++ b/frontend/app/src/screens/role-management/types.ts @@ -0,0 +1,9 @@ +export type PermissionDecisionData = "DENY" | "ALLOW_ALL" | "ALLOW_DEFAULT" | "ALLOW_OTHER"; + +export type PermissionAction = "view" | "create" | "update" | "delete"; + +export type PermissionData = Record; + +export type PermissionDecision = { isAllowed: true } | { isAllowed: false; message: string }; + +export type Permission = Record; diff --git a/frontend/app/src/utils/permissions.ts b/frontend/app/src/utils/permissions.ts index 78f74d6699..134cb734cb 100644 --- a/frontend/app/src/utils/permissions.ts +++ b/frontend/app/src/utils/permissions.ts @@ -1,115 +1,82 @@ import { Branch } from "@/generated/graphql"; +import { + Permission, + PermissionAction, + PermissionData, + PermissionDecision, + PermissionDecisionData, +} from "@/screens/role-management/types"; +import { store } from "@/state"; +import { currentBranchAtom } from "@/state/atoms/branches.atom"; +import { configState } from "@/state/atoms/config.atom"; +import { warnUnexpectedType } from "./common"; -type Action = "view" | "create" | "update" | "delete"; - -type Decision = "DENY" | "ALLOW_ALL" | "ALLOW_DEFAULT" | "ALLOW_OTHER"; - -const getAllowedValue = (decision: Decision, isOnDefaultBranch: boolean) => { - if (decision === "ALLOW_ALL") return true; - if (decision === "ALLOW_DEFAULT" && isOnDefaultBranch) return true; - if (decision === "ALLOW_OTHER" && !isOnDefaultBranch) return true; - return false; +const isActionAllowedOnBranch = ( + decision: PermissionDecisionData, + isOnDefaultBranch: boolean +): boolean => { + switch (decision) { + case "ALLOW_ALL": + return true; + case "ALLOW_DEFAULT": + return isOnDefaultBranch; + case "ALLOW_OTHER": + return !isOnDefaultBranch; + default: + return false; + } }; -const getMessage = (decision: Decision, action: string) => { +const getMessage = (decision: PermissionDecisionData, action: string): string => { switch (decision) { - case "DENY": { - return `You can't ${action} this object.`; - } - case "ALLOW_DEFAULT": { - return `You can't ${action} this object in this branch. Switch to the default branch.`; - } - case "ALLOW_OTHER": { - return `You can't ${action} this object in the default branch. Switch to another one.`; - } + case "DENY": + return `You don't have permission to ${action} this object.`; + case "ALLOW_DEFAULT": + return `This action is only allowed on the default branch. Please switch to the default branch to ${action} this object.`; + case "ALLOW_OTHER": + return `This action is not allowed on the default branch. Please switch to a different branch to ${action} this object.`; + case "ALLOW_ALL": + return `You have permission to ${action} this object on any branch.`; + default: + warnUnexpectedType(decision); + return `Unable to determine permission to ${action} this object. Please contact your administrator.`; } }; -export type PermissionAction = - | { - isAllowed: true; - } - | { - isAllowed: false; - message: string; - }; - -export type Permission = { - view: PermissionAction; - create: PermissionAction; - update: PermissionAction; - delete: PermissionAction; -}; +export function getPermission(permission?: Array<{ node: PermissionData }>): Permission { + if (!permission) return PERMISSION_ALLOW_ALL; -export function getPermission( - permission: Array<{ node: Record }>, - currentBranch: Branch | null -): Permission { + const config = store.get(configState); + const currentBranch = store.get(currentBranchAtom); const isOnDefaultBranch = !!currentBranch?.is_default; - const isViewAllowed = permission.find(({ node }) => - getAllowedValue(node.view, isOnDefaultBranch) - ); - - const isCreateAllowed = permission.find(({ node }) => - getAllowedValue(node.create, isOnDefaultBranch) - ); + const createPermissionAction = (action: PermissionAction): PermissionDecision => { + const permissionNode = permission.find(({ node }) => + isActionAllowedOnBranch(node[action], isOnDefaultBranch) + ); - const isUpdateAllowed = permission.find(({ node }) => - getAllowedValue(node.update, isOnDefaultBranch) - ); - - const isDeleteAllowed = permission.find(({ node }) => - getAllowedValue(node.delete, isOnDefaultBranch) - ); + if (permissionNode) { + return { isAllowed: true }; + } else { + const deniedNode = permission.find(({ node }) => node[action] === "DENY"); + const message = deniedNode + ? getMessage(deniedNode.node[action], action) + : getMessage("DENY", action); + return { isAllowed: false, message }; + } + }; return { - view: isViewAllowed - ? { - isAllowed: true, - } - : { - isAllowed: false, - message: "You can't access this object.", - }, - create: isCreateAllowed - ? { - isAllowed: true, - } - : { - isAllowed: false, - message: "You can't create this object", - }, - update: isUpdateAllowed - ? { - isAllowed: true, - } - : { - isAllowed: false, - message: "You can't update this object", - }, - delete: isDeleteAllowed - ? { - isAllowed: true, - } - : { - isAllowed: false, - message: "You can't delete this object", - }, + view: createPermissionAction("view"), + create: createPermissionAction("create"), + update: createPermissionAction("update"), + delete: createPermissionAction("delete"), }; } -export const PERMISSION_ALLOW: Permission = { - view: { - isAllowed: true, - }, - create: { - isAllowed: true, - }, - update: { - isAllowed: true, - }, - delete: { - isAllowed: true, - }, +export const PERMISSION_ALLOW_ALL: Permission = { + view: { isAllowed: true }, + create: { isAllowed: true }, + update: { isAllowed: true }, + delete: { isAllowed: true }, }; From e2bfc1e0dffe8ce2824e52b56daa22c6a0f69e81 Mon Sep 17 00:00:00 2001 From: bilalabbad Date: Wed, 16 Oct 2024 15:58:02 +0200 Subject: [PATCH 30/40] better message --- .../form/object-create-form-trigger.tsx | 2 +- .../action-buttons/relationships-buttons.tsx | 2 +- .../object-item-details-paginated.tsx | 2 +- .../object-items/object-items-paginated.tsx | 8 +++--- frontend/app/src/utils/permissions.ts | 25 ++++++++++++------- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/frontend/app/src/components/form/object-create-form-trigger.tsx b/frontend/app/src/components/form/object-create-form-trigger.tsx index ebb0843f62..db1058eb1d 100644 --- a/frontend/app/src/components/form/object-create-form-trigger.tsx +++ b/frontend/app/src/components/form/object-create-form-trigger.tsx @@ -2,8 +2,8 @@ import SlideOver, { SlideOverTitle } from "@/components/display/slide-over"; import ObjectForm from "@/components/form/object-form"; import { ARTIFACT_OBJECT } from "@/config/constants"; import graphqlClient from "@/graphql/graphqlClientApollo"; +import { Permission } from "@/screens/role-management/types"; import { IModelSchema } from "@/state/atoms/schema.atom"; -import { Permission } from "@/utils/permissions"; import { Icon } from "@iconify-icon/react"; import { useState } from "react"; import { Button, ButtonProps } from "../buttons/button-primitive"; diff --git a/frontend/app/src/screens/object-item-details/action-buttons/relationships-buttons.tsx b/frontend/app/src/screens/object-item-details/action-buttons/relationships-buttons.tsx index f85250089c..fba8685cb8 100644 --- a/frontend/app/src/screens/object-item-details/action-buttons/relationships-buttons.tsx +++ b/frontend/app/src/screens/object-item-details/action-buttons/relationships-buttons.tsx @@ -7,8 +7,8 @@ import { QSP } from "@/config/qsp"; import graphqlClient from "@/graphql/graphqlClientApollo"; import { ADD_RELATIONSHIP } from "@/graphql/mutations/relationships/addRelationship"; import { useMutation } from "@/hooks/useQuery"; +import { Permission } from "@/screens/role-management/types"; import { genericsState, schemaState } from "@/state/atoms/schema.atom"; -import { Permission } from "@/utils/permissions"; import { Icon } from "@iconify-icon/react"; import { useAtomValue } from "jotai"; import { useState } from "react"; diff --git a/frontend/app/src/screens/object-item-details/object-item-details-paginated.tsx b/frontend/app/src/screens/object-item-details/object-item-details-paginated.tsx index 5469406cf8..2a311c4ab6 100644 --- a/frontend/app/src/screens/object-item-details/object-item-details-paginated.tsx +++ b/frontend/app/src/screens/object-item-details/object-item-details-paginated.tsx @@ -9,6 +9,7 @@ import graphqlClient from "@/graphql/graphqlClientApollo"; import { useTitle } from "@/hooks/useTitle"; import NoDataFound from "@/screens/errors/no-data-found"; import ObjectItemMetaEdit from "@/screens/object-item-meta-edit/object-item-meta-edit"; +import { Permission } from "@/screens/role-management/types"; import { TaskItemDetails } from "@/screens/tasks/task-item-details"; import { TaskItems } from "@/screens/tasks/task-items"; import { currentBranchAtom } from "@/state/atoms/branches.atom"; @@ -23,7 +24,6 @@ import { getObjectTabs, getTabs, } from "@/utils/getSchemaObjectColumns"; -import { Permission } from "@/utils/permissions"; import { LockClosedIcon } from "@heroicons/react/24/outline"; import { Icon } from "@iconify-icon/react"; import { useAtom } from "jotai"; diff --git a/frontend/app/src/screens/object-items/object-items-paginated.tsx b/frontend/app/src/screens/object-items/object-items-paginated.tsx index 6d646f0ce7..8197b58aab 100644 --- a/frontend/app/src/screens/object-items/object-items-paginated.tsx +++ b/frontend/app/src/screens/object-items/object-items-paginated.tsx @@ -100,10 +100,6 @@ export default function ObjectItems({ const debouncedHandleSearch = debounce(handleSearch, 500); - if (!loading && !permission.view.isAllowed) { - return ; - } - if (error) { if (error.networkError?.statusCode === 403) { const { message } = error.networkError?.result?.errors?.[0] ?? {}; @@ -114,6 +110,10 @@ export default function ObjectItems({ return ; } + if (!loading && !permission.view.isAllowed) { + return ; + } + return ( <>
{ +const getMessage = (action: string, decision?: PermissionDecisionData): string => { + if (!decision) + return `Unable to determine permission to ${action} this object. Please contact your administrator.`; + switch (decision) { case "DENY": return `You don't have permission to ${action} this object.`; @@ -39,7 +42,7 @@ const getMessage = (decision: PermissionDecisionData, action: string): string => return `You have permission to ${action} this object on any branch.`; default: warnUnexpectedType(decision); - return `Unable to determine permission to ${action} this object. Please contact your administrator.`; + return ""; } }; @@ -51,18 +54,22 @@ export function getPermission(permission?: Array<{ node: PermissionData }>): Per const isOnDefaultBranch = !!currentBranch?.is_default; const createPermissionAction = (action: PermissionAction): PermissionDecision => { - const permissionNode = permission.find(({ node }) => + if (action === "view" && config?.main.allow_anonymous_access) return { isAllowed: true }; + + const permissionAllowNode = permission.find(({ node }) => isActionAllowedOnBranch(node[action], isOnDefaultBranch) ); - if (permissionNode) { + if (permissionAllowNode) { return { isAllowed: true }; } else { - const deniedNode = permission.find(({ node }) => node[action] === "DENY"); - const message = deniedNode - ? getMessage(deniedNode.node[action], action) - : getMessage("DENY", action); - return { isAllowed: false, message }; + const permissionDeniedNode = permission.find( + ({ node }) => !isActionAllowedOnBranch(node[action], isOnDefaultBranch) + ); + return { + isAllowed: false, + message: getMessage(action, permissionDeniedNode?.node?.[action]), + }; } }; From 69bf48a8bcabb30ef18dbaed4d514581423192eb Mon Sep 17 00:00:00 2001 From: bilalabbad Date: Wed, 16 Oct 2024 17:18:41 +0200 Subject: [PATCH 31/40] disable generic selector item when not allowed to create --- .../components/form/generic-object-form.tsx | 4 +- .../src/components/form/generic-selector.tsx | 75 +++++++++++++++++-- .../form/object-create-form-trigger.tsx | 5 +- frontend/app/src/hooks/useSchema.ts | 2 +- .../action-buttons/relationships-buttons.tsx | 2 +- .../object-item-details-paginated.tsx | 2 +- .../queries/getObjectPermissions.ts | 24 ++++++ .../{role-management => permission}/types.ts | 2 +- .../src/screens/role-management/accounts.tsx | 2 +- .../role-management/global-permissions.tsx | 2 +- .../src/screens/role-management/groups.tsx | 2 +- .../role-management/object-permissions.tsx | 2 +- .../app/src/screens/role-management/roles.tsx | 2 +- frontend/app/src/utils/permissions.ts | 2 +- 14 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 frontend/app/src/screens/permission/queries/getObjectPermissions.ts rename frontend/app/src/screens/{role-management => permission}/types.ts (92%) diff --git a/frontend/app/src/components/form/generic-object-form.tsx b/frontend/app/src/components/form/generic-object-form.tsx index 95c9ab1c7a..af6c512566 100644 --- a/frontend/app/src/components/form/generic-object-form.tsx +++ b/frontend/app/src/components/form/generic-object-form.tsx @@ -9,8 +9,8 @@ interface GenericObjectFormProps extends Omit { } export const GenericObjectForm = ({ genericSchema, ...props }: GenericObjectFormProps) => { - const [kindToCreate, setKindToCreate] = useState( - genericSchema.used_by?.length === 1 ? genericSchema.used_by[0] : undefined + const [kindToCreate, setKindToCreate] = useState( + genericSchema.used_by?.length === 1 ? genericSchema.used_by[0] : null ); if (!genericSchema.used_by || genericSchema.used_by?.length === 0) { diff --git a/frontend/app/src/components/form/generic-selector.tsx b/frontend/app/src/components/form/generic-selector.tsx index e263977474..93cb873bf0 100644 --- a/frontend/app/src/components/form/generic-selector.tsx +++ b/frontend/app/src/components/form/generic-selector.tsx @@ -1,28 +1,50 @@ -import { Combobox, tComboboxItem } from "@/components/ui/combobox-legacy"; +import { Badge } from "@/components/ui/badge"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "@/components/ui/combobox"; import Label from "@/components/ui/label"; import { PROFILE_KIND } from "@/config/constants"; +import useQuery from "@/hooks/useQuery"; +import { useSchema } from "@/hooks/useSchema"; +import LoadingScreen from "@/screens/loading-screen/loading-screen"; +import { getObjectPermissionsQuery } from "@/screens/permission/queries/getObjectPermissions"; +import { PermissionData } from "@/screens/permission/types"; import { genericsState, profilesAtom, schemaState } from "@/state/atoms/schema.atom"; +import { getPermission } from "@/utils/permissions"; +import { gql } from "@apollo/client"; import { useAtomValue } from "jotai/index"; -import { useId } from "react"; +import React, { useId, useState } from "react"; type GenericSelectorProps = { currentKind: string; kindInheritingFromGeneric: string[]; - value?: string; - onChange: (item: string) => void; + value?: string | null; + onChange: (item: string | null) => void; }; export const GenericSelector = ({ currentKind, kindInheritingFromGeneric, - ...props + value, + onChange, }: GenericSelectorProps) => { const id = useId(); const nodeSchemas = useAtomValue(schemaState); const nodeGenerics = useAtomValue(genericsState); const profileSchemas = useAtomValue(profilesAtom); + const { schema } = useSchema(value); + const [open, setOpen] = useState(false); + const { data, loading } = useQuery(gql(getObjectPermissionsQuery(currentKind))); - const items: Array = kindInheritingFromGeneric + if (loading) return ; + const permissionsData: Array<{ node: PermissionData }> = data?.[currentKind]?.permissions?.edges; + + const items = kindInheritingFromGeneric .map((usedByKind) => { const relatedSchema = [...nodeSchemas, ...profileSchemas].find( (schema) => schema.kind === usedByKind @@ -62,7 +84,46 @@ export const GenericSelector = ({ return (
- + + + {schema && } + + + + + No schema found. + {items.map((item) => { + const itemValue = item?.value as string; + const permissionToCreate = getPermission( + permissionsData.filter(({ node }) => node.kind === itemValue) + ).create; + + return ( + { + onChange(value === itemValue ? null : itemValue); + setOpen(false); + }} + disabled={!permissionToCreate.isAllowed} + > + + + ); + })} + + + +
+ ); +}; + +const SchemaItem = ({ label, badge }: { label: string; badge: string }) => { + return ( +
+ {label} {badge}
); }; diff --git a/frontend/app/src/components/form/object-create-form-trigger.tsx b/frontend/app/src/components/form/object-create-form-trigger.tsx index db1058eb1d..e2e612e455 100644 --- a/frontend/app/src/components/form/object-create-form-trigger.tsx +++ b/frontend/app/src/components/form/object-create-form-trigger.tsx @@ -2,7 +2,7 @@ import SlideOver, { SlideOverTitle } from "@/components/display/slide-over"; import ObjectForm from "@/components/form/object-form"; import { ARTIFACT_OBJECT } from "@/config/constants"; import graphqlClient from "@/graphql/graphqlClientApollo"; -import { Permission } from "@/screens/role-management/types"; +import { Permission } from "@/screens/permission/types"; import { IModelSchema } from "@/state/atoms/schema.atom"; import { Icon } from "@iconify-icon/react"; import { useState } from "react"; @@ -29,11 +29,10 @@ export const ObjectCreateFormTrigger = ({ } const isAllowed = permission.create.isAllowed; - const tooltipMessage = permission.create.message; return ( <> - +