mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
feat: refactor access rule management to utilize new app and dataset access rule services, enhance role binding functionality, and remove deprecated components
This commit is contained in:
parent
7aa8f1a0b6
commit
cbedd2b852
@ -1,120 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogCloseButton,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import { useCallback, useState } from 'react'
|
||||
import AccessRulesEditor from '@/app/components/access-rules-editor'
|
||||
|
||||
export type AccessConfigModalProps = {
|
||||
open: boolean
|
||||
title: string
|
||||
description: string
|
||||
initialRules: AccessRule[]
|
||||
/**
|
||||
* Optional override label for the primary action. Defaults to "Save".
|
||||
*/
|
||||
saveLabel?: string
|
||||
/**
|
||||
* Optional override label for the cancel action. Defaults to "Cancel".
|
||||
*/
|
||||
cancelLabel?: string
|
||||
onClose: () => void
|
||||
onSave?: (rules: AccessRule[]) => void
|
||||
}
|
||||
|
||||
type AccessConfigModalBodyProps = Omit<AccessConfigModalProps, 'open'>
|
||||
|
||||
const AccessConfigModalBody = ({
|
||||
title,
|
||||
description,
|
||||
initialRules,
|
||||
saveLabel = 'Save',
|
||||
cancelLabel = 'Cancel',
|
||||
onClose,
|
||||
onSave,
|
||||
}: AccessConfigModalBodyProps) => {
|
||||
const [rules, setRules] = useState<AccessRule[]>(initialRules)
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave?.(rules)
|
||||
onClose()
|
||||
}, [onClose, onSave, rules])
|
||||
|
||||
return (
|
||||
<DialogContent
|
||||
className="flex max-h-[85vh] w-[520px] flex-col overflow-hidden p-0"
|
||||
backdropProps={{ forceRender: true }}
|
||||
>
|
||||
<div className="relative shrink-0 px-6 pt-6 pb-4">
|
||||
<DialogCloseButton />
|
||||
<div className="pr-8">
|
||||
<DialogTitle className="system-xl-semibold text-text-primary">
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-1 system-sm-regular text-text-tertiary">
|
||||
{description}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea
|
||||
className="min-h-0 flex-1"
|
||||
slotClassNames={{ viewport: 'px-6 overscroll-contain' }}
|
||||
>
|
||||
<AccessRulesEditor rules={rules} onRulesChange={setRules} />
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-divider-subtle px-6 py-4">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
{saveLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
const AccessConfigModal = ({
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
initialRules,
|
||||
saveLabel,
|
||||
cancelLabel,
|
||||
onClose,
|
||||
onSave,
|
||||
}: AccessConfigModalProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
{open && (
|
||||
<AccessConfigModalBody
|
||||
title={title}
|
||||
description={description}
|
||||
initialRules={initialRules}
|
||||
saveLabel={saveLabel}
|
||||
cancelLabel={cancelLabel}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccessConfigModal
|
||||
@ -1,97 +1,119 @@
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
AccessRule,
|
||||
AssignedRole,
|
||||
} from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import type { AccessPolicyWithBindings, RemoveBindingPayload } from '@/models/access-control'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useCallback, useState } from 'react'
|
||||
import AccessRuleRow from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import AddRuleTargetsModal from '@/app/components/header/account-setting/access-rules-page/add-rule-targets-modal'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { useUpdateAppAccessRuleBindings } from '@/service/access-control/use-app-access-config'
|
||||
import { useUpdateDatasetAccessRuleBindings } from '@/service/access-control/use-dataset-access-config'
|
||||
|
||||
export type AccessRulesEditorProps = {
|
||||
rules: AccessRule[]
|
||||
/**
|
||||
* Called whenever assigned roles/members are mutated. The editor is
|
||||
* controlled when this callback is provided, uncontrolled (with internal
|
||||
* state seeded from `rules`) otherwise.
|
||||
*/
|
||||
onRulesChange?: (rules: AccessRule[]) => void
|
||||
rules: AccessPolicyWithBindings[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AccessRulesEditor = ({
|
||||
rules: rulesProp,
|
||||
onRulesChange,
|
||||
rules,
|
||||
className,
|
||||
}: AccessRulesEditorProps) => {
|
||||
const isControlled = typeof onRulesChange === 'function'
|
||||
const [internalRules, setInternalRules] = useState<AccessRule[]>(rulesProp)
|
||||
const rules = isControlled ? rulesProp : internalRules
|
||||
const { appId } = useParams() as { appId: string }
|
||||
const [currentRule, setCurrentRule] = useState<AccessPolicyWithBindings | null>(null)
|
||||
|
||||
const updateRules = useCallback(
|
||||
(updater: (prev: AccessRule[]) => AccessRule[]) => {
|
||||
if (isControlled) {
|
||||
onRulesChange(updater(rulesProp))
|
||||
return
|
||||
}
|
||||
setInternalRules(prev => updater(prev))
|
||||
},
|
||||
[isControlled, onRulesChange, rulesProp],
|
||||
)
|
||||
|
||||
const [addingRule, setAddingRule] = useState<AccessRule | null>(null)
|
||||
|
||||
const handleAddRole = useCallback((rule: AccessRule) => {
|
||||
setAddingRule(rule)
|
||||
const handleAddRole = useCallback((rule: AccessPolicyWithBindings) => {
|
||||
setCurrentRule(rule)
|
||||
}, [])
|
||||
|
||||
const handleCloseAddModal = useCallback(() => {
|
||||
setAddingRule(null)
|
||||
setCurrentRule(null)
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: updateAppAccessRuleBindings } = useUpdateAppAccessRuleBindings()
|
||||
const { mutateAsync: updateDatasetAccessRuleBindings } = useUpdateDatasetAccessRuleBindings()
|
||||
|
||||
const handleAddSubmit = useCallback(
|
||||
(_selection: { roleIds: string[], memberIds: string[] }) => {
|
||||
// TODO: wire up to API when backend is ready.
|
||||
(selection: { roleIds: string[], memberIds: string[] }) => {
|
||||
const { policy } = currentRule || {}
|
||||
const { id: policyId, resource_type } = policy || {}
|
||||
if (resource_type === 'app') {
|
||||
updateAppAccessRuleBindings({
|
||||
appId,
|
||||
policyId: policyId || '',
|
||||
role_ids: selection.roleIds,
|
||||
account_ids: selection.memberIds,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success('Rule binding updated successfully')
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (resource_type === 'dataset') {
|
||||
updateDatasetAccessRuleBindings({
|
||||
datasetId: appId,
|
||||
policyId: policyId || '',
|
||||
role_ids: selection.roleIds,
|
||||
account_ids: selection.memberIds,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success('Rule binding updated successfully')
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[],
|
||||
[appId, currentRule, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings],
|
||||
)
|
||||
|
||||
const handleRemoveRole = useCallback(
|
||||
(target: AccessRule, role: AssignedRole) => {
|
||||
updateRules(prev =>
|
||||
prev.map(rule =>
|
||||
rule.id === target.id
|
||||
? {
|
||||
...rule,
|
||||
assignedRoles: rule.assignedRoles.filter(r => r.id !== role.id),
|
||||
}
|
||||
: rule,
|
||||
),
|
||||
)
|
||||
(payload: RemoveBindingPayload) => {
|
||||
const { policy_id, role_ids, account_ids, resource_type } = payload
|
||||
if (resource_type === 'app') {
|
||||
updateAppAccessRuleBindings({
|
||||
appId,
|
||||
policyId: policy_id,
|
||||
role_ids,
|
||||
account_ids,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success('Rule binding removed successfully')
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (resource_type === 'dataset') {
|
||||
updateDatasetAccessRuleBindings({
|
||||
datasetId: appId,
|
||||
policyId: policy_id,
|
||||
role_ids,
|
||||
account_ids,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success('Rule binding removed successfully')
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[updateRules],
|
||||
[appId, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)}>
|
||||
{rules.map((rule, index) => (
|
||||
<AccessRuleRow
|
||||
key={rule.id}
|
||||
key={rule.policy.id}
|
||||
rule={rule}
|
||||
showMenu={false}
|
||||
onAddRole={handleAddRole}
|
||||
onRemoveRole={handleRemoveRole}
|
||||
onRemove={handleRemoveRole}
|
||||
className={cn(index > 0 && 'border-t border-divider-subtle')}
|
||||
/>
|
||||
))}
|
||||
|
||||
{addingRule && (
|
||||
{currentRule && (
|
||||
<AddRuleTargetsModal
|
||||
open
|
||||
ruleName={addingRule.name}
|
||||
initialRoleIds={addingRule.assignedRoles.map(role => role.id)}
|
||||
initialMemberIds={[]}
|
||||
ruleName={currentRule.policy.name}
|
||||
initialRoleIds={currentRule.roles.map(role => role.role_id)}
|
||||
initialMemberIds={currentRule.accounts.map(account => account.account_id)}
|
||||
onClose={handleCloseAddModal}
|
||||
onSubmit={handleAddSubmit}
|
||||
/>
|
||||
|
||||
@ -1,61 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import AccessRulesEditor from '@/app/components/access-rules-editor'
|
||||
|
||||
// TODO: replace with the per-app access rules fetched from the access-rules
|
||||
// API once available. Mirrors the workspace-level App access rules catalog.
|
||||
const DEFAULT_APP_ACCESS_RULES: AccessRule[] = [
|
||||
{
|
||||
id: 'app-full-access',
|
||||
name: 'Full access',
|
||||
description: 'Highest level. Can edit, publish, delete apps, and manage access for this app.',
|
||||
assignedRoles: [
|
||||
{ id: 'owner', name: 'Owner' },
|
||||
{ id: 'admin', name: 'Admin' },
|
||||
{ id: 'app-admin', name: 'App Admin' },
|
||||
{ id: 'executive', name: 'Executive' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'app-can-edit',
|
||||
name: 'Can edit',
|
||||
description: 'Modify Prompts, adjust workflows, change variables. Test and publish updates.',
|
||||
assignedRoles: [
|
||||
{ id: 'app-editor', name: 'App Editor' },
|
||||
{ id: 'it-staff', name: 'IT Staff' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'app-can-view-and-use',
|
||||
name: 'Can view & use',
|
||||
description: 'View and use the app. Access Prompt and workflow logs. Cannot modify.',
|
||||
assignedRoles: [
|
||||
{ id: 'tester', name: 'Tester' },
|
||||
{ id: 'ops-staff', name: 'Ops Staff' },
|
||||
{ id: 'member', name: 'Member' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'app-can-preview',
|
||||
name: 'Can preview',
|
||||
description: 'View the app in the list only. Cannot open the editor or use the app.',
|
||||
assignedRoles: [
|
||||
{ id: 'partner', name: 'Partner' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
]
|
||||
import { useAppAccessRules } from '@/service/access-control/use-app-access-config'
|
||||
|
||||
type AppAccessConfigPageProps = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
const AppAccessConfigPage = ({ appId: _appId }: AppAccessConfigPageProps) => {
|
||||
const AppAccessConfigPage = ({ appId }: AppAccessConfigPageProps) => {
|
||||
const { data: appAccessRulesResponse } = useAppAccessRules(appId)
|
||||
|
||||
const appAccessRules = appAccessRulesResponse?.items || []
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
className="h-full bg-components-panel-bg"
|
||||
@ -64,7 +21,7 @@ const AppAccessConfigPage = ({ appId: _appId }: AppAccessConfigPageProps) => {
|
||||
<div className="w-full px-16 py-8">
|
||||
<h1 className="title-2xl-semi-bold text-text-primary">Access Config</h1>
|
||||
<div className="mt-6">
|
||||
<AccessRulesEditor rules={DEFAULT_APP_ACCESS_RULES} />
|
||||
<AccessRulesEditor rules={appAccessRules} />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@ -4,7 +4,6 @@ import type { App } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -79,7 +78,7 @@ export default function AccessControl(props: AccessControlProps) {
|
||||
<AccessControlItem type={AccessMode.ORGANIZATION}>
|
||||
<div className="flex items-center p-3">
|
||||
<div className="flex grow items-center gap-x-2">
|
||||
<RiBuildingLine className="h-4 w-4 text-text-primary" />
|
||||
<span className="i-ri-building-line h-4 w-4 text-text-primary" />
|
||||
<p className="system-sm-medium text-text-primary">{t('accessControlDialog.accessItems.organization', { ns: 'app' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -90,7 +89,7 @@ export default function AccessControl(props: AccessControlProps) {
|
||||
<AccessControlItem type={AccessMode.EXTERNAL_MEMBERS}>
|
||||
<div className="flex items-center p-3">
|
||||
<div className="flex grow items-center gap-x-2">
|
||||
<RiVerifiedBadgeLine className="h-4 w-4 text-text-primary" />
|
||||
<span className="i-ri-verified-badge-line h-4 w-4 text-text-primary" />
|
||||
<p className="system-sm-medium text-text-primary">{t('accessControlDialog.accessItems.external', { ns: 'app' })}</p>
|
||||
</div>
|
||||
{!hideTip && <WebAppSSONotEnabledTip />}
|
||||
@ -98,7 +97,7 @@ export default function AccessControl(props: AccessControlProps) {
|
||||
</AccessControlItem>
|
||||
<AccessControlItem type={AccessMode.PUBLIC}>
|
||||
<div className="flex items-center gap-x-2 p-3">
|
||||
<RiGlobalLine className="h-4 w-4 text-text-primary" />
|
||||
<span className="i-ri-global-line h-4 w-4 text-text-primary" />
|
||||
<p className="system-sm-medium text-text-primary">{t('accessControlDialog.accessItems.anyone', { ns: 'app' })}</p>
|
||||
</div>
|
||||
</AccessControlItem>
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import type { App } from '@/types/app'
|
||||
import AccessConfigModal from '@/app/components/access-config-modal'
|
||||
|
||||
// TODO: replace with the per-app access rules fetched from the access-rules API
|
||||
// once available. The catalog mirrors the workspace-level App access rules and
|
||||
// adds app-specific rules that can only be assigned per-app.
|
||||
const DEFAULT_APP_ACCESS_RULES: AccessRule[] = [
|
||||
{
|
||||
id: 'app-full-access',
|
||||
name: 'Full access',
|
||||
description: 'Highest level. Can edit, publish, delete apps, and manage access for this app.',
|
||||
assignedRoles: [
|
||||
{ id: 'owner', name: 'Owner' },
|
||||
{ id: 'admin', name: 'Admin' },
|
||||
{ id: 'marketing-lead', name: 'Marketing Lead' },
|
||||
{ id: 'kb-admin', name: 'KB Admin' },
|
||||
{ id: 'app-admin', name: 'App Admin' },
|
||||
{ id: 'executive', name: 'Executive' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'app-can-edit',
|
||||
name: 'Can edit',
|
||||
description: 'Modify Prompts, adjust workflows, change variables. Test and publish updates.',
|
||||
assignedRoles: [
|
||||
{ id: 'owner', name: 'Owner' },
|
||||
{ id: 'admin', name: 'Admin' },
|
||||
{ id: 'marketing-lead', name: 'Marketing Lead' },
|
||||
{ id: 'kb-admin', name: 'KB Admin' },
|
||||
{ id: 'app-admin', name: 'App Admin' },
|
||||
{ id: 'executive', name: 'Executive' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'app-can-view-and-use',
|
||||
name: 'Can view & use',
|
||||
description: 'View and use the app. Access Prompt and workflow logs. Cannot modify.',
|
||||
assignedRoles: [
|
||||
{ id: 'owner', name: 'Owner' },
|
||||
{ id: 'admin', name: 'Admin' },
|
||||
{ id: 'marketing-lead', name: 'Marketing Lead' },
|
||||
{ id: 'kb-admin', name: 'KB Admin' },
|
||||
{ id: 'app-admin', name: 'App Admin' },
|
||||
{ id: 'executive', name: 'Executive' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'app-can-preview',
|
||||
name: 'Can preview',
|
||||
description: 'View the app in the list only. Cannot open the editor or use the app.',
|
||||
assignedRoles: [
|
||||
{ id: 'owner', name: 'Owner' },
|
||||
{ id: 'admin', name: 'Admin' },
|
||||
{ id: 'marketing-lead', name: 'Marketing Lead' },
|
||||
{ id: 'kb-admin', name: 'KB Admin' },
|
||||
{ id: 'app-admin', name: 'App Admin' },
|
||||
{ id: 'executive', name: 'Executive' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'app-can-optimize-prompt',
|
||||
name: 'Can optimize prompt',
|
||||
description: 'Dedicated prompt optimization access.',
|
||||
assignedRoles: [
|
||||
{ id: 'owner', name: 'Owner' },
|
||||
{ id: 'admin', name: 'Admin' },
|
||||
{ id: 'marketing-lead', name: 'Marketing Lead' },
|
||||
{ id: 'kb-admin', name: 'KB Admin' },
|
||||
{ id: 'app-admin', name: 'App Admin' },
|
||||
{ id: 'executive', name: 'Executive' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
]
|
||||
|
||||
export type AppAccessConfigModalProps = {
|
||||
open: boolean
|
||||
app: Pick<App, 'id' | 'name'>
|
||||
onClose: () => void
|
||||
onSave?: (rules: AccessRule[]) => void
|
||||
}
|
||||
|
||||
const AppAccessConfigModal = ({
|
||||
open,
|
||||
app: _app,
|
||||
onClose,
|
||||
onSave,
|
||||
}: AppAccessConfigModalProps) => {
|
||||
return (
|
||||
<AccessConfigModal
|
||||
open={open}
|
||||
title="App Access Config"
|
||||
description="Configure access levels for this specific app."
|
||||
initialRules={DEFAULT_APP_ACCESS_RULES}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppAccessConfigModal
|
||||
@ -71,9 +71,6 @@ const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/ds
|
||||
const AccessControl = dynamic(() => import('@/app/components/app/app-access-control'), {
|
||||
ssr: false,
|
||||
})
|
||||
const AppAccessConfigModal = dynamic(() => import('@/app/components/apps/app-access-config-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
type AppCardProps = {
|
||||
app: App
|
||||
@ -93,7 +90,6 @@ type AppCardOperationsMenuProps = {
|
||||
onSwitch: () => void
|
||||
onDelete: () => void
|
||||
onAccessControl: () => void
|
||||
onAccessConfig: () => void
|
||||
}
|
||||
|
||||
const AppCardOperationsMenu: React.FC<AppCardOperationsMenuProps> = ({
|
||||
@ -107,10 +103,10 @@ const AppCardOperationsMenu: React.FC<AppCardOperationsMenuProps> = ({
|
||||
onSwitch,
|
||||
onDelete,
|
||||
onAccessControl,
|
||||
onAccessConfig,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const openAsyncWindow = useAsyncWindowOpen()
|
||||
const { push } = useRouter()
|
||||
|
||||
const handleMenuAction = useCallback((e: React.MouseEvent<HTMLElement>, action: () => void) => {
|
||||
e.stopPropagation()
|
||||
@ -139,6 +135,11 @@ const AppCardOperationsMenu: React.FC<AppCardOperationsMenuProps> = ({
|
||||
}
|
||||
}, [app.id, openAsyncWindow])
|
||||
|
||||
const handleOpenAccessConfig = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation()
|
||||
push(`/app/${app.id}/access-config`)
|
||||
}, [app.id, push])
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={e => handleMenuAction(e, onEdit)}>
|
||||
@ -176,7 +177,7 @@ const AppCardOperationsMenu: React.FC<AppCardOperationsMenuProps> = ({
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={e => handleMenuAction(e, onAccessConfig)}>
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={handleOpenAccessConfig}>
|
||||
<span className="text-sm leading-5 text-text-secondary">Access Config</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@ -230,7 +231,6 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [confirmDeleteInput, setConfirmDeleteInput] = useState('')
|
||||
const [showAccessControl, setShowAccessControl] = useState(false)
|
||||
const [showAccessConfig, setShowAccessConfig] = useState(false)
|
||||
const [isOperationsMenuOpen, setIsOperationsMenuOpen] = useState(false)
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation()
|
||||
@ -303,13 +303,6 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleShowAccessConfig = useCallback(() => {
|
||||
setIsOperationsMenuOpen(false)
|
||||
queueMicrotask(() => {
|
||||
setShowAccessConfig(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
|
||||
name,
|
||||
icon_type,
|
||||
@ -698,13 +691,6 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
|
||||
{showAccessControl && (
|
||||
<AccessControl app={app} onConfirm={onUpdateAccessControl} onClose={() => setShowAccessControl(false)} />
|
||||
)}
|
||||
{showAccessConfig && (
|
||||
<AppAccessConfigModal
|
||||
open
|
||||
app={app}
|
||||
onClose={() => setShowAccessConfig(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,70 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import AccessRulesEditor from '@/app/components/access-rules-editor'
|
||||
|
||||
// TODO: replace with the per-knowledge-base access rules fetched from the
|
||||
// access-rules API once available. Mirrors the workspace-level Knowledge Base
|
||||
// access rules catalog.
|
||||
const DEFAULT_KB_ACCESS_RULES: AccessRule[] = [
|
||||
{
|
||||
id: 'kb-full-access',
|
||||
name: 'Full access',
|
||||
description: 'Highest level. Can edit, publish, delete, and manage access for this knowledge base.',
|
||||
assignedRoles: [
|
||||
{ id: 'owner', name: 'Owner' },
|
||||
{ id: 'admin', name: 'Admin' },
|
||||
{ id: 'kb-admin', name: 'KB Admin' },
|
||||
{ id: 'executive', name: 'Executive' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'kb-can-edit',
|
||||
name: 'Can edit',
|
||||
description: 'Edit knowledge base content, modify settings, and run tests.',
|
||||
assignedRoles: [
|
||||
{ id: 'kb-editor', name: 'KB Editor' },
|
||||
{ id: 'ops-staff', name: 'Ops Staff' },
|
||||
{ id: 'it-staff', name: 'IT Staff' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'kb-can-view-and-use',
|
||||
name: 'Can view & use',
|
||||
description: 'View knowledge base sources, configs, and logs. Cannot modify content.',
|
||||
assignedRoles: [
|
||||
{ id: 'member', name: 'Member' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'kb-can-preview',
|
||||
name: 'Can preview',
|
||||
description: 'View in the list only. Cannot access the detail page.',
|
||||
assignedRoles: [
|
||||
{ id: 'partner', name: 'Partner' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'kb-can-test',
|
||||
name: 'Can test',
|
||||
description: 'Test knowledge base retrieval efficiency in sandbox.',
|
||||
assignedRoles: [
|
||||
{ id: 'tester', name: 'Tester' },
|
||||
],
|
||||
permissions: [],
|
||||
},
|
||||
]
|
||||
import { useDatasetAccessRules } from '@/service/access-control/use-dataset-access-config'
|
||||
|
||||
type DatasetAccessConfigPageProps = {
|
||||
datasetId: string
|
||||
}
|
||||
|
||||
const DatasetAccessConfigPage = ({ datasetId: _datasetId }: DatasetAccessConfigPageProps) => {
|
||||
const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) => {
|
||||
const { data: datasetAccessRulesResponse } = useDatasetAccessRules(datasetId)
|
||||
|
||||
const datasetAccessRules = datasetAccessRulesResponse?.items || []
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
className="h-full bg-components-panel-bg"
|
||||
@ -73,7 +21,7 @@ const DatasetAccessConfigPage = ({ datasetId: _datasetId }: DatasetAccessConfigP
|
||||
<div className="px-12 py-8">
|
||||
<h1 className="title-2xl-semi-bold text-text-primary">Access Config</h1>
|
||||
<div className="mt-6">
|
||||
<AccessRulesEditor rules={DEFAULT_KB_ACCESS_RULES} />
|
||||
<AccessRulesEditor rules={datasetAccessRules} />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@ -1,13 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import type { AccessPolicyWithBindings, BindingType } from '@/models/access-control'
|
||||
import type { AccessPolicyWithBindings, BindingType, RemoveBindingPayload } from '@/models/access-control'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { memo, useCallback } from 'react'
|
||||
import {
|
||||
useUpdateAppAccessRuleBindings,
|
||||
useUpdateDatasetAccessRuleBindings,
|
||||
} from '@/service/access-control/use-workspace-access-rules'
|
||||
import AccessRuleRowMenu from './access-rule-row-menu'
|
||||
import RoleTag from './role-tag'
|
||||
|
||||
@ -17,6 +12,7 @@ export type AccessRuleRowProps = {
|
||||
showMenu?: boolean
|
||||
onEdit?: (rule: AccessPolicyWithBindings) => void
|
||||
onAddRole?: (rule: AccessPolicyWithBindings) => void
|
||||
onRemove?: (payload: RemoveBindingPayload) => void
|
||||
}
|
||||
|
||||
const AccessRuleRow = ({
|
||||
@ -25,6 +21,7 @@ const AccessRuleRow = ({
|
||||
showMenu = true,
|
||||
onEdit,
|
||||
onAddRole,
|
||||
onRemove,
|
||||
}: AccessRuleRowProps) => {
|
||||
const { policy, roles, accounts } = rule
|
||||
const { id: policyId, resource_type } = policy
|
||||
@ -32,12 +29,13 @@ const AccessRuleRow = ({
|
||||
const handleEdit = useCallback(() => onEdit?.(rule), [onEdit, rule])
|
||||
const handleAddRole = useCallback(() => onAddRole?.(rule), [onAddRole, rule])
|
||||
|
||||
const { mutateAsync: updateAppAccessRuleBindings } = useUpdateAppAccessRuleBindings()
|
||||
const { mutateAsync: updateDatasetAccessRuleBindings } = useUpdateDatasetAccessRuleBindings()
|
||||
const handleRemove = useCallback((id: string, type: BindingType) => {
|
||||
if (!onRemove)
|
||||
return
|
||||
|
||||
const handleRemoveRole = useCallback((id: string, type: BindingType) => {
|
||||
const payload = {
|
||||
id: policyId,
|
||||
const payload: RemoveBindingPayload = {
|
||||
policy_id: policyId,
|
||||
resource_type,
|
||||
role_ids: roles.map(role => role.role_id),
|
||||
account_ids: accounts.map(account => account.account_id),
|
||||
}
|
||||
@ -47,21 +45,8 @@ const AccessRuleRow = ({
|
||||
else if (type === 'account') {
|
||||
payload.account_ids = payload.account_ids.filter(accountId => accountId !== id)
|
||||
}
|
||||
if (resource_type === 'app') {
|
||||
updateAppAccessRuleBindings(payload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Access rule updated successfully')
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (resource_type === 'dataset') {
|
||||
updateDatasetAccessRuleBindings(payload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Access rule updated successfully')
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [accounts, policyId, resource_type, roles, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings])
|
||||
onRemove(payload)
|
||||
}, [accounts, onRemove, policyId, resource_type, roles])
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-start gap-2 py-3.5', className)}>
|
||||
@ -79,7 +64,7 @@ const AccessRuleRow = ({
|
||||
id={role.role_id}
|
||||
label={role.role_name}
|
||||
type="role"
|
||||
onRemove={handleRemoveRole}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
))}
|
||||
{accounts.map(account => (
|
||||
@ -88,7 +73,7 @@ const AccessRuleRow = ({
|
||||
id={account.account_id}
|
||||
label={account.account_name}
|
||||
type="account"
|
||||
onRemove={handleRemoveRole}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import type { AccessPolicyWithBindings } from '@/models/access-control'
|
||||
import type { AccessPolicyWithBindings, RemoveBindingPayload } from '@/models/access-control'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { memo } from 'react'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { memo, useCallback } from 'react'
|
||||
import {
|
||||
useUpdateAppAccessRuleBindings,
|
||||
useUpdateDatasetAccessRuleBindings,
|
||||
} from '@/service/access-control/use-workspace-access-rules'
|
||||
import AccessRuleRow from './access-rule-row'
|
||||
|
||||
type AccessRuleSectionProps = {
|
||||
@ -27,6 +32,32 @@ const AccessRuleSection = ({
|
||||
onAddRole,
|
||||
className,
|
||||
}: AccessRuleSectionProps) => {
|
||||
const { mutateAsync: updateAppAccessRuleBindings } = useUpdateAppAccessRuleBindings()
|
||||
const { mutateAsync: updateDatasetAccessRuleBindings } = useUpdateDatasetAccessRuleBindings()
|
||||
|
||||
const handleRemoveRole = useCallback((payload: RemoveBindingPayload) => {
|
||||
const { policy_id, resource_type, role_ids, account_ids } = payload
|
||||
const updatePayload = {
|
||||
id: policy_id,
|
||||
role_ids,
|
||||
account_ids,
|
||||
}
|
||||
if (resource_type === 'app') {
|
||||
updateAppAccessRuleBindings(updatePayload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Access rule updated successfully')
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (resource_type === 'dataset') {
|
||||
updateDatasetAccessRuleBindings(updatePayload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Access rule updated successfully')
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [updateAppAccessRuleBindings, updateDatasetAccessRuleBindings])
|
||||
|
||||
return (
|
||||
<section className={cn('flex flex-col', className)}>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
@ -50,6 +81,7 @@ const AccessRuleSection = ({
|
||||
className={cn(index > 0 && 'border-t border-divider-subtle')}
|
||||
onEdit={onEditRule}
|
||||
onAddRole={onAddRole}
|
||||
onRemove={handleRemoveRole}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { AccessPolicyWithBindings } from '@/models/access-control'
|
||||
import { useWorkspaceAppAccessRules } from '@/service/access-control/use-workspace-access-rules'
|
||||
import {
|
||||
useWorkspaceAppAccessRules,
|
||||
} from '@/service/access-control/use-workspace-access-rules'
|
||||
import AccessRuleSection from './access-rule-section'
|
||||
|
||||
type AppAccessRuleSectionProps = {
|
||||
|
||||
@ -179,3 +179,8 @@ export type UpdateRolesOfMemberRequest = {
|
||||
member_id: string
|
||||
role_ids: string[]
|
||||
}
|
||||
|
||||
export type RemoveBindingPayload = {
|
||||
policy_id: string
|
||||
resource_type: AccessPolicyResourceType
|
||||
} & BindingsPayload
|
||||
|
||||
29
web/service/access-control/use-app-access-config.ts
Normal file
29
web/service/access-control/use-app-access-config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { BindingsPayload, GetAppAccessPolicyByAppIdResponse } from '@/models/access-control'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { get, put } from '../base'
|
||||
|
||||
const NAME_SPACE = 'app-access-config'
|
||||
|
||||
export const useAppAccessRules = (appId: string) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'app-access-rules', appId],
|
||||
queryFn: () => get<GetAppAccessPolicyByAppIdResponse>(`/workspaces/current/rbac/apps/${appId}/access-policy`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateAppAccessRuleBindings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'update-app-access-rule-bindings'],
|
||||
mutationFn: (data: { appId: string, policyId: string } & BindingsPayload) => {
|
||||
const { appId, policyId, ...payload } = data
|
||||
return put(`/workspaces/current/rbac/apps/${appId}/access-policies/${policyId}/bindings`, {
|
||||
body: payload,
|
||||
})
|
||||
},
|
||||
onSuccess: (_, { appId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'app-access-rules', appId] })
|
||||
},
|
||||
})
|
||||
}
|
||||
29
web/service/access-control/use-dataset-access-config.ts
Normal file
29
web/service/access-control/use-dataset-access-config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { BindingsPayload, GetAppAccessPolicyByAppIdResponse } from '@/models/access-control'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { get, put } from '../base'
|
||||
|
||||
const NAME_SPACE = 'dataset-access-config'
|
||||
|
||||
export const useDatasetAccessRules = (datasetId: string) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'dataset-access-rules', datasetId],
|
||||
queryFn: () => get<GetAppAccessPolicyByAppIdResponse>(`/workspaces/current/rbac/datasets/${datasetId}/access-policy`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateDatasetAccessRuleBindings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'update-dataset-access-rule-bindings'],
|
||||
mutationFn: (data: { datasetId: string, policyId: string } & BindingsPayload) => {
|
||||
const { datasetId, policyId, ...payload } = data
|
||||
return put(`/workspaces/current/rbac/datasets/${datasetId}/access-policies/${policyId}/bindings`, {
|
||||
body: payload,
|
||||
})
|
||||
},
|
||||
onSuccess: (_, { datasetId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'dataset-access-rules', datasetId] })
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user