Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/Pam.Domain/Entities/AccessDecision.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Bit.Core.Entities;
using Bit.Core.Utilities;
using Bit.Pam.Enums;

namespace Bit.Pam.Entities;

/// <summary>
/// A single decision on a <see cref="AccessRequest"/>. In v0 there is exactly one decision per request: an automated
/// <see cref="AccessDeciderKind.Automatic"/> verdict for auto-approval, or a <see cref="AccessDeciderKind.Human"/>
/// verdict once approver endpoints land.
/// </summary>
public class AccessDecision : ITableObject<Guid>
{
public Guid Id { get; set; }

/// <summary>
/// The request this decision was made on.
/// </summary>
public Guid AccessRequestId { get; set; }

/// <summary>
/// Discriminates the decision: determines whether <see cref="ApproverId"/> or <see cref="ConditionKind"/> is populated.
/// </summary>
public AccessDeciderKind DeciderKind { get; set; }

/// <summary>
/// The human approver. NULL when <see cref="DeciderKind"/> is <see cref="AccessDeciderKind.Automatic"/>.
/// </summary>
public Guid? ApproverId { get; set; }

/// <summary>
/// The condition kind that decided (e.g. <see cref="AccessConditionKind.IpAllowlist"/>). NULL when
/// <see cref="DeciderKind"/> is <see cref="AccessDeciderKind.Human"/>.
/// </summary>
public AccessConditionKind? ConditionKind { get; set; }

/// <summary>
/// The approve-or-deny outcome recorded by this decision.
/// </summary>
public AccessDecisionVerdict Verdict { get; set; }

/// <summary>
/// Human comment, or a future automatic-evaluation reason string.
/// </summary>
public string? Comment { get; set; }

/// <summary>
/// Forward-compatible snapshot of the inputs the evaluation saw. Null in this slice (no signals are evaluated).
/// </summary>
public string? EvaluationContext { get; set; }

/// <summary>
/// When the decision was recorded, stamped in UTC at construction.
/// </summary>
public DateTime CreationDate { get; set; } = DateTime.UtcNow;

public void SetNewId()
{
Id = CombGuid.Generate();
}
}
61 changes: 61 additions & 0 deletions src/Pam.Domain/Entities/AccessLease.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Bit.Core.Entities;
using Bit.Core.Utilities;
using Bit.Pam.Enums;

namespace Bit.Pam.Entities;

/// <summary>
/// An active grant of access to a cipher, born from an approved <see cref="AccessRequest"/>. Only
/// <see cref="AccessLeaseStatus.Active"/> leases inside their <see cref="NotBefore"/>/<see cref="NotAfter"/> window
/// authorize access.
/// </summary>
public class AccessLease : ITableObject<Guid>
{
public Guid Id { get; set; }

/// <summary>
/// The request that birthed this lease.
/// </summary>
public Guid AccessRequestId { get; set; }

public Guid OrganizationId { get; set; }
public Guid CollectionId { get; set; }
public Guid CipherId { get; set; }
public Guid RequesterId { get; set; }

/// <summary>
/// The lease's position in its lifecycle. Only an <see cref="AccessLeaseStatus.Active"/> lease within its window
/// authorizes access.
/// </summary>
public AccessLeaseStatus Status { get; set; }

/// <summary>
/// The start of the granted access window, carried over from the approved <see cref="AccessRequest"/>.
/// </summary>
public DateTime NotBefore { get; set; }

/// <summary>
/// The end of the granted access window.
/// </summary>
public DateTime NotAfter { get; set; }

/// <summary>
/// Set when an operator revokes the lease (<see cref="AccessLeaseStatus.Revoked"/>). NULL otherwise.
/// </summary>
public DateTime? RevokedDate { get; set; }

/// <summary>
/// The operator who revoked the lease. NULL unless <see cref="Status"/> is <see cref="AccessLeaseStatus.Revoked"/>.
/// </summary>
public Guid? RevokedBy { get; set; }

/// <summary>
/// When the lease was minted, stamped in UTC at construction.
/// </summary>
public DateTime CreationDate { get; set; } = DateTime.UtcNow;

public void SetNewId()
{
Id = CombGuid.Generate();
}
}
72 changes: 72 additions & 0 deletions src/Pam.Domain/Entities/AccessRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Bit.Core.Entities;
using Bit.Core.Utilities;
using Bit.Pam.Enums;

namespace Bit.Pam.Entities;

/// <summary>
/// A request to lease access to a cipher in a leasing-governed collection. Auto-approved requests are created
/// already <see cref="AccessRequestStatus.Approved"/>; requests that require human approval are created
/// <see cref="AccessRequestStatus.Pending"/> and resolved later by an approver. Neither approval mints the lease —
/// the requester activates the approved request within its window, and that activation produces the
/// <see cref="AccessLease"/>.
/// </summary>
public class AccessRequest : ITableObject<Guid>
{
public Guid Id { get; set; }

/// <summary>
/// NULL for original requests. Set only for extension requests, which point at the lease being extended.
/// </summary>
public Guid? ExtensionOfLeaseId { get; set; }

public Guid OrganizationId { get; set; }
public Guid CollectionId { get; set; }
public Guid CipherId { get; set; }
public Guid RequesterId { get; set; }

/// <summary>
/// The access rule that governed this request, resolved once at submit (oldest wins) and pinned here so every
/// downstream operation reads the same rule rather than re-resolving. Null for requests created before pinning
/// existed, or when the cipher was not leasing-gated through a stored rule.
/// </summary>
public Guid? RuleId { get; set; }

/// <summary>
/// The requested access window. For automatic approval this is <c>now</c>; for human approval it is the
/// requester-supplied start.
/// </summary>
public DateTime NotBefore { get; set; }

/// <summary>
/// The end of the requested access window. For automatic approval this is <c>now + duration</c>; for human
/// approval it is the requester-supplied end.
/// </summary>
public DateTime NotAfter { get; set; }

/// <summary>
/// Optional for automatic approval, required for human approval (enforced in the command).
/// </summary>
public string? Reason { get; set; }

/// <summary>
/// The request's position in its lifecycle. Created <see cref="AccessRequestStatus.Pending"/> for human approval or
/// already <see cref="AccessRequestStatus.Approved"/> for automatic approval, then settling in one terminal state.
/// </summary>
public AccessRequestStatus Status { get; set; }

/// <summary>
/// When the request was submitted, stamped in UTC at construction.
/// </summary>
public DateTime CreationDate { get; set; } = DateTime.UtcNow;

/// <summary>
/// Set when the request leaves <see cref="AccessRequestStatus.Pending"/>.
/// </summary>
public DateTime? ResolvedDate { get; set; }

public void SetNewId()
{
Id = CombGuid.Generate();
}
}
18 changes: 18 additions & 0 deletions src/Pam.Domain/Enums/AccessConditionKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Bit.Pam.Enums;

/// <summary>
/// The kind of access condition that produced an automatic <see cref="Entities.AccessDecision"/>. Mirrors the
/// <c>kind</c> discriminator on <see cref="Models.Conditions.AccessCondition"/>, which remains the JSON wire format

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎨 SUGGESTED: Models.Conditions.AccessCondition does not resolve

Details and fix

No AccessCondition type exists under Bit.Pam.Models.Conditions (or anywhere in the codebase), so this <see cref> will not link. The sibling AccessRule.cs documents the same concept with plain code markup precisely because the type isn't referenceable from this project:

/// <c>kind</c> discriminator on <c>AccessCondition</c>, which remains the JSON wire format

Switching to <c>AccessCondition</c> keeps the two files consistent and avoids a dangling reference. (Doc-only; no build impact since GenerateDocumentationFile is not set.)

/// for a rule's conditions; this enum is the persisted, type-safe form recorded against the decision.
/// </summary>
public enum AccessConditionKind : byte
{
/// <summary>Requires a human approver; matching requests are routed for manual approval rather than auto-decided.</summary>
HumanApproval = 0,

/// <summary>Matches when the requester's IP is on a configured allowlist.</summary>
IpAllowlist = 1,

/// <summary>Matches when the request falls within a configured time-of-day window.</summary>
TimeOfDay = 2,
}
25 changes: 25 additions & 0 deletions src/Pam.Domain/Enums/AccessDeciderKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Bit.Pam.Enums;

/// <summary>
/// Who made a <see cref="Entities.AccessDecision"/>: an automatic condition evaluation or a human approver.
/// </summary>
public enum AccessDeciderKind : byte
{
/// <summary>A condition on the governing access rule decided, with no human involved.</summary>
Automatic = 0,

/// <summary>A human approver decided.</summary>
Human = 1,
}

/// <summary>
/// The verdict recorded on a <see cref="Entities.AccessDecision"/>.
/// </summary>
public enum AccessDecisionVerdict : byte
{
/// <summary>Access was refused.</summary>
Deny = 0,

/// <summary>Access was granted.</summary>
Approve = 1,
}
23 changes: 23 additions & 0 deletions src/Pam.Domain/Enums/AccessLeaseExtendOutcome.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Bit.Pam.Enums;

/// <summary>
/// The result of a race-safe lease extension. The extension stored procedure returns a distinct integer code so the
/// caller can tell a lease that is no longer extendable apart from one that has already been extended.
/// </summary>
public enum AccessLeaseExtendOutcome
{
/// <summary>The lease's window was extended (stored proc returned 1).</summary>
Extended = 1,

/// <summary>
/// The lease was no longer active, or its window had already ended, when the guarded update ran (stored proc
/// returned 0). A concurrent revoke or expiry likely won.
/// </summary>
LeaseNotActive = 0,

/// <summary>
/// The lease has already been extended (a lease may be extended once; stored proc returned -1). Nothing was
/// persisted.
/// </summary>
AlreadyExtended = -1,
}
23 changes: 23 additions & 0 deletions src/Pam.Domain/Enums/AccessLeaseMintOutcome.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Bit.Pam.Enums;

/// <summary>
/// The result of a race-safe lease mint. The mint stored procedures return a distinct integer code so the caller can
/// tell a lost-the-race precondition failure apart from a per-cipher single-active-lease conflict.
/// </summary>
public enum AccessLeaseMintOutcome
{
/// <summary>The active lease was minted (stored proc returned 1).</summary>
Minted = 1,

/// <summary>
/// A precondition no longer held when the guarded insert ran (stored proc returned 0, or the unique-index
/// backstop fired). A concurrent activation likely won; the caller re-reads the winner.
/// </summary>
PreconditionFailed = 0,

/// <summary>
/// Another active in-window lease already exists for this cipher and the governing rule enforces a per-cipher
/// singleton (stored proc returned -1). Nothing was persisted.
/// </summary>
SingleActiveLeaseConflict = -1,
}
19 changes: 19 additions & 0 deletions src/Pam.Domain/Enums/AccessLeaseStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Bit.Pam.Enums;

/// <summary>
/// Lifecycle of a <see cref="Entities.AccessLease"/>. Only <see cref="Active"/> leases authorize access.
/// </summary>
public enum AccessLeaseStatus : byte
{
/// <summary>Live; within its window it authorizes access.</summary>
Active = 0,

/// <summary>The lease's window ended on its own.</summary>
Expired = 1,

/// <summary>An operator ended the lease early.</summary>
Revoked = 2,

/// <summary>The holder ended their own lease early, as opposed to <see cref="Revoked"/> (an operator ended it).</summary>
Cancelled = 3,
}
23 changes: 23 additions & 0 deletions src/Pam.Domain/Enums/AccessRequestStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Bit.Pam.Enums;

/// <summary>
/// Lifecycle of a <see cref="Entities.AccessRequest"/>. A request starts <see cref="Pending"/> and moves to exactly
/// one terminal state. Auto-approved requests are created already <see cref="Approved"/>.
/// </summary>
public enum AccessRequestStatus : byte
{
/// <summary>Awaiting a human approver's decision.</summary>
Pending = 0,

/// <summary>Approved automatically or by an approver; the requester can activate it into a lease within its window.</summary>
Approved = 1,

/// <summary>An approver refused the request.</summary>
Denied = 2,

/// <summary>Withdrawn by the requester before it was decided.</summary>
Cancelled = 3,

/// <summary>The approval window lapsed with no decision recorded.</summary>
ExpiredUnanswered = 4,
}
33 changes: 33 additions & 0 deletions src/Pam.Domain/Models/AccessRequestDecision.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Bit.Pam.Enums;

namespace Bit.Pam.Models;

/// <summary>
/// One decision on an <see cref="Entities.AccessRequest"/>, projected from an <see cref="Entities.AccessDecision"/>
/// row. The element of <see cref="AccessRequestDetails.Decisions"/> — there is one per recorded decision, human or
/// automatic. A human decision carries the approver's identity (<see cref="Id"/> plus the denormalized name/email); an
/// automatic decision has none (<see cref="Id"/> null — it was decided by an access-rule condition).
/// </summary>
public class AccessRequestDecision
{
/// <summary>Who decided: a human approver or an automatic condition evaluation.</summary>
public AccessDeciderKind DeciderKind { get; set; }

/// <summary>The human approver, or null for an automatic decision.</summary>
public Guid? Id { get; set; }

/// <summary>The human approver's display name, or null (automatic, or the server could not resolve the user).</summary>
public string? Name { get; set; }

/// <summary>The human approver's email, the fallback display when <see cref="Name"/> is unset.</summary>
public string? Email { get; set; }

/// <summary>The decision's comment (a human approver's note, or a future automatic-evaluation reason), if any.</summary>
public string? Comment { get; set; }

/// <summary>The verdict reached.</summary>
public AccessDecisionVerdict Verdict { get; set; }

/// <summary>When the decision was made (the decision's CreationDate).</summary>
public DateTime DecidedAt { get; set; }
}
Loading
Loading