diff --git a/backend/danswer/db/users.py b/backend/danswer/db/users.py new file mode 100644 index 00000000000..c5f5a5c7309 --- /dev/null +++ b/backend/danswer/db/users.py @@ -0,0 +1,12 @@ +from collections.abc import Sequence + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from danswer.db.models import User + + +def list_users(db_session: Session) -> Sequence[User]: + """List all users. No pagination as of now, as the # of users + is assumed to be relatively small (<< 1 million)""" + return db_session.scalars(select(User)).unique().all() diff --git a/backend/danswer/main.py b/backend/danswer/main.py index 72962ef26b2..fcd4f18dcd5 100644 --- a/backend/danswer/main.py +++ b/backend/danswer/main.py @@ -34,6 +34,7 @@ from danswer.server.health import router as health_router from danswer.server.manage import router as admin_router from danswer.server.search_backend import router as backend_router +from danswer.server.users import router as user_router from danswer.utils.logger import setup_logger @@ -66,6 +67,7 @@ def get_application() -> FastAPI: application.include_router(backend_router) application.include_router(event_processing_router) application.include_router(admin_router) + application.include_router(user_router) application.include_router(health_router) application.include_router( diff --git a/backend/danswer/server/manage.py b/backend/danswer/server/manage.py index 4041d683111..01a67ca33e6 100644 --- a/backend/danswer/server/manage.py +++ b/backend/danswer/server/manage.py @@ -8,11 +8,8 @@ from fastapi import Request from fastapi import Response from fastapi import UploadFile -from fastapi_users.db import SQLAlchemyUserDatabase -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from danswer.auth.schemas import UserRole from danswer.auth.users import current_admin_user from danswer.auth.users import current_user from danswer.configs.app_configs import DISABLE_GENERATIVE_AI @@ -20,9 +17,6 @@ from danswer.configs.constants import GEN_AI_API_KEY_STORAGE_KEY from danswer.connectors.file.utils import write_temp_files from danswer.connectors.google_drive.connector_auth import build_service_account_creds -from danswer.connectors.google_drive.connector_auth import ( - DB_CREDENTIALS_DICT_SERVICE_ACCOUNT_KEY, -) from danswer.connectors.google_drive.connector_auth import DB_CREDENTIALS_DICT_TOKEN_KEY from danswer.connectors.google_drive.connector_auth import delete_google_app_cred from danswer.connectors.google_drive.connector_auth import delete_service_account_key @@ -58,7 +52,6 @@ from danswer.db.deletion_attempt import create_deletion_attempt from danswer.db.deletion_attempt import get_deletion_attempts from danswer.db.engine import get_session -from danswer.db.engine import get_sqlalchemy_async_engine from danswer.db.index_attempt import create_index_attempt from danswer.db.index_attempt import get_latest_index_attempts from danswer.db.models import DeletionAttempt @@ -87,7 +80,6 @@ from danswer.server.models import ObjectCreationIdResponse from danswer.server.models import RunConnectorRequest from danswer.server.models import StatusResponse -from danswer.server.models import UserByEmail from danswer.server.models import UserRoleResponse from danswer.utils.logger import setup_logger @@ -101,23 +93,6 @@ """Admin only API endpoints""" -@router.patch("/promote-user-to-admin", response_model=None) -async def promote_admin( - user_email: UserByEmail, user: User = Depends(current_admin_user) -) -> None: - if user.role != UserRole.ADMIN: - raise HTTPException(status_code=401, detail="Unauthorized") - async with AsyncSession(get_sqlalchemy_async_engine()) as asession: - user_db = SQLAlchemyUserDatabase(asession, User) # type: ignore - user_to_promote = await user_db.get_by_email(user_email.user_email) - if not user_to_promote: - raise HTTPException(status_code=404, detail="User not found") - user_to_promote.role = UserRole.ADMIN - asession.add(user_to_promote) - await asession.commit() - return - - @router.get("/admin/connector/google-drive/app-credential") def check_google_app_credentials_exist( _: User = Depends(current_admin_user), diff --git a/backend/danswer/server/users.py b/backend/danswer/server/users.py new file mode 100644 index 00000000000..b9e3f7e6dde --- /dev/null +++ b/backend/danswer/server/users.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter +from fastapi import Depends +from fastapi import HTTPException +from fastapi_users.db import SQLAlchemyUserDatabase +from fastapi_users_db_sqlalchemy import UUID_ID +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Session + +from danswer.auth.schemas import UserRead +from danswer.auth.schemas import UserRole +from danswer.auth.users import current_admin_user +from danswer.db.engine import get_session +from danswer.db.engine import get_sqlalchemy_async_engine +from danswer.db.models import User +from danswer.db.users import list_users +from danswer.server.models import UserByEmail + + +router = APIRouter(prefix="/manage") + + +@router.patch("/promote-user-to-admin") +async def promote_admin( + user_email: UserByEmail, user: User = Depends(current_admin_user) +) -> None: + if user.role != UserRole.ADMIN: + raise HTTPException(status_code=401, detail="Unauthorized") + async with AsyncSession(get_sqlalchemy_async_engine()) as asession: + user_db = SQLAlchemyUserDatabase[User, UUID_ID](asession, User) + user_to_promote = await user_db.get_by_email(user_email.user_email) + if not user_to_promote: + raise HTTPException(status_code=404, detail="User not found") + user_to_promote.role = UserRole.ADMIN + asession.add(user_to_promote) + await asession.commit() + return + + +@router.get("/users") +def list_all_users( + _: User | None = Depends(current_admin_user), + db_session: Session = Depends(get_session), +) -> list[UserRead]: + users = list_users(db_session) + return [UserRead.from_orm(user) for user in users] diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index 1fc8fe132f9..761b3dd04f4 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -17,6 +17,7 @@ import { ZulipIcon, ProductboardIcon, LinearIcon, + UsersIcon, } from "@/components/icons/icons"; import { DISABLE_AUTH } from "@/lib/constants"; import { getCurrentUserSS } from "@/lib/userSS"; @@ -161,6 +162,15 @@ export default async function AdminLayout({ ), link: "/admin/connectors/bookstack", }, + { + name: ( +