| | | 1 | | using Elsa.Identity.Contracts; |
| | | 2 | | using Elsa.Identity.Options; |
| | | 3 | | using JetBrains.Annotations; |
| | | 4 | | using Microsoft.Extensions.DependencyInjection; |
| | | 5 | | using Microsoft.Extensions.Hosting; |
| | | 6 | | using Microsoft.Extensions.Logging; |
| | | 7 | | using Microsoft.Extensions.Options; |
| | | 8 | | |
| | | 9 | | namespace Elsa.Identity.HostedServices; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Reports, at startup, an instance nobody can sign in to: an empty user store with no bootstrap configured. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <remarks> |
| | | 15 | | /// Bootstrapping the first identity is a chicken-and-egg problem — every management endpoint requires a |
| | | 16 | | /// permission, which requires a role, which requires a user. Elsa answers it declaratively: configure |
| | | 17 | | /// <see cref="DefaultAdminUserOptions"/> to seed an admin, or <see cref="AdminApiKeyOptions"/> to accept an |
| | | 18 | | /// out-of-band key. Both work in a deployed environment, and both attach an identity to whatever the caller |
| | | 19 | | /// then does. |
| | | 20 | | /// |
| | | 21 | | /// This replaces the localhost permission grant that used to ride on the SecurityRoot policy. That grant |
| | | 22 | | /// trusted network position, which is exactly the signal that stops meaning anything behind a reverse proxy, |
| | | 23 | | /// inside a container, or across a port-forward — and it granted unauthenticated access, so the bootstrap |
| | | 24 | | /// action had no identity to audit. What it did offer was a hint that something needed configuring; without |
| | | 25 | | /// it, an unconfigured instance would answer every request with 403 and no explanation. This says so instead. |
| | | 26 | | /// </remarks> |
| | | 27 | | [UsedImplicitly] |
| | 8 | 28 | | public class IdentityBootstrapDiagnostic( |
| | 8 | 29 | | IServiceScopeFactory scopeFactory, |
| | 8 | 30 | | IOptions<DefaultAdminUserOptions> adminUserOptions, |
| | 8 | 31 | | IOptions<AdminApiKeyOptions> adminApiKeyOptions, |
| | 8 | 32 | | ILogger<IdentityBootstrapDiagnostic> logger) : IHostedService |
| | | 33 | | { |
| | | 34 | | /// <inheritdoc /> |
| | | 35 | | public async Task StartAsync(CancellationToken cancellationToken) |
| | | 36 | | { |
| | 8 | 37 | | var admin = adminUserOptions.Value; |
| | 8 | 38 | | var adminUserConfigured = !string.IsNullOrWhiteSpace(admin.AdminUserName) && !string.IsNullOrWhiteSpace(admin.Ad |
| | 8 | 39 | | var apiKeyConfigured = !string.IsNullOrWhiteSpace(adminApiKeyOptions.Value.ApiKey); |
| | | 40 | | |
| | 8 | 41 | | if (adminUserConfigured || apiKeyConfigured) |
| | 5 | 42 | | return; |
| | | 43 | | |
| | | 44 | | try |
| | | 45 | | { |
| | 3 | 46 | | using var scope = scopeFactory.CreateScope(); |
| | 3 | 47 | | var userStore = scope.ServiceProvider.GetRequiredService<IUserStore>(); |
| | | 48 | | |
| | 3 | 49 | | if ((await userStore.FindManyAsync(new(), cancellationToken)).Any()) |
| | 1 | 50 | | return; |
| | | 51 | | |
| | 1 | 52 | | logger.LogError( |
| | 1 | 53 | | "No users exist and no identity bootstrap is configured, so nothing can sign in and every " + |
| | 1 | 54 | | "management endpoint will answer 403. Configure one of: (1) a seeded administrator via " + |
| | 1 | 55 | | "UseDefaultAdmin(...) or the DefaultAdminUser configuration section, which creates the admin " + |
| | 1 | 56 | | "role and user at startup and is idempotent; or (2) an admin API key via UseAdminApiKey(...) " + |
| | 1 | 57 | | "or the AdminApiKey setting. Both work in a deployed environment."); |
| | 1 | 58 | | } |
| | 1 | 59 | | catch (Exception e) |
| | | 60 | | { |
| | | 61 | | // A store that cannot be read yet is not this check's problem to report; it will surface on its |
| | | 62 | | // own. Never let a diagnostic take the host down. |
| | 1 | 63 | | logger.LogDebug(e, "Could not determine whether the user store is empty; skipping the bootstrap check."); |
| | 1 | 64 | | } |
| | 8 | 65 | | } |
| | | 66 | | |
| | | 67 | | /// <inheritdoc /> |
| | 6 | 68 | | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; |
| | | 69 | | } |