diff --git a/Sources/CoreBaggage/Baggage.swift b/Sources/CoreBaggage/Baggage.swift index 5c84e73..fa0dfea 100644 --- a/Sources/CoreBaggage/Baggage.swift +++ b/Sources/CoreBaggage/Baggage.swift @@ -185,6 +185,7 @@ extension Baggage { return self._storage.count } + /// Returns true if the baggage has no items stored. public var isEmpty: Bool { return self._storage.isEmpty } @@ -193,12 +194,26 @@ extension Baggage { /// /// Order of those invocations is NOT guaranteed and should not be relied on. /// + /// ### Access Control + /// + /// By default accesses ALL `public` and `publicExceptLogging` values. + /// Baggage items whose keys are marked with `private` access, are never accessed by a forEach. + /// /// - Parameter body: A closure invoked with the type erased key and value stored for the key in this baggage. - public func forEach(_ body: (AnyBaggageKey, Any) throws -> Void) rethrows { + public func forEach(access: ForEachAccessType = .allPublic, _ body: (AnyBaggageKey, Any) throws -> Void) rethrows { try self._storage.forEach { key, value in - try body(key, value) + if key.access.rawValue <= access.rawValue { + try body(key, value) + } } } + + public enum ForEachAccessType: Int, Hashable { + /// Access all accessible values (i.e. `public` and `publicExceptLogging` values; `private` values are skipped) + case allPublic = 1 + /// Access all accessible values for purposes of logging (i.e. ONLY `public`, skipping `publicExceptLogging` and `private` values) + case logging = 0 + } } extension Baggage: CustomStringConvertible { diff --git a/Sources/CoreBaggage/BaggageAccessPolicy.swift b/Sources/CoreBaggage/BaggageAccessPolicy.swift new file mode 100644 index 0000000..3a7e21e --- /dev/null +++ b/Sources/CoreBaggage/BaggageAccessPolicy.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing Baggage open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Configures the policy related to values stored using this key. +/// +/// This can be used to ensure that a value should never be logged automatically by a logger associated to a context. +/// +/// Summary: +/// - `public` - items are accessible by other modules via `baggage.forEach` and direct key lookup, +/// and will be logged by the `DefaultContext` `logger. +/// - `publicExceptLogging` - items are accessible by other modules via `baggage.forEach` and direct key lookup, +/// however will NOT be logged by the `DefaultContext` `logger. +/// - `private` - items are NOT accessible by other modules via `baggage.forEach` nor are they logged by default. +/// The only way to gain access to a private baggage item is through it's key or accessor, which means that +/// access is controlled using Swift's native access control mechanism, i.e. a `private`/`internal` `Key` and `set` accessor, +/// will result in a baggage item that may only be set by the owning module, but read by anyone via the (`public`) accessor. +public enum BaggageAccessPolicy: Int, Hashable { + /// Access to this baggage item is NOT restricted. + /// This baggage item will be listed when `baggage.forEach` is invoked, and thus modules other than the defining + /// module may gain access to it and potentially log or pass it to other parts of the system. + /// + /// Note that this can happen regardless of the key being declared private or internal. + /// + /// ### Example + /// When module `A` defines `AKey` and keeps it `private`, any other module still may call `baggage.forEach` + case `public` = 0 + + /// Access to this baggage item is NOT restricted, however the `DefaultContext` (and any other well-behaved context) + /// MUST NOT log this baggage item. + /// + /// This policy can be useful if some user sensitive value must be carried in baggage context, however it should never + /// appear in log statements. While usually such items should not be put into baggage, we offer this mode as a way of + /// threading through a system values which should not be logged nor pollute log statements. + case publicExceptLogging = 1 + + /// Access to this baggage item is RESTRICTED and can only be performed by a direct subscript lookup into the baggage. + /// + /// This effectively restricts the access to the baggage item, to any party which has access to the associated + /// `BaggageKey`. E.g. if the baggage key is defined internal or private, and the `set` accessor is also internal or + /// private, no other module would be able to modify this baggage once it was set on a baggage context. + case `private` = 2 +} + +extension Baggage { + public typealias AccessPolicy = BaggageAccessPolicy +} diff --git a/Sources/CoreBaggage/BaggageKey.swift b/Sources/CoreBaggage/BaggageKey.swift index af3e7c5..bd217c0 100644 --- a/Sources/CoreBaggage/BaggageKey.swift +++ b/Sources/CoreBaggage/BaggageKey.swift @@ -53,10 +53,28 @@ public protocol BaggageKey { /// /// Defaults to `nil`. static var nameOverride: String? { get } + + /// Configures the policy related to values stored using this key. + /// + /// This can be used to ensure that a value should never be logged automatically by a logger associated to a context. + /// + /// Summary: + /// - `public` - items are accessible by other modules via `baggage.forEach` and direct key lookup, + /// and will be logged by the `DefaultContext` `logger. + /// - `publicExceptLogging` - items are accessible by other modules via `baggage.forEach` and direct key lookup, + /// however will NOT be logged by the `DefaultContext` `logger. + /// - `private` - items are NOT accessible by other modules via `baggage.forEach` nor are they logged by default. + /// The only way to gain access to a private baggage item is through it's key or accessor, which means that + /// access is controlled using Swift's native access control mechanism, i.e. a `private`/`internal` `Key` and `set` accessor, + /// will result in a baggage item that may only be set by the owning module, but read by anyone via the (`public`) accessor. + /// + /// Defaults to `.public`. + static var access: Baggage.AccessPolicy { get } } extension BaggageKey { public static var nameOverride: String? { return nil } + public static var access: Baggage.AccessPolicy { return .public } } /// A type-erased `BaggageKey` used when iterating through the `Baggage` using its `forEach` method. @@ -64,6 +82,8 @@ public struct AnyBaggageKey { /// The key's type represented erased to an `Any.Type`. public let keyType: Any.Type + public let access: Baggage.AccessPolicy + private let _nameOverride: String? /// A human-readable String representation of the underlying key. @@ -75,6 +95,13 @@ public struct AnyBaggageKey { init(_ keyType: Key.Type) where Key: BaggageKey { self.keyType = keyType self._nameOverride = keyType.nameOverride + self.access = keyType.access + } +} + +extension AnyBaggageKey: CustomStringConvertible { + public var description: String { + return "AnyBaggageKey(\(self.name), access: \(self.access))" } } diff --git a/Tests/CoreBaggageTests/BaggageTests+XCTest.swift b/Tests/CoreBaggageTests/BaggageTests+XCTest.swift index 0a123e6..cca067c 100644 --- a/Tests/CoreBaggageTests/BaggageTests+XCTest.swift +++ b/Tests/CoreBaggageTests/BaggageTests+XCTest.swift @@ -30,6 +30,8 @@ extension BaggageTests { ("testEmptyBaggageDescription", testEmptyBaggageDescription), ("testSingleKeyBaggageDescription", testSingleKeyBaggageDescription), ("testMultiKeysBaggageDescription", testMultiKeysBaggageDescription), + ("test_forEach_forLogging_respects_BaggageAccessPolicy", test_forEach_forLogging_respects_BaggageAccessPolicy), + ("test_forEach_respects_BaggageAccessPolicy", test_forEach_respects_BaggageAccessPolicy), ("test_todo_context", test_todo_context), ("test_topLevel", test_topLevel), ] diff --git a/Tests/CoreBaggageTests/BaggageTests.swift b/Tests/CoreBaggageTests/BaggageTests.swift index 0396d0a..be81f70 100644 --- a/Tests/CoreBaggageTests/BaggageTests.swift +++ b/Tests/CoreBaggageTests/BaggageTests.swift @@ -55,7 +55,7 @@ final class BaggageTests: XCTestCase { func testMultiKeysBaggageDescription() { var baggage = Baggage.topLevel baggage.testID = 42 - baggage[SecondTestIDKey.self] = "test" + baggage[Baggage.SecondTestIDKey.self] = "test" let description = String(describing: baggage) XCTAssert(description.starts(with: "Baggage(keys: ["), "Was: \(description)") @@ -64,6 +64,51 @@ final class BaggageTests: XCTestCase { XCTAssert(description.contains("ExplicitKeyName"), "Was: \(description)") } + // ==== ---------------------------------------------------------------------------------------------------------------- + // MARK: Access Policy + + func test_forEach_forLogging_respects_BaggageAccessPolicy() { + var baggage = Baggage.topLevel + + baggage.testID = 42 + baggage.publicExceptLogging = "dont-log-me" + baggage.private = "dont-log-me-either-not-even-foreach" + + XCTAssertEqual(baggage.testID, 42) + XCTAssertEqual(baggage.publicExceptLogging, "dont-log-me") + XCTAssertEqual(baggage.private, "dont-log-me-either-not-even-foreach") + + var count = 0 + baggage.forEach(access: .logging) { key, value in + print(" access: \(key) = \(value)") + XCTAssertNotEqual(key.name, "\(Baggage.PublicExceptLoggingKey.self)") + XCTAssertNotEqual(key.name, "\(Baggage.PrivateKey.self)") + count += 1 + } + XCTAssertEqual(count, 1) + } + + func test_forEach_respects_BaggageAccessPolicy() { + var baggage = Baggage.topLevel + + baggage.testID = 42 + baggage.publicExceptLogging = "dont-log-me" + baggage.private = "dont-log-me-either-not-even-foreach" + + XCTAssertEqual(baggage.testID, 42) + XCTAssertEqual(baggage.publicExceptLogging, "dont-log-me") + XCTAssertEqual(baggage.private, "dont-log-me-either-not-even-foreach") + + var count = 0 + baggage.forEach { key, value in + print(" access: \(key) = \(value)") + // the "dont log" may still appear here; but our `DefaultLoggingContext` in can handle it properly (other package) + XCTAssertNotEqual(key.name, "\(Baggage.PrivateKey.self)") + count += 1 + } + XCTAssertEqual(count, 2) + } + // ==== ------------------------------------------------------------------------------------------------------------ // MARK: Factories @@ -83,7 +128,7 @@ private enum TestIDKey: Baggage.Key { typealias Value = Int } -private extension Baggage { +extension Baggage { var testID: Int? { get { return self[TestIDKey.self] @@ -92,10 +137,42 @@ private extension Baggage { self[TestIDKey.self] = newValue } } + + enum SecondTestIDKey: Baggage.Key { + typealias Value = String + + static let nameOverride: String? = "ExplicitKeyName" + } } -private enum SecondTestIDKey: Baggage.Key { - typealias Value = String +extension Baggage { + var publicExceptLogging: String? { + get { + return self[PublicExceptLoggingKey.self] + } + set { + self[PublicExceptLoggingKey.self] = newValue + } + } - static let nameOverride: String? = "ExplicitKeyName" + var `private`: String? { + get { + return self[PrivateKey.self] + } + set { + self[PrivateKey.self] = newValue + } + } + + enum PublicExceptLoggingKey: Baggage.Key { + typealias Value = String + static let nameOverride: String? = "public-except-logging" + static let access: BaggageAccessPolicy = .publicExceptLogging + } + + enum PrivateKey: Baggage.Key { + typealias Value = String + static let nameOverride: String? = "private-DONT_SHOW_IN_LOGS_PLEASE" + static let access: BaggageAccessPolicy = .private + } }