diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 685908bb7..439994b74 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -2,11 +2,11 @@ name: Test docs on: push: paths: - - 'keep/poviders/**' + - 'keep/providers/**' - 'docs/**' pull_request: paths: - - 'keep/poviders/**' + - 'keep/providers/**' - 'docs/**' workflow_dispatch: concurrency: diff --git a/docs/deployment/authentication/azuread-auth.mdx b/docs/deployment/authentication/azuread-auth.mdx index 2ee4fd365..3dae091ab 100644 --- a/docs/deployment/authentication/azuread-auth.mdx +++ b/docs/deployment/authentication/azuread-auth.mdx @@ -8,7 +8,7 @@ This feature is a part of Keep Enterprise. Talk to us to get access: https://www.keephq.dev/meet-keep -Keep supports enterprise authentication through Azure Active Directory (Azure AD), enabling organizations to use their existing Microsoft identity platform for secure access management. +Keep supports enterprise authentication through Azure Entre ID (formerly known as Azure AD), enabling organizations to use their existing Microsoft identity platform for secure access management. ## When to Use @@ -49,9 +49,9 @@ We use "Web" platform instead of "Single Page Application (SPA)" because Keep's -For localhost, the redirect would be http://localhost:3000/api/auth/callback/azure-ad +For localhost, the redirect would be http://localhost:3000/api/auth/callback/microsoft-entra-id -For production, it should be something like http://your_keep_frontend_domain/api/auth/callback/azure-ad +For production, it should be something like http://your_keep_frontend_domain/api/auth/callback/microsoft-entra-id diff --git a/docs/images/azuread_3.png b/docs/images/azuread_3.png index c7466ca36..4e91c7263 100644 Binary files a/docs/images/azuread_3.png and b/docs/images/azuread_3.png differ diff --git a/docs/providers/documentation/gke-provider.mdx b/docs/providers/documentation/gke-provider.mdx index 416d6301e..b395d399d 100644 --- a/docs/providers/documentation/gke-provider.mdx +++ b/docs/providers/documentation/gke-provider.mdx @@ -1,7 +1,7 @@ --- -title: "GKE" -sidebarTitle: "GKE Provider" -description: "GKE provider allows managing Google Kubernetes Engine clusters and related resources." +title: "Google Kubernetes Engine" +sidebarTitle: "Google Kubernetes Engine Provider" +description: "Google Kubernetes Engine provider allows managing Google Kubernetes Engine clusters and related resources." --- ## Inputs diff --git a/docs/providers/documentation/teams-provider.mdx b/docs/providers/documentation/teams-provider.mdx index 2a907da42..673b9dc07 100644 --- a/docs/providers/documentation/teams-provider.mdx +++ b/docs/providers/documentation/teams-provider.mdx @@ -1,7 +1,7 @@ --- -title: "Teams Provider" -sidebarTitle: "Teams Provider" -description: "Teams Provider is a provider that allows to notify alerts to Microsoft Teams chats." +title: "Microsoft Teams Provider" +sidebarTitle: "Microsoft Teams Provider" +description: "Microsoft Teams Provider is a provider that allows to notify alerts to Microsoft Teams chats." --- ## Inputs diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx index 74e1ddc87..3e8767f60 100644 --- a/docs/providers/overview.mdx +++ b/docs/providers/overview.mdx @@ -45,7 +45,7 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t /> @@ -653,7 +653,7 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t > diff --git a/keep-ui/app/(keep)/alerts/alert-table.tsx b/keep-ui/app/(keep)/alerts/alert-table.tsx index d61cb2167..5a27259da 100644 --- a/keep-ui/app/(keep)/alerts/alert-table.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table.tsx @@ -264,15 +264,20 @@ export function AlertTable({ }; return ( -
- -
- {/* Setting min-h-10 to avoid jumping when actions are shown */} + // Add h-screen to make it full height and remove the default flex-col gap +
+ {/* Add padding to account for any top nav/header */} +
+ +
+ + {/* Make actions/presets section fixed height */} +
{selectedRowIds.length ? ( )}
-
-
- -
-
- -
- {/* For dynamic preset, add alert tabs*/} - {!presetStatic && ( - - )} -
- - - -
-
- + + {/* Main content area - uses flex-grow to fill remaining space */} +
+
+ {/* Facets sidebar */} +
+ +
+ + {/* Table section */} +
+ +
+ {!presetStatic && ( +
+ +
+ )} + +
+ + {/* Make table wrapper scrollable */} +
+ + + +
+
+
+ +
-
+ + {/* Pagination footer - fixed height */} +
+ setIsSidebarOpen(false)} diff --git a/keep-ui/app/(keep)/alerts/alerts-table-body.tsx b/keep-ui/app/(keep)/alerts/alerts-table-body.tsx index 0ee55b8a0..08a4c61bf 100644 --- a/keep-ui/app/(keep)/alerts/alerts-table-body.tsx +++ b/keep-ui/app/(keep)/alerts/alerts-table-body.tsx @@ -35,13 +35,15 @@ export function AlertsTableBody({ if (showEmptyState) { return ( <> -
- +
+
+ +
{modalOpen && ( (); useEffect(() => { - console.log("Fetching providers"); async function fetchProviders() { - console.log("Fetching providers 2"); const response = await getProviders(); setProviders(response as Providers); } - console.log("Fetching providers 3"); fetchProviders(); - console.log("Fetching providers 4"); }, []); useEffect(() => { @@ -69,9 +65,9 @@ export default function SignInForm({ params }: { params?: { amt: string } }) { } else if (providers.keycloak) { console.log("Signing in with keycloak provider"); signIn("keycloak", { callbackUrl: "/" }); - } else if (providers["azure-ad"]) { + } else if (providers["microsoft-entra-id"]) { console.log("Signing in with Azure AD provider"); - signIn("azure-ad", { callbackUrl: "/" }); + signIn("microsoft-entra-id", { callbackUrl: "/" }); } else if ( providers.credentials && providers.credentials.name == "NoAuth" diff --git a/keep-ui/app/api/auth/[...nextauth]/route.ts b/keep-ui/app/api/auth/[...nextauth]/route.ts index 7c62e2db1..be6543d31 100644 --- a/keep-ui/app/api/auth/[...nextauth]/route.ts +++ b/keep-ui/app/api/auth/[...nextauth]/route.ts @@ -1,2 +1,23 @@ import { handlers } from "@/auth"; -export const { GET, POST } = handlers; +import { NextRequest } from "next/server"; + +const reqWithTrustedOrigin = (req: NextRequest): NextRequest => { + if (process.env.AUTH_TRUST_HOST !== "true") return req; + const proto = req.headers.get("x-forwarded-proto"); + const host = req.headers.get("x-forwarded-host"); + if (!proto || !host) { + console.warn("Missing x-forwarded-proto or x-forwarded-host headers."); + return req; + } + const envOrigin = `${proto}://${host}`; + const { href, origin } = req.nextUrl; + return new NextRequest(href.replace(origin, envOrigin), req); +}; + +export const GET = (req: NextRequest) => { + return handlers.GET(reqWithTrustedOrigin(req)); +}; + +export const POST = (req: NextRequest) => { + return handlers.POST(reqWithTrustedOrigin(req)); +}; diff --git a/keep-ui/auth.config.ts b/keep-ui/auth.config.ts new file mode 100644 index 000000000..5ec968059 --- /dev/null +++ b/keep-ui/auth.config.ts @@ -0,0 +1,274 @@ +import type { NextAuthConfig } from "next-auth"; +import Credentials from "next-auth/providers/credentials"; +import Keycloak from "next-auth/providers/keycloak"; +import Auth0 from "next-auth/providers/auth0"; +import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; +import { AuthError } from "next-auth"; +import { AuthenticationError, AuthErrorCodes } from "@/errors"; +import type { JWT } from "next-auth/jwt"; +import type { User } from "next-auth"; +import { getApiURL } from "@/utils/apiUrl"; +import { + AuthType, + MULTI_TENANT, + SINGLE_TENANT, + NO_AUTH, + NoAuthUserEmail, + NoAuthTenant, +} from "@/utils/authenticationType"; + +export class BackendRefusedError extends AuthError { + static type = "BackendRefusedError"; +} + +// Determine auth type with backward compatibility +const authTypeEnv = process.env.AUTH_TYPE; +export const authType = + authTypeEnv === MULTI_TENANT + ? AuthType.AUTH0 + : authTypeEnv === SINGLE_TENANT + ? AuthType.DB + : authTypeEnv === NO_AUTH + ? AuthType.NOAUTH + : (authTypeEnv as AuthType); + +async function refreshAccessToken(token: any) { + const issuerUrl = process.env.KEYCLOAK_ISSUER; + const refreshTokenUrl = `${issuerUrl}/protocol/openid-connect/token`; + + try { + const response = await fetch(refreshTokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: process.env.KEYCLOAK_ID!, + client_secret: process.env.KEYCLOAK_SECRET!, + grant_type: "refresh_token", + refresh_token: token.refreshToken, + }), + }); + + const refreshedTokens = await response.json(); + + if (!response.ok) { + throw new Error( + `Refresh token failed: ${response.status} ${response.statusText}` + ); + } + + return { + ...token, + accessToken: refreshedTokens.access_token, + accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000, + refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, + }; + } catch (error) { + console.error("Error refreshing access token:", error); + return { + ...token, + error: "RefreshAccessTokenError", + }; + } +} + +// Base provider configurations without AzureAD +const baseProviderConfigs = { + [AuthType.AUTH0]: [ + Auth0({ + clientId: process.env.AUTH0_CLIENT_ID!, + clientSecret: process.env.AUTH0_CLIENT_SECRET!, + issuer: process.env.AUTH0_ISSUER!, + authorization: { + params: { + prompt: "login", + }, + }, + }), + ], + [AuthType.DB]: [ + Credentials({ + name: "Credentials", + credentials: { + username: { label: "Username", type: "text", placeholder: "keep" }, + password: { label: "Password", type: "password", placeholder: "keep" }, + }, + async authorize(credentials): Promise { + try { + const response = await fetch(`${getApiURL()}/signin`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(credentials), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + console.error("Authentication failed:", errorData); + throw new AuthenticationError(AuthErrorCodes.INVALID_CREDENTIALS); + } + + const user = await response.json(); + if (!user.accessToken) return null; + + return { + id: user.id, + name: user.name, + email: user.email, + accessToken: user.accessToken, + tenantId: user.tenantId, + role: user.role, + }; + } catch (error) { + if (error instanceof TypeError && error.message === "fetch failed") { + throw new AuthenticationError(AuthErrorCodes.CONNECTION_REFUSED); + } + + if (error instanceof AuthenticationError) { + throw error; + } + + throw new AuthenticationError(AuthErrorCodes.SERVICE_UNAVAILABLE); + } + }, + }), + ], + [AuthType.NOAUTH]: [ + Credentials({ + name: "NoAuth", + credentials: {}, + async authorize(): Promise { + return { + id: "keep-user-for-no-auth-purposes", + name: "Keep", + email: NoAuthUserEmail, + accessToken: "keep-token-for-no-auth-purposes", + tenantId: NoAuthTenant, + role: "user", + }; + }, + }), + ], + [AuthType.KEYCLOAK]: [ + Keycloak({ + clientId: process.env.KEYCLOAK_ID!, + clientSecret: process.env.KEYCLOAK_SECRET!, + issuer: process.env.KEYCLOAK_ISSUER, + authorization: { params: { scope: "openid email profile roles" } }, + }), + ], + [AuthType.AZUREAD]: [ + MicrosoftEntraID({ + clientId: process.env.KEEP_AZUREAD_CLIENT_ID!, + clientSecret: process.env.KEEP_AZUREAD_CLIENT_SECRET!, + issuer: `https://login.microsoftonline.com/${process.env + .KEEP_AZUREAD_TENANT_ID!}/v2.0`, + authorization: { + params: { + scope: `api://${process.env + .KEEP_AZUREAD_CLIENT_ID!}/default openid profile email`, + }, + }, + client: { + token_endpoint_auth_method: "client_secret_post", + }, + }), + ], +}; + +export const config = { + trustHost: true, + providers: + baseProviderConfigs[authType as keyof typeof baseProviderConfigs] || + baseProviderConfigs[AuthType.NOAUTH], + pages: { + signIn: "/signin", + }, + session: { + strategy: "jwt" as const, + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + callbacks: { + authorized({ auth, request: { nextUrl } }) { + const isLoggedIn = !!auth?.user; + const isOnDashboard = nextUrl.pathname.startsWith("/dashboard"); + if (isOnDashboard) { + if (isLoggedIn) return true; + return false; + } + return true; + }, + jwt: async ({ token, user, account, profile }): Promise => { + if (account && user) { + let accessToken: string | undefined; + let tenantId: string | undefined = user.tenantId; + let role: string | undefined = user.role; + + if (authType === AuthType.AZUREAD) { + accessToken = account.access_token; + if (account.id_token) { + try { + const payload = JSON.parse( + Buffer.from(account.id_token.split(".")[1], "base64").toString() + ); + role = payload.roles?.[0] || "user"; + tenantId = payload.tid || undefined; + } catch (e) { + console.warn("Failed to decode id_token:", e); + } + } + } else if (authType == AuthType.AUTH0) { + accessToken = account.id_token; + if ((profile as any)?.keep_tenant_id) { + tenantId = (profile as any).keep_tenant_id; + } + if ((profile as any)?.keep_role) { + role = (profile as any).keep_role; + } + } else { + accessToken = + user.accessToken || account.access_token || account.id_token; + } + if (!accessToken) { + throw new Error("No access token available"); + } + + token.accessToken = accessToken; + token.tenantId = tenantId; + token.role = role; + + if (authType === AuthType.KEYCLOAK) { + token.refreshToken = account.refresh_token; + token.accessTokenExpires = + Date.now() + (account.expires_in as number) * 1000; + } + } else if ( + authType === AuthType.KEYCLOAK && + token.accessTokenExpires && + typeof token.accessTokenExpires === "number" && + Date.now() < token.accessTokenExpires + ) { + token = await refreshAccessToken(token); + if (!token.accessToken) { + throw new Error("Failed to refresh access token"); + } + } + + return token; + }, + session: async ({ session, token, user }) => { + return { + ...session, + accessToken: token.accessToken as string, + tenantId: token.tenantId as string, + userRole: token.role as string, + user: { + ...session.user, + accessToken: token.accessToken as string, + tenantId: token.tenantId as string, + role: token.role as string, + }, + }; + }, + }, +} satisfies NextAuthConfig; diff --git a/keep-ui/auth.ts b/keep-ui/auth.ts index 159de7439..6c2cb25a6 100644 --- a/keep-ui/auth.ts +++ b/keep-ui/auth.ts @@ -1,264 +1,103 @@ import NextAuth from "next-auth"; -import type { NextAuthConfig } from "next-auth"; -import Credentials from "next-auth/providers/credentials"; -import Keycloak from "next-auth/providers/keycloak"; -import Auth0 from "next-auth/providers/auth0"; -import MicrosoftEntraID from "@auth/core/providers/microsoft-entra-id"; -import { AuthError } from "next-auth"; -import { AuthenticationError, AuthErrorCodes } from "@/errors"; -import type { JWT } from "@auth/core/jwt"; +import { customFetch } from "next-auth"; +import { config, authType } from "@/auth.config"; +import { ProxyAgent, fetch as undici } from "undici"; +import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; +import { AuthType } from "@/utils/authenticationType"; + +const proxyUrl = + process.env.HTTP_PROXY || + process.env.HTTPS_PROXY || + process.env.http_proxy || + process.env.https_proxy; + +function proxyFetch( + ...args: Parameters +): ReturnType { + console.log( + "Proxy called for URL:", + args[0] instanceof Request ? args[0].url : args[0] + ); + const dispatcher = new ProxyAgent(proxyUrl!); + + if (args[0] instanceof Request) { + const request = args[0]; + // @ts-expect-error `undici` has a `duplex` option + return undici(request.url, { + ...args[1], + method: request.method, + headers: request.headers as HeadersInit, + body: request.body, + dispatcher, + }); + } -export class BackendRefusedError extends AuthError { - static type = "BackendRefusedError"; + // @ts-expect-error `undici` has a `duplex` option + return undici(args[0], { ...(args[1] || {}), dispatcher }); } -import { getApiURL } from "@/utils/apiUrl"; -import { - AuthType, - MULTI_TENANT, - SINGLE_TENANT, - NO_AUTH, - NoAuthUserEmail, - NoAuthTenant, -} from "@/utils/authenticationType"; -import type { User } from "next-auth"; - -// Determine auth type with backward compatibility -const authTypeEnv = process.env.AUTH_TYPE; -const authType = - authTypeEnv === MULTI_TENANT - ? AuthType.AUTH0 - : authTypeEnv === SINGLE_TENANT - ? AuthType.DB - : authTypeEnv === NO_AUTH - ? AuthType.NOAUTH - : (authTypeEnv as AuthType); +// Modify the config if using Azure AD with proxy +if (authType === AuthType.AZUREAD && proxyUrl) { + const provider = config.providers[0] as ReturnType; -async function refreshAccessToken(token: any) { - const issuerUrl = process.env.KEYCLOAK_ISSUER; - const refreshTokenUrl = `${issuerUrl}/protocol/openid-connect/token`; - - try { - const response = await fetch(refreshTokenUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - client_id: process.env.KEYCLOAK_ID!, - client_secret: process.env.KEYCLOAK_SECRET!, - grant_type: "refresh_token", - refresh_token: token.refreshToken, - }), - }); + if (!proxyUrl) { + console.log("Proxy is not enabled for Azure AD"); + } else { + console.log("Proxy is enabled for Azure AD:", proxyUrl); + } - const refreshedTokens = await response.json(); + // Override the `customFetch` symbol in the provider + provider[customFetch] = async (...args: Parameters) => { + const url = new URL(args[0] instanceof Request ? args[0].url : args[0]); + console.log("Custom Fetch Intercepted:", url.toString()); + + // Handle `.well-known/openid-configuration` logic + if (url.pathname.endsWith(".well-known/openid-configuration")) { + console.log("Intercepting .well-known/openid-configuration"); + const response = await proxyFetch(...args); + const json = await response.clone().json(); + const tenantRe = /microsoftonline\.com\/(\w+)\/v2\.0/; + const tenantId = provider.issuer?.match(tenantRe)?.[1] ?? "common"; + const issuer = json.issuer.replace("{tenantid}", tenantId); + console.log("Modified issuer:", issuer); + return Response.json({ ...json, issuer }); + } - if (!response.ok) { - throw new Error( - `Refresh token failed: ${response.status} ${response.statusText}` - ); + // Fallback for all other requests + return proxyFetch(...args); + }; + + // Override profile since it uses fetch without customFetch + provider.profile = async (profile, tokens) => { + const profilePhotoSize = 48; + console.log("Fetching profile photo via proxy"); + + const response = await proxyFetch( + `https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`, + { headers: { Authorization: `Bearer ${tokens.access_token}` } } + ); + + let image: string | null = null; + if (response.ok && typeof Buffer !== "undefined") { + try { + const pictureBuffer = await response.arrayBuffer(); + const pictureBase64 = Buffer.from(pictureBuffer).toString("base64"); + image = `data:image/jpeg;base64,${pictureBase64}`; + } catch (error) { + console.error("Error processing profile photo:", error); + } } return { - ...token, - accessToken: refreshedTokens.access_token, - accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000, - refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, - }; - } catch (error) { - console.error("Error refreshing access token:", error); - return { - ...token, - error: "RefreshAccessTokenError", + id: profile.sub, + name: profile.name, + email: profile.email, + image: image ?? null, + accessToken: tokens.access_token ?? "", }; - } + }; } -// Define provider configurations -const providerConfigs = { - [AuthType.AUTH0]: [ - Auth0({ - clientId: process.env.AUTH0_CLIENT_ID!, - clientSecret: process.env.AUTH0_CLIENT_SECRET!, - issuer: process.env.AUTH0_ISSUER!, - authorization: { - params: { - prompt: "login", - }, - }, - }), - ], - [AuthType.DB]: [ - Credentials({ - name: "Credentials", - credentials: { - username: { label: "Username", type: "text", placeholder: "keep" }, - password: { label: "Password", type: "password", placeholder: "keep" }, - }, - async authorize(credentials): Promise { - try { - const response = await fetch(`${getApiURL()}/signin`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(credentials), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - console.error("Authentication failed:", errorData); - throw new AuthenticationError(AuthErrorCodes.INVALID_CREDENTIALS); - } - - const user = await response.json(); - if (!user.accessToken) return null; - - return { - id: user.id, - name: user.name, - email: user.email, - accessToken: user.accessToken, - tenantId: user.tenantId, - role: user.role, - }; - } catch (error) { - if (error instanceof TypeError && error.message === "fetch failed") { - throw new AuthenticationError(AuthErrorCodes.CONNECTION_REFUSED); - } - - if (error instanceof AuthenticationError) { - throw error; - } - - throw new AuthenticationError(AuthErrorCodes.SERVICE_UNAVAILABLE); - } - }, - }), - ], - [AuthType.NOAUTH]: [ - Credentials({ - name: "NoAuth", - credentials: {}, - async authorize(): Promise { - return { - id: "keep-user-for-no-auth-purposes", - name: "Keep", - email: NoAuthUserEmail, - accessToken: "keep-token-for-no-auth-purposes", - tenantId: NoAuthTenant, - role: "user", - }; - }, - }), - ], - [AuthType.KEYCLOAK]: [ - Keycloak({ - clientId: process.env.KEYCLOAK_ID!, - clientSecret: process.env.KEYCLOAK_SECRET!, - issuer: process.env.KEYCLOAK_ISSUER, - authorization: { params: { scope: "openid email profile roles" } }, - }), - ], - [AuthType.AZUREAD]: [ - MicrosoftEntraID({ - clientId: process.env.KEEP_AZUREAD_CLIENT_ID!, - clientSecret: process.env.KEEP_AZUREAD_CLIENT_SECRET!, - issuer: process.env.KEEP_AZUREAD_TENANT_ID!, - authorization: { - params: { - scope: `api://${process.env - .KEEP_AZUREAD_CLIENT_ID!}/default openid profile email`, - }, - }, - }), - ], -}; - -// Create the config -const config = { - trustHost: true, - providers: - providerConfigs[authType as keyof typeof providerConfigs] || - providerConfigs[AuthType.NOAUTH], - pages: { - signIn: "/signin", - }, - session: { - strategy: "jwt" as const, - maxAge: 30 * 24 * 60 * 60, // 30 days - }, - callbacks: { - authorized({ auth, request: { nextUrl } }) { - const isLoggedIn = !!auth?.user; - const isOnDashboard = nextUrl.pathname.startsWith("/dashboard"); - if (isOnDashboard) { - if (isLoggedIn) return true; - return false; - } - return true; - }, - jwt: async ({ token, user, account, profile }): Promise => { - if (account && user) { - let accessToken: string | undefined; - let tenantId: string | undefined = user.tenantId; - let role: string | undefined = user.role; - // Ensure we always have an accessToken - if (authType == AuthType.AUTH0) { - accessToken = account.id_token; - if ((profile as any)?.keep_tenant_id) { - tenantId = (profile as any).keep_tenant_id; - } - if ((profile as any)?.keep_role) { - role = (profile as any).keep_role; - } - } else { - accessToken = - user.accessToken || account.access_token || account.id_token; - } - if (!accessToken) { - throw new Error("No access token available"); - } - - token.accessToken = accessToken; - token.tenantId = tenantId; - token.role = role; - - if (authType === AuthType.KEYCLOAK) { - token.refreshToken = account.refresh_token; - token.accessTokenExpires = - Date.now() + (account.expires_in as number) * 1000; - } - } else if ( - authType === AuthType.KEYCLOAK && - token.accessTokenExpires && - typeof token.accessTokenExpires === "number" && - Date.now() < token.accessTokenExpires - ) { - token = await refreshAccessToken(token); - if (!token.accessToken) { - throw new Error("Failed to refresh access token"); - } - } - - return token; - }, - session: async ({ session, token, user }) => { - return { - ...session, - accessToken: token.accessToken as string, - tenantId: token.tenantId as string, - userRole: token.role as string, - user: { - ...session.user, - accessToken: token.accessToken as string, - tenantId: token.tenantId as string, - role: token.role as string, - }, - }; - }, - }, -} satisfies NextAuthConfig; - console.log("Starting Keep frontend with auth type:", authType); export const { handlers, auth, signIn, signOut } = NextAuth(config); diff --git a/keep-ui/middleware.tsx b/keep-ui/middleware.tsx index 052c05792..ccdaa8355 100644 --- a/keep-ui/middleware.tsx +++ b/keep-ui/middleware.tsx @@ -1,15 +1,21 @@ -import { auth } from "@/auth"; -import { NextResponse } from "next/server"; +import { NextResponse, type NextRequest } from "next/server"; +import { getToken } from "next-auth/jwt"; +import type { JWT } from "next-auth/jwt"; import { getApiURL } from "@/utils/apiUrl"; +import { config as authConfig } from "@/auth.config"; +import NextAuth from "next-auth"; -// Use auth as a wrapper for middleware logic -export default auth(async (req) => { - const { pathname, searchParams } = req.nextUrl; +const { auth } = NextAuth(authConfig); - // Keep it on header so it can be used in server components - const requestHeaders = new Headers(req.headers); - requestHeaders.set("x-url", req.url); +export async function middleware(request: NextRequest) { + const { pathname, searchParams } = request.nextUrl; + const session = await auth(); + const role = session?.userRole; + const isAuthenticated = !!session; + // Keep it on header so it can be used in server components + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-url", request.url); // Handle legacy /backend/ redirects if (pathname.startsWith("/backend/")) { const apiUrl = getApiURL(); @@ -25,36 +31,37 @@ export default auth(async (req) => { if (pathname.startsWith("/api/")) { return NextResponse.next(); } + // If not authenticated and not on signin page, redirect to signin - if (!req.auth && !pathname.startsWith("/signin")) { + if (!isAuthenticated && !pathname.startsWith("/signin")) { console.log("Redirecting to signin page because user is not authenticated"); - return NextResponse.redirect(new URL("/signin", req.url)); + return NextResponse.redirect(new URL("/signin", request.url)); } - // else if authenticated and on signin page, redirect to dashboard - if (req.auth && pathname.startsWith("/signin")) { + // If authenticated and on signin page, redirect to incidents + if (isAuthenticated && pathname.startsWith("/signin")) { console.log( "Redirecting to incidents because user try to get /signin but already authenticated" ); - return NextResponse.redirect(new URL("/incidents", req.url)); + return NextResponse.redirect(new URL("/incidents", request.url)); } // Role-based routing (NOC users) - if (req.auth?.user?.role === "noc" && !pathname.startsWith("/alerts")) { - return NextResponse.redirect(new URL("/alerts/feed", req.url)); + if (role === "noc" && !pathname.startsWith("/alerts")) { + return NextResponse.redirect(new URL("/alerts/feed", request.url)); } // Allow all other authenticated requests console.log("Allowing request to pass through"); - console.log("Request URL: ", req.url); - // console.log("Request headers: ", requestHeaders) + console.log("Request URL: ", request.url); + return NextResponse.next({ request: { // Apply new request headers headers: requestHeaders, }, }); -}); +} // Update the matcher to handle static files and public routes export const config = { diff --git a/keep-ui/next.config.js b/keep-ui/next.config.js index 47f5c2ca4..6d4ace32f 100644 --- a/keep-ui/next.config.js +++ b/keep-ui/next.config.js @@ -3,6 +3,29 @@ const { withSentryConfig } = require("@sentry/nextjs"); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, + webpack: ( + config, + { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack } + ) => { + // Only apply proxy configuration for Node.js server runtime + if (isServer && nextRuntime === "nodejs") { + // Add environment variables for proxy at build time + config.plugins.push( + new webpack.DefinePlugin({ + "process.env.IS_NODEJS_RUNTIME": JSON.stringify(true), + }) + ); + } else { + // For edge runtime and client + config.plugins.push( + new webpack.DefinePlugin({ + "process.env.IS_NODEJS_RUNTIME": JSON.stringify(false), + }) + ); + } + + return config; + }, transpilePackages: ["next-auth"], images: { remotePatterns: [ @@ -29,12 +52,7 @@ const nextConfig = { ], }, compiler: { - removeConsole: - process.env.NODE_ENV === "production" - ? { - exclude: ["error"], - } - : process.env.REMOVE_CONSOLE === "true", + removeConsole: false, }, output: "standalone", productionBrowserSourceMaps: diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 11e155b6a..c6e0749ac 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@auth/core": "^0.37.4", "@boiseitguru/cookie-cutter": "^0.2.3", "@copilotkit/react-core": "^1.3.15", "@copilotkit/react-ui": "^1.3.15", @@ -48,6 +49,7 @@ "eslint-scope": "^7.2.0", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.4.1", + "https-proxy-agent": "^7.0.5", "js-yaml": "^4.1.0", "lodash.debounce": "^4.0.8", "lucide-react": "^0.460.0", @@ -97,6 +99,7 @@ "swr": "^2.2.5", "tailwind-merge": "^1.12.0", "tailwindcss": "^3.4.1", + "undici": "^6.21.0", "uuid": "^8.3.2", "yaml": "^2.2.2", "zustand": "^5.0.1" @@ -196,18 +199,16 @@ } }, "node_modules/@auth/core": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", - "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "version": "0.37.4", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.4.tgz", + "integrity": "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw==", "license": "ISC", "dependencies": { "@panva/hkdf": "^1.2.1", - "@types/cookie": "0.6.0", - "cookie": "0.7.1", - "jose": "^5.9.3", - "oauth4webapi": "^3.0.0", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" + "jose": "^5.9.6", + "oauth4webapi": "^3.1.1", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", @@ -226,16 +227,6 @@ } } }, - "node_modules/@auth/core/node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -6964,6 +6955,31 @@ "node": ">=10" } }, + "node_modules/@sentry/cli/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sentry/cli/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@sentry/core": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.38.0.tgz", @@ -8681,14 +8697,15 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/agentkeepalive": { @@ -12119,6 +12136,31 @@ "node": ">=12" } }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gcp-metadata": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", @@ -12860,27 +12902,17 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/humanize-ms": { @@ -13692,29 +13724,6 @@ } } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -15783,6 +15792,59 @@ } } }, + "node_modules/next-auth/node_modules/@auth/core": { + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", + "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "@types/cookie": "0.6.0", + "cookie": "0.7.1", + "jose": "^5.9.3", + "oauth4webapi": "^3.0.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-auth/node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/next-auth/node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -16723,13 +16785,10 @@ } }, "node_modules/preact-render-to-string": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", - "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", "license": "MIT", - "dependencies": { - "pretty-format": "^3.8.0" - }, "peerDependencies": { "preact": ">=10" } @@ -20011,6 +20070,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", + "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index 172d2dfd2..4678a06dc 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -10,6 +10,7 @@ "lint": "next lint" }, "dependencies": { + "@auth/core": "^0.37.4", "@boiseitguru/cookie-cutter": "^0.2.3", "@copilotkit/react-core": "^1.3.15", "@copilotkit/react-ui": "^1.3.15", @@ -49,6 +50,7 @@ "eslint-scope": "^7.2.0", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.4.1", + "https-proxy-agent": "^7.0.5", "js-yaml": "^4.1.0", "lodash.debounce": "^4.0.8", "lucide-react": "^0.460.0", @@ -98,6 +100,7 @@ "swr": "^2.2.5", "tailwind-merge": "^1.12.0", "tailwindcss": "^3.4.1", + "undici": "^6.21.0", "uuid": "^8.3.2", "yaml": "^2.2.2", "zustand": "^5.0.1" diff --git a/keep-ui/proxyFetch.node.ts b/keep-ui/proxyFetch.node.ts new file mode 100644 index 000000000..76defb940 --- /dev/null +++ b/keep-ui/proxyFetch.node.ts @@ -0,0 +1,24 @@ +// proxyFetch.node.ts +import { ProxyAgent, fetch as undici } from "undici"; +import type { ProxyFetchFn } from "./proxyFetch"; + +export const createProxyFetch = async (): Promise => { + const proxyUrl = + process.env.HTTP_PROXY || + process.env.HTTPS_PROXY || + process.env.http_proxy || + process.env.https_proxy; + + if (!proxyUrl) { + return undefined; + } + + const dispatcher = new ProxyAgent(proxyUrl); + + return function proxy( + ...args: Parameters + ): ReturnType { + // @ts-expect-error `undici` has a `duplex` option + return undici(args[0], { ...args[1], dispatcher }); + }; +}; diff --git a/keep-ui/proxyFetch.ts b/keep-ui/proxyFetch.ts new file mode 100644 index 000000000..d0d1c936e --- /dev/null +++ b/keep-ui/proxyFetch.ts @@ -0,0 +1,11 @@ +// proxyFetch.ts + +// We only export the type from this file +export type ProxyFetchFn = ( + ...args: Parameters +) => ReturnType; + +// This function will be imported dynamically only in Node.js environment +export const createProxyFetch = async (): Promise => { + return undefined; +}; diff --git a/keep-ui/tsconfig.json b/keep-ui/tsconfig.json index 2087f066f..7aceabb38 100644 --- a/keep-ui/tsconfig.json +++ b/keep-ui/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "sourceMap": true, "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, diff --git a/keep-ui/types/auth.d.ts b/keep-ui/types/auth.d.ts index 22237903d..6dd72458c 100644 --- a/keep-ui/types/auth.d.ts +++ b/keep-ui/types/auth.d.ts @@ -1,6 +1,7 @@ -import type { DefaultSession } from "@auth/core/types"; +import type { DefaultSession } from "next-auth"; +import type { JWT } from "next-auth/jwt"; -declare module "@auth/core/types" { +declare module "next-auth" { interface Session { accessToken: string; tenantId?: string; @@ -13,7 +14,7 @@ declare module "@auth/core/types" { accessToken: string; tenantId?: string; role?: string; - }; + } & DefaultSession["user"]; } interface User { @@ -26,9 +27,9 @@ declare module "@auth/core/types" { } } -declare module "@auth/core/jwt" { +declare module "next-auth/jwt" { interface JWT { - accessToken: string; // Changed to required + accessToken: string; tenantId?: string; role?: string; } diff --git a/proxy/README.md b/proxy/README.md new file mode 100644 index 000000000..524e922e5 --- /dev/null +++ b/proxy/README.md @@ -0,0 +1,137 @@ +# Development Proxy Setup + +This directory contains the configuration files and Docker services needed to run Keep with a proxy setup, primarily used for testing and development scenarios requiring proxy configurations (e.g., corporate environments, Azure AD authentication). + +## Directory Structure + +``` +proxy/ +├── docker-compose-proxy.yml # Docker Compose configuration for proxy setup +├── squid.conf # Squid proxy configuration +├── nginx.conf # Nginx reverse proxy configuration +└── README.md # This file +``` + +## Components + +The setup consists of several services: + +- **Squid Proxy**: Acts as a forward proxy for HTTP/HTTPS traffic +- **Nginx**: Serves as a reverse proxy/tunnel +- **Keep Frontend**: The Keep UI service configured to use the proxy +- **Keep Backend**: The Keep API service +- **Keep WebSocket**: The WebSocket server for real-time updates + +## Network Architecture + +The setup uses two Docker networks: + +- `proxy-net`: External network for proxy communication +- `internal`: Internal network with no external access (secure network for inter-service communication) + +## Configuration + +### Environment Variables + +The Keep Frontend service is preconfigured with proxy-related environment variables: + +```env +http_proxy=http://proxy:3128 +https_proxy=http://proxy:3128 +HTTP_PROXY=http://proxy:3128 +HTTPS_PROXY=http://proxy:3128 +npm_config_proxy=http://proxy:3128 +npm_config_https_proxy=http://proxy:3128 +``` + +### Usage + +1. Start the proxy environment: + +```bash +docker compose -f docker-compose-proxy.yml up +``` + +2. To run in detached mode: + +```bash +docker compose -f docker-compose-proxy.yml up -d +``` + +3. To stop all services: + +```bash +docker compose -f docker-compose-proxy.yml down +``` + +### Accessing Services + +- Keep Frontend: http://localhost:3000 +- Keep Backend: http://localhost:8080 +- Squid Proxy: localhost:3128 + +## Custom Configuration + +### Modifying Proxy Settings + +To modify the Squid proxy configuration: + +1. Edit `squid.conf` +2. Restart the proxy service: + +```bash +docker compose -f docker-compose-proxy.yml restart proxy +``` + +### Modifying Nginx Settings + +To modify the Nginx reverse proxy configuration: + +1. Edit `nginx.conf` +2. Restart the nginx service: + +```bash +docker compose -f docker-compose-proxy.yml restart tunnel +``` + +## Troubleshooting + +If you encounter connection issues: + +1. Verify proxy is running: + +```bash +docker compose -f docker-compose-proxy.yml ps +``` + +2. Check proxy logs: + +```bash +docker compose -f docker-compose-proxy.yml logs proxy +``` + +3. Test proxy connection: + +```bash +curl -x http://localhost:3128 https://www.google.com +``` + +## Development Notes + +- The proxy setup is primarily intended for development and testing +- When using Azure AD authentication, ensure the proxy configuration matches your environment's requirements +- SSL certificate validation is disabled by default for development purposes (`npm_config_strict_ssl=false`) + +## Security Considerations + +- This setup is intended for development environments only +- The internal network is isolated from external access for security +- Modify security settings in `squid.conf` and `nginx.conf` according to your requirements + +## Contributing + +When modifying the proxy setup: + +1. Document any changes to configuration files +2. Test the setup with both proxy and non-proxy environments +3. Update this README if adding new features or configurations diff --git a/proxy/docker-compose-proxy.yml b/proxy/docker-compose-proxy.yml index ac4a61689..a75ead324 100644 --- a/proxy/docker-compose-proxy.yml +++ b/proxy/docker-compose-proxy.yml @@ -26,7 +26,7 @@ services: ports: - "3000:3000" extends: - file: docker-compose.common.yml + file: ../docker-compose.common.yml service: keep-frontend-common image: us-central1-docker.pkg.dev/keephq/keep/keep-ui:feature_proxy environment: @@ -49,15 +49,15 @@ services: depends_on: - keep-backend - proxy - # networks: - # - proxy-net - # - internal + networks: + # - proxy-net + - internal keep-backend: ports: - "8080:8080" extends: - file: docker-compose.common.yml + file: ../docker-compose.common.yml service: keep-backend-common image: us-central1-docker.pkg.dev/keephq/keep/keep-api environment: @@ -70,7 +70,7 @@ services: keep-websocket-server: extends: - file: docker-compose.common.yml + file: ../docker-compose.common.yml service: keep-websocket-server-common networks: - internal diff --git a/proxy/squid.conf b/proxy/squid.conf index e36a34687..2e569360f 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -1,26 +1,31 @@ # Port configurations http_port 3128 -dns_nameservers 8.8.8.8 8.8.4.4 # Google DNS servers, adjust as needed # DNS configurations +dns_nameservers 8.8.8.8 8.8.4.4 dns_v4_first on -dns_timeout 5 seconds -positive_dns_ttl 24 hours -negative_dns_ttl 1 minutes -# Allow all clients in our Docker network -acl localnet src 172.16.0.0/12 +# ACL definitions +acl SSL_ports port 443 +acl Safe_ports port 80 # http +acl Safe_ports port 443 # https +acl Safe_ports port 1025-65535 # unprivileged ports +acl CONNECT method CONNECT +acl localnet src 172.16.0.0/12 # Docker network + +# Access rules - order is important +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localnet http_access allow all -# Basic settings +# Logging +debug_options ALL,1 28,3 + +# Cache settings cache_dir ufs /var/spool/squid 100 16 256 coredump_dir /var/spool/squid -# DNS cache settings -ipcache_size 1024 -ipcache_low 90 -ipcache_high 95 - # Refresh patterns refresh_pattern ^ftp: 1440 20% 10080 refresh_pattern ^gopher: 1440 0% 1440 diff --git a/pyproject.toml b/pyproject.toml index 85a037ae5..83eeea92c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "keep" -version = "0.29.3" +version = "0.29.5" description = "Alerting. for developers, by developers." authors = ["Keep Alerting LTD"] packages = [{include = "keep"}] diff --git a/scripts/docs_get_providers_list.py b/scripts/docs_get_providers_list.py index e2459c12c..caee00c79 100644 --- a/scripts/docs_get_providers_list.py +++ b/scripts/docs_get_providers_list.py @@ -14,9 +14,8 @@ LOGO_DEV_PUBLISHABLE_KEY = "pk_dfXfZBoKQMGDTIgqu7LvYg" -NON_DOCUMENTED_PROVIDERS = ( - [] -) # known not documented providers https://github.com/keephq/keep/issues/2033 +NON_DOCUMENTED_PROVIDERS = [ +] def validate_overview_is_complete(documented_providers): @@ -49,6 +48,7 @@ def validate_all_providers_are_documented(documented_providers): if ( provider_name not in documented_providers and provider_name not in NON_DOCUMENTED_PROVIDERS + and not provider.coming_soon ): raise Exception( f"""Provider "{provider_name}" is not documented in the docs/providers/documentation folder,