Skip to content

Commit

Permalink
feat(auth): oauth2proxy (keephq#2014)
Browse files Browse the repository at this point in the history
Co-authored-by: Tal <[email protected]>
  • Loading branch information
shahargl and talboren authored Oct 1, 2024
1 parent b3669f1 commit 6cbfeca
Show file tree
Hide file tree
Showing 20 changed files with 391 additions and 102 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ tempo-data/
# docs
docs/node_modules/

oauth2.cfg


scripts/automatic_extraction_rules.py

Expand All @@ -209,3 +211,4 @@ ee/experimental/ai_temp/*

oauth2.cfg
scripts/keep_slack_bot.py
keepnew.db
32 changes: 32 additions & 0 deletions docs/deployment/authentication/oauth2proxy-auth.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: "OAuth2Proxy Authentication"
---

Delegate authentication to Oauth2Proxy.


### When to Use

- **oauth2-proxy user:** Use this authentication method if you want to delegate authentication to an external Oauth2Proxy service.

### Setup Instructions

To start Keep with Oauth2Proxy authentication, set the following environment variables:

#### Frontend Environment Variables

| Environment Variable | Description | Required | Default Value |
|--------------------|-----------|:--------:|:-------------:|
| AUTH_TYPE | Set to 'OAUTH2PROXY' for OAUTH2PROXY authentication | Yes | - |

#### Backend Environment Variables

| Environment Variable | Description | Required | Default Value |
|--------------------|-----------|:--------:|:-------------:|
| AUTH_TYPE | Set to 'OAUTH2PROXY' for OAUTH2PROXY authentication | Yes | - |
| KEEP_OAUTH2_PROXY_USER_HEADER | Header for the authenticated user's email | Yes | x-forwarded-email |
| KEEP_OAUTH2_PROXY_ROLE_HEADER | Header for the authenticated user's role | Yes | x-forwarded-groups |
| KEEP_OAUTH2_PROXY_AUTO_CREATE_USER | Automatically create user if not exists | No | true |
| KEEP_OAUTH2_PROXY_ADMIN_ROLE | Role name for admin users | No | admin |
| KEEP_OAUTH2_PROXY_NOC_ROLE | Role name for NOC (Network Operations Center) users | No | noc |
| KEEP_OAUTH2_PROXY_WEBHOOK_ROLE | Role name for webhook users | No | webhook |
14 changes: 8 additions & 6 deletions docs/deployment/authentication/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ Choosing the right authentication strategy depends on your specific use case, se

### Authentication Features Comparison

| Identity Provider | RBAC | SAML/OIDC | SSO | LDAP | Resource-based permission | User Management | Group Management | On Prem | License |
|:---:|:----:|:---------:|:---:|:----:|:-------------------------:|:----------------:|:-----------------:|:-------:|:-------:|
| **No Auth** ||||||||| **OSS** |
| **DB** | ✅ <br />(Predefiend roles) |||||||| **OSS** |
| **Auth0** | ✅ <br />(Predefiend roles) ||| 🚧 | 🚧 || 🚧 || **EE** |
| **Keycloak** | ✅ <br />(Custom roles) |||||||| **EE** |
| Identity Provider | RBAC | SAML/OIDC/SSO | LDAP | Resource-based permission | User Management | Group Management | On Prem | License |
|:---:|:----:|:---------:|:----:|:-------------------------:|:----------------:|:-----------------:|:-------:|:-------:|
| **No Auth** |||||||| **OSS** |
| **DB** | ✅ <br />(Predefiend roles) ||||||| **OSS** |
| **Auth0** | ✅ <br />(Predefiend roles) || 🚧 | 🚧 || 🚧 || **EE** |
| **Keycloak** | ✅ <br />(Custom roles) ||||||| **EE** |
| **Oauth2Proxy** | ✅ <br />(Predefiend roles) |||| N/A | N/A || **OSS** |
### How To Configure
<Tip>
Some authentication providers require additional environment variables. These will be covered in detail on the specific authentication provider pages.
Expand All @@ -39,5 +40,6 @@ The authentication scheme on Keep is controlled with environment variables both
| **DB** | `AUTH_TYPE=DB` | `KEEP_JWT_SECRET` |
| **Auth0** | `AUTH_TYPE=AUTH0` | `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET` |
| **Keycloak** | `AUTH_TYPE=KEYCLOAK` | `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_CLIENT_SECRET` |
| **Oauth2Proxy** | `AUTH_TYPE=OAUTH2PROXY` | `OAUTH2_PROXY_USER_HEADER`, `OAUTH2_PROXY_ROLE_HEADER`, `OAUTH2_PROXY_AUTO_CREATE_USER` |

For more details on each authentication strategy, including setup instructions and implications, refer to the respective sections.
3 changes: 2 additions & 1 deletion docs/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"deployment/authentication/no-auth",
"deployment/authentication/db-auth",
"deployment/authentication/auth0-auth",
"deployment/authentication/keycloak-auth"
"deployment/authentication/keycloak-auth",
"deployment/authentication/oauth2proxy-auth"
]
},
{
Expand Down
1 change: 1 addition & 0 deletions keep-ui/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ export const authOptions =
? singleTenantAuthOptions
: authType === AuthenticationType.KEYCLOAK
? keycloakAuthOptions
// oauth2proxy same configuration as noauth
: noAuthOptions;

export default NextAuth(authOptions);
1 change: 1 addition & 0 deletions keep-ui/utils/authenticationType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum AuthenticationType {
AUTH0 = "AUTH0",
DB = "DB",
KEYCLOAK = "KEYCLOAK",
OAUTH2PROXY = "OAUTH2PROXY",
NOAUTH = "NOAUTH" // Default
}

Expand Down
15 changes: 11 additions & 4 deletions keep/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
KEEP_ARQ_TASK_POOL_BASIC_PROCESSING,
KEEP_ARQ_TASK_POOL_NONE,
)
from keep.api.core.config import AuthenticationType
from keep.api.core.db import get_api_key
from keep.api.core.dependencies import SINGLE_TENANT_UUID
from keep.api.logging import CONFIG as logging_config
Expand Down Expand Up @@ -59,7 +58,10 @@
from keep.api.routes.auth import groups as auth_groups
from keep.api.routes.auth import permissions, roles, users
from keep.event_subscriber.event_subscriber import EventSubscriber
from keep.identitymanager.identitymanagerfactory import IdentityManagerFactory
from keep.identitymanager.identitymanagerfactory import (
IdentityManagerFactory,
IdentityManagerTypes,
)
from keep.posthog.posthog import get_posthog_client

# load all providers into cache
Expand All @@ -77,7 +79,7 @@
SCHEDULER = os.environ.get("SCHEDULER", "true") == "true"
CONSUMER = os.environ.get("CONSUMER", "true") == "true"

AUTH_TYPE = os.environ.get("AUTH_TYPE", AuthenticationType.NO_AUTH.value)
AUTH_TYPE = os.environ.get("AUTH_TYPE", IdentityManagerTypes.NOAUTH.value).lower()
PROVISION_RESOURCES = os.environ.get("PROVISION_RESOURCES", "true") == "true"
try:
KEEP_VERSION = metadata.version("keep")
Expand Down Expand Up @@ -178,7 +180,7 @@ async def dispatch(self, request: Request, call_next):


def get_app(
auth_type: AuthenticationType = AuthenticationType.NO_AUTH.value,
auth_type: IdentityManagerTypes = IdentityManagerTypes.NOAUTH.value,
) -> FastAPI:
if not os.environ.get("KEEP_API_URL", None):
os.environ["KEEP_API_URL"] = f"http://{HOST}:{PORT}"
Expand Down Expand Up @@ -344,6 +346,11 @@ async def log_middleware(request: Request, call_next):
f"Request started: {request.method} {request.url.path}",
extra={"tenant_id": identity},
)

# for debugging purposes, log the payload
if os.environ.get("LOG_AUTH_PAYLOAD", "false") == "true":
logger.info(f"Request headers: {request.headers}")

start_time = time.time()
request.state.tenant_id = identity
response = await call_next(request)
Expand Down
19 changes: 14 additions & 5 deletions keep/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import keep.api.logging
from keep.api.api import AUTH_TYPE
from keep.api.core.config import AuthenticationType
from keep.api.core.db_on_start import migrate_db, try_create_single_tenant
from keep.api.core.dependencies import SINGLE_TENANT_UUID
from keep.identitymanager.identitymanagerfactory import IdentityManagerTypes

PORT = int(os.environ.get("PORT", 8080))

Expand All @@ -16,15 +16,24 @@
def on_starting(server=None):
"""This function is called by the gunicorn server when it starts"""
logger.info("Keep server starting")

migrate_db()

# Create single tenant if it doesn't exist
if AUTH_TYPE in [
AuthenticationType.SINGLE_TENANT.value,
AuthenticationType.NO_AUTH.value,
IdentityManagerTypes.DB.value,
IdentityManagerTypes.NOAUTH.value,
IdentityManagerTypes.OAUTH2PROXY.value,
"no_auth", # backwards compatibility
"single_tenant", # backwards compatibility
]:
try_create_single_tenant(SINGLE_TENANT_UUID)
# for oauth2proxy, we don't want to create the default user
try_create_single_tenant(
SINGLE_TENANT_UUID,
create_default_user=(
False if AUTH_TYPE == IdentityManagerTypes.OAUTH2PROXY.value else True
),
)

if os.environ.get("USE_NGROK", "false") == "true":
from pyngrok import ngrok
Expand Down
8 changes: 0 additions & 8 deletions keep/api/core/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import pathlib
from enum import Enum

from starlette.config import Config

Expand All @@ -10,10 +9,3 @@
config = Config(BASE_DIR / ".env")
except FileNotFoundError:
config = Config()


class AuthenticationType(Enum):
MULTI_TENANT = "MULTI_TENANT"
SINGLE_TENANT = "SINGLE_TENANT"
KEYCLOAK = "KEYCLOAK"
NO_AUTH = "NO_AUTH"
32 changes: 32 additions & 0 deletions keep/api/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,38 @@ def create_user(tenant_id, username, password, role):
return user


def update_user_last_sign_in(tenant_id, username):
from keep.api.models.db.user import User

with Session(engine) as session:
user = session.exec(
select(User)
.where(User.tenant_id == tenant_id)
.where(User.username == username)
).first()
if user:
user.last_sign_in = datetime.utcnow()
session.add(user)
session.commit()
return user


def update_user_role(tenant_id, username, role):
from keep.api.models.db.user import User

with Session(engine) as session:
user = session.exec(
select(User)
.where(User.tenant_id == tenant_id)
.where(User.username == username)
).first()
if user and user.role != role:
user.role = role
session.add(user)
session.commit()
return user


def save_workflow_results(tenant_id, workflow_execution_id, workflow_results):
with Session(engine) as session:
workflow_execution = session.exec(
Expand Down
8 changes: 4 additions & 4 deletions keep/api/core/db_on_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
engine = create_db_engine()


def try_create_single_tenant(tenant_id: str) -> None:
def try_create_single_tenant(tenant_id: str, create_default_user=True) -> None:
"""
Creates the single tenant and the default user if they don't exist.
"""
Expand Down Expand Up @@ -71,7 +71,7 @@ def try_create_single_tenant(tenant_id: str) -> None:
# check if at least one user exists:
user = session.exec(select(User)).first()
# if no users exist, let's create the default user
if not user:
if not user and create_default_user:
logger.info("Creating default user")
default_username = os.environ.get("KEEP_DEFAULT_USERNAME", "keep")
default_password = hashlib.sha256(
Expand Down Expand Up @@ -150,7 +150,7 @@ def try_create_single_tenant(tenant_id: str) -> None:
pass
logger.info(f"Api key {api_key_name} provisioned")
logger.info("Api keys provisioned")

# commit the changes
session.commit()
logger.info("Single tenant created")
Expand Down Expand Up @@ -181,4 +181,4 @@ def migrate_db():
os.path.dirname(os.path.abspath(__file__)) + "/../models/db/migrations",
)
alembic.command.upgrade(config, "head")
logger.info("Finished migrations")
logger.info("Finished migrations")
64 changes: 1 addition & 63 deletions keep/api/routes/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import io
import json
import logging
import os
import smtplib
from email.mime.text import MIMEText
from typing import Optional, Tuple
Expand All @@ -11,11 +10,10 @@
from pydantic import BaseModel, Field
from sqlmodel import Session

from keep.api.core.config import AuthenticationType, config
from keep.api.core.config import config
from keep.api.core.db import get_session
from keep.api.models.alert import AlertDto
from keep.api.models.smtp import SMTPSettings
from keep.api.models.user import User
from keep.api.models.webhook import WebhookSettings
from keep.api.utils.tenant_utils import (
create_api_key,
Expand All @@ -35,8 +33,6 @@

logger = logging.getLogger(__name__)

auth_type = os.environ.get("AUTH_TYPE", AuthenticationType.NO_AUTH.value)


class CreateUserRequest(BaseModel):
email: str = Field(alias="username")
Expand Down Expand Up @@ -83,63 +79,6 @@ def webhook_settings(
)


@router.get("/users", description="Get all users")
def get_users(
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["read:settings"])
),
) -> list[User]:
tenant_id = authenticated_entity.tenant_id
identity_manager = IdentityManagerFactory.get_identity_manager(
tenant_id=tenant_id,
identity_manager_type=auth_type,
context_manager=ContextManager(tenant_id=tenant_id),
)
users = identity_manager.get_users()
return users


@router.delete("/users/{user_email}", description="Delete a user")
def delete_user(
user_email: str,
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["delete:settings"])
),
):
tenant_id = authenticated_entity.tenant_id
identity_manager = IdentityManagerFactory.get_identity_manager(
tenant_id=tenant_id,
identity_manager_type=auth_type,
context_manager=ContextManager(tenant_id=tenant_id),
)

return identity_manager.delete_user(user_email)


@router.post("/users", description="Create a user")
async def create_user(
request_data: CreateUserRequest,
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["write:settings"])
),
):
tenant_id = authenticated_entity.tenant_id
user_email = request_data.email
password = request_data.password
role = request_data.role

if not user_email:
raise HTTPException(status_code=400, detail="Email is required")

identity_manager = IdentityManagerFactory.get_identity_manager(
tenant_id=tenant_id,
identity_manager_type=auth_type,
context_manager=ContextManager(tenant_id=tenant_id),
)
user = identity_manager.create_user(user_email, password, role)
return user


@router.post("/smtp", description="Install or update SMTP settings")
async def update_smtp_settings(
smtp_settings: SMTPSettings = Body(...),
Expand Down Expand Up @@ -430,7 +369,6 @@ async def get_sso_settings(
):
identity_manager = IdentityManagerFactory.get_identity_manager(
tenant_id=authenticated_entity.tenant_id,
identity_manager_type=auth_type,
context_manager=ContextManager(tenant_id=authenticated_entity.tenant_id),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def _verify_bearer_token(self, token: str) -> AuthenticatedEntity:
# validate the token
jwt_secret = os.environ.get("KEEP_JWT_SECRET")
if not jwt_secret:
self.logger.warning("missing KEEP_JWT_SECRET environment variable")
raise HTTPException(status_code=401, detail="Missing JWT secret")

try:
Expand Down
Empty file.
Loading

0 comments on commit 6cbfeca

Please sign in to comment.