🗨️ feat: Granular Prompt Permissions via ACL and Permission Bits

feat: Implement prompt permissions management and access control middleware

fix: agent deletion process to remove associated permissions and ACL entries

fix: Import Permissions for enhanced access control in GrantAccessDialog

feat: use PromptGroup for access control

- Added migration script for PromptGroup permissions, categorizing groups into global view access and private groups.
- Created unit tests for the migration script to ensure correct categorization and permission granting.
- Introduced middleware for checking access permissions on PromptGroups and prompts via their groups.
- Updated routes to utilize new access control middleware for PromptGroups.
- Enhanced access role definitions to include roles specific to PromptGroups.
- Modified ACL entry schema and types to accommodate PromptGroup resource type.
- Updated data provider to include new access role identifiers for PromptGroups.

feat: add generic access management dialogs and hooks for resource permissions

fix: remove duplicate imports in FileContext component

fix: remove duplicate mongoose dependency in package.json

feat: add access permissions handling for dynamic resource types and add promptGroup roles

feat: implement centralized role localization and update access role types

refactor: simplify author handling in prompt group routes and enhance ACL checks

feat: implement addPromptToGroup functionality and update PromptForm to use it

feat: enhance permission handling in ChatGroupItem, DashGroupItem, and PromptForm components

chore: rename migration script for prompt group permissions and update package.json scripts

chore: update prompt tests
This commit is contained in:
Danny Avila 2025-07-26 12:28:31 -04:00
parent 7e7e75714e
commit ae732b2ebc
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
46 changed files with 3505 additions and 408 deletions

View file

@ -1,90 +1,57 @@
import React, { useEffect, useMemo } from 'react';
import React from 'react';
import { Share2Icon } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions } from 'librechat-data-provider';
import {
Button,
Switch,
OGDialog,
OGDialogTitle,
OGDialogClose,
OGDialogContent,
OGDialogTrigger,
useToastContext,
} from '@librechat/client';
import type {
TPromptGroup,
TStartupConfig,
TUpdatePromptGroupPayload,
SystemRoles,
Permissions,
PermissionTypes,
PERMISSION_BITS,
} from 'librechat-data-provider';
import { useUpdatePromptGroup, useGetStartupConfig } from '~/data-provider';
import { useLocalize } from '~/hooks';
import { Button } from '@librechat/client';
import type { TPromptGroup } from 'librechat-data-provider';
import { useAuthContext, useHasAccess, useResourcePermissions } from '~/hooks';
import { GenericGrantAccessDialog } from '~/components/Sharing';
type FormValues = {
[Permissions.SHARED_GLOBAL]: boolean;
};
const SharePrompt = React.memo(
({ group, disabled }: { group?: TPromptGroup; disabled: boolean }) => {
const { user } = useAuthContext();
const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: boolean }) => {
const localize = useLocalize();
const { showToast } = useToastContext();
const updateGroup = useUpdatePromptGroup();
const { data: startupConfig = {} as TStartupConfig, isFetching } = useGetStartupConfig();
const { instanceProjectId } = startupConfig;
const groupIsGlobal = useMemo(
() => ((group?.projectIds ?? []) as string[]).includes(instanceProjectId as string),
[group, instanceProjectId],
);
const {
control,
setValue,
handleSubmit,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onChange',
defaultValues: {
[Permissions.SHARED_GLOBAL]: groupIsGlobal,
},
});
useEffect(() => {
setValue(Permissions.SHARED_GLOBAL, groupIsGlobal);
}, [groupIsGlobal, setValue]);
if (group == null || !instanceProjectId) {
return null;
}
const onSubmit = (data: FormValues) => {
const groupId = group._id ?? '';
if (groupId === '' || !instanceProjectId) {
return;
}
if (data[Permissions.SHARED_GLOBAL] === true && groupIsGlobal) {
showToast({
message: localize('com_ui_prompt_already_shared_to_all'),
status: 'info',
});
return;
}
const payload = {} as TUpdatePromptGroupPayload;
if (data[Permissions.SHARED_GLOBAL] === true) {
payload.projectIds = [startupConfig.instanceProjectId];
} else {
payload.removeProjectIds = [startupConfig.instanceProjectId];
}
updateGroup.mutate({
id: groupId,
payload,
// Check if user has permission to share prompts globally
const hasAccessToSharePrompts = useHasAccess({
permissionType: PermissionTypes.PROMPTS,
permission: Permissions.SHARED_GLOBAL,
});
};
return (
<OGDialog>
<OGDialogTrigger asChild>
// Check user's permissions on this specific promptGroup
// The query will be disabled if groupId is empty
const groupId = group?._id || '';
const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions(
'promptGroup',
groupId,
);
// Early return if no group
if (!group || !groupId) {
return null;
}
const canShareThisPrompt = hasPermission(PERMISSION_BITS.SHARE);
const shouldShowShareButton =
(group.author === user?.id || user?.role === SystemRoles.ADMIN || canShareThisPrompt) &&
hasAccessToSharePrompts &&
!permissionsLoading;
if (!shouldShowShareButton) {
return null;
}
return (
<GenericGrantAccessDialog
resourceDbId={groupId}
resourceName={group.name}
resourceType="promptGroup"
disabled={disabled}
>
<Button
variant="default"
size="sm"
@ -94,50 +61,11 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
>
<Share2Icon className="size-5 cursor-pointer text-white" />
</Button>
</OGDialogTrigger>
<OGDialogContent className="w-11/12 max-w-lg" role="dialog" aria-labelledby="dialog-title">
<OGDialogTitle id="dialog-title" className="truncate pr-2" title={group.name}>
{localize('com_ui_share_var', { 0: `"${group.name}"` })}
</OGDialogTitle>
<form className="p-2" onSubmit={handleSubmit(onSubmit)} aria-describedby="form-description">
<div id="form-description" className="sr-only">
{localize('com_ui_share_form_description')}
</div>
<div className="mb-4 flex items-center justify-between gap-2 py-4">
<div className="flex items-center" id="share-to-all-users">
{localize('com_ui_share_to_all_users')}
</div>
<Controller
name={Permissions.SHARED_GLOBAL}
control={control}
disabled={isFetching === true || updateGroup.isLoading || !instanceProjectId}
render={({ field }) => (
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
aria-labelledby="share-to-all-users"
/>
)}
/>
</div>
<div className="flex justify-end">
<OGDialogClose asChild>
<Button
type="submit"
disabled={isSubmitting || isFetching}
variant="submit"
aria-label={localize('com_ui_save')}
>
{localize('com_ui_save')}
</Button>
</OGDialogClose>
</div>
</form>
</OGDialogContent>
</OGDialog>
);
};
</GenericGrantAccessDialog>
);
},
);
SharePrompt.displayName = 'SharePrompt';
export default SharePrompt;