From 642862bede5afd91ebf4401cb19c2b2a74dfc0a4 Mon Sep 17 00:00:00 2001 From: Chris Weaver <25087905+Weves@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:06:38 -0700 Subject: [PATCH] Make public credentials accessible by all admins (#337) --- backend/danswer/db/credentials.py | 9 +- backend/danswer/main.py | 2 + backend/danswer/server/credential.py | 128 ++++++++++++++++++ backend/danswer/server/manage.py | 75 ---------- .../app/admin/connectors/bookstack/page.tsx | 16 +-- .../app/admin/connectors/confluence/page.tsx | 16 +-- web/src/app/admin/connectors/github/page.tsx | 22 +-- .../connectors/google-drive/Credential.tsx | 11 +- .../admin/connectors/google-drive/page.tsx | 27 ++-- web/src/app/admin/connectors/guru/page.tsx | 13 +- web/src/app/admin/connectors/jira/page.tsx | 26 +++- web/src/app/admin/connectors/linear/page.tsx | 14 +- web/src/app/admin/connectors/notion/page.tsx | 22 +-- .../admin/connectors/productboard/page.tsx | 23 ++-- web/src/app/admin/connectors/slab/page.tsx | 15 +- web/src/app/admin/connectors/slack/page.tsx | 16 +-- web/src/app/admin/connectors/zulip/page.tsx | 22 +-- web/src/app/admin/users/page.tsx | 7 +- web/src/lib/credential.ts | 12 +- web/src/lib/hooks.ts | 15 ++ 20 files changed, 290 insertions(+), 201 deletions(-) create mode 100644 backend/danswer/server/credential.py create mode 100644 web/src/lib/hooks.ts diff --git a/backend/danswer/db/credentials.py b/backend/danswer/db/credentials.py index 18e900dbd19..9e0374902dd 100644 --- a/backend/danswer/db/credentials.py +++ b/backend/danswer/db/credentials.py @@ -20,14 +20,17 @@ def fetch_credentials( - user: User | None, db_session: Session, + user: User | None = None, + public_only: bool | None = None, ) -> list[Credential]: stmt = select(Credential) if user: stmt = stmt.where( or_(Credential.user_id == user.id, Credential.user_id.is_(None)) ) + if public_only is not None: + stmt = stmt.where(Credential.public_doc == public_only) results = db_session.scalars(stmt) return list(results.all()) @@ -106,7 +109,7 @@ def backend_update_credential_json( def delete_credential( credential_id: int, - user: User, + user: User | None, db_session: Session, ) -> None: credential = fetch_credential_by_id(credential_id, user, db_session) @@ -146,7 +149,7 @@ def create_initial_public_credential() -> None: def delete_google_drive_service_account_credentials( user: User | None, db_session: Session ) -> None: - credentials = fetch_credentials(user, db_session) + credentials = fetch_credentials(db_session=db_session, user=user) for credential in credentials: if credential.credential_json.get(DB_CREDENTIALS_DICT_SERVICE_ACCOUNT_KEY): db_session.delete(credential) diff --git a/backend/danswer/main.py b/backend/danswer/main.py index fcd4f18dcd5..197bec11473 100644 --- a/backend/danswer/main.py +++ b/backend/danswer/main.py @@ -30,6 +30,7 @@ from danswer.datastores.document_index import get_default_document_index from danswer.db.credentials import create_initial_public_credential from danswer.direct_qa.llm_utils import get_default_llm +from danswer.server.credential import router as credential_router from danswer.server.event_loading import router as event_processing_router from danswer.server.health import router as health_router from danswer.server.manage import router as admin_router @@ -68,6 +69,7 @@ def get_application() -> FastAPI: application.include_router(event_processing_router) application.include_router(admin_router) application.include_router(user_router) + application.include_router(credential_router) application.include_router(health_router) application.include_router( diff --git a/backend/danswer/server/credential.py b/backend/danswer/server/credential.py new file mode 100644 index 00000000000..591ff388270 --- /dev/null +++ b/backend/danswer/server/credential.py @@ -0,0 +1,128 @@ +from fastapi import APIRouter +from fastapi import Depends +from fastapi import HTTPException +from sqlalchemy.orm import Session + +from danswer.auth.users import current_admin_user +from danswer.auth.users import current_user +from danswer.db.credentials import create_credential +from danswer.db.credentials import delete_credential +from danswer.db.credentials import fetch_credential_by_id +from danswer.db.credentials import fetch_credentials +from danswer.db.credentials import update_credential +from danswer.db.engine import get_session +from danswer.db.models import User +from danswer.server.models import CredentialBase +from danswer.server.models import CredentialSnapshot +from danswer.server.models import ObjectCreationIdResponse +from danswer.server.models import StatusResponse + + +router = APIRouter(prefix="/manage") + + +"""Admin-only endpoints""" + + +@router.get("/admin/credential") +def list_credentials_admin( + _: User = Depends(current_admin_user), + db_session: Session = Depends(get_session), +) -> list[CredentialSnapshot]: + """Lists all public credentials""" + credentials = fetch_credentials(db_session=db_session, public_only=True) + return [ + CredentialSnapshot.from_credential_db_model(credential) + for credential in credentials + ] + + +@router.delete("/admin/credential/{credential_id}") +def delete_credential_by_id_admin( + credential_id: int, + _: User = Depends(current_admin_user), + db_session: Session = Depends(get_session), +) -> StatusResponse: + """Same as the user endpoint, but can delete any credential (not just the user's own)""" + delete_credential(db_session=db_session, credential_id=credential_id, user=None) + return StatusResponse( + success=True, message="Credential deleted successfully", data=credential_id + ) + + +"""Endpoints for all""" + + +@router.get("/credential") +def list_credentials( + user: User | None = Depends(current_user), + db_session: Session = Depends(get_session), +) -> list[CredentialSnapshot]: + credentials = fetch_credentials(db_session=db_session, user=user) + return [ + CredentialSnapshot.from_credential_db_model(credential) + for credential in credentials + ] + + +@router.get("/credential/{credential_id}") +def get_credential_by_id( + credential_id: int, + user: User = Depends(current_user), + db_session: Session = Depends(get_session), +) -> CredentialSnapshot | StatusResponse[int]: + credential = fetch_credential_by_id(credential_id, user, db_session) + if credential is None: + raise HTTPException( + status_code=401, + detail=f"Credential {credential_id} does not exist or does not belong to user", + ) + + return CredentialSnapshot.from_credential_db_model(credential) + + +@router.post("/credential") +def create_credential_from_model( + connector_info: CredentialBase, + user: User = Depends(current_user), + db_session: Session = Depends(get_session), +) -> ObjectCreationIdResponse: + return create_credential(connector_info, user, db_session) + + +@router.patch("/credential/{credential_id}") +def update_credential_from_model( + credential_id: int, + credential_data: CredentialBase, + user: User = Depends(current_user), + db_session: Session = Depends(get_session), +) -> CredentialSnapshot | StatusResponse[int]: + updated_credential = update_credential( + credential_id, credential_data, user, db_session + ) + if updated_credential is None: + raise HTTPException( + status_code=401, + detail=f"Credential {credential_id} does not exist or does not belong to user", + ) + + return CredentialSnapshot( + id=updated_credential.id, + credential_json=updated_credential.credential_json, + user_id=updated_credential.user_id, + public_doc=updated_credential.public_doc, + time_created=updated_credential.time_created, + time_updated=updated_credential.time_updated, + ) + + +@router.delete("/credential/{credential_id}") +def delete_credential_by_id( + credential_id: int, + user: User = Depends(current_user), + db_session: Session = Depends(get_session), +) -> StatusResponse: + delete_credential(credential_id, user, db_session) + return StatusResponse( + success=True, message="Credential deleted successfully", data=credential_id + ) diff --git a/backend/danswer/server/manage.py b/backend/danswer/server/manage.py index 01a67ca33e6..6dbcea67a06 100644 --- a/backend/danswer/server/manage.py +++ b/backend/danswer/server/manage.py @@ -641,81 +641,6 @@ def get_connector_by_id( ) -@router.get("/credential") -def get_credentials( - user: User = Depends(current_user), - db_session: Session = Depends(get_session), -) -> list[CredentialSnapshot]: - credentials = fetch_credentials(user, db_session) - return [ - CredentialSnapshot.from_credential_db_model(credential) - for credential in credentials - ] - - -@router.get("/credential/{credential_id}") -def get_credential_by_id( - credential_id: int, - user: User = Depends(current_user), - db_session: Session = Depends(get_session), -) -> CredentialSnapshot | StatusResponse[int]: - credential = fetch_credential_by_id(credential_id, user, db_session) - if credential is None: - raise HTTPException( - status_code=401, - detail=f"Credential {credential_id} does not exist or does not belong to user", - ) - - return CredentialSnapshot.from_credential_db_model(credential) - - -@router.post("/credential") -def create_credential_from_model( - connector_info: CredentialBase, - user: User = Depends(current_user), - db_session: Session = Depends(get_session), -) -> ObjectCreationIdResponse: - return create_credential(connector_info, user, db_session) - - -@router.patch("/credential/{credential_id}") -def update_credential_from_model( - credential_id: int, - credential_data: CredentialBase, - user: User = Depends(current_user), - db_session: Session = Depends(get_session), -) -> CredentialSnapshot | StatusResponse[int]: - updated_credential = update_credential( - credential_id, credential_data, user, db_session - ) - if updated_credential is None: - raise HTTPException( - status_code=401, - detail=f"Credential {credential_id} does not exist or does not belong to user", - ) - - return CredentialSnapshot( - id=updated_credential.id, - credential_json=updated_credential.credential_json, - user_id=updated_credential.user_id, - public_doc=updated_credential.public_doc, - time_created=updated_credential.time_created, - time_updated=updated_credential.time_updated, - ) - - -@router.delete("/credential/{credential_id}") -def delete_credential_by_id( - credential_id: int, - user: User = Depends(current_user), - db_session: Session = Depends(get_session), -) -> StatusResponse: - delete_credential(credential_id, user, db_session) - return StatusResponse( - success=True, message="Credential deleted successfully", data=credential_id - ) - - @router.put("/connector/{connector_id}/credential/{credential_id}") def associate_credential_to_connector( connector_id: int, diff --git a/web/src/app/admin/connectors/bookstack/page.tsx b/web/src/app/admin/connectors/bookstack/page.tsx index bc92d9dacee..d32985db7e5 100644 --- a/web/src/app/admin/connectors/bookstack/page.tsx +++ b/web/src/app/admin/connectors/bookstack/page.tsx @@ -8,16 +8,16 @@ import { CredentialForm } from "@/components/admin/connectors/CredentialForm"; import { BookstackCredentialJson, BookstackConfig, - Credential, ConnectorIndexingStatus, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; import { fetcher } from "@/lib/fetcher"; import { LoadingAnimation } from "@/components/Loading"; -import { deleteCredential, linkCredential } from "@/lib/credential"; +import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable"; import { usePopup } from "@/components/admin/connectors/Popup"; +import { usePublicCredentials } from "@/lib/hooks"; const Main = () => { const { popup, setPopup } = usePopup(); @@ -35,10 +35,8 @@ const Main = () => { data: credentialsData, isLoading: isCredentialsLoading, error: isCredentialsError, - } = useSWR[]>( - "/api/manage/credential", - fetcher - ); + refreshCredentials, + } = usePublicCredentials(); if ( (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) || @@ -91,8 +89,8 @@ const Main = () => { }); return; } - await deleteCredential(bookstackCredential.id); - mutate("/api/manage/credential"); + await adminDeleteCredential(bookstackCredential.id); + refreshCredentials(); }} > @@ -146,7 +144,7 @@ const Main = () => { }} onSubmit={(isSuccess) => { if (isSuccess) { - mutate("/api/manage/credential"); + refreshCredentials(); mutate("/api/manage/admin/connector/indexing-status"); } }} diff --git a/web/src/app/admin/connectors/confluence/page.tsx b/web/src/app/admin/connectors/confluence/page.tsx index 509a2f70021..a526923e62f 100644 --- a/web/src/app/admin/connectors/confluence/page.tsx +++ b/web/src/app/admin/connectors/confluence/page.tsx @@ -8,16 +8,16 @@ import { CredentialForm } from "@/components/admin/connectors/CredentialForm"; import { ConfluenceCredentialJson, ConfluenceConfig, - Credential, ConnectorIndexingStatus, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; import { fetcher } from "@/lib/fetcher"; import { LoadingAnimation } from "@/components/Loading"; -import { deleteCredential, linkCredential } from "@/lib/credential"; +import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable"; import { usePopup } from "@/components/admin/connectors/Popup"; +import { usePublicCredentials } from "@/lib/hooks"; const Main = () => { const { popup, setPopup } = usePopup(); @@ -35,10 +35,8 @@ const Main = () => { data: credentialsData, isLoading: isCredentialsLoading, error: isCredentialsError, - } = useSWR[]>( - "/api/manage/credential", - fetcher - ); + refreshCredentials, + } = usePublicCredentials(); if ( (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) || @@ -97,8 +95,8 @@ const Main = () => { }); return; } - await deleteCredential(confluenceCredential.id); - mutate("/api/manage/credential"); + await adminDeleteCredential(confluenceCredential.id); + refreshCredentials(); }} > @@ -143,7 +141,7 @@ const Main = () => { }} onSubmit={(isSuccess) => { if (isSuccess) { - mutate("/api/manage/credential"); + refreshCredentials(); } }} /> diff --git a/web/src/app/admin/connectors/github/page.tsx b/web/src/app/admin/connectors/github/page.tsx index 83e690f5a51..ac30dca23ee 100644 --- a/web/src/app/admin/connectors/github/page.tsx +++ b/web/src/app/admin/connectors/github/page.tsx @@ -15,8 +15,9 @@ import { import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; import { LoadingAnimation } from "@/components/Loading"; import { CredentialForm } from "@/components/admin/connectors/CredentialForm"; -import { deleteCredential, linkCredential } from "@/lib/credential"; +import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable"; +import { usePublicCredentials } from "@/lib/hooks"; const Main = () => { const { mutate } = useSWRConfig(); @@ -33,10 +34,8 @@ const Main = () => { data: credentialsData, isLoading: isCredentialsLoading, error: isCredentialsError, - } = useSWR[]>( - "/api/manage/credential", - fetcher - ); + refreshCredentials, + } = usePublicCredentials(); if ( (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) || @@ -60,9 +59,10 @@ const Main = () => { (connectorIndexingStatus) => connectorIndexingStatus.connector.source === "github" ); - const githubCredential = credentialsData.filter( - (credential) => credential.credential_json?.github_access_token - )[0]; + const githubCredential: Credential = + credentialsData.filter( + (credential) => credential.credential_json?.github_access_token + )[0]; return ( <> @@ -80,8 +80,8 @@ const Main = () => {