diff --git a/ee/identitymanager/identity_managers/keycloak/keycloak_identitymanager.py b/ee/identitymanager/identity_managers/keycloak/keycloak_identitymanager.py index 1646a8d4a..0fa6c9b14 100644 --- a/ee/identitymanager/identity_managers/keycloak/keycloak_identitymanager.py +++ b/ee/identitymanager/identity_managers/keycloak/keycloak_identitymanager.py @@ -27,7 +27,7 @@ class KeycloakIdentityManager(BaseIdentityManager): - + """ RESOURCES = { "preset": { "table": "preset", @@ -38,6 +38,9 @@ class KeycloakIdentityManager(BaseIdentityManager): "uid": "id", }, } + """ + + RESOURCES = {} def __init__(self, tenant_id, context_manager: ContextManager, **kwargs): super().__init__(tenant_id, context_manager, **kwargs) @@ -409,6 +412,8 @@ def create_user( "email": user_email, "enabled": True, "firstName": user_name, + "lastName": user_name, + "emailVerified": True, } if password: user_data["credentials"] = [ @@ -628,7 +633,7 @@ def create_user_policy(self, perm, permission: ResourcePermission) -> None: # TODO: this is not efficient, we should cache this users = self.keycloak_admin.get_users({}) user = next( - (user for user in users if user["email"] == perm.id), + (user for user in users if user.get("email") == perm.id), None, ) if not user: @@ -663,6 +668,13 @@ def create_user_policy(self, perm, permission: ResourcePermission) -> None: return policy_id def create_group_policy(self, perm, permission: ResourcePermission) -> None: + group_name = perm.id + group = self.keycloak_admin.get_groups(query={"search": perm.id}) + if not group or len(group) > 1: + self.logger.error("Problem with group - should be 1 but got %s", len(group)) + raise HTTPException(status_code=400, detail="Problem with group") + group = group[0] + group_id = group["id"] resp = self.keycloak_admin.connection.raw_post( f"{self.admin_url}/authz/resource-server/policy/group", data=json.dumps( @@ -670,12 +682,13 @@ def create_group_policy(self, perm, permission: ResourcePermission) -> None: "name": f"Allow group {perm.id} to access resource type {permission.resource_type} with name {permission.resource_name}", "description": json.dumps( { - "group_id": perm.id, + "group_name": group_name, + "group_id": group_id, "resource_id": permission.resource_id, } ), "logic": "POSITIVE", - "groups": [{"id": perm.id, "extendChildren": False}], + "groups": [{"id": group_id, "extendChildren": False}], "groupsClaim": "", } ), @@ -838,12 +851,14 @@ def get_permissions(self) -> list[ResourcePermission]: if resource_id not in resources_to_policies: resources_to_policies[resource_id] = [] if policy.get("type") == "user": + user_email = details.get("user_email") resources_to_policies[resource_id].append( - {"id": details.get("user_email"), "type": "user"} + {"id": user_email, "type": "user"} ) else: + group_name = details.get("group_name") resources_to_policies[resource_id].append( - {"id": details["group_id"], "type": "group"} + {"id": group_name, "type": "group"} ) permissions_dto = [] for resource in resources: @@ -858,6 +873,7 @@ def get_permissions(self) -> list[ResourcePermission]: permissions=[ PermissionEntity( id=policy["id"], + name=policy.get("name", ""), type=policy["type"], ) for policy in resources_to_policies.get(resource_id, []) @@ -868,6 +884,9 @@ def get_permissions(self) -> list[ResourcePermission]: except KeycloakGetError as e: self.logger.error("Failed to fetch permissions from Keycloak: %s", str(e)) raise HTTPException(status_code=500, detail="Failed to fetch permissions") + except Exception as e: + self.logger.error("Failed to fetch permissions from Keycloak: %s", str(e)) + raise HTTPException(status_code=500, detail="Failed to fetch permissions") # TODO: this should use UMA and not evaluation since evaluation needs admin access def get_user_permission_on_resource_type( @@ -916,6 +935,10 @@ def get_user_permission_on_resource_type( for result in results if result["status"] == "PERMIT" ] + # there is some bug/limitation in keycloak where if the resource_type does not exist, it returns + # all other objects, so lets handle it by checking if the word "with" is one of the results name + if any("with" in result for result in allowed_resources_ids): + return [] return allowed_resources_ids except Exception as e: self.logger.error( diff --git a/keep-ui/app/settings/auth/permissions-sidebar.tsx b/keep-ui/app/settings/auth/permissions-sidebar.tsx index 4c4f4ed5f..db6aac975 100644 --- a/keep-ui/app/settings/auth/permissions-sidebar.tsx +++ b/keep-ui/app/settings/auth/permissions-sidebar.tsx @@ -1,48 +1,47 @@ -import { Fragment, useEffect, useState } from "react"; +import { Fragment, useEffect } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { Text, Button, Badge, - TextInput, MultiSelect, MultiSelectItem, Callout, + Title, + Subtitle, } from "@tremor/react"; import { IoMdClose } from "react-icons/io"; +import { User, Group, Role } from "app/settings/models"; import { useForm, Controller, SubmitHandler, FieldValues, } from "react-hook-form"; -import { Permission, User, Group, Role } from "app/settings/models"; +import "./multiselect.css"; interface PermissionSidebarProps { isOpen: boolean; toggle: VoidFunction; - accessToken: string; - selectedPermission: Permission | null; - resourceTypes: string[]; - resources: { [key: string]: any[] }; + selectedResource: any; entityOptions: { user: User[]; group: Group[]; role: Role[]; }; - onSavePermission: (permissionData: any) => Promise; + onSavePermissions: ( + resourceId: string, + assignments: string[] + ) => Promise; isDisabled?: boolean; } const PermissionSidebar = ({ isOpen, toggle, - accessToken, - selectedPermission, - resourceTypes, - resources, + selectedResource, entityOptions, - onSavePermission, + onSavePermissions, isDisabled = false, }: PermissionSidebarProps) => { const { @@ -50,30 +49,30 @@ const PermissionSidebar = ({ handleSubmit, setValue, reset, - watch, formState: { errors, isDirty }, clearErrors, setError, } = useForm({ defaultValues: { - resourceType: "", - resourceId: "", - entity: "", + assignments: [], }, }); - const [isSubmitting, setIsSubmitting] = useState(false); - const selectedResourceType = watch("resourceType"); + useEffect(() => { + if (isOpen && selectedResource) { + setValue("assignments", selectedResource.assignments || []); + clearErrors(); + } + }, [selectedResource, setValue, isOpen, clearErrors]); - // Combine all entity options into a single array with type labels const getAllEntityOptions = () => { const options = []; for (const user of entityOptions.user) { options.push({ - id: `user-${user.name || user.email}`, - label: `${user.name || user.email} (User)`, - value: `user_${user.name || user.email}`, + id: `user-${user.email}`, + label: `${user.email || user.name} (User)`, + value: `user_${user.email}`, // Format: type_id }); } @@ -81,60 +80,34 @@ const PermissionSidebar = ({ options.push({ id: `group-${group.id}`, label: `${group.name} (Group)`, - value: `group_${group.id}`, + value: `group_${group.name}`, // Format: type_id }); } - + /* Support roles in the future for (const role of entityOptions.role) { options.push({ id: `role-${role.id}`, label: `${role.name} (Role)`, - value: `role_${role.id}`, + value: `role_${role.id}`, // Format: type_id }); } + */ return options; }; - useEffect(() => { - if (isOpen) { - if (selectedPermission) { - setValue("resourceType", selectedPermission.type); - setValue("resourceId", selectedPermission.resource_id); - setValue("entity", selectedPermission.entity_id); - } else { - reset({ - resourceType: "", - resourceId: "", - entity: "", - }); - } - clearErrors(); - } - }, [selectedPermission, setValue, isOpen, reset, clearErrors]); - const onSubmit: SubmitHandler = async (data) => { - setIsSubmitting(true); - clearErrors(); - try { - await onSavePermission({ - type: data.resourceType, - resource_id: data.resourceId, - entity_id: data.entity, - }); + await onSavePermissions(selectedResource.id, data.assignments); handleClose(); } catch (error) { setError("root.serverError", { - message: "Failed to save permission", + message: "Failed to save permissions", }); - } finally { - setIsSubmitting(false); } }; const handleClose = () => { - setIsSubmitting(false); clearErrors(); reset(); toggle(); @@ -166,7 +139,7 @@ const PermissionSidebar = ({
- {selectedPermission ? "Edit Permission" : "Add Permission"} + Manage Permissions Beta @@ -175,92 +148,36 @@ const PermissionSidebar = ({
+
-
- - ( - { - if (value.length > 0) { - field.onChange(value[0]); - // Reset resourceId when type changes - setValue("resourceId", ""); - } - }} - className="custom-multiselect" - disabled={isDisabled || !!selectedPermission} - > - {resourceTypes.map((type) => ( - - {type} - - ))} - - )} - /> +
+ Resource + + {selectedResource?.name} +
- {selectedResourceType && ( -
- - ( - { - if (value.length > 0) { - field.onChange(value[0]); - } - }} - className="custom-multiselect" - disabled={isDisabled || !!selectedPermission} - > - {resources[selectedResourceType]?.map((resource) => ( - - {resource.name} - - ))} - - )} - /> -
- )} +
+ Type + + {selectedResource?.type} + +
-
- +
+ Assign To ( { - if (value.length > 0) { - field.onChange(value[0]); - } - }} + {...field} + onValueChange={(value) => field.onChange(value)} + value={field.value as string[]} className="custom-multiselect" disabled={isDisabled} > @@ -278,14 +195,14 @@ const PermissionSidebar = ({ {errors.root?.serverError && ( {errors.root.serverError.message} )} -
+
)}
diff --git a/keep-ui/app/settings/auth/permissions-tab.tsx b/keep-ui/app/settings/auth/permissions-tab.tsx index 2016cc8b1..6a98e5445 100644 --- a/keep-ui/app/settings/auth/permissions-tab.tsx +++ b/keep-ui/app/settings/auth/permissions-tab.tsx @@ -1,24 +1,34 @@ import React, { useState, useEffect } from "react"; -import { Title, Subtitle, Card, TextInput, Button } from "@tremor/react"; -import { FaUserLock } from "react-icons/fa"; -import { Permission, User, Group, Role } from "app/settings/models"; -import { useApiUrl } from "utils/hooks/useConfig"; -import { useSession } from "next-auth/react"; -import Loading from "app/loading"; -import { PermissionsTable } from "./permissions-table"; -import PermissionSidebar from "./permissions-sidebar"; +import { Title, Subtitle, Card, TextInput } from "@tremor/react"; import { usePermissions } from "utils/hooks/usePermissions"; import { useUsers } from "utils/hooks/useUsers"; import { useGroups } from "utils/hooks/useGroups"; import { useRoles } from "utils/hooks/useRoles"; import { usePresets } from "utils/hooks/usePresets"; import { useIncidents } from "utils/hooks/useIncidents"; +import Loading from "app/loading"; +import { PermissionsTable } from "./permissions-table"; +import PermissionSidebar from "./permissions-sidebar"; +import { useApiUrl } from "utils/hooks/useConfig"; +import { useSession } from "next-auth/react"; interface Props { accessToken: string; isDisabled?: boolean; } +interface PermissionEntity { + id: string; + type: string; // 'user' or 'group' or 'role' +} + +interface ResourcePermission { + resource_id: string; + resource_name: string; + resource_type: string; + permissions: PermissionEntity[]; +} + export default function PermissionsTab({ accessToken, isDisabled = false, @@ -26,8 +36,7 @@ export default function PermissionsTab({ const { data: session } = useSession(); const apiUrl = useApiUrl(); const [isSidebarOpen, setIsSidebarOpen] = useState(false); - const [selectedPermission, setSelectedPermission] = - useState(null); + const [selectedResource, setSelectedResource] = useState(null); const [filter, setFilter] = useState(""); // Fetch data using custom hooks @@ -40,116 +49,113 @@ export default function PermissionsTab({ const { data: incidents } = useIncidents(); const [loading, setLoading] = useState(true); + const [resources, setResources] = useState([]); - // Define available resource types and their corresponding resources - const resourceTypes = ["preset", "incident"]; - const resources = { - preset: presets || [], - incident: incidents || [], - }; - - // Define entity types for permission assignment - const entityTypes = [ - { id: "user", name: "User" }, - { id: "group", name: "Group" }, - { id: "role", name: "Role" }, - ]; - - // Entity options for assignment - const entityOptions = { - user: users || [], - group: groups || [], - role: roles || [], - }; - + // Combine all resources and their permissions useEffect(() => { - if (permissions && users && groups && roles && presets && incidents) { + if (presets && incidents && permissions) { + const allResources = [ + ...(presets?.map((preset) => ({ + id: preset.id, + name: preset.name, + type: "preset", + assignments: + permissions + ?.filter((p) => p.resource_id === preset.id) + .flatMap((p) => + p.permissions.map((perm) => `${perm.type}_${perm.id}`) + ) || [], + })) || []), + ...(incidents?.items.map((incident) => ({ + id: incident.id, + name: incident.user_generated_name || incident.ai_generated_name, + type: "incident", + assignments: + permissions + ?.filter((p) => p.resource_id === incident.id) + .flatMap((p) => + p.permissions.map((perm) => `${perm.type}_${perm.id}`) + ) || [], + })) || []), + ]; + // Compare current and new resources to prevent unnecessary updates + const resourcesString = JSON.stringify(allResources); + const currentResourcesString = JSON.stringify(resources); + + if (resourcesString !== currentResourcesString) { + setResources(allResources); + } setLoading(false); } - }, [permissions, users, groups, roles, presets, incidents]); + }, [presets, incidents, permissions]); - const handleSavePermission = async (permissionData: any) => { + const handleSavePermissions = async ( + resourceId: string, + assignments: string[] + ) => { try { - const method = selectedPermission ? "PUT" : "POST"; - const url = selectedPermission - ? `${apiUrl}/auth/permissions/${selectedPermission.id}` - : `${apiUrl}/auth/permissions`; + // Convert assignments array to PermissionEntity array + const permissions: PermissionEntity[] = assignments.map((assignment) => { + // Parse the assignment string to get type and id + const [type, ...idParts] = assignment.split("_"); + return { + id: idParts.join("_"), // Rejoin in case the id itself contains underscores + type: type, + }; + }); - const response = await fetch(url, { - method, + // Find the resource details + const resource = resources.find((r) => r.id === resourceId); + if (!resource) { + throw new Error("Resource not found"); + } + + // Create the resource permission object + const resourcePermission: ResourcePermission[] = [ + { + resource_id: resource.id, + resource_name: resource.name, + resource_type: resource.type, + permissions: permissions, + }, + ]; + + // Send to the backend + const response = await fetch(`${apiUrl}/auth/permissions`, { + method: "POST", headers: { Authorization: `Bearer ${session?.accessToken}`, "Content-Type": "application/json", }, - body: JSON.stringify(permissionData), + body: JSON.stringify(resourcePermission), }); - if (response.ok) { - await mutatePermissions(); - setIsSidebarOpen(false); + if (!response.ok) { + throw new Error("Failed to save permissions"); } - } catch (error) { - console.error("Error saving permission:", error); - } - }; - const handleDeletePermission = async ( - permissionId: string, - event: React.MouseEvent - ) => { - event.stopPropagation(); - if (window.confirm("Are you sure you want to delete this permission?")) { - try { - const response = await fetch( - `${apiUrl}/auth/permissions/${permissionId}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${session?.accessToken}`, - }, - } - ); - - if (response.ok) { - await mutatePermissions(); - } - } catch (error) { - console.error("Error deleting permission:", error); - } + await mutatePermissions(); + } catch (error) { + console.error("Error saving permissions:", error); + throw error; } }; if (loading) return ; - const filteredPermissions = - permissions?.filter( - (permission) => - permission.name?.toLowerCase().includes(filter.toLowerCase()) - ) || []; + const filteredResources = resources.filter((resource) => + resource.name.toLowerCase().includes(filter.toLowerCase()) + ); return (
-
-
- Permissions Management - Manage permissions for resources -
- +
+ Permissions Management + Manage permissions for resources
setFilter(e.target.value)} className="mb-4" @@ -158,12 +164,11 @@ export default function PermissionsTab({ { - setSelectedPermission(permission); + resources={filteredResources} + onRowClick={(resource) => { + setSelectedResource(resource); setIsSidebarOpen(true); }} - onDeletePermission={handleDeletePermission} isDisabled={isDisabled} /> @@ -171,15 +176,13 @@ export default function PermissionsTab({ setIsSidebarOpen(false)} - accessToken={accessToken} - selectedPermission={selectedPermission} - resourceTypes={resourceTypes} - resources={{ - preset: resources.preset, - incident: Array.isArray(resources.incident) ? resources.incident : [], + selectedResource={selectedResource} + entityOptions={{ + user: users || [], + group: groups || [], + role: roles || [], }} - entityOptions={entityOptions} - onSavePermission={handleSavePermission} + onSavePermissions={handleSavePermissions} isDisabled={isDisabled} />
diff --git a/keep-ui/app/settings/auth/permissions-table.tsx b/keep-ui/app/settings/auth/permissions-table.tsx index 5e6ed49b9..ea44a0415 100644 --- a/keep-ui/app/settings/auth/permissions-table.tsx +++ b/keep-ui/app/settings/auth/permissions-table.tsx @@ -7,80 +7,73 @@ import { TableBody, TableCell, Badge, - Button, Text, } from "@tremor/react"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { Permission } from "app/settings/models"; interface PermissionsTableProps { - permissions: Permission[]; - onRowClick: (permission: Permission) => void; - onDeletePermission: (resourceId: string, event: React.MouseEvent) => void; + resources: any[]; + onRowClick: (resource: any) => void; isDisabled?: boolean; } export function PermissionsTable({ - permissions, + resources, onRowClick, - onDeletePermission, isDisabled = false, }: PermissionsTableProps) { return ( - Resource Name - Resource Type - Assigned To - + Resource Name + Resource Type + Assigned To - {permissions.map((permission) => ( + {resources.map((resource) => ( !isDisabled && onRowClick(permission)} + onClick={() => !isDisabled && onRowClick(resource)} > - -
- {permission.name} -
+ + {resource.name} - + - {permission.type} + {resource.type} - +
- {permission.permissions?.slice(0, 5).map((perm, index) => ( - - {perm.id} - - ))} - {permission.permissions?.length > 5 && ( - - +{permission.permissions.length - 5} more - + {resource.assignments.length > 0 ? ( + <> + {resource.assignments + .slice(0, 5) + .map((assignment: string, index: number) => { + const [type, ...rest] = assignment.split("_"); + const displayId = rest.join("_"); + return ( + + {`${displayId} (${type})`} + + ); + })} + {resource.assignments.length > 5 && ( + + +{resource.assignments.length - 5} more + + )} + + ) : ( + No assignments )}
- - {!isDisabled && ( -