Skip to content
Draft
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
23 changes: 12 additions & 11 deletions bitwarden_license/src/Services/Pam/rotation-server.allium
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,12 @@ enum FailureSyncState { target_unchanged | target_updated | indeterminate }
-- Entities

-- A target system the access connector can rotate credentials in (Entra, a SQL Server, a custom
-- script, ...). Defined and owned server-side: the admin registers it with a name and a
-- rotation method, and the server holds NO connection details -- those live in the
-- access connector's local resolver, keyed by this id (see access-connector.allium). Status enables or
-- disables the whole target. Status literals (active|disabled) are deliberately distinct
-- from ConnectorRegistration's (enrolled|revoked) so untyped trigger-param inference stays
-- unambiguous.
-- script, an on-premises Active Directory domain, ...). Defined and owned server-side: the admin
-- registers it with a name and a rotation method, and the server holds NO connection details --
-- those live in the access connector's local resolver, keyed by this id (see
-- access-connector.allium). Status enables or disables the whole target. Status literals
-- (active|disabled) are deliberately distinct from ConnectorRegistration's (enrolled|revoked) so
-- untyped trigger-param inference stays unambiguous.
entity TargetSystem {
id: String
name: String
Expand All @@ -196,8 +196,9 @@ entity TargetSystem {
-- manual: no access connector integration -- rotations are recorded out of band (Goal 3); kind,
-- password_policy and supports_session_termination do not apply.
method: automatic | manual
-- Which integration the access connector runs (Entra, MSSQL, or a custom script).
kind: entra | mssql | custom_script when method = automatic
-- Which integration the access connector runs (Entra, MSSQL, a custom script, or
-- on-premises Active Directory -- distinct from entra, which is Entra ID).
kind: entra | mssql | custom_script | active_directory when method = automatic
password_policy: PasswordPolicy when method = automatic
-- Whether the target can terminate active sessions for a rotated account. Used at
-- config time to reject terminate_sessions on a target that cannot honour it
Expand Down Expand Up @@ -294,9 +295,9 @@ entity RotationConfig {
target_system: TargetSystem
-- account_identity is an opaque String: the account's identifier in the target system,
-- interpreted by the target's kind (an Entra UPN/email, a SQL Server login, a
-- custom-script argument). No type or kind discriminator -- the access connector's kind
-- integration knows how to read it; it is carried verbatim to the access connector on the claim
-- and never parsed server-side.
-- custom-script argument, an Active Directory sAMAccountName). No type or kind
-- discriminator -- the access connector's kind integration knows how to read it; it is
-- carried verbatim to the access connector on the claim and never parsed server-side.
account_identity: String
terminate_sessions: Boolean
-- Whether this credential rotates automatically or by hand is a property of its target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,23 @@ await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegisterAsyn
await sutProvider.GetDependency<IPamTargetSystemRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default!);
}

[Theory, BitAutoData]
[Theory]
[BitAutoData(PamTargetSystemKind.Entra)]
[BitAutoData(PamTargetSystemKind.Mssql)]
[BitAutoData(PamTargetSystemKind.CustomScript)]
[BitAutoData(PamTargetSystemKind.ActiveDirectory)]
public async Task RegisterAsync_AutomaticHappyPath_CreatesTargetWithSerializedPolicy(
Guid organizationId, Guid actingUserId, string name)
PamTargetSystemKind kind, Guid organizationId, Guid actingUserId, string name)
{
var sutProvider = Setup();
sutProvider.GetDependency<IPamTargetSystemRepository>().CreateAsync(Arg.Any<PamTargetSystem>())
.Returns(call => Task.FromResult(call.Arg<PamTargetSystem>()));

var result = await sutProvider.Sut.RegisterAsync(
organizationId, actingUserId, name, PamTargetSystemMethod.Automatic, PamTargetSystemKind.Mssql, _policy, true);
organizationId, actingUserId, name, PamTargetSystemMethod.Automatic, kind, _policy, true);

Assert.Equal(PamTargetSystemMethod.Automatic, result.Method);
Assert.Equal(PamTargetSystemKind.Mssql, result.Kind);
Assert.Equal(kind, result.Kind);
Assert.True(result.SupportsSessionTermination);
Assert.Equal(PamTargetSystemStatus.Active, result.Status);
Assert.Equal(PamPasswordPolicy.Serialize(_policy), result.PasswordPolicy);
Expand Down
6 changes: 3 additions & 3 deletions src/Pam.Domain/Entities/PamTargetSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace Bit.Pam.Entities;

/// <summary>
/// A system PAM can rotate credentials against: an <see cref="PamTargetSystemMethod.Automatic"/> target driven by a
/// <see cref="PamDaemon"/> (Entra, MSSQL, or a custom script), or a <see cref="PamTargetSystemMethod.Manual"/> target
/// that only tracks a schedule and records rotations a human performs out of band. <see cref="Kind"/> and
/// <see cref="PasswordPolicy"/> are set only for an automatic target.
/// <see cref="PamDaemon"/> (Entra, MSSQL, a custom script, or Active Directory), or a
/// <see cref="PamTargetSystemMethod.Manual"/> target that only tracks a schedule and records rotations a human
/// performs out of band. <see cref="Kind"/> and <see cref="PasswordPolicy"/> are set only for an automatic target.
/// </summary>
public class PamTargetSystem : ITableObject<Guid>
{
Expand Down
3 changes: 3 additions & 0 deletions src/Pam.Domain/Enums/PamTargetSystemKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ public enum PamTargetSystemKind : byte
Entra = 0,
Mssql = 1,
CustomScript = 2,

/// <summary>On-premises Active Directory, not Entra ID β€” see <see cref="Entra"/>.</summary>
ActiveDirectory = 3,
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ public async Task CreateAsync_ThenRead_RoundTripsFields(
Assert.Equal(PamTargetSystemStatus.Active, persisted.Status);
}

// Kind is a bare TINYINT with no check constraint: every connector kind persists on every provider without a
// schema change.
[DatabaseTheory, DatabaseData]
public async Task CreateAsync_ThenRead_RoundTripsEveryAutomaticKind(
IOrganizationRepository organizationRepository,
IPamTargetSystemRepository pamTargetSystemRepository)
{
var organization = await organizationRepository.CreateTestOrganizationAsync();
var now = DateTime.UtcNow;

foreach (var kind in Enum.GetValues<PamTargetSystemKind>())
{
var target = await pamTargetSystemRepository.CreateAsync(
BuildTarget(organization.Id, $"target-{kind}", now, kind));

var persisted = await pamTargetSystemRepository.GetByIdAsync(target.Id);

Assert.NotNull(persisted);
Assert.Equal(kind, persisted!.Kind);
}
}

// A manual target carries no connector; Kind/PasswordPolicy stay null through the round trip.
[DatabaseTheory, DatabaseData]
public async Task ReplaceAsync_UpdatesFields(
Expand Down Expand Up @@ -192,14 +214,18 @@ private static async Task<PamDaemon> CreateEnrolledDaemonAsync(
});
}

private static PamTargetSystem BuildTarget(Guid organizationId, string name, DateTime now) => new()
{
OrganizationId = organizationId,
Name = name,
Method = PamTargetSystemMethod.Automatic,
Kind = PamTargetSystemKind.Entra,
Status = PamTargetSystemStatus.Active,
CreationDate = now,
RevisionDate = now,
};
private static PamTargetSystem BuildTarget(
Guid organizationId,
string name,
DateTime now,
PamTargetSystemKind kind = PamTargetSystemKind.Entra) => new()
{
OrganizationId = organizationId,
Name = name,
Method = PamTargetSystemMethod.Automatic,
Kind = kind,
Status = PamTargetSystemStatus.Active,
CreationDate = now,
RevisionDate = now,
};
}
Loading