Skip to content

Service-layer auth enforcement for budget mutations (#420)#230

Open
peterdrier wants to merge 2 commits intomainfrom
sprint/20260415/batch-4
Open

Service-layer auth enforcement for budget mutations (#420)#230
peterdrier wants to merge 2 commits intomainfrom
sprint/20260415/batch-4

Conversation

@peterdrier
Copy link
Copy Markdown
Owner

Summary

Phase 3c (final slice) of the first-class authorization transition plan. Budget mutations are now enforced at the service boundary in addition to the existing controller checks, protecting budget writes regardless of call path (controllers, background jobs, future APIs). Follows the pattern established by #210 (role assignment) and the TeamAuthorizationHandler DI-cycle fix from 225ac14.

  • BudgetOperationRequirement moved to Humans.Application.Authorization with a new Manage singleton alongside the existing Edit.
  • New BudgetManageAuthorizationHandler succeeds for Admin / FinanceAdmin / SystemPrincipal on resource-free Manage checks. Existing BudgetAuthorizationHandler still handles Edit against a BudgetCategory resource (department-coordinator scoping), now also allows SystemPrincipal, and lazily resolves IBudgetService via IServiceProvider to avoid a DI cycle now that BudgetService itself takes IAuthorizationService.
  • Every IBudgetService mutation method accepts ClaimsPrincipal and calls IAuthorizationService.AuthorizeAsync before touching the database — unauthorized callers raise UnauthorizedAccessException. Line-item writes authorize against the loaded BudgetCategory (Edit); year/group/category/projection/ticketing-sync writes authorize against null (Manage).
  • BudgetController and FinanceController forward User to the service; the controller-level [Authorize(Policy = FinanceAdminOrAdmin)] and resource-based Edit check stay as defense in depth.
  • TicketingBudgetService / ITicketingBudgetService forward ClaimsPrincipal to BudgetService. TicketingBudgetSyncJob and DevelopmentBudgetSeeder pass SystemPrincipal.Instance.
  • RoleChecks is now an internal helper — part of the Phase 1 cleanup called out in docs/plans/2026-04-03-first-class-authorization-transition.md. ViewPolicies was already removed in earlier phases (no references remain outside that plan doc).

Tests

  • BudgetServiceTests updated for the new ctor, plus new cases covering unauthorized CreateLineItem / UpdateLineItem / DeleteLineItem / CreateYear / UpdateYearStatus / CreateGroup / CreateCategory paths (UnauthorizedAccessException thrown, no DB mutation) and an authorized FinanceAdmin path.
  • BudgetAuthorizationHandlerTests updated for the IServiceProvider constructor and exercises the new SystemPrincipal override plus the "Manage requirement not handled here" guard.
  • New BudgetManageAuthorizationHandlerTests covers Admin / FinanceAdmin / SystemPrincipal / Board (denied) / regular user (denied) / unauthenticated user (denied) / "Edit requirement not handled here" guard.
  • docs/sections/Budget.md gains an Authorization note describing the two-layer defense.

Test plan

  • dotnet build Humans.slnx — clean
  • dotnet test Humans.Application.Tests — 965 passed, 0 failed
  • dotnet format Humans.slnx --verify-no-changes — clean
  • Integration tests (Humans.Integration.Tests) — pre-existing Docker/testcontainers unavailability on this host, unrelated to this change
  • Smoke test /Finance mutations in QA as FinanceAdmin (create/edit/delete year, group, category, line item)
  • Smoke test /Budget department coordinator editing of own-team line items
  • Verify background TicketingBudgetSyncJob runs after deploy (budget audit log should show system actor)

Closes nobodies-collective#420

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

peterdrier and others added 2 commits April 15, 2026 03:46
Promote QA batch: service ownership, dashboard cluster, rota partials
…ive#420)

Phase 3c (final slice) of the first-class authorization transition plan.
Budget mutations are now protected at the service boundary regardless of
call path (controllers, background jobs, future API surfaces), mirroring
the pattern established by #210 (role assignment).

- BudgetOperationRequirement moves to Humans.Application.Authorization
  and gains a resource-free Manage static alongside the existing Edit.
- New BudgetManageAuthorizationHandler succeeds for Admin / FinanceAdmin
  / SystemPrincipal on the Manage requirement (used for budget years,
  groups, categories, ticketing projection parameters, and ticketing
  sync jobs).
- Existing BudgetAuthorizationHandler still handles Edit against a
  BudgetCategory resource (department-coordinator scoping) and now
  succeeds for SystemPrincipal. Uses IServiceProvider to lazily resolve
  IBudgetService, matching TeamAuthorizationHandler so the new
  BudgetService -> IAuthorizationService dependency does not introduce
  a DI cycle.
- Every IBudgetService mutation method now accepts a ClaimsPrincipal
  and calls IAuthorizationService.AuthorizeAsync before touching the
  database. Unauthorized callers raise UnauthorizedAccessException.
  Line-item create/update/delete authorize against the loaded
  BudgetCategory (Edit); year/group/category/projection/sync mutations
  authorize against null (Manage).
- BudgetController and FinanceController forward User to the service;
  the controller-level [Authorize(Policy = FinanceAdminOrAdmin)] and
  resource-based edit check stay as defense in depth.
- TicketingBudgetService and ITicketingBudgetService forward
  ClaimsPrincipal through to BudgetService. TicketingBudgetSyncJob and
  DevelopmentBudgetSeeder pass SystemPrincipal.Instance.
- RoleChecks is now an internal helper — part of the Phase 1 cleanup
  called out in docs/plans/2026-04-03-first-class-authorization-transition.md.
  ViewPolicies was already removed in earlier phases (no references
  remain outside that plan doc).

Tests:
- BudgetServiceTests updated for the new ctor, plus new cases covering
  unauthorized CreateLineItem / UpdateLineItem / DeleteLineItem /
  CreateYear / UpdateYearStatus / CreateGroup / CreateCategory paths
  (UnauthorizedAccessException thrown, no DB mutation) and an
  authorized FinanceAdmin path.
- BudgetAuthorizationHandlerTests updated for the IServiceProvider
  constructor and exercises the new SystemPrincipal override plus the
  "Manage requirement not handled" guard.
- New BudgetManageAuthorizationHandlerTests covers Admin, FinanceAdmin,
  SystemPrincipal, Board (denied), regular user (denied), unauthenticated
  user (denied), and the "Edit requirement not handled" guard.
- docs/sections/Budget.md gains an Authorization note describing the
  two-layer defense.

Closes nobodies-collective#420

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coolify-nuc-humans
Copy link
Copy Markdown

coolify-nuc-humans bot commented Apr 15, 2026

The preview deployment for humans-qa is ready. 🟢

Open Preview | Open Build Logs | Open Application Logs

Last updated at: 2026-04-15 02:32:33 CET

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: budget mutations

1 participant