From b98ba99b236771c03101848a49afb3222e39f4b2 Mon Sep 17 00:00:00 2001 From: ToughGuysDeservePink Date: Tue, 23 Jun 2026 17:00:16 +0800 Subject: [PATCH 1/2] fix: correct misleading password length validation message (#37796) --- api/libs/password.py | 2 +- api/tests/unit_tests/libs/test_password.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/api/libs/password.py b/api/libs/password.py index cdf55c57e5b..3313278492a 100644 --- a/api/libs/password.py +++ b/api/libs/password.py @@ -13,7 +13,7 @@ def valid_password(password): if re.match(pattern, password) is not None: return password - raise ValueError("Password must contain letters and numbers, and the length must be greater than 8.") + raise ValueError("Password must contain letters and numbers, and the length must be at least 8 characters.") def hash_password(password_str, salt_byte): diff --git a/api/tests/unit_tests/libs/test_password.py b/api/tests/unit_tests/libs/test_password.py index 79fc792cc5f..3cdf22e8051 100644 --- a/api/tests/unit_tests/libs/test_password.py +++ b/api/tests/unit_tests/libs/test_password.py @@ -35,6 +35,13 @@ class TestValidPassword: with pytest.raises(ValueError): valid_password("") + def test_should_reject_password_shorter_than_minimum_length(self): + """A 7-character password with letters and numbers is rejected for length.""" + with pytest.raises(ValueError) as exc_info: + valid_password("abc1234") + + assert "at least 8" in str(exc_info.value) + class TestPasswordHashing: """Test password hashing and comparison""" From db9b899321d01591d640f451fbecc2f0f1376a1d Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Tue, 23 Jun 2026 17:08:08 +0800 Subject: [PATCH 2/2] chore: compatiable old role update (#37804) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/services/enterprise/base.py | 6 +- api/services/enterprise/rbac_service.py | 73 ++++++++++++++----- .../services/enterprise/test_rbac_service.py | 45 +++++++++++- 3 files changed, 103 insertions(+), 21 deletions(-) diff --git a/api/services/enterprise/base.py b/api/services/enterprise/base.py index 7ded11a1658..4e441b53af0 100644 --- a/api/services/enterprise/base.py +++ b/api/services/enterprise/base.py @@ -183,7 +183,11 @@ class EnterpriseRequest(BaseRequest): if account_id: inner_headers[INNER_ACCOUNT_ID_HEADER] = account_id - if not cls.base_url.startswith("http") or not cls.base_url.startswith("https") or not cls.base_url: + if ( + not cls.rbac_base_url.startswith("http") + or not cls.rbac_base_url.startswith("https") + or not cls.rbac_base_url + ): raise ValueError("ENTERPRISE_RBAC_API_URL is required when RBAC_ENABLED=true") url = f"{cls.rbac_base_url}{endpoint}" diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index be94925b94a..b5585932b29 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -534,6 +534,31 @@ def _legacy_role_permission_keys(role: TenantAccountRole) -> list[str]: ) +def _legacy_member_roles_response( + tenant_id: str, member_account_id: str, role: TenantAccountRole | str | None +) -> MemberRolesResponse: + if not role: + return MemberRolesResponse(account_id=member_account_id, roles=[]) + + tenant_role = TenantAccountRole(role) + role_value = tenant_role.value + return MemberRolesResponse( + account_id=member_account_id, + roles=[ + RBACRole( + id=role_value, + name=role_value, + description="", + is_builtin=True, + type="", + permission_keys=_legacy_role_permission_keys(tenant_role), + role_tag="owner" if tenant_role == TenantAccountRole.OWNER else role_value, + tenant_id=tenant_id, + ) + ], + ) + + def _legacy_my_permissions(tenant_id: str, account_id: str | None) -> MyPermissionsResponse: if not account_id: return MyPermissionsResponse() @@ -1582,23 +1607,7 @@ class RBACService: TenantAccountJoin.account_id == member_account_id, ) ) - return MemberRolesResponse( - account_id=member_account_id, - roles=[ - RBACRole( - id=role, - name=role, - description="", - is_builtin=True, - type="", - permission_keys=_legacy_role_permission_keys(role), - role_tag="owner" if role == "owner" else role, - tenant_id=tenant_id, - ) - ] - if role - else [], - ) + return _legacy_member_roles_response(tenant_id, member_account_id, role) @staticmethod def batch_get( @@ -1629,6 +1638,36 @@ class RBACService: member_account_id: str, role_ids: list[str], ) -> MemberRolesResponse: + if not dify_config.RBAC_ENABLED: + if len(role_ids) != 1: + raise ValueError("Legacy workspace member role update requires exactly one role.") + + tenant_role = TenantAccountRole(role_ids[0]) + with session_factory.create_session() as session: + target_member_join = session.scalar( + select(TenantAccountJoin).where( + TenantAccountJoin.tenant_id == tenant_id, + TenantAccountJoin.account_id == member_account_id, + ) + ) + if not target_member_join: + raise ValueError("Member not in tenant.") + + if tenant_role == TenantAccountRole.OWNER: + current_owner_join = session.scalar( + select(TenantAccountJoin).where( + TenantAccountJoin.tenant_id == tenant_id, + TenantAccountJoin.role == TenantAccountRole.OWNER, + ) + ) + if current_owner_join and current_owner_join.account_id != member_account_id: + current_owner_join.role = TenantAccountRole.ADMIN + + target_member_join.role = tenant_role + session.commit() + + return _legacy_member_roles_response(tenant_id, member_account_id, tenant_role) + data = _inner_call( "PUT", f"{_INNER_PREFIX}/members/rbac-roles", diff --git a/api/tests/unit_tests/services/enterprise/test_rbac_service.py b/api/tests/unit_tests/services/enterprise/test_rbac_service.py index b43c01778eb..5dc68008840 100644 --- a/api/tests/unit_tests/services/enterprise/test_rbac_service.py +++ b/api/tests/unit_tests/services/enterprise/test_rbac_service.py @@ -745,15 +745,54 @@ class TestMemberRoles: def test_replace(self, mock_send: MagicMock): mock_send.return_value = {"account_id": "acct-2", "roles": []} - svc.RBACService.MemberRoles.replace( - "tenant-1", "acct-1", "acct-2", role_ids=["workspace.owner", "workspace.editor"] - ) + with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True): + svc.RBACService.MemberRoles.replace( + "tenant-1", "acct-1", "acct-2", role_ids=["workspace.owner", "workspace.editor"] + ) call = _call_args(mock_send) assert call.method == "PUT" assert call.endpoint == "/rbac/members/rbac-roles" assert call.params == {"account_id": "acct-2"} assert call.json == {"role_ids": ["workspace.owner", "workspace.editor"]} + def test_replace_updates_legacy_join_role_when_rbac_disabled(self, mock_send: MagicMock): + session = MagicMock() + session.__enter__.return_value = session + target_join = SimpleNamespace(role=svc.TenantAccountRole.NORMAL, account_id="acct-2") + session.scalar.return_value = target_join + + with ( + patch(f"{MODULE}.dify_config.RBAC_ENABLED", False), + patch(f"{MODULE}.session_factory.create_session", return_value=session), + ): + out = svc.RBACService.MemberRoles.replace("tenant-1", "acct-1", "acct-2", role_ids=["editor"]) + + mock_send.assert_not_called() + session.commit.assert_called_once() + assert target_join.role == svc.TenantAccountRole.EDITOR + assert out.account_id == "acct-2" + assert out.roles[0].id == "editor" + assert "app.acl.preview" in out.roles[0].permission_keys + + def test_replace_legacy_owner_demotes_current_owner_when_rbac_disabled(self, mock_send: MagicMock): + session = MagicMock() + session.__enter__.return_value = session + target_join = SimpleNamespace(role=svc.TenantAccountRole.NORMAL, account_id="acct-2") + owner_join = SimpleNamespace(role=svc.TenantAccountRole.OWNER, account_id="acct-owner") + session.scalar.side_effect = [target_join, owner_join] + + with ( + patch(f"{MODULE}.dify_config.RBAC_ENABLED", False), + patch(f"{MODULE}.session_factory.create_session", return_value=session), + ): + out = svc.RBACService.MemberRoles.replace("tenant-1", "acct-1", "acct-2", role_ids=["owner"]) + + mock_send.assert_not_called() + session.commit.assert_called_once() + assert target_join.role == svc.TenantAccountRole.OWNER + assert owner_join.role == svc.TenantAccountRole.ADMIN + assert out.roles[0].id == "owner" + def test_batch_get(self, mock_send: MagicMock): mock_send.return_value = { "acct-2": [