Files
gorbenko.pavel d076f4e98d
Tests / backend-test (push) Successful in 1m50s
Tests / frontend-test (push) Successful in 49s
Sla (#11)
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/11
2026-08-13 04:44:08 +00:00

71 lines
3.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}
}