[AZ-535] [AZ-533] Logout/revocation surface + UAV mission tokens
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status

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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 05:51:23 +03:00
parent 51a293dbcc
commit 8e7c602f51
19 changed files with 1210 additions and 25 deletions
+9
View File
@@ -59,6 +59,15 @@ public enum ExceptionEnum
[Description("Refresh token is invalid, expired, or has been revoked.")]
InvalidRefreshToken = 52,
[Description("Session not found.")]
SessionNotFound = 53,
[Description("Mission token request is invalid.")]
InvalidMissionRequest = 54,
[Description("Aircraft not found or wrong role.")]
AircraftNotFound = 55,
[Description("No file provided.")]
NoFileProvided = 60,
}
+5
View File
@@ -8,5 +8,10 @@ public enum RoleEnum
CompanionPC = 30,
Admin = 40, //
ResourceUploader = 50, //Uploading dll and ai models
// AZ-535 — service-to-service identity (one per verifier: satellite-provider,
// gps-denied, ui). Only authorized to read /sessions/revoked snapshot; not
// valid for any user-facing endpoint. Each verifier deployment gets one
// dedicated Service user.
Service = 60,
ApiAdmin = 1000 //everything
}
+49 -15
View File
@@ -7,23 +7,57 @@ namespace Azaion.Common.Entities;
/// </summary>
public class Session
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string RefreshHash { get; set; } = null!;
public Guid FamilyId { get; set; }
public DateTime IssuedAt { get; set; }
public DateTime LastUsedAt { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime? RevokedAt { get; set; }
public string? RevokedReason { get; set; }
public Guid? ParentSessionId { get; set; }
public DateTime FamilyStartedAt { get; set; }
public Guid Id { get; set; }
public Guid UserId { get; set; }
/// <summary>
/// AZ-531 — sha256(opaque refresh) for interactive sessions. AZ-533 mission
/// sessions have no refresh value and store NULL here.
/// </summary>
public string? RefreshHash { get; set; }
public Guid FamilyId { get; set; }
public DateTime IssuedAt { get; set; }
public DateTime LastUsedAt { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime? RevokedAt { get; set; }
public string? RevokedReason { get; set; }
public Guid? ParentSessionId { get; set; }
public DateTime FamilyStartedAt { get; set; }
/// <summary>
/// AZ-535 — audit trail for who revoked the session (user id of the admin or
/// the user themselves on /logout). Null for system revocations (rotation,
/// reuse detection, post-flight reconnect).
/// </summary>
public Guid? RevokedByUserId { get; set; }
/// <summary>
/// AZ-533 — session class. <see cref="SessionClasses.Interactive"/> is the
/// default refresh-backed interactive session (AZ-531); <see cref="SessionClasses.Mission"/>
/// is a long-lived no-refresh token issued for a single UAV mission.
/// </summary>
public string Class { get; set; } = SessionClasses.Interactive;
/// <summary>
/// AZ-533 — for mission sessions: the aircraft (CompanionPC user) the mission
/// token belongs to. Used by the auto-revoke-on-reconnect middleware. Null for
/// interactive sessions.
/// </summary>
public Guid? AircraftId { get; set; }
}
public static class SessionRevokedReasons
{
public const string Rotated = "rotated";
public const string ReuseDetected = "reuse_detected";
public const string LoggedOut = "logged_out";
public const string FamilyRevoked = "family_revoked";
public const string Rotated = "rotated";
public const string ReuseDetected = "reuse_detected";
public const string LoggedOut = "logged_out";
public const string LoggedOutAll = "logged_out_all";
public const string AdminRevoked = "admin_revoked";
public const string PostFlightReconnect = "post_flight_reconnect";
public const string FamilyRevoked = "family_revoked";
}
public static class SessionClasses
{
public const string Interactive = "interactive";
public const string Mission = "mission";
}
@@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations;
namespace Azaion.Common.Requests;
/// <summary>
/// AZ-533 — body for <c>POST /sessions/mission</c>. Pilot (interactive session)
/// asks admin to mint a long-lived no-refresh token for a single UAV flight.
/// </summary>
public class MissionSessionRequest
{
[Required] public string MissionId { get; set; } = null!;
[Required] public Guid AircraftId { get; set; }
[Required] public double PlannedDurationH { get; set; }
public IList<string>? RequestedScope { get; set; }
/// <summary>
/// Optional bbox of the operating area. Informational until the verifier
/// (satellite-provider) enforces it; included verbatim in the token claim.
/// </summary>
public ValidRegion? ValidRegion { get; set; }
}
public class ValidRegion
{
public double MinLat { get; set; }
public double MinLon { get; set; }
public double MaxLat { get; set; }
public double MaxLon { get; set; }
}
public class MissionSessionResponse
{
public string AccessToken { get; set; } = null!;
public DateTime AccessExp { get; set; }
public string TokenClass { get; set; } = "mission";
public Guid SessionId { get; set; }
}