Add data model, DbContext and initial migration (M1)

Schema for identity, vaults, grants, hosts and the sync change log, verified against a
real PostgreSQL 18 container rather than an in-memory provider: partial unique indexes,
CHECK constraints, citext and identity-always columns are all provider behaviour that an
in-memory fake would not exercise.

Invariants pushed into the database, so they hold even when application code has a bug:
- ck_host_relay_target is a security boundary, not tidiness. A host may carry a plaintext
  hostname and port ONLY when relay is deliberately enabled. Both directions are tested;
  the important one is that relay-disabled hosts cannot carry an address, since otherwise
  a bug would silently give the server infrastructure visibility it was never granted.
- ck_vault_owner: exactly one of owner_user_id or team_id, or permission resolution would
  have no defined answer.
- ck_vault_key_grant_recipient: member grants name a user; recovery and escrow grants are
  wrapped to a key and must not.
- ck_user_key_wrap_kdf: a password-derived wrap without its parameters is permanently
  unopenable, so a partial write is rejected outright.

Present from the first migration on purpose:
- GrantKind (Member/Recovery/Escrow). Recovery cannot be bolted on later — every vault
  created before it existed would be unrecoverable by design.
- team and team_membership, though team features are M3. Adding them later would mean
  introducing a foreign key on a live vault table.
- Host.ContentKeyId, reserved for per-item content keys wrapped to individual users.
- user_key as its own table, so key rotation does not require altering the user row.

Two things verified rather than assumed:
- Npgsql's UseXminAsConcurrencyToken helper no longer exists in EF 10, so xmin is mapped
  directly in XminConcurrency. The generated migration *looks* like it creates an xmin
  column; it does not. Confirmed by inspecting pg_attribute (attnum -2, a system column)
  and by grepping the emitted DDL. A test pins both, because had it created a real column
  PostgreSQL would have rejected the name.
- EF Core is now pinned centrally. The Npgsql provider asks for 10.0.4 while
  EntityFrameworkCore.Design pulls 10.0.10, and because Design is PrivateAssets=all that
  higher version does not flow to referencing projects — producing a CS1705 in any test
  project referencing Infrastructure.

Also commits artifacts/schema/v0.1.sql, the idempotent script, as the baseline for future
upgrade tests.

Verified: 0 warnings, 122 tests pass (27 new against Postgres), format clean.
This commit is contained in:
2026-07-28 14:17:37 +02:00
parent 06d04b490b
commit eaf68c86b0
24 changed files with 5843 additions and 0 deletions
@@ -0,0 +1,971 @@
// <auto-generated />
using System;
using DodoSSH.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DodoSSH.Infrastructure.Migrations
{
[DbContext(typeof(DodoDbContext))]
[Migration("20260728113419_InitialSchema")]
partial class InitialSchema
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("dodo")
.HasAnnotation("ProductVersion", "10.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DodoSSH.Domain.Device", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("EnrolledAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("enrolled_at_utc");
b.Property<DateTimeOffset?>("LastSeenAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_at_utc");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<int>("Platform")
.HasColumnType("integer")
.HasColumnName("platform");
b.Property<byte[]>("PublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("public_key");
b.Property<DateTimeOffset?>("RevokedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked_at_utc");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_device");
b.HasIndex("UserId")
.HasDatabaseName("ix_device_user_id");
b.ToTable("device", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.Host", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<long>("ChangeSequence")
.HasColumnType("bigint")
.HasColumnName("change_sequence");
b.Property<Guid?>("ContentKeyId")
.HasColumnType("uuid")
.HasColumnName("content_key_id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<Guid>("CreatedByUserId")
.HasColumnType("uuid")
.HasColumnName("created_by_user_id");
b.Property<byte[]>("DataKeyWrap")
.HasColumnType("bytea")
.HasColumnName("data_key_wrap");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<Guid?>("GroupId")
.HasColumnType("uuid")
.HasColumnName("group_id");
b.Property<string>("Hostname")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("hostname");
b.Property<int>("KeyGeneration")
.HasColumnType("integer")
.HasColumnName("key_generation");
b.Property<byte[]>("Payload")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("payload");
b.Property<short>("PayloadAadVersion")
.HasColumnType("smallint")
.HasColumnName("payload_aad_version");
b.Property<int?>("Port")
.HasColumnType("integer")
.HasColumnName("port");
b.Property<bool>("RelayEnabled")
.HasColumnType("boolean")
.HasColumnName("relay_enabled");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at_utc");
b.Property<Guid>("UpdatedByUserId")
.HasColumnType("uuid")
.HasColumnName("updated_by_user_id");
b.Property<Guid>("VaultId")
.HasColumnType("uuid")
.HasColumnName("vault_id");
b.Property<int>("Version")
.HasColumnType("integer")
.HasColumnName("version");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_host");
b.HasIndex("VaultId")
.HasDatabaseName("ix_host_vault_live")
.HasFilter("deleted_at_utc IS NULL");
b.HasIndex("VaultId", "ChangeSequence")
.HasDatabaseName("ix_host_vault_id_change_sequence");
b.ToTable("host", "dodo", t =>
{
t.HasCheckConstraint("ck_host_port_range", "port IS NULL OR (port BETWEEN 1 AND 65535)");
t.HasCheckConstraint("ck_host_relay_target", "(relay_enabled AND hostname IS NOT NULL AND port IS NOT NULL)\nOR (NOT relay_enabled AND hostname IS NULL AND port IS NULL)");
t.HasCheckConstraint("ck_host_version", "version >= 1");
});
});
modelBuilder.Entity("DodoSSH.Domain.KeyLogEntry", b =>
{
b.Property<long>("Sequence")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("sequence");
NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property<long>("Sequence"));
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<byte[]>("EncryptionPublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("encryption_public_key");
b.Property<int>("Generation")
.HasColumnType("integer")
.HasColumnName("generation");
b.Property<byte[]>("Hash")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("hash");
b.Property<byte[]>("PreviousHash")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("previous_hash");
b.Property<byte[]>("SigningPublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("signing_public_key");
b.Property<byte[]>("StatementSignature")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("bytea")
.HasColumnName("statement_signature");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Sequence")
.HasName("pk_key_log");
b.HasIndex("Hash")
.IsUnique()
.HasDatabaseName("ix_key_log_hash");
b.HasIndex("UserId")
.HasDatabaseName("ix_key_log_user_id");
b.ToTable("key_log", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.SyncChange", b =>
{
b.Property<long>("Sequence")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("sequence");
NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property<long>("Sequence"));
b.Property<Guid>("ActorUserId")
.HasColumnType("uuid")
.HasColumnName("actor_user_id");
b.Property<Guid>("EntityId")
.HasColumnType("uuid")
.HasColumnName("entity_id");
b.Property<int>("EntityType")
.HasColumnType("integer")
.HasColumnName("entity_type");
b.Property<DateTimeOffset>("OccurredAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("occurred_at_utc");
b.Property<int>("Operation")
.HasColumnType("integer")
.HasColumnName("operation");
b.Property<int>("Revision")
.HasColumnType("integer")
.HasColumnName("revision");
b.Property<Guid>("VaultId")
.HasColumnType("uuid")
.HasColumnName("vault_id");
b.HasKey("Sequence")
.HasName("pk_sync_change");
b.HasIndex("VaultId", "Sequence")
.HasDatabaseName("ix_sync_change_vault_id_sequence");
b.HasIndex("VaultId", "EntityId", "Sequence")
.IsDescending(false, false, true)
.HasDatabaseName("ix_sync_change_vault_id_entity_id_sequence");
b.ToTable("sync_change", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.SyncOperationReceipt", b =>
{
b.Property<Guid>("OperationId")
.HasColumnType("uuid")
.HasColumnName("operation_id");
b.Property<long?>("AppliedChangeSequence")
.HasColumnType("bigint")
.HasColumnName("applied_change_sequence");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<int?>("ResultVersion")
.HasColumnType("integer")
.HasColumnName("result_version");
b.Property<Guid>("VaultId")
.HasColumnType("uuid")
.HasColumnName("vault_id");
b.HasKey("OperationId")
.HasName("pk_sync_operation_receipt");
b.HasIndex("VaultId", "CreatedAtUtc")
.HasDatabaseName("ix_sync_operation_receipt_vault_id_created_at_utc");
b.ToTable("sync_operation_receipt", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.Team", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<Guid>("CreatedByUserId")
.HasColumnType("uuid")
.HasColumnName("created_by_user_id");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<string>("Description")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("description");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("citext")
.HasColumnName("slug");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_team");
b.HasIndex("Slug")
.IsUnique()
.HasDatabaseName("ix_team_slug")
.HasFilter("deleted_at_utc IS NULL");
b.ToTable("team", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.TeamMembership", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<Guid?>("InvitedByUserId")
.HasColumnType("uuid")
.HasColumnName("invited_by_user_id");
b.Property<DateTimeOffset?>("JoinedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("joined_at_utc");
b.Property<int>("Role")
.HasColumnType("integer")
.HasColumnName("role");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<Guid>("TeamId")
.HasColumnType("uuid")
.HasColumnName("team_id");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_team_membership");
b.HasIndex("UserId")
.HasDatabaseName("ix_team_membership_user_id");
b.HasIndex("TeamId", "UserId")
.IsUnique()
.HasDatabaseName("ix_team_membership_team_id_user_id")
.HasFilter("deleted_at_utc IS NULL");
b.ToTable("team_membership", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.UserAccount", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<string>("DisplayName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("display_name");
b.Property<string>("Email")
.HasMaxLength(320)
.HasColumnType("citext")
.HasColumnName("email");
b.Property<DateTimeOffset?>("EnrolledAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("enrolled_at_utc");
b.Property<string>("Issuer")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("issuer");
b.Property<DateTimeOffset?>("LastSeenAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_at_utc");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<string>("Subject")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("subject");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at_utc");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_user_account");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("ix_user_account_email")
.HasFilter("email IS NOT NULL AND deleted_at_utc IS NULL");
b.HasIndex("Issuer", "Subject")
.IsUnique()
.HasDatabaseName("ix_user_account_issuer_subject");
b.ToTable("user_account", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.UserKey", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<byte[]>("EncryptionPublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("encryption_public_key");
b.Property<byte[]>("FingerprintSha256")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("fingerprint_sha256");
b.Property<int>("Generation")
.HasColumnType("integer")
.HasColumnName("generation");
b.Property<string>("IdentityProviderBinding")
.HasColumnType("jsonb")
.HasColumnName("identity_provider_binding");
b.Property<bool>("IsCurrent")
.HasColumnType("boolean")
.HasColumnName("is_current");
b.Property<DateTimeOffset?>("RevokedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked_at_utc");
b.Property<byte[]>("SigningPublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("signing_public_key");
b.Property<string>("Statement")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("statement");
b.Property<byte[]>("StatementSignature")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("bytea")
.HasColumnName("statement_signature");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_user_key");
b.HasIndex("FingerprintSha256")
.IsUnique()
.HasDatabaseName("ix_user_key_fingerprint_sha256");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("ix_user_key_current")
.HasFilter("is_current");
b.HasIndex("UserId", "Generation")
.IsUnique()
.HasDatabaseName("ix_user_key_user_id_generation");
b.ToTable("user_key", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.UserKeyWrap", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<Guid?>("DeviceId")
.HasColumnType("uuid")
.HasColumnName("device_id");
b.Property<string>("KdfAlgorithm")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("kdf_algorithm");
b.Property<int?>("KdfMemoryKibibytes")
.HasColumnType("integer")
.HasColumnName("kdf_memory_kibibytes");
b.Property<int?>("KdfParallelism")
.HasColumnType("integer")
.HasColumnName("kdf_parallelism");
b.Property<int?>("KdfPasses")
.HasColumnType("integer")
.HasColumnName("kdf_passes");
b.Property<byte[]>("KdfSalt")
.HasMaxLength(64)
.HasColumnType("bytea")
.HasColumnName("kdf_salt");
b.Property<int>("Kind")
.HasColumnType("integer")
.HasColumnName("kind");
b.Property<DateTimeOffset?>("LastUsedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_used_at_utc");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.Property<byte[]>("Wrap")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("wrap");
b.Property<int>("WrapVersion")
.HasColumnType("integer")
.HasColumnName("wrap_version");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_user_key_wrap");
b.HasIndex("DeviceId")
.HasDatabaseName("ix_user_key_wrap_device_id");
b.HasIndex("UserId", "DeviceId")
.IsUnique()
.HasDatabaseName("ix_user_key_wrap_user_device")
.HasFilter("device_id IS NOT NULL");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasDatabaseName("ix_user_key_wrap_user_kind")
.HasFilter("device_id IS NULL");
b.ToTable("user_key_wrap", "dodo", t =>
{
t.HasCheckConstraint("ck_user_key_wrap_device", "(kind = 2 AND device_id IS NOT NULL) OR (kind <> 2 AND device_id IS NULL)");
t.HasCheckConstraint("ck_user_key_wrap_kdf", "(kind IN (1, 3) AND kdf_algorithm IS NOT NULL AND kdf_salt IS NOT NULL\n AND kdf_memory_kibibytes IS NOT NULL AND kdf_passes IS NOT NULL\n AND kdf_parallelism IS NOT NULL)\nOR (kind IN (2, 4) AND kdf_algorithm IS NULL AND kdf_salt IS NULL)");
});
});
modelBuilder.Entity("DodoSSH.Domain.Vault", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<int>("KeyGeneration")
.HasColumnType("integer")
.HasColumnName("key_generation");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<int>("OwnerKind")
.HasColumnType("integer")
.HasColumnName("owner_kind");
b.Property<Guid?>("OwnerUserId")
.HasColumnType("uuid")
.HasColumnName("owner_user_id");
b.Property<int>("RekeyReason")
.HasColumnType("integer")
.HasColumnName("rekey_reason");
b.Property<bool>("RekeyRequired")
.HasColumnType("boolean")
.HasColumnName("rekey_required");
b.Property<Guid?>("TeamId")
.HasColumnType("uuid")
.HasColumnName("team_id");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at_utc");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_vault");
b.HasIndex("OwnerUserId")
.HasDatabaseName("ix_vault_owner_user_id");
b.HasIndex("TeamId")
.HasDatabaseName("ix_vault_team_id");
b.ToTable("vault", "dodo", t =>
{
t.HasCheckConstraint("ck_vault_key_generation", "key_generation >= 1");
t.HasCheckConstraint("ck_vault_owner", "(owner_kind = 1 AND owner_user_id IS NOT NULL AND team_id IS NULL)\nOR (owner_kind = 2 AND team_id IS NOT NULL AND owner_user_id IS NULL)");
});
});
modelBuilder.Entity("DodoSSH.Domain.VaultKeyGrant", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<byte[]>("GranterKeyFingerprint")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("granter_key_fingerprint");
b.Property<Guid>("GranterUserId")
.HasColumnType("uuid")
.HasColumnName("granter_user_id");
b.Property<int>("KeyGeneration")
.HasColumnType("integer")
.HasColumnName("key_generation");
b.Property<byte[]>("KeyLogHead")
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("key_log_head");
b.Property<int>("Kind")
.HasColumnType("integer")
.HasColumnName("kind");
b.Property<byte[]>("RecipientKeyFingerprint")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("recipient_key_fingerprint");
b.Property<Guid?>("RecipientUserId")
.HasColumnType("uuid")
.HasColumnName("recipient_user_id");
b.Property<DateTimeOffset?>("RevokedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked_at_utc");
b.Property<byte[]>("Signature")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("bytea")
.HasColumnName("signature");
b.Property<int>("State")
.HasColumnType("integer")
.HasColumnName("state");
b.Property<Guid>("VaultId")
.HasColumnType("uuid")
.HasColumnName("vault_id");
b.Property<byte[]>("WrappedKey")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("wrapped_key");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_vault_key_grant");
b.HasIndex("RecipientUserId")
.HasDatabaseName("ix_vault_key_grant_recipient_user_id");
b.HasIndex("VaultId", "KeyGeneration", "RecipientUserId")
.IsUnique()
.HasDatabaseName("ix_vault_key_grant_vault_id_key_generation_recipient_user_id")
.HasFilter("revoked_at_utc IS NULL AND recipient_user_id IS NOT NULL");
b.ToTable("vault_key_grant", "dodo", t =>
{
t.HasCheckConstraint("ck_vault_key_grant_recipient", "(kind = 1 AND recipient_user_id IS NOT NULL) OR (kind <> 1 AND recipient_user_id IS NULL)");
});
});
modelBuilder.Entity("DodoSSH.Domain.Device", b =>
{
b.HasOne("DodoSSH.Domain.UserAccount", "User")
.WithMany("Devices")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_device_users_user_id");
b.Navigation("User");
});
modelBuilder.Entity("DodoSSH.Domain.Host", b =>
{
b.HasOne("DodoSSH.Domain.Vault", "Vault")
.WithMany("Hosts")
.HasForeignKey("VaultId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_host_vaults_vault_id");
b.Navigation("Vault");
});
modelBuilder.Entity("DodoSSH.Domain.TeamMembership", b =>
{
b.HasOne("DodoSSH.Domain.Team", "Team")
.WithMany("Memberships")
.HasForeignKey("TeamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_team_membership_team_team_id");
b.HasOne("DodoSSH.Domain.UserAccount", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_team_membership_users_user_id");
b.Navigation("Team");
b.Navigation("User");
});
modelBuilder.Entity("DodoSSH.Domain.UserKey", b =>
{
b.HasOne("DodoSSH.Domain.UserAccount", "User")
.WithMany("Keys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_user_key_user_account_user_id");
b.Navigation("User");
});
modelBuilder.Entity("DodoSSH.Domain.UserKeyWrap", b =>
{
b.HasOne("DodoSSH.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_user_key_wrap_device_device_id");
b.HasOne("DodoSSH.Domain.UserAccount", "User")
.WithMany("KeyWraps")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_user_key_wrap_user_account_user_id");
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("DodoSSH.Domain.Vault", b =>
{
b.HasOne("DodoSSH.Domain.UserAccount", "OwnerUser")
.WithMany()
.HasForeignKey("OwnerUserId")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_vault_user_account_owner_user_id");
b.HasOne("DodoSSH.Domain.Team", "Team")
.WithMany()
.HasForeignKey("TeamId")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_vault_team_team_id");
b.Navigation("OwnerUser");
b.Navigation("Team");
});
modelBuilder.Entity("DodoSSH.Domain.VaultKeyGrant", b =>
{
b.HasOne("DodoSSH.Domain.UserAccount", "RecipientUser")
.WithMany()
.HasForeignKey("RecipientUserId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_vault_key_grant_user_account_recipient_user_id");
b.HasOne("DodoSSH.Domain.Vault", "Vault")
.WithMany("KeyGrants")
.HasForeignKey("VaultId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_vault_key_grant_vault_vault_id");
b.Navigation("RecipientUser");
b.Navigation("Vault");
});
modelBuilder.Entity("DodoSSH.Domain.Team", b =>
{
b.Navigation("Memberships");
});
modelBuilder.Entity("DodoSSH.Domain.UserAccount", b =>
{
b.Navigation("Devices");
b.Navigation("KeyWraps");
b.Navigation("Keys");
});
modelBuilder.Entity("DodoSSH.Domain.Vault", b =>
{
b.Navigation("Hosts");
b.Navigation("KeyGrants");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,583 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DodoSSH.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class InitialSchema : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "dodo");
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:citext", ",,");
migrationBuilder.CreateTable(
name: "key_log",
schema: "dodo",
columns: table => new
{
sequence = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
generation = table.Column<int>(type: "integer", nullable: false),
encryption_public_key = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
signing_public_key = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
statement_signature = table.Column<byte[]>(type: "bytea", maxLength: 64, nullable: false),
previous_hash = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
hash = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_key_log", x => x.sequence);
});
migrationBuilder.CreateTable(
name: "sync_change",
schema: "dodo",
columns: table => new
{
sequence = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn),
vault_id = table.Column<Guid>(type: "uuid", nullable: false),
entity_type = table.Column<int>(type: "integer", nullable: false),
entity_id = table.Column<Guid>(type: "uuid", nullable: false),
operation = table.Column<int>(type: "integer", nullable: false),
revision = table.Column<int>(type: "integer", nullable: false),
actor_user_id = table.Column<Guid>(type: "uuid", nullable: false),
occurred_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_sync_change", x => x.sequence);
});
migrationBuilder.CreateTable(
name: "sync_operation_receipt",
schema: "dodo",
columns: table => new
{
operation_id = table.Column<Guid>(type: "uuid", nullable: false),
vault_id = table.Column<Guid>(type: "uuid", nullable: false),
applied_change_sequence = table.Column<long>(type: "bigint", nullable: true),
result_version = table.Column<int>(type: "integer", nullable: true),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_sync_operation_receipt", x => x.operation_id);
});
migrationBuilder.CreateTable(
name: "team",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
slug = table.Column<string>(type: "citext", maxLength: 128, nullable: false),
description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
created_by_user_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
deleted_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_team", x => x.id);
});
migrationBuilder.CreateTable(
name: "user_account",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
issuer = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
subject = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
email = table.Column<string>(type: "citext", maxLength: 320, nullable: true),
display_name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
status = table.Column<int>(type: "integer", nullable: false),
enrolled_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
updated_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
last_seen_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
deleted_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_user_account", x => x.id);
});
migrationBuilder.CreateTable(
name: "device",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
platform = table.Column<int>(type: "integer", nullable: false),
public_key = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
enrolled_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
last_seen_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
revoked_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_device", x => x.id);
table.ForeignKey(
name: "fk_device_users_user_id",
column: x => x.user_id,
principalSchema: "dodo",
principalTable: "user_account",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "team_membership",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
team_id = table.Column<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
role = table.Column<int>(type: "integer", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
invited_by_user_id = table.Column<Guid>(type: "uuid", nullable: true),
joined_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
deleted_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_team_membership", x => x.id);
table.ForeignKey(
name: "fk_team_membership_team_team_id",
column: x => x.team_id,
principalSchema: "dodo",
principalTable: "team",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_team_membership_users_user_id",
column: x => x.user_id,
principalSchema: "dodo",
principalTable: "user_account",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "user_key",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
generation = table.Column<int>(type: "integer", nullable: false),
encryption_public_key = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
signing_public_key = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
fingerprint_sha256 = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
statement = table.Column<string>(type: "jsonb", nullable: false),
statement_signature = table.Column<byte[]>(type: "bytea", maxLength: 64, nullable: false),
identity_provider_binding = table.Column<string>(type: "jsonb", nullable: true),
is_current = table.Column<bool>(type: "boolean", nullable: false),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
revoked_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_user_key", x => x.id);
table.ForeignKey(
name: "fk_user_key_user_account_user_id",
column: x => x.user_id,
principalSchema: "dodo",
principalTable: "user_account",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "vault",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
owner_kind = table.Column<int>(type: "integer", nullable: false),
owner_user_id = table.Column<Guid>(type: "uuid", nullable: true),
team_id = table.Column<Guid>(type: "uuid", nullable: true),
key_generation = table.Column<int>(type: "integer", nullable: false),
rekey_required = table.Column<bool>(type: "boolean", nullable: false),
rekey_reason = table.Column<int>(type: "integer", nullable: false),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
updated_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
deleted_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_vault", x => x.id);
table.CheckConstraint("ck_vault_key_generation", "key_generation >= 1");
table.CheckConstraint("ck_vault_owner", "(owner_kind = 1 AND owner_user_id IS NOT NULL AND team_id IS NULL)\nOR (owner_kind = 2 AND team_id IS NOT NULL AND owner_user_id IS NULL)");
table.ForeignKey(
name: "fk_vault_team_team_id",
column: x => x.team_id,
principalSchema: "dodo",
principalTable: "team",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "fk_vault_user_account_owner_user_id",
column: x => x.owner_user_id,
principalSchema: "dodo",
principalTable: "user_account",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "user_key_wrap",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
kind = table.Column<int>(type: "integer", nullable: false),
device_id = table.Column<Guid>(type: "uuid", nullable: true),
wrap = table.Column<byte[]>(type: "bytea", nullable: false),
wrap_version = table.Column<int>(type: "integer", nullable: false),
kdf_algorithm = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
kdf_salt = table.Column<byte[]>(type: "bytea", maxLength: 64, nullable: true),
kdf_memory_kibibytes = table.Column<int>(type: "integer", nullable: true),
kdf_passes = table.Column<int>(type: "integer", nullable: true),
kdf_parallelism = table.Column<int>(type: "integer", nullable: true),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
last_used_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_user_key_wrap", x => x.id);
table.CheckConstraint("ck_user_key_wrap_device", "(kind = 2 AND device_id IS NOT NULL) OR (kind <> 2 AND device_id IS NULL)");
table.CheckConstraint("ck_user_key_wrap_kdf", "(kind IN (1, 3) AND kdf_algorithm IS NOT NULL AND kdf_salt IS NOT NULL\n AND kdf_memory_kibibytes IS NOT NULL AND kdf_passes IS NOT NULL\n AND kdf_parallelism IS NOT NULL)\nOR (kind IN (2, 4) AND kdf_algorithm IS NULL AND kdf_salt IS NULL)");
table.ForeignKey(
name: "fk_user_key_wrap_device_device_id",
column: x => x.device_id,
principalSchema: "dodo",
principalTable: "device",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_user_key_wrap_user_account_user_id",
column: x => x.user_id,
principalSchema: "dodo",
principalTable: "user_account",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "host",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
vault_id = table.Column<Guid>(type: "uuid", nullable: false),
payload = table.Column<byte[]>(type: "bytea", nullable: false),
data_key_wrap = table.Column<byte[]>(type: "bytea", nullable: true),
content_key_id = table.Column<Guid>(type: "uuid", nullable: true),
key_generation = table.Column<int>(type: "integer", nullable: false),
payload_aad_version = table.Column<short>(type: "smallint", nullable: false),
relay_enabled = table.Column<bool>(type: "boolean", nullable: false),
hostname = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
port = table.Column<int>(type: "integer", nullable: true),
group_id = table.Column<Guid>(type: "uuid", nullable: true),
version = table.Column<int>(type: "integer", nullable: false),
change_sequence = table.Column<long>(type: "bigint", nullable: false),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
updated_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
deleted_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
created_by_user_id = table.Column<Guid>(type: "uuid", nullable: false),
updated_by_user_id = table.Column<Guid>(type: "uuid", nullable: false),
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_host", x => x.id);
table.CheckConstraint("ck_host_port_range", "port IS NULL OR (port BETWEEN 1 AND 65535)");
table.CheckConstraint("ck_host_relay_target", "(relay_enabled AND hostname IS NOT NULL AND port IS NOT NULL)\nOR (NOT relay_enabled AND hostname IS NULL AND port IS NULL)");
table.CheckConstraint("ck_host_version", "version >= 1");
table.ForeignKey(
name: "fk_host_vaults_vault_id",
column: x => x.vault_id,
principalSchema: "dodo",
principalTable: "vault",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "vault_key_grant",
schema: "dodo",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
vault_id = table.Column<Guid>(type: "uuid", nullable: false),
key_generation = table.Column<int>(type: "integer", nullable: false),
kind = table.Column<int>(type: "integer", nullable: false),
recipient_user_id = table.Column<Guid>(type: "uuid", nullable: true),
recipient_key_fingerprint = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
wrapped_key = table.Column<byte[]>(type: "bytea", nullable: false),
granter_user_id = table.Column<Guid>(type: "uuid", nullable: false),
granter_key_fingerprint = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: false),
key_log_head = table.Column<byte[]>(type: "bytea", maxLength: 32, nullable: true),
signature = table.Column<byte[]>(type: "bytea", maxLength: 64, nullable: false),
state = table.Column<int>(type: "integer", nullable: false),
created_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
revoked_at_utc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_vault_key_grant", x => x.id);
table.CheckConstraint("ck_vault_key_grant_recipient", "(kind = 1 AND recipient_user_id IS NOT NULL) OR (kind <> 1 AND recipient_user_id IS NULL)");
table.ForeignKey(
name: "fk_vault_key_grant_user_account_recipient_user_id",
column: x => x.recipient_user_id,
principalSchema: "dodo",
principalTable: "user_account",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_vault_key_grant_vault_vault_id",
column: x => x.vault_id,
principalSchema: "dodo",
principalTable: "vault",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_device_user_id",
schema: "dodo",
table: "device",
column: "user_id");
migrationBuilder.CreateIndex(
name: "ix_host_vault_id_change_sequence",
schema: "dodo",
table: "host",
columns: new[] { "vault_id", "change_sequence" });
migrationBuilder.CreateIndex(
name: "ix_host_vault_live",
schema: "dodo",
table: "host",
column: "vault_id",
filter: "deleted_at_utc IS NULL");
migrationBuilder.CreateIndex(
name: "ix_key_log_hash",
schema: "dodo",
table: "key_log",
column: "hash",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_key_log_user_id",
schema: "dodo",
table: "key_log",
column: "user_id");
migrationBuilder.CreateIndex(
name: "ix_sync_change_vault_id_entity_id_sequence",
schema: "dodo",
table: "sync_change",
columns: new[] { "vault_id", "entity_id", "sequence" },
descending: new[] { false, false, true });
migrationBuilder.CreateIndex(
name: "ix_sync_change_vault_id_sequence",
schema: "dodo",
table: "sync_change",
columns: new[] { "vault_id", "sequence" });
migrationBuilder.CreateIndex(
name: "ix_sync_operation_receipt_vault_id_created_at_utc",
schema: "dodo",
table: "sync_operation_receipt",
columns: new[] { "vault_id", "created_at_utc" });
migrationBuilder.CreateIndex(
name: "ix_team_slug",
schema: "dodo",
table: "team",
column: "slug",
unique: true,
filter: "deleted_at_utc IS NULL");
migrationBuilder.CreateIndex(
name: "ix_team_membership_team_id_user_id",
schema: "dodo",
table: "team_membership",
columns: new[] { "team_id", "user_id" },
unique: true,
filter: "deleted_at_utc IS NULL");
migrationBuilder.CreateIndex(
name: "ix_team_membership_user_id",
schema: "dodo",
table: "team_membership",
column: "user_id");
migrationBuilder.CreateIndex(
name: "ix_user_account_email",
schema: "dodo",
table: "user_account",
column: "email",
unique: true,
filter: "email IS NOT NULL AND deleted_at_utc IS NULL");
migrationBuilder.CreateIndex(
name: "ix_user_account_issuer_subject",
schema: "dodo",
table: "user_account",
columns: new[] { "issuer", "subject" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_user_key_current",
schema: "dodo",
table: "user_key",
column: "user_id",
unique: true,
filter: "is_current");
migrationBuilder.CreateIndex(
name: "ix_user_key_fingerprint_sha256",
schema: "dodo",
table: "user_key",
column: "fingerprint_sha256",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_user_key_user_id_generation",
schema: "dodo",
table: "user_key",
columns: new[] { "user_id", "generation" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_user_key_wrap_device_id",
schema: "dodo",
table: "user_key_wrap",
column: "device_id");
migrationBuilder.CreateIndex(
name: "ix_user_key_wrap_user_device",
schema: "dodo",
table: "user_key_wrap",
columns: new[] { "user_id", "device_id" },
unique: true,
filter: "device_id IS NOT NULL");
migrationBuilder.CreateIndex(
name: "ix_user_key_wrap_user_kind",
schema: "dodo",
table: "user_key_wrap",
columns: new[] { "user_id", "kind" },
unique: true,
filter: "device_id IS NULL");
migrationBuilder.CreateIndex(
name: "ix_vault_owner_user_id",
schema: "dodo",
table: "vault",
column: "owner_user_id");
migrationBuilder.CreateIndex(
name: "ix_vault_team_id",
schema: "dodo",
table: "vault",
column: "team_id");
migrationBuilder.CreateIndex(
name: "ix_vault_key_grant_recipient_user_id",
schema: "dodo",
table: "vault_key_grant",
column: "recipient_user_id");
migrationBuilder.CreateIndex(
name: "ix_vault_key_grant_vault_id_key_generation_recipient_user_id",
schema: "dodo",
table: "vault_key_grant",
columns: new[] { "vault_id", "key_generation", "recipient_user_id" },
unique: true,
filter: "revoked_at_utc IS NULL AND recipient_user_id IS NOT NULL");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "host",
schema: "dodo");
migrationBuilder.DropTable(
name: "key_log",
schema: "dodo");
migrationBuilder.DropTable(
name: "sync_change",
schema: "dodo");
migrationBuilder.DropTable(
name: "sync_operation_receipt",
schema: "dodo");
migrationBuilder.DropTable(
name: "team_membership",
schema: "dodo");
migrationBuilder.DropTable(
name: "user_key",
schema: "dodo");
migrationBuilder.DropTable(
name: "user_key_wrap",
schema: "dodo");
migrationBuilder.DropTable(
name: "vault_key_grant",
schema: "dodo");
migrationBuilder.DropTable(
name: "device",
schema: "dodo");
migrationBuilder.DropTable(
name: "vault",
schema: "dodo");
migrationBuilder.DropTable(
name: "team",
schema: "dodo");
migrationBuilder.DropTable(
name: "user_account",
schema: "dodo");
}
}
}
@@ -0,0 +1,968 @@
// <auto-generated />
using System;
using DodoSSH.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DodoSSH.Infrastructure.Migrations
{
[DbContext(typeof(DodoDbContext))]
partial class DodoDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("dodo")
.HasAnnotation("ProductVersion", "10.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DodoSSH.Domain.Device", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("EnrolledAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("enrolled_at_utc");
b.Property<DateTimeOffset?>("LastSeenAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_at_utc");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<int>("Platform")
.HasColumnType("integer")
.HasColumnName("platform");
b.Property<byte[]>("PublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("public_key");
b.Property<DateTimeOffset?>("RevokedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked_at_utc");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_device");
b.HasIndex("UserId")
.HasDatabaseName("ix_device_user_id");
b.ToTable("device", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.Host", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<long>("ChangeSequence")
.HasColumnType("bigint")
.HasColumnName("change_sequence");
b.Property<Guid?>("ContentKeyId")
.HasColumnType("uuid")
.HasColumnName("content_key_id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<Guid>("CreatedByUserId")
.HasColumnType("uuid")
.HasColumnName("created_by_user_id");
b.Property<byte[]>("DataKeyWrap")
.HasColumnType("bytea")
.HasColumnName("data_key_wrap");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<Guid?>("GroupId")
.HasColumnType("uuid")
.HasColumnName("group_id");
b.Property<string>("Hostname")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("hostname");
b.Property<int>("KeyGeneration")
.HasColumnType("integer")
.HasColumnName("key_generation");
b.Property<byte[]>("Payload")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("payload");
b.Property<short>("PayloadAadVersion")
.HasColumnType("smallint")
.HasColumnName("payload_aad_version");
b.Property<int?>("Port")
.HasColumnType("integer")
.HasColumnName("port");
b.Property<bool>("RelayEnabled")
.HasColumnType("boolean")
.HasColumnName("relay_enabled");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at_utc");
b.Property<Guid>("UpdatedByUserId")
.HasColumnType("uuid")
.HasColumnName("updated_by_user_id");
b.Property<Guid>("VaultId")
.HasColumnType("uuid")
.HasColumnName("vault_id");
b.Property<int>("Version")
.HasColumnType("integer")
.HasColumnName("version");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_host");
b.HasIndex("VaultId")
.HasDatabaseName("ix_host_vault_live")
.HasFilter("deleted_at_utc IS NULL");
b.HasIndex("VaultId", "ChangeSequence")
.HasDatabaseName("ix_host_vault_id_change_sequence");
b.ToTable("host", "dodo", t =>
{
t.HasCheckConstraint("ck_host_port_range", "port IS NULL OR (port BETWEEN 1 AND 65535)");
t.HasCheckConstraint("ck_host_relay_target", "(relay_enabled AND hostname IS NOT NULL AND port IS NOT NULL)\nOR (NOT relay_enabled AND hostname IS NULL AND port IS NULL)");
t.HasCheckConstraint("ck_host_version", "version >= 1");
});
});
modelBuilder.Entity("DodoSSH.Domain.KeyLogEntry", b =>
{
b.Property<long>("Sequence")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("sequence");
NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property<long>("Sequence"));
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<byte[]>("EncryptionPublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("encryption_public_key");
b.Property<int>("Generation")
.HasColumnType("integer")
.HasColumnName("generation");
b.Property<byte[]>("Hash")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("hash");
b.Property<byte[]>("PreviousHash")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("previous_hash");
b.Property<byte[]>("SigningPublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("signing_public_key");
b.Property<byte[]>("StatementSignature")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("bytea")
.HasColumnName("statement_signature");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Sequence")
.HasName("pk_key_log");
b.HasIndex("Hash")
.IsUnique()
.HasDatabaseName("ix_key_log_hash");
b.HasIndex("UserId")
.HasDatabaseName("ix_key_log_user_id");
b.ToTable("key_log", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.SyncChange", b =>
{
b.Property<long>("Sequence")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("sequence");
NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property<long>("Sequence"));
b.Property<Guid>("ActorUserId")
.HasColumnType("uuid")
.HasColumnName("actor_user_id");
b.Property<Guid>("EntityId")
.HasColumnType("uuid")
.HasColumnName("entity_id");
b.Property<int>("EntityType")
.HasColumnType("integer")
.HasColumnName("entity_type");
b.Property<DateTimeOffset>("OccurredAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("occurred_at_utc");
b.Property<int>("Operation")
.HasColumnType("integer")
.HasColumnName("operation");
b.Property<int>("Revision")
.HasColumnType("integer")
.HasColumnName("revision");
b.Property<Guid>("VaultId")
.HasColumnType("uuid")
.HasColumnName("vault_id");
b.HasKey("Sequence")
.HasName("pk_sync_change");
b.HasIndex("VaultId", "Sequence")
.HasDatabaseName("ix_sync_change_vault_id_sequence");
b.HasIndex("VaultId", "EntityId", "Sequence")
.IsDescending(false, false, true)
.HasDatabaseName("ix_sync_change_vault_id_entity_id_sequence");
b.ToTable("sync_change", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.SyncOperationReceipt", b =>
{
b.Property<Guid>("OperationId")
.HasColumnType("uuid")
.HasColumnName("operation_id");
b.Property<long?>("AppliedChangeSequence")
.HasColumnType("bigint")
.HasColumnName("applied_change_sequence");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<int?>("ResultVersion")
.HasColumnType("integer")
.HasColumnName("result_version");
b.Property<Guid>("VaultId")
.HasColumnType("uuid")
.HasColumnName("vault_id");
b.HasKey("OperationId")
.HasName("pk_sync_operation_receipt");
b.HasIndex("VaultId", "CreatedAtUtc")
.HasDatabaseName("ix_sync_operation_receipt_vault_id_created_at_utc");
b.ToTable("sync_operation_receipt", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.Team", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<Guid>("CreatedByUserId")
.HasColumnType("uuid")
.HasColumnName("created_by_user_id");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<string>("Description")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("description");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("citext")
.HasColumnName("slug");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_team");
b.HasIndex("Slug")
.IsUnique()
.HasDatabaseName("ix_team_slug")
.HasFilter("deleted_at_utc IS NULL");
b.ToTable("team", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.TeamMembership", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<Guid?>("InvitedByUserId")
.HasColumnType("uuid")
.HasColumnName("invited_by_user_id");
b.Property<DateTimeOffset?>("JoinedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("joined_at_utc");
b.Property<int>("Role")
.HasColumnType("integer")
.HasColumnName("role");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<Guid>("TeamId")
.HasColumnType("uuid")
.HasColumnName("team_id");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_team_membership");
b.HasIndex("UserId")
.HasDatabaseName("ix_team_membership_user_id");
b.HasIndex("TeamId", "UserId")
.IsUnique()
.HasDatabaseName("ix_team_membership_team_id_user_id")
.HasFilter("deleted_at_utc IS NULL");
b.ToTable("team_membership", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.UserAccount", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<string>("DisplayName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("display_name");
b.Property<string>("Email")
.HasMaxLength(320)
.HasColumnType("citext")
.HasColumnName("email");
b.Property<DateTimeOffset?>("EnrolledAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("enrolled_at_utc");
b.Property<string>("Issuer")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("issuer");
b.Property<DateTimeOffset?>("LastSeenAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_at_utc");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<string>("Subject")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("subject");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at_utc");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_user_account");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("ix_user_account_email")
.HasFilter("email IS NOT NULL AND deleted_at_utc IS NULL");
b.HasIndex("Issuer", "Subject")
.IsUnique()
.HasDatabaseName("ix_user_account_issuer_subject");
b.ToTable("user_account", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.UserKey", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<byte[]>("EncryptionPublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("encryption_public_key");
b.Property<byte[]>("FingerprintSha256")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("fingerprint_sha256");
b.Property<int>("Generation")
.HasColumnType("integer")
.HasColumnName("generation");
b.Property<string>("IdentityProviderBinding")
.HasColumnType("jsonb")
.HasColumnName("identity_provider_binding");
b.Property<bool>("IsCurrent")
.HasColumnType("boolean")
.HasColumnName("is_current");
b.Property<DateTimeOffset?>("RevokedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked_at_utc");
b.Property<byte[]>("SigningPublicKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("signing_public_key");
b.Property<string>("Statement")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("statement");
b.Property<byte[]>("StatementSignature")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("bytea")
.HasColumnName("statement_signature");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_user_key");
b.HasIndex("FingerprintSha256")
.IsUnique()
.HasDatabaseName("ix_user_key_fingerprint_sha256");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("ix_user_key_current")
.HasFilter("is_current");
b.HasIndex("UserId", "Generation")
.IsUnique()
.HasDatabaseName("ix_user_key_user_id_generation");
b.ToTable("user_key", "dodo");
});
modelBuilder.Entity("DodoSSH.Domain.UserKeyWrap", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<Guid?>("DeviceId")
.HasColumnType("uuid")
.HasColumnName("device_id");
b.Property<string>("KdfAlgorithm")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("kdf_algorithm");
b.Property<int?>("KdfMemoryKibibytes")
.HasColumnType("integer")
.HasColumnName("kdf_memory_kibibytes");
b.Property<int?>("KdfParallelism")
.HasColumnType("integer")
.HasColumnName("kdf_parallelism");
b.Property<int?>("KdfPasses")
.HasColumnType("integer")
.HasColumnName("kdf_passes");
b.Property<byte[]>("KdfSalt")
.HasMaxLength(64)
.HasColumnType("bytea")
.HasColumnName("kdf_salt");
b.Property<int>("Kind")
.HasColumnType("integer")
.HasColumnName("kind");
b.Property<DateTimeOffset?>("LastUsedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_used_at_utc");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.Property<byte[]>("Wrap")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("wrap");
b.Property<int>("WrapVersion")
.HasColumnType("integer")
.HasColumnName("wrap_version");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_user_key_wrap");
b.HasIndex("DeviceId")
.HasDatabaseName("ix_user_key_wrap_device_id");
b.HasIndex("UserId", "DeviceId")
.IsUnique()
.HasDatabaseName("ix_user_key_wrap_user_device")
.HasFilter("device_id IS NOT NULL");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasDatabaseName("ix_user_key_wrap_user_kind")
.HasFilter("device_id IS NULL");
b.ToTable("user_key_wrap", "dodo", t =>
{
t.HasCheckConstraint("ck_user_key_wrap_device", "(kind = 2 AND device_id IS NOT NULL) OR (kind <> 2 AND device_id IS NULL)");
t.HasCheckConstraint("ck_user_key_wrap_kdf", "(kind IN (1, 3) AND kdf_algorithm IS NOT NULL AND kdf_salt IS NOT NULL\n AND kdf_memory_kibibytes IS NOT NULL AND kdf_passes IS NOT NULL\n AND kdf_parallelism IS NOT NULL)\nOR (kind IN (2, 4) AND kdf_algorithm IS NULL AND kdf_salt IS NULL)");
});
});
modelBuilder.Entity("DodoSSH.Domain.Vault", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<DateTimeOffset?>("DeletedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at_utc");
b.Property<int>("KeyGeneration")
.HasColumnType("integer")
.HasColumnName("key_generation");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<int>("OwnerKind")
.HasColumnType("integer")
.HasColumnName("owner_kind");
b.Property<Guid?>("OwnerUserId")
.HasColumnType("uuid")
.HasColumnName("owner_user_id");
b.Property<int>("RekeyReason")
.HasColumnType("integer")
.HasColumnName("rekey_reason");
b.Property<bool>("RekeyRequired")
.HasColumnType("boolean")
.HasColumnName("rekey_required");
b.Property<Guid?>("TeamId")
.HasColumnType("uuid")
.HasColumnName("team_id");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at_utc");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_vault");
b.HasIndex("OwnerUserId")
.HasDatabaseName("ix_vault_owner_user_id");
b.HasIndex("TeamId")
.HasDatabaseName("ix_vault_team_id");
b.ToTable("vault", "dodo", t =>
{
t.HasCheckConstraint("ck_vault_key_generation", "key_generation >= 1");
t.HasCheckConstraint("ck_vault_owner", "(owner_kind = 1 AND owner_user_id IS NOT NULL AND team_id IS NULL)\nOR (owner_kind = 2 AND team_id IS NOT NULL AND owner_user_id IS NULL)");
});
});
modelBuilder.Entity("DodoSSH.Domain.VaultKeyGrant", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at_utc");
b.Property<byte[]>("GranterKeyFingerprint")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("granter_key_fingerprint");
b.Property<Guid>("GranterUserId")
.HasColumnType("uuid")
.HasColumnName("granter_user_id");
b.Property<int>("KeyGeneration")
.HasColumnType("integer")
.HasColumnName("key_generation");
b.Property<byte[]>("KeyLogHead")
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("key_log_head");
b.Property<int>("Kind")
.HasColumnType("integer")
.HasColumnName("kind");
b.Property<byte[]>("RecipientKeyFingerprint")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("bytea")
.HasColumnName("recipient_key_fingerprint");
b.Property<Guid?>("RecipientUserId")
.HasColumnType("uuid")
.HasColumnName("recipient_user_id");
b.Property<DateTimeOffset?>("RevokedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked_at_utc");
b.Property<byte[]>("Signature")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("bytea")
.HasColumnName("signature");
b.Property<int>("State")
.HasColumnType("integer")
.HasColumnName("state");
b.Property<Guid>("VaultId")
.HasColumnType("uuid")
.HasColumnName("vault_id");
b.Property<byte[]>("WrappedKey")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("wrapped_key");
b.Property<uint>("xmin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Id")
.HasName("pk_vault_key_grant");
b.HasIndex("RecipientUserId")
.HasDatabaseName("ix_vault_key_grant_recipient_user_id");
b.HasIndex("VaultId", "KeyGeneration", "RecipientUserId")
.IsUnique()
.HasDatabaseName("ix_vault_key_grant_vault_id_key_generation_recipient_user_id")
.HasFilter("revoked_at_utc IS NULL AND recipient_user_id IS NOT NULL");
b.ToTable("vault_key_grant", "dodo", t =>
{
t.HasCheckConstraint("ck_vault_key_grant_recipient", "(kind = 1 AND recipient_user_id IS NOT NULL) OR (kind <> 1 AND recipient_user_id IS NULL)");
});
});
modelBuilder.Entity("DodoSSH.Domain.Device", b =>
{
b.HasOne("DodoSSH.Domain.UserAccount", "User")
.WithMany("Devices")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_device_users_user_id");
b.Navigation("User");
});
modelBuilder.Entity("DodoSSH.Domain.Host", b =>
{
b.HasOne("DodoSSH.Domain.Vault", "Vault")
.WithMany("Hosts")
.HasForeignKey("VaultId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_host_vaults_vault_id");
b.Navigation("Vault");
});
modelBuilder.Entity("DodoSSH.Domain.TeamMembership", b =>
{
b.HasOne("DodoSSH.Domain.Team", "Team")
.WithMany("Memberships")
.HasForeignKey("TeamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_team_membership_team_team_id");
b.HasOne("DodoSSH.Domain.UserAccount", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_team_membership_users_user_id");
b.Navigation("Team");
b.Navigation("User");
});
modelBuilder.Entity("DodoSSH.Domain.UserKey", b =>
{
b.HasOne("DodoSSH.Domain.UserAccount", "User")
.WithMany("Keys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_user_key_user_account_user_id");
b.Navigation("User");
});
modelBuilder.Entity("DodoSSH.Domain.UserKeyWrap", b =>
{
b.HasOne("DodoSSH.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_user_key_wrap_device_device_id");
b.HasOne("DodoSSH.Domain.UserAccount", "User")
.WithMany("KeyWraps")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_user_key_wrap_user_account_user_id");
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("DodoSSH.Domain.Vault", b =>
{
b.HasOne("DodoSSH.Domain.UserAccount", "OwnerUser")
.WithMany()
.HasForeignKey("OwnerUserId")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_vault_user_account_owner_user_id");
b.HasOne("DodoSSH.Domain.Team", "Team")
.WithMany()
.HasForeignKey("TeamId")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_vault_team_team_id");
b.Navigation("OwnerUser");
b.Navigation("Team");
});
modelBuilder.Entity("DodoSSH.Domain.VaultKeyGrant", b =>
{
b.HasOne("DodoSSH.Domain.UserAccount", "RecipientUser")
.WithMany()
.HasForeignKey("RecipientUserId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_vault_key_grant_user_account_recipient_user_id");
b.HasOne("DodoSSH.Domain.Vault", "Vault")
.WithMany("KeyGrants")
.HasForeignKey("VaultId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_vault_key_grant_vault_vault_id");
b.Navigation("RecipientUser");
b.Navigation("Vault");
});
modelBuilder.Entity("DodoSSH.Domain.Team", b =>
{
b.Navigation("Memberships");
});
modelBuilder.Entity("DodoSSH.Domain.UserAccount", b =>
{
b.Navigation("Devices");
b.Navigation("KeyWraps");
b.Navigation("Keys");
});
modelBuilder.Entity("DodoSSH.Domain.Vault", b =>
{
b.Navigation("Hosts");
b.Navigation("KeyGrants");
});
#pragma warning restore 612, 618
}
}
}