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 = (