diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index a56e2fca6e1147..490d2e3a1e172d 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -41,6 +41,7 @@ cloud_edition_billing_enabled, enable_change_email, enterprise_license_required, + model_validate, only_edition_cloud, setup_required, with_current_user, @@ -332,9 +333,9 @@ class AccountAvatarApi(Resource): @login_required @account_initialization_required @with_current_user - def get(self, current_user: Account): - args = AccountAvatarQuery.model_validate(request.args.to_dict(flat=True)) - avatar = args.avatar + @model_validate(AccountAvatarQuery) + def get(self, req_data: AccountAvatarQuery, current_user: Account): + avatar = req_data.avatar if avatar.startswith(("http://", "https://")): return AvatarUrlResponse(avatar_url=avatar).model_dump(mode="json") diff --git a/api/controllers/console/workspace/endpoint.py b/api/controllers/console/workspace/endpoint.py index 8ee83fd0f5738d..f02ae4a73fb95a 100644 --- a/api/controllers/console/workspace/endpoint.py +++ b/api/controllers/console/workspace/endpoint.py @@ -11,7 +11,6 @@ from http import HTTPStatus from typing import Any -from flask import request from flask_restx import Resource from pydantic import BaseModel, Field @@ -294,14 +293,14 @@ class EndpointListApi(Resource): @account_initialization_required @with_current_user_id @with_current_tenant_id - def get(self, tenant_id: str, user_id: str): - args = EndpointListQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(EndpointListQuery) + def get(self, req_data: EndpointListQuery, tenant_id: str, user_id: str): endpoints = EndpointService.list_endpoints( tenant_id=tenant_id, user_id=user_id, - page=args.page, - page_size=args.page_size, + page=req_data.page, + page_size=req_data.page_size, ) return EndpointListResponse(endpoints=endpoints).model_dump(mode="json") @@ -322,15 +321,15 @@ class EndpointListForSinglePluginApi(Resource): @account_initialization_required @with_current_user_id @with_current_tenant_id - def get(self, tenant_id: str, user_id: str): - args = EndpointListForPluginQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(EndpointListForPluginQuery) + def get(self, req_data: EndpointListForPluginQuery, tenant_id: str, user_id: str): endpoints = EndpointService.list_endpoints_for_single_plugin( tenant_id=tenant_id, user_id=user_id, - plugin_id=args.plugin_id, - page=args.page, - page_size=args.page_size, + plugin_id=req_data.plugin_id, + page=req_data.page, + page_size=req_data.page_size, ) return EndpointListResponse(endpoints=endpoints).model_dump(mode="json") diff --git a/api/controllers/console/workspace/models.py b/api/controllers/console/workspace/models.py index b3c7833a070c83..54d021251fdc2c 100644 --- a/api/controllers/console/workspace/models.py +++ b/api/controllers/console/workspace/models.py @@ -1,7 +1,6 @@ import logging from typing import Any, cast -from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator @@ -18,6 +17,7 @@ RBACResourceScope, account_initialization_required, is_admin_or_owner_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -212,12 +212,12 @@ class DefaultModelApi(Resource): @login_required @account_initialization_required @with_current_tenant_id - def get(self, tenant_id: str): - args = ParserGetDefault.model_validate(request.args.to_dict(flat=True)) + @model_validate(ParserGetDefault) + def get(self, req_data: ParserGetDefault, tenant_id: str): model_provider_service = ModelProviderService() default_model_entity = model_provider_service.get_default_model_of_model_type( - tenant_id=tenant_id, model_type=args.model_type + tenant_id=tenant_id, model_type=req_data.model_type ) return DefaultModelDataResponse(data=default_model_entity).model_dump(mode="json") @@ -230,10 +230,10 @@ def get(self, tenant_id: str): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, tenant_id: str): - args = ParserPostDefault.model_validate(console_ns.payload) + @model_validate(ParserPostDefault) + def post(self, req_data: ParserPostDefault, tenant_id: str): model_provider_service = ModelProviderService() - model_settings = args.model_settings + model_settings = req_data.model_settings for model_setting in model_settings: if model_setting.provider is None: continue @@ -279,43 +279,43 @@ def get(self, tenant_id: str, provider: str): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, tenant_id: str, provider: str): + @model_validate(ParserPostModels) + def post(self, req_data: ParserPostModels, tenant_id: str, provider: str): # To save the model's load balance configs - args = ParserPostModels.model_validate(console_ns.payload) - if args.config_from == "custom-model": - if not args.credential_id: + if req_data.config_from == "custom-model": + if not req_data.credential_id: raise ValueError("credential_id is required when configuring a custom-model") service = ModelProviderService() service.switch_active_custom_model_credential( tenant_id=tenant_id, provider=provider, - model_type=args.model_type, - model=args.model, - credential_id=args.credential_id, + model_type=req_data.model_type, + model=req_data.model, + credential_id=req_data.credential_id, ) model_load_balancing_service = ModelLoadBalancingService() - if args.load_balancing and args.load_balancing.configs: + if req_data.load_balancing and req_data.load_balancing.configs: # save load balancing configs model_load_balancing_service.update_load_balancing_configs( tenant_id=tenant_id, provider=provider, - model=args.model, - model_type=args.model_type, - configs=args.load_balancing.configs, - config_from=args.config_from or "", + model=req_data.model, + model_type=req_data.model_type, + configs=req_data.load_balancing.configs, + config_from=req_data.config_from or "", session=db.session(), ) - if args.load_balancing.enabled: + if req_data.load_balancing.enabled: model_load_balancing_service.enable_model_load_balancing( - tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type + tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type ) else: model_load_balancing_service.disable_model_load_balancing( - tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type + tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type ) return SimpleResultResponse(result="success").model_dump(mode="json"), 200 @@ -328,12 +328,12 @@ def post(self, tenant_id: str, provider: str): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) @account_initialization_required @with_current_tenant_id - def delete(self, tenant_id: str, provider: str): - args = ParserDeleteModels.model_validate(console_ns.payload) + @model_validate(ParserDeleteModels) + def delete(self, req_data: ParserDeleteModels, tenant_id: str, provider: str): model_provider_service = ModelProviderService() model_provider_service.remove_model( - tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type + tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type ) return "", 204 @@ -352,29 +352,29 @@ class ModelProviderModelCredentialApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - def get(self, tenant_id: str, user: Account, provider: str): - args = ParserGetCredentials.model_validate(request.args.to_dict(flat=True)) + @model_validate(ParserGetCredentials) + def get(self, req_data: ParserGetCredentials, tenant_id: str, user: Account, provider: str): model_provider_service = ModelProviderService() current_credential = model_provider_service.get_model_credential( tenant_id=tenant_id, provider=provider, - model_type=args.model_type, - model=args.model, - credential_id=args.credential_id, + model_type=req_data.model_type, + model=req_data.model, + credential_id=req_data.credential_id, ) model_load_balancing_service = ModelLoadBalancingService() is_load_balancing_enabled, load_balancing_configs = model_load_balancing_service.get_load_balancing_configs( tenant_id=tenant_id, provider=provider, - model=args.model, - model_type=args.model_type, + model=req_data.model, + model_type=req_data.model_type, session=db.session(), - config_from=args.config_from or "", + config_from=req_data.config_from or "", ) - if args.config_from == "predefined-model": + if req_data.config_from == "predefined-model": # Only the predefined-model branch needs visibility filtering by user. # The account is injected once by the handler and only passed into the # service branch that needs user-scoped credential visibility. @@ -387,8 +387,8 @@ def get(self, tenant_id: str, user: Account, provider: str): available_credentials = model_provider_service.get_provider_model_available_credentials( tenant_id=tenant_id, provider=provider, - model_type=args.model_type, - model=args.model, + model_type=req_data.model_type, + model=req_data.model, ) credentials: dict[str, Any] = {} @@ -414,8 +414,8 @@ def get(self, tenant_id: str, user: Account, provider: str): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, tenant_id: str, provider: str): - args = ParserCreateCredential.model_validate(console_ns.payload) + @model_validate(ParserCreateCredential) + def post(self, req_data: ParserCreateCredential, tenant_id: str, provider: str): model_provider_service = ModelProviderService() @@ -423,17 +423,17 @@ def post(self, tenant_id: str, provider: str): model_provider_service.create_model_credential( tenant_id=tenant_id, provider=provider, - model=args.model, - model_type=args.model_type, - credentials=args.credentials, - credential_name=args.name, + model=req_data.model, + model_type=req_data.model_type, + credentials=req_data.credentials, + credential_name=req_data.name, ) except CredentialsValidateFailedError as ex: logger.exception( "Failed to save model credentials, tenant_id: %s, model: %s, model_type: %s", tenant_id, - args.model, - args.model_type, + req_data.model, + req_data.model_type, ) raise ValueError(str(ex)) @@ -447,8 +447,8 @@ def post(self, tenant_id: str, provider: str): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) @account_initialization_required @with_current_tenant_id - def put(self, current_tenant_id: str, provider: str): - args = ParserUpdateCredential.model_validate(console_ns.payload) + @model_validate(ParserUpdateCredential) + def put(self, req_data: ParserUpdateCredential, current_tenant_id: str, provider: str): model_provider_service = ModelProviderService() @@ -456,11 +456,11 @@ def put(self, current_tenant_id: str, provider: str): model_provider_service.update_model_credential( tenant_id=current_tenant_id, provider=provider, - model_type=args.model_type, - model=args.model, - credentials=args.credentials, - credential_id=args.credential_id, - credential_name=args.name, + model_type=req_data.model_type, + model=req_data.model, + credentials=req_data.credentials, + credential_id=req_data.credential_id, + credential_name=req_data.name, ) except CredentialsValidateFailedError as ex: raise ValueError(str(ex)) @@ -475,16 +475,16 @@ def put(self, current_tenant_id: str, provider: str): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) @account_initialization_required @with_current_tenant_id - def delete(self, current_tenant_id: str, provider: str): - args = ParserDeleteCredential.model_validate(console_ns.payload) + @model_validate(ParserDeleteCredential) + def delete(self, req_data: ParserDeleteCredential, current_tenant_id: str, provider: str): model_provider_service = ModelProviderService() model_provider_service.remove_model_credential( tenant_id=current_tenant_id, provider=provider, - model_type=args.model_type, - model=args.model, - credential_id=args.credential_id, + model_type=req_data.model_type, + model=req_data.model, + credential_id=req_data.credential_id, ) return "", 204 @@ -500,16 +500,16 @@ class ModelProviderModelCredentialSwitchApi(Resource): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, current_tenant_id: str, provider: str): - args = ParserSwitch.model_validate(console_ns.payload) + @model_validate(ParserSwitch) + def post(self, req_data: ParserSwitch, current_tenant_id: str, provider: str): service = ModelProviderService() service.add_model_credential_to_model_list( tenant_id=current_tenant_id, provider=provider, - model_type=args.model_type, - model=args.model, - credential_id=args.credential_id, + model_type=req_data.model_type, + model=req_data.model, + credential_id=req_data.credential_id, ) return SimpleResultResponse(result="success").model_dump(mode="json") @@ -525,12 +525,12 @@ class ModelProviderModelEnableApi(Resource): @account_initialization_required @with_current_tenant_id @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) - def patch(self, tenant_id: str, provider: str): - args = ParserDeleteModels.model_validate(console_ns.payload) + @model_validate(ParserDeleteModels) + def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str): model_provider_service = ModelProviderService() model_provider_service.enable_model( - tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type + tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type ) return SimpleResultResponse(result="success").model_dump(mode="json") @@ -547,12 +547,12 @@ class ModelProviderModelDisableApi(Resource): @account_initialization_required @with_current_tenant_id @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) - def patch(self, tenant_id: str, provider: str): - args = ParserDeleteModels.model_validate(console_ns.payload) + @model_validate(ParserDeleteModels) + def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str): model_provider_service = ModelProviderService() model_provider_service.disable_model( - tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type + tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type ) return SimpleResultResponse(result="success").model_dump(mode="json") @@ -579,8 +579,8 @@ class ModelProviderModelValidateApi(Resource): @login_required @account_initialization_required @with_current_tenant_id - def post(self, tenant_id: str, provider: str): - args = ParserValidate.model_validate(console_ns.payload) + @model_validate(ParserValidate) + def post(self, req_data: ParserValidate, tenant_id: str, provider: str): model_provider_service = ModelProviderService() @@ -591,9 +591,9 @@ def post(self, tenant_id: str, provider: str): model_provider_service.validate_model_credentials( tenant_id=tenant_id, provider=provider, - model=args.model, - model_type=args.model_type, - credentials=args.credentials, + model=req_data.model, + model_type=req_data.model_type, + credentials=req_data.credentials, ) except CredentialsValidateFailedError as ex: result = False @@ -617,12 +617,12 @@ class ModelProviderModelParameterRuleApi(Resource): @login_required @account_initialization_required @with_current_tenant_id - def get(self, tenant_id: str, provider: str): - args = ParserParameter.model_validate(request.args.to_dict(flat=True)) + @model_validate(ParserParameter) + def get(self, req_data: ParserParameter, tenant_id: str, provider: str): model_provider_service = ModelProviderService() parameter_rules = model_provider_service.get_model_parameter_rules( - tenant_id=tenant_id, provider=provider, model=args.model + tenant_id=tenant_id, provider=provider, model=req_data.model ) return ModelParameterRuleListResponse(data=parameter_rules).model_dump(mode="json") diff --git a/api/controllers/console/workspace/rbac.py b/api/controllers/console/workspace/rbac.py index 3a0f2fa60829b3..f446b56fc585d3 100644 --- a/api/controllers/console/workspace/rbac.py +++ b/api/controllers/console/workspace/rbac.py @@ -11,7 +11,7 @@ from configs import dify_config from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns -from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required +from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required from core.db.session_factory import session_factory from core.rbac import RBACResourceWhitelistScope from extensions.ext_database import db @@ -310,19 +310,19 @@ class RBACRolesApi(Resource): RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False ) @console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__]) - def get(self): + @model_validate(_RolesListQuery) + def get(self, req_data: _RolesListQuery): tenant_id, account_id = _current_ids() - query = _RolesListQuery.model_validate(request.args.to_dict(flat=True)) - options = query.to_inner_options() + options = req_data.to_inner_options() if not dify_config.RBAC_ENABLED: result = _legacy_workspace_roles( - options, include_owner=query.include_owner, billing_enabled=dify_config.BILLING_ENABLED + options, include_owner=req_data.include_owner, billing_enabled=dify_config.BILLING_ENABLED ) else: result = svc.RBACService.Roles.list( tenant_id, account_id, - include_owner=query.include_owner, + include_owner=req_data.include_owner, biiling_enabled=dify_config.BILLING_ENABLED, options=options, ) diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index b228d536aa57b1..8b329e135366a4 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -39,6 +39,7 @@ account_initialization_required, edit_permission_required, is_admin_or_owner_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -233,13 +234,12 @@ class TriggerSubscriptionBuilderCreateApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - def post(self, tenant_id: str, user: Account, provider: str): + @model_validate(TriggerSubscriptionBuilderCreatePayload) + def post(self, req_data: TriggerSubscriptionBuilderCreatePayload, tenant_id: str, user: Account, provider: str): """Add a new subscription instance for a trigger provider""" - payload = TriggerSubscriptionBuilderCreatePayload.model_validate(console_ns.payload or {}) - try: - credential_type = CredentialType.of(payload.credential_type) + credential_type = CredentialType.of(req_data.credential_type) subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder( tenant_id=tenant_id, user_id=user.id, @@ -298,11 +298,17 @@ class TriggerSubscriptionBuilderVerifyApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str): + @model_validate(TriggerSubscriptionBuilderVerifyPayload) + def post( + self, + req_data: TriggerSubscriptionBuilderVerifyPayload, + tenant_id: str, + user: Account, + provider: str, + subscription_builder_id: str, + ): """Verify and update a subscription instance for a trigger provider""" - payload = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {}) - try: # Use atomic update_and_verify to prevent race conditions result = TriggerSubscriptionBuilderService.update_and_verify_builder( @@ -311,7 +317,7 @@ def post(self, tenant_id: str, user: Account, provider: str, subscription_builde provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, subscription_builder_updater=SubscriptionBuilderUpdater( - credentials=payload.credentials, + credentials=req_data.credentials, ), ) return dump_response(TriggerVerificationResponse, result) @@ -337,10 +343,17 @@ class TriggerSubscriptionBuilderUpdateApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str): + @model_validate(TriggerSubscriptionBuilderUpdatePayload) + def post( + self, + req_data: TriggerSubscriptionBuilderUpdatePayload, + tenant_id: str, + user: Account, + provider: str, + subscription_builder_id: str, + ): """Update a subscription instance for a trigger provider""" - payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) try: return TriggerSubscriptionBuilderService.update_trigger_subscription_builder( tenant_id=tenant_id, @@ -348,10 +361,10 @@ def post(self, tenant_id: str, user: Account, provider: str, subscription_builde provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, subscription_builder_updater=SubscriptionBuilderUpdater( - name=payload.name, - parameters=payload.parameters, - properties=payload.properties, - credentials=payload.credentials, + name=req_data.name, + parameters=req_data.parameters, + properties=req_data.properties, + credentials=req_data.credentials, ), ).model_dump(mode="json") except Exception as e: @@ -406,9 +419,16 @@ class TriggerSubscriptionBuilderBuildApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str): + @model_validate(TriggerSubscriptionBuilderUpdatePayload) + def post( + self, + req_data: TriggerSubscriptionBuilderUpdatePayload, + tenant_id: str, + user: Account, + provider: str, + subscription_builder_id: str, + ): """Build a subscription instance for a trigger provider""" - payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) try: # Use atomic update_and_build to prevent race conditions TriggerSubscriptionBuilderService.update_and_build_builder( @@ -417,9 +437,9 @@ def post(self, tenant_id: str, user: Account, provider: str, subscription_builde provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, subscription_builder_updater=SubscriptionBuilderUpdater( - name=payload.name, - parameters=payload.parameters, - properties=payload.properties, + name=req_data.name, + parameters=req_data.parameters, + properties=req_data.properties, ), ) return SimpleResultResponse(result="success").model_dump(mode="json") @@ -442,11 +462,10 @@ class TriggerSubscriptionUpdateApi(Resource): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, tenant_id: str, subscription_id: str): + @model_validate(TriggerSubscriptionBuilderUpdatePayload) + def post(self, req_data: TriggerSubscriptionBuilderUpdatePayload, tenant_id: str, subscription_id: str): """Update a subscription instance""" - request = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) - subscription = TriggerProviderService.get_subscription_by_id( tenant_id=tenant_id, subscription_id=subscription_id, @@ -458,7 +477,9 @@ def post(self, tenant_id: str, subscription_id: str): try: # For rename only, just update the name - rename = request.name is not None and not any((request.credentials, request.parameters, request.properties)) + rename = req_data.name is not None and not any( + (req_data.credentials, req_data.parameters, req_data.properties) + ) # When credential type is UNAUTHORIZED, it indicates the subscription was manually created # For Manually created subscription, they dont have credentials, parameters # They only have name and properties(which is input by user) @@ -467,8 +488,8 @@ def post(self, tenant_id: str, subscription_id: str): TriggerProviderService.update_trigger_subscription( tenant_id=tenant_id, subscription_id=subscription_id, - name=request.name, - properties=request.properties, + name=req_data.name, + properties=req_data.properties, ) return SimpleResultResponse(result="success").model_dump(mode="json") @@ -476,11 +497,11 @@ def post(self, tenant_id: str, subscription_id: str): # we need to call third party provider(e.g. GitHub) to rebuild the subscription TriggerProviderService.rebuild_trigger_subscription( tenant_id=tenant_id, - name=request.name, + name=req_data.name, provider_id=provider_id, subscription_id=subscription_id, - credentials=request.credentials or subscription.credentials, - parameters=request.parameters or subscription.parameters, + credentials=req_data.credentials or subscription.credentials, + parameters=req_data.parameters or subscription.parameters, ) return SimpleResultResponse(result="success").model_dump(mode="json") except ValueError as e: @@ -740,18 +761,17 @@ def get(self, tenant_id: str, provider: str): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, tenant_id: str, provider: str): + @model_validate(TriggerOAuthClientPayload) + def post(self, req_data: TriggerOAuthClientPayload, tenant_id: str, provider: str): """Configure custom OAuth client for a provider""" - payload = TriggerOAuthClientPayload.model_validate(console_ns.payload or {}) - try: provider_id = TriggerProviderID(provider) result = TriggerProviderService.save_custom_oauth_client_params( tenant_id=tenant_id, provider_id=provider_id, - client_params=payload.client_params, - enabled=payload.enabled, + client_params=req_data.client_params, + enabled=req_data.enabled, ) return dump_response(SimpleResultResponse, result) @@ -805,18 +825,24 @@ class TriggerSubscriptionVerifyApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - def post(self, tenant_id: str, user: Account, provider: str, subscription_id: str): + @model_validate(TriggerSubscriptionBuilderVerifyPayload) + def post( + self, + req_data: TriggerSubscriptionBuilderVerifyPayload, + tenant_id: str, + user: Account, + provider: str, + subscription_id: str, + ): """Verify credentials for an existing subscription (edit mode only)""" - verify_request = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {}) - try: result = TriggerProviderService.verify_subscription_credentials( tenant_id=tenant_id, user_id=user.id, provider_id=TriggerProviderID(provider), subscription_id=subscription_id, - credentials=verify_request.credentials, + credentials=req_data.credentials, ) return dump_response(TriggerVerificationResponse, result) except ValueError as e: diff --git a/api/tests/unit_tests/controllers/console/workspace/test_accounts.py b/api/tests/unit_tests/controllers/console/workspace/test_accounts.py index 7b0606d90c8eaf..6abc1f0379ea4f 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_accounts.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_accounts.py @@ -16,6 +16,7 @@ from controllers.console.error import AccountInFreezeError from controllers.console.workspace.account import ( AccountAvatarApi, + AccountAvatarQuery, AccountDeleteApi, AccountDeleteVerifyApi, AccountInitApi, @@ -216,7 +217,7 @@ def test_get_avatar_signed_url_when_upload_owned_by_current_account(self, app: F return_value="https://signed/example", ) as sign_mock, ): - result = method(api, user) + result = method(api, AccountAvatarQuery(avatar=file_id), user) assert result == {"avatar_url": "https://signed/example"} sign_mock.assert_called_once_with(upload_file_id=file_id) @@ -256,7 +257,7 @@ def test_get_avatar_not_found_when_upload_created_by_other_account_same_tenant( ) as sign_mock, ): with pytest.raises(NotFound): - method(api, user) + method(api, AccountAvatarQuery(avatar=file_id), user) sign_mock.assert_not_called() @@ -289,7 +290,7 @@ def test_get_avatar_signed_url_when_upload_owned_by_current_account_in_other_ten return_value="https://signed/example", ) as sign_mock, ): - result = method(api, user) + result = method(api, AccountAvatarQuery(avatar=file_id), user) assert result == {"avatar_url": "https://signed/example"} sign_mock.assert_called_once_with(upload_file_id=file_id) @@ -308,7 +309,7 @@ def test_get_avatar_https_pass_through_without_signing(self, app: Flask): return_value="https://signed/should-not-use", ) as sign_mock, ): - result = method(api, user) + result = method(api, AccountAvatarQuery(avatar=external), user) assert result == {"avatar_url": external} sign_mock.assert_not_called() diff --git a/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py b/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py index c3369342ea6ce1..a34349710ea401 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py @@ -17,7 +17,9 @@ EndpointIdPayload, EndpointItemApi, EndpointListApi, + EndpointListForPluginQuery, EndpointListForSinglePluginApi, + EndpointListQuery, EndpointUpdatePayload, LegacyEndpointUpdatePayload, ) @@ -146,7 +148,7 @@ def test_list_success(self, app: Flask): return_value=[endpoint_entity], ), ): - result = method(api, "t1", "u1") + result = method(api, EndpointListQuery(page=1, page_size=10), "t1", "u1") endpoint = result["endpoints"][0] assert endpoint["id"] == "e1" @@ -180,7 +182,7 @@ def test_list_invalid_query(self, app: Flask): app.test_request_context("/?page=0&page_size=10"), ): with pytest.raises(ValueError): - method(api, "t1", "u1") + method(api, EndpointListQuery(page=0, page_size=10), "t1", "u1") class TestEndpointListForSinglePluginApi: @@ -195,7 +197,7 @@ def test_list_for_plugin_success(self, app: Flask): return_value=[_endpoint_entity()], ), ): - result = method(api, "t1", "u1") + result = method(api, EndpointListForPluginQuery(page=1, page_size=10, plugin_id="p1"), "t1", "u1") assert result["endpoints"][0]["id"] == "e1" assert result["endpoints"][0]["settings"]["api_key"] == "pl********et" @@ -209,7 +211,7 @@ def test_list_for_plugin_missing_param(self, app: Flask): app.test_request_context("/?page=1&page_size=10"), ): with pytest.raises(ValueError): - method(api, "t1", "u1") + method(api, EndpointListForPluginQuery(page=1, page_size=10), "t1", "u1") class TestEndpointItemApi: diff --git a/api/tests/unit_tests/controllers/console/workspace/test_models.py b/api/tests/unit_tests/controllers/console/workspace/test_models.py index 424c1ef1d75466..dd0718f33bcad9 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_models.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_models.py @@ -15,6 +15,16 @@ ModelProviderModelEnableApi, ModelProviderModelParameterRuleApi, ModelProviderModelValidateApi, + ParserCreateCredential, + ParserDeleteCredential, + ParserDeleteModels, + ParserGetCredentials, + ParserGetDefault, + ParserParameter, + ParserPostDefault, + ParserPostModels, + ParserSwitch, + ParserValidate, ) from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.errors.validate import CredentialsValidateFailedError @@ -43,7 +53,7 @@ def test_get_success(self, app: Flask): }, } - result = method(api, "tenant1") + result = method(api, ParserGetDefault(model_type=ModelType.LLM), "tenant1") assert "data" in result @@ -65,7 +75,7 @@ def test_post_success(self, app: Flask): app.test_request_context("/", json=payload), patch("controllers.console.workspace.models.ModelProviderService"), ): - result = method(api, "tenant1") + result = method(api, ParserPostDefault.model_validate(payload), "tenant1") assert result["result"] == "success" @@ -79,7 +89,7 @@ def test_get_returns_empty_when_no_default(self, app: Flask): ): service.return_value.get_default_model_of_model_type.return_value = None - result = method(api, "t1") + result = method(api, ParserGetDefault(model_type=ModelType.LLM), "t1") assert "data" in result @@ -117,7 +127,7 @@ def test_post_models_success(self, app: Flask): patch("controllers.console.workspace.models.ModelProviderService"), patch("controllers.console.workspace.models.ModelLoadBalancingService"), ): - result, status = method(api, "tenant1", "openai") + result, status = method(api, ParserPostModels.model_validate(payload), "tenant1", "openai") assert status == 200 @@ -134,7 +144,7 @@ def test_delete_model_success(self, app: Flask): app.test_request_context("/", json=payload), patch("controllers.console.workspace.models.ModelProviderService"), ): - result, status = method(api, "tenant1", "openai") + result, status = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai") assert status == 204 @@ -177,7 +187,13 @@ def test_get_credentials_success(self, app: Flask): provider_service.return_value.provider_manager.get_provider_model_available_credentials.return_value = [] lb_service.return_value.get_load_balancing_configs.return_value = (False, []) - result = method(api, "tenant1", SimpleNamespace(id="u1"), "openai") + result = method( + api, + ParserGetCredentials(model="gpt-4", model_type=ModelType.LLM), + "tenant1", + SimpleNamespace(id="u1"), + "openai", + ) assert "credentials" in result @@ -195,7 +211,7 @@ def test_create_credential_success(self, app: Flask): app.test_request_context("/", json=payload), patch("controllers.console.workspace.models.ModelProviderService"), ): - result, status = method(api, "tenant1", "openai") + result, status = method(api, ParserCreateCredential.model_validate(payload), "tenant1", "openai") assert status == 201 @@ -212,7 +228,13 @@ def test_get_empty_credentials(self, app: Flask): service.return_value.provider_manager.get_provider_model_available_credentials.return_value = [] lb.return_value.get_load_balancing_configs.return_value = (False, []) - result = method(api, "t1", SimpleNamespace(id="u1"), "openai") + result = method( + api, + ParserGetCredentials(model="gpt", model_type=ModelType.LLM), + "t1", + SimpleNamespace(id="u1"), + "openai", + ) assert result["credentials"] == {} @@ -230,7 +252,7 @@ def test_delete_success(self, app: Flask): app.test_request_context("/", json=payload), patch("controllers.console.workspace.models.ModelProviderService"), ): - result, status = method(api, "t1", "openai") + result, status = method(api, ParserDeleteCredential.model_validate(payload), "t1", "openai") assert status == 204 @@ -250,7 +272,7 @@ def test_switch_success(self, app: Flask): app.test_request_context("/", json=payload), patch("controllers.console.workspace.models.ModelProviderService"), ): - result = method(api, "tenant1", "openai") + result = method(api, ParserSwitch.model_validate(payload), "tenant1", "openai") assert result["result"] == "success" @@ -269,7 +291,7 @@ def test_enable_model(self, app: Flask): app.test_request_context("/", json=payload), patch("controllers.console.workspace.models.ModelProviderService"), ): - result = method(api, "tenant1", "openai") + result = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai") assert result["result"] == "success" @@ -286,7 +308,7 @@ def test_disable_model(self, app: Flask): app.test_request_context("/", json=payload), patch("controllers.console.workspace.models.ModelProviderService"), ): - result = method(api, "tenant1", "openai") + result = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai") assert result["result"] == "success" @@ -306,7 +328,7 @@ def test_validate_success(self, app: Flask): app.test_request_context("/", json=payload), patch("controllers.console.workspace.models.ModelProviderService"), ): - result = method(api, "tenant1", "openai") + result = method(api, ParserValidate.model_validate(payload), "tenant1", "openai") assert result["result"] == "success" @@ -327,7 +349,7 @@ def test_validate_failure(self, app: Flask, model_name: str): ): service_mock.return_value.validate_model_credentials.side_effect = CredentialsValidateFailedError("invalid") - result = method(api, "tenant1", "openai") + result = method(api, ParserValidate.model_validate(payload), "tenant1", "openai") assert result["result"] == "error" @@ -343,7 +365,7 @@ def test_parameter_rules(self, app: Flask): ): service_mock.return_value.get_model_parameter_rules.return_value = [] - result = method(api, "tenant1", "openai") + result = method(api, ParserParameter(model="gpt-4"), "tenant1", "openai") assert "data" in result @@ -371,7 +393,7 @@ def test_empty_rules(self, app: Flask): ): service.return_value.get_model_parameter_rules.return_value = [] - result = method(api, "t1", "openai") + result = method(api, ParserParameter(model="gpt"), "t1", "openai") assert result["data"] == [] diff --git a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py index 6508b7cc0dcb0c..b45f6183cbf9bb 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py @@ -26,6 +26,7 @@ from configs import dify_config from controllers.console.workspace import rbac as rbac_mod +from controllers.console.workspace.rbac import _RolesListQuery @pytest.fixture @@ -175,7 +176,10 @@ def test_roles_get_returns_legacy_compatible_roles_when_rbac_disabled(self, app) patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list, ): - response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi()) + response = inspect.unwrap(rbac_mod.RBACRolesApi.get)( + rbac_mod.RBACRolesApi(), + _RolesListQuery.model_validate({"page": 1, "limit": 2, "include_owner": 1}), + ) owner_permission_keys = rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["owner"] valid_owner_permission_keys = [] @@ -230,7 +234,7 @@ def test_roles_get_filters_out_owner_when_include_owner_is_zero(self, app): patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"), ): - response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi()) + response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi(), _RolesListQuery()) names = [r["name"] for r in response["data"]] assert "owner" not in names @@ -242,7 +246,10 @@ def test_roles_get_keeps_owner_when_include_owner_is_one(self, app): patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"), ): - response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi()) + response = inspect.unwrap(rbac_mod.RBACRolesApi.get)( + rbac_mod.RBACRolesApi(), + _RolesListQuery.model_validate({"include_owner": 1}), + ) names = [r["name"] for r in response["data"]] assert "owner" in names @@ -254,7 +261,7 @@ def test_roles_get_filters_out_owner_by_default(self, app): patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"), ): - response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi()) + response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi(), _RolesListQuery()) names = [r["name"] for r in response["data"]] assert "owner" not in names @@ -267,7 +274,10 @@ def test_roles_get_forwards_outer_pagination_params(self, app): patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list, patch("controllers.console.workspace.rbac._dump", return_value={}), ): - inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi()) + inspect.unwrap(rbac_mod.RBACRolesApi.get)( + rbac_mod.RBACRolesApi(), + _RolesListQuery.model_validate({"page": 2, "limit": 50, "reverse": True, "include_owner": 1}), + ) _, kwargs = mock_list.call_args options = kwargs["options"] diff --git a/api/tests/unit_tests/controllers/console/workspace/test_trigger_provider_apis.py b/api/tests/unit_tests/controllers/console/workspace/test_trigger_provider_apis.py index 0dcad28d4929a8..074f8d527c2561 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_trigger_provider_apis.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_trigger_provider_apis.py @@ -15,15 +15,19 @@ TriggerOAuthAuthorizeApi, TriggerOAuthCallbackApi, TriggerOAuthClientManageApi, + TriggerOAuthClientPayload, TriggerProviderIconApi, TriggerProviderInfoApi, TriggerProviderListApi, TriggerSubscriptionBuilderBuildApi, TriggerSubscriptionBuilderCreateApi, + TriggerSubscriptionBuilderCreatePayload, TriggerSubscriptionBuilderGetApi, TriggerSubscriptionBuilderLogsApi, TriggerSubscriptionBuilderUpdateApi, + TriggerSubscriptionBuilderUpdatePayload, TriggerSubscriptionBuilderVerifyApi, + TriggerSubscriptionBuilderVerifyPayload, TriggerSubscriptionListApi, TriggerSubscriptionUpdateApi, TriggerSubscriptionVerifyApi, @@ -163,7 +167,13 @@ def test_create_builder(self, app: Flask) -> None: return_value=subscription_builder(), ), ): - result = method(api, "t1", mock_user(), "github") + result = method( + api, + TriggerSubscriptionBuilderCreatePayload(credential_type="UNAUTHORIZED"), + "t1", + mock_user(), + "github", + ) assert result["subscription_builder"]["id"] == "b1" def test_get_builder(self, app: Flask) -> None: @@ -196,7 +206,14 @@ def test_verify_builder(self, app: Flask) -> None: return_value={"verified": True}, ), ): - assert method(api, "t1", mock_user(), "github", "b1") == {"verified": True} + assert method( + api, + TriggerSubscriptionBuilderVerifyPayload(credentials={"a": 1}), + "t1", + mock_user(), + "github", + "b1", + ) == {"verified": True} def test_verify_builder_error(self, app: Flask) -> None: api = TriggerSubscriptionBuilderVerifyApi() @@ -210,7 +227,14 @@ def test_verify_builder_error(self, app: Flask) -> None: ), ): with pytest.raises(ValueError): - method(api, "t1", mock_user(), "github", "b1") + method( + api, + TriggerSubscriptionBuilderVerifyPayload(credentials={}), + "t1", + mock_user(), + "github", + "b1", + ) def test_update_builder(self, app: Flask) -> None: api = TriggerSubscriptionBuilderUpdateApi() @@ -223,7 +247,17 @@ def test_update_builder(self, app: Flask) -> None: return_value=subscription_builder(), ) as mock_update_builder, ): - assert method(api, "t1", mock_user(), "github", "b1")["id"] == "b1" + assert ( + method( + api, + TriggerSubscriptionBuilderUpdatePayload(name="n"), + "t1", + mock_user(), + "github", + "b1", + )["id"] + == "b1" + ) mock_update_builder.assert_called_once_with( tenant_id="t1", user_id="u1", @@ -263,7 +297,14 @@ def test_build(self, app: Flask) -> None: return_value=None, ), ): - assert method(api, "t1", mock_user(), "github", "b1") == {"result": "success"} + assert method( + api, + TriggerSubscriptionBuilderUpdatePayload(name="x"), + "t1", + mock_user(), + "github", + "b1", + ) == {"result": "success"} class TestTriggerSubscriptionCrud: @@ -283,7 +324,12 @@ def test_update_rename_only(self, app: Flask) -> None: ), patch("controllers.console.workspace.trigger_providers.TriggerProviderService.update_trigger_subscription"), ): - assert method(api, "t1", "s1") == {"result": "success"} + assert method( + api, + TriggerSubscriptionBuilderUpdatePayload(name="x"), + "t1", + "s1", + ) == {"result": "success"} def test_update_not_found(self, app: Flask) -> None: api = TriggerSubscriptionUpdateApi() @@ -297,7 +343,7 @@ def test_update_not_found(self, app: Flask) -> None: ), ): with pytest.raises(NotFoundError): - method(api, "t1", "x") + method(api, TriggerSubscriptionBuilderUpdatePayload(name="x"), "t1", "x") def test_update_rebuild(self, app: Flask) -> None: api = TriggerSubscriptionUpdateApi() @@ -319,7 +365,12 @@ def test_update_rebuild(self, app: Flask) -> None: "controllers.console.workspace.trigger_providers.TriggerProviderService.rebuild_trigger_subscription" ), ): - assert method(api, "t1", "s1") == {"result": "success"} + assert method( + api, + TriggerSubscriptionBuilderUpdatePayload(credentials={}), + "t1", + "s1", + ) == {"result": "success"} class TestTriggerOAuthApis: @@ -499,7 +550,12 @@ def test_post_client(self, app: Flask) -> None: return_value={"result": "success"}, ), ): - assert method(api, "t1", "github") == {"result": "success"} + assert method( + api, + TriggerOAuthClientPayload(enabled=True), + "t1", + "github", + ) == {"result": "success"} def test_delete_client(self, app: Flask) -> None: api = TriggerOAuthClientManageApi() @@ -526,7 +582,7 @@ def test_oauth_client_post_value_error(self, app: Flask) -> None: ), ): with pytest.raises(BadRequest): - method(api, "t1", "github") + method(api, TriggerOAuthClientPayload(enabled=True), "t1", "github") class TestTriggerSubscriptionVerifyApi: @@ -541,7 +597,14 @@ def test_verify_success(self, app: Flask) -> None: return_value={"verified": True}, ), ): - assert method(api, "t1", mock_user(), "github", "s1") == {"verified": True} + assert method( + api, + TriggerSubscriptionBuilderVerifyPayload(credentials={}), + "t1", + mock_user(), + "github", + "s1", + ) == {"verified": True} @pytest.mark.parametrize("raised_exception", [ValueError("bad"), Exception("boom")]) def test_verify_errors(self, app: Flask, raised_exception: Exception) -> None: @@ -556,4 +619,11 @@ def test_verify_errors(self, app: Flask, raised_exception: Exception) -> None: ), ): with pytest.raises(BadRequest): - method(api, "t1", mock_user(), "github", "s1") + method( + api, + TriggerSubscriptionBuilderVerifyPayload(credentials={}), + "t1", + mock_user(), + "github", + "s1", + )