feat: Implement API client and token models with hashing and JWT authentication

- Added ApiClientModel and ApiTokenModel for managing API clients and tokens.
- Introduced ConfigurationDefinitionModel and ConfigurationValueModel for configuration management.
- Created CredentialSecretModel for storing credential secrets.
- Developed DeploymentArtifactModel and DeploymentBatchModel for deployment management.
- Enhanced DeploymentTargetModel and DeploymentTemplateSelectionModel to support template revisions.
- Added TemplateRevisionModel and TemplateVersionModel for versioning templates.
- Implemented ApiClientSecretHasher for secure secret hashing.
- Created ApiTokenService for generating and validating JWT tokens.
- Updated QueueJobService to handle deployment requests with artifacts.
- Configured authentication settings in appsettings.json for JWT and Negotiate authentication.
This commit is contained in:
Torsten Brendgen
2026-07-09 14:44:38 +02:00
parent 1aa2adac08
commit dc93ea0813
72 changed files with 32906 additions and 516 deletions

View File

@@ -19,6 +19,13 @@ namespace Microsoft.SelfService.Portal.Core.API.Context
public DbSet<DeploymentGroupModel> DeploymentGroups { get; set; }
public DbSet<TemplateModel> Templates { get; set; }
public DbSet<TemplateVersionModel> TemplateVersions { get; set; }
public DbSet<TemplateRevisionModel> TemplateRevisions { get; set; }
public DbSet<ConfigurationDefinitionModel> ConfigurationDefinitions { get; set; }
public DbSet<ConfigurationValueModel> ConfigurationValues { get; set; }
public DbSet<DeploymentArtifactModel> DeploymentArtifacts { get; set; }
public DbSet<CredentialSecretModel> CredentialSecrets { get; set; }
public DbSet<ApiClientModel> ApiClients { get; set; }
public DbSet<ApiTokenModel> ApiTokens { get; set; }
public DbSet<DeploymentRuleModel> DeploymentRules { get; set; }
public DbSet<DeploymentRuleStepModel> DeploymentRuleSteps { get; set; }
public DbSet<DeploymentTemplateSelectionModel> DeploymentTemplateSelections { get; set; }
@@ -148,6 +155,18 @@ namespace Microsoft.SelfService.Portal.Core.API.Context
.WithMany(dg => dg.Deployments)
.HasForeignKey(d => d.DeploymentGroupId);
modelBuilder.Entity<DeploymentModel>()
.HasOne(deployment => deployment.TemplateRevision)
.WithMany(revision => revision.Deployments)
.HasForeignKey(deployment => deployment.TemplateRevisionId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<DeploymentModel>()
.HasOne(deployment => deployment.CurrentArtifact)
.WithMany()
.HasForeignKey(deployment => deployment.CurrentArtifactId)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<DeploymentGroupModel>()
.HasOne(dg => dg.Template)
.WithMany(t => t.DeploymentGroups)
@@ -170,6 +189,12 @@ namespace Microsoft.SelfService.Portal.Core.API.Context
.HasForeignKey(selection => selection.TemplateVersionId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<DeploymentTemplateSelectionModel>()
.HasOne(selection => selection.TemplateRevision)
.WithMany(revision => revision.DeploymentTemplateSelections)
.HasForeignKey(selection => selection.TemplateRevisionId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<DeploymentTemplateSelectionModel>()
.HasIndex(selection => new { selection.DeploymentGroupId, selection.SortOrder })
.IsUnique();
@@ -250,6 +275,161 @@ namespace Microsoft.SelfService.Portal.Core.API.Context
modelBuilder.Entity<TemplateVersionModel>()
.ToTable(table => table.HasCheckConstraint("CK_TemplateVersions_JsonData_IsJson", "ISJSON([JsonData]) = 1"));
modelBuilder.Entity<TemplateRevisionModel>()
.HasOne(revision => revision.TemplateVersion)
.WithMany(version => version.TemplateRevisions)
.HasForeignKey(revision => revision.TemplateVersionId);
modelBuilder.Entity<TemplateRevisionModel>()
.HasIndex(revision => new { revision.TemplateVersionId, revision.RevisionNumber })
.IsUnique();
modelBuilder.Entity<TemplateRevisionModel>()
.HasIndex(revision => revision.TemplateVersionId)
.IsUnique()
.HasFilter("[IsCurrent] = 1");
modelBuilder.Entity<TemplateRevisionModel>()
.Property(revision => revision.JsonHash)
.HasMaxLength(64);
modelBuilder.Entity<TemplateRevisionModel>()
.Property(revision => revision.SchemaVersion)
.HasMaxLength(32);
modelBuilder.Entity<TemplateRevisionModel>()
.ToTable(table => table.HasCheckConstraint("CK_TemplateRevisions_JsonData_IsJson", "ISJSON([JsonData]) = 1"));
modelBuilder.Entity<ConfigurationDefinitionModel>()
.HasOne(definition => definition.TemplateRevision)
.WithMany(revision => revision.ConfigurationDefinitions)
.HasForeignKey(definition => definition.TemplateRevisionId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ConfigurationDefinitionModel>()
.HasIndex(definition => new { definition.TemplateRevisionId, definition.Kind, definition.Name })
.IsUnique()
.HasFilter("[TemplateRevisionId] IS NOT NULL");
modelBuilder.Entity<ConfigurationDefinitionModel>()
.Property(definition => definition.Kind)
.HasMaxLength(32);
modelBuilder.Entity<ConfigurationDefinitionModel>()
.Property(definition => definition.DefinitionHash)
.HasMaxLength(64);
modelBuilder.Entity<ConfigurationDefinitionModel>()
.ToTable(table => table.HasCheckConstraint("CK_ConfigurationDefinitions_PropertiesJson_IsJson", "ISJSON([PropertiesJson]) = 1"));
modelBuilder.Entity<ConfigurationValueModel>()
.HasOne(value => value.ConfigurationDefinition)
.WithMany(definition => definition.Values)
.HasForeignKey(value => value.ConfigurationDefinitionId);
modelBuilder.Entity<ConfigurationValueModel>()
.HasIndex(value => new { value.ConfigurationDefinitionId, value.ScopeType, value.ScopeId, value.SortOrder });
modelBuilder.Entity<ConfigurationValueModel>()
.Property(value => value.ScopeType)
.HasMaxLength(64);
modelBuilder.Entity<ConfigurationValueModel>()
.Property(value => value.ValueSourceType)
.HasMaxLength(64);
modelBuilder.Entity<ConfigurationValueModel>()
.Property(value => value.ValueHash)
.HasMaxLength(64);
modelBuilder.Entity<ConfigurationValueModel>()
.ToTable(table => table.HasCheckConstraint("CK_ConfigurationValues_ValueJson_IsJson", "[ValueJson] IS NULL OR ISJSON([ValueJson]) = 1"));
modelBuilder.Entity<DeploymentArtifactModel>()
.HasOne(artifact => artifact.DeploymentGroup)
.WithMany(group => group.Artifacts)
.HasForeignKey(artifact => artifact.DeploymentGroupId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DeploymentArtifactModel>()
.HasOne(artifact => artifact.Deployment)
.WithMany(deployment => deployment.Artifacts)
.HasForeignKey(artifact => new { artifact.TargetId, artifact.DeploymentGroupId })
.HasPrincipalKey(deployment => new { deployment.TargetId, deployment.DeploymentGroupId })
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<DeploymentArtifactModel>()
.HasOne(artifact => artifact.Target)
.WithMany()
.HasForeignKey(artifact => artifact.TargetId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<DeploymentArtifactModel>()
.Property(artifact => artifact.ArtifactType)
.HasMaxLength(64);
modelBuilder.Entity<DeploymentArtifactModel>()
.Property(artifact => artifact.InputHash)
.HasMaxLength(64);
modelBuilder.Entity<DeploymentArtifactModel>()
.Property(artifact => artifact.OutputHash)
.HasMaxLength(64);
modelBuilder.Entity<DeploymentArtifactModel>()
.ToTable(table => table.HasCheckConstraint("CK_DeploymentArtifacts_DeploymentJson_IsJson", "ISJSON([DeploymentJson]) = 1"));
modelBuilder.Entity<DeploymentArtifactModel>()
.ToTable(table => table.HasCheckConstraint("CK_DeploymentArtifacts_SourceJson_IsJson", "[SourceJson] IS NULL OR ISJSON([SourceJson]) = 1"));
modelBuilder.Entity<DeploymentArtifactModel>()
.ToTable(table => table.HasCheckConstraint("CK_DeploymentArtifacts_ResolvedJson_IsJson", "[ResolvedJson] IS NULL OR ISJSON([ResolvedJson]) = 1"));
modelBuilder.Entity<DeploymentArtifactModel>()
.ToTable(table => table.HasCheckConstraint("CK_DeploymentArtifacts_SourceSnapshotJson_IsJson", "[SourceSnapshotJson] IS NULL OR ISJSON([SourceSnapshotJson]) = 1"));
modelBuilder.Entity<CredentialSecretModel>()
.HasIndex(secret => secret.Name)
.IsUnique();
modelBuilder.Entity<CredentialSecretModel>()
.ToTable(table => table.HasCheckConstraint("CK_CredentialSecrets_MetadataJson_IsJson", "[MetadataJson] IS NULL OR ISJSON([MetadataJson]) = 1"));
modelBuilder.Entity<ApiClientModel>()
.HasIndex(client => client.ClientId)
.IsUnique();
modelBuilder.Entity<ApiClientModel>()
.Property(client => client.ClientId)
.HasMaxLength(128);
modelBuilder.Entity<ApiClientModel>()
.ToTable(table => table.HasCheckConstraint("CK_ApiClients_ScopesJson_IsJson", "ISJSON([ScopesJson]) = 1"));
modelBuilder.Entity<ApiTokenModel>()
.HasOne(token => token.ApiClient)
.WithMany()
.HasForeignKey(token => token.ApiClientId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<ApiTokenModel>()
.HasIndex(token => token.Jti)
.IsUnique();
modelBuilder.Entity<ApiTokenModel>()
.HasIndex(token => new { token.SubjectType, token.Subject });
modelBuilder.Entity<ApiTokenModel>()
.Property(token => token.Jti)
.HasMaxLength(64);
modelBuilder.Entity<ApiTokenModel>()
.Property(token => token.SubjectType)
.HasMaxLength(32);
modelBuilder.Entity<ApiTokenModel>()
.ToTable(table => table.HasCheckConstraint("CK_ApiTokens_ScopesJson_IsJson", "ISJSON([ScopesJson]) = 1"));
modelBuilder.Entity<DeploymentRuleStepModel>()
.HasOne(step => step.DeploymentRule)
.WithMany(rule => rule.Steps)