Skip to content

[US-11.9 / OBT-237] Let project managers manage member roles (not other managers) - #92

Open
levigtri wants to merge 5 commits into
mainfrom
levigft/obt-237-us-119-restrict-project-access-role-changes-to-platform
Open

[US-11.9 / OBT-237] Let project managers manage member roles (not other managers)#92
levigtri wants to merge 5 commits into
mainfrom
levigft/obt-237-us-119-restrict-project-access-role-changes-to-platform

Conversation

@levigtri

@levigtri levigtri commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

Reworks project access-role authorization to the 2026-07 role hierarchy: a manager of the project may now manage members — grant a user with a role, promote a member to manager, and revoke a member — but may never demote or remove another manager (that stays platform-admin-only). Platform admins may change or revoke any user's role on any project.

Rebased onto origin/main so it is independent of US-11.6/US-11.8 (previously stacked). is_project_manager is duplicated additively (identical to the copy US-11.8 adds) so both branches stay independent and mergeable in any order.

Changes

  1. Manager checkapp/services/project/is_project_manager.py: true when the user holds the manager role on the project.
  2. Target lookupapp/services/project/get_user_project_access.py: returns a user's access row on a project (or None).
  3. Authorization servicesapp/services/project/assert_can_grant_access.py (admin or project-manager) and app/services/project/assert_can_modify_member_role.py (admin, or project-manager acting on a member; 403 on a manager target, 404 on an unknown target).
  4. Wire the routesapp/api/projects/access.py: grant uses assert_can_grant_access; role update and revoke use assert_can_modify_member_role.
  5. Role enumapp/models/project.py: ProjectGrantUserAccess.role tightened to Literal["member","manager"].
  6. Teststests/test_project_access_roles.py, tests/baker.py (role on make_project_user_access): admin/manager/member × member/manager-target matrix.

Type of Change

  • Feature + business-rule refactor
  • Database migration
  • Breaking change (grant/patch/revoke now return 403 in the new cases)

Testing

  • uv run ruff check . / uv run ruff format --check . — clean.
  • uv run --group dev python -m pytest tests/test_project_access_roles.py tests/test_project_service.py — 37 passed.

Summary by CodeRabbit

  • New Features

    • Strengthened project access control for granting access and updating/revoking member roles.
    • Added role-specific permission helpers so only platform admins or project managers can make sensitive access changes.
    • Tightened project member roles to only member and manager, with enforcement for manager–manager modification rules.
    • Added consistent “not authorized” and “access not found” outcomes.
  • Tests

    • Added async test coverage for manager detection, admin vs member permission boundaries, manager role modification restrictions, and missing access target handling.

@levigtri levigtri self-assigned this Jul 4, 2026
@levigtri
levigtri force-pushed the levigft/obt-237-us-119-restrict-project-access-role-changes-to-platform branch from 71b39f7 to 6fb1756 Compare July 4, 2026 20:34
Rework project access-role authorization to the role hierarchy: a manager
of the project may grant a user with a role, promote a member to manager,
and revoke a member, but may never demote or remove another manager (403);
platform admins may act on anyone. Grant uses assert_can_grant_access;
role-update and revoke use assert_can_modify_member_role. The grant role is
restricted to member/manager.
@levigtri
levigtri force-pushed the levigft/obt-237-us-119-restrict-project-access-role-changes-to-platform branch from 6fb1756 to b425778 Compare July 9, 2026 18:44
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: e9f154e6-4584-469c-b3e9-3e15ab240bae

📥 Commits

Reviewing files that changed from the base of the PR and between 7b7f794 and 77f87b0.

📒 Files selected for processing (1)
  • tests/baker.py
💤 Files with no reviewable changes (1)
  • tests/baker.py

📝 Walkthrough

Walkthrough

Project access endpoints now use manager-specific authorization checks. Project roles are constrained to member or manager, new service helpers implement authorization and access lookup, and tests cover admin, manager, member, and missing-target cases.

Changes

Project access authorization

Layer / File(s) Summary
Access role primitives
app/models/project.py, app/services/project/...
Project grant roles accept only member or manager; service exports, access lookup, and manager-role detection are added.
Authorization rules
app/services/project/assert_can_grant_access.py, app/services/project/assert_can_modify_member_role.py
Platform admins bypass checks, managers can grant or modify member roles, and managers cannot modify another manager’s role.
Endpoint integration and validation
app/api/projects/access.py, tests/baker.py, tests/test_project_access_roles.py
Grant, update, and revoke endpoints use specialized checks, while fixtures and tests cover allowed and rejected role operations.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AccessEndpoints
  participant ProjectAuthorization
  participant ProjectUserAccess
  Client->>AccessEndpoints: grant, update, or revoke access
  AccessEndpoints->>ProjectAuthorization: validate actor and target
  ProjectAuthorization->>ProjectUserAccess: query project role or access
  ProjectUserAccess-->>ProjectAuthorization: access record or no record
  ProjectAuthorization-->>AccessEndpoints: allow or raise authorization error
  AccessEndpoints-->>Client: operation response or error
Loading

Suggested reviewers: henokteixeira, caliridaniel

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: project managers can manage member roles, but not other managers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch levigft/obt-237-us-119-restrict-project-access-role-changes-to-platform

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/services/project/assert_can_grant_access.py`:
- Around line 8-13: Replace the separate assert_can_grant_access and
grant_user_access calls with a single transactional service operation that
performs authorization and grants access atomically. In that service, lock the
actor’s direct-access row before validating manager permissions, while
preserving the platform-admin path; update the route to delegate entirely to
this service and keep database/business-rule orchestration out of app/api.

In `@app/services/project/is_project_manager.py`:
- Line 8: Add a concise docstring to the public is_project_manager function
describing that it determines whether the specified user manages the specified
project and returns a boolean result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 3a9a5f98-7ebe-40a8-b490-7ccae2ce7692

📥 Commits

Reviewing files that changed from the base of the PR and between d669a56 and a51298e.

📒 Files selected for processing (9)
  • app/api/projects/access.py
  • app/models/project.py
  • app/services/project/__init__.py
  • app/services/project/assert_can_grant_access.py
  • app/services/project/assert_can_modify_member_role.py
  • app/services/project/get_user_project_access.py
  • app/services/project/is_project_manager.py
  • tests/baker.py
  • tests/test_project_access_roles.py

Comment on lines +8 to +13
async def assert_can_grant_access(db: AsyncSession, actor: User, project_id: str) -> None:
"""Only a platform admin or a manager of the project may grant project access."""
if actor.is_platform_admin:
return
if not await is_project_manager(db, actor.id, project_id):
raise AuthorizationError("You must be a manager of this project")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Eliminate the authorization TOCTOU before granting access.

The shown route checks this helper and subsequently calls grant_user_access in a separate awaited operation. A manager can be revoked or demoted after Line 12 succeeds but before the grant is committed, allowing a no-longer-authorized actor to grant access. Move authorization and the grant into one transactional service operation and lock the actor’s direct-access row while validating it.

As per coding guidelines, app/api must not orchestrate business rules and database access must live in app/services.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/project/assert_can_grant_access.py` around lines 8 - 13, Replace
the separate assert_can_grant_access and grant_user_access calls with a single
transactional service operation that performs authorization and grants access
atomically. In that service, lock the actor’s direct-access row before
validating manager permissions, while preserving the platform-admin path; update
the route to delegate entirely to this service and keep database/business-rule
orchestration out of app/api.

Source: Coding guidelines

from app.db.models.project import ProjectUserAccess


async def is_project_manager(db: AsyncSession, user_id: str, project_id: str) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document this public service function.

Proposed fix
 async def is_project_manager(db: AsyncSession, user_id: str, project_id: str) -> bool:
+    """Return whether a user has the manager role for a project."""

As per coding guidelines, public service functions need concise docstrings.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def is_project_manager(db: AsyncSession, user_id: str, project_id: str) -> bool:
async def is_project_manager(db: AsyncSession, user_id: str, project_id: str) -> bool:
"""Return whether a user has the manager role for a project."""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/project/is_project_manager.py` at line 8, Add a concise
docstring to the public is_project_manager function describing that it
determines whether the specified user manages the specified project and returns
a boolean result.

Source: Coding guidelines

@levigtri

Copy link
Copy Markdown
Member Author

@coderabbitai — decisions on this review:

  1. assert_can_grant_access.py (merge authorization + grant into a single
    transactional service with a row-level lock on the actor's access row):
    Won't do. The access router deliberately follows an "assert authorization in
    the route, then act in the service" pattern (grant_organization_access,
    update_user_access_role, revoke_user_access all do this). Collapsing only the
    grant path into a single locked service would make it inconsistent with its
    siblings, and the TOCTOU window it guards (actor demoted between check and grant)
    is negligible in practice.

  2. Docstring coverage:
    Won't do. Docstrings are intentionally omitted in this codebase.

  3. PR title check:
    Addressed — title updated to reflect the actual change (project managers may
    manage member roles; only platform admins may act on other managers).

@levigtri levigtri changed the title [US-11.9 / OBT-237] Restrict project access-role updates to platform admins [US-11.9 / OBT-237] Let project managers manage member roles (not other managers) Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@levigtri Thanks for the detailed rationale on each point — noted and agreed:

  1. assert_can_grant_access.py locking suggestion — Makes sense. Keeping the "assert in route, act in service" pattern consistent across grant_organization_access, update_user_access_role, and revoke_user_access is more valuable than closing a negligible TOCTOU window on just one path.
  2. Docstrings — Understood, won't flag this going forward for this codebase.
  3. PR title — Confirmed, the updated title accurately reflects the manager/admin role-modification behavior.

I'll keep these conventions in mind for future reviews in this repo.

(=^・ω・^=) noted, thanks for walking through the reasoning!


✏️ Learnings added
Learnt from: levigtri
URL: https://github.com/shemaobt/tripod-api/pull/92

Timestamp: 2026-07-16T00:32:41.477Z
Learning: In the tripod-api repository, the project access router (app/api/projects/access.py) intentionally follows an "assert authorization in the route, then act in the service" pattern for grant_organization_access, update_user_access_role, and revoke_user_access. Do not suggest merging authorization checks and the action into a single transactional service (e.g., with a row-level lock) for just one of these paths, as that would be inconsistent with the sibling routes; the associated TOCTOU window is considered negligible by the maintainer (levigtri).

Learnt from: levigtri
URL: https://github.com/shemaobt/tripod-api/pull/92

Timestamp: 2026-07-16T00:32:41.477Z
Learning: In the tripod-api repository, docstrings are intentionally omitted throughout the codebase. Do not flag missing docstrings as a review issue.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@levigtri
levigtri requested review from joaocarvoli and removed request for joaocarvoli July 16, 2026 00:53
@levigtri

Copy link
Copy Markdown
Member Author

Follow-up: stripped the existing docstrings from this PR's service files (assert_can_grant_access, assert_can_modify_member_role, get_user_project_access) to match the codebase's no-docstring convention.

@joaocarvoli joaocarvoli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

only a few comments

ProjectUserAccessRoleUpdate,
)
from app.services import project_service
from app.services.project import assert_can_grant_access, assert_can_modify_member_role

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe here you could call these two via project_service. like the rest of the file already does — you exported them on the __init__ in this same PR, and your own tests call them that way. I am not too sure we have direct imports like this on other routers, please verify that and if the namespaced usage is majority adapt this file.

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