< Summary

Information
Class: Elsa.ExternalAuthentication.Endpoints.Connections.ManagedSecretBindingCleanup
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ConnectionManagementEndpoints.cs
Line coverage
50%
Covered lines: 3
Uncovered lines: 3
Coverable lines: 6
Total lines: 528
Line coverage: 50%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryRemoveAsync()100%1150%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ConnectionManagementEndpoints.cs

#LineLine coverage
 1using Elsa.Authorization;
 2using System.Text;
 3using System.Text.Json;
 4using Elsa.Abstractions;
 5using Elsa.Common.Multitenancy;
 6using Elsa.ExternalAuthentication.Contracts;
 7using Elsa.ExternalAuthentication.Models;
 8using Elsa.ExternalAuthentication.Permissions;
 9using Elsa.ExternalAuthentication.Services;
 10using Microsoft.AspNetCore.Http;
 11using Microsoft.Extensions.Logging;
 12
 13namespace Elsa.ExternalAuthentication.Endpoints.Connections;
 14
 15internal sealed class ListConnections(IdentityProviderConnectionManagementService management, IConnectionObservationStor
 16{
 17    public override void Configure()
 18    {
 19        Get("/external-authentication/connections");
 20        RequirePermission(ExternalAuthenticationResourcePermissions.Connections, CoreVerbs.View);
 21    }
 22
 23    public override async Task<ConnectionListResponse> ExecuteAsync(ConnectionListRequest request, CancellationToken can
 24    {
 25        var requestedScope = request.Scope ?? request.ScopeKind;
 26        var scope = ConnectionScope.Host;
 27        if ((!string.IsNullOrWhiteSpace(requestedScope) && !requestedScope.Equals("host", StringComparison.OrdinalIgnore
 28            !string.IsNullOrWhiteSpace(request.TenantId) ||
 29            request.PageSize is < 1 or > 100 ||
 30            !IsKnownSource(request.Source) ||
 31            !TryDecodeCursor(request.Cursor, out var cursor))
 32        {
 33            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
 34            return new([], null);
 35        }
 36
 37        var filter = new ConnectionFilter
 38        {
 39            Search = request.Search,
 40            Ownership = request.Source?.Equals("configuration", StringComparison.OrdinalIgnoreCase) == true ? Connection
 41            Scope = scope,
 42            AdapterType = request.AdapterType,
 43            IsEnabled = request.Enabled,
 44            IsArchived = request.Archived
 45        };
 46        var connections = (await management.ListAsync(tenantAccessor.TenantId, filter, cancellationToken))
 47            .Where(x => !request.Valid.HasValue || request.Valid.Value == (x.Validity == ConnectionValidity.Valid))
 48            .Where(x => !request.Shadowed.HasValue || request.Shadowed.Value == x.IsShadowed)
 49            .OrderBy(x => x.Scope.Kind)
 50            .ThenBy(x => x.Scope.TenantId, StringComparer.Ordinal)
 51            .ThenBy(x => x.Connection.DisplayOrder)
 52            .ThenBy(x => x.Connection.Key, StringComparer.Ordinal)
 53            .ThenBy(x => x.Connection.Id, StringComparer.Ordinal)
 54            .Where(x => cursor is null || Compare(CursorFor(x), cursor) > 0)
 55            .Take(request.PageSize.GetValueOrDefault(100) + 1)
 56            .ToArray();
 57        var hasNextPage = connections.Length > request.PageSize.GetValueOrDefault(100);
 58        var page = hasNextPage ? connections[..^1] : connections;
 59        var observationResults = await Task.WhenAll(page.Select(x => observations.FindLatestAsync(x.Connection.Id, cance
 60        var items = await Task.WhenAll(page.Select((x, index) => ConnectionResponse.FromAsync(x, management, adapters, o
 61        return new(items, hasNextPage ? EncodeCursor(CursorFor(page[^1])) : null);
 62    }
 63
 64    private static bool IsKnownSource(string? source) => string.IsNullOrWhiteSpace(source) ||
 65        source.Equals("configuration", StringComparison.OrdinalIgnoreCase) ||
 66        source.Equals("database", StringComparison.OrdinalIgnoreCase);
 67
 68    private static ConnectionCursor CursorFor(EffectiveIdentityProviderConnection connection) => new((int)connection.Sco
 69    private static int Compare(ConnectionCursor left, ConnectionCursor right)
 70    {
 71        var result = left.ScopeKind.CompareTo(right.ScopeKind);
 72        result = result != 0 ? result : string.Compare(left.TenantId, right.TenantId, StringComparison.Ordinal);
 73        result = result != 0 ? result : left.Order.CompareTo(right.Order);
 74        result = result != 0 ? result : string.Compare(left.Key, right.Key, StringComparison.Ordinal);
 75        return result != 0 ? result : string.Compare(left.Id, right.Id, StringComparison.Ordinal);
 76    }
 77
 78    private static string EncodeCursor(ConnectionCursor cursor) => Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSer
 79    private static bool TryDecodeCursor(string? value, out ConnectionCursor? cursor)
 80    {
 81        cursor = null;
 82        if (string.IsNullOrWhiteSpace(value))
 83            return true;
 84        try
 85        {
 86            var padded = value.Replace('-', '+').Replace('_', '/');
 87            padded = padded.PadRight(padded.Length + (4 - padded.Length % 4) % 4, '=');
 88            cursor = JsonSerializer.Deserialize<ConnectionCursor>(Encoding.UTF8.GetString(Convert.FromBase64String(padde
 89            return cursor is not null &&
 90                cursor.ScopeKind is >= (int)ConnectionScopeKind.Host and <= (int)ConnectionScopeKind.Tenant &&
 91                cursor.TenantId is not null && cursor.Key is not null && cursor.Id is not null &&
 92                cursor.TenantId.Length <= 256 && cursor.Key.Length <= 128 && cursor.Id.Length <= 256;
 93        }
 94        catch (Exception exception) when (exception is FormatException or JsonException)
 95        {
 96            return false;
 97        }
 98    }
 99
 100    private sealed record ConnectionCursor(int ScopeKind, string TenantId, int Order, string Key, string Id);
 101}
 102
 103internal sealed class GetConnection(IdentityProviderConnectionManagementService management, IConnectionObservationStore 
 104{
 105    public override void Configure()
 106    {
 107        Get("/external-authentication/connections/{connectionId}");
 108        RequirePermission(ExternalAuthenticationResourcePermissions.Connections, CoreVerbs.View);
 109    }
 110
 111    public override async Task HandleAsync(CancellationToken cancellationToken)
 112    {
 113        var result = await management.FindAsync(Route<string>("connectionId")!, tenantAccessor.TenantId, cancellationTok
 114        if (result is not ManagementConnectionLookupResult.Found(var connection))
 115        {
 116            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status404NotFound, "not_found", "The
 117            return;
 118        }
 119
 120        ConnectionEndpointSupport.SetEtag(HttpContext, connection.Connection.Revision);
 121        await HttpContext.Response.WriteAsJsonAsync(await ConnectionResponse.FromAsync(connection, management, adapters,
 122    }
 123}
 124
 125internal sealed class CreateConnection(IdentityProviderConnectionManagementService management, IExternalAuthenticationAd
 126{
 127    public override void Configure()
 128    {
 129        Post("/external-authentication/connections");
 130        RequirePermission(ExternalAuthenticationResourcePermissions.Connections, CoreVerbs.Create);
 131    }
 132
 133    public override async Task HandleAsync(ConnectionRequest request, CancellationToken cancellationToken)
 134    {
 135        if (!request.HasOnlyHostScope())
 136        {
 137            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status400BadRequest, "host_scope_req
 138            return;
 139        }
 140        if (request.SecretBindings is not null)
 141        {
 142            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status400BadRequest, "secret_binding
 143            return;
 144        }
 145        if (ConnectionEndpointSupport.RequiresPolicyManagement(request) && !ConnectionEndpointSupport.HasPermission(Http
 146        {
 147            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status403Forbidden, "forbidden", "Th
 148            return;
 149        }
 150
 151        var result = await management.CreateAsync(request.ToConnection(), User, tenantAccessor.TenantId, request.Confirm
 152        if (result is not ManagementConnectionMutationResult.Success(var connection))
 153        {
 154            await ConnectionEndpointSupport.SendMutationResultAsync(HttpContext, result, management, cancellationToken);
 155            return;
 156        }
 157
 158        var effective = new EffectiveIdentityProviderConnection(connection, ConnectionSourceOwnership.Database, ToScope(
 159        HttpContext.Response.StatusCode = StatusCodes.Status201Created;
 160        HttpContext.Response.Headers.Location = $"/external-authentication/connections/{Uri.EscapeDataString(connection.
 161        ConnectionEndpointSupport.SetEtag(HttpContext, connection.Revision);
 162        await HttpContext.Response.WriteAsJsonAsync(await ConnectionResponse.FromAsync(effective, management, adapters, 
 163    }
 164
 165    private static ConnectionScope ToScope(string tenantId) => tenantId == ConnectionScope.HostTenantId ? ConnectionScop
 166}
 167
 168internal sealed class UpdateConnection(IdentityProviderConnectionManagementService management, IExternalAuthenticationAd
 169{
 170    public override void Configure()
 171    {
 172        Put("/external-authentication/connections/{connectionId}");
 173        RequirePermission(ExternalAuthenticationResourcePermissions.Connections, CoreVerbs.Update);
 174    }
 175
 176    public override async Task HandleAsync(ConnectionRequest request, CancellationToken cancellationToken)
 177    {
 178        if (!request.HasOnlyHostScope())
 179        {
 180            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status400BadRequest, "host_scope_req
 181            return;
 182        }
 183        if (!ConnectionEndpointSupport.TryGetExpectedRevision(HttpContext, out var revision))
 184        {
 185            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status428PreconditionRequired, "prec
 186            return;
 187        }
 188
 189        var existing = await management.FindAsync(Route<string>("connectionId")!, tenantAccessor.TenantId, cancellationT
 190        if (existing is not ManagementConnectionLookupResult.Found(var effective))
 191        {
 192            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status404NotFound, "not_found", "The
 193            return;
 194        }
 195        if (!ConnectionEndpointSupport.IsDatabaseOwned(effective))
 196        {
 197            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status403Forbidden, "forbidden", "Co
 198            return;
 199        }
 200        if (request.SecretBindings is not null)
 201        {
 202            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status400BadRequest, "secret_binding
 203            return;
 204        }
 205        if (ConnectionEndpointSupport.RequiresPolicyManagement(request) && !ConnectionEndpointSupport.HasPermission(Http
 206        {
 207            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status403Forbidden, "forbidden", "Th
 208            return;
 209        }
 210
 211        var candidate = request.ToConnection();
 212        if (request.SecretBindings is null)
 213            candidate.SecretBindings = IdentityProviderConnectionCloner.Clone(effective.Connection).SecretBindings;
 214
 215        var result = await management.UpdateAsync(effective.Connection.Id, candidate, revision, User, tenantAccessor.Ten
 216        if (result is not ManagementConnectionMutationResult.Success(var connection))
 217        {
 218            await ConnectionEndpointSupport.SendMutationResultAsync(HttpContext, result, management, cancellationToken);
 219            return;
 220        }
 221
 222        var responseConnection = new EffectiveIdentityProviderConnection(connection, ConnectionSourceOwnership.Database,
 223        ConnectionEndpointSupport.SetEtag(HttpContext, connection.Revision);
 224        await HttpContext.Response.WriteAsJsonAsync(await ConnectionResponse.FromAsync(responseConnection, management, a
 225    }
 226}
 227
 228internal abstract class ConnectionLifecycleEndpoint(IdentityProviderConnectionManagementService management, IExternalAut
 229{
 230    protected abstract ConnectionLifecycle Action { get; }
 231    protected abstract string Verb { get; }
 232    protected abstract void ConfigureRoute();
 233
 234    public override void Configure()
 235    {
 236        ConfigureRoute();
 237        RequirePermission(ExternalAuthenticationResourcePermissions.Connections, Verb);
 238    }
 239
 240    public override async Task HandleAsync(CancellationToken cancellationToken)
 241    {
 242        if (!ConnectionEndpointSupport.TryGetExpectedRevision(HttpContext, out var revision))
 243        {
 244            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status428PreconditionRequired, "prec
 245            return;
 246        }
 247        var existing = await management.FindAsync(Route<string>("connectionId")!, tenantAccessor.TenantId, cancellationT
 248        if (existing is not ManagementConnectionLookupResult.Found(var effective))
 249        {
 250            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status404NotFound, "not_found", "The
 251            return;
 252        }
 253        if (!ConnectionEndpointSupport.IsDatabaseOwned(effective))
 254        {
 255            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status403Forbidden, "forbidden", "Co
 256            return;
 257        }
 258
 259        var confirmOverride = string.Equals(HttpContext.Request.Query["confirmFinalLoginPathOverride"], "true", StringCo
 260        var revokeActiveSessions = string.Equals(HttpContext.Request.Query["revokeActiveSessions"], "true", StringCompar
 261        if (revokeActiveSessions && !ConnectionEndpointSupport.HasPermission(HttpContext, ExternalAuthenticationResource
 262        {
 263            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status403Forbidden, "forbidden", "Re
 264            return;
 265        }
 266        var result = await management.ChangeLifecycleAsync(effective.Connection.Id, Action, revision, User, tenantAccess
 267        if (result is not ManagementConnectionMutationResult.Success(var connection))
 268        {
 269            await ConnectionEndpointSupport.SendMutationResultAsync(HttpContext, result, management, cancellationToken);
 270            return;
 271        }
 272
 273        ConnectionEndpointSupport.SetEtag(HttpContext, connection.Revision);
 274        HttpContext.Response.StatusCode = StatusCodes.Status200OK;
 275        await HttpContext.Response.WriteAsJsonAsync(await ConnectionResponse.FromAsync(new(connection, ConnectionSourceO
 276    }
 277}
 278
 279internal sealed class EnableConnection(IdentityProviderConnectionManagementService management, IExternalAuthenticationAd
 280{
 281    protected override ConnectionLifecycle Action => ConnectionLifecycle.Enabled;
 282    protected override string Verb => CoreVerbs.Update;
 283    protected override void ConfigureRoute() => Post("/external-authentication/connections/{connectionId}/enable");
 284}
 285
 286internal sealed class DisableConnection(IdentityProviderConnectionManagementService management, IExternalAuthenticationA
 287{
 288    protected override ConnectionLifecycle Action => ConnectionLifecycle.Disabled;
 289    protected override string Verb => CoreVerbs.Update;
 290    protected override void ConfigureRoute() => Post("/external-authentication/connections/{connectionId}/disable");
 291}
 292
 293internal sealed class ArchiveConnection(IdentityProviderConnectionManagementService management, IExternalAuthenticationA
 294{
 295    protected override ConnectionLifecycle Action => ConnectionLifecycle.Archived;
 296    protected override string Verb => ExternalAuthenticationVerbs.Archive;
 297    protected override void ConfigureRoute() => Delete("/external-authentication/connections/{connectionId}");
 298}
 299
 300internal sealed class RestoreConnection(IdentityProviderConnectionManagementService management, IExternalAuthenticationA
 301{
 302    protected override ConnectionLifecycle Action => ConnectionLifecycle.Draft;
 303    protected override string Verb => ExternalAuthenticationVerbs.Archive;
 304    protected override void ConfigureRoute() => Post("/external-authentication/connections/{connectionId}/restore");
 305}
 306
 307internal sealed class ValidateConnection(IdentityProviderConnectionManagementService management, ITenantAccessor tenantA
 308{
 309    public override void Configure()
 310    {
 311        Post("/external-authentication/connections/{connectionId}/validate");
 312        RequirePermission(ExternalAuthenticationResourcePermissions.Connections, CoreVerbs.View);
 313    }
 314
 315    public override async Task HandleAsync(CancellationToken cancellationToken)
 316    {
 317        var result = await management.FindAsync(Route<string>("connectionId")!, tenantAccessor.TenantId, cancellationTok
 318        if (result is not ManagementConnectionLookupResult.Found(var connection))
 319        {
 320            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status404NotFound, "not_found", "The
 321            return;
 322        }
 323
 324        var validation = await management.ValidateAsync(connection.Connection, User, tenantAccessor.TenantId, requireCom
 325        await HttpContext.Response.WriteAsJsonAsync(new ConnectionValidationResponse(validation.IsValid, validation.Erro
 326    }
 327}
 328
 329/// <summary>Stores a write-only secret through an installed managed secret writer.</summary>
 330internal sealed class ReplaceManagedSecretBinding(
 331    IdentityProviderConnectionManagementService management,
 332    IExternalAuthenticationAdapterRegistry adapters,
 333    IIdentityProviderConnectionStore store,
 334    IEnumerable<IManagedSecretBindingWriter> managedSecretBindingWriters,
 335    ITenantAccessor tenantAccessor,
 336    ILogger<ReplaceManagedSecretBinding> logger) : ElsaEndpoint<ManagedSecretBindingRequest>
 337{
 338    private readonly IReadOnlyDictionary<string, IManagedSecretBindingWriter> _writers = managedSecretBindingWriters.ToD
 339
 340    public override void Configure()
 341    {
 342        Put("/external-authentication/connections/{connectionId}/secret-bindings/{fieldName}/managed");
 343        RequirePermission(ExternalAuthenticationResourcePermissions.Connections, CoreVerbs.Update);
 344    }
 345
 346    public override async Task HandleAsync(ManagedSecretBindingRequest request, CancellationToken cancellationToken)
 347    {
 348        if (!ConnectionEndpointSupport.TryGetExpectedRevision(HttpContext, out var revision))
 349        {
 350            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status428PreconditionRequired, "prec
 351            return;
 352        }
 353
 354        var fieldName = Route<string>("fieldName")!;
 355        if (string.IsNullOrWhiteSpace(request.Value) || string.IsNullOrWhiteSpace(request.ResolverType) || !_writers.Try
 356        {
 357            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status400BadRequest, "invalid_manage
 358            return;
 359        }
 360
 361        var lookup = await management.FindAsync(Route<string>("connectionId")!, tenantAccessor.TenantId, cancellationTok
 362        if (lookup is not ManagementConnectionLookupResult.Found(var effective))
 363        {
 364            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status404NotFound, "not_found", "The
 365            return;
 366        }
 367        if (!ConnectionEndpointSupport.IsDatabaseOwned(effective))
 368        {
 369            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status403Forbidden, "forbidden", "Co
 370            return;
 371        }
 372        if (effective.Connection.Revision != revision)
 373        {
 374            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status412PreconditionFailed, "revisi
 375            return;
 376        }
 377        if (!adapters.TryGet(effective.Connection.AdapterType, out var adapter) || !adapter.Describe().Fields.Any(x => x
 378        {
 379            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status400BadRequest, "undeclared_sec
 380            return;
 381        }
 382
 383        using var value = new SensitiveString(request.Value);
 384        var stagedBinding = await writer.StageAsync(new(effective.Connection.Id, fieldName, value), cancellationToken);
 385        if (effective.Connection.SecretBindings.TryGetValue(fieldName, out var liveBinding) &&
 386            string.Equals(liveBinding.ResolverType, stagedBinding.ResolverType, StringComparison.Ordinal) &&
 387            string.Equals(liveBinding.Reference, stagedBinding.Reference, StringComparison.Ordinal))
 388            throw new InvalidOperationException("A managed secret writer returned the live secret reference instead of a
 389        var candidate = IdentityProviderConnectionCloner.Clone(effective.Connection);
 390        candidate.SecretBindings[fieldName] = stagedBinding;
 391        ManagementConnectionMutationResult result;
 392        try
 393        {
 394            result = await management.UpdateAsync(candidate.Id, candidate, revision, User, tenantAccessor.TenantId, fals
 395        }
 396        catch
 397        {
 398            await CleanupAfterExceptionalFailureAsync();
 399            throw;
 400        }
 401        if (result is not ManagementConnectionMutationResult.Success(var connection))
 402        {
 403            await ManagedSecretBindingCleanup.TryRemoveAsync(writer, stagedBinding, effective.Connection.Id, logger);
 404            await ConnectionEndpointSupport.SendMutationResultAsync(HttpContext, result, management, cancellationToken);
 405            return;
 406        }
 407
 408        if (effective.Connection.SecretBindings.TryGetValue(fieldName, out var previousBinding) &&
 409            previousBinding.Ownership == SecretBindingOwnership.Managed &&
 410            _writers.TryGetValue(previousBinding.ResolverType, out var previousWriter))
 411            await ManagedSecretBindingCleanup.TryRemoveAsync(previousWriter, previousBinding, effective.Connection.Id, l
 412
 413        ConnectionEndpointSupport.SetEtag(HttpContext, connection.Revision);
 414        await HttpContext.Response.WriteAsJsonAsync(await ConnectionResponse.FromAsync(new(connection, ConnectionSourceO
 415
 416        return;
 417
 418        async ValueTask CleanupAfterExceptionalFailureAsync()
 419        {
 420            try
 421            {
 422                var persisted = await store.FindByIdAsync(effective.Connection.Id, CancellationToken.None);
 423                var stagedBindingWasPublished = persisted?.SecretBindings.TryGetValue(fieldName, out var publishedBindin
 424                    string.Equals(publishedBinding.ResolverType, stagedBinding.ResolverType, StringComparison.Ordinal) &
 425                    string.Equals(publishedBinding.Reference, stagedBinding.Reference, StringComparison.Ordinal);
 426                if (!stagedBindingWasPublished)
 427                    await ManagedSecretBindingCleanup.TryRemoveAsync(writer, stagedBinding, effective.Connection.Id, log
 428            }
 429            catch (Exception verificationException)
 430            {
 431                logger.LogWarning(
 432                    verificationException,
 433                    "Could not verify whether staged external-authentication secret material was published for connectio
 434                    effective.Connection.Id);
 435            }
 436        }
 437    }
 438}
 439
 440internal sealed class RemoveSecretBinding(
 441    IdentityProviderConnectionManagementService management,
 442    IExternalAuthenticationAdapterRegistry adapters,
 443    IEnumerable<IManagedSecretBindingWriter> managedSecretBindingWriters,
 444    ITenantAccessor tenantAccessor,
 445    ILogger<RemoveSecretBinding> logger) : ElsaEndpointWithoutRequest
 446{
 447    private readonly IReadOnlyDictionary<string, IManagedSecretBindingWriter> _writers = managedSecretBindingWriters.ToD
 448
 449    public override void Configure()
 450    {
 451        Delete("/external-authentication/connections/{connectionId}/secret-bindings/{fieldName}");
 452        RequirePermission(ExternalAuthenticationResourcePermissions.Connections, CoreVerbs.Update);
 453    }
 454
 455    public override async Task HandleAsync(CancellationToken cancellationToken)
 456    {
 457        if (!ConnectionEndpointSupport.TryGetExpectedRevision(HttpContext, out var revision))
 458        {
 459            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status428PreconditionRequired, "prec
 460            return;
 461        }
 462        var lookup = await management.FindAsync(Route<string>("connectionId")!, tenantAccessor.TenantId, cancellationTok
 463        if (lookup is not ManagementConnectionLookupResult.Found(var effective))
 464        {
 465            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status404NotFound, "not_found", "The
 466            return;
 467        }
 468        if (!ConnectionEndpointSupport.IsDatabaseOwned(effective))
 469        {
 470            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status403Forbidden, "forbidden", "Co
 471            return;
 472        }
 473        var fieldName = Route<string>("fieldName")!;
 474        if (!effective.Connection.SecretBindings.TryGetValue(fieldName, out var existingBinding))
 475        {
 476            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status404NotFound, "not_found", "The
 477            return;
 478        }
 479        if (existingBinding.Ownership != SecretBindingOwnership.Managed)
 480        {
 481            await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status403Forbidden, "external_secret
 482            return;
 483        }
 484        var candidate = IdentityProviderConnectionCloner.Clone(effective.Connection);
 485        candidate.SecretBindings.Remove(fieldName);
 486        var result = await management.UpdateAsync(candidate.Id, candidate, revision, User, tenantAccessor.TenantId, fals
 487        if (result is not ManagementConnectionMutationResult.Success(var connection))
 488        {
 489            await ConnectionEndpointSupport.SendMutationResultAsync(HttpContext, result, management, cancellationToken);
 490            return;
 491        }
 492        if (_writers.TryGetValue(existingBinding.ResolverType, out var writer))
 493            await ManagedSecretBindingCleanup.TryRemoveAsync(writer, existingBinding, effective.Connection.Id, logger);
 494        ConnectionEndpointSupport.SetEtag(HttpContext, connection.Revision);
 495        await HttpContext.Response.WriteAsJsonAsync(await ConnectionResponse.FromAsync(new(connection, ConnectionSourceO
 496    }
 497}
 498
 499internal static class ManagedSecretBindingCleanup
 500{
 501    public static async ValueTask TryRemoveAsync(IManagedSecretBindingWriter writer, SecretBinding binding, string conne
 502    {
 503        try
 504        {
 2505            await writer.RemoveAsync(binding, CancellationToken.None);
 2506        }
 0507        catch (Exception exception)
 508        {
 0509            logger.LogWarning(exception, "Failed to remove detached external-authentication secret material for connecti
 0510        }
 2511    }
 512}
 513
 514internal sealed class ConnectionListRequest
 515{
 516    public string? Search { get; set; }
 517    public string? Source { get; set; }
 518    public string? Scope { get; set; }
 519    public string? ScopeKind { get; set; }
 520    public string? TenantId { get; set; }
 521    public string? AdapterType { get; set; }
 522    public bool? Enabled { get; set; }
 523    public bool? Valid { get; set; }
 524    public bool? Shadowed { get; set; }
 525    public bool? Archived { get; set; }
 526    public string? Cursor { get; set; }
 527    public int? PageSize { get; set; }
 528}

Methods/Properties

TryRemoveAsync()