using System.Security.Cryptography.X509Certificates; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Tnsu.ApprovalRoute.Infrastructure.Persistence; namespace Tnsu.ApprovalRoute.Infrastructure.SharePoint; public interface ICertificateProvider { Task GetCertificateAsync(CancellationToken cancellationToken); } /// /// Сертификат для входа в SharePoint — из таблицы settings (option_name задаётся /// в SharePointOptions.CertificateSettingName, по умолчанию 'AzurePfx'). /// /// Синглтон с кешем: разбор .pfx стоит заметно дороже запроса к базе, а сам /// сертификат меняется раз в год. Блокировка — чтобы параллельные запросы не /// разбирали его одновременно при холодном старте. /// /// Собственная таблица, а не общая с api-tnsu-portal, — осознанный выбор: базы /// у сервисов разные и связывать их не хотим. Цена — копию сертификата придётся /// обновлять в двух местах. /// public sealed class DbCertificateProvider( IServiceScopeFactory scopeFactory, IOptions options) : ICertificateProvider { private readonly SemaphoreSlim _lock = new(1, 1); private X509Certificate2? _cached; public async Task GetCertificateAsync(CancellationToken cancellationToken) { if (_cached is not null) return _cached; await _lock.WaitAsync(cancellationToken); try { if (_cached is not null) return _cached; var settingName = options.Value.CertificateSettingName; // Своя область: провайдер — синглтон, а DbContext зарегистрирован // как scoped, напрямую внедрять его сюда нельзя. using var scope = scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var pfx = await db.Settings.AsNoTracking() .Where(x => x.OptionName == settingName) .Select(x => x.FileData) .FirstOrDefaultAsync(cancellationToken); if (pfx is null || pfx.Length == 0) throw new InvalidOperationException( $"Сертификат не найден: в таблице settings нет записи «{settingName}» с непустым file_data."); _cached = X509CertificateLoader.LoadPkcs12( pfx, options.Value.CertificatePassword, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet); return _cached; } finally { _lock.Release(); } } }