mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix(web): block resource access for Agent apps (#41424)
This commit is contained in:
parent
1971450ad8
commit
f7d6cd1afe
@ -48,14 +48,18 @@ const mockUsePathname = mockNavigation.usePathname
|
||||
const mockUseRouter = mockNavigation.useRouter
|
||||
const mockFetchAppDetailDirect = vi.mocked(fetchAppDetailDirect)
|
||||
|
||||
const createAppDetail = (overrides: Partial<App> = {}) =>
|
||||
type AppDetailFixture = App & {
|
||||
bound_agent_id?: string | null
|
||||
}
|
||||
|
||||
const createAppDetail = (overrides: Partial<AppDetailFixture> = {}) =>
|
||||
({
|
||||
id: 'app-1',
|
||||
name: 'Demo App',
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
permission_keys: [AppACLPermission.ViewLayout, AppACLPermission.Monitor],
|
||||
...overrides,
|
||||
}) as App
|
||||
}) as AppDetailFixture
|
||||
|
||||
const waitForAppContent = async () => {
|
||||
await waitFor(() => {
|
||||
@ -424,6 +428,52 @@ describe('AppDetailLayout', () => {
|
||||
expect(useStore.getState().appDetail?.id).toBe('app-1')
|
||||
})
|
||||
|
||||
it('should redirect Agent app access config URLs to the Agent configure page', async () => {
|
||||
mockPathname = '/app/app-1/access-config'
|
||||
mockFetchAppDetailDirect.mockResolvedValue(
|
||||
createAppDetail({
|
||||
mode: AppModeEnum.AGENT,
|
||||
bound_agent_id: 'agent-1',
|
||||
permission_keys: [AppACLPermission.AccessConfig],
|
||||
}),
|
||||
)
|
||||
|
||||
render(
|
||||
<AppDetailLayout appId="app-1">
|
||||
<div>App page content</div>
|
||||
</AppDetailLayout>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/agents/agent-1/configure')
|
||||
})
|
||||
expect(screen.queryByText('App page content')).not.toBeInTheDocument()
|
||||
expect(useStore.getState().appDetail).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should keep Agent app access config content hidden while redirecting cached app data', async () => {
|
||||
mockPathname = '/app/app-1/access-config'
|
||||
useStore.getState().setAppDetail(
|
||||
createAppDetail({
|
||||
mode: AppModeEnum.AGENT,
|
||||
bound_agent_id: 'agent-1',
|
||||
permission_keys: [AppACLPermission.AccessConfig],
|
||||
}),
|
||||
)
|
||||
|
||||
render(
|
||||
<AppDetailLayout appId="app-1">
|
||||
<div>App page content</div>
|
||||
</AppDetailLayout>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('App page content')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/agents/agent-1/configure')
|
||||
})
|
||||
expect(mockFetchAppDetailDirect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should redirect access config pages when RBAC is disabled', async () => {
|
||||
mockIsRbacEnabled = false
|
||||
mockPathname = '/app/app-1/access-config'
|
||||
|
||||
@ -78,6 +78,8 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
appDetail?.id === appId ? appDetail : appDetailRes?.id === appId ? appDetailRes : null
|
||||
const pageTitle = appDetailPageTitle(pathname, t)
|
||||
const appName = routeAppDetail?.id === appId ? routeAppDetail.name : undefined
|
||||
const shouldBlockAgentResourceAccess =
|
||||
routeAppDetail?.mode === AppModeEnum.AGENT && pathname.endsWith('/access-config')
|
||||
|
||||
useDocumentTitle(`${pageTitle} · ${appName || t(($) => $['menus.appDetail'], { ns: 'common' })}`)
|
||||
|
||||
@ -145,7 +147,8 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
(isLogsPath && !appACLCapabilities.canAccessLogAndAnnotation) ||
|
||||
(isAnnotationsPath && !appACLCapabilities.canAccessLogAndAnnotation) ||
|
||||
(isOverviewPath && !appACLCapabilities.canMonitor) ||
|
||||
(isAccessConfigPath && !appACLCapabilities.canAccessConfig) ||
|
||||
(isAccessConfigPath &&
|
||||
(routeAppDetail.mode === AppModeEnum.AGENT || !appACLCapabilities.canAccessConfig)) ||
|
||||
(isDeployPath &&
|
||||
(routeAppDetail.mode !== AppModeEnum.WORKFLOW || !appACLCapabilities.canDeploy))
|
||||
) {
|
||||
@ -194,27 +197,28 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
])
|
||||
|
||||
const isWorkflowPage = pathname.endsWith('/workflow')
|
||||
const content = !appDetail ? (
|
||||
<div className="flex min-w-0 grow items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-0 min-h-0 min-w-0 grow overflow-hidden',
|
||||
!isWorkflowPage && 'pt-1 pr-1 pb-1',
|
||||
)}
|
||||
>
|
||||
const content =
|
||||
!appDetail || shouldBlockAgentResourceAccess ? (
|
||||
<div className="flex min-w-0 grow items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'min-w-0 grow overflow-hidden bg-components-panel-bg',
|
||||
!isWorkflowPage && 'rounded-lg shadow-xs shadow-shadow-shadow-3',
|
||||
'relative flex h-0 min-h-0 min-w-0 grow overflow-hidden',
|
||||
!isWorkflowPage && 'pt-1 pr-1 pb-1',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
className={cn(
|
||||
'min-w-0 grow overflow-hidden bg-components-panel-bg',
|
||||
!isWorkflowPage && 'rounded-lg shadow-xs shadow-shadow-shadow-3',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-background-body">
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { AppACLPermission } from '@/utils/permission'
|
||||
import AppDetailSection from '../app-detail-section'
|
||||
|
||||
@ -237,20 +238,37 @@ describe('AppDetailSection', () => {
|
||||
expect(screen.queryByRole('link', { name: 'common.appMenus.deploy' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render resource access navigation when app access config permission is granted', () => {
|
||||
it.each([AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT])(
|
||||
'should render resource access navigation for %s apps when app access config permission is granted',
|
||||
(mode) => {
|
||||
// Arrange
|
||||
mockAppMode = mode
|
||||
mockAppPermissionKeys = [AppACLPermission.AccessConfig]
|
||||
|
||||
// Act
|
||||
render(<AppDetailSection />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'common.settings.resourceAccess' }),
|
||||
).toHaveAttribute('href', '/app/app-1/access-config')
|
||||
expect(
|
||||
screen.queryByRole('link', { name: 'common.appMenus.overview' }),
|
||||
).not.toBeInTheDocument()
|
||||
},
|
||||
)
|
||||
|
||||
it('should hide resource access navigation for Agent apps', () => {
|
||||
// Arrange
|
||||
mockAppMode = AppModeEnum.AGENT
|
||||
mockAppPermissionKeys = [AppACLPermission.AccessConfig]
|
||||
|
||||
// Act
|
||||
render(<AppDetailSection />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('link', { name: 'common.settings.resourceAccess' })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/app-1/access-config',
|
||||
)
|
||||
expect(
|
||||
screen.queryByRole('link', { name: 'common.appMenus.overview' }),
|
||||
screen.queryByRole('link', { name: 'common.settings.resourceAccess' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -101,6 +101,7 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
|
||||
const supportsAppDeploy = appDetail.mode === AppModeEnum.WORKFLOW
|
||||
const supportsAnnotations =
|
||||
appDetail.mode !== AppModeEnum.WORKFLOW && appDetail.mode !== AppModeEnum.COMPLETION
|
||||
const supportsResourceAccess = appDetail.mode !== AppModeEnum.AGENT
|
||||
const appACLCapabilities = getAppACLCapabilities(appDetail.permission_keys, {
|
||||
currentUserId,
|
||||
resourceMaintainer: appDetail.maintainer,
|
||||
@ -165,7 +166,7 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appACLCapabilities.canAccessConfig
|
||||
...(supportsResourceAccess && appACLCapabilities.canAccessConfig
|
||||
? [
|
||||
{
|
||||
name: t(($) => $['settings.resourceAccess'], { ns: 'common' }),
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
useAppUserAccessSettings,
|
||||
} from '@/service/access-control/use-app-access-config'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { AppACLPermission } from '@/utils/permission'
|
||||
import AppAccessConfigPage from '../index'
|
||||
|
||||
@ -365,6 +366,23 @@ describe('AppAccessConfigPage', () => {
|
||||
expect(useAppUserAccessSettings).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not mount access config data hooks for Agent apps', () => {
|
||||
useStore.setState({
|
||||
appDetail: {
|
||||
id: 'app-1',
|
||||
mode: AppModeEnum.AGENT,
|
||||
maintainer: 'account-1',
|
||||
permission_keys: [AppACLPermission.AccessConfig],
|
||||
} as unknown as NonNullable<ReturnType<typeof useStore.getState>['appDetail']>,
|
||||
})
|
||||
|
||||
render(<AppAccessConfigPage appId="app-1" />)
|
||||
|
||||
expect(screen.queryByTestId('access-rules-editor')).not.toBeInTheDocument()
|
||||
expect(useAppAccessRules).not.toHaveBeenCalled()
|
||||
expect(useAppUserAccessSettings).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow the maintainer with app management workspace permission', () => {
|
||||
mockConsoleState.userProfile = { id: 'account-1' }
|
||||
mockConsoleState.workspacePermissionKeys = ['app.create_and_management']
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
useUpdateAppAutomaticIncludeWorkspaceMembers,
|
||||
useUpdateAppUserAccessSettings,
|
||||
} from '@/service/access-control/use-app-access-config'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getAppACLCapabilities } from '@/utils/permission'
|
||||
|
||||
type AppAccessConfigPageProps = {
|
||||
@ -257,7 +258,13 @@ const AppAccessConfigPage = ({ appId }: AppAccessConfigPageProps) => {
|
||||
],
|
||||
)
|
||||
|
||||
if (!appDetail || appDetail.id !== appId || !appACLCapabilities.canAccessConfig) return null
|
||||
if (
|
||||
!appDetail ||
|
||||
appDetail.id !== appId ||
|
||||
appDetail.mode === AppModeEnum.AGENT ||
|
||||
!appACLCapabilities.canAccessConfig
|
||||
)
|
||||
return null
|
||||
|
||||
return <AppAccessConfigContent appId={appId} maintainerId={appDetail.maintainer} />
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user