Skip to content

Service-layer auth enforcement: Google sync actions (#419)#223

Merged
peterdrier merged 2 commits intomainfrom
sprint/2026-04-13/batch-3
Apr 13, 2026
Merged

Service-layer auth enforcement: Google sync actions (#419)#223
peterdrier merged 2 commits intomainfrom
sprint/2026-04-13/batch-3

Conversation

@peterdrier
Copy link
Copy Markdown
Owner

Summary

Phase 3b of the authorization transition plan. Pushes authorization checks into IGoogleSyncService and ISyncSettingsService so external Google Workspace API calls can never be made without a prior privilege check, regardless of call path. Mirrors the nobodies-collective#418 precedent exactly (resource-based handler + ClaimsPrincipal threaded through service methods, SystemPrincipal for jobs).

Automated by /execute-sprint batch 3.

Design

  • New GoogleSyncOperationRequirement with two operation classes: Preview (read-only Google API — list groups, check drift) and Execute (mutations — add/remove members, provision, remediate).
  • New GoogleSyncAuthorizationHandler:
    • SystemPrincipal → always succeeds (background jobs).
    • Admin → always succeeds for both Preview and Execute.
    • TeamsAdmin/Board → Preview-only (they can view the sync dashboard but not mutate).
    • Everyone else → denied.
  • Every IGoogleSyncService method with external side effects now takes a ClaimsPrincipal and calls IAuthorizationService.AuthorizeAsync at the top of the method, before touching any Google client. Unauthorized callers raise UnauthorizedAccessException.
  • SyncResourcesByTypeAsync and SyncSingleResourceAsync pick their requirement from the SyncAction argument so a Preview action only needs Preview privilege.
  • ISyncSettingsService.UpdateModeAsync is also guarded, since sync settings gate every downstream sync job.
  • GoogleAdminService.LinkGroupToTeamAsync and TeamResourceService.SetRestrictInheritedAccessAsync accept and forward a ClaimsPrincipal so the chain stays typed end-to-end.
  • All background jobs (SystemTeamSyncJob, GoogleResourceReconciliationJob, GoogleResourceProvisionJob, ProcessGoogleSyncOutboxJob, SuspendNonCompliantMembersJob) pass SystemPrincipal.Instance.
  • Controllers still carry their [Authorize(Policy = PolicyNames.AdminOnly)] (and analogous) attributes — defense in depth, per .claude/DESIGN_RULES.md.

Acceptance criteria

  • Google sync service methods enforce authorization internally
  • External Google API calls never made without prior auth check (guard runs before any await Get*ServiceAsync() / HTTP call)
  • Background sync jobs use system-level context (SystemPrincipal.Instance)
  • Unit tests verify unauthorized callers throw before reaching the Google API
  • Controllers still authorize before calling the service (attributes preserved on every action)

Tests

  • GoogleSyncAuthorizationHandlerTests — 18 cases covering SystemPrincipal, Admin, TeamsAdmin/Board Preview-only, and denial matrix for every other admin role.
  • GoogleWorkspaceSyncServiceTests — 21 cases: one per guarded public method asserting UnauthorizedAccessException is thrown for an unprivileged principal and that IAuthorizationService.AuthorizeAsync is invoked exactly once; Preview-vs-Execute requirement selection; null-principal ArgumentNullException.
  • SyncSettingsServiceTests — new ThrowsUnauthorized_WhenAuthorizationFails case.
  • Existing job and service tests updated for the new signatures.
  • Full Humans.Application.Tests suite: 997 passed, 0 failed.
  • Integration tests pre-existing Docker/testcontainers infra failures, unrelated to this change.

Test plan

  • Smoke test on preview: admin can trigger sync; non-admin hitting a direct route gets a 403 (still via controller attribute).
  • Verify scheduled reconciliation and system team sync jobs still run and complete on preview after deploy.
  • Verify sync settings changes by admin still work and are audited.

Closes nobodies-collective#419

🤖 Generated with Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2834de5007

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

try
{
var groupResult = await _googleSyncService.EnsureTeamGroupAsync(id);
var groupResult = await _googleSyncService.EnsureTeamGroupAsync(id, User);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid execute-only sync call in TeamsAdmin edit path

This action is still authorized for TeamsAdminBoardOrAdmin, but the new EnsureTeamGroupAsync(id, User) call now requires the Execute requirement (Admin/system only), so TeamsAdmin/Board edits deterministically throw UnauthorizedAccessException before any Google-group logic runs. In EditTeam, that exception is caught after UpdateTeamAsync has already persisted changes, so non-admin editors now get a spurious failure on every edit and prefix changes can be saved without the corresponding group sync being applied.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed — fixed in 19cabd9.

The handler was too coarse: it had only Preview (read-only) and Execute (everything else, Admin+system only). Per the docs/sections/Teams.md invariant, TeamsAdmin is explicitly authorized to "link/unlink Google resources on all teams" but cannot "execute sync actions" — so the two responsibilities needed separate tiers.

Changes:

  • Added a TeamResource requirement that TeamsAdmin + Board + Admin + system can satisfy
  • Moved EnsureTeamGroupAsync and ProvisionTeamGroupAsync (called internally by the former) to use TeamResource
  • Execute stays Admin/system-only for workspace-wide sync actions (SyncResourcesByTypeAsync, UpdateDriveFolderPathsAsync, RemediateGroupSettingsAsync, etc.)
  • Added 5 handler tests for the new tier (TeamsAdmin/Board/Admin/system allowed, other admin roles denied)

The EditTeam and CreateTeam paths now satisfy the service-layer auth check for TeamsAdmin/Board editors, so the UpdateTeamAsync-then-catch silent drop you flagged no longer happens.

…lective#419)

Push authorization into IGoogleSyncService and ISyncSettingsService so
external Google Workspace API calls can never be made without a prior
privilege check, regardless of call path. Mirrors the nobodies-collective#418 precedent.

- New GoogleSyncOperationRequirement (Preview / Execute) with
  GoogleSyncAuthorizationHandler — Admin and SystemPrincipal always
  succeed; TeamsAdmin/Board are allowed Preview-only; everyone else
  is denied.
- Every IGoogleSyncService method with external side effects now
  accepts ClaimsPrincipal and calls IAuthorizationService.AuthorizeAsync
  before any Google SDK call. Unauthorized callers raise
  UnauthorizedAccessException.
- Preview actions (SyncResourcesByTypeAsync Preview, CheckGroupSettings,
  GetEmailMismatches, GetAllDomainGroups) require Preview; mutating
  actions require Execute. SyncResourcesByTypeAsync and
  SyncSingleResourceAsync pick the requirement based on SyncAction.
- ISyncSettingsService.UpdateModeAsync is similarly guarded since it
  gates all downstream sync behavior.
- Background jobs (SystemTeamSyncJob, GoogleResourceReconciliationJob,
  GoogleResourceProvisionJob, ProcessGoogleSyncOutboxJob,
  SuspendNonCompliantMembersJob) now pass SystemPrincipal.Instance.
- Controllers (GoogleController, TeamController, TeamAdminController)
  forward User to the service — [Authorize(Policy = ...)] attributes
  remain as the first-line check (defense in depth).
- GoogleAdminService.LinkGroupToTeamAsync and
  TeamResourceService.SetRestrictInheritedAccessAsync accept and
  forward a ClaimsPrincipal so the chain stays typed end-to-end.
- Stub implementations updated for the new interface shape.

Tests:
- New GoogleSyncAuthorizationHandlerTests covering system override,
  Admin, TeamsAdmin/Board Preview-only, denial matrix.
- New GoogleWorkspaceSyncServiceTests asserting every guarded method
  throws UnauthorizedAccessException for an unprivileged principal
  before touching the Google API, plus Preview-vs-Execute requirement
  selection and a null-principal guard.
- SyncSettingsServiceTests adds a denied-auth throw case.
- Existing job and service tests updated for the new signatures.
- Full Humans.Application.Tests suite passes (997 total). Integration
  test failures are pre-existing Docker/testcontainers infra issues,
  unrelated to this change.

Closes nobodies-collective#419

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@peterdrier peterdrier force-pushed the sprint/2026-04-13/batch-3 branch from 2834de5 to 1b6b162 Compare April 13, 2026 15:26
…ctive#419 review)

Codex P1 finding on PR #223: EditTeam and CreateTeam are authorized
for TeamsAdminBoardOrAdmin, but EnsureTeamGroupAsync now guards on
the Execute requirement which only allows Admin/system. TeamsAdmin
and Board editors get UnauthorizedAccessException on every edit, and
because the exception is caught after UpdateTeamAsync has already
persisted, prefix changes save without the corresponding group sync.

The handler was too coarse. docs/sections/Teams.md invariant splits
these responsibilities:
- TeamsAdmin can "link/unlink Google resources on all teams"
- TeamsAdmin cannot "execute sync actions"

Add a TeamResource requirement tier that sits between Preview and
Execute. The handler allows TeamsAdmin + Board + Admin + system for
TeamResource operations. Execute stays Admin + system only for bulk
sync and reconciliation.

Change EnsureTeamGroupAsync and ProvisionTeamGroupAsync (called by
the former) to use TeamResource. All other Execute-gated methods
(SyncResourcesByType, UpdateDriveFolderPaths, RemediateGroupSettings,
etc.) stay as Execute — those are workspace-wide sync actions.

Added handler tests covering TeamsAdmin, Board, Admin, and system
can manage team resources, and non-Teams admin roles cannot.
@peterdrier peterdrier merged commit 1626098 into main Apr 13, 2026
3 checks passed
@peterdrier peterdrier deleted the sprint/2026-04-13/batch-3 branch April 13, 2026 16:16
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.

Add service-layer auth enforcement: Google sync actions

1 participant