chore: compatiable result is none

This commit is contained in:
fatelei 2026-05-09 17:45:35 +08:00
parent 917a9e519e
commit 197f5cd03f
No known key found for this signature in database
GPG Key ID: 2F91DA05646F4EED
4 changed files with 75 additions and 26 deletions

View File

@ -6,7 +6,7 @@ from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationError
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationError, field_validator
from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config
@ -246,6 +246,13 @@ class _ReplaceBindingsRequest(BaseModel):
role_ids: list[str] = []
account_ids: list[str] = []
@field_validator("role_ids", "account_ids", mode="before")
@classmethod
def _coerce_bindings(cls, value: Any) -> list[str]:
if value is None:
return []
return value
@console_ns.route("/workspaces/current/rbac/my-permissions")
class RBACMyPermissionsApi(Resource):
@ -466,6 +473,13 @@ class RBACWorkspaceDatasetMemberBindingsApi(Resource):
class _ReplaceMemberRolesRequest(BaseModel):
role_ids: list[str] = []
@field_validator("role_ids", mode="before")
@classmethod
def _coerce_role_ids(cls, value: Any) -> list[str]:
if value is None:
return []
return value
@console_ns.route("/workspaces/current/rbac/members/<uuid:member_id>/rbac-roles")
class RBACMemberRolesApi(Resource):

View File

@ -113,6 +113,13 @@ class AccessMatrixItem(_RBACModel):
role_ids: list[str] = Field(default_factory=list)
account_ids: list[str] = Field(default_factory=list)
@field_validator("role_ids", "account_ids", mode="before")
@classmethod
def _coerce_empty_lists(cls, value: Any) -> list[str]:
if value is None:
return []
return value
class AppAccessMatrix(_RBACModel):
app_id: str = ""
@ -194,16 +201,37 @@ class AccessPolicyUpdate(_RBACModel):
class ReplaceRoleBindings(_RBACModel):
role_ids: list[str] = Field(default_factory=list)
@field_validator("role_ids", mode="before")
@classmethod
def _coerce_role_ids(cls, value: Any) -> list[str]:
if value is None:
return []
return value
class ReplaceMemberBindings(_RBACModel):
account_ids: list[str] = Field(default_factory=list)
@field_validator("account_ids", mode="before")
@classmethod
def _coerce_account_ids(cls, value: Any) -> list[str]:
if value is None:
return []
return value
class ReplaceBindings(_RBACModel):
role_ids: list[str] = Field(default_factory=list)
account_ids: list[str] = Field(default_factory=list)
@field_validator("role_ids", "account_ids", mode="before")
@classmethod
def _coerce_bindings(cls, value: Any) -> list[str]:
if value is None:
return []
return value
class ListOption(_RBACModel):
page_number: int | None = None

View File

@ -2,11 +2,8 @@
The controllers here are thin: almost every non-trivial behaviour lives in
``services.enterprise.rbac_service`` (covered by its own suite). These tests
therefore focus on the three Flask-layer concerns the service layer cannot
exercise:
therefore focus on the Flask-layer concerns the service layer cannot exercise:
* ``enterprise_only`` rejects community-edition calls with 403 (it is the
outermost decorator, so it fires before any auth middleware).
* ``_current_ids`` raises 404 when the session has no tenant.
* The pydantic request models accept / reject bodies as expected.
@ -25,7 +22,7 @@ from unittest.mock import patch
import pytest
from flask import Flask
from pydantic import ValidationError
from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.exceptions import NotFound
from controllers.console.workspace import rbac as rbac_mod
@ -40,26 +37,6 @@ def app():
def _enabled(enabled: bool):
return patch("controllers.console.workspace.rbac.dify_config.ENTERPRISE_ENABLED", enabled)
class TestEnterpriseGate:
"""``enterprise_only`` is the outermost decorator on every resource, so we
can exercise it directly no auth stubs required.
"""
def test_catalog_forbidden_when_disabled(self, app):
with app.test_request_context("/workspaces/current/rbac/role-permissions/catalog"), _enabled(False):
with pytest.raises(Forbidden):
rbac_mod.RBACWorkspaceCatalogApi().get()
def test_roles_post_forbidden_when_disabled(self, app):
with (
app.test_request_context("/workspaces/current/rbac/roles", method="POST", json={}),
_enabled(False),
):
with pytest.raises(Forbidden):
rbac_mod.RBACRolesApi().post()
class TestCurrentIds:
def test_rejects_missing_tenant(self):
with patch("controllers.console.workspace.rbac.current_account_with_tenant") as mock_user:
@ -115,6 +92,15 @@ class TestPydanticModels:
assert parsed.role_ids == []
assert parsed.account_ids == []
def test_replace_bindings_coerce_null_lists(self):
parsed = rbac_mod._ReplaceBindingsRequest.model_validate({"role_ids": None, "account_ids": None})
assert parsed.role_ids == []
assert parsed.account_ids == []
def test_replace_member_roles_coerce_null_list(self):
parsed = rbac_mod._ReplaceMemberRolesRequest.model_validate({"role_ids": None})
assert parsed.role_ids == []
def test_pagination_query_accepts_page_and_limit_aliases(self):
parsed = rbac_mod._PaginationQuery.model_validate({"page": 3, "limit": 25, "reverse": True})
assert parsed.page_number == 3

View File

@ -252,6 +252,27 @@ class TestWorkspaceAccess:
assert call.endpoint == "/rbac/workspace/datasets/access-policy"
assert call.params is None
def test_workspace_matrix_coerces_null_bindings(self, mock_send: MagicMock):
mock_send.return_value = {
"items": [
{
"policy": {
"id": "policy-1",
"resource_type": "app",
"name": "Workspace App Access",
},
"role_ids": None,
"account_ids": None,
}
],
"pagination": None,
}
out = svc.RBACService.WorkspaceAccess.app_matrix("tenant-1")
assert out.items[0].role_ids == []
assert out.items[0].account_ids == []
def test_workspace_app_replace_bindings(self, mock_send: MagicMock):
mock_send.return_value = {"data": []}
payload = svc.ReplaceBindings(role_ids=["workspace.editor"], account_ids=["acct-2"])