d076f4e98d
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/11
71 lines
3.0 KiB
C#
71 lines
3.0 KiB
C#
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<X509Certificate2> GetCertificateAsync(CancellationToken cancellationToken);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сертификат для входа в SharePoint — из таблицы settings (option_name задаётся
|
||
/// в SharePointOptions.CertificateSettingName, по умолчанию 'AzurePfx').
|
||
///
|
||
/// Синглтон с кешем: разбор .pfx стоит заметно дороже запроса к базе, а сам
|
||
/// сертификат меняется раз в год. Блокировка — чтобы параллельные запросы не
|
||
/// разбирали его одновременно при холодном старте.
|
||
///
|
||
/// Собственная таблица, а не общая с api-tnsu-portal, — осознанный выбор: базы
|
||
/// у сервисов разные и связывать их не хотим. Цена — копию сертификата придётся
|
||
/// обновлять в двух местах.
|
||
/// </summary>
|
||
public sealed class DbCertificateProvider(
|
||
IServiceScopeFactory scopeFactory,
|
||
IOptions<SharePointOptions> options) : ICertificateProvider
|
||
{
|
||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||
private X509Certificate2? _cached;
|
||
|
||
public async Task<X509Certificate2> 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<ApprovalRouteDbContext>();
|
||
|
||
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();
|
||
}
|
||
}
|
||
}
|