Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensure event data is sandboxed per app for all platforms #113

Merged
merged 17 commits into from
Feb 5, 2024
Merged
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
12 changes: 10 additions & 2 deletions Amplitude-Swift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -176,6 +178,8 @@
BA994B9B2A4F4B7500D0913F /* legacy_v4.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = legacy_v4.sqlite; sourceTree = "<group>"; };
BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyInterceptor.swift; sourceTree = "<group>"; };
BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyInterceptorTests.swift; sourceTree = "<group>"; };
D010435E2B6C59EE00F8173C /* SandboxHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxHelper.swift; sourceTree = "<group>"; };
D01043602B6C5A8500F8173C /* SandboxHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxHelperTests.swift; sourceTree = "<group>"; };
OBJ_10 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
OBJ_11 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
OBJ_12 /* EventBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBridge.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -394,6 +398,7 @@
OBJ_52 /* UrlExtension.swift */,
BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */,
8EDEC54AB4DF9E1074C3D6A4 /* Weak.swift */,
D010435E2B6C59EE00F8173C /* SandboxHelper.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -499,6 +504,7 @@
OBJ_74 /* UrlExtensionTests.swift */,
BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */,
8EDEC4F83BFAA664749FAEF0 /* QueueTimeTests.swift */,
D01043602B6C5A8500F8173C /* SandboxHelperTests.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions Sources/Amplitude/Storages/PersistentStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -168,6 +169,10 @@ class PersistentStorage: Storage {
}
return result
}

internal func isStorageSandboxed() -> Bool {
return SandboxHelper().isSandboxEnabled()
}
}

extension PersistentStorage {
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down
25 changes: 25 additions & 0 deletions Sources/Amplitude/Utilities/SandboxHelper.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions Tests/AmplitudeTests/Storages/PersistentStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
12 changes: 12 additions & 0 deletions Tests/AmplitudeTests/Supports/TestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
34 changes: 34 additions & 0 deletions Tests/AmplitudeTests/Utilities/SandboxHelperTests.swift
Original file line number Diff line number Diff line change
@@ -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
}
Loading