Repository navigation
refactor: update workspace manage role usage in permissions - #7838
Conversation
| 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") |
There was a problem hiding this comment.
Here are the issues and suggestions for this test code:
Potential Issues
-
SimpleNamespaceused forquery_params— Only.get()is stubbed. IfWorkspaceUserListView.get()(or a serializer it calls) iterates, uses__getitem__,keys(), or checks truthiness ofquery_params, these tests will raiseAttributeError. Prefer a realdjango.http.QueryDict({}, mutable=False)orRequestFactory— it's free and matches production behavior. -
SimpleNamespacerequest instead of a real request object — Bypasses middleware,request.GET, and any attribute access order. If the view ever readsrequest.method,request.path, or DRF-style attributes, the test breaks for the wrong reason. Considerself.client.get(...)with an overriddenauth, orRequestFactory+ manualauthinjection. -
Authconstruction assumptions — Test assumesAuth(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 actualAuthimplementation. -
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: patchingget_user_liston the class means theselfargument binding depends on how it's called (class vs instance). Ensure you're not accidentally patching a static/instance-method mismatch — aMagicMockassigned as a class attribute will receiveselfas first arg unless bound correctly, which can mask bugs. -
Assertion on
result.status_code == 200without 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. -
except ... self.fail(...)pattern — fine, butself.failinsidetryafter catching will re-raiseAssertionError; ensureAppUnauthorizedFailedisn't a subclass ofExceptionhierarchy whereself.failtriggers further exception handling. PreferassertRaises/ positive assertion without try/except, or useself.assertNotRaiseshelper.
Style / Minor
- Magical string literal
"workspace-1"repeated — define a constant to avoid typo drift between request setup and assert. - No test for mismatched workspace — e.g., role scoped to
workspace-2requestingworkspace-1should be denied. This is the key security boundary; currently untested. permissions={}empty — no test verifying that added permissions don't bypass scoping.SimpleTestCasemay fail at runtime if DB access occurs (serializer could hit DB); useTestCaseif there's any chance of DB queries.- Missing
asyncconsideration — if the view supports async, tests should reflect the sync path being called intentionally. - Patch target duplication — extract patch string into a module-level constant or use
patch.object.
Suggested Improvements (summary)
- Use
RequestFactory/QueryDictinstead ofSimpleNamespace. - 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
subTestto 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( |
There was a problem hiding this comment.
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
-
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_MANAGEhad 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.
-
Consistency concern
- Only these permission checks were changed. Grep the codebase for other usages of
RoleConstants.WORKSPACE_MANAGEto ensure they don’t also needget_workspace_role(). Mismatched usage can lead to authorization bypass or denial.
- Only these permission checks were changed. Grep the codebase for other usages of
-
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.
- Because decorators are evaluated at import, any circular import or initialization-order issue in
-
Type / return value
- Ensure
get_workspace_role()returns the same type thathas_permissionsexpects (string, enum, list?). A silent type change could make checks always false or always true.
- Ensure
-
RoleConstants.ADMINleft unchanged- Confirm
ADMINdoes not have an equivalent scope-dependent form; otherwise this is inconsistent.
- Confirm
-
Duplication
- The triple
(PermissionConstant.X, RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE.get_workspace_role())is repeated many times. Consider extracting a helper, e.g.:then useWORKSPACE_MANAGE_ROLES = (RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@has_permissions(PermissionConstants.SYSTEM_USER_GROUP_READ, *WORKSPACE_MANAGE_ROLES). This reduces drift.
- The triple
-
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.
-
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@classmethodor 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.
| RoleConstants.EXTENDS_USER.get_workspace_role(), | ||
| ) | ||
| def get(self, request: Request, workspace_id): | ||
| nick_name = request.query_params.get("nick_name", None) |
There was a problem hiding this comment.
Here's my review of the diff:
Potential Issues
1. Inconsistent transformation (likely a bug)
Two entries are left untransformed:
RoleConstants.ADMINRoleConstants.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 tupleWORKSPACE_USER_LIST_ROLES) or build the list once. This avoids drift like the inconsistentADMINhandling above. - Deduplicate defensively:
roles = list(dict.fromkeys(...))or asetbefore 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.
refactor: update workspace manage role usage in permissions