Skip to content

To support multi-tenancy with DB based identity management. Assumes t… #5110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/deployment/authentication/db-auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ To start Keep with DB authentication, set the following environment variables:
| KEEP_DEFAULT_PASSWORD | Default admin password | No | Backend | keep |
| KEEP_FORCE_RESET_DEFAULT_PASSWORD | Override the current admin password | No | Backend | false |

### Backend Setup Instructions for Multi-tenancy with DB-based authentication

To start Keep with DB authentication that supports multi-tenancy, set the following environment variables for the backend service:

| Environment Variable | Description | Required | Default Value |
|--------------------|:-----------:|:--------:|:----------------:|
| AUTH_TYPE | Set to 'DBMT' for database authentication | Yes | - |
| KEEP_JWT_SECRET | Secret for JWT token generation | Yes | - |
| KEEP_DEFAULT_USERNAME | Default admin username | No | keep@keep |
| KEEP_DEFAULT_PASSWORD | Default admin password | No | keep |
| KEEP_FORCE_RESET_DEFAULT_PASSWORD | Override the current admin password | No | false |

Note: all user names must be in the 'user@tenant' format.

### Example configuration

Use the `docker-compose-with-auth.yml` for an easy setup, which includes necessary environment variables for enabling basic authentication.
1 change: 1 addition & 0 deletions keep/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def on_starting(server=None):
# Create single tenant if it doesn't exist
if AUTH_TYPE in [
IdentityManagerTypes.DB.value,
IdentityManagerTypes.DBMT.value,
IdentityManagerTypes.NOAUTH.value,
IdentityManagerTypes.OAUTH2PROXY.value,
"no_auth", # backwards compatibility
Expand Down
20 changes: 14 additions & 6 deletions keep/api/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,12 @@ def get_user_by_api_key(api_key: str):
return api_key.created_by


def _get_tenant_id(username):
if '@' not in username[1:-1]:
return SINGLE_TENANT_UUID
else:
return username.split('@')[1]

# this is only for single tenant
def get_user(username, password, update_sign_in=True):
from keep.api.models.db.user import User
Expand All @@ -2036,7 +2042,7 @@ def get_user(username, password, update_sign_in=True):
with Session(engine, expire_on_commit=False) as session:
user = session.exec(
select(User)
.where(User.tenant_id == SINGLE_TENANT_UUID)
.where(User.tenant_id == _get_tenant_id(username))
.where(User.username == username)
.where(User.password_hash == password_hash)
).first()
Expand All @@ -2053,17 +2059,19 @@ def get_users(tenant_id=None):
tenant_id = tenant_id or SINGLE_TENANT_UUID

with Session(engine) as session:
users = session.exec(select(User).where(User.tenant_id == tenant_id)).all()
if tenant_id == SINGLE_TENANT_UUID:
users = session.exec(select(User)).all()
else:
users = session.exec(select(User).where(User.tenant_id == tenant_id)).all()
return users


def delete_user(username):
from keep.api.models.db.user import User

with Session(engine) as session:
user = session.exec(
select(User)
.where(User.tenant_id == SINGLE_TENANT_UUID)
.where(User.tenant_id == _get_tenant_id(username))
.where(User.username == username)
).first()
if user:
Expand Down Expand Up @@ -5852,6 +5860,7 @@ def dismiss_error_alerts(tenant_id: str, alert_id=None, dismissed_by=None) -> No

def create_tenant(tenant_name: str) -> str:
with Session(engine) as session:
tenant_id = tenant_name
try:
# check if the tenant exist:
logger.info("Checking if tenant exists")
Expand All @@ -5860,7 +5869,6 @@ def create_tenant(tenant_name: str) -> str:
).first()
if not tenant:
# Do everything related with single tenant creation in here
tenant_id = str(uuid4())
logger.info(
"Creating tenant",
extra={"tenant_id": tenant_id, "tenant_name": tenant_name},
Expand Down Expand Up @@ -5897,7 +5905,7 @@ def create_single_tenant_for_e2e(tenant_id: str) -> None:
if not tenant:
# Do everything related with single tenant creation in here
logger.info("Creating single tenant", extra={"tenant_id": tenant_id})
session.add(Tenant(id=tenant_id, name="Single Tenant"))
session.add(Tenant(id=tenant_id, name=tenant_id))
else:
logger.info("Single tenant already exists")

Expand Down
2 changes: 1 addition & 1 deletion keep/api/core/db_on_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def try_create_single_tenant(tenant_id: str, create_default_user=True) -> None:
if not tenant:
# Do everything related with single tenant creation in here
logger.info("Creating single tenant")
session.add(Tenant(id=tenant_id, name="Single Tenant"))
session.add(Tenant(id=tenant_id, name=tenant_id))
else:
logger.info("Single tenant already exists")

Expand Down
2 changes: 1 addition & 1 deletion keep/api/core/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

# Just a fake random tenant id
SINGLE_TENANT_UUID = "keep"
SINGLE_TENANT_EMAIL = "admin@keephq"
SINGLE_TENANT_EMAIL = "keep@keep"

PUSHER_ROOT_CA = config("PUSHER_ROOT_CA", default=None)

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from keep.identitymanager.identity_managers.db.db_authverifier import DbAuthVerifier

class DbMtAuthVerifier(DbAuthVerifier):
pass
130 changes: 130 additions & 0 deletions keep/identitymanager/identity_managers/dbmt/dbmt_identitymanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import os

import jwt
from fastapi import HTTPException
from fastapi.responses import JSONResponse

from keep.api.core.db import create_tenant as create_tenant_in_db
from keep.api.core.db import create_user as create_user_in_db
from keep.api.core.db import delete_user as delete_user_from_db
from keep.api.core.db import get_user
from keep.api.core.db import get_users as get_users_from_db
from keep.api.core.dependencies import SINGLE_TENANT_UUID
from keep.api.models.user import User
from keep.contextmanager.contextmanager import ContextManager
from keep.identitymanager.identity_managers.db.db_authverifier import DbAuthVerifier
from keep.identitymanager.identitymanager import BaseIdentityManager


class DbMtIdentityManager(BaseIdentityManager):
def __init__(self, tenant_id, context_manager: ContextManager, **kwargs):
super().__init__(tenant_id, context_manager, **kwargs)
self.logger.info("DB Multi-Tenant Identity Manager initialized")

def _get_tenant_id(self, username):
if '@' not in username[1:-1]:
raise HTTPException(status_code=401, detail=f"A username must be in the form 'user@tenant'")
else:
return username.split('@')[1]

def on_start(self, app) -> None:
"""
Initialize the identity manager.
"""
# This is a special method that is called when the identity manager is
# initialized. It is used to set up the identity manager with the FastAPI
self.logger.info("Adding signin endpoint")

@app.post("/signin")
def signin(body: dict):
# block empty passwords (e.g. user provisioned)
if not body.get("password"):
return JSONResponse(
status_code=401,
content={"message": "Empty password"},
)

# validate the user/password
user = get_user(body.get("username"), body.get("password"))
if not user:
return JSONResponse(
status_code=401,
content={"message": "Invalid username or password"},
)
# generate a JWT secret
jwt_secret = os.environ.get("KEEP_JWT_SECRET")
if not jwt_secret:
self.logger.info("missing KEEP_JWT_SECRET environment variable")
raise HTTPException(status_code=401, detail="Missing JWT secret")
token = jwt.encode(
{
"email": user.username,
"tenant_id": self._get_tenant_id(user.username),
"role": user.role,
},
jwt_secret,
algorithm="HS256",
)
# return the token
return {
"accessToken": token,
"tenantId": user.username,
"email": self._get_tenant_id(user.username),
"role": user.role,
}

self.logger.info("Added signin endpoint")

def get_users(self, tenant_id=None) -> list[User]:
tenant_id = tenant_id or self.tenant_id
users = get_users_from_db(tenant_id)
users = [
User(
email=f"{user.username}",
name=user.username,
role=user.role,
last_login=str(user.last_sign_in) if user.last_sign_in else None,
created_at=str(user.created_at),
)
for user in users
]
return users

def create_user(
self, user_email: str, user_name: str, password: str, role: str, groups: list
) -> dict:
# Username is redundant, but we need it in other auth types
# Groups: for future use
tenant_id = self._get_tenant_id(user_email)
if self.tenant_id != tenant_id and self.tenant_id != SINGLE_TENANT_UUID:
raise HTTPException(status_code=401, detail=f"A user at {tenant_id} can't be created by a user at {self.tenant_id}")

try:
tenant_id = create_tenant_in_db(tenant_id)
user = create_user_in_db(tenant_id, user_email, password, role)
return User(
email=user_email,
name=user_email,
role=role,
last_login=None,
created_at=str(user.created_at),
)
except Exception:
raise HTTPException(status_code=409, detail="User already exists")

def delete_user(self, user_email: str) -> dict:
tenant_id = self._get_tenant_id(user_email)
if self.tenant_id != tenant_id and self.tenant_id != SINGLE_TENANT_UUID:
raise HTTPException(status_code=401, detail=f"A user at {tenant_id} can't be deleted by a user at {self.tenant_id}")

try:
delete_user_from_db(user_email)
return {"status": "OK"}
except Exception:
raise HTTPException(status_code=404, detail="User not found")

def get_auth_verifier(self, scopes) -> DbAuthVerifier:
return DbAuthVerifier(scopes)

def update_user(self, user_email: str, update_data: dict) -> User:
raise NotImplementedError("DbIdentityManager.update_user")
1 change: 1 addition & 0 deletions keep/identitymanager/identitymanagerfactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class IdentityManagerTypes(enum.Enum):
AUTH0 = "auth0"
KEYCLOAK = "keycloak"
DB = "db"
DBMT = "dbmt"
NOAUTH = "noauth"
OAUTH2PROXY = "oauth2proxy"

Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy