diff --git a/Amplitude-Swift.xcodeproj/project.pbxproj b/Amplitude-Swift.xcodeproj/project.pbxproj index 684d2a0b..9c5bb4b3 100644 --- a/Amplitude-Swift.xcodeproj/project.pbxproj +++ b/Amplitude-Swift.xcodeproj/project.pbxproj @@ -61,6 +61,8 @@ BA994B9D2A4F4FCB00D0913F /* legacy_v4.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = BA994B9B2A4F4B7500D0913F /* legacy_v4.sqlite */; }; BA9BEA4B299FB43B00BC0F7C /* IdentifyInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */; }; BA9BEA4D299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */; }; + D010435F2B6C59EE00F8173C /* SandboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D010435E2B6C59EE00F8173C /* SandboxHelper.swift */; }; + D01043612B6C5A8500F8173C /* SandboxHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01043602B6C5A8500F8173C /* SandboxHelperTests.swift */; }; OBJ_100 /* Mediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* Mediator.swift */; }; OBJ_101 /* AmplitudeDestinationPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* AmplitudeDestinationPlugin.swift */; }; OBJ_102 /* ContextPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* ContextPlugin.swift */; }; @@ -123,14 +125,14 @@ isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; proxyType = 1; - remoteGlobalIDString = amplitude-swift::Amplitude-Swift; + remoteGlobalIDString = "amplitude-swift::Amplitude-Swift"; remoteInfo = "Amplitude-Swift"; }; 580FD1F1294A56F60036777B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; proxyType = 1; - remoteGlobalIDString = amplitude-swift::Amplitude-SwiftTests; + remoteGlobalIDString = "amplitude-swift::Amplitude-SwiftTests"; remoteInfo = "Amplitude-SwiftTests"; }; /* End PBXContainerItemProxy section */ @@ -176,6 +178,8 @@ BA994B9B2A4F4B7500D0913F /* legacy_v4.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = legacy_v4.sqlite; sourceTree = ""; }; BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyInterceptor.swift; sourceTree = ""; }; BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyInterceptorTests.swift; sourceTree = ""; }; + D010435E2B6C59EE00F8173C /* SandboxHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxHelper.swift; sourceTree = ""; }; + D01043602B6C5A8500F8173C /* SandboxHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxHelperTests.swift; sourceTree = ""; }; OBJ_10 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; OBJ_11 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; OBJ_12 /* EventBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBridge.swift; sourceTree = ""; }; @@ -394,6 +398,7 @@ OBJ_52 /* UrlExtension.swift */, BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */, 8EDEC54AB4DF9E1074C3D6A4 /* Weak.swift */, + D010435E2B6C59EE00F8173C /* SandboxHelper.swift */, ); path = Utilities; sourceTree = ""; @@ -499,6 +504,7 @@ OBJ_74 /* UrlExtensionTests.swift */, BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */, 8EDEC4F83BFAA664749FAEF0 /* QueueTimeTests.swift */, + D01043602B6C5A8500F8173C /* SandboxHelperTests.swift */, ); path = Utilities; sourceTree = ""; @@ -640,6 +646,7 @@ OBJ_154 /* TypesTests.swift in Sources */, OBJ_155 /* EventPipelineTests.swift in Sources */, OBJ_156 /* HttpClientTests.swift in Sources */, + D01043612B6C5A8500F8173C /* SandboxHelperTests.swift in Sources */, OBJ_157 /* PersistentStorageResponseHandlerTests.swift in Sources */, OBJ_158 /* UrlExtensionTests.swift in Sources */, 8EDEC4EE0DE1C89889F451B5 /* QueueTimeTests.swift in Sources */, @@ -698,6 +705,7 @@ 8EDEC8F8DD2CDCD6568512F8 /* RemnantDataMigration.swift in Sources */, 8EDEC977C03AA2676724F436 /* BasePlugins.swift in Sources */, 8EDEC1073A308B12B5CCD975 /* AnalyticsConnectorPlugin.swift in Sources */, + D010435F2B6C59EE00F8173C /* SandboxHelper.swift in Sources */, 8EDEC3283B812D5D34DADF7B /* AnalyticsConnectorIdentityPlugin.swift in Sources */, 8EDEC4D0C0CE07BF211804CC /* DefaultTrackingOptions.swift in Sources */, 8EDEC30C0075E9D92B1B5210 /* UIKitScreenViews.swift in Sources */, diff --git a/Amplitude-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Amplitude-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata index fe1aa713..919434a6 100644 --- a/Amplitude-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/Amplitude-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,4 @@ - \ No newline at end of file + diff --git a/Sources/Amplitude/Storages/PersistentStorage.swift b/Sources/Amplitude/Storages/PersistentStorage.swift index e91aece0..cde30c1b 100644 --- a/Sources/Amplitude/Storages/PersistentStorage.swift +++ b/Sources/Amplitude/Storages/PersistentStorage.swift @@ -15,10 +15,9 @@ class PersistentStorage: Storage { let fileManager: FileManager private var outputStream: OutputFileStream? internal weak var amplitude: Amplitude? - // Store event.callback in memory as it cannot be ser/deser in files. private var eventCallbackMap: [String: EventCallback] - + private var appPath: String! let syncQueue = DispatchQueue(label: "syncPersistentStorage.amplitude.com") init(storagePrefix: String) { @@ -28,6 +27,8 @@ class PersistentStorage: Storage { self.userDefaults = UserDefaults(suiteName: "\(PersistentStorage.AMP_STORAGE_PREFIX).\(self.storagePrefix)") self.fileManager = FileManager.default self.eventCallbackMap = [String: EventCallback]() + // Make sure Amplitude data is sandboxed per app + self.appPath = isStorageSandboxed() ? "" : "\(Bundle.main.bundleIdentifier!)/" } func write(key: StorageKey, value: Any?) throws { @@ -168,6 +169,10 @@ class PersistentStorage: Storage { } return result } + + internal func isStorageSandboxed() -> Bool { + return SandboxHelper().isSandboxEnabled() + } } extension PersistentStorage { @@ -239,6 +244,8 @@ extension PersistentStorage { } internal func getEventsStorageDirectory(createDirectory: Bool = true) -> URL { + // TODO: Update to use applicationSupportDirectory for all platforms (this will require a migration) + // let searchPathDirectory = FileManager.SearchPathDirectory.applicationSupportDirectory // tvOS doesn't have access to document // macOS /Documents dir might be synced with iCloud #if os(tvOS) || os(macOS) @@ -249,7 +256,7 @@ extension PersistentStorage { let urls = fileManager.urls(for: searchPathDirectory, in: .userDomainMask) let docUrl = urls[0] - let storageUrl = docUrl.appendingPathComponent("amplitude/\(eventsFileKey)/") + let storageUrl = docUrl.appendingPathComponent("amplitude/\(appPath ?? "")\(eventsFileKey)/") if createDirectory { // try to create it, will fail if already exists. // tvOS, watchOS regularly clear out data. diff --git a/Sources/Amplitude/Utilities/SandboxHelper.swift b/Sources/Amplitude/Utilities/SandboxHelper.swift new file mode 100644 index 00000000..d5132669 --- /dev/null +++ b/Sources/Amplitude/Utilities/SandboxHelper.swift @@ -0,0 +1,25 @@ +// +// SandboxHelper.swift +// Amplitude-Swift +// +// Created by Justin Fiedler on 2/1/24. +// + +import Foundation + +public class SandboxHelper { + internal func getEnvironment() -> [String: String] { + return ProcessInfo.processInfo.environment + } + + public func isSandboxEnabled() -> Bool { + #if os(macOS) + // Check if macOS app has "App Sandbox" enabled + let environment = getEnvironment() + return environment["APP_SANDBOX_CONTAINER_ID"] != nil + #else + // Other platforms (iOS, tvOS, watchOs) are sandboxed by default + return true + #endif + } +} diff --git a/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift b/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift index 5cf457c6..2a60bf8f 100644 --- a/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift +++ b/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift @@ -49,4 +49,28 @@ final class PersistentStorageTests: XCTestCase { XCTAssertNotEqual(eventFiles?[0].pathExtension, PersistentStorage.TEMP_FILE_EXTENSION) persistentStorage.reset() } + + #if os(macOS) + func testMacOsStorageDirectorySandboxedWhenAppSandboxDisabled() { + let persistentStorage = PersistentStorage(storagePrefix: "mac-instance") + + let bundleId = Bundle.main.bundleIdentifier! + let storageUrl = persistentStorage.getEventsStorageDirectory(createDirectory: false) + + XCTAssertEqual(persistentStorage.isStorageSandboxed(), false) + XCTAssertEqual(storageUrl.absoluteString.contains(bundleId), true) + persistentStorage.reset() + } + + func testMacOsStorageDirectorySandboxedWhenAppSandboxEnabled() { + let persistentStorage = FakePersistentStorageAppSandboxEnabled(storagePrefix: "mac-app-sandbox-instance") + + let bundleId = Bundle.main.bundleIdentifier! + let storageUrl = persistentStorage.getEventsStorageDirectory(createDirectory: false) + + XCTAssertEqual(persistentStorage.isStorageSandboxed(), true) + XCTAssertEqual(storageUrl.absoluteString.contains(bundleId), false) + persistentStorage.reset() + } + #endif } diff --git a/Tests/AmplitudeTests/Supports/TestUtilities.swift b/Tests/AmplitudeTests/Supports/TestUtilities.swift index 3652ebf7..134c53a6 100644 --- a/Tests/AmplitudeTests/Supports/TestUtilities.swift +++ b/Tests/AmplitudeTests/Supports/TestUtilities.swift @@ -248,3 +248,15 @@ class TestIdentifyInterceptor: IdentifyInterceptor { overridenIdentifyBatchIntervalMillis = identifyBatchIntervalMillis } } + +class FakeSandboxHelperWithAppSandboxContainer: SandboxHelper { + override func getEnvironment() -> [String: String] { + return ["APP_SANDBOX_CONTAINER_ID": "test-container-id"] + } +} + +class FakePersistentStorageAppSandboxEnabled: PersistentStorage { + override internal func isStorageSandboxed() -> Bool { + return true + } +} diff --git a/Tests/AmplitudeTests/Utilities/SandboxHelperTests.swift b/Tests/AmplitudeTests/Utilities/SandboxHelperTests.swift new file mode 100644 index 00000000..c1d69680 --- /dev/null +++ b/Tests/AmplitudeTests/Utilities/SandboxHelperTests.swift @@ -0,0 +1,34 @@ +// +// SandboxHelperTests.swift +// Amplitude-SwiftTests +// +// Created by Justin Fiedler on 2/1/24. +// + +import XCTest + +@testable import AmplitudeSwift + +final class SandboxHelperTests: XCTestCase { + + func testIsSandboxEnabled() { + let sandboxHelper = SandboxHelper() + let isSandboxed = sandboxHelper.isSandboxEnabled() + + #if os(macOS) + XCTAssertEqual(isSandboxed, false) + #else + XCTAssertEqual(isSandboxed, true) + #endif + } + + #if os(macOS) + func testIsSandboxEnabledWithMacOSAppSandbox() { + let sandboxHelper = FakeSandboxHelperWithAppSandboxContainer() + + let isSandboxed = sandboxHelper.isSandboxEnabled() + + XCTAssertEqual(isSandboxed, true) + } + #endif +}