Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions Sources/Configuration/Documentation.docc/Proposals/SCO-0004.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# SCO-0004: SecretMarkingProvider

Add a wrapper provider to mark configuration values as secrets based on key patterns.

## Overview

- Proposal: SCO-0004
- Author(s): [Gautam Raju](https://github.com/gautamrajur)
- Status: **Awaiting Review**
- Issue: [apple/swift-configuration#131](https://github.com/apple/swift-configuration/issues/131)
- Implementation:
- [apple/swift-configuration#XX](https://github.com/apple/swift-configuration/pull/XX)

### Introduction

Add `SecretMarkingProvider`, a wrapper provider that marks configuration values as secrets based on key patterns, enabling post-hoc secret identification for providers that don't natively support it.

### Motivation

When integrating with external configuration sources, values may not be properly marked as secrets. This is common with:

- Environment variables from external systems
- Third-party configuration providers
- Legacy configuration files without secret metadata

Currently, `SecretsSpecifier` only works at provider initialization time. If you receive configuration from a provider you don't control, there's no way to mark specific values as secrets without implementing a custom wrapper.

For example, an `EnvironmentVariablesProvider` initialized without a `secretsSpecifier` will expose all values as non-secrets, including sensitive data like `DATABASE_PASSWORD` or `API_TOKEN`.

### Proposed solution

Add `SecretMarkingProvider<Upstream>`, a generic wrapper that:
1. Delegates all lookups to the upstream provider
2. Marks values as secrets when the key matches a user-provided predicate
3. Preserves existing secret status (never removes the `isSecret` flag)

```swift
let envProvider = EnvironmentVariablesProvider()

let secretMarkedProvider = SecretMarkingProvider(upstream: envProvider) { key in
key.description.lowercased().contains("password") ||
key.description.lowercased().contains("secret")
}

let config = ConfigReader(provider: secretMarkedProvider)
let dbPassword = config.string(forKey: "database.password") // marked as secret
```

Convenience methods on `ConfigProvider`:

```swift
// Predicate-based
let provider = envProvider.markSecrets { $0.description.contains("password") }

// Set-based
let provider = envProvider.markSecretsForKeys(["database.password", "api.key"])
```

### Detailed design

#### SecretMarkingProvider

A generic struct wrapping any `ConfigProvider`:

```swift
public struct SecretMarkingProvider<Upstream: ConfigProvider>: Sendable {
private let isSecretKey: @Sendable (AbsoluteConfigKey) -> Bool
private let upstream: Upstream

public init(
upstream: Upstream,
isSecretKey: @Sendable @escaping (_ key: AbsoluteConfigKey) -> Bool
)
}
```

Implements all `ConfigProvider` methods by delegating to upstream and marking results:
- `value(forKey:type:)`
- `fetchValue(forKey:type:)`
- `watchValue(forKey:type:updatesHandler:)`
- `snapshot()`
- `watchSnapshot(updatesHandler:)`

#### SecretMarkedSnapshot

A private snapshot wrapper that applies the same secret-marking logic.

#### Convenience operators

Extensions on `ConfigProvider`:

```swift
extension ConfigProvider {
func markSecrets(
where isSecretKey: @Sendable @escaping (AbsoluteConfigKey) -> Bool
) -> SecretMarkingProvider<Self>

func markSecretsForKeys(_ keys: Set<AbsoluteConfigKey>) -> SecretMarkingProvider<Self>
}
```

### API stability

This change is purely additive:
- New public type: `SecretMarkingProvider<Upstream>`
- New methods on `ConfigProvider`: `markSecrets(where:)`, `markSecretsForKeys(_:)`
- No changes to existing APIs

### Future directions

- Could integrate with `SecretsSpecifier` to provide a unified API for secret detection
- Could add logging/metrics for when secrets are marked

### Alternatives considered

1. **Extend SecretsSpecifier to work post-hoc** - Would require significant changes to the provider protocol and existing implementations.

2. **Add secret marking to ConfigReader** - Would only work at the reader level, not propagated through snapshots or watch sequences.

3. **Document the workaround** - Users could implement their own wrapper, but this is boilerplate that the library should provide.

Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,22 @@ extension ConfigProvider {
) -> KeyMappingProvider<Self> {
KeyMappingProvider(upstream: self, keyMapper: transform)
}

/// Creates a provider that marks values as secrets based on the given predicate.
///
/// - Parameter isSecretKey: A closure that returns `true` for keys whose values should be secrets.
/// - Returns: A provider that marks matching values as secrets.
public func markSecrets(
where isSecretKey: @Sendable @escaping (_ key: AbsoluteConfigKey) -> Bool
) -> SecretMarkingProvider<Self> {
SecretMarkingProvider(upstream: self, isSecretKey: isSecretKey)
}

/// Creates a provider that marks values as secrets for the specified keys.
///
/// - Parameter keys: Keys whose values should be marked as secrets.
/// - Returns: A provider that marks the specified keys' values as secrets.
public func markSecretsForKeys(_ keys: Set<AbsoluteConfigKey>) -> SecretMarkingProvider<Self> {
SecretMarkingProvider(upstream: self) { keys.contains($0) }
}
}
197 changes: 197 additions & 0 deletions Sources/Configuration/Providers/Wrappers/SecretMarkingProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftConfiguration open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftConfiguration project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftConfiguration project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A configuration provider that marks values as secrets based on key patterns.
///
/// Use `SecretMarkingProvider` to mark configuration values as secrets when the upstream
/// provider doesn't identify sensitive data. This is particularly useful when integrating
/// with external configuration sources or when you want to apply consistent secret handling
/// across providers that use different conventions.
///
/// ### Common use cases
///
/// Use `SecretMarkingProvider` for:
/// - Marking environment variables containing passwords or API keys as secrets.
/// - Adding secret protection to third-party configuration providers.
///
/// ## Example
///
/// Use `SecretMarkingProvider` when you want to mark secrets for specific providers:
///
/// ```swift
/// let envProvider = EnvironmentVariablesProvider()
///
/// let secretMarkedProvider = SecretMarkingProvider(upstream: envProvider) { key in
/// key.description.lowercased().contains("password") ||
/// key.description.lowercased().contains("secret")
/// }
///
/// let config = ConfigReader(provider: secretMarkedProvider)
/// let dbPassword = config.string(forKey: "database.password") // marked as secret
/// ```
///
/// ## Convenience method
///
/// You can also use the ``ConfigProvider/markSecrets(where:)`` convenience method:
///
/// ```swift
/// let provider = EnvironmentVariablesProvider()
/// .markSecrets { $0.description.contains("password") }
/// ```
@available(Configuration 1.0, *)
public struct SecretMarkingProvider<Upstream: ConfigProvider>: Sendable {
/// The predicate to check if a key's value should be marked as secret.
private let isSecretKey: @Sendable (AbsoluteConfigKey) -> Bool

/// The upstream provider.
private let upstream: Upstream

/// Creates a new provider that marks values as secrets based on a predicate.
///
/// - Parameters:
/// - upstream: The upstream provider to delegate to.
/// - isSecretKey: A closure that determines whether values for a given key should be marked as secrets.
public init(
upstream: Upstream,
isSecretKey: @Sendable @escaping (_ key: AbsoluteConfigKey) -> Bool
) {
self.isSecretKey = isSecretKey
self.upstream = upstream
}
}

@available(Configuration 1.0, *)
extension SecretMarkingProvider {
private func markSecretIfNeeded(_ value: ConfigValue?, forKey key: AbsoluteConfigKey) -> ConfigValue? {
guard var value else { return nil }
if isSecretKey(key) {
value = ConfigValue(value.content, isSecret: true)
}
return value
}

private func markSecretIfNeeded(_ result: LookupResult, forKey key: AbsoluteConfigKey) -> LookupResult {
LookupResult(
encodedKey: result.encodedKey,
value: markSecretIfNeeded(result.value, forKey: key)
)
}
}

@available(Configuration 1.0, *)
extension SecretMarkingProvider: ConfigProvider {
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
public var providerName: String {
"SecretMarkingProvider[upstream: \(upstream.providerName)]"
}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
public func value(forKey key: AbsoluteConfigKey, type: ConfigType) throws -> LookupResult {
let result = try upstream.value(forKey: key, type: type)
return markSecretIfNeeded(result, forKey: key)
}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
public func fetchValue(forKey key: AbsoluteConfigKey, type: ConfigType) async throws -> LookupResult {
let result = try await upstream.fetchValue(forKey: key, type: type)
return markSecretIfNeeded(result, forKey: key)
}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
public func watchValue<Return: ~Copyable>(
forKey key: AbsoluteConfigKey,
type: ConfigType,
updatesHandler: (
_ updates: ConfigUpdatesAsyncSequence<Result<LookupResult, any Error>, Never>
) async throws -> Return
) async throws -> Return {
try await upstream.watchValue(forKey: key, type: type) { sequence in
try await updatesHandler(
ConfigUpdatesAsyncSequence(
sequence
.map { result in
result.map { lookupResult in
self.markSecretIfNeeded(lookupResult, forKey: key)
}
}
)
)
}
}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
public func snapshot() -> any ConfigSnapshot {
SecretMarkedSnapshot(isSecretKey: self.isSecretKey, upstream: self.upstream.snapshot())
}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
public func watchSnapshot<Return: ~Copyable>(
updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<any ConfigSnapshot, Never>) async throws -> Return
) async throws -> Return {
try await upstream.watchSnapshot { sequence in
try await updatesHandler(
ConfigUpdatesAsyncSequence(
sequence
.map { snapshot in
SecretMarkedSnapshot(isSecretKey: self.isSecretKey, upstream: snapshot)
}
)
)
}
}
}

/// A snapshot that marks values as secrets based on key patterns.
@available(Configuration 1.0, *)
private struct SecretMarkedSnapshot: ConfigSnapshot {

/// The predicate to check if a key's value should be marked as secret.
let isSecretKey: @Sendable (AbsoluteConfigKey) -> Bool

/// The upstream snapshot to delegate to.
var upstream: any ConfigSnapshot

var providerName: String {
"SecretMarkingProvider[upstream: \(self.upstream.providerName)]"
}

func value(forKey key: AbsoluteConfigKey, type: ConfigType) throws -> LookupResult {
let result = try upstream.value(forKey: key, type: type)
guard var value = result.value else {
return result
}
if isSecretKey(key) {
value = ConfigValue(value.content, isSecret: true)
}
return LookupResult(encodedKey: result.encodedKey, value: value)
}
}

@available(Configuration 1.0, *)
extension SecretMarkingProvider: CustomStringConvertible {
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
public var description: String {
"SecretMarkingProvider[upstream: \(self.upstream)]"
}
}

@available(Configuration 1.0, *)
extension SecretMarkingProvider: CustomDebugStringConvertible {
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
public var debugDescription: String {
let upstreamDebug = String(reflecting: self.upstream)
return "SecretMarkingProvider[upstream: \(upstreamDebug)]"
}
}

Loading