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:
twwu 2026-05-12 16:56:55 +08:00
parent 7aa8f1a0b6
commit cbedd2b852
13 changed files with 213 additions and 447 deletions

View File

@ -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

View File

@ -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}
/>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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)}
/>
)}
</>
)
}

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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 = {

View File

@ -179,3 +179,8 @@ export type UpdateRolesOfMemberRequest = {
member_id: string
role_ids: string[]
}
export type RemoveBindingPayload = {
policy_id: string
resource_type: AccessPolicyResourceType
} & BindingsPayload

View 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] })
},
})
}

View 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] })
},
})
}