Files
admin/_docs/03_implementation/batch_03_cycle2_report.md
Oleksandr Bezdieniezhnykh 8e7c602f51
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status
[AZ-535] [AZ-533] Logout/revocation surface + UAV mission tokens
AZ-535: POST /logout (caller's session), /logout/all (all sessions for user),
admin POST /sessions/{sid}/revoke, and verifier-only GET /sessions/revoked
snapshot. New Service role gates the snapshot. Idempotent revoke; reason +
revoked_by_user_id audited per row.

AZ-533: POST /sessions/mission mints a long-lived no-refresh ES256 token bound
to one aircraft + one mission. Audience narrowed to satellite-provider, hard
12 h cap, persisted as class='mission' so the existing logout/revoke surface
covers it. Successful CompanionPC /login or /token/refresh auto-revokes that
aircraft's open mission session (post-flight reconnect).

Schema: 09_sessions_logout_and_mission.sql adds revoked_by_user_id, class,
aircraft_id; drops NOT NULL on refresh_hash for mission rows; adds two partial
indexes for the auto-revoke and snapshot hot paths.

Tests: 13 new e2e tests, all green; full suite 75/76 (1 pre-existing flake in
PasswordHashingTests AC5 timing assertion, unrelated to this batch).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 05:51:23 +03:00

7.8 KiB
Raw Permalink Blame History

Batch Report

Batch: 3 (cycle 2) Tasks: AZ-535 (logout_revocation), AZ-533 (mission_token_uav) Date: 2026-05-14 Total Complexity: 8 points (5 + 3) Epic: AZ-529 — Auth Mechanism Modernization

Task Results

Task Status Files Modified Tests AC Coverage Issues
AZ-535 Done 6 source + 1 sql migration + 1 test 7/7 pass 4/4 None
AZ-533 Done 4 source + (shared migration) + 1 test 6/6 pass 4/4 None

Files Touched

Source (production)

  • Azaion.AdminApi/Program.cs — DI for ISessionService/IMissionTokenService; new revocationReaderPolicy (Service|ApiAdmin); new endpoints /logout, /logout/all, /sessions/{sid}/revoke, /sessions/revoked, /sessions/mission; /login + /token/refresh now trigger RevokeMissionsForAircraft when the authenticated user is a CompanionPC
  • Azaion.AdminApi/BusinessExceptionHandler.cs — map SessionNotFound → 404, InvalidMissionRequest / AircraftNotFound → 400
  • Azaion.Common/BusinessException.cs — add SessionNotFound = 53, InvalidMissionRequest = 54, AircraftNotFound = 55
  • Azaion.Common/Entities/Session.cs — add RevokedByUserId, Class, AircraftId; RefreshHash made nullable; SessionRevokedReasons extended with LoggedOutAll, AdminRevoked, PostFlightReconnect; new SessionClasses { Interactive, Mission }
  • Azaion.Common/Entities/RoleEnum.cs — add Service = 60 (verifier identity)
  • Azaion.Common/Requests/MissionSessionRequest.csnew; MissionSessionRequest / MissionSessionResponse / ValidRegion
  • Azaion.Services/SessionService.csnew; RevokeBySid (idempotent), RevokeAllForUser, RevokeMissionsForAircraft, GetRevokedSince (TTL-bounded snapshot)
  • Azaion.Services/MissionTokenService.csnew; mission-id regex + duration bounds + aircraft-role validation; mints ES256 token with mission_id/aircraft_id/token_class/valid_region claims and narrowed aud=satellite-provider; persists session row BEFORE returning token

Migrations / infra

  • env/db/09_sessions_logout_and_mission.sqlnew; ALTER TABLE adds revoked_by_user_id, class, aircraft_id; drops NOT NULL on refresh_hash (mission rows have no refresh value); two partial indexes (sessions_aircraft_active_idx, sessions_revoked_at_idx)
  • e2e/db-init/00_run_all.sh — apply 09_sessions_logout_and_mission.sql in test DB

Tests

  • e2e/Azaion.E2E/Tests/LogoutRevocationTests.csnew; 7 tests (logout idempotent, logout/all, admin revoke-by-sid, non-admin forbidden, service polls snapshot, non-service forbidden)
  • e2e/Azaion.E2E/Tests/MissionTokenTests.csnew; 6 tests (claims+lifetime, mission-id pattern, duration bounds×2, aircraft role, auto-revoke on reconnect)
  • e2e/Azaion.E2E/Helpers/DbHelper.cs — add CountActiveSessionsForUser, CountOpenMissionsForAircraft, GetRevocationInfo, PromoteToService

Test Run Results

Batch 3 only (--filter LogoutRevocationTests|MissionTokenTests): 13 / 13 passed, 22 s. Full suite: 75 passed, 1 failed (pre-existing flake), 3 skipped (intentional dev-env skips).

The single failure was PasswordHashingTests.AC5_Verify_uses_constant_time_comparator_no_obvious_timing_leak. Verified pre-existing by re-running it solo — passes in isolation. The assertion bound (per-length mean spread < 0.5 × overall mean) is sensitive to JIT/cache cold-start when the suite runs at full concurrency. Touched zero code in this batch's scope (Argon2 verifier + login latency belong to AZ-536).

AC Coverage

AZ-535 (4/4)

  • AC-1: POST /logout revokes the caller's session, idempotent — AC1_Logout_revokes_caller_session_and_blocks_refresh + AC1_Logout_is_idempotent
  • AC-2: POST /logout/all revokes every session for the caller — AC2_Logout_all_revokes_every_session_for_the_user
  • AC-3: Admin revoke-by-sid; non-admin forbidden — AC3_Admin_can_revoke_any_session_by_sid + AC3_Non_admin_cannot_revoke_other_sessions
  • AC-4: GET /sessions/revoked?since=… — Service role can read; non-service forbidden — AC4_Verifier_polls_revoked_snapshot_with_service_role + AC4_NonService_user_cannot_read_revoked_snapshot

AZ-533 (4/4)

  • AC-1: Mission token has long lifetime + mission_id/aircraft_id/token_class/aud=satellite-provider claims; sessions row class=mission, aircraft_id set — AC1_Mission_token_carries_required_claims_and_long_lifetime
  • AC-2: Mission ID regex M-YYYY-MM-DD-NNN enforced; planned_duration ∈ [0.1, 12]h — AC2_Mission_id_must_match_pattern + AC2_Planned_duration_must_be_within_bounds(0.05) + (13)
  • AC-3: Aircraft must exist with Role=CompanionPCAC3_Aircraft_must_exist_with_companionpc_role
  • AC-4: Aircraft re-login auto-revokes its open mission session — AC4_Aircraft_login_auto_revokes_open_mission_sessions

Key Implementation Decisions

  1. refresh_hash nullable, not a separate mission_sessions table. Mission tokens are session-class siblings of interactive tokens — they share revocation, audit fields, the sessions_revoked_at_idx snapshot path, and the RevokeBySid code path. Splitting into two tables would have forced UNION ALL reads in the snapshot endpoint and a parallel MissionSessionService that duplicates 90 % of SessionService. Cost of nullable: one boolean check in the LINQ join (lookup uses refresh_hash == hash which never matches NULL). Cost avoided: an entire second persistence path.

  2. Service role separate from CompanionPC. Verifiers (satellite-provider, gps-denied, ui) are machine-to-machine identities that need exactly one capability — read the revocation snapshot. Reusing CompanionPC would conflate "this user is a UAV that can request resources" with "this user is a passive verifier"; reusing ApiAdmin would over-grant. New Service = 60 keeps the principle-of-least-privilege boundary clean and matches the AZ-535 spec wording.

  3. Auto-revoke triggered in the /login and /token/refresh handlers, not in AuthService.CreateToken. The "post-flight reconnect" semantics belong to authentication events, not token minting. Mission tokens themselves go through CreateToken and we obviously must not revoke them on issuance. Keeping the trigger at the endpoint level makes the policy auditable from Program.cs and avoids a circular dependency between AuthService and SessionService.

  4. Snapshot endpoint floors since at now - 12 h. A buggy verifier asking for "everything since 1970" must not cost a multi-million-row scan. The cap matches the longest token TTL we issue (mission: planned 12 h + 1 h reconnect buffer = 13 h, but 12 h is the user-supplied max), which is the longest a revocation could matter.

  5. Persist the mission session row BEFORE minting the token. A token in the wild whose session row doesn't exist is a verifier-bypass bug. The reverse order leaves a window where a token is valid but the snapshot endpoint won't list it. Insert-then-mint closes that window.

  6. MFA gate (amr=["pwd","mfa"]) recorded as a TODO comment. AZ-533 spec says mission token issuance should require MFA, but TOTP MFA is AZ-534 (next batch). The endpoint is currently gated on RequireAuthorization only; the comment in Program.cs makes the dependency explicit so AZ-534's PR will surface this site.

Backward Compatibility

  • Existing sessions rows from AZ-531 keep class='interactive' (default) and aircraft_id=NULL. No data migration needed.
  • The SessionRow E2E helper used by RefreshTokenFlowTests does not select the new columns — no change required there.
  • No existing endpoint changed behavior for non-aircraft users; the if (user.Role == RoleEnum.CompanionPC) guard makes the auto-revoke a no-op for everyone else.