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:
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):

View File

@ -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}"

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:
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",

View File

@ -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"""

View File

@ -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": [