feat: Implement PostgreSQL repositories for various entities
- Added BootstrapInviteRepository for managing bootstrap invites. - Added ClientRepository for handling OAuth/OpenID clients. - Introduced LoginAttemptRepository for logging login attempts. - Created OidcTokenRepository for managing OpenIddict tokens and refresh tokens. - Implemented RevocationExportStateRepository for persisting revocation export state. - Added RevocationRepository for managing revocations. - Introduced ServiceAccountRepository for handling service accounts.
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Ldap.Bootstrap;
|
||||
|
||||
internal sealed class LdapBootstrapAuditDocument
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
|
||||
|
||||
[BsonElement("plugin")]
|
||||
public string Plugin { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("username")]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("dn")]
|
||||
public string DistinguishedName { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("operation")]
|
||||
public string Operation { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("secretHash")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? SecretHash { get; set; }
|
||||
|
||||
[BsonElement("timestamp")]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
[BsonElement("metadata")]
|
||||
public Dictionary<string, string?> Metadata { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Ldap.Claims;
|
||||
|
||||
internal sealed class InMemoryLdapClaimsCache : ILdapClaimsCache
|
||||
{
|
||||
private readonly string pluginName;
|
||||
private readonly LdapClaimsCacheOptions options;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly ConcurrentDictionary<string, LdapClaimsCacheEntry> entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeSpan entryLifetime;
|
||||
|
||||
public InMemoryLdapClaimsCache(string pluginName, LdapClaimsCacheOptions options, TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(pluginName);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
this.pluginName = pluginName;
|
||||
this.options = options;
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
entryLifetime = TimeSpan.FromSeconds(options.TtlSeconds);
|
||||
}
|
||||
|
||||
public ValueTask<LdapCachedClaims?> GetAsync(string subjectId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectId);
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
if (entries.TryGetValue(BuildKey(subjectId), out var entry))
|
||||
{
|
||||
if (entry.ExpiresAt <= now)
|
||||
{
|
||||
entries.TryRemove(BuildKey(subjectId), out _);
|
||||
return ValueTask.FromResult<LdapCachedClaims?>(null);
|
||||
}
|
||||
|
||||
return ValueTask.FromResult<LdapCachedClaims?>(entry.Claims);
|
||||
}
|
||||
|
||||
return ValueTask.FromResult<LdapCachedClaims?>(null);
|
||||
}
|
||||
|
||||
public ValueTask SetAsync(string subjectId, LdapCachedClaims claims, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectId);
|
||||
ArgumentNullException.ThrowIfNull(claims);
|
||||
|
||||
if (options.MaxEntries > 0)
|
||||
{
|
||||
EnforceCapacity(options.MaxEntries);
|
||||
}
|
||||
|
||||
var expiresAt = timeProvider.GetUtcNow() + entryLifetime;
|
||||
entries[BuildKey(subjectId)] = new LdapClaimsCacheEntry(claims, expiresAt);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void EnforceCapacity(int maxEntries)
|
||||
{
|
||||
while (entries.Count >= maxEntries)
|
||||
{
|
||||
var oldest = entries.OrderBy(kv => kv.Value.ExpiresAt).FirstOrDefault();
|
||||
if (oldest.Key is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
entries.TryRemove(oldest.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildKey(string subjectId) => $"{pluginName}:{subjectId}".ToLowerInvariant();
|
||||
|
||||
private sealed record LdapClaimsCacheEntry(LdapCachedClaims Claims, DateTimeOffset ExpiresAt);
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Ldap.Claims;
|
||||
|
||||
internal sealed class MongoLdapClaimsCache : ILdapClaimsCache
|
||||
{
|
||||
private readonly string pluginName;
|
||||
private readonly IMongoCollection<LdapClaimsCacheDocument> collection;
|
||||
private readonly LdapClaimsCacheOptions options;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly ILogger<MongoLdapClaimsCache> logger;
|
||||
private readonly TimeSpan entryLifetime;
|
||||
|
||||
public MongoLdapClaimsCache(
|
||||
string pluginName,
|
||||
IMongoDatabase database,
|
||||
LdapClaimsCacheOptions cacheOptions,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<MongoLdapClaimsCache> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pluginName);
|
||||
ArgumentNullException.ThrowIfNull(database);
|
||||
ArgumentNullException.ThrowIfNull(cacheOptions);
|
||||
this.pluginName = pluginName;
|
||||
options = cacheOptions;
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
entryLifetime = TimeSpan.FromSeconds(cacheOptions.TtlSeconds);
|
||||
var collectionName = cacheOptions.ResolveCollectionName(pluginName);
|
||||
collection = database.GetCollection<LdapClaimsCacheDocument>(collectionName);
|
||||
EnsureIndexes();
|
||||
}
|
||||
|
||||
public async ValueTask<LdapCachedClaims?> GetAsync(string subjectId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectId);
|
||||
|
||||
var document = await collection
|
||||
.Find(doc => doc.Id == BuildDocumentId(subjectId))
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (document is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (document.ExpiresAt <= timeProvider.GetUtcNow())
|
||||
{
|
||||
await collection.DeleteOneAsync(doc => doc.Id == document.Id, cancellationToken).ConfigureAwait(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> roles = document.Roles is { Count: > 0 }
|
||||
? document.Roles.AsReadOnly()
|
||||
: Array.Empty<string>();
|
||||
|
||||
var attributes = document.Attributes is { Count: > 0 }
|
||||
? new Dictionary<string, string>(document.Attributes, StringComparer.OrdinalIgnoreCase)
|
||||
: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return new LdapCachedClaims(roles, attributes);
|
||||
}
|
||||
|
||||
public async ValueTask SetAsync(string subjectId, LdapCachedClaims claims, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectId);
|
||||
ArgumentNullException.ThrowIfNull(claims);
|
||||
|
||||
if (options.MaxEntries > 0)
|
||||
{
|
||||
await EnforceCapacityAsync(options.MaxEntries, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var document = new LdapClaimsCacheDocument
|
||||
{
|
||||
Id = BuildDocumentId(subjectId),
|
||||
Plugin = pluginName,
|
||||
SubjectId = subjectId,
|
||||
CachedAt = now,
|
||||
ExpiresAt = now + entryLifetime,
|
||||
Roles = claims.Roles?.ToList() ?? new List<string>(),
|
||||
Attributes = claims.Attributes?.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value,
|
||||
StringComparer.OrdinalIgnoreCase) ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
};
|
||||
|
||||
await collection.ReplaceOneAsync(
|
||||
existing => existing.Id == document.Id,
|
||||
document,
|
||||
new ReplaceOptions { IsUpsert = true },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string BuildDocumentId(string subjectId)
|
||||
=> $"{pluginName}:{subjectId}".ToLowerInvariant();
|
||||
|
||||
private async Task EnforceCapacityAsync(int maxEntries, CancellationToken cancellationToken)
|
||||
{
|
||||
var total = await collection.CountDocumentsAsync(
|
||||
Builders<LdapClaimsCacheDocument>.Filter.Empty,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (total < maxEntries)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var surplus = (int)(total - maxEntries + 1);
|
||||
var ids = await collection
|
||||
.Find(Builders<LdapClaimsCacheDocument>.Filter.Empty)
|
||||
.SortBy(doc => doc.CachedAt)
|
||||
.Limit(surplus)
|
||||
.Project(doc => doc.Id)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var deleteFilter = Builders<LdapClaimsCacheDocument>.Filter.In(doc => doc.Id, ids);
|
||||
await collection.DeleteManyAsync(deleteFilter, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void EnsureIndexes()
|
||||
{
|
||||
var expiresIndex = Builders<LdapClaimsCacheDocument>.IndexKeys.Ascending(doc => doc.ExpiresAt);
|
||||
var indexModel = new CreateIndexModel<LdapClaimsCacheDocument>(
|
||||
expiresIndex,
|
||||
new CreateIndexOptions
|
||||
{
|
||||
Name = "idx_expires_at",
|
||||
ExpireAfter = TimeSpan.Zero
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
collection.Indexes.CreateOne(indexModel);
|
||||
}
|
||||
catch (MongoCommandException ex) when (ex.CodeName.Equals("IndexOptionsConflict", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogDebug(ex, "LDAP claims cache index already exists for plugin {Plugin}.", pluginName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LdapClaimsCacheDocument
|
||||
{
|
||||
[BsonId]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("plugin")]
|
||||
public string Plugin { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("subjectId")]
|
||||
public string SubjectId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("roles")]
|
||||
public List<string> Roles { get; set; } = new();
|
||||
|
||||
[BsonElement("attributes")]
|
||||
public Dictionary<string, string> Attributes { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
[BsonElement("cachedAt")]
|
||||
public DateTimeOffset CachedAt { get; set; }
|
||||
|
||||
[BsonElement("expiresAt")]
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
}
|
||||
@@ -5,8 +5,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Authority.Plugin.Ldap.Connections;
|
||||
using StellaOps.Authority.Plugin.Ldap.Security;
|
||||
@@ -32,7 +30,7 @@ internal sealed class LdapClientProvisioningStore : IClientProvisioningStore
|
||||
private readonly IAuthorityRevocationStore revocationStore;
|
||||
private readonly ILdapConnectionFactory connectionFactory;
|
||||
private readonly IOptionsMonitor<LdapPluginOptions> optionsMonitor;
|
||||
private readonly IMongoDatabase mongoDatabase;
|
||||
private readonly IAuthorityAirgapAuditStore auditStore;
|
||||
private readonly TimeProvider clock;
|
||||
private readonly ILogger<LdapClientProvisioningStore> logger;
|
||||
|
||||
@@ -42,7 +40,7 @@ internal sealed class LdapClientProvisioningStore : IClientProvisioningStore
|
||||
IAuthorityRevocationStore revocationStore,
|
||||
ILdapConnectionFactory connectionFactory,
|
||||
IOptionsMonitor<LdapPluginOptions> optionsMonitor,
|
||||
IMongoDatabase mongoDatabase,
|
||||
IAuthorityAirgapAuditStore auditStore,
|
||||
TimeProvider clock,
|
||||
ILogger<LdapClientProvisioningStore> logger)
|
||||
{
|
||||
@@ -51,7 +49,7 @@ internal sealed class LdapClientProvisioningStore : IClientProvisioningStore
|
||||
this.revocationStore = revocationStore ?? throw new ArgumentNullException(nameof(revocationStore));
|
||||
this.connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
this.optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
|
||||
this.mongoDatabase = mongoDatabase ?? throw new ArgumentNullException(nameof(mongoDatabase));
|
||||
this.auditStore = auditStore ?? throw new ArgumentNullException(nameof(auditStore));
|
||||
this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
@@ -198,26 +196,35 @@ internal sealed class LdapClientProvisioningStore : IClientProvisioningStore
|
||||
}
|
||||
|
||||
var collectionName = options.ResolveAuditCollectionName(pluginName);
|
||||
var collection = mongoDatabase.GetCollection<LdapClientProvisioningAuditDocument>(collectionName);
|
||||
|
||||
var record = new LdapClientProvisioningAuditDocument
|
||||
var properties = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
Plugin = pluginName,
|
||||
ClientId = document.ClientId,
|
||||
DistinguishedName = BuildDistinguishedName(document.ClientId, options),
|
||||
Operation = operation,
|
||||
SecretHash = document.SecretHash,
|
||||
Tenant = document.Properties.TryGetValue(AuthorityClientMetadataKeys.Tenant, out var tenant) ? tenant : null,
|
||||
Project = document.Properties.TryGetValue(AuthorityClientMetadataKeys.Project, out var project) ? project : null,
|
||||
Timestamp = clock.GetUtcNow(),
|
||||
Metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["senderConstraint"] = document.SenderConstraint,
|
||||
["plugin"] = pluginName
|
||||
}
|
||||
["senderConstraint"] = document.SenderConstraint,
|
||||
["plugin"] = pluginName,
|
||||
["distinguishedName"] = BuildDistinguishedName(document.ClientId, options),
|
||||
["collection"] = collectionName,
|
||||
["tenant"] = document.Properties.TryGetValue(AuthorityClientMetadataKeys.Tenant, out var tenant) ? tenant : null,
|
||||
["project"] = document.Properties.TryGetValue(AuthorityClientMetadataKeys.Project, out var project) ? project : null,
|
||||
["secretHash"] = document.SecretHash
|
||||
};
|
||||
|
||||
await collection.InsertOneAsync(record, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var auditDocument = new AuthorityAirgapAuditDocument
|
||||
{
|
||||
EventType = $"ldap.client.{operation}",
|
||||
OperatorId = pluginName,
|
||||
ComponentId = collectionName,
|
||||
Outcome = "success",
|
||||
OccurredAt = clock.GetUtcNow(),
|
||||
Properties = properties
|
||||
.Where(kv => !string.IsNullOrWhiteSpace(kv.Value) || kv.Key is "plugin" or "collection" or "distinguishedName")
|
||||
.Select(kv => new AuthorityAirgapAuditPropertyDocument
|
||||
{
|
||||
Name = kv.Key,
|
||||
Value = kv.Value ?? string.Empty
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
await auditStore.InsertAsync(auditDocument, cancellationToken, session: null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RecordRevocationAsync(string clientId, CancellationToken cancellationToken)
|
||||
@@ -429,39 +436,3 @@ internal sealed class LdapClientProvisioningStore : IClientProvisioningStore
|
||||
private LdapClientProvisioningOptions GetProvisioningOptions()
|
||||
=> optionsMonitor.Get(pluginName).ClientProvisioning;
|
||||
}
|
||||
|
||||
internal sealed class LdapClientProvisioningAuditDocument
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
|
||||
|
||||
[BsonElement("plugin")]
|
||||
public string Plugin { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("clientId")]
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("dn")]
|
||||
public string DistinguishedName { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("operation")]
|
||||
public string Operation { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("secretHash")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? SecretHash { get; set; }
|
||||
|
||||
[BsonElement("tenant")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? Tenant { get; set; }
|
||||
|
||||
[BsonElement("project")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? Project { get; set; }
|
||||
|
||||
[BsonElement("timestamp")]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
[BsonElement("metadata")]
|
||||
public Dictionary<string, string?> Metadata { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using StellaOps.Authority.Plugin.Ldap.Bootstrap;
|
||||
using StellaOps.Authority.Plugin.Ldap.ClientProvisioning;
|
||||
using StellaOps.Authority.Plugin.Ldap.Connections;
|
||||
using StellaOps.Authority.Plugin.Ldap.Monitoring;
|
||||
using StellaOps.Authority.Plugin.Ldap.Security;
|
||||
using StellaOps.Authority.Storage.Mongo.Documents;
|
||||
using StellaOps.Authority.Storage.Mongo.Stores;
|
||||
using StellaOps.Cryptography.Audit;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Ldap.Credentials;
|
||||
@@ -27,7 +27,7 @@ internal sealed class LdapCredentialStore : IUserCredentialStore
|
||||
private readonly ILdapConnectionFactory connectionFactory;
|
||||
private readonly ILogger<LdapCredentialStore> logger;
|
||||
private readonly LdapMetrics metrics;
|
||||
private readonly IMongoDatabase mongoDatabase;
|
||||
private readonly IAuthorityAirgapAuditStore auditStore;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly Func<TimeSpan, CancellationToken, Task> delayAsync;
|
||||
|
||||
@@ -37,7 +37,7 @@ internal sealed class LdapCredentialStore : IUserCredentialStore
|
||||
ILdapConnectionFactory connectionFactory,
|
||||
ILogger<LdapCredentialStore> logger,
|
||||
LdapMetrics metrics,
|
||||
IMongoDatabase mongoDatabase,
|
||||
IAuthorityAirgapAuditStore auditStore,
|
||||
TimeProvider timeProvider,
|
||||
Func<TimeSpan, CancellationToken, Task>? delayAsync = null)
|
||||
{
|
||||
@@ -46,7 +46,7 @@ internal sealed class LdapCredentialStore : IUserCredentialStore
|
||||
this.connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||
this.mongoDatabase = mongoDatabase ?? throw new ArgumentNullException(nameof(mongoDatabase));
|
||||
this.auditStore = auditStore ?? throw new ArgumentNullException(nameof(auditStore));
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
this.delayAsync = delayAsync ?? ((delay, token) => Task.Delay(delay, token));
|
||||
}
|
||||
@@ -511,31 +511,35 @@ internal sealed class LdapCredentialStore : IUserCredentialStore
|
||||
}
|
||||
|
||||
var collectionName = options.ResolveAuditCollectionName(pluginName);
|
||||
var collection = mongoDatabase.GetCollection<LdapBootstrapAuditDocument>(collectionName);
|
||||
|
||||
var document = new LdapBootstrapAuditDocument
|
||||
var metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
Plugin = pluginName,
|
||||
Username = NormalizeUsername(registration.Username),
|
||||
DistinguishedName = distinguishedName,
|
||||
Operation = "upsert",
|
||||
SecretHash = string.IsNullOrWhiteSpace(registration.Password)
|
||||
? null
|
||||
: AuthoritySecretHasher.ComputeHash(registration.Password!),
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["requirePasswordReset"] = registration.RequirePasswordReset ? "true" : "false",
|
||||
["email"] = registration.Email
|
||||
}
|
||||
["requirePasswordReset"] = registration.RequirePasswordReset ? "true" : "false",
|
||||
["email"] = registration.Email,
|
||||
["dn"] = distinguishedName,
|
||||
["collection"] = collectionName,
|
||||
["username"] = NormalizeUsername(registration.Username)
|
||||
};
|
||||
|
||||
foreach (var attribute in registration.Attributes)
|
||||
{
|
||||
document.Metadata[$"attr.{attribute.Key}"] = attribute.Value;
|
||||
metadata[$"attr.{attribute.Key}"] = attribute.Value;
|
||||
}
|
||||
|
||||
await collection.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var auditDocument = new AuthorityAirgapAuditDocument
|
||||
{
|
||||
EventType = "ldap.bootstrap.upsert",
|
||||
OperatorId = pluginName,
|
||||
ComponentId = collectionName,
|
||||
Outcome = "success",
|
||||
OccurredAt = timeProvider.GetUtcNow(),
|
||||
Properties = metadata.Select(pair => new AuthorityAirgapAuditPropertyDocument
|
||||
{
|
||||
Name = pair.Key,
|
||||
Value = pair.Value ?? string.Empty
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
await auditStore.InsertAsync(auditDocument, cancellationToken, session: null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, IReadOnlyCollection<string>> BuildBootstrapAttributes(
|
||||
|
||||
@@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using StellaOps.Authority.Plugin.Ldap.ClientProvisioning;
|
||||
using StellaOps.Authority.Plugin.Ldap.Claims;
|
||||
@@ -51,7 +50,7 @@ internal sealed class LdapPluginRegistrar : IAuthorityPluginRegistrar
|
||||
sp.GetRequiredService<ILdapConnectionFactory>(),
|
||||
sp.GetRequiredService<ILogger<LdapCredentialStore>>(),
|
||||
sp.GetRequiredService<LdapMetrics>(),
|
||||
sp.GetRequiredService<IMongoDatabase>(),
|
||||
sp.GetRequiredService<IAuthorityAirgapAuditStore>(),
|
||||
ResolveTimeProvider(sp)));
|
||||
|
||||
context.Services.AddScoped(sp => new LdapClientProvisioningStore(
|
||||
@@ -60,7 +59,7 @@ internal sealed class LdapPluginRegistrar : IAuthorityPluginRegistrar
|
||||
sp.GetRequiredService<IAuthorityRevocationStore>(),
|
||||
sp.GetRequiredService<ILdapConnectionFactory>(),
|
||||
sp.GetRequiredService<IOptionsMonitor<LdapPluginOptions>>(),
|
||||
sp.GetRequiredService<IMongoDatabase>(),
|
||||
sp.GetRequiredService<IAuthorityAirgapAuditStore>(),
|
||||
sp.GetRequiredService<TimeProvider>(),
|
||||
sp.GetRequiredService<ILogger<LdapClientProvisioningStore>>()));
|
||||
|
||||
@@ -75,12 +74,10 @@ internal sealed class LdapPluginRegistrar : IAuthorityPluginRegistrar
|
||||
return DisabledLdapClaimsCache.Instance;
|
||||
}
|
||||
|
||||
return new MongoLdapClaimsCache(
|
||||
return new InMemoryLdapClaimsCache(
|
||||
pluginName,
|
||||
sp.GetRequiredService<IMongoDatabase>(),
|
||||
cacheOptions,
|
||||
ResolveTimeProvider(sp),
|
||||
sp.GetRequiredService<ILogger<MongoLdapClaimsCache>>());
|
||||
ResolveTimeProvider(sp));
|
||||
});
|
||||
|
||||
context.Services.AddScoped(sp => new LdapClaimsEnricher(
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
<ProjectReference Include="..\\StellaOps.Auth.Abstractions\\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Authority.Storage.Mongo\\StellaOps.Authority.Storage.Mongo.csproj" />
|
||||
<ProjectReference Include="..\\..\\..\\__Libraries\\StellaOps.Plugin\\StellaOps.Plugin.csproj" />
|
||||
<ProjectReference Include="..\\..\\..\\__Libraries\\StellaOps.Authority.Storage.Postgres\\StellaOps.Authority.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Authority.Storage.Postgres\\StellaOps.Authority.Storage.Postgres.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user