diff --git a/backend/danswer/db/connector_credential_pair.py b/backend/danswer/db/connector_credential_pair.py index a44182d254b..6df56966d7f 100644 --- a/backend/danswer/db/connector_credential_pair.py +++ b/backend/danswer/db/connector_credential_pair.py @@ -76,8 +76,10 @@ def _add_user_filters( .where(~UG__CCpair.user_group_id.in_(user_groups)) .correlate(ConnectorCredentialPair) ) + where_clause |= ConnectorCredentialPair.access_type == AccessType.SYNC else: where_clause |= ConnectorCredentialPair.access_type == AccessType.PUBLIC + where_clause |= ConnectorCredentialPair.access_type == AccessType.SYNC return stmt.where(where_clause) diff --git a/backend/danswer/server/documents/cc_pair.py b/backend/danswer/server/documents/cc_pair.py index 4b38eec7f71..55808ebcee7 100644 --- a/backend/danswer/server/documents/cc_pair.py +++ b/backend/danswer/server/documents/cc_pair.py @@ -387,6 +387,7 @@ def associate_credential_to_connector( user=user, target_group_ids=metadata.groups, object_is_public=metadata.access_type == AccessType.PUBLIC, + object_is_perm_sync=metadata.access_type == AccessType.SYNC, ) try: diff --git a/backend/danswer/server/documents/connector.py b/backend/danswer/server/documents/connector.py index 24f9920ac74..cc6b1419bae 100644 --- a/backend/danswer/server/documents/connector.py +++ b/backend/danswer/server/documents/connector.py @@ -81,7 +81,6 @@ from danswer.db.models import IndexingStatus from danswer.db.models import SearchSettings from danswer.db.models import User -from danswer.db.models import UserRole from danswer.db.search_settings import get_current_search_settings from danswer.db.search_settings import get_secondary_search_settings from danswer.file_store.file_store import get_default_file_store @@ -665,7 +664,8 @@ def create_connector_from_model( db_session=db_session, user=user, target_group_ids=connector_data.groups, - object_is_public=connector_data.is_public, + object_is_public=connector_data.access_type == AccessType.PUBLIC, + object_is_perm_sync=connector_data.access_type == AccessType.SYNC, ) connector_base = connector_data.to_connector_base() return create_connector( @@ -683,32 +683,31 @@ def create_connector_with_mock_credential( user: User = Depends(current_curator_or_admin_user), db_session: Session = Depends(get_session), ) -> StatusResponse: - if user and user.role != UserRole.ADMIN: - if connector_data.is_public: - raise HTTPException( - status_code=401, - detail="User does not have permission to create public credentials", - ) - if not connector_data.groups: - raise HTTPException( - status_code=401, - detail="Curators must specify 1+ groups", - ) + fetch_ee_implementation_or_noop( + "danswer.db.user_group", "validate_user_creation_permissions", None + )( + db_session=db_session, + user=user, + target_group_ids=connector_data.groups, + object_is_public=connector_data.access_type == AccessType.PUBLIC, + object_is_perm_sync=connector_data.access_type == AccessType.SYNC, + ) try: _validate_connector_allowed(connector_data.source) connector_response = create_connector( - db_session=db_session, connector_data=connector_data + db_session=db_session, + connector_data=connector_data, ) mock_credential = CredentialBase( - credential_json={}, admin_public=True, source=connector_data.source + credential_json={}, + admin_public=True, + source=connector_data.source, ) credential = create_credential( - mock_credential, user=user, db_session=db_session - ) - - access_type = ( - AccessType.PUBLIC if connector_data.is_public else AccessType.PRIVATE + credential_data=mock_credential, + user=user, + db_session=db_session, ) response = add_credential_to_connector( @@ -716,7 +715,7 @@ def create_connector_with_mock_credential( user=user, connector_id=cast(int, connector_response.id), # will aways be an int credential_id=credential.id, - access_type=access_type, + access_type=connector_data.access_type, cc_pair_name=connector_data.name, groups=connector_data.groups, ) @@ -741,7 +740,8 @@ def update_connector_from_model( db_session=db_session, user=user, target_group_ids=connector_data.groups, - object_is_public=connector_data.is_public, + object_is_public=connector_data.access_type == AccessType.PUBLIC, + object_is_perm_sync=connector_data.access_type == AccessType.SYNC, ) connector_base = connector_data.to_connector_base() except ValueError as e: diff --git a/backend/danswer/server/documents/models.py b/backend/danswer/server/documents/models.py index a541ae92c48..07610984515 100644 --- a/backend/danswer/server/documents/models.py +++ b/backend/danswer/server/documents/models.py @@ -64,11 +64,11 @@ class ConnectorBase(BaseModel): class ConnectorUpdateRequest(ConnectorBase): - is_public: bool = True + access_type: AccessType groups: list[int] = Field(default_factory=list) def to_connector_base(self) -> ConnectorBase: - return ConnectorBase(**self.model_dump(exclude={"is_public", "groups"})) + return ConnectorBase(**self.model_dump(exclude={"access_type", "groups"})) class ConnectorSnapshot(ConnectorBase): diff --git a/backend/ee/danswer/db/user_group.py b/backend/ee/danswer/db/user_group.py index 470a46e688b..0ba4e3bd49b 100644 --- a/backend/ee/danswer/db/user_group.py +++ b/backend/ee/danswer/db/user_group.py @@ -124,16 +124,21 @@ def _cleanup_document_set__user_group_relationships__no_commit( def validate_user_creation_permissions( db_session: Session, user: User | None, - target_group_ids: list[int] | None, - object_is_public: bool | None, + target_group_ids: list[int] | None = None, + object_is_public: bool | None = None, + object_is_perm_sync: bool | None = None, ) -> None: """ + All users can create/edit permission synced objects if they don't specify a group All admin actions are allowed. Prevents non-admins from creating/editing: - public objects - objects with no groups - objects that belong to a group they don't curate """ + if object_is_perm_sync and not target_group_ids: + return + if not user or user.role == UserRole.ADMIN: return diff --git a/web/src/app/admin/connector/[ccPairId]/page.tsx b/web/src/app/admin/connector/[ccPairId]/page.tsx index 9e6deefc1c4..33cbfe5207f 100644 --- a/web/src/app/admin/connector/[ccPairId]/page.tsx +++ b/web/src/app/admin/connector/[ccPairId]/page.tsx @@ -214,9 +214,11 @@ function Main({ ccPairId }: { ccPairId: number }) { {!ccPair.is_editable_for_current_user && (
- {ccPair.is_public + {ccPair.access_type === "public" ? "Public connectors are not editable by curators." - : "This connector belongs to groups where you don't have curator permissions, so it's not editable."} + : ccPair.access_type === "sync" + ? "Sync connectors are not editable by curators." + : "This connector belongs to groups where you don't have curator permissions, so it's not editable."}
)} diff --git a/web/src/app/admin/connector/[ccPairId]/types.ts b/web/src/app/admin/connector/[ccPairId]/types.ts index 5e9cec428c1..97a57e0d738 100644 --- a/web/src/app/admin/connector/[ccPairId]/types.ts +++ b/web/src/app/admin/connector/[ccPairId]/types.ts @@ -4,6 +4,7 @@ import { DeletionAttemptSnapshot, IndexAttemptSnapshot, ValidStatuses, + AccessType, } from "@/lib/types"; export enum ConnectorCredentialPairStatus { @@ -22,7 +23,7 @@ export interface CCPairFullInfo { number_of_index_attempts: number; last_index_attempt_status: ValidStatuses | null; latest_deletion_attempt: DeletionAttemptSnapshot | null; - is_public: boolean; + access_type: AccessType; is_editable_for_current_user: boolean; deletion_failure_message: string | null; indexing: boolean; diff --git a/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx b/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx index 9ea868c9389..4d0ec712f5f 100644 --- a/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx +++ b/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx @@ -54,13 +54,13 @@ const BASE_CONNECTOR_URL = "/api/manage/admin/connector"; export async function submitConnector( connector: ConnectorBase, connectorId?: number, - fakeCredential?: boolean, - isPublicCcpair?: boolean // exclusively for mock credentials, when also need to specify ccpair details + fakeCredential?: boolean ): Promise<{ message: string; isSuccess: boolean; response?: Connector }> { const isUpdate = connectorId !== undefined; if (!connector.connector_specific_config) { connector.connector_specific_config = {} as T; } + console.log(connector); try { if (fakeCredential) { @@ -71,7 +71,7 @@ export async function submitConnector( headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ ...connector, is_public: isPublicCcpair }), + body: JSON.stringify({ ...connector }), } ); if (response.ok) { @@ -112,6 +112,7 @@ export default function AddConnector({ connector: ConfigurableSources; }) { const router = useRouter(); + console.log(connector); // State for managing credentials and files const [currentCredential, setCurrentCredential] = @@ -268,7 +269,7 @@ export default function AddConnector({ advancedConfiguration.refreshFreq, advancedConfiguration.pruneFreq, advancedConfiguration.indexingStart, - values.access_type == "public", + values.access_type, groups, name ); @@ -285,7 +286,7 @@ export default function AddConnector({ setPopup, setSelectedFiles, name, - access_type == "public", + access_type, groups ); if (response) { @@ -293,6 +294,7 @@ export default function AddConnector({ } return; } + console.log(connector); const { message, isSuccess, response } = await submitConnector( { @@ -300,15 +302,14 @@ export default function AddConnector({ input_type: isLoadState(connector) ? "load_state" : "poll", // single case name: name, source: connector, - is_public: access_type == "public", + access_type: access_type, refresh_freq: advancedConfiguration.refreshFreq || null, prune_freq: advancedConfiguration.pruneFreq || null, indexing_start: advancedConfiguration.indexingStart || null, groups: groups, }, undefined, - credentialActivated ? false : true, - access_type == "public" + credentialActivated ? false : true ); // If no credential if (!credentialActivated) { diff --git a/web/src/app/admin/connectors/[connector]/pages/DynamicConnectorCreationForm.tsx b/web/src/app/admin/connectors/[connector]/pages/DynamicConnectorCreationForm.tsx index dc7f75e0632..0f3cd05341a 100644 --- a/web/src/app/admin/connectors/[connector]/pages/DynamicConnectorCreationForm.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/DynamicConnectorCreationForm.tsx @@ -164,7 +164,7 @@ const DynamicConnectionForm: FC = ({ )} - + {config.advanced_values.length > 0 && ( <> diff --git a/web/src/app/admin/connectors/[connector]/pages/utils/files.ts b/web/src/app/admin/connectors/[connector]/pages/utils/files.ts index c7b36ac0fbe..890ce5cc5d0 100644 --- a/web/src/app/admin/connectors/[connector]/pages/utils/files.ts +++ b/web/src/app/admin/connectors/[connector]/pages/utils/files.ts @@ -2,13 +2,14 @@ import { PopupSpec } from "@/components/admin/connectors/Popup"; import { createConnector, runConnector } from "@/lib/connector"; import { createCredential, linkCredential } from "@/lib/credential"; import { FileConfig } from "@/lib/connectors/connectors"; +import { AccessType } from "@/lib/types"; export const submitFiles = async ( selectedFiles: File[], setPopup: (popup: PopupSpec) => void, setSelectedFiles: (files: File[]) => void, name: string, - isPublic: boolean, + access_type: string, groups?: number[] ) => { const formData = new FormData(); @@ -42,7 +43,7 @@ export const submitFiles = async ( refresh_freq: null, prune_freq: null, indexing_start: null, - is_public: isPublic, + access_type: access_type, groups: groups, }); if (connectorErrorMsg || !connector) { @@ -61,7 +62,7 @@ export const submitFiles = async ( credential_json: {}, admin_public: true, source: "file", - curator_public: isPublic, + curator_public: true, groups: groups, name, }); @@ -80,7 +81,7 @@ export const submitFiles = async ( connector.id, credentialId, name, - isPublic ? "public" : "private", + access_type as AccessType, groups ); if (!credentialResponse.ok) { diff --git a/web/src/app/admin/connectors/[connector]/pages/utils/google_site.ts b/web/src/app/admin/connectors/[connector]/pages/utils/google_site.ts index 45b713fe786..e297c4e394a 100644 --- a/web/src/app/admin/connectors/[connector]/pages/utils/google_site.ts +++ b/web/src/app/admin/connectors/[connector]/pages/utils/google_site.ts @@ -10,7 +10,7 @@ export const submitGoogleSite = async ( refreshFreq: number, pruneFreq: number, indexingStart: Date, - is_public: boolean, + access_type: string, groups: number[], name?: string ) => { @@ -44,7 +44,7 @@ export const submitGoogleSite = async ( base_url: base_url, zip_path: filePaths[0], }, - is_public: is_public, + access_type: access_type, refresh_freq: refreshFreq, prune_freq: pruneFreq, indexing_start: indexingStart, diff --git a/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx b/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx index faea0de39b3..62ce28a870e 100644 --- a/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx +++ b/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx @@ -394,6 +394,7 @@ export function CCPairIndexingStatusTable({ indexing_start: new Date("2023-07-01T12:00:00Z"), id: 1, credential_ids: [], + access_type: "public", time_created: "2023-07-01T12:00:00Z", time_updated: "2023-07-01T12:00:00Z", }, diff --git a/web/src/components/IsPublicGroupSelector.tsx b/web/src/components/IsPublicGroupSelector.tsx index 6cb953f5bdf..2de39dfe598 100644 --- a/web/src/components/IsPublicGroupSelector.tsx +++ b/web/src/components/IsPublicGroupSelector.tsx @@ -33,7 +33,7 @@ export const IsPublicGroupSelector = ({ const { isAdmin, user, isLoadingUser, isCurator } = useUser(); const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled(); const [shouldHideContent, setShouldHideContent] = useState(false); - + console.log(formikProps.values); useEffect(() => { if (user && userGroups && isPaidEnterpriseFeaturesEnabled) { const isUserAdmin = user.role === UserRole.ADMIN; diff --git a/web/src/components/admin/connectors/AccessTypeForm.tsx b/web/src/components/admin/connectors/AccessTypeForm.tsx index 8993e28cdb3..5950cb6fdc0 100644 --- a/web/src/components/admin/connectors/AccessTypeForm.tsx +++ b/web/src/components/admin/connectors/AccessTypeForm.tsx @@ -62,7 +62,7 @@ export function AccessTypeForm({ }); } - if (isAutoSyncSupported && isAdmin && isPaidEnterpriseEnabled) { + if (isAutoSyncSupported && isPaidEnterpriseEnabled) { options.push({ name: "Auto Sync Permissions", value: "sync", @@ -73,7 +73,7 @@ export function AccessTypeForm({ return ( <> - {isPaidEnterpriseEnabled && isAdmin && ( + {isPaidEnterpriseEnabled && (isAdmin || isAutoSyncSupported) && ( <>
diff --git a/web/src/components/admin/connectors/AccessTypeGroupSelector.tsx b/web/src/components/admin/connectors/AccessTypeGroupSelector.tsx index 7080f92c82c..7fbc0b3de1a 100644 --- a/web/src/components/admin/connectors/AccessTypeGroupSelector.tsx +++ b/web/src/components/admin/connectors/AccessTypeGroupSelector.tsx @@ -6,9 +6,20 @@ import { Separator } from "@/components/ui/separator"; import { FiUsers } from "react-icons/fi"; import { UserGroup, UserRole } from "@/lib/types"; import { useUserGroups } from "@/lib/hooks"; -import { AccessType } from "@/lib/types"; +import { + AccessType, + ValidAutoSyncSources, + ConfigurableSources, + validAutoSyncSources, +} from "@/lib/types"; import { useUser } from "@/components/user/UserProvider"; +function isValidAutoSyncSource( + value: ConfigurableSources +): value is ValidAutoSyncSources { + return validAutoSyncSources.includes(value as ValidAutoSyncSources); +} + // This should be included for all forms that require groups / public access // to be set, and access to this / permissioning should be handled within this component itself. @@ -17,11 +28,16 @@ export type AccessTypeGroupSelectorFormType = { groups: number[]; }; -export function AccessTypeGroupSelector({}: {}) { +export function AccessTypeGroupSelector({ + connector, +}: { + connector: ConfigurableSources; +}) { const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups(); const { isAdmin, user, isLoadingUser, isCurator } = useUser(); const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled(); const [shouldHideContent, setShouldHideContent] = useState(false); + const isAutoSyncSupported = isValidAutoSyncSource(connector); const [access_type, meta, access_type_helpers] = useField("access_type"); @@ -34,13 +50,18 @@ export function AccessTypeGroupSelector({}: {}) { access_type_helpers.setValue("public"); return; } - if (!isUserAdmin) { + if (!isUserAdmin && !isAutoSyncSupported) { access_type_helpers.setValue("private"); } - if (userGroups.length === 1 && !isUserAdmin) { + if ( + access_type.value === "private" && + userGroups.length === 1 && + !isUserAdmin + ) { groups_helpers.setValue([userGroups[0].id]); setShouldHideContent(true); } else if (access_type.value !== "private") { + // If the access type is public or sync, empty the groups selection groups_helpers.setValue([]); setShouldHideContent(false); } else { diff --git a/web/src/components/credentials/actions/CreateCredential.tsx b/web/src/components/credentials/actions/CreateCredential.tsx index cdbb7f664eb..df11decdf38 100644 --- a/web/src/components/credentials/actions/CreateCredential.tsx +++ b/web/src/components/credentials/actions/CreateCredential.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; import { ValidSources } from "@/lib/types"; import { FaAccusoft } from "react-icons/fa"; import { submitCredential } from "@/components/admin/connectors/CredentialForm"; @@ -15,8 +14,6 @@ import { credentialTemplates, getDisplayNameForCredentialKey, } from "@/lib/connectors/credentials"; -import { getCurrentUser } from "@/lib/user"; -import { User, UserRole } from "@/lib/types"; import { PlusCircleIcon } from "../../icons/icons"; import { GmailMain } from "@/app/admin/connectors/[connector]/pages/gmail/GmailPage"; import { ActionType, dictionaryType } from "../types"; diff --git a/web/src/lib/connectors/connectors.tsx b/web/src/lib/connectors/connectors.tsx index 89dca2f287f..77bea11c808 100644 --- a/web/src/lib/connectors/connectors.tsx +++ b/web/src/lib/connectors/connectors.tsx @@ -1032,7 +1032,7 @@ export interface ConnectorBase { refresh_freq: number | null; prune_freq: number | null; indexing_start: Date | null; - is_public?: boolean; + access_type: string; groups?: number[]; }