diff --git a/src/api/services/search.ts b/src/api/services/search.ts index a24376b43..b164f181c 100644 --- a/src/api/services/search.ts +++ b/src/api/services/search.ts @@ -5,6 +5,8 @@ export type SearchResourcesRequest = { checks?: PlaybookResourceSelector[]; components?: PlaybookResourceSelector[]; configs?: PlaybookResourceSelector[]; + connections?: PlaybookResourceSelector[]; + playbooks?: PlaybookResourceSelector[]; }; type SearchedResource = { @@ -22,6 +24,8 @@ type SelectedResources = { configs: SearchedResource[]; checks: SearchedResource[]; components: SearchedResource[]; + connections: SearchedResource[]; + playbooks: SearchedResource[]; }; export async function searchResources(input: SearchResourcesRequest) { diff --git a/src/api/services/users.ts b/src/api/services/users.ts index 82f897686..20ba24001 100644 --- a/src/api/services/users.ts +++ b/src/api/services/users.ts @@ -17,8 +17,11 @@ export const getPerson = (id: string) => ); export const getPersons = () => + // email=NULl filters out system user resolvePostGrestRequestWithPagination( - IncidentCommander.get(`/people?select=*&order=name.asc`) + IncidentCommander.get( + `/people?select=*&deleted_at=is.null&email=not.is.null&order=name.asc` + ) ); export const getPersonWithEmail = (email: string) => diff --git a/src/api/types/permissions.ts b/src/api/types/permissions.ts index 0b101505c..0d269e374 100644 --- a/src/api/types/permissions.ts +++ b/src/api/types/permissions.ts @@ -12,22 +12,37 @@ export type PermissionTable = { deny?: boolean; object?: string; subject?: string; - subject_type?: "playbook" | "team" | "person" | "notification" | "component"; + subject_type?: + | "group" + | "playbook" + | "team" + | "person" + | "notification" + | "component"; + created_by: string; + updated_by: string; + created_at: string; + updated_at: string; + until?: string; + source?: string; + tags?: Record; + agents?: string[]; + + // Resources object_selector?: Record[]; component_id?: string; config_id?: string; canary_id?: string; - playbook_id?: string; - created_by: string; connection_id?: string; + + // Deprecated fields + // These are subject fields that we do not use anymore. + // Instead we use the "subject" and "subject_type" field. + // Instead of setting person_id, we set subject = and subject_type = "person" person_id?: string; notification_id?: string; team_id?: string; - updated_by: string; - created_at: string; - updated_at: string; - until?: string; - source?: string; + playbook_id?: string; }; export type PermissionAPIResponse = PermissionTable & { diff --git a/src/components/Forms/Formik/FormikConnectionField.tsx b/src/components/Forms/Formik/FormikConnectionField.tsx index afe46e895..33a3d6634 100644 --- a/src/components/Forms/Formik/FormikConnectionField.tsx +++ b/src/components/Forms/Formik/FormikConnectionField.tsx @@ -45,7 +45,7 @@ export default function FormikConnectionField({ return ( (""); @@ -81,12 +85,32 @@ export default function FormikResourceSelectorDropdown({ agent: r.agent || "all" })) ] + : undefined, + connections: connectionResourceSelector + ? [ + ...connectionResourceSelector.map((r) => ({ + ...r, + search: searchText, + name: r.name || "*" + })) + ] + : undefined, + playbooks: playbookResourceSelector + ? [ + ...playbookResourceSelector.map((r) => ({ + ...r, + search: searchText, + name: r.name || "*" + })) + ] : undefined }), [ configResourceSelector, componentResourceSelector, checkResourceSelector, + connectionResourceSelector, + playbookResourceSelector, searchText ] ); @@ -101,7 +125,9 @@ export default function FormikResourceSelectorDropdown({ enabled: configResourceSelector !== undefined || componentResourceSelector !== undefined || - checkResourceSelector !== undefined, // || (field.value === undefined && field.value === "" && field.value === null), + checkResourceSelector !== undefined || + connectionResourceSelector !== undefined || + playbookResourceSelector !== undefined, // || (field.value === undefined && field.value === "" && field.value === null), select: (data) => { if (data?.checks) { return data.checks.map( @@ -159,6 +185,58 @@ export default function FormikResourceSelectorDropdown({ } as FormikSelectDropdownOption; }); } + if (data?.connections) { + return data.connections.map((connection) => { + const tags = Object.values(connection.tags ?? {}).map( + (value) => value + ); + + return { + icon: ( + + ), + value: connection.id, + search: connection.name, + label: ( +
+ {connection.name} + + {connection.type.split("::").at(-1)?.toLocaleLowerCase()} + + {tags.map((tag) => ( + + {tag} + + ))} +
+ ) + } as FormikSelectDropdownOption; + }); + } + if (data?.playbooks) { + return data.playbooks.map((playbook) => { + return { + icon: ( + + ), + value: playbook.id, + search: playbook.name, + label: ( +
+ {playbook.name} +
+ ) + } as FormikSelectDropdownOption; + }); + } }, keepPreviousData: true, staleTime: 0, diff --git a/src/components/Forms/Formik/FormikRoleDropdown.tsx b/src/components/Forms/Formik/FormikRoleDropdown.tsx new file mode 100644 index 000000000..946517f3a --- /dev/null +++ b/src/components/Forms/Formik/FormikRoleDropdown.tsx @@ -0,0 +1,37 @@ +import FormikSelectDropdown from "./FormikSelectDropdown"; + +type FormikRoleDropdownProps = { + name: string; + label?: string; + required?: boolean; + hint?: string; + className?: string; +}; + +export default function FormikRoleDropdown({ + name, + label, + required = false, + hint, + className = "flex flex-col space-y-2 py-2" +}: FormikRoleDropdownProps) { + const options = [ + { label: "Admin", value: "admin" }, + { label: "Editor", value: "editor" }, + { label: "Guest", value: "guest" }, + { label: "Viewer", value: "viewer" }, + { label: "Responder", value: "responder" }, + { label: "Commander", value: "commander" } + ]; + + return ( + + ); +} diff --git a/src/components/Permissions/ManagePermissions/Forms/FormikPermissionSelectResourceFields.tsx b/src/components/Permissions/ManagePermissions/Forms/FormikPermissionSelectResourceFields.tsx index 47527ccaa..67d322b95 100644 --- a/src/components/Permissions/ManagePermissions/Forms/FormikPermissionSelectResourceFields.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/FormikPermissionSelectResourceFields.tsx @@ -1,6 +1,4 @@ import FormikCanaryDropdown from "@flanksource-ui/components/Forms/Formik/FormikCanaryDropdown"; -import FormikConnectionField from "@flanksource-ui/components/Forms/Formik/FormikConnectionField"; -import FormikPlaybooksDropdown from "@flanksource-ui/components/Forms/Formik/FormikPlaybooksDropdown"; import FormikResourceSelectorDropdown from "@flanksource-ui/components/Forms/Formik/FormikResourceSelectorDropdown"; import FormikSelectDropdown from "@flanksource-ui/components/Forms/Formik/FormikSelectDropdown"; import { Switch } from "@flanksource-ui/ui/FormControls/Switch"; @@ -78,7 +76,11 @@ export default function FormikPermissionSelectResourceFields() { )} {switchOption === "Playbook" && ( - + )} {switchOption === "Canary" && ( @@ -86,7 +88,11 @@ export default function FormikPermissionSelectResourceFields() { )} {switchOption === "Connection" && ( - + )} {switchOption === "Global" && ( diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx index cb37e64b6..2c007d607 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx @@ -6,6 +6,7 @@ import { PermissionTable } from "@flanksource-ui/api/types/permissions"; import FormikCheckbox from "@flanksource-ui/components/Forms/Formik/FormikCheckbox"; import FormikSelectDropdown from "@flanksource-ui/components/Forms/Formik/FormikSelectDropdown"; import FormikTextArea from "@flanksource-ui/components/Forms/Formik/FormikTextArea"; +import FormikKeyValueMapField from "@flanksource-ui/components/Forms/Formik/FormikKeyValueMapField"; import CanEditResource from "@flanksource-ui/components/Settings/CanEditResource"; import { toastError, @@ -18,15 +19,16 @@ import { Modal } from "@flanksource-ui/ui/Modal"; import { useMutation } from "@tanstack/react-query"; import { AxiosError } from "axios"; import clsx from "clsx"; -import { Form, Formik } from "formik"; +import { Form, Formik, useFormikContext } from "formik"; import { useMemo } from "react"; import { FaSpinner } from "react-icons/fa"; import { AuthorizationAccessCheck } from "../../AuthorizationAccessCheck"; -import { permissionsActionsList } from "../../PermissionsView"; +import { getActionsForResourceType, ResourceType } from "../../PermissionsView"; import DeletePermission from "./DeletePermission"; import FormikPermissionSelectResourceFields from "./FormikPermissionSelectResourceFields"; import PermissionResource from "./PermissionResource"; import PermissionsSubjectControls from "./PermissionSubjectControls"; +import { useAllAgentNamesQuery } from "../../../../api/query-hooks"; type PermissionFormProps = { onClose: () => void; @@ -34,6 +36,37 @@ type PermissionFormProps = { data?: Partial; }; +function PermissionActionDropdown() { + const { values } = useFormikContext>(); + + const resourceType = useMemo(() => { + if (values.playbook_id) return "playbook"; + if (values.config_id) return "catalog"; + if (values.component_id) return "component"; + if (values.connection_id) return "connection"; + if (values.canary_id) return "canary"; + if (values.object) return "global"; + return undefined; + }, [values]); + + const availableActions = useMemo( + () => getActionsForResourceType(resourceType), + [resourceType] + ); + + if (!resourceType) { + return null; + } + + return ( + + ); +} + export default function PermissionForm({ onClose, isOpen = false, @@ -53,6 +86,17 @@ export default function PermissionForm({ const { user } = useUser(); + const { data: agents } = useAllAgentNamesQuery({}); + + const agentOptions = useMemo( + () => + (agents || []).map((agent) => ({ + label: agent.name || agent.id, + value: agent.id + })), + [agents] + ); + const { isLoading: adding, mutate: add } = useMutation({ mutationFn: async (data: PermissionTable) => { const res = await addPermission({ @@ -123,8 +167,12 @@ export default function PermissionForm({ notification_id: data?.notification_id, person_id: data?.person_id, team_id: data?.team_id, + subject: data?.subject, + subject_type: data?.subject_type, until: data?.until, - source: data?.source || "UI" + source: data?.source || "UI", + tags: data?.tags || {}, + agents: data?.agents || [] }} onSubmit={(v) => { if (!data?.id) { @@ -140,6 +188,7 @@ export default function PermissionForm({ >
+ {isResourceIdProvided ? (
@@ -148,13 +197,24 @@ export default function PermissionForm({ ) : ( )} - - + +
+
+ + +
+
(() => { - if (teamId) return "Team"; - if (personId) return "Person"; - if (notificationId) return "Notification"; + if (teamId || (subjectType === "team" && subject)) return "Team"; + if (personId || (subjectType === "person" && subject)) return "Person"; + if (notificationId || (subjectType === "notification" && subject)) + return "Notification"; + if (subjectType === "group" && subject) return "Role"; + if (subjectType === "playbook" && subject) return "Playbook"; return "Team"; }); useEffect(() => { if (teamId) { setSwitchOption("Team"); - } else if (personId) { + } else if (personId || (subjectType === "person" && subject)) { setSwitchOption("Person"); } else if (notificationId) { setSwitchOption("Notification"); + } else if (subjectType === "group" && subject) { + setSwitchOption("Role"); + } else if (subjectType === "playbook" && subject) { + setSwitchOption("Playbook"); } - }, [teamId, personId, notificationId]); + }, [teamId, personId, notificationId, subjectType, subject]); return (
@@ -37,35 +48,48 @@ export default function PermissionsSubjectControls() {
{ setSwitchOption(v); + + // These are old deprecated values that must never be set anymore. + setFieldValue("person_id", undefined); + setFieldValue("notification_id", undefined); + setFieldValue("team_id", undefined); + if (v === "Team") { - setFieldValue("person_id", undefined); - setFieldValue("notification_id", undefined); + setFieldValue("subject_type", "team"); } else if (v === "Person") { - setFieldValue("team_id", undefined); - setFieldValue("notification_id", undefined); - } else { - setFieldValue("team_id", undefined); - setFieldValue("person_id", undefined); + setFieldValue("subject_type", "person"); + } else if (v === "Notification") { + setFieldValue("subject_type", "notification"); + } else if (v === "Role") { + setFieldValue("subject_type", "group"); + } else if (v === "Playbook") { + setFieldValue("subject_type", "playbook"); } }} />
{switchOption === "Team" && ( - + )} {switchOption === "Person" && ( - + )} {switchOption === "Notification" && ( - + + )} + {switchOption === "Role" && ( + + )} + {switchOption === "Playbook" && ( + )}
diff --git a/src/components/Permissions/PermissionsTable.tsx b/src/components/Permissions/PermissionsTable.tsx index ed06247f0..553821160 100644 --- a/src/components/Permissions/PermissionsTable.tsx +++ b/src/components/Permissions/PermissionsTable.tsx @@ -14,6 +14,10 @@ import { BsBan } from "react-icons/bs"; import { Link } from "react-router-dom"; import { Badge } from "@flanksource-ui/ui/Badge/Badge"; +const formatTagText = (key: string, value: string): string => { + return `${key}: ${value}`; +}; + const permissionsTableColumns: MRT_ColumnDef[] = [ { header: "Subject", @@ -21,18 +25,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ Cell: ({ row }) => { const { team, group, person, subject, notification, playbook } = row.original; - const { tags, agents } = row.original; - const rlsFilter = []; - - if (tags && Object.keys(tags).length > 0) { - rlsFilter.push(tags); - } - - if (agents && agents.length > 0) { - rlsFilter.push({ agents: agents }); - } - - const rlsPayload = rlsFilter.length > 0 ? JSON.stringify(rlsFilter) : ""; if (group) { const groupName = group.name || subject; @@ -43,7 +35,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ {groupName} {/* Add link to permission group when we have a permission group page */} - {rlsPayload && }
); } @@ -53,7 +44,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [
{person.name} - {rlsPayload && }
); } @@ -63,7 +53,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [
{team.name} - {rlsPayload && }
); } @@ -81,7 +70,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ notification.name} - {rlsPayload && } ); } @@ -100,7 +88,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ - {rlsPayload && } ); } @@ -121,36 +108,90 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ const connection = row.original.connection; const object = row.original.object; const objectSelector = row.original.object_selector; + const { tags, agents } = row.original; + + const renderRlsBadges = (): JSX.Element[] => { + const badges: JSX.Element[] = []; + + // Add tag badges + if (tags && Object.keys(tags).length > 0) { + Object.entries(tags).forEach(([key, value]) => { + badges.push( + + ); + }); + } + + // Add agent badges + if (agents && agents.length > 0) { + agents.forEach((agent, index) => { + badges.push( + + ); + }); + } + + return badges; + }; + + const rlsBadges = renderRlsBadges(); if (objectSelector) { return ( - - {JSON.stringify(objectSelector)} - +
+ + {JSON.stringify(objectSelector)} + + {rlsBadges.length > 0 && ( +
{rlsBadges}
+ )} +
); } if (object) { - return permissionObjectList.find((o) => o.value === object)?.label; + return ( +
+ + {permissionObjectList.find((o) => o.value === object)?.label} + + {rlsBadges.length > 0 && ( +
{rlsBadges}
+ )} +
+ ); } return ( -
- {config && } - {/* {check && } */} - {playbook && } - {component && ( - +
+
+ {config && } + {/* {check && } */} + {playbook && } + {component && ( + + )} + {connection && } +
+ {rlsBadges.length > 0 && ( +
{rlsBadges}
)} - {connection && }
); } diff --git a/src/components/Permissions/PermissionsView.tsx b/src/components/Permissions/PermissionsView.tsx index 6712e5474..a00faadc2 100644 --- a/src/components/Permissions/PermissionsView.tsx +++ b/src/components/Permissions/PermissionsView.tsx @@ -22,15 +22,54 @@ export const permissionsActionsList: FormikSelectDropdownOption[] = [ { value: "*", label: "*" }, { value: "create,read,update,delete", label: "create,read,update,delete" }, { value: "playbook:run", label: "playbook:run" }, - { value: "playbook:approve", label: "playbook:approve" } + { value: "playbook:approve", label: "playbook:approve" }, + { value: "playbook:*", label: "playbook:*" } ]; +const commonActions: FormikSelectDropdownOption[] = [ + { value: "read", label: "read" }, + { value: "update", label: "update" }, + { value: "create", label: "create" }, + { value: "delete", label: "delete" }, + { value: "*", label: "*" }, + { value: "create,read,update,delete", label: "create,read,update,delete" } +]; + +const playbookSpecificActions: FormikSelectDropdownOption[] = [ + { value: "playbook:run", label: "playbook:run" }, + { value: "playbook:approve", label: "playbook:approve" }, + { value: "playbook:*", label: "playbook:*" } +]; + +export type ResourceType = + | "catalog" + | "component" + | "playbook" + | "connection" + | "canary" + | "global"; + +export function getActionsForResourceType( + resourceType?: ResourceType +): FormikSelectDropdownOption[] { + if (!resourceType) { + return []; + } + + if (resourceType === "playbook") { + return [...commonActions, ...playbookSpecificActions]; + } + + return commonActions; +} + type PermissionsViewProps = { permissionRequest: FetchPermissionsInput; setIsLoading?: (isLoading: boolean) => void; hideResourceColumn?: boolean; newPermissionData?: Partial; showAddPermission?: boolean; + onRefetch?: (refetch: () => void) => void; }; export default function PermissionsView({ @@ -38,7 +77,8 @@ export default function PermissionsView({ setIsLoading = () => {}, hideResourceColumn = false, newPermissionData, - showAddPermission = false + showAddPermission = false, + onRefetch }: PermissionsViewProps) { const [selectedPermission, setSelectedPermission] = useState(); @@ -66,6 +106,12 @@ export default function PermissionsView({ setIsLoading(isLoading); }, [isLoading, setIsLoading]); + useEffect(() => { + if (onRefetch) { + onRefetch(refetch); + } + }, [onRefetch, refetch]); + const totalEntries = data?.totalEntries || 0; const pageCount = totalEntries ? Math.ceil(totalEntries / pageSize) : 1; const permissions = data?.data || []; diff --git a/src/pages/Settings/PermissionsPage.tsx b/src/pages/Settings/PermissionsPage.tsx index c601e9e95..48d90d79e 100644 --- a/src/pages/Settings/PermissionsPage.tsx +++ b/src/pages/Settings/PermissionsPage.tsx @@ -8,12 +8,11 @@ import { } from "@flanksource-ui/ui/BreadcrumbNav"; import { Head } from "@flanksource-ui/ui/Head"; import { SearchLayout } from "@flanksource-ui/ui/Layout/SearchLayout"; -import { useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; +import { useState, useRef } from "react"; export function PermissionsPage() { const [isLoading, setIsLoading] = useState(false); - const client = useQueryClient(); + const refetchFunctionRef = useRef<(() => void) | null>(null); return ( <> @@ -38,11 +37,7 @@ export function PermissionsPage() { ]} /> } - onRefresh={() => - client.refetchQueries({ - queryKey: ["permissions"] - }) - } + onRefresh={() => refetchFunctionRef.current?.()} contentClass="p-0 h-full" loading={isLoading} > @@ -51,6 +46,9 @@ export function PermissionsPage() { { + refetchFunctionRef.current = refetch; + }} />