Files
Microsoft.SelfService.Porta…/Repository/TemplateRepository.cs
Torsten Brendgen dc93ea0813 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.
2026-07-09 14:44:38 +02:00

465 lines
18 KiB
C#

using Microsoft.EntityFrameworkCore;
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
{
public class TemplateRepository : ITemplateInterface
{
private readonly DataContext _context;
public TemplateRepository(DataContext context)
{
_context = context;
}
public ICollection<TemplateModel> GetTemplates()
{
var templates = _context.Templates
.AsNoTracking()
.Include(template => template.TemplateVersions)
.ToList();
foreach (var template in templates)
{
ApplyPublishedVersion(template);
}
return templates;
}
public TemplateModel GetTemplateById(Guid Id)
{
var template = _context.Templates
.AsNoTracking()
.Include(t => t.TemplateVersions)
.Where(t => t.Id == Id)
.FirstOrDefault();
if (template != null)
{
ApplyPublishedVersion(template);
}
return template;
}
public bool AddTemplateById(TemplateModel template)
{
if (template.Id == Guid.Empty)
{
template.Id = Guid.NewGuid();
}
var document = ConfigurationDocumentValidator.NormalizeAndValidate(template.JSONData, ConfigurationDocumentKind.Template);
template.JSONData = document.JsonData;
var version = new TemplateVersionModel
{
Id = Guid.NewGuid(),
TemplateId = template.Id,
Version = string.IsNullOrWhiteSpace(template.Version) ? "1.0.0" : template.Version,
SchemaVersion = document.SchemaVersion,
IsPublished = true,
PublishedAt = DateTime.UtcNow,
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);
return SaveChanges();
}
public bool EditTemplateById(TemplateModel template)
{
var existingTemplate = _context.Templates
.Include(existing => existing.TemplateVersions)
.FirstOrDefault(existing => existing.Id == template.Id);
if (existingTemplate == null)
{
return false;
}
var document = ConfigurationDocumentValidator.NormalizeAndValidate(template.JSONData, ConfigurationDocumentKind.Template);
var jsonData = document.JsonData;
var publishedVersion = existingTemplate.TemplateVersions.FirstOrDefault(version => version.IsPublished);
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;
existingTemplate.Description = template.Description;
existingTemplate.JSONData = jsonData;
existingTemplate.TemplateCategoryId = template.TemplateCategoryId;
existingTemplate.DeploymentRuleId = template.DeploymentRuleId;
existingTemplate.Modified = DateTime.UtcNow;
existingTemplate.ModifiedBy = template.ModifiedBy;
if (jsonChanged)
{
var version = publishedVersion;
if (version == null || !string.Equals(version.Version, template.Version, StringComparison.OrdinalIgnoreCase))
{
var versionName = GetUniqueVersionName(
existingTemplate.Id,
string.IsNullOrWhiteSpace(template.Version) ? DateTime.UtcNow.ToString("yyyyMMddHHmmss") : template.Version);
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;
}
var revision = CreateTemplateRevision(version.Id, jsonData, document.SchemaVersion, true, template.ModifiedBy);
_context.TemplateRevisions.Add(revision);
}
return SaveChanges();
}
public bool DeleteTemplateById(TemplateModel template)
{
_context.Remove(template);
return SaveChanges();
}
public bool CheckTemplateById(Guid Id)
{
return _context.Templates
.Any(t => t.Id == Id);
}
public TemplateModel GetTemplateByName(string Name)
{
return _context.Templates
.AsNoTracking()
.Where(t => t.Name == Name)
.FirstOrDefault();
}
public bool CheckTemplateByName(string Name)
{
return _context.Templates
.Any(t => t.Name == Name);
}
public ICollection<TemplateVersionModel> GetTemplateVersions(Guid templateId)
{
return _context.TemplateVersions
.AsNoTracking()
.Include(version => version.TemplateRevisions)
.Where(version => version.TemplateId == templateId)
.OrderByDescending(version => version.Created)
.ToList();
}
public TemplateVersionModel? GetTemplateVersionById(Guid templateId, Guid versionId)
{
return _context.TemplateVersions
.AsNoTracking()
.Include(version => version.TemplateRevisions)
.FirstOrDefault(version => version.TemplateId == templateId && version.Id == versionId);
}
public TemplateVersionModel? GetPublishedTemplateVersion(Guid templateId)
{
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))
{
return false;
}
version.Id = version.Id == Guid.Empty ? Guid.NewGuid() : version.Id;
version.TemplateId = templateId;
version.Version = string.IsNullOrWhiteSpace(version.Version) ? "1.0.0" : version.Version;
var document = ConfigurationDocumentValidator.NormalizeAndValidate(
version.JsonData,
ConfigurationDocumentKind.Template,
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)
{
foreach (var existingVersion in _context.TemplateVersions.Where(existing => existing.TemplateId == templateId && existing.IsPublished))
{
existingVersion.IsPublished = false;
}
version.IsPublished = true;
version.PublishedAt = DateTime.UtcNow;
}
_context.TemplateVersions.Add(version);
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
.FirstOrDefault(existing => existing.TemplateId == templateId && existing.Id == versionId);
if (version == null)
{
return false;
}
foreach (var existingVersion in _context.TemplateVersions.Where(existing => existing.TemplateId == templateId && existing.IsPublished))
{
existingVersion.IsPublished = false;
}
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)
{
template.Version = version.Version;
template.JSONData = version.JsonData;
template.Modified = DateTime.UtcNow;
template.ModifiedBy = version.PublishedBy;
}
return SaveChanges();
}
public bool CheckTemplateVersionById(Guid templateId, Guid versionId)
{
return _context.TemplateVersions
.Any(version => version.TemplateId == templateId && version.Id == versionId);
}
public bool CheckTemplateVersionByVersion(Guid templateId, string version)
{
return _context.TemplateVersions
.Any(existing => existing.TemplateId == templateId && existing.Version == version);
}
public bool SaveChanges()
{
var saved = _context.SaveChanges();
return saved > 0 ? true : false;
}
private static void ApplyPublishedVersion(TemplateModel template)
{
var publishedVersion = template.TemplateVersions?.FirstOrDefault(version => version.IsPublished);
if (publishedVersion == null)
{
return;
}
template.Version = publishedVersion.Version;
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)
{
if (!CheckTemplateVersionByVersion(templateId, version))
{
return version;
}
var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
var candidate = $"{version}-{timestamp}";
var counter = 1;
while (CheckTemplateVersionByVersion(templateId, candidate))
{
counter++;
candidate = $"{version}-{timestamp}-{counter}";
}
return candidate;
}
}
}