Merge branch 'main' into feat/refine-snippet-siderbar

This commit is contained in:
JzoNg 2026-06-23 17:32:49 +08:00
commit 5bb1ec77f6
5 changed files with 111 additions and 22 deletions

View File

@ -13,7 +13,7 @@ def valid_password(password):
if re.match(pattern, password) is not None: if re.match(pattern, password) is not None:
return password 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): def hash_password(password_str, salt_byte):

View File

@ -183,7 +183,11 @@ class EnterpriseRequest(BaseRequest):
if account_id: if account_id:
inner_headers[INNER_ACCOUNT_ID_HEADER] = 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") raise ValueError("ENTERPRISE_RBAC_API_URL is required when RBAC_ENABLED=true")
url = f"{cls.rbac_base_url}{endpoint}" url = f"{cls.rbac_base_url}{endpoint}"

View File

@ -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: def _legacy_my_permissions(tenant_id: str, account_id: str | None) -> MyPermissionsResponse:
if not account_id: if not account_id:
return MyPermissionsResponse() return MyPermissionsResponse()
@ -1582,23 +1607,7 @@ class RBACService:
TenantAccountJoin.account_id == member_account_id, TenantAccountJoin.account_id == member_account_id,
) )
) )
return MemberRolesResponse( return _legacy_member_roles_response(tenant_id, member_account_id, role)
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 [],
)
@staticmethod @staticmethod
def batch_get( def batch_get(
@ -1629,6 +1638,36 @@ class RBACService:
member_account_id: str, member_account_id: str,
role_ids: list[str], role_ids: list[str],
) -> MemberRolesResponse: ) -> 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( data = _inner_call(
"PUT", "PUT",
f"{_INNER_PREFIX}/members/rbac-roles", f"{_INNER_PREFIX}/members/rbac-roles",

View File

@ -35,6 +35,13 @@ class TestValidPassword:
with pytest.raises(ValueError): with pytest.raises(ValueError):
valid_password("") 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: class TestPasswordHashing:
"""Test password hashing and comparison""" """Test password hashing and comparison"""

View File

@ -745,15 +745,54 @@ class TestMemberRoles:
def test_replace(self, mock_send: MagicMock): def test_replace(self, mock_send: MagicMock):
mock_send.return_value = {"account_id": "acct-2", "roles": []} mock_send.return_value = {"account_id": "acct-2", "roles": []}
svc.RBACService.MemberRoles.replace( with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True):
"tenant-1", "acct-1", "acct-2", role_ids=["workspace.owner", "workspace.editor"] svc.RBACService.MemberRoles.replace(
) "tenant-1", "acct-1", "acct-2", role_ids=["workspace.owner", "workspace.editor"]
)
call = _call_args(mock_send) call = _call_args(mock_send)
assert call.method == "PUT" assert call.method == "PUT"
assert call.endpoint == "/rbac/members/rbac-roles" assert call.endpoint == "/rbac/members/rbac-roles"
assert call.params == {"account_id": "acct-2"} assert call.params == {"account_id": "acct-2"}
assert call.json == {"role_ids": ["workspace.owner", "workspace.editor"]} 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): def test_batch_get(self, mock_send: MagicMock):
mock_send.return_value = { mock_send.return_value = {
"acct-2": [ "acct-2": [