Phase 1 WP-2–10: Repositories, audit service, security & auth (LDAP, JWT, roles, policies, data protection)

- WP-2: SecurityRepository + CentralUiRepository with audit log queries
- WP-3: AuditService with transactional guarantee (same SaveChangesAsync)
- WP-4: Optimistic concurrency tests (deployment records vs template last-write-wins)
- WP-5: Seed data (SCADA-Admins → Admin role mapping)
- WP-6: LdapAuthService (direct bind, TLS enforcement, group query)
- WP-7: JwtTokenService (HMAC-SHA256, 15-min refresh, 30-min idle timeout)
- WP-8: RoleMapper (LDAP groups → roles with site-scoped deployment)
- WP-9: Authorization policies (Admin/Design/Deployment + site scope handler)
- WP-10: Shared Data Protection keys via EF Core
141 tests pass, zero warnings.
This commit is contained in:
Joseph Doherty
2026-03-16 19:32:43 -04:00
parent 1996b21961
commit cafb7d2006
31 changed files with 3356 additions and 8 deletions
@@ -0,0 +1,93 @@
using Microsoft.EntityFrameworkCore;
using ScadaLink.Commons.Entities.Security;
using ScadaLink.Commons.Interfaces.Repositories;
namespace ScadaLink.ConfigurationDatabase.Repositories;
public class SecurityRepository : ISecurityRepository
{
private readonly ScadaLinkDbContext _context;
public SecurityRepository(ScadaLinkDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
// LdapGroupMapping
public async Task<LdapGroupMapping?> GetMappingByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.LdapGroupMappings.FindAsync(new object[] { id }, cancellationToken);
}
public async Task<IReadOnlyList<LdapGroupMapping>> GetAllMappingsAsync(CancellationToken cancellationToken = default)
{
return await _context.LdapGroupMappings.ToListAsync(cancellationToken);
}
public async Task<IReadOnlyList<LdapGroupMapping>> GetMappingsByRoleAsync(string role, CancellationToken cancellationToken = default)
{
return await _context.LdapGroupMappings
.Where(m => m.Role == role)
.ToListAsync(cancellationToken);
}
public async Task AddMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default)
{
await _context.LdapGroupMappings.AddAsync(mapping, cancellationToken);
}
public Task UpdateMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default)
{
_context.LdapGroupMappings.Update(mapping);
return Task.CompletedTask;
}
public async Task DeleteMappingAsync(int id, CancellationToken cancellationToken = default)
{
var mapping = await _context.LdapGroupMappings.FindAsync(new object[] { id }, cancellationToken);
if (mapping != null)
{
_context.LdapGroupMappings.Remove(mapping);
}
}
// SiteScopeRule
public async Task<SiteScopeRule?> GetScopeRuleByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.SiteScopeRules.FindAsync(new object[] { id }, cancellationToken);
}
public async Task<IReadOnlyList<SiteScopeRule>> GetScopeRulesForMappingAsync(int ldapGroupMappingId, CancellationToken cancellationToken = default)
{
return await _context.SiteScopeRules
.Where(r => r.LdapGroupMappingId == ldapGroupMappingId)
.ToListAsync(cancellationToken);
}
public async Task AddScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default)
{
await _context.SiteScopeRules.AddAsync(rule, cancellationToken);
}
public Task UpdateScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default)
{
_context.SiteScopeRules.Update(rule);
return Task.CompletedTask;
}
public async Task DeleteScopeRuleAsync(int id, CancellationToken cancellationToken = default)
{
var rule = await _context.SiteScopeRules.FindAsync(new object[] { id }, cancellationToken);
if (rule != null)
{
_context.SiteScopeRules.Remove(rule);
}
}
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
return await _context.SaveChangesAsync(cancellationToken);
}
}