diff --git a/keep-ui/features/incident-list/ui/incident-list.tsx b/keep-ui/features/incident-list/ui/incident-list.tsx
index bb6542035..878cc2a24 100644
--- a/keep-ui/features/incident-list/ui/incident-list.tsx
+++ b/keep-ui/features/incident-list/ui/incident-list.tsx
@@ -108,7 +108,7 @@ export function IncidentList({
if (incidentsError) {
return (
-
+
);
}
diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json
index 99aa8c7c5..dcbf65583 100644
--- a/keep-ui/package-lock.json
+++ b/keep-ui/package-lock.json
@@ -79,6 +79,7 @@
"cookie": "^0.7.0",
"cosmiconfig": "^7.1.0",
"cross-spawn": "^7.0.3",
+ "crypto-js": "^4.2.0",
"css-unit-converter": "^1.1.2",
"cssesc": "^3.0.0",
"csstype": "^3.1.2",
@@ -267,6 +268,7 @@
"picomatch": "^2.3.1",
"pify": "^2.3.0",
"pirates": "^4.0.5",
+ "pkce-challenge": "^4.1.0",
"plotly.js": "^2.32.0",
"postcss": "^8.4.31",
"postcss-import": "^15.1.0",
@@ -377,6 +379,7 @@
"devDependencies": {
"@next/bundle-analyzer": "^14.2.15",
"@tailwindcss/typography": "^0.5.12",
+ "@types/crypto-js": "^4.2.2",
"@types/d3-time-format": "^4.0.3",
"@types/js-cookie": "^3.0.3",
"@types/js-yaml": "^4.0.5",
@@ -7000,6 +7003,13 @@
"url": "https://opencollective.com/turf"
}
},
+ "node_modules/@types/crypto-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
+ "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@@ -9256,6 +9266,12 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+ "license": "MIT"
+ },
"node_modules/css-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
@@ -16957,6 +16973,15 @@
"node": ">= 6"
}
},
+ "node_modules/pkce-challenge": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz",
+ "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
"node_modules/plotly.js": {
"version": "2.32.0",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.32.0.tgz",
diff --git a/keep-ui/package.json b/keep-ui/package.json
index 4eba2a13a..66f37c350 100644
--- a/keep-ui/package.json
+++ b/keep-ui/package.json
@@ -80,6 +80,7 @@
"cookie": "^0.7.0",
"cosmiconfig": "^7.1.0",
"cross-spawn": "^7.0.3",
+ "crypto-js": "^4.2.0",
"css-unit-converter": "^1.1.2",
"cssesc": "^3.0.0",
"csstype": "^3.1.2",
@@ -268,6 +269,7 @@
"picomatch": "^2.3.1",
"pify": "^2.3.0",
"pirates": "^4.0.5",
+ "pkce-challenge": "^4.1.0",
"plotly.js": "^2.32.0",
"postcss": "^8.4.31",
"postcss-import": "^15.1.0",
@@ -378,6 +380,7 @@
"devDependencies": {
"@next/bundle-analyzer": "^14.2.15",
"@tailwindcss/typography": "^0.5.12",
+ "@types/crypto-js": "^4.2.2",
"@types/d3-time-format": "^4.0.3",
"@types/js-cookie": "^3.0.3",
"@types/js-yaml": "^4.0.5",
diff --git a/keep-ui/pages/api/auth/[...nextauth].ts b/keep-ui/pages/api/auth/[...nextauth].ts
index 11d1f179f..55b8395b5 100644
--- a/keep-ui/pages/api/auth/[...nextauth].ts
+++ b/keep-ui/pages/api/auth/[...nextauth].ts
@@ -4,6 +4,10 @@ import KeycloakProvider, {
KeycloakProfile,
} from "next-auth/providers/keycloak";
import Auth0Provider from "next-auth/providers/auth0";
+import AzureADProvider from "next-auth/providers/azure-ad";
+import SHA256 from "crypto-js/sha256";
+import Base64 from "crypto-js/enc-base64";
+
import { getApiURL } from "utils/apiUrl";
import {
AuthenticationType,
@@ -28,15 +32,16 @@ if (authTypeEnv === MULTI_TENANT) {
} else {
authType = authTypeEnv;
}
-
/*
-This file implements three different authentication flows:
+This file implements different authentication flows:
1. Multi-tenant authentication using Auth0
2. Single-tenant authentication using username/password
3. No authentication
+4. Keycloak authentication
+5. Azure AD authentication
-Depends on authType which can be NO_AUTH, SINGLE_TENANT or MULTI_TENANT
+Depends on authType which can be NO_AUTH, SINGLE_TENANT, MULTI_TENANT, KEYCLOAK, or AZURE_AD
Note that the same environment variable should be set in the backend too.
*/
@@ -329,6 +334,68 @@ const keycloakAuthOptions = {
},
} as AuthOptions;
+const azureADAuthOptions = {
+ providers: [
+ AzureADProvider({
+ clientId: process.env.KEEP_AZUREAD_CLIENT_ID!,
+ clientSecret: process.env.KEEP_AZUREAD_CLIENT_SECRET!,
+ tenantId: process.env.KEEP_AZUREAD_TENANT_ID!,
+ authorization: {
+ params: {
+ scope:
+ "api://d6cc6406-9de5-4a9f-bcf1-79e35e14cd2f/default openid profile email",
+ },
+ },
+ checks: ["pkce"],
+ client: {
+ token_endpoint_auth_method: "client_secret_post",
+ },
+ }),
+ ],
+ pages: {
+ signIn: "/signin",
+ },
+ debug: true,
+ cookies: {
+ pkceCodeVerifier: {
+ name: "next-auth.pkce.code_verifier",
+ options: {
+ httpOnly: true,
+ sameSite: "none",
+ path: "/",
+ secure: true,
+ maxAge: 900, // 15 minutes
+ },
+ },
+ },
+ session: {
+ strategy: "jwt",
+ maxAge: 30 * 24 * 60 * 60, // 30 days
+ },
+ callbacks: {
+ async redirect({ url, baseUrl }) {
+ console.log("Redirecting to: ", url);
+ return baseUrl;
+ },
+ async jwt({ token, account, profile }) {
+ if (account) {
+ console.log("Account: ", account);
+ console.log("access_token: ", account.access_token);
+ token.accessToken = account.access_token;
+ token.keep_tenant_id = process.env.KEEP_AZUREAD_TENANT_ID;
+ token.keep_role = "user"; // Default role - adjust as needed
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ session.accessToken = token.accessToken as string;
+ session.tenantId = token.keep_tenant_id as string;
+ session.userRole = token.keep_role as string;
+ return session;
+ },
+ },
+} as AuthOptions;
+
console.log("Starting Keep frontend with auth type: ", authType);
export const authOptions =
authType === AuthenticationType.AUTH0
@@ -337,6 +404,8 @@ export const authOptions =
? singleTenantAuthOptions
: authType === AuthenticationType.KEYCLOAK
? keycloakAuthOptions
+ : authType === AuthenticationType.AZUREAD
+ ? azureADAuthOptions
: // oauth2proxy same configuration as noauth
noAuthOptions;
diff --git a/keep-ui/pages/signin.tsx b/keep-ui/pages/signin.tsx
index 28ae9789a..9c922f388 100644
--- a/keep-ui/pages/signin.tsx
+++ b/keep-ui/pages/signin.tsx
@@ -1,27 +1,20 @@
import { signIn, getProviders } from "next-auth/react";
import { useEffect, useState } from "react";
-interface Providers {
- auth0?: {
- // Define the properties that your auth0 provider has
- name: string;
- type: string;
- signinUrl: string;
- };
- credentials?: {
- // Similarly define for credentials provider
- name: string;
- type: string;
- signinUrl: string;
- };
- keycloak?: {
- // Similarly define for keycloak provider
- name: string;
- type: string;
- signinUrl: string;
- };
+interface Provider {
+ id: string;
+ name: string;
+ type: string;
+ signinUrl: string;
+ callbackUrl: string;
}
+interface Providers {
+ auth0?: Provider;
+ credentials?: Provider;
+ keycloak?: Provider;
+ "azure-ad"?: Provider;
+}
export async function getServerSideProps(context: any) {
return {
@@ -59,11 +52,17 @@ export default function SignIn({ params }: { params?: { amt: string } }) {
console.log("Signing in with credentials provider");
signIn("credentials", { callbackUrl: "/" });
} else if (providers.keycloak) {
- console.log('Signing in with keycloak provider');
- signIn('keycloak', { callbackUrl: "/" });
+ console.log("Signing in with keycloak provider");
+ signIn("keycloak", { callbackUrl: "/" });
+ } else if (providers["azure-ad"]) {
+ console.log("Signing in with Azure AD provider");
+ signIn("azure-ad", { callbackUrl: "/" });
+ } else {
+ console.log("No provider found");
+ console.log(providers);
}
}
- }, [providers]);
+ }, [providers, params]);
return
Redirecting for authentication...
;
}
diff --git a/keep-ui/utils/authenticationType.ts b/keep-ui/utils/authenticationType.ts
index 4cee9c542..f8ac6a308 100644
--- a/keep-ui/utils/authenticationType.ts
+++ b/keep-ui/utils/authenticationType.ts
@@ -1,11 +1,12 @@
// AuthenticationType.ts
export enum AuthenticationType {
- AUTH0 = "AUTH0",
- DB = "DB",
- KEYCLOAK = "KEYCLOAK",
- OAUTH2PROXY = "OAUTH2PROXY",
- NOAUTH = "NOAUTH" // Default
+ AUTH0 = "AUTH0",
+ DB = "DB",
+ KEYCLOAK = "KEYCLOAK",
+ OAUTH2PROXY = "OAUTH2PROXY",
+ AZUREAD = "AZUREAD",
+ NOAUTH = "NOAUTH", // Default
}
// Backward compatibility
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-ui/utils/hooks/usePresets.ts b/keep-ui/utils/hooks/usePresets.ts
index 3559427d9..00e49588f 100644
--- a/keep-ui/utils/hooks/usePresets.ts
+++ b/keep-ui/utils/hooks/usePresets.ts
@@ -70,30 +70,13 @@ export const usePresets = (type?: string, useFilters?: boolean) => {
setPresetsOrderFromLS((current) =>
updatePresets(
presetsOrderRef.current,
- newPresets.filter(
- (p) =>
- ![
- "feed",
- "deleted",
- "dismissed",
- "without-incident",
- "groups",
- ].includes(p.name)
- )
+ newPresets.filter((p) => !["feed"].includes(p.name))
)
);
setStaticPresetsOrderFromLS((current) =>
updatePresets(
staticPresetsOrderRef.current,
- newPresets.filter((p) =>
- [
- "feed",
- "deleted",
- "dismissed",
- "without-incident",
- "groups",
- ].includes(p.name)
- )
+ newPresets.filter((p) => ["feed"].includes(p.name))
)
);
};
diff --git a/keep/api/api.py b/keep/api/api.py
index 6bd4ac81d..865bb2e32 100644
--- a/keep/api/api.py
+++ b/keep/api/api.py
@@ -174,7 +174,7 @@ async def root():
logger.info(f"Starting Keep with authentication type: {AUTH_TYPE}")
# If we run Keep with SINGLE_TENANT auth type, we want to add the signin endpoint
identity_manager = IdentityManagerFactory.get_identity_manager(
- None, None, AUTH_TYPE
+ SINGLE_TENANT_UUID, None, AUTH_TYPE
)
# if any endpoints needed, add them on_start
identity_manager.on_start(app)
diff --git a/keep/api/consts.py b/keep/api/consts.py
index d36d048fd..3aa27031b 100644
--- a/keep/api/consts.py
+++ b/keep/api/consts.py
@@ -14,10 +14,10 @@
id=StaticPresetsId.FEED_PRESET_ID.value,
name="feed",
options=[
- {"label": "CEL", "value": "(!deleted && !dismissed)"},
+ {"label": "CEL", "value": ""},
{
"label": "SQL",
- "value": {"sql": "(deleted=false AND dismissed=false)", "params": {}},
+ "value": {"sql": "", "params": {}},
},
],
created_by=None,
@@ -26,36 +26,7 @@
should_do_noise_now=False,
static=True,
tags=[],
- ),
- "dismissed": PresetDto(
- id=StaticPresetsId.DISMISSED_PRESET_ID.value,
- name="dismissed",
- options=[
- {"label": "CEL", "value": "dismissed"},
- {"label": "SQL", "value": {"sql": "dismissed=true", "params": {}}},
- ],
- created_by=None,
- is_private=False,
- is_noisy=False,
- should_do_noise_now=False,
- static=True,
- tags=[],
- ),
- "without-incident": PresetDto(
- id=StaticPresetsId.WITHOUT_INCIDENT_PRESET_ID.value,
- name="without-incident",
- options=[
- {"label": "CEL", "value": "incident == null"},
- {"label": "SQL", "value": {"sql": "incident is null", "params": {}}},
- ],
- created_by=None,
- is_private=False,
- is_noisy=False,
- should_do_noise_now=False,
- static=True,
- tags=[],
- ),
-
+ )
}
###
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 5ea74889b..a38b2f53b 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -12,7 +12,7 @@
from collections import defaultdict
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
-from typing import Any, Callable, Dict, List, Tuple, Union
+from typing import Any, Callable, Dict, List, Tuple, Type, Union
from uuid import uuid4
import numpy as np
@@ -38,7 +38,7 @@
from sqlalchemy.exc import IntegrityError, OperationalError
from sqlalchemy.orm import joinedload, selectinload, subqueryload
from sqlalchemy.sql import exists, expression
-from sqlmodel import Session, col, or_, select, text
+from sqlmodel import Session, SQLModel, col, or_, select, text
from keep.api.core.db_utils import create_db_engine, get_json_extract_field
@@ -1466,14 +1466,14 @@ def get_user(username, password, update_sign_in=True):
return user
-def get_users():
+def get_users(tenant_id=None):
from keep.api.core.dependencies import SINGLE_TENANT_UUID
from keep.api.models.db.user import User
+ tenant_id = tenant_id or SINGLE_TENANT_UUID
+
with Session(engine) as session:
- users = session.exec(
- select(User).where(User.tenant_id == SINGLE_TENANT_UUID)
- ).all()
+ users = session.exec(select(User).where(User.tenant_id == tenant_id)).all()
return users
@@ -4361,3 +4361,67 @@ def get_alerts_metrics_by_provider(
}
for row in results
}
+
+
+def get_table_class(table_name: str) -> Type[SQLModel]:
+ """
+ Get the SQLModel table class dynamically based on table name.
+ Assumes table classes follow PascalCase naming convention.
+
+ Args:
+ table_name (str): Name of the table in snake_case (e.g. "alerts", "rules")
+
+ Returns:
+ Type[SQLModel]: The corresponding SQLModel table class
+ """
+ # Convert snake_case to PascalCase and remove trailing 's' if exists
+ class_name = "".join(
+ word.capitalize() for word in table_name.rstrip("s").split("_")
+ )
+
+ # Get all SQLModel subclasses from the imported modules
+ model_classes = {
+ cls.__name__: cls
+ for cls in SQLModel.__subclasses__()
+ if hasattr(cls, "__tablename__")
+ }
+
+ if class_name not in model_classes:
+ raise ValueError(f"No table class found for table name: {table_name}")
+
+ return model_classes[class_name]
+
+
+def get_resource_ids_by_resource_type(
+ tenant_id: str, table_name: str, uid: str, session: Optional[Session] = None
+) -> List[str]:
+ """
+ Get all unique IDs from a table grouped by a specified UID column.
+
+ Args:
+ tenant_id (str): The tenant ID to filter by
+ table_name (str): Name of the table (e.g. "alerts", "rules")
+ uid (str): Name of the column to group by
+ session (Optional[Session]): SQLModel session
+
+ Returns:
+ List[str]: List of unique IDs
+
+ Example:
+ >>> get_resource_ids_by_resource_type("tenant123", "alerts", "alert_id")
+ ['id1', 'id2', 'id3']
+ """
+ with existed_or_new_session(session) as session:
+ # Get the table class dynamically
+ table_class = get_table_class(table_name)
+
+ # Create the query using SQLModel's select
+ query = (
+ select(getattr(table_class, uid))
+ .distinct()
+ .where(getattr(table_class, "tenant_id") == tenant_id)
+ )
+
+ # Execute the query and return results
+ result = session.exec(query)
+ return result.all()
diff --git a/keep/api/models/db/preset.py b/keep/api/models/db/preset.py
index ac7d139b6..7e20b2b07 100644
--- a/keep/api/models/db/preset.py
+++ b/keep/api/models/db/preset.py
@@ -63,7 +63,7 @@ def to_dict(self):
# datatype represents a query with CEL (str) and SQL (dict)
class PresetSearchQuery(BaseModel):
- cel_query: constr(min_length=1)
+ cel_query: constr(min_length=0)
sql_query: Dict[str, Any]
limit: conint(ge=0) = 1000
timeframe: conint(ge=0) = 0
diff --git a/keep/api/routes/preset.py b/keep/api/routes/preset.py
index 526cda450..06ff94bbc 100644
--- a/keep/api/routes/preset.py
+++ b/keep/api/routes/preset.py
@@ -187,6 +187,7 @@ def get_presets(
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,
@@ -198,10 +199,9 @@ def get_presets(
preset_ids=allowed_preset_ids,
)
presets_dto = [PresetDto(**preset.to_dict()) for preset in presets]
- # add static presets
- presets_dto.append(STATIC_PRESETS["feed"])
- presets_dto.append(STATIC_PRESETS["dismissed"])
- presets_dto.append(STATIC_PRESETS["without-incident"])
+ # add static presets (unless allowed_preset_ids is set)
+ if not allowed_preset_ids:
+ presets_dto.append(STATIC_PRESETS["feed"])
logger.info("Got all presets")
# get the number of alerts + noisy alerts for each preset
@@ -389,7 +389,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,
@@ -427,6 +427,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})
diff --git a/keep/identitymanager/identity_managers/db/db_identitymanager.py b/keep/identitymanager/identity_managers/db/db_identitymanager.py
index 88b3e2850..8d2d442f2 100644
--- a/keep/identitymanager/identity_managers/db/db_identitymanager.py
+++ b/keep/identitymanager/identity_managers/db/db_identitymanager.py
@@ -68,8 +68,8 @@ def signin(body: dict):
self.logger.info("Added signin endpoint")
- def get_users(self) -> list[User]:
- users = get_users_from_db()
+ def get_users(self, tenant_id=None) -> list[User]:
+ users = get_users_from_db(tenant_id)
users = [
User(
email=f"{user.username}",
diff --git a/keep/identitymanager/identitymanager.py b/keep/identitymanager/identitymanager.py
index 633280d22..0e509cdbe 100644
--- a/keep/identitymanager/identitymanager.py
+++ b/keep/identitymanager/identitymanager.py
@@ -206,7 +206,7 @@ def get_permissions(self) -> list[ResourcePermission]:
Returns:
list: A list of permission objects.
"""
- pass
+ return []
def get_user_permission_on_resource_type(
self, resource_type: str, authenticated_entity: AuthenticatedEntity
diff --git a/keep/rulesengine/rulesengine.py b/keep/rulesengine/rulesengine.py
index 4ec370942..363901c16 100644
--- a/keep/rulesengine/rulesengine.py
+++ b/keep/rulesengine/rulesengine.py
@@ -9,10 +9,13 @@
import celpy.evaluation
from sqlmodel import Session
-from keep.api.consts import STATIC_PRESETS
-from keep.api.core.db import assign_alert_to_incident, get_incident_for_grouping_rule, is_all_incident_alerts_resolved, \
- is_first_incident_alert_resolved, is_last_incident_alert_resolved
+from keep.api.core.db import assign_alert_to_incident, get_incident_for_grouping_rule
from keep.api.core.db import get_rules as get_rules_db
+from keep.api.core.db import (
+ is_all_incident_alerts_resolved,
+ is_first_incident_alert_resolved,
+ is_last_incident_alert_resolved,
+)
from keep.api.models.alert import AlertDto, AlertSeverity, IncidentDto, IncidentStatus
from keep.api.models.db.rule import ResolveOn
from keep.api.utils.cel_utils import preprocess_cel_expression
@@ -42,7 +45,9 @@ def __init__(self, tenant_id=None):
self.logger = logging.getLogger(__name__)
self.env = celpy.Environment()
- def run_rules(self, events: list[AlertDto], session: Optional[Session] = None) -> list[IncidentDto]:
+ def run_rules(
+ self, events: list[AlertDto], session: Optional[Session] = None
+ ) -> list[IncidentDto]:
self.logger.info("Running rules")
rules = get_rules_db(tenant_id=self.tenant_id)
@@ -68,26 +73,38 @@ def run_rules(self, events: list[AlertDto], session: Optional[Session] = None) -
rule_fingerprint = self._calc_rule_fingerprint(event, rule)
incident = get_incident_for_grouping_rule(
- self.tenant_id, rule, rule.timeframe, rule_fingerprint,
- session=session
+ self.tenant_id,
+ rule,
+ rule.timeframe,
+ rule_fingerprint,
+ session=session,
)
incident = assign_alert_to_incident(
alert_id=event.event_id,
incident=incident,
tenant_id=self.tenant_id,
- session=session
+ session=session,
)
should_resolve = False
- if rule.resolve_on == ResolveOn.ALL.value and is_all_incident_alerts_resolved(incident, session=session):
+ if (
+ rule.resolve_on == ResolveOn.ALL.value
+ and is_all_incident_alerts_resolved(incident, session=session)
+ ):
should_resolve = True
- elif rule.resolve_on == ResolveOn.FIRST.value and is_first_incident_alert_resolved(incident, session=session):
+ elif (
+ rule.resolve_on == ResolveOn.FIRST.value
+ and is_first_incident_alert_resolved(incident, session=session)
+ ):
should_resolve = True
- if rule.resolve_on == ResolveOn.LAST.value and is_last_incident_alert_resolved(incident, session=session):
+ if (
+ rule.resolve_on == ResolveOn.LAST.value
+ and is_last_incident_alert_resolved(incident, session=session)
+ ):
should_resolve = True
if should_resolve:
@@ -223,14 +240,9 @@ def filter_alerts(
list[AlertDto]: list of alerts that are related to the cel
"""
logger = logging.getLogger(__name__)
-
- # tb: temp hack because this function is super slow
- if cel == STATIC_PRESETS.get("feed", {}).options[0].get("value"):
- return [
- alert
- for alert in alerts
- if (alert.deleted == False and alert.dismissed == False)
- ]
+ # if the cel is empty, return all the alerts
+ if cel == "":
+ return alerts
# if the cel is empty, return all the alerts
if not cel:
logger.debug("No CEL expression provided")
diff --git a/keep/searchengine/searchengine.py b/keep/searchengine/searchengine.py
index 014c23ed5..c123fdfd5 100644
--- a/keep/searchengine/searchengine.py
+++ b/keep/searchengine/searchengine.py
@@ -7,9 +7,9 @@
from keep.api.core.tenant_configuration import TenantConfiguration
from keep.api.models.alert import AlertDto, AlertStatus
from keep.api.models.db.preset import PresetDto, PresetSearchQuery
+from keep.api.models.time_stamp import TimeStampFilter
from keep.api.utils.enrichment_helpers import convert_db_alerts_to_dto_alerts
from keep.rulesengine.rulesengine import RulesEngine
-from keep.api.models.time_stamp import TimeStampFilter
class SearchMode(enum.Enum):
@@ -52,7 +52,9 @@ def __init__(self, tenant_id):
extra={"tenant_id": self.tenant_id, "search_mode": self.search_mode},
)
- def _get_last_alerts(self, limit=1000, timeframe: int = 0, time_stamp:TimeStampFilter=None) -> list[AlertDto]:
+ def _get_last_alerts(
+ self, limit=1000, timeframe: int = 0, time_stamp: TimeStampFilter = None
+ ) -> list[AlertDto]:
"""Get the last alerts
Returns:
@@ -63,13 +65,18 @@ def _get_last_alerts(self, limit=1000, timeframe: int = 0, time_stamp:TimeStampF
upper_timestamp = time_stamp.upper_timestamp if time_stamp else None
alerts = get_last_alerts(
- tenant_id=self.tenant_id, limit=limit, timeframe=timeframe,
- lower_timestamp=lower_timestamp, upper_timestamp=upper_timestamp,
- with_incidents=True
+ tenant_id=self.tenant_id,
+ limit=limit,
+ timeframe=timeframe,
+ lower_timestamp=lower_timestamp,
+ upper_timestamp=upper_timestamp,
+ with_incidents=True,
)
# convert the alerts to DTO
alerts_dto = convert_db_alerts_to_dto_alerts(alerts)
- self.logger.info(f"Finished getting last alerts {lower_timestamp} {upper_timestamp} {time_stamp}")
+ self.logger.info(
+ f"Finished getting last alerts {lower_timestamp} {upper_timestamp} {time_stamp}"
+ )
return alerts_dto
def search_alerts_by_cel(
@@ -113,7 +120,8 @@ def _search_alerts_by_sql(
query = self._create_raw_sql(sql_query.get("sql"), sql_query.get("params"))
# get the alerts from elastic
elastic_sql_query = (
- f"""select * from "{self.elastic_client.alerts_index}" where {query}"""
+ f"""select * from "{self.elastic_client.alerts_index}" """
+ + (f"where {query}" if query else "")
)
if timeframe:
elastic_sql_query += f" and lastReceived > now() - {timeframe}s"
@@ -156,8 +164,7 @@ def search_alerts(self, query: PresetSearchQuery) -> list[AlertDto]:
return filtered_alerts
def search_preset_alerts(
- self, presets: list[PresetDto],
- time_stamp: TimeStampFilter = None
+ self, presets: list[PresetDto], time_stamp: TimeStampFilter = None
) -> dict[str, list[AlertDto]]:
"""Search for alerts based on a list of queries
@@ -184,6 +191,7 @@ def search_preset_alerts(
)
preset.alerts_count = len(filtered_alerts)
# update noisy
+
if preset.is_noisy:
firing_filtered_alerts = list(
filter(
@@ -220,7 +228,10 @@ def search_preset_alerts(
preset.sql_query.get("sql"), preset.sql_query.get("params")
)
# get number of alerts and number of noisy alerts
- elastic_sql_query = f"""select count(*), MAX(CASE WHEN isNoisy = true AND dismissed = false AND deleted = false THEN 1 ELSE 0 END) from "{self.elastic_client.alerts_index}" where {query}"""
+ elastic_sql_query = (
+ f"""select count(*), MAX(CASE WHEN isNoisy = true AND dismissed = false AND deleted = false THEN 1 ELSE 0 END) from "{self.elastic_client.alerts_index}" """
+ + (f" where {query}" if query else "")
+ )
results = self.elastic_client.run_query(elastic_sql_query)
if results:
preset.alerts_count = results["rows"][0][0]
diff --git a/keycloak/Dockerfile.keycloak b/keycloak/Dockerfile.keycloak
index 639e636a1..7ffc2718e 100644
--- a/keycloak/Dockerfile.keycloak
+++ b/keycloak/Dockerfile.keycloak
@@ -13,6 +13,8 @@ COPY themes/keep.jar /opt/keycloak/providers/keep.jar
# Copy the last login event listener
COPY event_listeners/last-login-event-listener-0.0.1-SNAPSHOT.jar /opt/keycloak/providers/keycloak-event-listener.jar
+COPY javascript_providers/keep-abac-policy.jar /opt/keycloak/providers/keep-abac-policy.jar
+
# Copy the custom entrypoint script and ensure it's executable
COPY --chmod=755 keycloak_entrypoint.sh /opt/keycloak/keycloak_entrypoint.sh
diff --git a/keycloak/docker-compose.yaml b/keycloak/docker-compose.yaml
index c03bf1380..927078b22 100644
--- a/keycloak/docker-compose.yaml
+++ b/keycloak/docker-compose.yaml
@@ -20,7 +20,7 @@ services:
- keycloak-and-mysql-network
keycloak:
- image: us-central1-docker.pkg.dev/keephq/keep/keep-keycloak
+ image: keep-keycloak
ports:
- 8181:8080
restart: unless-stopped
@@ -41,11 +41,13 @@ services:
KEEP_AUTH_REDICERT_URL: http://localhost:3000/*
KEEP_CLIENT_ID: keep
KEEP_KEYCLOAK_SECRET: keep-keycloak-secret
+ KEYCLOAK_DEBUG: false # used in entrypoint
entrypoint: ["/opt/keycloak/keycloak_entrypoint.sh"]
volumes:
- ./keep-realm.json:/opt/keycloak/data/import/keep-realm.json
- ./themes/keep.jar:/opt/keycloak/providers/keep.jar
- ./event_listeners/last-login-event-listener-0.0.1-SNAPSHOT.jar:/opt/keycloak/providers/keycloak-event-listener.jar
+ # - ./javascript_providers/keep-js-policies.jar:/opt/keycloak/providers/keep-js-policies.jar
- ./keycloak_entrypoint.sh:/opt/keycloak/keycloak_entrypoint.sh
depends_on:
- mysql
diff --git a/keycloak/javascript_providers/keep-abac-policy.jar b/keycloak/javascript_providers/keep-abac-policy.jar
new file mode 100644
index 000000000..ba1dd6746
Binary files /dev/null and b/keycloak/javascript_providers/keep-abac-policy.jar differ
diff --git a/keycloak/javascript_providers/keep-abac-policy/META-INF/keycloak-scripts.json b/keycloak/javascript_providers/keep-abac-policy/META-INF/keycloak-scripts.json
new file mode 100644
index 000000000..7d6bda35a
--- /dev/null
+++ b/keycloak/javascript_providers/keep-abac-policy/META-INF/keycloak-scripts.json
@@ -0,0 +1,9 @@
+{
+ "policies": [
+ {
+ "name": "Keep ABAC Policy",
+ "fileName": "keep-abac-policy.js",
+ "description": "Keep ABAC Policy"
+ }
+ ]
+}
diff --git a/keycloak/javascript_providers/keep-abac-policy/keep-abac-policy.js b/keycloak/javascript_providers/keep-abac-policy/keep-abac-policy.js
new file mode 100644
index 000000000..f06d29b05
--- /dev/null
+++ b/keycloak/javascript_providers/keep-abac-policy/keep-abac-policy.js
@@ -0,0 +1,81 @@
+// Start policy evaluation logging
+print("=== Starting Environment Policy Evaluation ===");
+
+// Get the evaluation instance and extract context and permission
+var context = $evaluation.getContext();
+var permission = $evaluation.getPermission();
+var resource = permission.getResource();
+
+// Get identity information and log
+var identity = context.getIdentity();
+print("Evaluating policy for user ID: " + identity.getId());
+
+// Get and log resource information
+print("Requested resource: " + resource.getName());
+
+// Log requested scopes
+var scopes = permission.getScopes();
+var scopeNames = [];
+for (var i = 0; i < scopes.length; i++) {
+ scopeNames.push(scopes[i].getName());
+}
+print("Requested scopes: " + scopeNames.join(", "));
+
+// Get context attributes for logging
+var contextAttributes = context.getAttributes();
+print("Client ID: " + contextAttributes.getValue("kc.client.id").asString(0));
+print(
+ "Client IP: " +
+ contextAttributes.getValue("kc.client.network.ip_address").asString(0),
+);
+print(
+ "Request time: " +
+ contextAttributes.getValue("kc.time.date_time").asString(0),
+);
+
+// Check resource attributes
+print("Checking resource attributes...");
+var resourceAttributes = resource.getAttributes();
+print("Resource attributes type: " + typeof resourceAttributes);
+
+try {
+ // Try to get the env attribute directly
+ var envAttribute = resourceAttributes.get("env");
+ print("Env attribute found: " + (envAttribute !== null));
+
+ if (envAttribute) {
+ print("Env attribute value: " + envAttribute);
+ var hasDevEnv = envAttribute.contains("dev");
+ print("Has dev environment: " + hasDevEnv);
+
+ if (hasDevEnv) {
+ print("Environment check passed: env=dev found in resource attributes");
+ permission.addClaim("resource_name", resource.getName());
+ permission.addClaim(
+ "access_reason",
+ "Development environment access granted",
+ );
+ $evaluation.grant();
+ } else {
+ print("Environment check failed: env value is not 'dev'");
+ permission.addClaim(
+ "access_reason",
+ "Resource is not in development environment",
+ );
+ $evaluation.deny();
+ }
+ } else {
+ print("No env attribute found");
+ permission.addClaim("access_reason", "No environment attribute found");
+ $evaluation.deny();
+ }
+} catch (e) {
+ print("Error checking attributes: " + e.message);
+ permission.addClaim(
+ "access_reason",
+ "Error checking environment: " + e.message,
+ );
+ $evaluation.deny();
+}
+
+print("=== Environment Policy Evaluation Complete ===");
diff --git a/keycloak/keep-realm.json b/keycloak/keep-realm.json
index a56d200c5..34bcec7e3 100644
--- a/keycloak/keep-realm.json
+++ b/keycloak/keep-realm.json
@@ -18,7 +18,12 @@
},
"realmRoles": ["offline_access", "uma_authorization", "admin"],
"clientRoles": {
- "realm-management": ["manage-users", "manage-identity-providers"]
+ "realm-management": [
+ "manage-users",
+ "manage-identity-providers",
+ "realm-admin"
+ ],
+ "keep": ["admin"]
}
}
],
@@ -28,9 +33,7 @@
"name": "Keep Application",
"enabled": true,
"clientAuthenticatorType": "client-secret",
- "redirectUris": [
- "${KEEP_AUTH_REDICERT_URL}"
- ],
+ "redirectUris": ["${KEEP_AUTH_REDICERT_URL}"],
"webOrigins": [],
"protocol": "openid-connect",
"attributes": {
@@ -43,9 +46,27 @@
"implicitFlowEnabled": false,
"fullScopeAllowed": true,
"authorizationServicesEnabled": true,
+ "authorizationSettings": {
+ "policyEnforcementMode": "ENFORCING",
+ "decisionStrategy": "AFFIRMATIVE",
+ "allowRemoteResourceManagement": true,
+ "resources": [],
+ "policies": []
+ },
"serviceAccountsEnabled": true,
- "defaultClientScopes": ["email", "roles", "web-origins", "profile", "active_organization"],
- "optionalClientScopes": ["offline_access", "microprofile-jwt", "phone", "address"],
+ "defaultClientScopes": [
+ "email",
+ "roles",
+ "web-origins",
+ "profile",
+ "active_organization"
+ ],
+ "optionalClientScopes": [
+ "offline_access",
+ "microprofile-jwt",
+ "phone",
+ "address"
+ ],
"access": {
"view": true,
"configure": true,
diff --git a/keycloak/keycloak_entrypoint.sh b/keycloak/keycloak_entrypoint.sh
index 74a95fce9..eb218ed00 100755
--- a/keycloak/keycloak_entrypoint.sh
+++ b/keycloak/keycloak_entrypoint.sh
@@ -26,9 +26,19 @@ if [ -z "$KEEP_REALM" ]; then
KEEP_REALM="keep"
fi
+# Enabled debug if $KEYCLOAK_DEBUG is set to true
+if [ "$KEYCLOAK_DEBUG" = "true" ]; then
+ echo "Enabling debug mode"
+ KEYCLOAK_LOG_LEVEL="DEBUG"
+else
+ KEYCLOAK_LOG_LEVEL="INFO"
+fi
+
# Start Keycloak in the background
echo "Starting Keycloak"
-/opt/keycloak/bin/kc.sh start-dev --log-level=DEBUG --features=preview --import-realm -Dkeycloak.profile.feature.scripts=enabled -Dkeycloak.migration.strategy=OVERWRITE_EXISTIN &
+command="/opt/keycloak/bin/kc.sh start-dev --verbose --log-level=${KEYCLOAK_LOG_LEVEL} --features=preview --import-realm -Dkeycloak.profile.feature.scripts=enabled -Dkeycloak.migration.strategy=OVERWRITE_EXISTING"
+echo "Running command: ${command}"
+/opt/keycloak/bin/kc.sh start-dev --log-level=${KEYCLOAK_LOG_LEVEL} --features=preview --features-disabled=dpop --import-realm -Dkeycloak.profile.feature.scripts=enabled -Dkeycloak.migration.strategy=OVERWRITE_EXISTING &
echo "Keycloak started"
# Try to connect to Keycloak - wait until Keycloak is ready or timeout
echo "Waiting for Keycloak to be ready"
diff --git a/keycloak/readme.md b/keycloak/readme.md
index 910bc3a8c..d1b39bca7 100644
--- a/keycloak/readme.md
+++ b/keycloak/readme.md
@@ -1,19 +1,22 @@
-
# Docker-compose example:
+
```
docker-compose -f keycloak/docker-compose.yaml up
```
+
Keycloak: http://localhost:8181/auth/ (keep_kc:keep_kc)
Keep login page: http://localhost:3000/
## For Azure:
-Instructions:
+
+Instructions:
+
1. https://rahulroyz.medium.com/using-keycloak-as-idp-for-azure-ad-sso-authentication-role-authorization-0b309c15eadc
2. https://rahulroyz.medium.com/using-keycloak-as-idp-for-azure-ad-role-authorization-part-2-map-ad-groups-to-keycloak-roles-9850d4acd536
Set email, first name & last name for keep_admin user: http://localhost:8181/auth/admin/master/console/#/keep/users
-Also please assign admin role for keep_admin.
+Also please assign admin role for keep_admin.
# Development
@@ -23,6 +26,7 @@ docker run --name phasetwo_test --rm -p 8181:8080 \
quay.io/phasetwo/phasetwo-keycloak:latest \
start-dev
```
+
```
http://localhost:8181/realms/keep/portal/
http://localhost:8181/realms/keep/portal/
@@ -30,41 +34,46 @@ https://euc1.auth.ac/auth/realms/keep/portal
```
# delete realm to refresh
+
1. delete the realm from the UI
2. restart
# how to use phasetwo plugins
-
# what to read:
+
1. main repo - https://github.com/p2-inc/keycloak-orgs
2. SSO wizzards -
3.
-
-
# New Tutorial
## Keycloak configuration
+
### https://github.com/p2-inc/keycloak-orgs
+
1. Change admin theme so that "Org" will show
2. Create organization
3. Add all members to organization
- TODO: how to do it automatically?
+ TODO: how to do it automatically?
4. For iframe -
1. http://localhost:8181/auth/admin/master/console/#/keep/realm-settings/security-defenses
2. frame-src 'self' http://localhost:3000; frame-ancestors 'self' http://localhost:3000; object-src 'none';
-
## LDAP
+
1. openldap container - the ldap server
2. ldap-ui - ui for the ldap
3. load ldap.ldif
-
http://localhost:8181/auth/admin/master/console/#/keep
-
## Sign in page
+
# 1. build: pnpm build:jar
+
+# How to compile the javascript
+
+cd javascript_providers
+jar cvf keep-abac-policy.jar -C keep-abac-policy .
diff --git a/pyproject.toml b/pyproject.toml
index 8051fc7b5..3f4d99726 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "keep"
-version = "0.28.3"
+version = "0.28.4"
description = "Alerting. for developers, by developers."
authors = ["Keep Alerting LTD"]
readme = "README.md"
diff --git a/tests/Dockerfile.keycloak.test b/tests/Dockerfile.keycloak.test
index e00fffab7..20c1c1132 100644
--- a/tests/Dockerfile.keycloak.test
+++ b/tests/Dockerfile.keycloak.test
@@ -1,5 +1,5 @@
# Use the Phase Two Keycloak image as the base
-FROM quay.io/phasetwo/phasetwo-keycloak:latest
+FROM quay.io/phasetwo/phasetwo-keycloak:25.0.4
# Set the working directory
WORKDIR /opt/keycloak
diff --git a/tests/docker-compose-keycloak.yml b/tests/docker-compose-keycloak.yml
index 2ebc5434d..0f448b0db 100644
--- a/tests/docker-compose-keycloak.yml
+++ b/tests/docker-compose-keycloak.yml
@@ -1,7 +1,13 @@
-version: '3.8'
+version: "3.8"
services:
keycloak:
image: us-central1-docker.pkg.dev/keephq/keep/keep-keycloak-test
+ # image: keep-keycloak-test
ports:
- "8787:8080"
+ environment:
+ KEYCLOAK_DEBUG: false # used in entrypoint
+ # entrypoint: ["sleep", "7200"]
+ # volumes:
+ # - ./keycloak-test-realm-export.json:/opt/keycloak/data/import/keep-realm.json
diff --git a/tests/keycloak-test-realm-export.json b/tests/keycloak-test-realm-export.json
index 94ed361a5..59b8dca30 100644
--- a/tests/keycloak-test-realm-export.json
+++ b/tests/keycloak-test-realm-export.json
@@ -38,7 +38,8 @@
],
"realmRoles": ["default-roles-keep", "admin"],
"clientRoles": {
- "realm-management": ["realm-admin"]
+ "realm-management": ["realm-admin"],
+ "keep": ["admin"]
}
}
],
@@ -64,9 +65,7 @@
"name": "Keep Application",
"enabled": true,
"clientAuthenticatorType": "client-secret",
- "redirectUris": [
- "http://localhost:3000/*"
- ],
+ "redirectUris": ["http://localhost:3000/*"],
"webOrigins": [],
"protocol": "openid-connect",
"attributes": {
@@ -79,9 +78,27 @@
"implicitFlowEnabled": false,
"fullScopeAllowed": true,
"authorizationServicesEnabled": true,
+ "authorizationSettings": {
+ "policyEnforcementMode": "ENFORCING",
+ "decisionStrategy": "AFFIRMATIVE",
+ "allowRemoteResourceManagement": true,
+ "resources": [],
+ "policies": []
+ },
"serviceAccountsEnabled": true,
- "defaultClientScopes": ["email", "roles", "web-origins", "profile", "active_organization"],
- "optionalClientScopes": ["offline_access", "microprofile-jwt", "phone", "address"],
+ "defaultClientScopes": [
+ "email",
+ "roles",
+ "web-origins",
+ "profile",
+ "active_organization"
+ ],
+ "optionalClientScopes": [
+ "offline_access",
+ "microprofile-jwt",
+ "phone",
+ "address"
+ ],
"access": {
"view": true,
"configure": true,