Skip to content

use perms instead of ffShelterOperatorBaOnlyFields (SDB-277) - #2461

Merged
tglaz merged 10 commits into
mainfrom
SDB-277/flag-to-perms
Sep 16, 2026
Merged

tglaz merged 10 commits into
mainfrom
SDB-277/flag-to-perms

Conversation

@tglaz

@tglaz tglaz commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Private Fields - Additional Contacts - BE perms

SDB-277

Summary by Sourcery

Replace feature-flag gating for BA-only additional contacts with global-tier ContactInfo permission checks.

New Features:

  • Add a global-tier permission selector for distinguishing global authority from scoped grants.
  • Control additional-contact visibility and updates through global ContactInfo permissions rather than a feature flag.

Bug Fixes:

  • Prevent users with scoped grants, including scoped ContactInfo roles, from viewing or editing BA-only additional contacts.

Enhancements:

  • Assign ContactInfo permissions to the Global Shelter Operator role while keeping them off the scoped Shelter Operator role.

Tests:

  • Add coverage for global, scoped, granted-scoped, unauthorized, and superuser permission behavior across selectors, services, and GraphQL operations.

Chores:

  • Remove the obsolete additional-contacts feature-flag constant and related flag-based checks.

@tglaz tglaz self-assigned this Sep 10, 2026
@sourcery-ai

sourcery-ai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Replaces ffShelterOperatorBaOnlyFields gating with global-tier ContactInfo permissions: only Global Shelter Operators and superusers can view or edit shelter additional contacts, while scoped and granted-scoped users remain denied; authorization is centralized in the shelter service and covered by selector, GraphQL, and service tests.

Sequence diagram for globally authorized shelter contacts

sequenceDiagram
    participant User
    participant GraphQL as ShelterGraphQL
    participant Service as shelter_update
    participant Permissions as can_globally
    participant Shelter as Shelter

    User->>GraphQL: update_shelter(data)
    GraphQL->>Service: shelter_update(user, data)
    Service->>Permissions: can_globally(user, ContactInfo.perms.CHANGE)
    alt global permission granted
        Permissions-->>Service: true
        Service->>Shelter: update additional_contacts
        Shelter-->>User: updated shelter
    else scoped or unauthorized
        Permissions-->>Service: false
        Service-->>User: PermissionDenied
    end
Loading

Sequence diagram for globally authorized additional-contact reads

sequenceDiagram
    participant User
    participant GraphQL as OperatorShelterType
    participant Permissions as can_globally
    participant Contacts as Shelter.additional_contacts

    User->>GraphQL: additional_contacts
    GraphQL->>Permissions: can_globally(user, ContactInfo.perms.VIEW)
    alt global permission granted
        Permissions-->>GraphQL: true
        GraphQL->>Contacts: all()
        Contacts-->>User: contact list
    else scoped or unauthorized
        Permissions-->>GraphQL: false
        GraphQL-->>User: empty list
    end
Loading

File-Level Changes

Change Details Files
Introduce a global-tier permission selector that excludes scoped grants.
  • Add can_globally based on the global arm of scopes.
  • Test global role, superuser, scoped grant, and unauthorized-user behavior.
apps/betterangels-backend/common/permissions/selectors.py
apps/betterangels-backend/common/tests/test_permissions_selectors.py
Replace feature-flag authorization for additional contacts with Global Shelter Operator permissions.
  • Grant ContactInfo permissions only to the global shelter operator role, not the scoped operator role.
  • Gate GraphQL additional-contact reads with global VIEW permission.
  • Centralize update authorization in shelter_update using global CHANGE permission before shelter lookup.
  • Remove the obsolete feature-flag constant and related imports/checks.
apps/betterangels-backend/shelters/groups.py
apps/betterangels-backend/shelters/schema.py
apps/betterangels-backend/shelters/services/shelter.py
apps/betterangels-backend/shelters/types/outputs.py
apps/betterangels-backend/shelters/constants.py
Expand authorization coverage for additional-contact reads and writes.
  • Verify GSO access and scoped/granted-scoped denial through GraphQL.
  • Verify service-level denial and preserve contact validation/error-shape coverage under GSO authorization.
  • Remove feature-flag overrides from shelter mutation and operator query tests.
apps/betterangels-backend/shelters/tests/test_mutations.py
apps/betterangels-backend/shelters/tests/test_operator_queries.py
apps/betterangels-backend/shelters/tests/test_service_shelter.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vecchp vecchp 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.

Permissions review — global-tier gate is the right call; two deploy items

Verified the mechanism against the grant model and walked every exposure path on this head:

What's right

  • holds_globally is the correct instrument for SDB-277: the requirement is "a scoped grant must never satisfy this", and no scoped predicate can express a negation of its own tier. Worth pinning the near-miss in the docstring, because it's tempting: can_anywhere admits scoped holders (ALL or s.exists()) and would look equivalent while re-opening exactly the hole this closes — any scoped role carrying the ContactInfo perms would pass. Suggested line: "sibling of can_anywhere, which admits scoped holders — use this only when scoped authority must never satisfy the gate."
  • Exposure audit on this head: additional_contacts is update-only (shelters/types/inputs.py:206 — not on the create input), and the only two GraphQL paths — updateShelter (shelters/schema.py:145) and OperatorShelterType.additional_contacts (shelters/types/outputs.py:289) — are both gated. Admin is staff-side. No bypass found.
  • Tests: the error-shape class logging in as GSO (so it exercises contact validation, not the gate) is the right adjustment.

Before deploy

  1. FE still reads the old flaglibs/react/shelter-operator/src/lib/components/AppSidebar/ShelterProfileLinks.tsx:21 gates on FeatureFlags.SO_BA_ONLY_ADMIN_FIELDS. With the BE no longer consulting it, the rollout interleaves: flag ON + scoped operator → FE shows the UI, BE returns [] and denies saves; flag OFF + GSO → FE hides UI the BE would allow. Suggest keeping the flag ON in all environments until the FE moves to permission-based gating (the BaPrivate contacts work, #2449, already carries that TODO) — or landing those together.
  2. Prod membership check — access now keys on a global role carrying the ContactInfo perms (GSO). Worth confirming every current BA user who had access under the flag holds that global role, otherwise it's a silent access loss on deploy.

Nit (non-blocking)

  • can_globally would match the can/can_obj/can_anywhere grammar; holds_globally is fine if the tier-statement name is deliberate.

LGTM otherwise — nice tightening.

@vecchp
vecchp self-requested a review September 11, 2026 00:46
@tglaz
tglaz marked this pull request as ready for review September 11, 2026 00:46

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="apps/betterangels-backend/shelters/services/shelter.py" line_range="297-301" />
<code_context>
     spas_served_ids = data.pop("spas_served_ids", None)

+    # BA-only field: gate on the global tier only — a scoped Grant must never
+    # pass (ADR 0001 §2.4).  Only global Roles carrying the ContactInfo perms
+    # (the Global Shelter Operator) satisfy this.  Checked before the shelter
+    # lookup so an unauthorized caller gets the same refusal whether or not
+    # the shelter exists or is visible.
+    if "additional_contacts" in data and not can_globally(user, ContactInfo.perms.CHANGE):
+        raise PermissionDenied("Editing additional contacts is not allowed with this role.")
+
</code_context>
<issue_to_address>
**nitpick:** The comments/docstrings claim that only the Global Shelter Operator role can pass the additional-contact gate, but `can_globally` also authorizes users with a direct `user_permissions` row for the ContactInfo permission. The documentation therefore falsely describes which users can read or edit this field.

**Suggested fix:** Describe the gate as requiring global-tier ContactInfo permission, including direct user permissions, rather than saying it is GSO-only.
</issue_to_address>

### Comment 2
<location path="apps/betterangels-backend/shelters/groups.py" line_range="58" />
<code_context>
         # path instead (ADR 0001 §2.4).
         ClientProfile.perms.VIEW,
+        # ContactInfo deliberately NOT on the scoped role as it is
+        # currenty used for internal BA users only.
     ],
     invite_html="account/email/shelter_operator_invite.html",
</code_context>
<issue_to_address>
**nitpick:** The newly added comment misspells `currently` as `currenty`, making the permission rationale harder to understand and searchable documentation inconsistent.

**Suggested fix:** Correct `currenty` to `currently`.

```suggestion
        # currently used for internal BA users only.
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread apps/betterangels-backend/shelters/services/shelter.py Outdated
Comment thread apps/betterangels-backend/shelters/groups.py Outdated
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

🚀 Expo continuous deployment is ready for betterangels!

  • Project → betterangels
  • Environment → Preview
  • Platforms → android, ios
  • Scheme → betterangels
  🤖 Android 🍎 iOS
Runtime Version 4d84f3f87051ebb9c1713e7e2d4522e4b23135d9 4d84f3f87051ebb9c1713e7e2d4522e4b23135d9
Build Details Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview
Runtime version: 4d84f3f87051ebb9c1713e7e2d4522e4b23135d9
App version: 1.2.11
Git commit: a1900145e6c2315da5a85930acd4fcf2f8619aa2
Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview
Runtime version: 4d84f3f87051ebb9c1713e7e2d4522e4b23135d9
App version: 1.2.11
Git commit: a1900145e6c2315da5a85930acd4fcf2f8619aa2
Update Details Update Permalink
DetailsBranch: SDB-277-flag-to-perms
Runtime version: 4d84f3f87051ebb9c1713e7e2d4522e4b23135d9
Git commit: 1a10cd0768b5330c86a97c4cb9b57200e585610b
Update Permalink
DetailsBranch: SDB-277-flag-to-perms
Runtime version: 4d84f3f87051ebb9c1713e7e2d4522e4b23135d9
Git commit: 1a10cd0768b5330c86a97c4cb9b57200e585610b
Update QR

iOS Simulator Build: Simulator Build Link

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

🔍 [betterangels-admin] Preview available at: https://admin.dev.betterangels.la/branches/SDB-277-flag-to-perms

Last updated: 2026-09-15T21:33:15.373Z

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

🔍 [shelter-web] Preview available at: https://shelter.dev.betterangels.la/branches/SDB-277-flag-to-perms

Last updated: 2026-09-15T21:33:15.370Z

@shiozakiyuko
shiozakiyuko self-requested a review September 14, 2026 16:52
@tglaz
tglaz added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit d9a7283 Sep 16, 2026
7 checks passed
@tglaz
tglaz deleted the SDB-277/flag-to-perms branch September 16, 2026 17:23
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.

3 participants