Skip to content

refactor: update workspace manage role usage in permissions - #7838

Merged
wxg0103 merged 1 commit into
v3from
pr@v3@refactor_update_workspace_manage_role
Nov 11, 2026
Merged

wxg0103 merged 1 commit into
v3from
pr@v3@refactor_update_workspace_manage_role

Conversation

@shaohuzhang1

Copy link
Copy Markdown
Contributor

refactor: update workspace manage role usage in permissions

request = self._request(["WORKSPACE_MANAGE"])
with patch("users.views.user.UserManageSerializer.get_user_list", return_value=[]):
with self.assertRaises(AppUnauthorizedFailed):
WorkspaceUserListView().get(request, workspace_id="workspace-1")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here are the issues and suggestions for this test code:

Potential Issues

  1. SimpleNamespace used for query_params — Only .get() is stubbed. If WorkspaceUserListView.get() (or a serializer it calls) iterates, uses __getitem__, keys(), or checks truthiness of query_params, these tests will raise AttributeError. Prefer a real django.http.QueryDict({}, mutable=False) or RequestFactory — it's free and matches production behavior.

  2. SimpleNamespace request instead of a real request object — Bypasses middleware, request.GET, and any attribute access order. If the view ever reads request.method, request.path, or DRF-style attributes, the test breaks for the wrong reason. Consider self.client.get(...) with an overridden auth, or RequestFactory + manual auth injection.

  3. Auth construction assumptions — Test assumes Auth(roles=set, permissions={}) is a valid constructor and that roles are plain strings like "WORKSPACE_MANAGE:w:workspace-1". This couples the test to an internal string format; if role encoding changes (e.g., tuple format), all these tests silently mis-test. Verify against the actual Auth implementation.

  4. Patching a specific call path (users.views.user.UserManageSerializer.get_user_list) — brittle: refactors (renaming class, moving method, renaming imported symbol) break the test without changing behavior. Also note: patching get_user_list on the class means the self argument binding depends on how it's called (class vs instance). Ensure you're not accidentally patching a static/instance-method mismatch — a MagicMock assigned as a class attribute will receive self as first arg unless bound correctly, which can mask bugs.

  5. Assertion on result.status_code == 200 without asserting response content — if the view returns an error payload with status 200 (common in some frameworks returning {"code": ...}), the test passes erroneously. Assert on body/empty list too.

  6. except ... self.fail(...) pattern — fine, but self.fail inside try after catching will re-raise AssertionError; ensure AppUnauthorizedFailed isn't a subclass of Exception hierarchy where self.fail triggers further exception handling. Prefer assertRaises / positive assertion without try/except, or use self.assertNotRaises helper.

Style / Minor

  1. Magical string literal "workspace-1" repeated — define a constant to avoid typo drift between request setup and assert.
  2. No test for mismatched workspace — e.g., role scoped to workspace-2 requesting workspace-1 should be denied. This is the key security boundary; currently untested.
  3. permissions={} empty — no test verifying that added permissions don't bypass scoping.
  4. SimpleTestCase may fail at runtime if DB access occurs (serializer could hit DB); use TestCase if there's any chance of DB queries.
  5. Missing async consideration — if the view supports async, tests should reflect the sync path being called intentionally.
  6. Patch target duplication — extract patch string into a module-level constant or use patch.object.

Suggested Improvements (summary)

  • Use RequestFactory/QueryDict instead of SimpleNamespace.
  • Add negative tests: mismatched workspace ID, missing role, role with wrong suffix.
  • Assert on response data, not just status.
  • Avoid patching deep internals; prefer testing via public API or injecting dependencies.
  • Parameterize the two tests with subTest to reduce duplication.

RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
def get(self, request: Request, workspace_id: str, user_group_id: str, current_page: int, page_size: int):
d = UserGroupListPageSerializer.Query(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review of the diff

Summary

The change replaces RoleConstants.WORKSPACE_MANAGE with RoleConstants.WORKSPACE_MANAGE.get_workspace_role() in all @has_permissions decorators. This looks intentional and consistent across every call site.

Observations / Potential Issues

  1. Method call at decoration time (evaluated once at import)

    • get_workspace_role() is now invoked at class/module import time, not per-request. This is fine only if the role value is static. If it depends on runtime state (workspace, user, settings), this is a bug. Worth confirming the method is pure/static.
    • If WORKSPACE_MANAGE had other attributes consumed elsewhere as an enum-like constant, converting via a method makes call sites less uniform. Consider exposing a separate constant (e.g., RoleConstants.WORKSPACE_ROLE) instead of a method call inside decorators.
  2. Consistency concern

    • Only these permission checks were changed. Grep the codebase for other usages of RoleConstants.WORKSPACE_MANAGE to ensure they don’t also need get_workspace_role(). Mismatched usage can lead to authorization bypass or denial.
  3. Import / side-effect ordering

    • Because decorators are evaluated at import, any circular import or initialization-order issue in RoleConstants.get_workspace_role() will now surface at import time rather than request time. Verify no lazy/DI dependency.
  4. Type / return value

    • Ensure get_workspace_role() returns the same type that has_permissions expects (string, enum, list?). A silent type change could make checks always false or always true.
  5. RoleConstants.ADMIN left unchanged

    • Confirm ADMIN does not have an equivalent scope-dependent form; otherwise this is inconsistent.
  6. Duplication

    • The triple (PermissionConstant.X, RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) is repeated many times. Consider extracting a helper, e.g.:
      WORKSPACE_MANAGE_ROLES = (RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
      then use @has_permissions(PermissionConstants.SYSTEM_USER_GROUP_READ, *WORKSPACE_MANAGE_ROLES). This reduces drift.
  7. Testability

    • Add/adjust tests asserting the decorated endpoints still grant workspace-manage users access and deny others. A silent "always deny" from a wrong return type is easy to miss in code review.
  8. Line length / formatting

    • The multi-line reformat is good; no issues.

Minor nits

  • No docstring/comment explains why get_workspace_role() is required vs. the constant. A brief comment near the first usage would help future readers.
  • Consider making get_workspace_role() a @classmethod or module-level function if it has no instance state, to clarify evaluation semantics.

Overall

The patch is consistent within this file and appears safe provided get_workspace_role() is pure, statically returns the expected role type, and all other call sites using WORKSPACE_MANAGE are updated identically. Recommend grepping the repo and adding authorization tests.

@wxg0103
wxg0103 merged commit 554cba1 into v3 Nov 11, 2026
3 of 4 checks passed
@wxg0103
wxg0103 deleted the pr@v3@refactor_update_workspace_manage_role branch November 11, 2026 09:12
Comment thread apps/users/views/user.py
RoleConstants.EXTENDS_USER.get_workspace_role(),
)
def get(self, request: Request, workspace_id):
nick_name = request.query_params.get("nick_name", None)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here's my review of the diff:

Potential Issues

1. Inconsistent transformation (likely a bug)

Two entries are left untransformed:

  • RoleConstants.ADMIN
  • RoleConstants.EXTENDS_ADMIN

But the similar "manage" and "user" roles got .get_workspace_role() applied. If the intent is that all workspace-scoped roles should be resolved via get_workspace_role(), then ADMIN/EXTENDS_ADMIN should probably be treated consistently (either both transformed or both left as-is). Verify whether ADMIN should also map to its workspace variant — if so, this patch is incomplete.

2. Assumption that get_workspace_role() exists on every constant

You're calling .get_workspace_role() on class attributes without any guard. If RoleConstants.WORKSPACE_MANAGE, EXTENDS_WORKSPACE_MANAGE, USER, or EXTENDS_USER are ever plain strings/enums without that method, this raises AttributeError at import/class-definition time. Confirm these constants are always objects exposing that method.

3. Decorator evaluated at class-load time

Because @has_permissions(...) runs at import, get_workspace_role() is called once per process start. If role definitions can change at runtime (e.g., DB-driven), the permission set will be stale. Probably fine for static config, but worth confirming it isn't intended to be dynamic.

4. Ordering / overlap semantics

After transformation some "extend" and base roles may collapse to the same value (e.g., if EXTENDS_USER.get_workspace_role() == USER.get_workspace_role()). Duplicate permissions passed to has_permissions are usually harmless but can mask logic errors. Worth asserting they remain distinct.

5. Behavioral change may widen/narrow access

If get_workspace_role() maps a global role to a workspace-scoped equivalent, this silently changes who can call GET. Ensure this matches the authorization design and add/extend tests covering each of the five role paths, plus an unauthorized case.

Optimization Suggestions

  • Centralize the mapping: instead of repeating .get_workspace_role() in every call site, define a helper (e.g., a module-level tuple WORKSPACE_USER_LIST_ROLES) or build the list once. This avoids drift like the inconsistent ADMIN handling above.
  • Deduplicate defensively: roles = list(dict.fromkeys(...)) or a set before passing, to guarantee no duplicate permissions.
  • Precompute the full role list at module scope rather than inside the decorator call, so it's obvious and testable.
  • Add a unit test that asserts the exact expected tuple of roles for this view, catching future discrepancies automatically.

Verdict

The change looks directionally correct, but the inconsistent handling of ADMIN/EXTENDS_ADMIN versus the other roles is the main thing to verify — it's either an intentional distinction that deserves a comment, or an oversight.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants