Skip to content

fix(api): keep workspace owner consistent on ownership transfer - #41267

Open
GareArc wants to merge 3 commits into
mainfrom
fix/owner-transfer-demote-to-normal
Open

fix(api): keep workspace owner consistent on ownership transfer#41267
GareArc wants to merge 3 commits into
mainfrom
fix/owner-transfer-demote-to-normal

Conversation

@GareArc

@GareArc GareArc commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Transferring workspace ownership from the console left the workspace in an inconsistent state, in two ways:

  1. The outgoing owner was demoted to admin, while the enterprise dashboard's equivalent action demotes them to a restricted member.
  2. With RBAC_ENABLED, the transfer only rebound the RBAC role and never wrote tenant_account_joins.role, so anything reading that column — the enterprise dashboard's workspace list/detail, and TenantService.is_owner, which guards the transfer endpoint itself — kept resolving to the previous owner.

Changes

Who gets demoted, and to what

  • Legacy (RBAC_ENABLED=false): the outgoing owner becomes normal instead of admin. Same correction applied in RBACService.MemberRoles.replace's legacy branch.
  • RBAC mode: the outgoing owner is resolved from RBAC via get_rbac_workspace_owner_account_id rather than from the core DB row, which may be stale. Only the owner role is stripped; any other roles they hold are kept, falling back to no_access (restricted member) when nothing remains. This mirrors rbacclient.TransferOwner in dify-enterprise.

Core owner column

tenant_account_joins.role now moves in both modes: whoever currently holds owner is demoted to normal, and the new owner is promoted. Keying the demotion off "whoever holds the column" rather than the RBAC-resolved account means workspaces already left inconsistent by an earlier RBAC-mode transfer are repaired on the next one, with no migration needed.

Extracted AccountService._resolve_role_id_by_tag from _resolve_legacy_role_id so a builtin role can be resolved by tag (no_access has no TenantAccountRole equivalent).

Testing

  • New parametrized unit test covering both RBAC demotion outcomes (keeps other roles / falls back to no_access) and asserting the core owner column swaps, including from a deliberately stale row.
  • Updated the existing legacy-mode unit and container integration tests for the adminnormal change.
  • tests/unit_tests/services/test_account_service.py, tests/unit_tests/services/enterprise/test_rbac_service.py, tests/unit_tests/controllers/openapi/test_workspaces_members.py: 210 passed.
  • tests/test_containers_integration_tests/services/test_account_service.py -k test_update_member_role: 3 passed.
  • ruff check and ruff format --check clean on all touched files.

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran make lint && make type-check (backend) and vp staged (frontend) to appease the lint gods

Transferring ownership from the console demoted the outgoing owner to
admin, and under RBAC never wrote tenant_account_joins.role at all, so
the enterprise dashboard and TenantService.is_owner kept resolving to the
previous owner.

- Legacy mode: demote the outgoing owner to normal instead of admin.
- RBAC mode: resolve the outgoing owner from RBAC instead of the core DB
  row, strip only the owner role and keep the rest, falling back to
  no_access when nothing remains.
- Move the core owner column in both modes, which also repairs rows left
  stale by earlier RBAC-mode transfers.
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 61.14% 61.14% +0.00%
Strict coverage 60.73% 60.73% +0.00%
Typed symbols 41,940 41,944 +4
Untyped symbols 26,838 26,838 0
Modules 3248 3248 0

The console owner-transfer controller test still asserted the outgoing
owner became admin. Also drop a redundant str() call on Tenant.id, which
is already typed str, flagged by pyrefly.
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.14286% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.70%. Comparing base (1299d99) to head (47463f1).

Files with missing lines Patch % Lines
api/services/account_service.py 55.00% 7 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #41267      +/-   ##
==========================================
- Coverage   86.73%   86.70%   -0.04%     
==========================================
  Files        5429     5272     -157     
  Lines      307710   303573    -4137     
  Branches    61800    60692    -1108     
==========================================
- Hits       266886   263199    -3687     
+ Misses      35567    35118     -449     
+ Partials     5257     5256       -1     
Flag Coverage Δ
api 86.60% <57.14%> (+<0.01%) ⬆️
cli ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@GareArc
GareArc marked this pull request as ready for review August 26, 2026 02:09
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Aug 26, 2026
@GareArc
GareArc requested review from fatelei and a lite review from Copilot August 26, 2026 02:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes inconsistencies in workspace ownership transfer by aligning the demotion behavior of the outgoing owner and ensuring the “core” workspace role column (tenant_account_joins.role) is kept consistent with RBAC ownership transfers.

Changes:

  • Adjusts legacy ownership transfer demotion from adminnormal (including the legacy branch of RBACService.MemberRoles.replace).
  • In RBAC mode, resolves the outgoing owner via RBAC, strips only the owner role while preserving any other roles (fallback to no_access when needed), and repairs stale tenant_account_joins.role ownership rows.
  • Adds/updates unit + container integration tests to cover RBAC demotion behavior and the DB owner-column swap (including stale-row repair).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
api/services/account_service.py Updates ownership transfer logic (RBAC + core DB owner column handling) and role-id resolution helper extraction.
api/services/enterprise/rbac_service.py Aligns legacy demotion behavior in MemberRoles.replace from admin to normal.
api/tests/unit_tests/services/test_account_service.py Adds RBAC-enabled ownership transfer unit coverage (demotion outcomes + stale DB owner repair).
api/tests/unit_tests/services/enterprise/test_rbac_service.py Updates expected legacy demotion behavior in RBAC service unit tests.
api/tests/test_containers_integration_tests/services/test_account_service.py Updates integration assertions for outgoing owner demotion (adminnormal).
api/tests/test_containers_integration_tests/controllers/console/workspace/test_members.py Updates console transfer-owner integration assertion for outgoing owner demotion (adminnormal).
Suppressed comments (1)

api/services/account_service.py:1812

  • In RBAC mode, update_member_role currently resolves the RBAC role ID using TenantAccountRole.OWNER unconditionally, so updating a member to admin/editor/normal would still bind the owner RBAC role. This breaks role updates whenever RBAC_ENABLED is true.

Resolve the RBAC role ID from new_tenant_role instead of hard-coding OWNER.

            resolved_role_id = AccountService._resolve_legacy_role_id(
                tenant_id=str(tenant.id),
                account_id=operator.id,
                role=TenantAccountRole.OWNER,
            )

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants