diff --git a/ee/identitymanager/identity_managers/keycloak/keycloak_authverifier.py b/ee/identitymanager/identity_managers/keycloak/keycloak_authverifier.py index 7b6717e53..ab8dafe33 100644 --- a/ee/identitymanager/identity_managers/keycloak/keycloak_authverifier.py +++ b/ee/identitymanager/identity_managers/keycloak/keycloak_authverifier.py @@ -103,3 +103,21 @@ def _authorize(self, authenticated_entity: AuthenticatedEntity) -> None: except Exception: raise HTTPException(status_code=401, detail="Permission check failed") return allowed + + def authorize_resource( + self, resource_type, resource_id, authenticated_entity: AuthenticatedEntity + ) -> None: + # use Keycloak's UMA to authorize + try: + permission = UMAPermission( + resource=resource_id, + ) + allowed = self.keycloak_uma.permissions_check( + token=authenticated_entity.token, permissions=[permission] + ) + if not allowed: + raise HTTPException(status_code=401, detail="Permission check failed") + # secure fallback + except Exception: + raise HTTPException(status_code=401, detail="Permission check failed") + return allowed diff --git a/keep-ui/app/alerts/alerts.tsx b/keep-ui/app/alerts/alerts.tsx index e780179ed..e3b808c02 100644 --- a/keep-ui/app/alerts/alerts.tsx +++ b/keep-ui/app/alerts/alerts.tsx @@ -15,6 +15,8 @@ import { ViewAlertModal } from "./ViewAlertModal"; import { useRouter, useSearchParams } from "next/navigation"; import AlertChangeStatusModal from "./alert-change-status-modal"; import { useAlertPolling } from "utils/hooks/usePusher"; +import NotFound from "@/app/not-found"; +import NotAuthorized from "@/app/not-authorized"; const defaultPresets: Preset[] = [ { @@ -25,7 +27,7 @@ const defaultPresets: Preset[] = [ is_noisy: false, alerts_count: 0, should_do_noise_now: false, - tags: [] + tags: [], }, { id: "dismissed", @@ -35,7 +37,7 @@ const defaultPresets: Preset[] = [ is_noisy: false, alerts_count: 0, should_do_noise_now: false, - tags: [] + tags: [], }, { id: "groups", @@ -45,7 +47,7 @@ const defaultPresets: Preset[] = [ is_noisy: false, alerts_count: 0, should_do_noise_now: false, - tags: [] + tags: [], }, { id: "without-incident", @@ -55,7 +57,7 @@ const defaultPresets: Preset[] = [ is_noisy: false, alerts_count: 0, should_do_noise_now: false, - tags: [] + tags: [], }, ]; @@ -97,11 +99,13 @@ export default function Alerts({ presetName }: AlertsProps) { const selectedPreset = presets.find( (preset) => preset.name.toLowerCase() === decodeURIComponent(presetName) ); + const { data: pollAlerts } = useAlertPolling(); const { data: alerts = [], isLoading: isAsyncLoading, mutate: mutateAlerts, + error: alertsError, } = usePresetAlerts(selectedPreset ? selectedPreset.name : ""); useEffect(() => { const fingerprint = searchParams?.get("alertPayloadFingerprint"); @@ -119,8 +123,12 @@ export default function Alerts({ presetName }: AlertsProps) { } }, [mutateAlerts, pollAlerts]); - if (selectedPreset === undefined) { - return null; + if (!selectedPreset) { + return ; + } + + if (alertsError) { + return ; } return ( diff --git a/keep-ui/app/not-authorized.tsx b/keep-ui/app/not-authorized.tsx new file mode 100644 index 000000000..1127e686c --- /dev/null +++ b/keep-ui/app/not-authorized.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Link } from "@/components/ui"; +import { Title, Button, Subtitle } from "@tremor/react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; + +export default function NotAuthorized() { + const router = useRouter(); + return ( +
+ 403 Not Authorized + + You do not have permission to access this page. If you believe this is + an error, please contact us on{" "} + + Slack + + + Keep + +
+ ); +} diff --git a/keep-ui/app/not-found.tsx b/keep-ui/app/not-found.tsx index 2cc1bbadd..643fbc35f 100644 --- a/keep-ui/app/not-found.tsx +++ b/keep-ui/app/not-found.tsx @@ -9,7 +9,7 @@ export default function NotFound() { const router = useRouter(); return (
- Page not found + 404 Page not found If you believe this is an error, please contact us on{" "} = { + feed: { + path: "/alerts/feed", + icon: AiOutlineSwap, + label: "Feed", + }, + "without-incident": { + path: "/alerts/without-incident", + icon: MdFlashOff, + label: "Without Incident", + }, + dismissed: { + path: "/alerts/dismissed", + icon: SilencedDoorbellNotification, + label: "Dismissed", + }, +}; + export const AlertsLinks = ({ session }: AlertsLinksProps) => { const [isTagModalOpen, setIsTagModalOpen] = useState(false); const [selectedTags, setSelectedTags] = useState([]); @@ -34,8 +58,10 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { }); const [staticPresets, setStaticPresets] = useState(staticPresetsOrderFromLS); - - const [storedTags, setStoredTags] = useLocalStorage("selectedTags", []); + const [storedTags, setStoredTags] = useLocalStorage( + "selectedTags", + [] + ); useEffect(() => { if ( @@ -52,14 +78,6 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { } }, []); - const mainPreset = staticPresets.find((preset) => preset.name === "feed"); - const dismissedPreset = staticPresets.find( - (preset) => preset.name === "dismissed" - ); - const withoutIncidentPreset = staticPresets.find( - (preset) => preset.name === "without-incident" - ); - const handleTagSelect = ( newValue: MultiValue<{ value: string; label: string }>, actionMeta: ActionMeta<{ value: string; label: string }> @@ -78,6 +96,26 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { setIsTagModalOpen(true); }; + const renderPresetLink = (presetName: string) => { + const preset = staticPresets.find((p) => p.name === presetName); + const config = PRESET_CONFIGS[presetName]; + + if (!preset || !config) return null; + + return ( +
  • + + {config.label} + +
  • + ); + }; + return ( <> @@ -93,7 +131,8 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { "absolute left-full ml-2 cursor-pointer text-gray-400 transition-opacity", { "opacity-100 text-orange-500": selectedTags.length > 0, - "opacity-0 group-hover:opacity-100 group-hover:text-orange-500": selectedTags.length === 0 + "opacity-0 group-hover:opacity-100 group-hover:text-orange-500": + selectedTags.length === 0, } )} size={16} @@ -114,42 +153,15 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { as="ul" className="space-y-2 overflow-auto min-w-[max-content] p-2 pr-4" > -
  • - - Feed - -
  • -
  • - - Without Incident - -
  • + {renderPresetLink("feed")} + {renderPresetLink("without-incident")} {session && ( )} -
  • - - Dismissed - -
  • + {renderPresetLink("dismissed")} )} @@ -161,7 +173,9 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { >
    Select tags to watch - Customize your presets list by watching specific tags. + + Customize your presets list by watching specific tags. + ({ value: tag, diff --git a/keep-ui/utils/hooks/useAlerts.ts b/keep-ui/utils/hooks/useAlerts.ts index 8296c513d..f14c06b00 100644 --- a/keep-ui/utils/hooks/useAlerts.ts +++ b/keep-ui/utils/hooks/useAlerts.ts @@ -61,6 +61,7 @@ export const useAlerts = () => { data: alertsFromEndpoint = [], mutate, isLoading, + error, } = useAllAlerts(presetName, options); useEffect(() => { @@ -85,6 +86,7 @@ export const useAlerts = () => { data: Array.from(alertsMap.values()), mutate: mutate, isLoading: isLoading, + error: error, }; }; diff --git a/keep/api/routes/preset.py b/keep/api/routes/preset.py index 840d51e9e..74fe69ff6 100644 --- a/keep/api/routes/preset.py +++ b/keep/api/routes/preset.py @@ -391,7 +391,7 @@ def update_preset( @router.get( "/{preset_name}/alerts", - description="Get a preset for tenant", + description="Get the alerts of a preset", ) def get_preset_alerts( request: Request, @@ -429,6 +429,19 @@ def get_preset_alerts( preset_dto = PresetDto(**preset.to_dict()) else: preset_dto = PresetDto(**preset.dict()) + + # get all preset ids that the user has access to + identity_manager = IdentityManagerFactory.get_identity_manager( + authenticated_entity.tenant_id + ) + # Note: if no limitations (allowed_preset_ids is []), then all presets are allowed + allowed_preset_ids = identity_manager.get_user_permission_on_resource_type( + resource_type="preset", + authenticated_entity=authenticated_entity, + ) + if allowed_preset_ids and str(preset_dto.id) not in allowed_preset_ids: + raise HTTPException(403, "Not authorized to access this preset") + search_engine = SearchEngine(tenant_id=tenant_id) preset_alerts = search_engine.search_alerts(preset_dto.query) logger.info("Got preset alerts", extra={"preset_name": preset_name})