Compare commits

...

1 Commits

Author SHA1 Message Date
Weves
e0abe3dd81 Allow curators to create public connectors / document sets 2025-06-30 19:53:21 -07:00
13 changed files with 91 additions and 25 deletions

View File

@@ -128,11 +128,14 @@ def validate_object_creation_for_user(
target_group_ids: list[int] | None = None,
object_is_public: bool | None = None,
object_is_perm_sync: bool | None = None,
object_is_owned_by_user: bool | None = None,
object_is_new: 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:
Curators and global curators can create public objects.
Prevents other non-admins from creating/editing:
- public objects
- objects with no groups
- objects that belong to a group they don't curate
@@ -143,13 +146,23 @@ def validate_object_creation_for_user(
if not user or user.role == UserRole.ADMIN:
return
if object_is_public:
detail = "User does not have permission to create public credentials"
# Allow curators and global curators to create public objects
# w/o associated groups IF the object is new/owned by them
if (
object_is_public
and user.role in [UserRole.CURATOR, UserRole.GLOBAL_CURATOR]
and (object_is_new or object_is_owned_by_user)
):
return
if object_is_public and user.role == UserRole.BASIC:
detail = "User does not have permission to create public objects"
logger.error(detail)
raise HTTPException(
status_code=400,
detail=detail,
)
if not target_group_ids:
detail = "Curators must specify 1+ groups"
logger.error(detail)

View File

@@ -629,6 +629,16 @@ def fetch_connector_credential_pairs(
return list(db_session.scalars(stmt).unique().all())
def fetch_connector_credential_pair_for_connector(
db_session: Session,
connector_id: int,
) -> ConnectorCredentialPair | None:
stmt = select(ConnectorCredentialPair).where(
ConnectorCredentialPair.connector_id == connector_id,
)
return db_session.scalar(stmt)
def resync_cc_pair(
cc_pair: ConnectorCredentialPair,
search_settings_id: int,

View File

@@ -81,6 +81,7 @@ def _add_user_filters(
.where(~DocumentSet__UG.user_group_id.in_(user_groups))
.correlate(DocumentSetDBModel)
)
where_clause |= DocumentSetDBModel.user_id == user.id
else:
where_clause |= DocumentSetDBModel.is_public == True # noqa: E712

View File

@@ -463,6 +463,7 @@ def associate_credential_to_connector(
target_group_ids=metadata.groups,
object_is_public=metadata.access_type == AccessType.PUBLIC,
object_is_perm_sync=metadata.access_type == AccessType.SYNC,
object_is_new=True,
)
try:

View File

@@ -73,6 +73,9 @@ from onyx.db.connector import get_connector_credential_ids
from onyx.db.connector import mark_ccpair_with_indexing_trigger
from onyx.db.connector import update_connector
from onyx.db.connector_credential_pair import add_credential_to_connector
from onyx.db.connector_credential_pair import (
fetch_connector_credential_pair_for_connector,
)
from onyx.db.connector_credential_pair import get_cc_pair_groups_for_ids
from onyx.db.connector_credential_pair import get_cc_pair_groups_for_ids_parallel
from onyx.db.connector_credential_pair import get_connector_credential_pair
@@ -890,6 +893,7 @@ def create_connector_from_model(
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,
object_is_new=True,
)
connector_base = connector_data.to_connector_base()
connector_response = create_connector(
@@ -1002,6 +1006,7 @@ def update_connector_from_model(
user: User = Depends(current_curator_or_admin_user),
db_session: Session = Depends(get_session),
) -> ConnectorSnapshot | StatusResponse[int]:
cc_pair = fetch_connector_credential_pair_for_connector(db_session, connector_id)
try:
_validate_connector_allowed(connector_data.source)
fetch_ee_implementation_or_noop(
@@ -1012,6 +1017,7 @@ def update_connector_from_model(
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,
object_is_owned_by_user=cc_pair and user and cc_pair.creator_id == user.id,
)
connector_base = connector_data.to_connector_base()
except ValueError as e:

View File

@@ -11,6 +11,7 @@ from onyx.configs.constants import OnyxCeleryPriority
from onyx.configs.constants import OnyxCeleryTask
from onyx.db.document_set import check_document_sets_are_public
from onyx.db.document_set import fetch_all_document_sets_for_user
from onyx.db.document_set import get_document_set_by_id
from onyx.db.document_set import insert_document_set
from onyx.db.document_set import mark_document_set_as_to_be_deleted
from onyx.db.document_set import update_document_set
@@ -42,6 +43,7 @@ def create_document_set(
user=user,
target_group_ids=document_set_creation_request.groups,
object_is_public=document_set_creation_request.is_public,
object_is_new=True,
)
try:
document_set_db_model, _ = insert_document_set(
@@ -64,10 +66,17 @@ def create_document_set(
@router.patch("/admin/document-set")
def patch_document_set(
document_set_update_request: DocumentSetUpdateRequest,
user: User = Depends(current_curator_or_admin_user),
user: User | None = Depends(current_curator_or_admin_user),
db_session: Session = Depends(get_session),
tenant_id: str = Depends(get_current_tenant_id),
) -> None:
document_set = get_document_set_by_id(db_session, document_set_update_request.id)
if document_set is None:
raise HTTPException(
status_code=404,
detail=f"Document set {document_set_update_request.id} does not exist",
)
fetch_ee_implementation_or_noop(
"onyx.db.user_group", "validate_object_creation_for_user", None
)(
@@ -75,6 +84,7 @@ def patch_document_set(
user=user,
target_group_ids=document_set_update_request.groups,
object_is_public=document_set_update_request.is_public,
object_is_owned_by_user=user and document_set.user_id == user.id,
)
try:
update_document_set(
@@ -99,6 +109,22 @@ def delete_document_set(
db_session: Session = Depends(get_session),
tenant_id: str = Depends(get_current_tenant_id),
) -> None:
document_set = get_document_set_by_id(db_session, document_set_id)
if document_set is None:
raise HTTPException(
status_code=404,
detail=f"Document set {document_set_id} does not exist",
)
fetch_ee_implementation_or_noop(
"onyx.db.user_group", "validate_object_creation_for_user", None
)(
db_session=db_session,
user=user,
object_is_public=document_set.is_public,
object_is_owned_by_user=user and document_set.user_id == user.id,
)
try:
mark_document_set_as_to_be_deleted(
db_session=db_session,

View File

@@ -33,7 +33,11 @@ function Main({ documentSetId }: { documentSetId: number }) {
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
if (isDocumentSetsLoading || isCCPairsLoading || userGroupsIsLoading) {
return <ThreeDotsLoader />;
return (
<div className="flex justify-center items-center min-h-[400px]">
<ThreeDotsLoader />
</div>
);
}
if (documentSetsError || !documentSets) {
@@ -98,7 +102,7 @@ export default function Page(props: {
const documentSetId = parseInt(params.documentSetId);
return (
<div>
<div className="container mx-auto">
<BackButton />
<Main documentSetId={documentSetId} />

View File

@@ -26,7 +26,11 @@ function Main() {
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
if (isCCPairsLoading || userGroupsIsLoading) {
return <ThreeDotsLoader />;
return (
<div className="flex justify-center items-center min-h-[400px]">
<ThreeDotsLoader />
</div>
);
}
if (ccPairsError || !ccPairs) {

View File

@@ -281,7 +281,11 @@ const Main = () => {
} = useDocumentSets(true);
if (isDocumentSetsLoading || isEditableDocumentSetsLoading) {
return <ThreeDotsLoader />;
return (
<div className="flex justify-center items-center min-h-[400px]">
<ThreeDotsLoader />
</div>
);
}
if (documentSetsError || !documentSets) {
@@ -308,9 +312,6 @@ const Main = () => {
href="/admin/documents/sets/new"
text="New Document Set"
/>
{/* <Link href="/admin/documents/sets/new">
<Button variant="navigate">New Document Set</Button>
</Link> */}
</div>
{documentSets.length > 0 && (

View File

@@ -36,7 +36,7 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
useEffect(() => {
if (user && userGroups && isPaidEnterpriseFeaturesEnabled) {
const isUserAdmin = user.role === UserRole.ADMIN;
if (!isUserAdmin) {
if (!isUserAdmin && userGroups.length > 0) {
formikProps.setFieldValue("is_public", false);
}
if (

View File

@@ -5,7 +5,6 @@ import {
ConfigurableSources,
validAutoSyncSources,
} from "@/lib/types";
import { useUser } from "@/components/user/UserProvider";
import { useField } from "formik";
import { AutoSyncOptions } from "./AutoSyncOptions";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
@@ -27,7 +26,6 @@ export function AccessTypeForm({
const isPaidEnterpriseEnabled = usePaidEnterpriseFeaturesEnabled();
const isAutoSyncSupported = isValidAutoSyncSource(connector);
const { isAdmin } = useUser();
useEffect(
() => {
@@ -55,16 +53,13 @@ export function AccessTypeForm({
description:
"Only users who have explicitly been given access to this connector (through the User Groups page) can access the documents pulled in by this connector",
},
];
if (isAdmin) {
options.push({
{
name: "Public",
value: "public",
description:
"Everyone with an account on Onyx can access the documents pulled in by this connector",
});
}
},
];
if (isAutoSyncSupported && isPaidEnterpriseEnabled) {
options.push({
@@ -77,7 +72,7 @@ export function AccessTypeForm({
return (
<>
{isPaidEnterpriseEnabled && (isAdmin || isAutoSyncSupported) && (
{isPaidEnterpriseEnabled && (
<>
<div>
<label className="text-text-950 font-medium">Document Access</label>
@@ -88,9 +83,9 @@ export function AccessTypeForm({
<DefaultDropdown
options={options}
selected={access_type.value}
onSelect={(selected) =>
access_type_helpers.setValue(selected as AccessType)
}
onSelect={(selected) => {
access_type_helpers.setValue(selected as AccessType);
}}
includeDefault={false}
/>
{access_type.value === "sync" && isAutoSyncSupported && (

View File

@@ -50,9 +50,12 @@ export function AccessTypeGroupSelector({
access_type_helpers.setValue("public");
return;
}
if (!isUserAdmin && !isAutoSyncSupported) {
// Only set default access type if it's not already set, to avoid overriding user selections
if (!access_type.value && !isUserAdmin && !isAutoSyncSupported) {
access_type_helpers.setValue("private");
}
if (
access_type.value === "private" &&
userGroups.length === 1 &&
@@ -76,6 +79,7 @@ export function AccessTypeGroupSelector({
access_type_helpers,
groups_helpers,
isPaidEnterpriseFeaturesEnabled,
isAutoSyncSupported,
]);
if (userGroupsIsLoading) {

View File

@@ -207,6 +207,7 @@ export default function CreateCredential({
) {
formikProps.setFieldValue("authentication_method", authMethod);
}
console.log(formikProps.isSubmitting);
return (
<Form className="w-full flex items-stretch">