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
+
+
+
+ {
+ router.back();
+ }}
+ color="orange"
+ variant="secondary"
+ >
+ Go back
+
+
+ );
+}
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})