From aff5c475418defeac9dcb15bed75be3385cb747f Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:01:12 +0800 Subject: [PATCH] refactor: remove backend-only system feature fields (#39543) --- api/AGENTS.md | 11 ++ api/controllers/API_SCHEMA_GUIDE.md | 41 +++++ .../console/auth/forgot_password.py | 2 +- api/controllers/console/auth/login.py | 4 +- api/controllers/console/auth/oauth.py | 2 +- api/controllers/console/feature.py | 18 +- api/controllers/web/feature.py | 23 +-- api/core/helper/credential_utils.py | 2 +- api/openapi/markdown/console-openapi.md | 24 +-- api/openapi/markdown/web-openapi.md | 28 +-- api/services/account_service.py | 14 +- api/services/feature_service.py | 29 +-- api/services/workflow_service.py | 2 +- .../controllers/openapi/conftest.py | 2 +- .../services/test_account_service.py | 166 +++++------------- .../services/test_feature_service.py | 13 -- .../services/test_webhook_service.py | 2 +- .../trigger/test_trigger_e2e.py | 4 +- .../commands/test_generate_swagger_specs.py | 20 +++ .../console/auth/test_email_verification.py | 22 +-- .../console/auth/test_login_logout.py | 8 +- .../controllers/console/auth/test_oauth.py | 2 +- .../controllers/console/test_feature.py | 1 + .../controllers/web/test_feature.py | 13 +- .../core/helper/test_credential_utils.py | 21 ++- .../services/test_account_service.py | 32 +--- .../test_feature_service_internal_policies.py | 42 +++++ .../api/console/system-features/orpc.gen.ts | 17 +- .../api/console/system-features/types.gen.ts | 7 - .../api/console/system-features/zod.gen.ts | 12 +- .../contracts/generated/api/web/orpc.gen.ts | 21 +-- .../contracts/generated/api/web/types.gen.ts | 7 - .../contracts/generated/api/web/zod.gen.ts | 12 +- web/test/console/system-features.ts | 9 - 34 files changed, 267 insertions(+), 366 deletions(-) create mode 100644 api/tests/unit_tests/services/test_feature_service_internal_policies.py diff --git a/api/AGENTS.md b/api/AGENTS.md index 0b7082113c7..474da7800b2 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -213,6 +213,17 @@ Before opening a PR / submitting: DTOs with `register_response_schema_models(...)`, serialize response DTOs with `dump_response(...)`, and avoid adding new legacy `ns.model(...)`, `@marshal_with(...)`, or GET `@ns.expect(...)` patterns. +### System Features Contract + +- Treat the shared Console/Web `/system-features` response as a minimal unauthenticated bootstrap allowlist, not a + general configuration or feature-discovery endpoint. Existing fields do not establish precedent. +- Before adding a field, read `controllers/API_SCHEMA_GUIDE.md#public-system-features-contract` and provide evidence + that both Console and Web have production consumers that require it before authentication. +- Never place backend-only policy, surface-specific configuration, post-authentication state, speculative values, or + large/slow payloads in `SystemFeatureModel`. Use the consumer or domain owner described in the schema guide. +- Agents and reviewers must reject additions whose owner, public exposure, pre-authentication need, or root SSR cost + is not explicit. + ### Miscellaneous - Use `configs.dify_config` for configuration—never read environment variables directly. diff --git a/api/controllers/API_SCHEMA_GUIDE.md b/api/controllers/API_SCHEMA_GUIDE.md index 6cfbab4b1c7..a1e412a3630 100644 --- a/api/controllers/API_SCHEMA_GUIDE.md +++ b/api/controllers/API_SCHEMA_GUIDE.md @@ -12,6 +12,47 @@ parameters, response schemas, and Swagger documentation. - Do not add new Flask-RESTX `fields.*` dictionaries, `Namespace.model(...)` exports, or `@marshal_with(...)` for migrated or new endpoints. - Do not use `@ns.expect(...)` for GET query parameters. Flask-RESTX documents that as a request body. +## Public System Features Contract + +The Console and Web `/system-features` endpoints share `SystemFeatureModel`. They are unauthenticated and may be +requested during root SSR, so treat this response as a minimal public bootstrap allowlist. It is not a general +configuration endpoint, a feature registry, or a mirror of environment and Enterprise settings. Existing fields are +legacy inventory and do not establish precedent for new fields. + +A new field is eligible only when all of the following are true: + +1. Both Console and Web have named production consumers for the field. +2. Both consumers need the value before authentication and tenant/workspace bootstrap to render initial state or + choose an authentication flow. +3. The value varies at runtime or by deployment and cannot be safely derived from an existing public contract. +4. The value is non-sensitive, safe to disclose without authentication, and has stable public API semantics. +5. Sending the value on every root bootstrap is demonstrably clearer and cheaper than a consumer-owned query. + +Do not add: + +- Backend-only policy or enforcement inputs, including security decisions, upload limits, or integration toggles. +- Console-only or Web-only configuration. +- Tenant, workspace, account, permission, billing-detail, or other post-authentication state. +- Provider payloads, operational diagnostics, large nested objects, or values without active consumers. +- Speculative fields added for possible future use. + +Route excluded values to their actual owner: + +- Keep backend enforcement behind a narrow service method or domain policy. +- Serve post-authentication state from an authenticated domain endpoint. +- Serve surface-specific bootstrap state from a Console- or Web-specific endpoint and account for its SSR, caching, + and failure cost explicitly. +- Load large, slow, or page-specific data lazily through a consumer-owned query. + +Every pull request that adds a System Features field must: + +- Name both production consumer paths and explain why they require the value before authentication. +- Document the root SSR request, payload, caching, and failure-mode impact. +- Update the Pydantic owner, regenerate OpenAPI Markdown and TypeScript/Zod contracts, and update shared fixtures. +- Add Console and Web schema regression coverage. Do not hand-edit generated contracts or add compatibility defaults. + +Reviewers should reject a field when its owner, pre-authentication need, or consumers are unclear. + ## Naming - Request body models: use a `Payload` suffix. diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 2c04dd397ef..8a46a2559cf 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -198,6 +198,6 @@ class ForgotPasswordResetApi(Resource): # Create workspace if needed if ( not TenantService.get_join_tenants(account, session=db.session()) - and FeatureService.get_system_features().is_allow_create_workspace + and FeatureService.is_workspace_creation_allowed() ): TenantService.create_owner_tenant(account, session=db.session()) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 637f9ca0825..49b248a1e48 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -163,7 +163,7 @@ class LoginApi(Resource): tenants = TenantService.get_join_tenants(account, session=db.session()) if len(tenants) == 0: if ( - FeatureService.get_system_features().is_allow_create_workspace + FeatureService.is_workspace_creation_allowed() and not FeatureService.get_license().workspaces.is_available() ): raise WorkspacesLimitExceeded() @@ -314,7 +314,7 @@ class EmailCodeLoginApi(Resource): workspaces = FeatureService.get_license().workspaces if not workspaces.is_available(): raise WorkspacesLimitExceeded() - if not FeatureService.get_system_features().is_allow_create_workspace: + if not FeatureService.is_workspace_creation_allowed(): raise NotAllowedCreateWorkspace() else: TenantService.create_owner_tenant(account, session=db.session()) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index a49cf47eaf6..c6b80fbb392 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -292,7 +292,7 @@ def _generate_account( if account: tenants = TenantService.get_join_tenants(account, session=db.session()) if not tenants: - if not FeatureService.get_system_features().is_allow_create_workspace: + if not FeatureService.is_workspace_creation_allowed(): raise WorkSpaceNotAllowedCreateError() else: TenantService.create_owner_tenant(account, session=db.session()) diff --git a/api/controllers/console/feature.py b/api/controllers/console/feature.py index 587871e7a97..594a8431513 100644 --- a/api/controllers/console/feature.py +++ b/api/controllers/console/feature.py @@ -123,24 +123,22 @@ class AppDslVersionApi(Resource): @console_ns.route("/system-features") class SystemFeatureApi(Resource): @console_ns.doc("get_system_features") - @console_ns.doc(description="Get system-wide feature configuration") + @console_ns.doc( + description="Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. " + "This is not a general feature registry." + ) @console_ns.response( 200, "Success", console_ns.models[SystemFeatureModel.__name__], ) def get(self): - """Get system-wide feature configuration + """Get the non-sensitive bootstrap snapshot exposed before authentication. - NOTE: This endpoint is unauthenticated by design, as it provides system features - data required for dashboard initialization. - - Authentication would create circular dependency (can't login without dashboard loading). - - Only non-sensitive configuration data should be returned by this endpoint. Authenticated - license detail is served separately by SystemFeatureLicenseApi. + Authentication configuration must be available before the authentication flow can be selected. + Authenticated license detail is served separately by SystemFeatureLicenseApi. """ - return FeatureService.get_system_features().model_dump() + return dump_response(SystemFeatureModel, FeatureService.get_system_features()) @console_ns.route("/system-features/license") diff --git a/api/controllers/web/feature.py b/api/controllers/web/feature.py index 2a600f445a2..919788687dc 100644 --- a/api/controllers/web/feature.py +++ b/api/controllers/web/feature.py @@ -2,6 +2,7 @@ from flask_restx import Resource from controllers.common.schema import register_response_schema_models from controllers.web import web_ns +from libs.helper import dump_response from services.feature_service import FeatureService, SystemFeatureModel register_response_schema_models(web_ns, SystemFeatureModel) @@ -10,7 +11,10 @@ register_response_schema_models(web_ns, SystemFeatureModel) @web_ns.route("/system-features") class SystemFeatureApi(Resource): @web_ns.doc("get_system_features") - @web_ns.doc(description="Get system feature flags and configuration") + @web_ns.doc( + description="Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. " + "This is not a general feature registry." + ) @web_ns.doc(responses={200: "System features retrieved successfully", 500: "Internal server error"}) @web_ns.response( 200, @@ -18,22 +22,11 @@ class SystemFeatureApi(Resource): web_ns.models[SystemFeatureModel.__name__], ) def get(self): - """Get system feature flags and configuration. - - Returns the current system feature flags and configuration - that control various functionalities across the platform. - - Returns: - dict: System feature configuration object + """Get the non-sensitive bootstrap snapshot exposed before authentication. This endpoint is akin to the `SystemFeatureApi` endpoint in api/controllers/console/feature.py, except it is intended for use by the web app, instead of the console dashboard. - NOTE: This endpoint is unauthenticated by design, as it provides system features - data required for webapp initialization. - - Authentication would create circular dependency (can't authenticate without webapp loading). - - Only non-sensitive configuration data should be returned by this endpoint. + Authentication configuration must be available before the authentication flow can be selected. """ - return FeatureService.get_system_features().model_dump() + return dump_response(SystemFeatureModel, FeatureService.get_system_features()) diff --git a/api/core/helper/credential_utils.py b/api/core/helper/credential_utils.py index a57474a8c12..19a62c43766 100644 --- a/api/core/helper/credential_utils.py +++ b/api/core/helper/credential_utils.py @@ -71,7 +71,7 @@ def check_credential_policy_compliance( ) from services.feature_service import FeatureService - if not FeatureService.get_system_features().plugin_manager.enabled or not credential_id: + if not FeatureService.is_plugin_manager_enabled() or not credential_id: return # Check if credential exists in database first (if requested) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 55b983deb5a..a40ee97685d 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -9475,16 +9475,11 @@ Used for frontend component type mapping | 200 | Success | **application/json**: [SchemaDefinitionsResponse](#schemadefinitionsresponse)
| ### [GET] /system-features -**Get system-wide feature configuration** +**Get the non-sensitive bootstrap snapshot exposed before authentication** -Get system-wide feature configuration -NOTE: This endpoint is unauthenticated by design, as it provides system features -data required for dashboard initialization. - -Authentication would create circular dependency (can't login without dashboard loading). - -Only non-sensitive configuration data should be returned by this endpoint. Authenticated -license detail is served separately by SystemFeatureLicenseApi. +Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. This is not a general feature registry. +Authentication configuration must be available before the authentication flow can be selected. +Authenticated license detail is served separately by SystemFeatureLicenseApi. #### Responses @@ -20521,12 +20516,6 @@ Shared permission levels for resources (datasets, credentials, etc.) | plugins | [ [PluginEntity](#pluginentity) ] | | Yes | | total | integer | | Yes | -#### PluginManagerModel - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| enabled | boolean | | Yes | - #### PluginManifestResponse | Name | Type | Description | Required | @@ -22052,6 +22041,8 @@ Model class for provider system configuration response. #### SystemFeatureModel +Non-sensitive bootstrap snapshot exposed before Console or Web authentication. + | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | branding | [BrandingModel](#brandingmodel) | | Yes | @@ -22068,14 +22059,11 @@ Model class for provider system configuration response. | enable_social_oauth_login | boolean | | Yes | | enable_step_by_step_tour | boolean | | Yes | | enable_trial_app | boolean | | Yes | -| is_allow_create_workspace | boolean | | Yes | | is_allow_register | boolean | | Yes | | is_email_setup | boolean | | Yes | | knowledge_fs_enabled | boolean | | Yes | | license | [LicenseStatusModel](#licensestatusmodel) | | Yes | -| max_plugin_package_size | integer,
**Default:** 15728640 | | Yes | | plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes | -| plugin_manager | [PluginManagerModel](#pluginmanagermodel) | | Yes | | rbac_enabled | boolean | | Yes | | sso_enforced_for_signin | boolean | | Yes | | sso_enforced_for_signin_protocol | string | | Yes | diff --git a/api/openapi/markdown/web-openapi.md b/api/openapi/markdown/web-openapi.md index 954e7c17e8a..4b292991a63 100644 --- a/api/openapi/markdown/web-openapi.md +++ b/api/openapi/markdown/web-openapi.md @@ -774,24 +774,13 @@ Retrieve app site information and configuration. | 500 | Internal Server Error | | ### [GET] /system-features -**Get system feature flags and configuration** - -Get system feature flags and configuration -Returns the current system feature flags and configuration -that control various functionalities across the platform. - -Returns: - dict: System feature configuration object +**Get the non-sensitive bootstrap snapshot exposed before authentication** +Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. This is not a general feature registry. This endpoint is akin to the `SystemFeatureApi` endpoint in api/controllers/console/feature.py, except it is intended for use by the web app, instead of the console dashboard. -NOTE: This endpoint is unauthenticated by design, as it provides system features -data required for webapp initialization. - -Authentication would create circular dependency (can't authenticate without webapp loading). - -Only non-sensitive configuration data should be returned by this endpoint. +Authentication configuration must be available before the authentication flow can be selected. #### Responses @@ -1412,12 +1401,6 @@ Form input definition. | ---- | ---- | ----------- | -------- | | PluginInstallationScope | string | | | -#### PluginManagerModel - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| enabled | boolean | | Yes | - #### RemoteFileInfo | Name | Type | Description | Required | @@ -1557,6 +1540,8 @@ Default configuration for form inputs. #### SystemFeatureModel +Non-sensitive bootstrap snapshot exposed before Console or Web authentication. + | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | branding | [BrandingModel](#brandingmodel) | | Yes | @@ -1573,14 +1558,11 @@ Default configuration for form inputs. | enable_social_oauth_login | boolean | | Yes | | enable_step_by_step_tour | boolean | | Yes | | enable_trial_app | boolean | | Yes | -| is_allow_create_workspace | boolean | | Yes | | is_allow_register | boolean | | Yes | | is_email_setup | boolean | | Yes | | knowledge_fs_enabled | boolean | | Yes | | license | [LicenseStatusModel](#licensestatusmodel) | | Yes | -| max_plugin_package_size | integer,
**Default:** 15728640 | | Yes | | plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes | -| plugin_manager | [PluginManagerModel](#pluginmanagermodel) | | Yes | | rbac_enabled | boolean | | Yes | | sso_enforced_for_signin | boolean | | Yes | | sso_enforced_for_signin_protocol | string | | Yes | diff --git a/api/services/account_service.py b/api/services/account_service.py index 5784f19a3a2..cc2d983c4ef 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -1258,11 +1258,7 @@ class TenantService: session: Session, ) -> Tenant: """Create tenant""" - if ( - not FeatureService.get_system_features().is_allow_create_workspace - and not is_setup - and not is_from_dashboard - ): + if not FeatureService.is_workspace_creation_allowed() and not is_setup and not is_from_dashboard: from controllers.console.error import NotAllowedCreateWorkspace raise NotAllowedCreateWorkspace() @@ -1325,11 +1321,7 @@ class TenantService: owner. It persists the legacy membership before creating the matching RBAC role binding, then makes the workspace current for the account. """ - if ( - not FeatureService.get_system_features().is_allow_create_workspace - and not is_setup - and not is_from_dashboard - ): + if not FeatureService.is_workspace_creation_allowed() and not is_setup and not is_from_dashboard: raise WorkSpaceNotAllowedCreateError() workspaces = FeatureService.get_license().workspaces @@ -2010,7 +2002,7 @@ class RegisterService: AccountService.link_account_integrate(provider, open_id, account, session=session) if ( - FeatureService.get_system_features().is_allow_create_workspace + FeatureService.is_workspace_creation_allowed() and create_workspace_required and FeatureService.get_license().workspaces.is_available() ): diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 8a4b56b672c..6225562ec25 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -161,30 +161,25 @@ class KnowledgeRateLimitModel(FeatureResponseModel): subscription_plan: str = "" -class PluginManagerModel(FeatureResponseModel): - enabled: bool = False - - class SystemFeatureModel(FeatureResponseModel): + """Non-sensitive bootstrap snapshot exposed before Console or Web authentication.""" + deployment_edition: DeploymentEdition enable_app_deploy: bool = False sso_enforced_for_signin: bool = False sso_enforced_for_signin_protocol: str = "" enable_marketplace: bool = False - max_plugin_package_size: int = dify_config.PLUGIN_MAX_PACKAGE_SIZE enable_email_code_login: bool = False enable_email_password_login: bool = True enable_social_oauth_login: bool = False enable_collaboration_mode: bool = True is_allow_register: bool = False - is_allow_create_workspace: bool = False is_email_setup: bool = False license: LicenseStatusModel = LicenseStatusModel() branding: BrandingModel = BrandingModel() webapp_auth: WebAppAuthModel = WebAppAuthModel() plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() enable_change_email: bool = True - plugin_manager: PluginManagerModel = PluginManagerModel() enable_creators_platform: bool = False enable_trial_app: bool = False enable_explore_banner: bool = False @@ -266,7 +261,6 @@ class FeatureService: system_features.branding.enabled = True system_features.webapp_auth.enabled = True system_features.enable_change_email = False - system_features.plugin_manager.enabled = True cls._fulfill_params_from_enterprise(system_features) if dify_config.MARKETPLACE_ENABLED: @@ -277,6 +271,21 @@ class FeatureService: return system_features + @classmethod + def is_workspace_creation_allowed(cls) -> bool: + """Resolve the backend workspace-creation policy, including the Enterprise override.""" + is_allowed = dify_config.ALLOW_CREATE_WORKSPACE + if not dify_config.ENTERPRISE_ENABLED: + return is_allowed + + enterprise_info = EnterpriseService.get_info() + return bool(enterprise_info.get("IsAllowCreateWorkspace", is_allowed)) + + @classmethod + def is_plugin_manager_enabled(cls) -> bool: + """Return whether Enterprise plugin credential policies must be enforced.""" + return dify_config.ENTERPRISE_ENABLED + @classmethod def get_license(cls) -> LicenseModel: """Return full license detail. Enterprise-only; requires an authenticated caller. @@ -299,7 +308,6 @@ class FeatureService: system_features.enable_social_oauth_login = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN system_features.enable_collaboration_mode = dify_config.ENABLE_COLLABORATION_MODE system_features.is_allow_register = dify_config.ALLOW_REGISTER - system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != "" system_features.enable_change_email = dify_config.ENABLE_CHANGE_EMAIL system_features.enable_trial_app = dify_config.ENABLE_TRIAL_APP @@ -465,9 +473,6 @@ class FeatureService: if "IsAllowRegister" in enterprise_info: features.is_allow_register = enterprise_info["IsAllowRegister"] - if "IsAllowCreateWorkspace" in enterprise_info: - features.is_allow_create_workspace = enterprise_info["IsAllowCreateWorkspace"] - if "EnableAppDeploy" in enterprise_info: features.enable_app_deploy = enterprise_info["EnableAppDeploy"] diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index b426f3363c0..0d73acfbca2 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -542,7 +542,7 @@ class WorkflowService: # Validate credentials before publishing, for credential policy check from services.feature_service import FeatureService - if FeatureService.get_system_features().plugin_manager.enabled: + if FeatureService.is_plugin_manager_enabled(): self._validate_workflow_credentials(draft_workflow, session=session) # validate graph structure diff --git a/api/tests/test_containers_integration_tests/controllers/openapi/conftest.py b/api/tests/test_containers_integration_tests/controllers/openapi/conftest.py index 00f605b74c5..8681f461975 100644 --- a/api/tests/test_containers_integration_tests/controllers/openapi/conftest.py +++ b/api/tests/test_containers_integration_tests/controllers/openapi/conftest.py @@ -61,7 +61,7 @@ def add_tenant_for_account( ) -> Tenant: """Create an additional tenant and join ``account`` to it (real service calls).""" with patch("services.account_service.FeatureService") as mock_feature_service: - mock_feature_service.get_system_features.return_value.is_allow_create_workspace = True + mock_feature_service.is_workspace_creation_allowed.return_value = True tenant = TenantService.create_tenant(name=name, session=session) TenantService.create_tenant_member(tenant, account, session, role=role) return tenant diff --git a/api/tests/test_containers_integration_tests/services/test_account_service.py b/api/tests/test_containers_integration_tests/services/test_account_service.py index 4579fbfbb40..26b20e83a9b 100644 --- a/api/tests/test_containers_integration_tests/services/test_account_service.py +++ b/api/tests/test_containers_integration_tests/services/test_account_service.py @@ -37,7 +37,7 @@ class TestAccountService: ): # Setup default mock returns mock_feature_service.get_system_features.return_value.is_allow_register = True - mock_feature_service.get_system_features.return_value.is_allow_create_workspace = True + mock_feature_service.is_workspace_creation_allowed.return_value = True mock_feature_service.get_license.return_value.workspaces.is_available.return_value = True mock_feature_service.get_license.return_value.seats.is_available.return_value = True mock_billing_service.is_email_in_freeze.return_value = False @@ -401,9 +401,7 @@ class TestAccountService: password = generate_valid_password(fake) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -436,9 +434,7 @@ class TestAccountService: password = generate_valid_password(fake) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = False + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = False mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False with pytest.raises(WorkSpaceNotAllowedCreateError): @@ -462,9 +458,7 @@ class TestAccountService: password = generate_valid_password(fake) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = False @@ -1274,7 +1268,7 @@ class TestTenantService: patch("services.account_service.BillingService") as mock_billing_service, ): # Setup default mock returns - mock_feature_service.get_system_features.return_value.is_allow_create_workspace = True + mock_feature_service.is_workspace_creation_allowed.return_value = True mock_feature_service.get_license.return_value.workspaces.is_available.return_value = True mock_feature_service.get_license.return_value.seats.is_available.return_value = True mock_billing_service.is_email_in_freeze.return_value = False @@ -1291,9 +1285,7 @@ class TestTenantService: fake = Faker() tenant_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1312,9 +1304,7 @@ class TestTenantService: fake = Faker() tenant_name = fake.company() # Setup mocks to disable workspace creation - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = False + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = False with pytest.raises(NotAllowedCreateWorkspace): # NotAllowedCreateWorkspace exception TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1328,9 +1318,7 @@ class TestTenantService: fake = Faker() custom_tenant_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = False + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = False # Create tenant with setup flag (should bypass workspace creation restriction) tenant = TenantService.create_tenant( @@ -1354,9 +1342,7 @@ class TestTenantService: name = fake.name() password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and account tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1390,9 +1376,7 @@ class TestTenantService: name2 = fake.name() password2 = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1430,9 +1414,7 @@ class TestTenantService: name = fake.name() password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and account tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1465,9 +1447,7 @@ class TestTenantService: tenant1_name = fake.company() tenant2_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create account and tenants account = AccountService.create_account( @@ -1504,9 +1484,7 @@ class TestTenantService: password = generate_valid_password(fake) tenant_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create account and tenant account = AccountService.create_account( @@ -1542,9 +1520,7 @@ class TestTenantService: name = fake.name() password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create account without setting current tenant account = AccountService.create_account( @@ -1570,9 +1546,7 @@ class TestTenantService: tenant1_name = fake.company() tenant2_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create account and tenants account = AccountService.create_account( @@ -1610,9 +1584,7 @@ class TestTenantService: name = fake.name() password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create account account = AccountService.create_account( @@ -1639,9 +1611,7 @@ class TestTenantService: password = generate_valid_password(fake) tenant_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create account and tenant account = AccountService.create_account( @@ -1670,9 +1640,7 @@ class TestTenantService: admin_name = fake.name() admin_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1717,9 +1685,7 @@ class TestTenantService: tenant_name = fake.company() invalid_role = fake.word() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1738,9 +1704,7 @@ class TestTenantService: name = fake.name() password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and account tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1775,9 +1739,7 @@ class TestTenantService: member_name = fake.name() member_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1818,9 +1780,7 @@ class TestTenantService: password = generate_valid_password(fake) invalid_action = "invalid_action_that_doesnt_exist" # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and account tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1853,9 +1813,7 @@ class TestTenantService: name = fake.name() password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and account tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1891,9 +1849,7 @@ class TestTenantService: member_name = fake.name() member_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -1981,9 +1937,7 @@ class TestTenantService: name = fake.name() password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and account tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -2017,9 +1971,7 @@ class TestTenantService: non_member_name = fake.name() non_member_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -2060,9 +2012,7 @@ class TestTenantService: member_name = fake.name() member_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -2113,9 +2063,7 @@ class TestTenantService: member_name = fake.name() member_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -2174,9 +2122,7 @@ class TestTenantService: member_name = fake.name() member_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -2214,9 +2160,7 @@ class TestTenantService: tenant2_name = fake.company() tenant3_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create multiple tenants tenant1 = TenantService.create_tenant(name=tenant1_name, session=db_session_with_containers) @@ -2241,9 +2185,7 @@ class TestTenantService: password = generate_valid_password(fake) workspace_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -2282,9 +2224,7 @@ class TestTenantService: existing_tenant_name = fake.company() new_workspace_name = fake.company() # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -2325,9 +2265,7 @@ class TestTenantService: password = generate_valid_password(fake) workspace_name = fake.company() # Setup mocks to disable workspace creation - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = False + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = False # Create account account = AccountService.create_account( @@ -2360,9 +2298,7 @@ class TestTenantService: normal_name = fake.name() normal_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -2429,9 +2365,7 @@ class TestTenantService: normal_name = fake.name() normal_password = generate_valid_password(fake) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant and accounts tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -2480,9 +2414,7 @@ class TestTenantService: theme = fake.random_element(elements=("dark", "light")) language = fake.random_element(elements=("zh-CN", "en-US")) # Setup mocks - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True # Create tenant with custom config tenant = TenantService.create_tenant(name=tenant_name, session=db_session_with_containers) @@ -2515,7 +2447,7 @@ class TestRegisterService: ): # Setup default mock returns mock_feature_service.get_system_features.return_value.is_allow_register = True - mock_feature_service.get_system_features.return_value.is_allow_create_workspace = True + mock_feature_service.is_workspace_creation_allowed.return_value = True mock_feature_service.get_license.return_value.workspaces.is_available.return_value = True mock_feature_service.get_license.return_value.seats.is_available.return_value = True mock_billing_service.is_email_in_freeze.return_value = False @@ -2629,9 +2561,7 @@ class TestRegisterService: language = fake.random_element(elements=("en-US", "zh-CN")) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -2673,9 +2603,7 @@ class TestRegisterService: language = fake.random_element(elements=("en-US", "zh-CN")) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -2722,9 +2650,7 @@ class TestRegisterService: language = fake.random_element(elements=("en-US", "zh-CN")) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -2768,9 +2694,7 @@ class TestRegisterService: language = fake.random_element(elements=("en-US", "zh-CN")) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = False + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = False mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False # with pytest.raises(AccountRegisterError, match="Workspace is not allowed to create."): @@ -2807,9 +2731,7 @@ class TestRegisterService: language = fake.random_element(elements=("en-US", "zh-CN")) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = False @@ -2886,9 +2808,7 @@ class TestRegisterService: language = fake.random_element(elements=("en-US", "zh-CN")) # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True diff --git a/api/tests/test_containers_integration_tests/services/test_feature_service.py b/api/tests/test_containers_integration_tests/services/test_feature_service.py index a7e2584c14b..7a86af9f410 100644 --- a/api/tests/test_containers_integration_tests/services/test_feature_service.py +++ b/api/tests/test_containers_integration_tests/services/test_feature_service.py @@ -285,7 +285,6 @@ class TestFeatureService: mock_config.ALLOW_REGISTER = False mock_config.ALLOW_CREATE_WORKSPACE = False mock_config.MAIL_TYPE = "smtp" - mock_config.PLUGIN_MAX_PACKAGE_SIZE = 100 # Act: Execute the method under test result = FeatureService.get_system_features() @@ -309,7 +308,6 @@ class TestFeatureService: assert result.enable_email_password_login is False assert result.enable_collaboration_mode is True assert result.is_allow_register is False - assert result.is_allow_create_workspace is False # Verify branding configuration assert result.branding.application_title == "Test Enterprise" @@ -480,15 +478,11 @@ class TestFeatureService: assert result.enable_social_oauth_login is False assert result.enable_collaboration_mode is False assert result.is_allow_register is True - assert result.is_allow_create_workspace is True assert result.is_email_setup is True # Verify marketplace configuration assert result.enable_marketplace is False - # Verify plugin package size (uses default value from dify_config) - assert result.max_plugin_package_size == 15728640 - def test_get_features_billing_disabled( self, db_session_with_containers: Session, mock_external_service_dependencies ): @@ -683,15 +677,11 @@ class TestFeatureService: assert result.enable_email_password_login is True assert result.enable_social_oauth_login is True assert result.is_allow_register is False - assert result.is_allow_create_workspace is False assert result.is_email_setup is False # Verify marketplace configuration assert result.enable_marketplace is True - # Verify plugin package size (uses default value from dify_config) - assert result.max_plugin_package_size == 15728640 - # Verify default license status assert result.license.status == "none" assert not hasattr(result.license, "expired_at") @@ -923,7 +913,6 @@ class TestFeatureService: assert result.enable_email_code_login is False assert result.enable_email_password_login is True assert result.is_allow_register is False - assert result.is_allow_create_workspace is False # Verify mock interactions mock_external_service_dependencies["enterprise_service"].get_info.assert_called_once() @@ -1672,7 +1661,6 @@ class TestFeatureService: assert result.enable_email_code_login is False assert result.enable_email_password_login is True assert result.is_allow_register is False - assert result.is_allow_create_workspace is False # Verify mock interactions mock_external_service_dependencies["enterprise_service"].get_info.assert_called_once() @@ -1851,7 +1839,6 @@ class TestFeatureService: assert result.enable_email_code_login is False assert result.enable_email_password_login is True assert result.is_allow_register is False - assert result.is_allow_create_workspace is False assert result.license.status == LicenseStatus.LOST # Verify mock interactions diff --git a/api/tests/test_containers_integration_tests/services/test_webhook_service.py b/api/tests/test_containers_integration_tests/services/test_webhook_service.py index ae99cc05ffb..b4022e44f56 100644 --- a/api/tests/test_containers_integration_tests/services/test_webhook_service.py +++ b/api/tests/test_containers_integration_tests/services/test_webhook_service.py @@ -41,7 +41,7 @@ class TestWebhookService: # Mock feature service mock_feature_service.get_system_features.return_value.is_allow_register = True - mock_feature_service.get_system_features.return_value.is_allow_create_workspace = True + mock_feature_service.is_workspace_creation_allowed.return_value = True yield { "async_service": mock_async_service, diff --git a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py index b6865510adf..90dd2bcfc84 100644 --- a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py +++ b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py @@ -112,8 +112,8 @@ def test_publish_blocks_start_and_trigger_coexistence( monkeypatch.setattr( feature_service_module.FeatureService, - "get_system_features", - classmethod(lambda _cls: SimpleNamespace(plugin_manager=SimpleNamespace(enabled=False))), + "is_plugin_manager_enabled", + classmethod(lambda _cls: False), ) monkeypatch.setattr("services.workflow_service.dify_config", SimpleNamespace(BILLING_ENABLED=False)) diff --git a/api/tests/unit_tests/commands/test_generate_swagger_specs.py b/api/tests/unit_tests/commands/test_generate_swagger_specs.py index 72669270ad1..7ec832c526c 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_specs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_specs.py @@ -110,6 +110,26 @@ def test_generate_specs_writes_unique_operation_ids(tmp_path): assert len(operation_ids) == len(set(operation_ids)) +def test_system_features_specs_exclude_backend_only_fields(tmp_path): + module = _load_generate_swagger_specs_module() + + written_paths = module.generate_specs(tmp_path) + excluded_fields = { + "is_allow_create_workspace", + "max_plugin_package_size", + "plugin_manager", + } + + for spec_name in ("console-openapi.json", "web-openapi.json"): + spec_path = next(path for path in written_paths if path.name == spec_name) + payload = json.loads(spec_path.read_text(encoding="utf-8")) + schemas = payload["components"]["schemas"] + system_features_schema = schemas["SystemFeatureModel"] + + assert excluded_fields.isdisjoint(system_features_schema["properties"]) + assert "PluginManagerModel" not in schemas + + def test_generate_specs_writes_get_operations_without_request_bodies(tmp_path): module = _load_generate_swagger_specs_module() diff --git a/api/tests/unit_tests/controllers/console/auth/test_email_verification.py b/api/tests/unit_tests/controllers/console/auth/test_email_verification.py index cb79ccc1835..eef39e8d208 100644 --- a/api/tests/unit_tests/controllers/console/auth/test_email_verification.py +++ b/api/tests/unit_tests/controllers/console/auth/test_email_verification.py @@ -442,10 +442,10 @@ class TestEmailCodeLoginApi: @patch("controllers.console.auth.login.AccountService.revoke_email_code_login_token") @patch("controllers.console.auth.login.AccountService.get_user_through_email") @patch("controllers.console.auth.login.TenantService.get_join_tenants") - @patch("controllers.console.auth.login.FeatureService.get_system_features") + @patch("controllers.console.auth.login.FeatureService.is_workspace_creation_allowed") def test_email_code_login_creates_workspace_for_user_without_tenant( self, - mock_get_features, + mock_is_workspace_creation_allowed, mock_get_tenants, mock_get_user, mock_revoke_token, @@ -465,10 +465,7 @@ class TestEmailCodeLoginApi: mock_get_data.return_value = {"email": "test@example.com", "code": "123456"} mock_get_user.return_value = mock_account mock_get_tenants.return_value = [] - mock_features = MagicMock() - mock_features.is_allow_create_workspace = True - mock_features.license.workspaces.is_available.return_value = True - mock_get_features.return_value = mock_features + mock_is_workspace_creation_allowed.return_value = True # Act & Assert - Should not raise WorkspacesLimitExceeded with app.test_request_context( @@ -486,10 +483,10 @@ class TestEmailCodeLoginApi: @patch("controllers.console.auth.login.AccountService.get_user_through_email") @patch("controllers.console.auth.login.TenantService.get_join_tenants") @patch("controllers.console.auth.login.FeatureService.get_license") - @patch("controllers.console.auth.login.FeatureService.get_system_features") + @patch("controllers.console.auth.login.FeatureService.is_workspace_creation_allowed") def test_email_code_login_workspace_limit_exceeded( self, - mock_get_features, + mock_is_workspace_creation_allowed, mock_get_license, mock_get_tenants, mock_get_user, @@ -510,6 +507,7 @@ class TestEmailCodeLoginApi: mock_get_user.return_value = mock_account mock_get_tenants.return_value = [] mock_get_license.return_value.workspaces.is_available.return_value = False + mock_is_workspace_creation_allowed.return_value = True # Act & Assert with app.test_request_context( @@ -526,10 +524,10 @@ class TestEmailCodeLoginApi: @patch("controllers.console.auth.login.AccountService.revoke_email_code_login_token") @patch("controllers.console.auth.login.AccountService.get_user_through_email") @patch("controllers.console.auth.login.TenantService.get_join_tenants") - @patch("controllers.console.auth.login.FeatureService.get_system_features") + @patch("controllers.console.auth.login.FeatureService.is_workspace_creation_allowed") def test_email_code_login_workspace_creation_not_allowed( self, - mock_get_features, + mock_is_workspace_creation_allowed, mock_get_tenants, mock_get_user, mock_revoke_token, @@ -548,9 +546,7 @@ class TestEmailCodeLoginApi: mock_get_data.return_value = {"email": "test@example.com", "code": "123456"} mock_get_user.return_value = mock_account mock_get_tenants.return_value = [] - mock_features = MagicMock() - mock_features.is_allow_create_workspace = False - mock_get_features.return_value = mock_features + mock_is_workspace_creation_allowed.return_value = False # Act & Assert with app.test_request_context( diff --git a/api/tests/unit_tests/controllers/console/auth/test_login_logout.py b/api/tests/unit_tests/controllers/console/auth/test_login_logout.py index cb0419c4acc..51428d3d883 100644 --- a/api/tests/unit_tests/controllers/console/auth/test_login_logout.py +++ b/api/tests/unit_tests/controllers/console/auth/test_login_logout.py @@ -344,10 +344,10 @@ class TestLoginApi: @patch("controllers.console.auth.login.AccountService.authenticate") @patch("controllers.console.auth.login.TenantService.get_join_tenants") @patch("controllers.console.auth.login.FeatureService.get_license") - @patch("controllers.console.auth.login.FeatureService.get_system_features") + @patch("controllers.console.auth.login.FeatureService.is_workspace_creation_allowed") def test_login_fails_when_no_workspace_and_limit_exceeded( self, - mock_get_features: MagicMock, + mock_is_workspace_creation_allowed: MagicMock, mock_get_license: MagicMock, mock_get_tenants: MagicMock, mock_authenticate: MagicMock, @@ -370,9 +370,7 @@ class TestLoginApi: mock_authenticate.return_value = mock_account mock_get_tenants.return_value = [] # No tenants - mock_features = MagicMock() - mock_features.is_allow_create_workspace = True - mock_get_features.return_value = mock_features + mock_is_workspace_creation_allowed.return_value = True mock_get_license.return_value.workspaces.is_available.return_value = False # Act & Assert diff --git a/api/tests/unit_tests/controllers/console/auth/test_oauth.py b/api/tests/unit_tests/controllers/console/auth/test_oauth.py index a32cac0225f..b8ec042f341 100644 --- a/api/tests/unit_tests/controllers/console/auth/test_oauth.py +++ b/api/tests/unit_tests/controllers/console/auth/test_oauth.py @@ -646,7 +646,7 @@ class TestAccountGeneration: ): mock_get_account.return_value = mock_account mock_tenant_service.get_join_tenants.return_value = [] - mock_feature_service.get_system_features.return_value.is_allow_create_workspace = True + mock_feature_service.is_workspace_creation_allowed.return_value = True with app.test_request_context(headers={"Accept-Language": "en-US,en;q=0.9"}): result, oauth_new_user = _generate_account("github", user_info) diff --git a/api/tests/unit_tests/controllers/console/test_feature.py b/api/tests/unit_tests/controllers/console/test_feature.py index 66fb0b00f74..19e30a1b08d 100644 --- a/api/tests/unit_tests/controllers/console/test_feature.py +++ b/api/tests/unit_tests/controllers/console/test_feature.py @@ -102,6 +102,7 @@ class TestSystemFeatureApi: result = api.get() assert result == system_features.model_dump() + assert result["is_allow_register"] is True assert result["enable_learn_app"] is True assert result["license"] == {"status": LicenseStatus.NONE} get_system_features.assert_called_once_with() diff --git a/api/tests/unit_tests/controllers/web/test_feature.py b/api/tests/unit_tests/controllers/web/test_feature.py index fe45d5f0592..c4701119eff 100644 --- a/api/tests/unit_tests/controllers/web/test_feature.py +++ b/api/tests/unit_tests/controllers/web/test_feature.py @@ -7,27 +7,26 @@ from unittest.mock import MagicMock, patch from flask import Flask from controllers.web.feature import SystemFeatureApi +from enums.deployment_edition import DeploymentEdition +from services.feature_service import SystemFeatureModel class TestSystemFeatureApi: @patch("controllers.web.feature.FeatureService.get_system_features") def test_returns_system_features(self, mock_features: MagicMock, app: Flask) -> None: - mock_model = MagicMock() - mock_model.model_dump.return_value = {"sso_enforced_for_signin": False, "webapp_auth": {"enabled": False}} - mock_features.return_value = mock_model + system_features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY) + mock_features.return_value = system_features with app.test_request_context("/system-features"): result = SystemFeatureApi().get() - assert result == {"sso_enforced_for_signin": False, "webapp_auth": {"enabled": False}} + assert result == system_features.model_dump() mock_features.assert_called_once() @patch("controllers.web.feature.FeatureService.get_system_features") def test_unauthenticated_access(self, mock_features: MagicMock, app: Flask) -> None: """SystemFeatureApi is unauthenticated by design — no WebApiResource decorator.""" - mock_model = MagicMock() - mock_model.model_dump.return_value = {} - mock_features.return_value = mock_model + mock_features.return_value = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY) # Verify it's a bare Resource, not WebApiResource from flask_restx import Resource diff --git a/api/tests/unit_tests/core/helper/test_credential_utils.py b/api/tests/unit_tests/core/helper/test_credential_utils.py index 08b273a8dae..8c32b13865a 100644 --- a/api/tests/unit_tests/core/helper/test_credential_utils.py +++ b/api/tests/unit_tests/core/helper/test_credential_utils.py @@ -1,4 +1,3 @@ -from types import SimpleNamespace from typing import cast import pytest @@ -51,8 +50,8 @@ def test_check_credential_policy_compliance_returns_when_feature_disabled( mocker: MockerFixture, ) -> None: mocker.patch( - "services.feature_service.FeatureService.get_system_features", - return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=False)), + "services.feature_service.FeatureService.is_plugin_manager_enabled", + return_value=False, ) check_call = mocker.patch( "services.enterprise.plugin_manager_service.PluginManagerService.check_credential_policy_compliance" @@ -67,8 +66,8 @@ def test_check_credential_policy_compliance_raises_when_credential_missing( mocker: MockerFixture, ) -> None: mocker.patch( - "services.feature_service.FeatureService.get_system_features", - return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=True)), + "services.feature_service.FeatureService.is_plugin_manager_enabled", + return_value=True, ) mocker.patch("core.helper.credential_utils.is_credential_exists", return_value=False) @@ -80,8 +79,8 @@ def test_check_credential_policy_compliance_calls_plugin_manager_with_request( mocker: MockerFixture, ) -> None: mocker.patch( - "services.feature_service.FeatureService.get_system_features", - return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=True)), + "services.feature_service.FeatureService.is_plugin_manager_enabled", + return_value=True, ) mocker.patch("core.helper.credential_utils.is_credential_exists", return_value=True) check_call = mocker.patch( @@ -101,8 +100,8 @@ def test_check_credential_policy_compliance_skips_existence_check_when_disabled( mocker: MockerFixture, ) -> None: mocker.patch( - "services.feature_service.FeatureService.get_system_features", - return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=True)), + "services.feature_service.FeatureService.is_plugin_manager_enabled", + return_value=True, ) exists_call = mocker.patch("core.helper.credential_utils.is_credential_exists") check_call = mocker.patch( @@ -124,8 +123,8 @@ def test_check_credential_policy_compliance_returns_when_credential_id_empty( mocker: MockerFixture, ) -> None: mocker.patch( - "services.feature_service.FeatureService.get_system_features", - return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=True)), + "services.feature_service.FeatureService.is_plugin_manager_enabled", + return_value=True, ) exists_call = mocker.patch("core.helper.credential_utils.is_credential_exists") check_call = mocker.patch( diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index bc8f4c763a7..ad40dab358c 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -803,9 +803,7 @@ class TestTenantService: """Creating an owner workspace persists both the tenant and owner membership.""" mock_account = TestAccountAssociatedDataFactory.create_account_mock() - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -1023,9 +1021,7 @@ class TestTenantService: self, sqlite_session: Session, mock_external_service_dependencies ): mock_account = TestAccountAssociatedDataFactory.create_account_mock(account_id="user-rbac", name="RBAC User") - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -1476,9 +1472,7 @@ class TestRegisterService: """Test successful account registration.""" # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -1585,9 +1579,7 @@ class TestRegisterService: monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", True, raising=False) mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -1624,9 +1616,7 @@ class TestRegisterService: monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", True, raising=False) mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -1659,9 +1649,7 @@ class TestRegisterService: """Test account registration with OAuth integration.""" # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -1703,9 +1691,7 @@ class TestRegisterService: """Test account registration with pending status.""" # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True @@ -1745,9 +1731,7 @@ class TestRegisterService: """Test registration when workspace creation is not allowed.""" # Setup mocks mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True - mock_external_service_dependencies[ - "feature_service" - ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True mock_external_service_dependencies[ "feature_service" ].get_license.return_value.workspaces.is_available.return_value = True diff --git a/api/tests/unit_tests/services/test_feature_service_internal_policies.py b/api/tests/unit_tests/services/test_feature_service_internal_policies.py new file mode 100644 index 00000000000..2ca475001e4 --- /dev/null +++ b/api/tests/unit_tests/services/test_feature_service_internal_policies.py @@ -0,0 +1,42 @@ +import pytest + +from services.feature_service import FeatureService + + +def test_workspace_creation_uses_environment_policy(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", False) + monkeypatch.setattr("services.feature_service.dify_config.ALLOW_CREATE_WORKSPACE", True) + monkeypatch.setattr( + "services.feature_service.EnterpriseService.get_info", + lambda: (_ for _ in ()).throw(AssertionError("enterprise API should not be called")), + ) + + assert FeatureService.is_workspace_creation_allowed() is True + + +def test_workspace_creation_uses_enterprise_policy(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True) + monkeypatch.setattr( + "services.feature_service.EnterpriseService.get_info", + lambda: {"IsAllowCreateWorkspace": False}, + ) + + assert FeatureService.is_workspace_creation_allowed() is False + + +def test_workspace_creation_keeps_environment_policy_when_enterprise_value_is_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True) + monkeypatch.setattr("services.feature_service.dify_config.ALLOW_CREATE_WORKSPACE", True) + monkeypatch.setattr("services.feature_service.EnterpriseService.get_info", lambda: {}) + + assert FeatureService.is_workspace_creation_allowed() is True + + +def test_plugin_manager_is_enabled_only_for_enterprise(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True) + assert FeatureService.is_plugin_manager_enabled() is True + + monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", False) + assert FeatureService.is_plugin_manager_enabled() is False diff --git a/packages/contracts/generated/api/console/system-features/orpc.gen.ts b/packages/contracts/generated/api/console/system-features/orpc.gen.ts index d9ebc5df2b4..4cdfe46ea87 100644 --- a/packages/contracts/generated/api/console/system-features/orpc.gen.ts +++ b/packages/contracts/generated/api/console/system-features/orpc.gen.ts @@ -28,26 +28,21 @@ export const license = { } /** - * Get system-wide feature configuration + * Get the non-sensitive bootstrap snapshot exposed before authentication * - * Get system-wide feature configuration - * NOTE: This endpoint is unauthenticated by design, as it provides system features - * data required for dashboard initialization. - * - * Authentication would create circular dependency (can't login without dashboard loading). - * - * Only non-sensitive configuration data should be returned by this endpoint. Authenticated - * license detail is served separately by SystemFeatureLicenseApi. + * Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. This is not a general feature registry. + * Authentication configuration must be available before the authentication flow can be selected. + * Authenticated license detail is served separately by SystemFeatureLicenseApi. */ export const get2 = oc .route({ description: - "Get system-wide feature configuration\nNOTE: This endpoint is unauthenticated by design, as it provides system features\ndata required for dashboard initialization.\n\nAuthentication would create circular dependency (can't login without dashboard loading).\n\nOnly non-sensitive configuration data should be returned by this endpoint. Authenticated\nlicense detail is served separately by SystemFeatureLicenseApi.", + 'Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. This is not a general feature registry.\nAuthentication configuration must be available before the authentication flow can be selected.\nAuthenticated license detail is served separately by SystemFeatureLicenseApi.', inputStructure: 'detailed', method: 'GET', operationId: 'getSystemFeatures', path: '/system-features', - summary: 'Get system-wide feature configuration', + summary: 'Get the non-sensitive bootstrap snapshot exposed before authentication', tags: ['console'], }) .output(zGetSystemFeaturesResponse) diff --git a/packages/contracts/generated/api/console/system-features/types.gen.ts b/packages/contracts/generated/api/console/system-features/types.gen.ts index 2f865c06736..076f98546cc 100644 --- a/packages/contracts/generated/api/console/system-features/types.gen.ts +++ b/packages/contracts/generated/api/console/system-features/types.gen.ts @@ -19,14 +19,11 @@ export type SystemFeatureModel = { enable_social_oauth_login: boolean enable_step_by_step_tour: boolean enable_trial_app: boolean - is_allow_create_workspace: boolean is_allow_register: boolean is_email_setup: boolean knowledge_fs_enabled: boolean license: LicenseStatusModel - max_plugin_package_size: number plugin_installation_permission: PluginInstallationPermissionModel - plugin_manager: PluginManagerModel rbac_enabled: boolean sso_enforced_for_signin: boolean sso_enforced_for_signin_protocol: string @@ -59,10 +56,6 @@ export type PluginInstallationPermissionModel = { restrict_to_marketplace_only: boolean } -export type PluginManagerModel = { - enabled: boolean -} - export type WebAppAuthModel = { allow_email_code_login: boolean allow_email_password_login: boolean diff --git a/packages/contracts/generated/api/console/system-features/zod.gen.ts b/packages/contracts/generated/api/console/system-features/zod.gen.ts index 1ef07bee0bd..20cf33d3891 100644 --- a/packages/contracts/generated/api/console/system-features/zod.gen.ts +++ b/packages/contracts/generated/api/console/system-features/zod.gen.ts @@ -20,13 +20,6 @@ export const zBrandingModel = z.object({ */ export const zDeploymentEdition = z.enum(['CLOUD', 'COMMUNITY', 'ENTERPRISE']) -/** - * PluginManagerModel - */ -export const zPluginManagerModel = z.object({ - enabled: z.boolean().default(false), -}) - /** * LicenseLimitationModel * @@ -109,6 +102,8 @@ export const zWebAppAuthModel = z.object({ /** * SystemFeatureModel + * + * Non-sensitive bootstrap snapshot exposed before Console or Web authentication. */ export const zSystemFeatureModel = z.object({ branding: zBrandingModel.default({ @@ -131,17 +126,14 @@ export const zSystemFeatureModel = z.object({ enable_social_oauth_login: z.boolean().default(false), enable_step_by_step_tour: z.boolean().default(false), enable_trial_app: z.boolean().default(false), - is_allow_create_workspace: z.boolean().default(false), is_allow_register: z.boolean().default(false), is_email_setup: z.boolean().default(false), knowledge_fs_enabled: z.boolean().default(false), license: zLicenseStatusModel.default({ status: 'none' }), - max_plugin_package_size: z.int().default(15728640), plugin_installation_permission: zPluginInstallationPermissionModel.default({ plugin_installation_scope: 'all', restrict_to_marketplace_only: false, }), - plugin_manager: zPluginManagerModel.default({ enabled: false }), rbac_enabled: z.boolean().default(false), sso_enforced_for_signin: z.boolean().default(false), sso_enforced_for_signin_protocol: z.string().default(''), diff --git a/packages/contracts/generated/api/web/orpc.gen.ts b/packages/contracts/generated/api/web/orpc.gen.ts index f0b9b5375e8..1b583366d38 100644 --- a/packages/contracts/generated/api/web/orpc.gen.ts +++ b/packages/contracts/generated/api/web/orpc.gen.ts @@ -939,34 +939,23 @@ export const site = { } /** - * Get system feature flags and configuration - * - * Get system feature flags and configuration - * Returns the current system feature flags and configuration - * that control various functionalities across the platform. - * - * Returns: - * dict: System feature configuration object + * Get the non-sensitive bootstrap snapshot exposed before authentication * + * Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. This is not a general feature registry. * This endpoint is akin to the `SystemFeatureApi` endpoint in api/controllers/console/feature.py, * except it is intended for use by the web app, instead of the console dashboard. * - * NOTE: This endpoint is unauthenticated by design, as it provides system features - * data required for webapp initialization. - * - * Authentication would create circular dependency (can't authenticate without webapp loading). - * - * Only non-sensitive configuration data should be returned by this endpoint. + * Authentication configuration must be available before the authentication flow can be selected. */ export const get13 = oc .route({ description: - "Get system feature flags and configuration\nReturns the current system feature flags and configuration\nthat control various functionalities across the platform.\n\nReturns:\n dict: System feature configuration object\n\nThis endpoint is akin to the `SystemFeatureApi` endpoint in api/controllers/console/feature.py,\nexcept it is intended for use by the web app, instead of the console dashboard.\n\nNOTE: This endpoint is unauthenticated by design, as it provides system features\ndata required for webapp initialization.\n\nAuthentication would create circular dependency (can't authenticate without webapp loading).\n\nOnly non-sensitive configuration data should be returned by this endpoint.", + 'Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. This is not a general feature registry.\nThis endpoint is akin to the `SystemFeatureApi` endpoint in api/controllers/console/feature.py,\nexcept it is intended for use by the web app, instead of the console dashboard.\n\nAuthentication configuration must be available before the authentication flow can be selected.', inputStructure: 'detailed', method: 'GET', operationId: 'getSystemFeatures', path: '/system-features', - summary: 'Get system feature flags and configuration', + summary: 'Get the non-sensitive bootstrap snapshot exposed before authentication', tags: ['web'], }) .output(zGetSystemFeaturesResponse) diff --git a/packages/contracts/generated/api/web/types.gen.ts b/packages/contracts/generated/api/web/types.gen.ts index 3425dc64e5d..14e9bbf2e53 100644 --- a/packages/contracts/generated/api/web/types.gen.ts +++ b/packages/contracts/generated/api/web/types.gen.ts @@ -390,10 +390,6 @@ export type PluginInstallationScope = | 'official_and_specific_partners' | 'official_only' -export type PluginManagerModel = { - enabled: boolean -} - export type RemoteFileInfo = { file_length: number file_type: string @@ -516,14 +512,11 @@ export type SystemFeatureModel = { enable_social_oauth_login: boolean enable_step_by_step_tour: boolean enable_trial_app: boolean - is_allow_create_workspace: boolean is_allow_register: boolean is_email_setup: boolean knowledge_fs_enabled: boolean license: LicenseStatusModel - max_plugin_package_size: number plugin_installation_permission: PluginInstallationPermissionModel - plugin_manager: PluginManagerModel rbac_enabled: boolean sso_enforced_for_signin: boolean sso_enforced_for_signin_protocol: string diff --git a/packages/contracts/generated/api/web/zod.gen.ts b/packages/contracts/generated/api/web/zod.gen.ts index 515245a69e3..f41de0e0077 100644 --- a/packages/contracts/generated/api/web/zod.gen.ts +++ b/packages/contracts/generated/api/web/zod.gen.ts @@ -454,13 +454,6 @@ export const zPluginInstallationPermissionModel = z.object({ restrict_to_marketplace_only: z.boolean().default(false), }) -/** - * PluginManagerModel - */ -export const zPluginManagerModel = z.object({ - enabled: z.boolean().default(false), -}) - /** * RemoteFileInfo */ @@ -756,6 +749,8 @@ export const zWebAppAuthModel = z.object({ /** * SystemFeatureModel + * + * Non-sensitive bootstrap snapshot exposed before Console or Web authentication. */ export const zSystemFeatureModel = z.object({ branding: zBrandingModel.default({ @@ -778,17 +773,14 @@ export const zSystemFeatureModel = z.object({ enable_social_oauth_login: z.boolean().default(false), enable_step_by_step_tour: z.boolean().default(false), enable_trial_app: z.boolean().default(false), - is_allow_create_workspace: z.boolean().default(false), is_allow_register: z.boolean().default(false), is_email_setup: z.boolean().default(false), knowledge_fs_enabled: z.boolean().default(false), license: zLicenseStatusModel.default({ status: 'none' }), - max_plugin_package_size: z.int().default(15728640), plugin_installation_permission: zPluginInstallationPermissionModel.default({ plugin_installation_scope: 'all', restrict_to_marketplace_only: false, }), - plugin_manager: zPluginManagerModel.default({ enabled: false }), rbac_enabled: z.boolean().default(false), sso_enforced_for_signin: z.boolean().default(false), sso_enforced_for_signin_protocol: z.string().default(''), diff --git a/web/test/console/system-features.ts b/web/test/console/system-features.ts index 761a0213927..50f01491730 100644 --- a/web/test/console/system-features.ts +++ b/web/test/console/system-features.ts @@ -21,11 +21,9 @@ const baseSystemFeatures = { enable_email_password_login: true, enable_social_oauth_login: false, enable_collaboration_mode: true, - is_allow_create_workspace: false, is_allow_register: false, is_email_setup: false, enable_change_email: true, - max_plugin_package_size: 15728640, license: { status: LicenseStatus.NONE, }, @@ -50,9 +48,6 @@ const baseSystemFeatures = { plugin_installation_scope: InstallationScope.ALL, restrict_to_marketplace_only: false, }, - plugin_manager: { - enabled: false, - }, rbac_enabled: false, enable_creators_platform: false, enable_trial_app: false, @@ -102,10 +97,6 @@ export const createSystemFeaturesFixture = ( ...baseSystemFeatures.license, ...overrides.license, }, - plugin_manager: { - ...baseSystemFeatures.plugin_manager, - ...overrides.plugin_manager, - }, }) export const createSystemFeaturesLicenseFixture = (