Conversation
…3 slice 2) - Note is OrgScoped (org_via=()) — the anchor for the org-scoped write arm (T2). - CASEWORKER_ROLE carries the note bundle (Note A/V/C/D, ServiceRequest A/V/C/D, Attachment A/V). The TEMPLATE stays ADD+VIEW for now: flipping model-level CHANGE/DELETE before the write cutover makes the legacy guardian prefilter treat them as 'all rows' and over-permits (caught by the notes permissions suite) — they move with the write cutover. - Reads: note field -> HasPerm(VIEW) + can_anywhere_checker; notes list -> NoteType.get_queryset hook (visible_rows_for_holder, memoized); services/service_categories/interaction_authors -> can_anywhere_checker. invalidate_scope_cache drops the new memo. - Query-count pins re-measured (the checker adds the grant lookups). Next (T2): write paths -> can_obj, create payload org + tolerant staging, service-request org reach, parity suite + authz canary. Tests: notes 116 passed; cross-app battery (tasks/teams/clients/gating) 530 passed; ruff clean.
- writable(qs, user, perm): the write rule as a queryset filter. can_obj() now delegates to it, so the single-row check and the queryset filter cannot drift (unsaved rows are no longer writable — fail-closed tightening). - get_writable_or_deny(qs, pk, user, perm, *, message=): the mutation leaf — the fetch *is* the gate (unfetchable means unwritable), one query instead of fetch-then-check, and missing/forbidden share one refusal. - Gating tripwire: writable(/get_writable_or_deny( count as gate markers; notes.schema registered; new scanner test rejects raw get_or_none(<Model>.objects.all(), pk) fetches in Mutation bodies (RAW_FETCH_EXEMPT is the migration ledger; tasks converts in a follow-up). - Selector tests: writable equivalence with can_obj, WRITE_SHARED all-or-none, undeclared platform-shared fail-closed without row-table access, and delegated-org authority answered with only the target passed (no topology at call sites). - Styleguide: authority-through-selectors rules (reads visible/checkers, writes writable/get_writable_or_deny, creates require_can, platform-only fields gate on the global tier — can_anywhere admits scoped holders). - E005 test modernization: the "unscoped model" example is now Attachment (Note has been OrgScoped since the notes cutover; the old premise was stale but the gap was never exercised by the subset battery).
…0003, DEV-2561) T1 (already on this branch): role-back + SHARED reads. This commits the write cutover: - Note is OrgScoped; CASEWORKER_ROLE carries the note bundle; the TEMPLATE moves with the writes (flipping model-level perms before the guardian write paths are gone over-permits via filter_for_user — T1 trap). - Reads SHARED: note field HasPerm(VIEW)+can_anywhere_checker; notes list via visible_rows_for_holder; service catalogs gated on Note ADD. - Writes fetch through the write-scoped selector: update/update_location/ revert/delete via get_writable_or_deny (delete keeps its custom message); service-request create gated on the owning note; service-request delete gated by the org of the note carrying it — detached SRs fail closed. - ServiceRequest is deliberately NOT OrgScoped (its service hop cannot resolve until OrganizationService is org-scoped). Consequence: the scoped role carries no SR or Attachment perms (E005 — scoped RoleDefs may only carry perms on OrgScoped-declared models); the SR gates ride the note and attachments ride the note gate (upload mutations no longer declare the Attachment HasPerm). - Create takes the payload org: CreateNoteInput.organizationId (optional) + resolve_org_or_deny + require_can(ADD). Tolerant staging: pre-payload builds still create via their legacy CASEWORKER group (PermissionError converted to the standard refusal); strict flip lands once the sending build is live (mirrors #2459). - Tests: pins re-measured (each converted gate -1 query; reverts +3 then -1); parity suite notes/tests/test_grant_authorization.py (role-backed, grant- only, legacy-inert, cross-org SHARED reads, org-scoped writes, compat window, detached-SR pin); authz registry canary notes/tests/test_authz_registry.py; schema.graphql regenerated (additive organizationId + directive swaps only).
- NoteEditorScreen: organizationId: activeOrgId via the shared useActiveOrgId hook; submission is skipped when no active org is known (matches the tasks-flow convention: "no active org to anchor the create to"). - Regenerated: libs/ba-platform types (CreateNoteInput.organizationId) and libs/expo/shared/clients schema (directive + input diff).
Contributor
Reviewer's GuideThis PR completes the notes grant-authority cutover by introducing a single write-scoped selector and fetch-as-gate pattern, migrating all note mutation and upload authorization to organization-scoped grants while preserving SHARED reads, adding a staged payload-organization create path with mobile wiring, and covering the behavior with registry, parity, canary, and regression suites. Sequence diagram for organization-scoped note mutation authorizationsequenceDiagram
participant Client
participant Mutation
participant Selector as writable
participant Gate as get_writable_or_deny
participant NoteRecord as Note
participant Service as note_update
Client->>Mutation: update_note(data)
Mutation->>Gate: get_writable_or_deny(Note.objects.all(), id, user, CHANGE)
Gate->>Selector: writable(qs, user, CHANGE)
Selector->>NoteRecord: Filter rows by organization grant
NoteRecord-->>Selector: Writable note or no row
Selector-->>Gate: Scoped queryset
alt note is writable
Gate-->>Mutation: Note
Mutation->>Service: note_update(note, organization)
Service-->>Client: Updated note
else missing or forbidden
Gate-->>Client: PermissionDenied
end
Sequence diagram for payload-organization note creationsequenceDiagram
participant Mobile as Mobile client
participant Mutation as create_note
participant Resolver as resolve_org_or_deny
participant Authority as require_can
participant Service as note_create
participant NoteEntity as Note
Mobile->>Mutation: createNote(organizationId, payload)
Mutation->>Resolver: resolve_org_or_deny(organizationId)
Resolver-->>Mutation: Organization
Mutation->>Authority: require_can(user, ADD, org)
alt grant allows ADD
Authority-->>Mutation: Allowed
Mutation->>Service: note_create(organization, payload)
Service->>NoteEntity: Create organization-linked note
NoteEntity-->>Mobile: Created note
else grant denied
Authority-->>Mobile: PermissionDenied
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
RFC 0003 slice 2 — the notes domain cutover to grant authority (ADR 0001; epic DEV-2551, ticket DEV-2561). Stacks on #2457 (tasks slice) and merges after it.
This PR carries both tranches of the slice:
cd955753, already pushed): role-back + SHARED reads — the CASEWORKER role gains the note bundle;note/notesreads cut over; pins.Design
writable(qs, user, perm)— the write rule as a queryset filter (can_objnow delegates to it; one source of truth) — plus the mutation leafget_writable_or_deny(qs, pk, user, perm): the fetch is the gate, one query instead of fetch-then-check, and missing/forbidden share one refusal.CreateNoteInput.organizationId(optional) +resolve_org_or_deny+require_can(ADD). Tolerant window: pre-payload builds keep creating via their legacy CASEWORKER group; the strict flip lands once the sending build is live (same recipe as feat(perm): task domain grant cutover — slice 1 (RFC 0003, DEV-2561) #2457/feat(perm): tasks createTask requires the payload org — strict flip (hold for mobile adoption) #2459; see docs/api-compatibility.md from docs: API compatibility — staged rollouts for mobile-visible schema changes #2460).servicehop cannot resolve untilOrganizationServiceis org-scoped): SR create rides the owning note; SR delete is gated by the org of the note carrying it; detached SRs fail closed. The SR perms return to the scoped role with the service-catalog cutover.HasPerm(Attachment ADD); the scoped role carries no Attachment/SR perms (E005 — a scoped role may only carry perms of OrgScoped-declared models).Enforcement
common/tests/test_org_mutation_gating.py:notes.schemaregistered;writable(/get_writable_or_deny(count as gate markers; a new scanner test rejects raw write-target fetches in Mutation bodies (exceptions carry reasons — the allowlist is the migration ledger).docs/styleguides/python.md: authority-through-selectors rules, including platform-only (holds_globally) guidance.notes/tests/test_grant_authorization.py(role-backed, grant-only, legacy-inert, cross-org SHARED reads + scoped writes, compat window, detached-SR pin) andnotes/tests/test_authz_registry.py(every Query/Mutation field registered with its route).Staging
organizationId(wired here viauseActiveOrgId).Tests
notes + selectors + gating + parity + canary: 138 green. Cross-app battery (tasks/teams/clients/common/accounts): 1118 passed. ruff + format clean. Mobile: tsc + eslint clean on touched files (vitest runs in CI).
Follow-ups
get_writable_or_deny(drops itsRAW_FETCH_EXEMPTentry; re-pins).PermissionedQuerySet; convert with their slices.Summary by Sourcery
Complete the notes domain cutover from legacy guardian authorization to grant-backed shared reads and organization-scoped writes while preserving compatibility for pre-payload clients.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: