diff --git a/.github/workflows/docker-build-push-backend-container-on-tag.yml b/.github/workflows/docker-build-push-backend-container-on-tag.yml index c8d5112e2ae8..26f3391c428c 100644 --- a/.github/workflows/docker-build-push-backend-container-on-tag.yml +++ b/.github/workflows/docker-build-push-backend-container-on-tag.yml @@ -30,8 +30,8 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: | - danswer/danswer-backend:${{ github.ref_name }} - danswer/danswer-backend:latest + danswer/danswer-ee-backend:${{ github.ref_name }} + danswer/danswer-ee-backend:latest build-args: | DANSWER_VERSION=${{ github.ref_name }} diff --git a/.github/workflows/docker-build-push-web-container-on-tag.yml b/.github/workflows/docker-build-push-web-container-on-tag.yml index 4e3d6a991377..0db7e8c9aa72 100644 --- a/.github/workflows/docker-build-push-web-container-on-tag.yml +++ b/.github/workflows/docker-build-push-web-container-on-tag.yml @@ -6,7 +6,7 @@ on: - '*' env: - REGISTRY_IMAGE: danswer/danswer-web-server + REGISTRY_IMAGE: danswer/danswer-ee-web-server jobs: build: @@ -34,8 +34,8 @@ jobs: with: images: ${{ env.REGISTRY_IMAGE }} tags: | - type=raw,value=danswer/danswer-web-server:${{ github.ref_name }} - type=raw,value=danswer/danswer-web-server:latest + type=raw,value=danswer/danswer-ee-web-server:${{ github.ref_name }} + type=raw,value=danswer/danswer-ee-web-server:latest - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -56,6 +56,7 @@ jobs: push: true build-args: | DANSWER_VERSION=${{ github.ref_name }} + NEXT_PUBLIC_ENABLE_PAID_EE_FEATURES=true # needed due to weird interactions with the builds for different platforms no-cache: true labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/docker-tag-latest.yml b/.github/workflows/docker-tag-latest.yml index c0853ff38358..f4c2b9e5a6c7 100644 --- a/.github/workflows/docker-tag-latest.yml +++ b/.github/workflows/docker-tag-latest.yml @@ -25,8 +25,8 @@ jobs: - name: Pull, Tag and Push Web Server Image run: | - docker buildx imagetools create -t danswer/danswer-web-server:latest danswer/danswer-web-server:${{ github.event.inputs.version }} + docker buildx imagetools create -t danswer/danswer-ee-web-server:latest danswer/danswer-ee-web-server:${{ github.event.inputs.version }} - name: Pull, Tag and Push API Server Image run: | - docker buildx imagetools create -t danswer/danswer-backend:latest danswer/danswer-backend:${{ github.event.inputs.version }} + docker buildx imagetools create -t danswer/danswer-ee-backend:latest danswer/danswer-ee-backend:${{ github.event.inputs.version }} diff --git a/.vscode/launch.template.jsonc b/.vscode/launch.template.jsonc index c5780a65a4f3..f4384a993a6d 100644 --- a/.vscode/launch.template.jsonc +++ b/.vscode/launch.template.jsonc @@ -51,7 +51,7 @@ "PYTHONUNBUFFERED": "1" }, "args": [ - "danswer.main:app", + "ee.danswer.main:app", "--reload", "--port", "8080" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 116e78b6f19d..135e2bf1dbdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,13 +152,13 @@ python ./scripts/dev_run_background_jobs.py To run the backend API server, navigate back to `danswer/backend` and run: ```bash -AUTH_TYPE=disabled uvicorn danswer.main:app --reload --port 8080 +AUTH_TYPE=disabled uvicorn ee.danswer.main:app --reload --port 8080 ``` _For Windows (for compatibility with both PowerShell and Command Prompt):_ ```bash powershell -Command " $env:AUTH_TYPE='disabled' - uvicorn danswer.main:app --reload --port 8080 + uvicorn ee.danswer.main:app --reload --port 8080 " ``` diff --git a/LICENSE b/LICENSE index 314f162560d5..bcbeb4130ecc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,10 @@ -MIT License +Copyright (c) 2023 DanswerAI, Inc. -Copyright (c) 2023 Yuhong Sun, Chris Weaver +Portions of this software are licensed as follows: + +* All content that resides under "ee" directories of this repository, if that directory exists, is licensed under the license defined in "backend/ee/LICENSE". +* All third party components incorporated into the Danswer Software are licensed under the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/backend/Dockerfile b/backend/Dockerfile index 4e298fd47da7..e845bb840c1b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -42,6 +42,11 @@ RUN apt-get remove -y --allow-remove-essential perl-base xserver-common xvfb cma rm -rf /var/lib/apt/lists/* && \ rm /usr/local/lib/python3.11/site-packages/tornado/test/test.key +# Enterprise Install +RUN apt-get update && apt-get install -y libxmlsec1-dev +COPY ./requirements/ee.txt /tmp/ee-requirements.txt +RUN pip install --no-cache-dir --upgrade -r /tmp/ee-requirements.txt + # Pre-downloading models for setups with limited egress RUN python -c "from transformers import AutoTokenizer; AutoTokenizer.from_pretrained('intfloat/e5-base-v2')" @@ -53,6 +58,11 @@ nltk.download('punkt', quiet=True);" # Set up application files WORKDIR /app + +# Enterprise Version Files +COPY ./ee /app/ee + +# Set up application files COPY ./danswer /app/danswer COPY ./shared_configs /app/shared_configs COPY ./alembic /app/alembic diff --git a/backend/ee/LICENSE b/backend/ee/LICENSE new file mode 100644 index 000000000000..6833fe7c3c2c --- /dev/null +++ b/backend/ee/LICENSE @@ -0,0 +1,36 @@ +The DanswerAI Enterprise license (the “Enterprise License”) +Copyright (c) 2023 DanswerAI, Inc. + +With regard to the Danswer Software: + +This software and associated documentation files (the "Software") may only be +used in production, if you (and any entity that you represent) have agreed to, +and are in compliance with, the DanswerAI Subscription Terms of Service, available +at https://danswer.ai/terms (the “Enterprise Terms”), or other +agreement governing the use of the Software, as agreed by you and DanswerAI, +and otherwise have a valid Danswer Enterprise license for the +correct number of user seats. Subject to the foregoing sentence, you are free to +modify this Software and publish patches to the Software. You agree that DanswerAI +and/or its licensors (as applicable) retain all right, title and interest in and +to all such modifications and/or patches, and all such modifications and/or +patches may only be used, copied, modified, displayed, distributed, or otherwise +exploited with a valid Danswer Enterprise license for the correct +number of user seats. Notwithstanding the foregoing, you may copy and modify +the Software for development and testing purposes, without requiring a +subscription. You agree that DanswerAI and/or its licensors (as applicable) retain +all right, title and interest in and to all such modifications. You are not +granted any other rights beyond what is expressly stated herein. Subject to the +foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, +and/or sell the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For all third party components incorporated into the Danswer Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/backend/ee/__init__.py b/backend/ee/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/ee/danswer/__init__.py b/backend/ee/danswer/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/ee/danswer/auth/__init__.py b/backend/ee/danswer/auth/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/ee/danswer/auth/users.py b/backend/ee/danswer/auth/users.py new file mode 100644 index 000000000000..13882368b286 --- /dev/null +++ b/backend/ee/danswer/auth/users.py @@ -0,0 +1,45 @@ +from fastapi import HTTPException +from fastapi import Request +from fastapi import status +from sqlalchemy.orm import Session + +from danswer.configs.app_configs import AUTH_TYPE +from danswer.configs.app_configs import DISABLE_AUTH +from danswer.configs.constants import AuthType +from danswer.db.models import User +from danswer.utils.logger import setup_logger +from ee.danswer.db.saml import get_saml_account +from ee.danswer.utils.secrets import extract_hashed_cookie + +logger = setup_logger() + + +def verify_auth_setting() -> None: + # All the Auth flows are valid for EE version + logger.info(f"Using Auth Type: {AUTH_TYPE.value}") + + +async def double_check_user( + request: Request, + user: User | None, + db_session: Session, + optional: bool = DISABLE_AUTH, +) -> User | None: + if optional: + return None + + # Check if the user has a session cookie from SAML + if AUTH_TYPE == AuthType.SAML: + saved_cookie = extract_hashed_cookie(request) + + if saved_cookie: + saml_account = get_saml_account(cookie=saved_cookie, db_session=db_session) + user = saml_account.user if saml_account else None + + if user is None: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied. User is not authenticated.", + ) + + return user diff --git a/backend/ee/danswer/configs/__init__.py b/backend/ee/danswer/configs/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/ee/danswer/configs/app_configs.py b/backend/ee/danswer/configs/app_configs.py new file mode 100644 index 000000000000..ff262cf424db --- /dev/null +++ b/backend/ee/danswer/configs/app_configs.py @@ -0,0 +1,10 @@ +import os + +# Applicable for OIDC Auth +OPENID_CONFIG_URL = os.environ.get("OPENID_CONFIG_URL", "") + +# Applicable for SAML Auth +SAML_CONF_DIR = ( + os.environ.get("SAML_CONF_DIR") + or "/app/danswer/backend/ee/danswer/configs/saml_config" +) diff --git a/backend/ee/danswer/configs/saml_config/template.settings.json b/backend/ee/danswer/configs/saml_config/template.settings.json new file mode 100644 index 000000000000..e3c828944afb --- /dev/null +++ b/backend/ee/danswer/configs/saml_config/template.settings.json @@ -0,0 +1,20 @@ +{ + "strict": true, + "debug": false, + "idp": { + "entityId": "", + "singleSignOnService": { + "url": " https://trial-1234567.okta.com/home/trial-1234567_danswer/somevalues/somevalues", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "" + }, + "sp": { + "entityId": "", + "assertionConsumerService": { + "url": "http://127.0.0.1:3000/auth/saml/callback", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "x509cert": "" + } +} diff --git a/backend/ee/danswer/db/__init__.py b/backend/ee/danswer/db/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/ee/danswer/db/models.py b/backend/ee/danswer/db/models.py new file mode 100644 index 000000000000..439232ee1ed5 --- /dev/null +++ b/backend/ee/danswer/db/models.py @@ -0,0 +1,26 @@ +import datetime + +from sqlalchemy import DateTime +from sqlalchemy import ForeignKey +from sqlalchemy import func +from sqlalchemy import Text +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship + +from danswer.db.models import Base +from danswer.db.models import User + + +class SamlAccount(Base): + __tablename__ = "saml" + + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), unique=True) + encrypted_cookie: Mapped[str] = mapped_column(Text, unique=True) + expires_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True)) + updated_at: Mapped[datetime.datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + user: Mapped[User] = relationship("User") diff --git a/backend/ee/danswer/db/saml.py b/backend/ee/danswer/db/saml.py new file mode 100644 index 000000000000..d9949f4eefbf --- /dev/null +++ b/backend/ee/danswer/db/saml.py @@ -0,0 +1,65 @@ +import datetime +from typing import cast +from uuid import UUID + +from sqlalchemy import and_ +from sqlalchemy import func +from sqlalchemy import select +from sqlalchemy.orm import Session + +from danswer.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS +from danswer.db.models import User +from ee.danswer.db.models import SamlAccount + + +def upsert_saml_account( + user_id: UUID, + cookie: str, + db_session: Session, + expiration_offset: int = SESSION_EXPIRE_TIME_SECONDS, +) -> datetime.datetime: + expires_at = func.now() + datetime.timedelta(seconds=expiration_offset) + + existing_saml_acc = ( + db_session.query(SamlAccount) + .filter(SamlAccount.user_id == user_id) + .one_or_none() + ) + + if existing_saml_acc: + existing_saml_acc.encrypted_cookie = cookie + existing_saml_acc.expires_at = cast(datetime.datetime, expires_at) + existing_saml_acc.updated_at = func.now() + saml_acc = existing_saml_acc + else: + saml_acc = SamlAccount( + user_id=user_id, + encrypted_cookie=cookie, + expires_at=expires_at, + ) + db_session.add(saml_acc) + + db_session.commit() + + return saml_acc.expires_at + + +def get_saml_account(cookie: str, db_session: Session) -> SamlAccount | None: + stmt = ( + select(SamlAccount) + .join(User, User.id == SamlAccount.user_id) # type: ignore + .where( + and_( + SamlAccount.encrypted_cookie == cookie, + SamlAccount.expires_at > func.now(), + ) + ) + ) + + result = db_session.execute(stmt) + return result.scalar_one_or_none() + + +def expire_saml_account(saml_account: SamlAccount, db_session: Session) -> None: + saml_account.expires_at = func.now() + db_session.commit() diff --git a/backend/ee/danswer/main.py b/backend/ee/danswer/main.py new file mode 100644 index 000000000000..ea1072615671 --- /dev/null +++ b/backend/ee/danswer/main.py @@ -0,0 +1,64 @@ +import uvicorn +from fastapi import FastAPI +from httpx_oauth.clients.openid import OpenID + +from danswer.auth.users import auth_backend +from danswer.auth.users import fastapi_users +from danswer.configs.app_configs import APP_HOST +from danswer.configs.app_configs import APP_PORT +from danswer.configs.app_configs import AUTH_TYPE +from danswer.configs.app_configs import OAUTH_CLIENT_ID +from danswer.configs.app_configs import OAUTH_CLIENT_SECRET +from danswer.configs.app_configs import SECRET +from danswer.configs.app_configs import WEB_DOMAIN +from danswer.configs.constants import AuthType +from danswer.main import get_application +from danswer.utils.logger import setup_logger +from danswer.utils.variable_functionality import global_version +from ee.danswer.configs.app_configs import OPENID_CONFIG_URL +from ee.danswer.server.saml import router as saml_router + +logger = setup_logger() + + +def get_ee_application() -> FastAPI: + # Anything that happens at import time is not guaranteed to be running ee-version + # Anything after the server startup will be running ee version + global_version.set_ee() + + application = get_application() + + if AUTH_TYPE == AuthType.OIDC: + application.include_router( + fastapi_users.get_oauth_router( + OpenID(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OPENID_CONFIG_URL), + auth_backend, + SECRET, + associate_by_email=True, + is_verified_by_default=True, + redirect_url=f"{WEB_DOMAIN}/auth/oidc/callback", + ), + prefix="/auth/oidc", + tags=["auth"], + ) + # need basic auth router for `logout` endpoint + application.include_router( + fastapi_users.get_auth_router(auth_backend), + prefix="/auth", + tags=["auth"], + ) + + elif AUTH_TYPE == AuthType.SAML: + application.include_router(saml_router) + + return application + + +app = get_ee_application() + + +if __name__ == "__main__": + logger.info( + f"Running Enterprise Danswer API Service on http://{APP_HOST}:{str(APP_PORT)}/" + ) + uvicorn.run(app, host=APP_HOST, port=APP_PORT) diff --git a/backend/ee/danswer/server/__init__.py b/backend/ee/danswer/server/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/ee/danswer/server/saml.py b/backend/ee/danswer/server/saml.py new file mode 100644 index 000000000000..5fe57efe0eac --- /dev/null +++ b/backend/ee/danswer/server/saml.py @@ -0,0 +1,177 @@ +import contextlib +import secrets +from typing import Any + +from fastapi import APIRouter +from fastapi import Depends +from fastapi import HTTPException +from fastapi import Request +from fastapi import Response +from fastapi import status +from fastapi_users import exceptions +from fastapi_users.password import PasswordHelper +from onelogin.saml2.auth import OneLogin_Saml2_Auth # type: ignore +from pydantic import BaseModel +from pydantic import EmailStr +from sqlalchemy.orm import Session + +from danswer.auth.schemas import UserCreate +from danswer.auth.schemas import UserRole +from danswer.auth.users import get_user_manager +from danswer.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS +from danswer.db.auth import get_user_count +from danswer.db.auth import get_user_db +from danswer.db.engine import get_async_session +from danswer.db.engine import get_session +from danswer.db.models import User +from danswer.utils.logger import setup_logger +from ee.danswer.configs.app_configs import SAML_CONF_DIR +from ee.danswer.db.saml import expire_saml_account +from ee.danswer.db.saml import get_saml_account +from ee.danswer.db.saml import upsert_saml_account +from ee.danswer.utils.secrets import encrypt_string +from ee.danswer.utils.secrets import extract_hashed_cookie + + +logger = setup_logger() +router = APIRouter(prefix="/auth/saml") + + +async def upsert_saml_user(email: str) -> User: + get_async_session_context = contextlib.asynccontextmanager( + get_async_session + ) # type:ignore + get_user_db_context = contextlib.asynccontextmanager(get_user_db) + get_user_manager_context = contextlib.asynccontextmanager(get_user_manager) + + async with get_async_session_context() as session: + async with get_user_db_context(session) as user_db: + async with get_user_manager_context(user_db) as user_manager: + try: + return await user_manager.get_by_email(email) + except exceptions.UserNotExists: + logger.info("Creating user from SAML login") + + user_count = await get_user_count() + role = UserRole.ADMIN if user_count == 0 else UserRole.BASIC + + fastapi_users_pw_helper = PasswordHelper() + password = fastapi_users_pw_helper.generate() + hashed_pass = fastapi_users_pw_helper.hash(password) + + user: User = await user_manager.create( + UserCreate( + email=EmailStr(email), + password=hashed_pass, + is_verified=True, + role=role, + ) + ) + + return user + + +async def prepare_from_fastapi_request(request: Request) -> dict[str, Any]: + form_data = await request.form() + if request.client is None: + raise ValueError("Invalid request for SAML") + + rv: dict[str, Any] = { + "http_host": request.client.host, + "server_port": request.url.port, + "script_name": request.url.path, + "post_data": {}, + "get_data": {}, + } + if request.query_params: + rv["get_data"] = (request.query_params,) + if "SAMLResponse" in form_data: + SAMLResponse = form_data["SAMLResponse"] + rv["post_data"]["SAMLResponse"] = SAMLResponse + if "RelayState" in form_data: + RelayState = form_data["RelayState"] + rv["post_data"]["RelayState"] = RelayState + return rv + + +class SAMLAuthorizeResponse(BaseModel): + authorization_url: str + + +@router.get("/authorize") +async def saml_login(request: Request) -> SAMLAuthorizeResponse: + req = await prepare_from_fastapi_request(request) + auth = OneLogin_Saml2_Auth(req, custom_base_path=SAML_CONF_DIR) + callback_url = auth.login() + return SAMLAuthorizeResponse(authorization_url=callback_url) + + +@router.post("/callback") +async def saml_login_callback( + request: Request, + db_session: Session = Depends(get_session), +) -> Response: + req = await prepare_from_fastapi_request(request) + auth = OneLogin_Saml2_Auth(req, custom_base_path=SAML_CONF_DIR) + auth.process_response() + errors = auth.get_errors() + if len(errors) != 0: + logger.error( + "Error when processing SAML Response: %s %s" + % (", ".join(errors), auth.get_last_error_reason()) + ) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied. Failed to parse SAML Response.", + ) + + if not auth.is_authenticated(): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied. User was not Authenticated.", + ) + + user_email = auth.get_attribute("email") + if not user_email: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="SAML is not set up correctly, email attribute must be provided.", + ) + + user_email = user_email[0] + + user = await upsert_saml_user(email=user_email) + + # Generate a random session cookie and Sha256 encrypt before saving + session_cookie = secrets.token_hex(16) + saved_cookie = encrypt_string(session_cookie) + + upsert_saml_account(user_id=user.id, cookie=saved_cookie, db_session=db_session) + + # Redirect to main Danswer search page + response = Response(status_code=status.HTTP_204_NO_CONTENT) + + response.set_cookie( + key="session", + value=session_cookie, + httponly=True, + secure=True, + max_age=SESSION_EXPIRE_TIME_SECONDS, + ) + + return response + + +@router.post("/logout") +def saml_logout( + request: Request, + db_session: Session = Depends(get_session), +) -> None: + saved_cookie = extract_hashed_cookie(request) + + if saved_cookie: + saml_account = get_saml_account(cookie=saved_cookie, db_session=db_session) + if saml_account: + expire_saml_account(saml_account, db_session) + + return diff --git a/backend/ee/danswer/utils/__init__.py b/backend/ee/danswer/utils/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/ee/danswer/utils/secrets.py b/backend/ee/danswer/utils/secrets.py new file mode 100644 index 000000000000..d59d3b77ac81 --- /dev/null +++ b/backend/ee/danswer/utils/secrets.py @@ -0,0 +1,14 @@ +import hashlib + +from fastapi import Request + +from danswer.configs.constants import SESSION_KEY + + +def encrypt_string(s: str) -> str: + return hashlib.sha256(s.encode()).hexdigest() + + +def extract_hashed_cookie(request: Request) -> str | None: + session_cookie = request.cookies.get(SESSION_KEY) + return encrypt_string(session_cookie) if session_cookie else None diff --git a/backend/requirements/ee.txt b/backend/requirements/ee.txt new file mode 100644 index 000000000000..0717e3a67e78 --- /dev/null +++ b/backend/requirements/ee.txt @@ -0,0 +1 @@ +python3-saml==1.15.0 diff --git a/deployment/docker_compose/docker-compose.dev.yml b/deployment/docker_compose/docker-compose.dev.yml index ac068036ac86..d668d21e516b 100644 --- a/deployment/docker_compose/docker-compose.dev.yml +++ b/deployment/docker_compose/docker-compose.dev.yml @@ -1,14 +1,14 @@ version: '3' services: api_server: - image: danswer/danswer-backend:latest + image: danswer/danswer-ee-backend:latest build: context: ../../backend dockerfile: Dockerfile command: > /bin/sh -c "alembic upgrade head && echo \"Starting Danswer Api Server\" && - uvicorn danswer.main:app --host 0.0.0.0 --port 8080" + uvicorn ee.danswer.main:app --host 0.0.0.0 --port 8080" depends_on: - relational_db - index @@ -30,6 +30,9 @@ services: - SMTP_USER=${SMTP_USER:-} - SMTP_PASS=${SMTP_PASS:-} - EMAIL_FROM=${EMAIL_FROM:-} + - OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID:-} + - OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET:-} + - OPENID_CONFIG_URL=${OPENID_CONFIG_URL:-} # Gen AI Settings - GEN_AI_MODEL_PROVIDER=${GEN_AI_MODEL_PROVIDER:-} - GEN_AI_MODEL_VERSION=${GEN_AI_MODEL_VERSION:-} @@ -93,7 +96,7 @@ services: background: - image: danswer/danswer-backend:latest + image: danswer/danswer-ee-backend:latest build: context: ../../backend dockerfile: Dockerfile @@ -190,7 +193,7 @@ services: web_server: - image: danswer/danswer-web-server:latest + image: danswer/danswer-ee-web-server:latest build: context: ../../web dockerfile: Dockerfile diff --git a/deployment/docker_compose/docker-compose.prod.yml b/deployment/docker_compose/docker-compose.prod.yml index 8ad0ad301d22..c25c7e524f1e 100644 --- a/deployment/docker_compose/docker-compose.prod.yml +++ b/deployment/docker_compose/docker-compose.prod.yml @@ -1,14 +1,14 @@ version: '3' services: api_server: - image: danswer/danswer-backend:latest + image: danswer/danswer-ee-backend:latest build: context: ../../backend dockerfile: Dockerfile command: > /bin/sh -c "alembic upgrade head && echo \"Starting Danswer Api Server\" && - uvicorn danswer.main:app --host 0.0.0.0 --port 8080" + uvicorn ee.danswer.main:app --host 0.0.0.0 --port 8080" depends_on: - relational_db - index @@ -17,7 +17,7 @@ services: env_file: - .env environment: - - AUTH_TYPE=${AUTH_TYPE:-google_oauth} + - AUTH_TYPE=${AUTH_TYPE:-oidc} - POSTGRES_HOST=relational_db - VESPA_HOST=index - MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server} @@ -31,7 +31,7 @@ services: background: - image: danswer/danswer-backend:latest + image: danswer/danswer-ee-backend:latest build: context: ../../backend dockerfile: Dockerfile @@ -45,7 +45,7 @@ services: env_file: - .env environment: - - AUTH_TYPE=${AUTH_TYPE:-google_oauth} + - AUTH_TYPE=${AUTH_TYPE:-oidc} - POSTGRES_HOST=relational_db - VESPA_HOST=index - MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server} @@ -60,7 +60,7 @@ services: web_server: - image: danswer/danswer-web-server:latest + image: danswer/danswer-ee-web-server:latest build: context: ../../web dockerfile: Dockerfile diff --git a/deployment/kubernetes/api_server-service-deployment.yaml b/deployment/kubernetes/api_server-service-deployment.yaml index eeac5fecc966..eb40372902b3 100644 --- a/deployment/kubernetes/api_server-service-deployment.yaml +++ b/deployment/kubernetes/api_server-service-deployment.yaml @@ -28,7 +28,7 @@ spec: spec: containers: - name: api-server - image: danswer/danswer-backend:latest + image: danswer/danswer-ee-backend:latest imagePullPolicy: IfNotPresent command: - "/bin/sh" @@ -36,7 +36,7 @@ spec: - | alembic upgrade head && echo "Starting Danswer Api Server" && - uvicorn danswer.main:app --host 0.0.0.0 --port 8080 + uvicorn ee.danswer.main:app --host 0.0.0.0 --port 8080 ports: - containerPort: 8080 # There are some extra values since this is shared between services diff --git a/deployment/kubernetes/background-deployment.yaml b/deployment/kubernetes/background-deployment.yaml index 82369e6d3e8b..4dbee7899ed9 100644 --- a/deployment/kubernetes/background-deployment.yaml +++ b/deployment/kubernetes/background-deployment.yaml @@ -14,7 +14,7 @@ spec: spec: containers: - name: background - image: danswer/danswer-backend:latest + image: danswer/danswer-ee-backend:latest imagePullPolicy: IfNotPresent command: ["/usr/bin/supervisord"] # There are some extra values since this is shared between services diff --git a/deployment/kubernetes/web_server-service-deployment.yaml b/deployment/kubernetes/web_server-service-deployment.yaml index b19b8e379868..3650b12c24bc 100644 --- a/deployment/kubernetes/web_server-service-deployment.yaml +++ b/deployment/kubernetes/web_server-service-deployment.yaml @@ -27,7 +27,7 @@ spec: spec: containers: - name: web-server - image: danswer/danswer-web-server:latest + image: danswer/danswer-ee-web-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 3000 diff --git a/web/src/app/auth/oidc/callback/route.ts b/web/src/app/auth/oidc/callback/route.ts new file mode 100644 index 000000000000..353119409b93 --- /dev/null +++ b/web/src/app/auth/oidc/callback/route.ts @@ -0,0 +1,23 @@ +import { getDomain } from "@/lib/redirectSS"; +import { buildUrl } from "@/lib/utilsSS"; +import { NextRequest, NextResponse } from "next/server"; + +export const GET = async (request: NextRequest) => { + // Wrapper around the FastAPI endpoint /auth/oidc/callback, + // which adds back a redirect to the main app. + const url = new URL(buildUrl("/auth/oidc/callback")); + url.search = request.nextUrl.search; + + const response = await fetch(url.toString()); + const setCookieHeader = response.headers.get("set-cookie"); + + if (!setCookieHeader) { + return NextResponse.redirect(new URL("/auth/error", getDomain(request))); + } + + const redirectResponse = NextResponse.redirect( + new URL("/", getDomain(request)) + ); + redirectResponse.headers.set("set-cookie", setCookieHeader); + return redirectResponse; +};