-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add targets * Add protocol for SecretsManager responses * Add types, helpers * Implement Secrets * Update for new target * Avoid duplicated file names * Add documentation catalog for Secrets * Minor fixes * Additional protocol conformances * Add factory
- Loading branch information
Showing
9 changed files
with
286 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
version: 1 | ||
builder: | ||
configs: | ||
- documentation_targets: [EmailSender, PersPersistenceistence] | ||
- documentation_targets: [EmailSender, PersPersistenceistence, Secrets] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
Sources/Persistence/Extensions.swift → Sources/Persistence/AWSDynamoDB+Utils.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
// | ||
// Extensions.swift | ||
// AWSDynamoDB+Utils.swift | ||
// AWSExtras | ||
// | ||
// Created by Mathew Gacy on 12/8/23. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# ``Secrets`` | ||
|
||
Retrieve secrets from AWS Secrets Manager. | ||
|
||
## Overview | ||
|
||
Secrets provides a wrapper around AWS Secrets Manager to improve testability. | ||
|
||
## Topics | ||
|
||
### Essentials |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// | ||
// SecretValue.swift | ||
// AWSExtras | ||
// | ||
// Created by Mathew Gacy on 12/22/23. | ||
// | ||
|
||
import AWSSecretsManager | ||
import Foundation | ||
|
||
/// A class of types providing secrets. | ||
protocol SecretValue: Equatable { | ||
/// The ARN of the secret. | ||
var arn: String? { get } | ||
|
||
/// The friendly name of the secret. | ||
var name: String? { get } | ||
|
||
/// The decrypted secret value, if the secret value was originally provided as binary data in | ||
/// the form of a byte array. | ||
/// | ||
/// If the secret was created by using the Secrets Manager console, or if the secret value was | ||
/// originally provided as a string, then this field is omitted. The secret value appears in | ||
/// `SecretString` instead. | ||
var secretBinary: Data? { get } | ||
|
||
/// The decrypted secret value, if the secret value was originally provided as a string or | ||
/// through the Secrets Manager console. | ||
/// | ||
/// If this secret was created by using the console, then Secrets Manager stores the information | ||
/// as a JSON structure of key/value pairs. | ||
var secretString: String? { get } | ||
} | ||
|
||
extension GetSecretValueOutput: SecretValue {} | ||
|
||
extension SecretsManagerClientTypes.SecretValueEntry: SecretValue {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
// | ||
// Secrets.swift | ||
// AWSExtras | ||
// | ||
// Created by Mathew Gacy on 12/22/23. | ||
// | ||
|
||
import AWSSecretsManager | ||
import Foundation | ||
|
||
/// A secret stored by AWS Secrets Manager. | ||
public struct Secret: Equatable, Sendable { | ||
|
||
/// A secret stored by AWS Secrets Manager. | ||
public enum Value: Equatable, Sendable { | ||
/// The decrypted secret value, if the secret value was originally provided as binary data in | ||
/// the form of a byte array. | ||
case binary(Data) | ||
/// The decrypted secret value, if the secret value was originally provided as a string or | ||
/// through the Secrets Manager console. | ||
/// | ||
/// If this secret was created by using the console, then Secrets Manager stores the information | ||
/// as a JSON structure of key/value pairs. | ||
case string(String) | ||
} | ||
|
||
/// The ARN of the secret. | ||
var arn: String | ||
|
||
/// The friendly name of the secret. | ||
var name: String | ||
|
||
/// The decrypted secret value. | ||
var value: Value | ||
|
||
/// Creates a new instance. | ||
/// | ||
/// - Parameters: | ||
/// - arn: The ARN of the secret. | ||
/// - name: The friendly name of the secret. | ||
/// - value: The decrypted secret value. | ||
public init(arn: String, name: String, value: Value) { | ||
self.arn = arn | ||
self.name = name | ||
self.value = value | ||
} | ||
} | ||
|
||
extension Secret { | ||
/// Creates a new instance. | ||
/// | ||
/// - Parameter secretValue: A secret value. | ||
init<V: SecretValue>(_ secretValue: V) throws { | ||
guard let arn = secretValue.arn, let name = secretValue.name else { | ||
throw SecretsError.missingData | ||
} | ||
|
||
self.arn = arn | ||
self.name = name | ||
if let string = secretValue.secretString { | ||
self.value = .string(string) | ||
} else if let data = secretValue.secretBinary { | ||
self.value = .binary(data) | ||
} else { | ||
throw SecretsError.missingData | ||
} | ||
} | ||
} | ||
|
||
/// An error that can be thrown when retrieving secrets. | ||
public enum SecretsError: LocalizedError { | ||
/// The secret is missing expected data. | ||
case missingData | ||
/// The decrypted secret is of an unexpected type. | ||
case wrongSecretType | ||
|
||
/// A localized message describing what error occurred. | ||
public var errorDescription: String? { | ||
switch self { | ||
case .missingData: | ||
return "The secret value is missing expected data." | ||
case .wrongSecretType: | ||
return "The decrypted secret is of an unexpected type." | ||
} | ||
} | ||
} | ||
|
||
extension Optional { | ||
/// Convienence method to `throw` if an optional type has a `nil` value. | ||
/// | ||
/// - Parameter error: The error to throw. | ||
/// - Returns: The unwrapped value. | ||
func unwrap(or error: @autoclosure () -> LocalizedError) throws -> Wrapped { | ||
switch self { | ||
case .some(let wrapped): return wrapped | ||
case .none: throw error() | ||
} | ||
} | ||
} | ||
|
||
/// A type that retrieves secrets. | ||
public struct Secrets: Sendable { | ||
/// The ARN or name of the secret to retrieve. | ||
public typealias ID = String | ||
|
||
/// A closure returning the secret string for the given identifier. | ||
var string: @Sendable (ID) async throws -> String | ||
|
||
/// A closure returning the secret data for the given identifier. | ||
var data: @Sendable (ID) async throws -> Data | ||
|
||
/// A closure returning secrets for the given identifiers. | ||
var batch: @Sendable ([ID]) async throws -> [Secret]? | ||
|
||
public init( | ||
string: @escaping @Sendable (Secrets.ID) async throws -> String, | ||
data: @escaping @Sendable (Secrets.ID) async throws -> Data, | ||
batch: @escaping @Sendable ([Secrets.ID]) async throws -> [Secret]? | ||
) { | ||
self.string = string | ||
self.data = data | ||
self.batch = batch | ||
} | ||
} | ||
|
||
public extension Secrets { | ||
/// Returns a live implementation. | ||
/// | ||
/// - Parameter region: The AWS region of the secrets manager. | ||
/// - Returns: A live instance. | ||
static func live(region: String) throws -> Self { | ||
let client = try SecretsManagerClient(region: region) | ||
return Secrets( | ||
string: { id in | ||
try await client.getSecretValue(input: GetSecretValueInput(secretId: id)) | ||
.secretString | ||
.unwrap(or: SecretsError.wrongSecretType) | ||
}, | ||
data: { id in | ||
try await client.getSecretValue(input: GetSecretValueInput(secretId: id)) | ||
.secretBinary | ||
.unwrap(or: SecretsError.wrongSecretType) | ||
}, | ||
batch: { secretIDs in | ||
let batchInput = BatchGetSecretValueInput(secretIdList: secretIDs) | ||
return try await client.batchGetSecretValue(input: batchInput) | ||
.secretValues? | ||
.map { try Secret($0) } | ||
}) | ||
} | ||
} | ||
|
||
/// A type that creates ``Secrets`` instances. | ||
public struct SecretsFactory: Sendable { | ||
/// The region where the secrets manager is located. | ||
public typealias Region = String | ||
|
||
/// A closure that creates and returns a ``Secrets`` instance. | ||
public var make: @Sendable (Region) throws -> Secrets | ||
|
||
/// Creates an instance. | ||
/// | ||
/// - Parameter make: A closure returning a ``Secrets`` instance. | ||
public init( | ||
make: @escaping @Sendable (SecretsFactory.Region) throws -> Secrets | ||
) { | ||
self.make = make | ||
} | ||
} | ||
|
||
public extension SecretsFactory { | ||
/// Returns a live implementation. | ||
static func live() -> Self { | ||
.init(make: { try Secrets.live(region: $0 ) }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// | ||
// SecretsTests.swift | ||
// AWSExtras | ||
// | ||
// Created by Mathew Gacy on 12/22/23. | ||
// | ||
|
||
@testable import Secrets | ||
import Foundation | ||
import XCTest | ||
|
||
final class SecretsTests: XCTestCase { | ||
func testSecrets() async throws { | ||
// ... | ||
} | ||
} |