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

@@ -55,6 +55,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
var templateVersion = requestedTemplateVersionId.HasValue
? _context.TemplateVersions
.Include(version => version.TemplateRevisions)
.Include(version => version.Template)
.ThenInclude(template => template.TemplateCategory)
.ThenInclude(category => category.Service)
@@ -66,6 +67,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
.Include(existing => existing.TemplateCategory)
.ThenInclude(existing => existing.Service)
.Include(existing => existing.TemplateVersions)
.ThenInclude(version => version.TemplateRevisions)
.FirstOrDefault(existing => existing.Id == deploymentBatch.TemplateId);
if (template == null)
@@ -128,11 +130,32 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
Id = Guid.NewGuid(),
DeploymentGroupId = deploymentBatch.Id,
TemplateVersionId = templateVersion.Id,
TemplateRevisionId = templateVersion.TemplateRevisions?.FirstOrDefault(revision => revision.IsCurrent)?.Id,
TemplateRole = "Service",
SortOrder = 10,
Alias = template.Name
});
}
else
{
foreach (var selection in deploymentBatch.TemplateSelections)
{
if (selection.TemplateRevisionId.HasValue)
{
continue;
}
var selectedVersion = selection.TemplateVersionId == templateVersion?.Id
? templateVersion
: _context.TemplateVersions
.Include(version => version.TemplateRevisions)
.FirstOrDefault(version => version.Id == selection.TemplateVersionId);
selection.TemplateRevisionId = selectedVersion?.TemplateRevisions
?.FirstOrDefault(revision => revision.IsCurrent)
?.Id;
}
}
if (deploymentBatch.TargetAssignments.Count == 0)
{
@@ -170,6 +193,10 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
{
DeploymentGroupId = deploymentBatch.Id,
TargetId = targetId,
TemplateRevisionId = deploymentBatch.TemplateSelections
.OrderBy(selection => selection.SortOrder)
.Select(selection => selection.TemplateRevisionId)
.FirstOrDefault(),
Status = QueueJobStatus.Pending,
JSONData = "{}"
});

View File

@@ -3,6 +3,7 @@ using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Interfaces;
using Microsoft.SelfService.Portal.Core.API.JsonDocuments;
using Microsoft.SelfService.Portal.Core.API.Models;
using System.Text.Json;
namespace Microsoft.SelfService.Portal.Core.API.Repository
{
@@ -67,6 +68,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
PublishedBy = template.CreatedBy
};
version.SetJsonData(document.JsonData);
version.TemplateRevisions.Add(CreateTemplateRevision(version.Id, document.JsonData, document.SchemaVersion, true, template.CreatedBy));
template.TemplateVersions.Add(version);
_context.Add(template);
@@ -87,7 +89,8 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
var document = ConfigurationDocumentValidator.NormalizeAndValidate(template.JSONData, ConfigurationDocumentKind.Template);
var jsonData = document.JsonData;
var publishedVersion = existingTemplate.TemplateVersions.FirstOrDefault(version => version.IsPublished);
var jsonChanged = publishedVersion == null || !string.Equals(publishedVersion.JsonData, jsonData, StringComparison.Ordinal);
var currentRevision = publishedVersion == null ? null : GetCurrentTemplateRevision(publishedVersion.Id);
var jsonChanged = currentRevision == null || !string.Equals(currentRevision.JsonData, jsonData, StringComparison.Ordinal);
existingTemplate.Name = template.Name;
existingTemplate.Version = template.Version;
@@ -100,28 +103,39 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
if (jsonChanged)
{
var versionName = GetUniqueVersionName(
existingTemplate.Id,
string.IsNullOrWhiteSpace(template.Version) ? DateTime.UtcNow.ToString("yyyyMMddHHmmss") : template.Version);
var version = new TemplateVersionModel
var version = publishedVersion;
if (version == null || !string.Equals(version.Version, template.Version, StringComparison.OrdinalIgnoreCase))
{
Id = Guid.NewGuid(),
TemplateId = existingTemplate.Id,
Version = versionName,
SchemaVersion = document.SchemaVersion,
IsPublished = true,
PublishedAt = DateTime.UtcNow,
PublishedBy = template.ModifiedBy
};
version.SetJsonData(jsonData);
var versionName = GetUniqueVersionName(
existingTemplate.Id,
string.IsNullOrWhiteSpace(template.Version) ? DateTime.UtcNow.ToString("yyyyMMddHHmmss") : template.Version);
foreach (var existingVersion in existingTemplate.TemplateVersions)
version = new TemplateVersionModel
{
Id = Guid.NewGuid(),
TemplateId = existingTemplate.Id,
Version = versionName,
SchemaVersion = document.SchemaVersion,
IsPublished = true,
PublishedAt = DateTime.UtcNow,
PublishedBy = template.ModifiedBy
};
existingTemplate.TemplateVersions.Add(version);
}
version.SetJsonData(jsonData);
version.SchemaVersion = document.SchemaVersion;
version.IsPublished = true;
version.PublishedAt = DateTime.UtcNow;
version.PublishedBy = template.ModifiedBy;
foreach (var existingVersion in existingTemplate.TemplateVersions.Where(existing => existing.Id != version.Id))
{
existingVersion.IsPublished = false;
}
existingTemplate.TemplateVersions.Add(version);
var revision = CreateTemplateRevision(version.Id, jsonData, document.SchemaVersion, true, template.ModifiedBy);
_context.TemplateRevisions.Add(revision);
}
return SaveChanges();
@@ -157,6 +171,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
{
return _context.TemplateVersions
.AsNoTracking()
.Include(version => version.TemplateRevisions)
.Where(version => version.TemplateId == templateId)
.OrderByDescending(version => version.Created)
.ToList();
@@ -166,6 +181,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
{
return _context.TemplateVersions
.AsNoTracking()
.Include(version => version.TemplateRevisions)
.FirstOrDefault(version => version.TemplateId == templateId && version.Id == versionId);
}
@@ -173,9 +189,45 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
{
return _context.TemplateVersions
.AsNoTracking()
.Include(version => version.TemplateRevisions)
.FirstOrDefault(version => version.TemplateId == templateId && version.IsPublished);
}
public ICollection<TemplateRevisionModel> GetTemplateRevisions(Guid templateId, Guid versionId)
{
if (!_context.TemplateVersions.Any(version => version.TemplateId == templateId && version.Id == versionId))
{
return new List<TemplateRevisionModel>();
}
return _context.TemplateRevisions
.AsNoTracking()
.Include(revision => revision.ConfigurationDefinitions)
.Where(revision => revision.TemplateVersionId == versionId)
.OrderByDescending(revision => revision.RevisionNumber)
.ToList();
}
public TemplateRevisionModel? GetTemplateRevisionById(Guid templateId, Guid versionId, Guid revisionId)
{
if (!_context.TemplateVersions.Any(version => version.TemplateId == templateId && version.Id == versionId))
{
return null;
}
return _context.TemplateRevisions
.AsNoTracking()
.Include(revision => revision.ConfigurationDefinitions)
.FirstOrDefault(revision => revision.TemplateVersionId == versionId && revision.Id == revisionId);
}
public TemplateRevisionModel? GetCurrentTemplateRevision(Guid versionId)
{
return _context.TemplateRevisions
.AsNoTracking()
.FirstOrDefault(revision => revision.TemplateVersionId == versionId && revision.IsCurrent);
}
public bool AddTemplateVersion(Guid templateId, TemplateVersionModel version, bool publish)
{
if (!_context.Templates.Any(template => template.Id == templateId))
@@ -192,6 +244,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
version.SchemaVersion);
version.SchemaVersion = document.SchemaVersion;
version.SetJsonData(document.JsonData);
version.TemplateRevisions.Add(CreateTemplateRevision(version.Id, document.JsonData, document.SchemaVersion, publish, version.PublishedBy));
if (publish)
{
@@ -208,6 +261,42 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
return SaveChanges();
}
public bool AddTemplateRevision(Guid templateId, Guid versionId, TemplateRevisionModel revision, bool publish)
{
var version = _context.TemplateVersions
.Include(existing => existing.TemplateRevisions)
.FirstOrDefault(existing => existing.TemplateId == templateId && existing.Id == versionId);
if (version == null)
{
return false;
}
var document = ConfigurationDocumentValidator.NormalizeAndValidate(
revision.JsonData,
ConfigurationDocumentKind.Template,
revision.SchemaVersion);
revision = CreateTemplateRevision(versionId, document.JsonData, document.SchemaVersion, publish, revision.PublishedBy);
if (publish)
{
foreach (var existingRevision in _context.TemplateRevisions.Where(existing => existing.TemplateVersionId == versionId && existing.IsCurrent))
{
existingRevision.IsCurrent = false;
existingRevision.IsPublished = false;
}
version.SetJsonData(document.JsonData);
version.SchemaVersion = document.SchemaVersion;
version.PublishedAt = DateTime.UtcNow;
version.PublishedBy = revision.PublishedBy;
}
_context.TemplateRevisions.Add(revision);
return SaveChanges();
}
public bool PublishTemplateVersion(Guid templateId, Guid versionId, string? publishedBy)
{
var version = _context.TemplateVersions
@@ -226,6 +315,13 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
version.IsPublished = true;
version.PublishedAt = DateTime.UtcNow;
version.PublishedBy = string.IsNullOrWhiteSpace(publishedBy) ? "System" : publishedBy;
var currentRevision = _context.TemplateRevisions.FirstOrDefault(revision => revision.TemplateVersionId == version.Id && revision.IsCurrent);
if (currentRevision != null)
{
currentRevision.IsPublished = true;
currentRevision.PublishedAt = DateTime.UtcNow;
currentRevision.PublishedBy = version.PublishedBy;
}
var template = _context.Templates.FirstOrDefault(existing => existing.Id == templateId);
if (template != null)
@@ -266,7 +362,82 @@ namespace Microsoft.SelfService.Portal.Core.API.Repository
}
template.Version = publishedVersion.Version;
template.JSONData = publishedVersion.JsonData;
var currentRevision = publishedVersion.TemplateRevisions?.FirstOrDefault(revision => revision.IsCurrent);
template.JSONData = currentRevision?.JsonData ?? publishedVersion.JsonData;
}
private TemplateRevisionModel CreateTemplateRevision(Guid templateVersionId, string jsonData, string schemaVersion, bool publish, string? publishedBy)
{
var revision = new TemplateRevisionModel
{
Id = Guid.NewGuid(),
TemplateVersionId = templateVersionId,
RevisionNumber = GetNextRevisionNumber(templateVersionId),
SchemaVersion = schemaVersion,
IsCurrent = true,
IsPublished = publish,
PublishedAt = publish ? DateTime.UtcNow : null,
PublishedBy = publishedBy ?? "System"
};
revision.SetJsonData(jsonData);
foreach (var existingRevision in _context.TemplateRevisions.Where(existing => existing.TemplateVersionId == templateVersionId && existing.IsCurrent))
{
existingRevision.IsCurrent = false;
}
foreach (var definition in ExtractConfigurationDefinitions(revision.Id, jsonData))
{
revision.ConfigurationDefinitions.Add(definition);
}
return revision;
}
private int GetNextRevisionNumber(Guid templateVersionId)
{
return (_context.TemplateRevisions
.Where(revision => revision.TemplateVersionId == templateVersionId)
.Select(revision => (int?)revision.RevisionNumber)
.Max() ?? 0) + 1;
}
private static IEnumerable<ConfigurationDefinitionModel> ExtractConfigurationDefinitions(Guid revisionId, string jsonData)
{
using var document = JsonDocument.Parse(jsonData);
var root = document.RootElement;
foreach (var definition in ExtractConfigurationDefinitions(revisionId, root, "parameters", ConfigurationDefinitionKinds.Parameter))
{
yield return definition;
}
foreach (var definition in ExtractConfigurationDefinitions(revisionId, root, "variables", ConfigurationDefinitionKinds.Variable))
{
yield return definition;
}
}
private static IEnumerable<ConfigurationDefinitionModel> ExtractConfigurationDefinitions(Guid revisionId, JsonElement root, string propertyName, string kind)
{
if (!root.TryGetProperty(propertyName, out var definitions) || definitions.ValueKind != JsonValueKind.Object)
{
yield break;
}
foreach (var property in definitions.EnumerateObject())
{
var propertiesJson = property.Value.GetRawText();
yield return new ConfigurationDefinitionModel
{
Id = Guid.NewGuid(),
TemplateRevisionId = revisionId,
Kind = kind,
Name = property.Name,
PropertiesJson = propertiesJson,
DefinitionHash = TemplateVersionModel.ComputeSha256(propertiesJson)
};
}
}
private string GetUniqueVersionName(Guid templateId, string version)