mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
refactor(web): remove permission catalog service hooks (#39342)
This commit is contained in:
parent
aa718add24
commit
7da6b2ce36
@ -6336,11 +6336,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/access-control/use-permission-catalog.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/access-control/use-workspace-access-rules.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import type { PermissionGroup } from '@/models/access-control'
|
||||
import type { PermissionCatalogResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { createTestQueryClient } from '@/test/query-client'
|
||||
import PermissionSetModal from '../index'
|
||||
|
||||
const expectedAppACLPermissionKeys = [
|
||||
@ -19,59 +23,71 @@ const expectedAppACLPermissionKeys = [
|
||||
const getPermissionKeyMatcher = (permissionKey: string) =>
|
||||
new RegExp(permissionKey.replaceAll('.', '\\.'))
|
||||
|
||||
const mockCatalogs = vi.hoisted(() => ({
|
||||
app: {
|
||||
groups: [] as PermissionGroup[],
|
||||
},
|
||||
dataset: {
|
||||
groups: [] as PermissionGroup[],
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/access-control/use-permission-catalog', () => ({
|
||||
useAppPermissionCatalog: () => ({
|
||||
data: { groups: mockCatalogs.app.groups },
|
||||
}),
|
||||
useDatasetPermissionCatalog: () => ({
|
||||
data: { groups: mockCatalogs.dataset.groups },
|
||||
}),
|
||||
}))
|
||||
|
||||
const createPermissionGroup = (overrides: Partial<PermissionGroup> = {}): PermissionGroup => ({
|
||||
group_key: 'app_management',
|
||||
group_name: 'App management',
|
||||
description: '',
|
||||
permissions: expectedAppACLPermissionKeys.map((permissionKey) => ({
|
||||
key: permissionKey,
|
||||
name: permissionKey,
|
||||
description: '',
|
||||
})),
|
||||
...overrides,
|
||||
vi.mock('react-i18next', async () => {
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
return createReactI18nextMock({
|
||||
'permission.group.app_acl': 'Translated app permissions',
|
||||
'permission.group.dataset_acl': 'Translated dataset permissions',
|
||||
})
|
||||
})
|
||||
|
||||
const appPermissionCatalog = {
|
||||
groups: [
|
||||
{
|
||||
group_key: 'app_management',
|
||||
group_name: 'App management',
|
||||
description: '',
|
||||
permissions: expectedAppACLPermissionKeys.map((permissionKey) => ({
|
||||
key: permissionKey,
|
||||
name: permissionKey,
|
||||
description: '',
|
||||
})),
|
||||
},
|
||||
],
|
||||
} satisfies PermissionCatalogResponse
|
||||
|
||||
const datasetPermissionCatalog = {
|
||||
groups: [
|
||||
{
|
||||
group_key: 'dataset_management',
|
||||
group_name: 'Dataset management',
|
||||
description: '',
|
||||
permissions: [
|
||||
{
|
||||
key: 'dataset.acl.edit',
|
||||
name: 'Edit dataset',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} satisfies PermissionCatalogResponse
|
||||
|
||||
const renderModal = (modal: ReactNode) => {
|
||||
const queryClient = createTestQueryClient()
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.workspaces.current.rbac.rolePermissions.catalog.app.get.queryKey({ input: {} }),
|
||||
appPermissionCatalog,
|
||||
)
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.workspaces.current.rbac.rolePermissions.catalog.dataset.get.queryKey({
|
||||
input: {},
|
||||
}),
|
||||
datasetPermissionCatalog,
|
||||
)
|
||||
|
||||
return render(<QueryClientProvider client={queryClient}>{modal}</QueryClientProvider>)
|
||||
}
|
||||
|
||||
describe('PermissionSetModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCatalogs.app.groups = [createPermissionGroup()]
|
||||
mockCatalogs.dataset.groups = [
|
||||
createPermissionGroup({
|
||||
group_key: 'dataset_management',
|
||||
group_name: 'Dataset management',
|
||||
permissions: [
|
||||
{
|
||||
key: 'dataset.acl.edit',
|
||||
name: 'Edit dataset',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
// Rendering keeps the form fields and permission picker available inside the modal.
|
||||
describe('Rendering', () => {
|
||||
it('should render create mode with app permission catalog', () => {
|
||||
render(
|
||||
renderModal(
|
||||
<PermissionSetModal
|
||||
open
|
||||
mode="create"
|
||||
@ -86,7 +102,7 @@ describe('PermissionSetModal', () => {
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/permission\.permissionSet\.nameLabel/)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('permission.permissionSet.descriptionLabel')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /App management/ })).toHaveAttribute(
|
||||
expect(screen.getByRole('button', { name: /Translated app permissions/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true',
|
||||
)
|
||||
@ -94,7 +110,7 @@ describe('PermissionSetModal', () => {
|
||||
})
|
||||
|
||||
it('should render the complete app ACL permission catalog', async () => {
|
||||
render(
|
||||
renderModal(
|
||||
<PermissionSetModal
|
||||
open
|
||||
mode="create"
|
||||
@ -109,7 +125,7 @@ describe('PermissionSetModal', () => {
|
||||
})
|
||||
|
||||
it('should render dataset permission catalog when resource type is dataset', () => {
|
||||
render(
|
||||
renderModal(
|
||||
<PermissionSetModal
|
||||
open
|
||||
mode="create"
|
||||
@ -122,7 +138,9 @@ describe('PermissionSetModal', () => {
|
||||
expect(
|
||||
screen.getByText('permission.permissionSet.modal.create.dataset.title'),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /Dataset management/ })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Translated dataset permissions/ }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText(/dataset\.acl\.edit/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -134,7 +152,7 @@ describe('PermissionSetModal', () => {
|
||||
const handleClose = vi.fn()
|
||||
const handleSubmit = vi.fn()
|
||||
|
||||
render(
|
||||
renderModal(
|
||||
<PermissionSetModal
|
||||
open
|
||||
mode="create"
|
||||
@ -168,7 +186,7 @@ describe('PermissionSetModal', () => {
|
||||
const user = userEvent.setup()
|
||||
const handleSubmit = vi.fn()
|
||||
|
||||
render(
|
||||
renderModal(
|
||||
<PermissionSetModal
|
||||
open
|
||||
mode="edit"
|
||||
@ -200,7 +218,7 @@ describe('PermissionSetModal', () => {
|
||||
// View mode is read-only and uses close-only footer actions.
|
||||
describe('Read-only Mode', () => {
|
||||
it('should disable editing and hide confirm action in view mode', () => {
|
||||
render(
|
||||
renderModal(
|
||||
<PermissionSetModal
|
||||
open
|
||||
mode="view"
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import type { SelectorKey } from 'i18next'
|
||||
import type { AccessPolicyResourceType } from '@/models/access-control'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useAppPermissionCatalog,
|
||||
useDatasetPermissionCatalog,
|
||||
} from '@/service/access-control/use-permission-catalog'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
export const usePermissionsGroups = (resourceType: AccessPolicyResourceType) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: appPermissionCatalog } = useAppPermissionCatalog(resourceType === 'app')
|
||||
const { data: datasetPermissionCatalog } = useDatasetPermissionCatalog(resourceType === 'dataset')
|
||||
|
||||
const permissionCatalog = resourceType === 'app' ? appPermissionCatalog : datasetPermissionCatalog
|
||||
const permissionCatalogQueryOptions =
|
||||
resourceType === 'app'
|
||||
? consoleQuery.workspaces.current.rbac.rolePermissions.catalog.app.get.queryOptions({
|
||||
input: {},
|
||||
})
|
||||
: consoleQuery.workspaces.current.rbac.rolePermissions.catalog.dataset.get.queryOptions({
|
||||
input: {},
|
||||
})
|
||||
const { data: permissionCatalog } = useQuery(permissionCatalogQueryOptions)
|
||||
|
||||
const groups = useMemo(() => {
|
||||
// Permission keys come from the catalog API, so this is a reviewed open-key boundary with a server-provided fallback.
|
||||
@ -22,20 +25,20 @@ export const usePermissionsGroups = (resourceType: AccessPolicyResourceType) =>
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
return (permissionCatalog?.groups || []).map((group) => ({
|
||||
return (permissionCatalog?.groups ?? []).map((group) => ({
|
||||
...group,
|
||||
group_name: t(($) => $[`group.${resourceType}_acl`], {
|
||||
ns: 'permission',
|
||||
defaultValue: group.group_name,
|
||||
}),
|
||||
permissions: group.permissions.map((permission) => ({
|
||||
permissions: (group.permissions ?? []).map((permission) => ({
|
||||
...permission,
|
||||
name: translatePermissionName(permission.key, permission.name),
|
||||
})),
|
||||
}))
|
||||
}, [permissionCatalog?.groups, resourceType, t])
|
||||
|
||||
const allPermissions = groups.flatMap((g) => g.permissions) || []
|
||||
const allPermissions = groups.flatMap((g) => g.permissions)
|
||||
|
||||
const permissionMap = Object.fromEntries(allPermissions.map((p) => [p.key, p]))
|
||||
|
||||
|
||||
@ -1,31 +1,47 @@
|
||||
import type { PermissionGroup, Role } from '@/models/access-control'
|
||||
import type { PermissionCatalogResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Role } from '@/models/access-control'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { createTestQueryClient } from '@/test/query-client'
|
||||
import RoleModal from '../index'
|
||||
|
||||
const mockWorkspacePermissionCatalog = vi.hoisted(() => ({
|
||||
groups: [] as PermissionGroup[],
|
||||
}))
|
||||
vi.mock('react-i18next', async () => {
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
return createReactI18nextMock({
|
||||
'permission.group.workspace_management': 'Translated workspace permissions',
|
||||
'permissionKeys.workspace.member.manage': 'Manage workspace members',
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock('@/service/access-control/use-permission-catalog', () => ({
|
||||
useWorkspacePermissionCatalog: () => ({
|
||||
data: { groups: mockWorkspacePermissionCatalog.groups },
|
||||
}),
|
||||
}))
|
||||
|
||||
const createPermissionGroup = (overrides: Partial<PermissionGroup> = {}): PermissionGroup => ({
|
||||
group_key: 'workspace_management',
|
||||
group_name: 'Workspace management',
|
||||
description: '',
|
||||
permissions: [
|
||||
const workspacePermissionCatalog = {
|
||||
groups: [
|
||||
{
|
||||
key: 'workspace.member.manage',
|
||||
name: 'Manage members',
|
||||
group_key: 'workspace_management',
|
||||
group_name: 'Workspace management',
|
||||
description: '',
|
||||
permissions: [
|
||||
{
|
||||
key: 'workspace.member.manage',
|
||||
name: 'Manage members',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
})
|
||||
} satisfies PermissionCatalogResponse
|
||||
|
||||
const renderModal = (modal: ReactNode) => {
|
||||
const queryClient = createTestQueryClient()
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.workspaces.current.rbac.rolePermissions.catalog.get.queryKey({ input: {} }),
|
||||
workspacePermissionCatalog,
|
||||
)
|
||||
|
||||
return render(<QueryClientProvider client={queryClient}>{modal}</QueryClientProvider>)
|
||||
}
|
||||
|
||||
const createRole = (overrides: Partial<Role> = {}): Role => ({
|
||||
id: 'role-1',
|
||||
@ -43,13 +59,12 @@ const createRole = (overrides: Partial<Role> = {}): Role => ({
|
||||
describe('RoleModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkspacePermissionCatalog.groups = [createPermissionGroup()]
|
||||
})
|
||||
|
||||
// Rendering keeps role fields and workspace permissions in one modal form.
|
||||
describe('Rendering', () => {
|
||||
it('should render edit mode with role values and selected permissions', () => {
|
||||
render(
|
||||
renderModal(
|
||||
<RoleModal open mode="edit" role={createRole()} onClose={vi.fn()} onSubmit={vi.fn()} />,
|
||||
)
|
||||
|
||||
@ -58,15 +73,14 @@ describe('RoleModal', () => {
|
||||
expect(screen.getByLabelText('permission.role.modal.descriptionLabel')).toHaveValue(
|
||||
'Can operate workspace',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: /Workspace management/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByText(/workspace\.member\.manage/)).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Translated workspace permissions/ }),
|
||||
).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByText('Manage workspace members')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable confirm action when role name is empty', () => {
|
||||
render(<RoleModal open mode="create" onClose={vi.fn()} onSubmit={vi.fn()} />)
|
||||
renderModal(<RoleModal open mode="create" onClose={vi.fn()} onSubmit={vi.fn()} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeDisabled()
|
||||
})
|
||||
@ -79,14 +93,14 @@ describe('RoleModal', () => {
|
||||
const handleClose = vi.fn()
|
||||
const handleSubmit = vi.fn()
|
||||
|
||||
render(<RoleModal open mode="create" onClose={handleClose} onSubmit={handleSubmit} />)
|
||||
renderModal(<RoleModal open mode="create" onClose={handleClose} onSubmit={handleSubmit} />)
|
||||
|
||||
await user.type(screen.getByLabelText('permission.role.modal.nameLabel'), ' Support role ')
|
||||
await user.type(
|
||||
screen.getByLabelText('permission.role.modal.descriptionLabel'),
|
||||
' Helps members ',
|
||||
)
|
||||
await user.click(screen.getByText(/workspace\.member\.manage/))
|
||||
await user.click(screen.getByText('Manage workspace members'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalledTimes(1)
|
||||
@ -102,7 +116,7 @@ describe('RoleModal', () => {
|
||||
const user = userEvent.setup()
|
||||
const handleSubmit = vi.fn()
|
||||
|
||||
render(
|
||||
renderModal(
|
||||
<RoleModal
|
||||
open
|
||||
mode="edit"
|
||||
@ -132,7 +146,7 @@ describe('RoleModal', () => {
|
||||
// View mode preserves the permission display but blocks edits and confirmation.
|
||||
describe('Read-only Mode', () => {
|
||||
it('should render role details as read-only in view mode', () => {
|
||||
render(
|
||||
renderModal(
|
||||
<RoleModal open mode="view" role={createRole()} onClose={vi.fn()} onSubmit={vi.fn()} />,
|
||||
)
|
||||
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import type { SelectorKey } from 'i18next'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWorkspacePermissionCatalog } from '@/service/access-control/use-permission-catalog'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
export const useWorkspacePermissionGroups = () => {
|
||||
const { t } = useTranslation()
|
||||
const { data: workspacePermissionCatalog } = useWorkspacePermissionCatalog()
|
||||
const { data: workspacePermissionCatalog } = useQuery(
|
||||
consoleQuery.workspaces.current.rbac.rolePermissions.catalog.get.queryOptions({
|
||||
input: {},
|
||||
}),
|
||||
)
|
||||
|
||||
const groups = useMemo(() => {
|
||||
// Permission keys come from the catalog API, so these are reviewed open-key boundaries with server-provided fallbacks.
|
||||
@ -20,17 +25,17 @@ export const useWorkspacePermissionGroups = () => {
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
return (workspacePermissionCatalog?.groups || []).map((group) => ({
|
||||
return (workspacePermissionCatalog?.groups ?? []).map((group) => ({
|
||||
...group,
|
||||
group_name: translatePermissionGroupName(group.group_key, group.group_name),
|
||||
permissions: group.permissions.map((permission) => ({
|
||||
permissions: (group.permissions ?? []).map((permission) => ({
|
||||
...permission,
|
||||
name: translatePermissionName(permission.key, permission.name),
|
||||
})),
|
||||
}))
|
||||
}, [t, workspacePermissionCatalog?.groups])
|
||||
|
||||
const allPermissions = groups.flatMap((g) => g.permissions) || []
|
||||
const allPermissions = groups.flatMap((g) => g.permissions)
|
||||
|
||||
const permissionMap = Object.fromEntries(allPermissions.map((p) => [p.key, p]))
|
||||
|
||||
|
||||
@ -61,10 +61,6 @@ export type PermissionGroup = {
|
||||
permissions: Permission[]
|
||||
}
|
||||
|
||||
export type PermissionGroups = {
|
||||
groups: PermissionGroup[]
|
||||
}
|
||||
|
||||
export type PermissionKey = string
|
||||
|
||||
type RoleType = 'workspace' | 'app' | 'dataset'
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import type { PermissionGroups } from '@/models/access-control'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { get } from '../base'
|
||||
|
||||
const NAME_SPACE = 'rbac-permission-catalog'
|
||||
|
||||
export const useWorkspacePermissionCatalog = () => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'workspace'],
|
||||
queryFn: () => get<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog'),
|
||||
})
|
||||
}
|
||||
|
||||
export const useAppPermissionCatalog = (enabled?: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'app'],
|
||||
queryFn: () => get<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog/app'),
|
||||
enabled: enabled ?? true,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDatasetPermissionCatalog = (enabled?: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'dataset'],
|
||||
queryFn: () =>
|
||||
get<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog/dataset'),
|
||||
enabled: enabled ?? true,
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user