mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
fix: validate plugin installation scope (#39669)
This commit is contained in:
+50
-74
@@ -4,125 +4,101 @@ import { InstallationScope } from '@/features/system-features/constants'
|
||||
import { renderHookWithConsoleQuery as renderHook } from '@/test/console/query-data'
|
||||
import { pluginInstallLimit } from '../use-install-plugin-limit'
|
||||
|
||||
type PluginInstallCandidate = Parameters<typeof pluginInstallLimit>[0]
|
||||
type SystemFeatures = Parameters<typeof pluginInstallLimit>[1]
|
||||
|
||||
const basePlugin = {
|
||||
from: 'marketplace' as const,
|
||||
verification: { authorized_category: 'langgenius' },
|
||||
} satisfies PluginInstallCandidate
|
||||
|
||||
function makeSystemFeatures(
|
||||
scope: PluginInstallationScope,
|
||||
restrictToMarketplaceOnly = false,
|
||||
): SystemFeatures {
|
||||
return {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: restrictToMarketplaceOnly,
|
||||
plugin_installation_scope: scope,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('pluginInstallLimit', () => {
|
||||
it('should allow all plugins when scope is ALL', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
const features = makeSystemFeatures(InstallationScope.ALL)
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny all plugins when scope is NONE', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.NONE,
|
||||
},
|
||||
}
|
||||
const features = makeSystemFeatures(InstallationScope.NONE)
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(false)
|
||||
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow langgenius plugins when scope is OFFICIAL_ONLY', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
const features = makeSystemFeatures(InstallationScope.OFFICIAL_ONLY)
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny non-official plugins when scope is OFFICIAL_ONLY', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, verification: { authorized_category: 'community' } }
|
||||
const features = makeSystemFeatures(InstallationScope.OFFICIAL_ONLY)
|
||||
const plugin = {
|
||||
...basePlugin,
|
||||
verification: { authorized_category: 'community' as const },
|
||||
} satisfies PluginInstallCandidate
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
expect(pluginInstallLimit(plugin, features).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow partner plugins when scope is OFFICIAL_AND_PARTNER', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_AND_PARTNER,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, verification: { authorized_category: 'partner' } }
|
||||
const features = makeSystemFeatures(InstallationScope.OFFICIAL_AND_PARTNER)
|
||||
const plugin = {
|
||||
...basePlugin,
|
||||
verification: { authorized_category: 'partner' as const },
|
||||
} satisfies PluginInstallCandidate
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(true)
|
||||
expect(pluginInstallLimit(plugin, features).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny github plugins when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, from: 'github' as const }
|
||||
const features = makeSystemFeatures(InstallationScope.ALL, true)
|
||||
const plugin = { ...basePlugin, from: 'github' as const } satisfies PluginInstallCandidate
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
expect(pluginInstallLimit(plugin, features).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should deny package plugins when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, from: 'package' as const }
|
||||
const features = makeSystemFeatures(InstallationScope.ALL, true)
|
||||
const plugin = { ...basePlugin, from: 'package' as const } satisfies PluginInstallCandidate
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
expect(pluginInstallLimit(plugin, features).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow marketplace plugins even when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
const features = makeSystemFeatures(InstallationScope.ALL, true)
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should default to langgenius when no verification info', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
const plugin = { from: 'marketplace' as const }
|
||||
const features = makeSystemFeatures(InstallationScope.OFFICIAL_ONLY)
|
||||
const plugin = { from: 'marketplace' as const } satisfies PluginInstallCandidate
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(true)
|
||||
expect(pluginInstallLimit(plugin, features).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should fallback to canInstall true for unrecognized scope', () => {
|
||||
it('should deny installation for an unrecognized runtime scope', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: 'unknown-scope' as unknown as PluginInstallationScope,
|
||||
plugin_installation_scope: 'unknown-scope',
|
||||
},
|
||||
}
|
||||
} as unknown as SystemFeatures
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -132,9 +108,9 @@ describe('usePluginInstallLimit', () => {
|
||||
const plugin = {
|
||||
from: 'marketplace' as const,
|
||||
verification: { authorized_category: 'langgenius' },
|
||||
}
|
||||
} satisfies PluginInstallCandidate
|
||||
|
||||
const { result } = renderHook(() => usePluginInstallLimit(plugin as never))
|
||||
const { result } = renderHook(() => usePluginInstallLimit(plugin))
|
||||
|
||||
expect(result.current.canInstall).toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,68 +1,55 @@
|
||||
import type { GetSystemFeaturesResponse } from '@dify/contracts/api/console/system-features/types.gen'
|
||||
import type { Plugin, PluginManifestInMarket } from '../../types'
|
||||
import type {
|
||||
PluginBundleDependencyType,
|
||||
PluginVerification,
|
||||
} from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { InstallationScope } from '@/features/system-features/constants'
|
||||
|
||||
type PluginProps = (Plugin | PluginManifestInMarket) & {
|
||||
from: 'github' | 'marketplace' | 'package'
|
||||
type PluginInstallCandidate = {
|
||||
from: PluginBundleDependencyType
|
||||
verification?: PluginVerification | null
|
||||
}
|
||||
type PluginInstallLimitResult = {
|
||||
canInstall: boolean
|
||||
}
|
||||
|
||||
function denyUnsupportedInstallationScope(_scope: never): PluginInstallLimitResult {
|
||||
return { canInstall: false }
|
||||
}
|
||||
|
||||
export function pluginInstallLimit(
|
||||
plugin: PluginProps,
|
||||
plugin: PluginInstallCandidate,
|
||||
systemFeatures: Pick<GetSystemFeaturesResponse, 'plugin_installation_permission'>,
|
||||
) {
|
||||
if (systemFeatures.plugin_installation_permission.restrict_to_marketplace_only) {
|
||||
const permission = systemFeatures.plugin_installation_permission
|
||||
if (permission.restrict_to_marketplace_only) {
|
||||
if (plugin.from === 'github' || plugin.from === 'package') return { canInstall: false }
|
||||
}
|
||||
|
||||
if (
|
||||
systemFeatures.plugin_installation_permission.plugin_installation_scope ===
|
||||
InstallationScope.ALL
|
||||
) {
|
||||
return {
|
||||
canInstall: true,
|
||||
}
|
||||
}
|
||||
if (
|
||||
systemFeatures.plugin_installation_permission.plugin_installation_scope ===
|
||||
InstallationScope.NONE
|
||||
) {
|
||||
return {
|
||||
canInstall: false,
|
||||
}
|
||||
}
|
||||
const verification = plugin.verification || {}
|
||||
if (!plugin.verification || !plugin.verification.authorized_category)
|
||||
verification.authorized_category = 'langgenius'
|
||||
const authorizedCategory = plugin.verification?.authorized_category ?? 'langgenius'
|
||||
const scope = permission.plugin_installation_scope
|
||||
|
||||
if (
|
||||
systemFeatures.plugin_installation_permission.plugin_installation_scope ===
|
||||
InstallationScope.OFFICIAL_ONLY
|
||||
) {
|
||||
return {
|
||||
canInstall: verification.authorized_category === 'langgenius',
|
||||
}
|
||||
}
|
||||
if (
|
||||
systemFeatures.plugin_installation_permission.plugin_installation_scope ===
|
||||
InstallationScope.OFFICIAL_AND_PARTNER
|
||||
) {
|
||||
return {
|
||||
canInstall:
|
||||
verification.authorized_category === 'langgenius' ||
|
||||
verification.authorized_category === 'partner',
|
||||
}
|
||||
}
|
||||
return {
|
||||
canInstall: true,
|
||||
switch (scope) {
|
||||
case InstallationScope.ALL:
|
||||
return { canInstall: true }
|
||||
case InstallationScope.NONE:
|
||||
return { canInstall: false }
|
||||
case InstallationScope.OFFICIAL_ONLY:
|
||||
return { canInstall: authorizedCategory === 'langgenius' }
|
||||
case InstallationScope.OFFICIAL_AND_PARTNER:
|
||||
return {
|
||||
canInstall: authorizedCategory === 'langgenius' || authorizedCategory === 'partner',
|
||||
}
|
||||
default:
|
||||
return denyUnsupportedInstallationScope(scope)
|
||||
}
|
||||
}
|
||||
|
||||
export default function usePluginInstallLimit(plugin: PluginProps): PluginInstallLimitResult {
|
||||
export default function usePluginInstallLimit(
|
||||
plugin: PluginInstallCandidate,
|
||||
): PluginInstallLimitResult {
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
|
||||
return pluginInstallLimit(plugin, systemFeatures)
|
||||
|
||||
Reference in New Issue
Block a user