{"record":{"id":"424e79443eadb08b","repo":"elsa-workflows/elsa-core","slug":"only-managed-secret-bindings-can-remove-managed-secret","errorCode":null,"errorMessage":"Only managed secret bindings can remove managed secret material.","messagePattern":"Only managed secret bindings can remove managed secret material\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication.Secrets/Services/ElsaSecretBindingResolver.cs","lineNumber":48,"sourceCode":"        // so a stale request can never rotate material used by the live binding.\n        var name = $\"external-authentication:{Guid.NewGuid():N}\";\n        var secret = await secretManager.CreateAsync(new()\n        {\n            Name = name,\n            DisplayName = $\"External authentication {request.FieldName}\",\n            TypeName = SecretTypeNames.Text,\n            StoreName = SecretStoreNames.Encrypted,\n            Value = request.Value.Reveal()\n        }, cancellationToken);\n\n        return new(ResolverType, secret.Name, Ownership: SecretBindingOwnership.Managed);\n    }\n\n    public async ValueTask RemoveAsync(SecretBinding binding, CancellationToken cancellationToken = default)\n    {\n        EnsureResolverType(binding);\n        if (binding.Ownership != SecretBindingOwnership.Managed)\n            throw new InvalidOperationException(\"Only managed secret bindings can remove managed secret material.\");\n        await secretManager.DeleteAsync(binding.Reference, cancellationToken);\n    }\n\n    public async ValueTask<SecretBindingState> GetStateAsync(SecretBinding binding, CancellationToken cancellationToken = default)\n    {\n        EnsureResolverType(binding);\n        var secret = await secretManager.GetAsync(binding.Reference, cancellationToken);\n        if (secret is null)\n            return new(false, false);\n\n        var configured = secret is { Status: SecretStatus.Active, LatestActiveVersion: not null };\n        if (!configured || !IsCompatible(secret, binding))\n            return new(configured, false);\n\n        var test = await secretManager.TestAsync(secret.Name, cancellationToken);\n        return new(true, test.Succeeded);\n    }\n","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication.Secrets/Services/ElsaSecretBindingResolver.cs#L30-L66","documentation":"RemoveAsync refuses to delete secret material referenced by a binding whose Ownership is not SecretBindingOwnership.Managed. This protects externally owned (e.g. configuration-backed read-only) bindings from having their referenced secret material deleted through this resolver.","triggerScenarios":"Calling RemoveAsync and passing a SecretBinding whose Ownership is System/External instead of Managed; typically after fetching or constructing a binding without checking its Ownership.","commonSituations":"Cleanup scripts that iterate all bindings and attempt removal without filtering by ownership; code paths that assume every binding the resolver hands out is managed.","solutions":["Check binding.Ownership == SecretBindingOwnership.Managed before calling RemoveAsync","Remove the binding from the source that owns it instead of deleting managed material","Skip non-managed bindings in bulk cleanup loops"],"exampleFix":"// before\nawait resolver.RemoveAsync(binding);\n// after\nif (binding.Ownership == SecretBindingOwnership.Managed)\n    await resolver.RemoveAsync(binding);","handlingStrategy":"type-guard","validationCode":"if (binding.Ownership != SecretBindingOwnership.Managed) return; // skip: not managed","typeGuard":"bool CanRemove(SecretBinding b) => b.Ownership == SecretBindingOwnership.Managed;","tryCatchPattern":"try { await resolver.RemoveAsync(binding); }\ncatch (InvalidOperationException) { /* binding not managed; skip or handle elsewhere */ }","preventionTips":["Filter bulk cleanup loops by Ownership == Managed before deleting","Model ownership checks in the domain layer so callers cannot bypass them","Never assume every binding handed out by a resolver is managed"],"tags":["secrets","ownership","state"],"backgroundTag":"invalid-state-transition","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}