diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 536a03dc8..9f8817d8d 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ 4A09BFC62684D599000E40AB /* VaultDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */; }; 4A09E54C27071F3C0056D32A /* ErrorMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09E54B27071F3C0056D32A /* ErrorMapperTests.swift */; }; 4A09E54E27071F4F0056D32A /* ErrorMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09E54D27071F4F0056D32A /* ErrorMapper.swift */; }; + 4A0AA12B2AB8DB1800CF24FD /* PermissionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */; }; + 4A0AA12D2ABA277800CF24FD /* PermissionProviderImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */; }; + 4A0AA12F2ABA2A1600CF24FD /* PermissionProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */; }; 4A0C07E225AC80C100B83211 /* UIView+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */; }; 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */; }; 4A0EAAD2296F604200E27B56 /* SessionTaskRegistratorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */; }; @@ -543,6 +546,9 @@ 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultDetailItem.swift; sourceTree = "<group>"; }; 4A09E54B27071F3C0056D32A /* ErrorMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMapperTests.swift; sourceTree = "<group>"; }; 4A09E54D27071F4F0056D32A /* ErrorMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMapper.swift; sourceTree = "<group>"; }; + 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProvider.swift; sourceTree = "<group>"; }; + 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProviderImplTests.swift; sourceTree = "<group>"; }; + 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProviderMock.swift; sourceTree = "<group>"; }; 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Preview.swift"; sourceTree = "<group>"; }; 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListPosition.swift; sourceTree = "<group>"; }; 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTaskRegistratorMock.swift; sourceTree = "<group>"; }; @@ -1192,6 +1198,7 @@ 4A9C8DFC27A007C2000063E4 /* FileProviderNotificatorTests.swift */, 4AFBFA19282946BF00E30818 /* InMemoryProgressManagerTests.swift */, 4AB1D4EF27D20420009060AB /* LocalURLProviderTests.swift */, + 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */, 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */, 4ADC66C427A7F6D6002E6CC7 /* UnlockMonitorTests.swift */, 4A4F47F224B875070033328B /* URL+NameCollisionExtensionTests.swift */, @@ -1717,6 +1724,7 @@ 4AEECD3E279EC48200C6E2B5 /* NSFileProviderChangeObserverMock.swift */, 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */, 4AEECD3A279EB24300C6E2B5 /* NSFileProviderEnumerationObserverMock.swift */, + 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */, 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */, 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */, 4ADC66C627A95E67002E6CC7 /* UnlockMonitorTaskExecutorMock.swift */, @@ -1901,6 +1909,7 @@ 4AB1D4EB27D0E027009060AB /* LocalURLProviderType.swift */, 4AA782DD282A8250001A71E3 /* NSFileProviderDomainProvider.swift */, 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */, + 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */, 4AEE6EE92825716400E1B35E /* ProgressManager.swift */, 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */, 4ADD233F26737CD400374E4E /* RootFileProviderItem.swift */, @@ -2537,6 +2546,7 @@ 4AFBFA1628293FE200E30818 /* UploadRetryingServiceSourceTests.swift in Sources */, 4AB1C33C265E9DBC00DC7A49 /* CloudTaskExecutorTestCase.swift in Sources */, 4AE5196727F495BF00BA6E4A /* WorkflowDependencyTasksCollectionMock.swift in Sources */, + 4A0AA12F2ABA2A1600CF24FD /* PermissionProviderMock.swift in Sources */, 4AC1157827F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift in Sources */, 4AE5196527F48D6600BA6E4A /* WorkflowDependencyFactoryTests.swift in Sources */, 4A49FABE271ECDE80069A0CC /* ItemEnumerationTaskManagerTests.swift in Sources */, @@ -2570,6 +2580,7 @@ 4ADC66C527A7F6D6002E6CC7 /* UnlockMonitorTests.swift in Sources */, 4ABC08D7250D1EB600E3CEDC /* DeletionTaskManagerTests.swift in Sources */, 4A511D45265EB13B000A0E01 /* ItemEnumerationTaskTests.swift in Sources */, + 4A0AA12D2ABA277800CF24FD /* PermissionProviderImplTests.swift in Sources */, 4A2F373724B47DB800460FD3 /* UploadTaskManagerTests.swift in Sources */, 4A248221266B8D37002D9F59 /* FileProviderAdapterImportDocumentTests.swift in Sources */, 4A511D5326615439000A0E01 /* ReparentTaskExecutorTests.swift in Sources */, @@ -2935,6 +2946,7 @@ 4A511D5D26668E47000A0E01 /* ReparentTaskRecord.swift in Sources */, 747F2F272587BC250072FB30 /* ReparentTask.swift in Sources */, 747F2F282587BC250072FB30 /* ReparentTaskDBManager.swift in Sources */, + 4A0AA12B2AB8DB1800CF24FD /* PermissionProvider.swift in Sources */, 4AB1D4EC27D0E027009060AB /* LocalURLProviderType.swift in Sources */, 4A511D4E2660FF9E000A0E01 /* WorkflowScheduler.swift in Sources */, 4AD9481A2909A66900072110 /* MaintenanceModeHelperServiceSource.swift in Sources */, diff --git a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift index e96a17901..9b8591d88 100644 --- a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift +++ b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift @@ -54,7 +54,9 @@ class AddHubVaultCoordinator: Coordinator { } extension AddHubVaultCoordinator: HubAuthenticationFlowDelegate { - func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async { + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { + let jwe = response.jwe + let privateKey = response.privateKey let hubVault = ExistingHubVault(vaultUID: vaultUID, delegateAccountUID: accountUID, jweData: jwe.compactSerializedData, diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift index 0da86df3c..e8fcd36ad 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift @@ -199,13 +199,9 @@ public class CryptomatorDatabase { } class func initialHubSupportMigration(_ db: Database) throws { - try db.create(table: "hubAccountInfo", body: { table in - table.column("userID", .text).primaryKey() - }) try db.create(table: "hubVaultAccount", body: { table in - table.column("id", .integer).primaryKey() - table.column("vaultUID", .text).notNull().unique().references("vaultAccounts", onDelete: .cascade) - table.column("hubUserID", .text).notNull().references("hubAccountInfo", onDelete: .cascade) + table.column("vaultUID", .text).primaryKey().references("vaultAccounts", onDelete: .cascade) + table.column("subscriptionState", .text).notNull() }) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 736a1b684..d7bea476a 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -12,7 +12,7 @@ import CryptomatorCloudAccessCore import Foundation public enum HubAuthenticationFlow { - case receivedExistingKey(Data) + case success(Data, [AnyHashable: Any]) case accessNotGranted case needsDeviceRegistration case licenseExceeded @@ -53,9 +53,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving var urlRequest = URLRequest(url: url) urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] let (data, response) = try await URLSession.shared.data(with: urlRequest) - switch (response as? HTTPURLResponse)?.statusCode { + let httpResponse = response as? HTTPURLResponse + switch httpResponse?.statusCode { case 200: - return .receivedExistingKey(data) + return .success(data, httpResponse?.allHeaderFields ?? [:]) case 402: return .licenseExceeded case 403: diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift index 1e37d9d2b..8269b4726 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift @@ -2,5 +2,11 @@ import CryptoKit import JOSESwift public protocol HubAuthenticationFlowDelegate: AnyObject { - func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async +} + +public struct HubUnlockResponse { + public let jwe: JWE + public let privateKey: P384.KeyAgreement.PrivateKey + public let subscriptionState: HubSubscriptionState } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 7b2f59031..449b10bd3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -1,4 +1,5 @@ import AppAuthCore +import CocoaLumberjackSwift import CryptoKit import CryptomatorCloudAccessCore import Foundation @@ -8,6 +9,8 @@ import UIKit public enum HubAuthenticationViewModelError: Error { case missingHubConfig case missingAuthState + case missingSubscriptionHeader + case unexpectedSubscriptionHeader } public class HubAuthenticationViewModel: ObservableObject { @@ -25,6 +28,10 @@ public class HubAuthenticationViewModel: ObservableObject { case needsAuthorization } + private enum Constants { + static var subscriptionState: String { "hub-subscription-state" } + } + @Published var authenticationFlowState: State = .userLogin @Published public var deviceName: String = UIDevice.current.name @@ -101,8 +108,8 @@ public class HubAuthenticationViewModel: ObservableObject { return } switch authFlow { - case let .receivedExistingKey(data): - await receivedExistingKey(data: data) + case let .success(data, header): + await receivedExistingKey(data: data, header: header) case .accessNotGranted: await setState(to: .accessNotGranted) case .needsDeviceRegistration: @@ -112,17 +119,22 @@ public class HubAuthenticationViewModel: ObservableObject { } } - private func receivedExistingKey(data: Data) async { + private func receivedExistingKey(data: Data, header: [AnyHashable: Any]) async { let privateKey: P384.KeyAgreement.PrivateKey let jwe: JWE + let subscriptionState: HubSubscriptionState do { privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey() jwe = try JWE(compactSerialization: data) + subscriptionState = try getSubscriptionState(from: header) } catch { await setStateToErrorState(with: error) return } - await delegate?.receivedExistingKey(jwe: jwe, privateKey: privateKey) + let response = HubUnlockResponse(jwe: jwe, + privateKey: privateKey, + subscriptionState: subscriptionState) + await delegate?.didSuccessfullyRemoteUnlock(response) } @MainActor @@ -133,4 +145,20 @@ public class HubAuthenticationViewModel: ObservableObject { private func setStateToErrorState(with error: Error) async { await setState(to: .error(description: error.localizedDescription)) } + + private func getSubscriptionState(from header: [AnyHashable: Any]) throws -> HubSubscriptionState { + guard let subscriptionStateValue = header[Constants.subscriptionState] as? String else { + DDLogError("Can't retrieve hub subscription state from header -> missing value") + throw HubAuthenticationViewModelError.missingSubscriptionHeader + } + switch subscriptionStateValue { + case "ACTIVE": + return .active + case "INACTIVE": + return .inactive + default: + DDLogError("Can't retrieve hub subscription state from header -> unexpected value") + throw HubAuthenticationViewModelError.unexpectedSubscriptionHeader + } + } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift new file mode 100644 index 000000000..f44ee2488 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift @@ -0,0 +1,72 @@ +import Dependencies +import Foundation +import GRDB + +public protocol HubRepository { + func save(_ vault: HubVault) throws + func getHubVault(vaultID: String) throws -> HubVault? +} + +public struct HubVault: Equatable { + public let vaultUID: String + public let subscriptionState: HubSubscriptionState +} + +private struct HubVaultRow: Codable, Equatable, PersistableRecord, FetchableRecord { + public static let databaseTableName = "hubVaultAccount" + + let vaultUID: String + let subscriptionState: HubSubscriptionState + + init(from vault: HubVault) { + self.vaultUID = vault.vaultUID + self.subscriptionState = vault.subscriptionState + } + + func toHubVault() -> HubVault { + HubVault(vaultUID: vaultUID, subscriptionState: subscriptionState) + } + + enum Columns: String, ColumnExpression { + case vaultUID, subscriptionState + } + + public func encode(to container: inout PersistenceContainer) { + container[Columns.vaultUID] = vaultUID + container[Columns.subscriptionState] = subscriptionState + } +} + +extension HubSubscriptionState: DatabaseValueConvertible {} + +public extension DependencyValues { + var hubRepository: HubRepository { + get { self[HubRepositoryKey.self] } + set { self[HubRepositoryKey.self] = newValue } + } +} + +private enum HubRepositoryKey: DependencyKey { + static var liveValue: HubRepository = HubDBRepository() + #if DEBUG + static var testValue: HubRepository = HubRepositoryMock() + #endif +} + +public class HubDBRepository: HubRepository { + @Dependency(\.database) private var database + + public func save(_ vault: HubVault) throws { + let row = HubVaultRow(from: vault) + try database.write { db in + try row.save(db) + } + } + + public func getHubVault(vaultID: String) throws -> HubVault? { + let row = try database.read { db in + try HubVaultRow.fetchOne(db, key: vaultID) + } + return row?.toHubVault() + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift new file mode 100644 index 000000000..daf4d3185 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift @@ -0,0 +1,4 @@ +public enum HubSubscriptionState: String, Codable { + case active + case inactive +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift index 9ecb8dbd3..9249a61ec 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift @@ -2,6 +2,7 @@ import AppAuthCore import CryptoKit import CryptomatorCloudAccessCore import CryptomatorCryptoLib +import Dependencies import JOSESwift import SwiftUI import UIKit @@ -15,6 +16,7 @@ public final class HubXPCLoginCoordinator: Coordinator { let hubAuthenticator: HubAuthenticating public let onUnlocked: () -> Void public let onErrorAlertDismissed: () -> Void + @Dependency(\.hubRepository) private var hubRepository public init(navigationController: UINavigationController, domain: NSFileProviderDomain, @@ -42,25 +44,26 @@ public final class HubXPCLoginCoordinator: Coordinator { } extension HubXPCLoginCoordinator: HubAuthenticationFlowDelegate { - public func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async { + public func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { let masterkey: Masterkey do { - masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey) + masterkey = try JWEHelper.decrypt(jwe: response.jwe, with: response.privateKey) } catch { handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) return } - let xpc: XPC<VaultUnlocking> do { - xpc = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) + let xpc: XPC<VaultUnlocking> = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) defer { fileProviderConnector.invalidateXPC(xpc) } try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue() - fileProviderConnector.invalidateXPC(xpc) + let hubVault = HubVault(vaultUID: domain.identifier.rawValue, subscriptionState: response.subscriptionState) + try hubRepository.save(hubVault) onUnlocked() } catch { handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) + return } } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift new file mode 100644 index 000000000..92e0d7896 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift @@ -0,0 +1,53 @@ +import Foundation + +#if DEBUG + +// MARK: - HubRepositoryMock - + +final class HubRepositoryMock: HubRepository { + // MARK: - save + + var saveThrowableError: Error? + var saveCallsCount = 0 + var saveCalled: Bool { + saveCallsCount > 0 + } + + var saveReceivedVault: HubVault? + var saveReceivedInvocations: [HubVault] = [] + var saveClosure: ((HubVault) throws -> Void)? + + func save(_ vault: HubVault) throws { + if let error = saveThrowableError { + throw error + } + saveCallsCount += 1 + saveReceivedVault = vault + saveReceivedInvocations.append(vault) + try saveClosure?(vault) + } + + // MARK: - getHubVault + + var getHubVaultVaultIDThrowableError: Error? + var getHubVaultVaultIDCallsCount = 0 + var getHubVaultVaultIDCalled: Bool { + getHubVaultVaultIDCallsCount > 0 + } + + var getHubVaultVaultIDReceivedVaultID: String? + var getHubVaultVaultIDReceivedInvocations: [String] = [] + var getHubVaultVaultIDReturnValue: HubVault? + var getHubVaultVaultIDClosure: ((String) throws -> HubVault?)? + + func getHubVault(vaultID: String) throws -> HubVault? { + if let error = getHubVaultVaultIDThrowableError { + throw error + } + getHubVaultVaultIDCallsCount += 1 + getHubVaultVaultIDReceivedVaultID = vaultID + getHubVaultVaultIDReceivedInvocations.append(vaultID) + return try getHubVaultVaultIDClosure.map({ try $0(vaultID) }) ?? getHubVaultVaultIDReturnValue + } +} +#endif diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift new file mode 100644 index 000000000..211b2f87f --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift @@ -0,0 +1,89 @@ +import GRDB +import XCTest +@testable import CryptomatorCommonCore + +final class HubDBRepositoryTests: XCTestCase { + private var inMemoryDB: DatabaseQueue! + private var repository: HubDBRepository! + private var vaultAccountManager: VaultAccountManager! + private var cloudAccountManager: CloudProviderAccountManager! + + override func setUpWithError() throws { + repository = HubDBRepository() + vaultAccountManager = VaultAccountDBManager() + cloudAccountManager = CloudProviderAccountDBManager() + } + + func testSaveAndRetrieve() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // WHEN + // saving a hub vault + let vault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(vault) + + // THEN + // it can be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertEqual(vault, retrievedVault) + } + + func testSaveToUpdate() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // WHEN + // saving a hub vault + let initialVault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(initialVault) + + // and saving the hub vault with the same vault ID but a changed subscription state + let updatedVault = HubVault(vaultUID: vaultID, subscriptionState: .inactive) + try repository.save(updatedVault) + + // THEN + // it the updated version can be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertEqual(updatedVault, retrievedVault) + } + + func testDeleteVaultAccountAlsoDeletesHubVault() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // and a hub vault has been created for the vault id + let vault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(vault) + + // WHEN + // the vault account gets deleted + try vaultAccountManager.removeAccount(with: vaultID) + + // THEN + // the hub vault account has been deleted and can not be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertNil(retrievedVault) + } +} diff --git a/CryptomatorFileProvider/DB/WorkingSetObserver.swift b/CryptomatorFileProvider/DB/WorkingSetObserver.swift index 8b35d1d62..b4f411807 100644 --- a/CryptomatorFileProvider/DB/WorkingSetObserver.swift +++ b/CryptomatorFileProvider/DB/WorkingSetObserver.swift @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import Dependencies import FileProvider import Foundation import GRDB @@ -23,8 +24,13 @@ class WorkingSetObserver: WorkingSetObserving { private let notificator: FileProviderNotificatorType private var currentWorkingSetItems = Set<FileProviderItem>() private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, database: DatabaseReader, notificator: FileProviderNotificatorType, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + database: DatabaseReader, + notificator: FileProviderNotificatorType, + uploadTaskManager: UploadTaskManager, + cachedFileManager: CachedFileManager) { self.domainIdentifier = domainIdentifier self.database = database self.notificator = notificator diff --git a/CryptomatorFileProvider/FileProviderAdapter.swift b/CryptomatorFileProvider/FileProviderAdapter.swift index ce2564f5d..2ff7e112c 100644 --- a/CryptomatorFileProvider/FileProviderAdapter.swift +++ b/CryptomatorFileProvider/FileProviderAdapter.swift @@ -74,6 +74,7 @@ public class FileProviderAdapter: FileProviderAdapterType { private let domainIdentifier: NSFileProviderDomainIdentifier private let fileCoordinator: NSFileCoordinator private let taskRegistrator: SessionTaskRegistrator + @Dependency(\.permissionProvider) private var permissionProvider init(domainIdentifier: NSFileProviderDomainIdentifier, uploadTaskManager: UploadTaskManager, diff --git a/CryptomatorFileProvider/FileProviderAdapterManager.swift b/CryptomatorFileProvider/FileProviderAdapterManager.swift index 660bf9626..d53e08185 100644 --- a/CryptomatorFileProvider/FileProviderAdapterManager.swift +++ b/CryptomatorFileProvider/FileProviderAdapterManager.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -32,12 +33,27 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { private let notificatorManager: FileProviderNotificatorManagerType private let queue = DispatchQueue(label: "FileProviderAdapterManager", qos: .userInitiated) private let providerIdentifier: String + @Dependency(\.permissionProvider) private var permissionProvider convenience init() { - self.init(masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, vaultKeepUnlockedHelper: VaultKeepUnlockedManager.shared, vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared, vaultManager: VaultDBManager.shared, adapterCache: FileProviderAdapterCache(), notificatorManager: FileProviderNotificatorManager.shared, unlockMonitor: UnlockMonitor(), providerIdentifier: NSFileProviderManager.default.providerIdentifier) + self.init(masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, + vaultKeepUnlockedHelper: VaultKeepUnlockedManager.shared, + vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared, + vaultManager: VaultDBManager.shared, + adapterCache: FileProviderAdapterCache(), + notificatorManager: FileProviderNotificatorManager.shared, + unlockMonitor: UnlockMonitor(), + providerIdentifier: NSFileProviderManager.default.providerIdentifier) } - init(masterkeyCacheManager: MasterkeyCacheManager, vaultKeepUnlockedHelper: VaultKeepUnlockedHelper, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings, vaultManager: VaultManager, adapterCache: FileProviderAdapterCacheType, notificatorManager: FileProviderNotificatorManagerType, unlockMonitor: UnlockMonitorType, providerIdentifier: String) { + init(masterkeyCacheManager: MasterkeyCacheManager, + vaultKeepUnlockedHelper: VaultKeepUnlockedHelper, + vaultKeepUnlockedSettings: VaultKeepUnlockedSettings, + vaultManager: VaultManager, + adapterCache: FileProviderAdapterCacheType, + notificatorManager: FileProviderNotificatorManagerType, + unlockMonitor: UnlockMonitorType, + providerIdentifier: String) { self.masterkeyCacheManager = masterkeyCacheManager self.vaultKeepUnlockedHelper = vaultKeepUnlockedHelper self.vaultKeepUnlockedSettings = vaultKeepUnlockedSettings @@ -190,7 +206,12 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { notificator: notificator, localURLProvider: delegate, taskRegistrator: taskRegistrator) - let workingSetObserver = WorkingSetObserver(domainIdentifier: domainIdentifier, database: database, notificator: notificator, uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager) + + let workingSetObserver = WorkingSetObserver(domainIdentifier: domainIdentifier, + database: database, + notificator: notificator, + uploadTaskManager: uploadTaskManager, + cachedFileManager: cachedFileManager) workingSetObserver.startObservation() return AdapterCacheItem(adapter: adapter, maintenanceManager: maintenanceManager, workingSetObserver: workingSetObserver) } diff --git a/CryptomatorFileProvider/FileProviderItem.swift b/CryptomatorFileProvider/FileProviderItem.swift index 64c822b6f..2b0bc5955 100644 --- a/CryptomatorFileProvider/FileProviderItem.swift +++ b/CryptomatorFileProvider/FileProviderItem.swift @@ -24,6 +24,7 @@ public class FileProviderItem: NSObject, NSFileProviderItem { let localURL: URL? let domainIdentifier: NSFileProviderDomainIdentifier @Dependency(\.fullVersionChecker) private var fullVersionChecker + @Dependency(\.permissionProvider) private var permissionProvider init(metadata: ItemMetadata, domainIdentifier: NSFileProviderDomainIdentifier, newestVersionLocallyCached: Bool = false, localURL: URL? = nil, error: Error? = nil) { self.metadata = metadata @@ -50,19 +51,7 @@ public class FileProviderItem: NSObject, NSFileProviderItem { } public var capabilities: NSFileProviderItemCapabilities { - if metadata.statusCode == .uploadError { - return .allowsDeleting - } - if !fullVersionChecker.isFullVersion { - return FileProviderItem.readOnlyCapabilities - } - if metadata.type == .folder { - return [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] - } - if metadata.statusCode == .isUploading { - return .allowsReading - } - return [.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + return permissionProvider.getPermissions(for: metadata, at: domainIdentifier) } public var filename: String { diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift index c3feed4e7..6e1c23446 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift @@ -8,6 +8,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import Dependencies import Foundation import Promises @@ -30,8 +31,13 @@ class DownloadTaskExecutor: WorkflowMiddleware { private let downloadTaskManager: DownloadTaskManager private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, downloadTaskManager: DownloadTaskManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager, + downloadTaskManager: DownloadTaskManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift index 23235e3b6..fd2623508 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift @@ -29,7 +29,9 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager @@ -53,11 +55,13 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { assert(itemMetadata.id != nil) assert(itemMetadata.type == .folder) - return provider.createFolder(at: itemMetadata.cloudPath).then { _ -> FileProviderItem in + return provider.createFolder(at: itemMetadata.cloudPath).then { [domainIdentifier, itemMetadataManager] _ -> FileProviderItem in itemMetadata.statusCode = .isUploaded itemMetadata.isPlaceholderItem = false - try self.itemMetadataManager.updateMetadata(itemMetadata) - return FileProviderItem(metadata: itemMetadata, domainIdentifier: self.domainIdentifier, newestVersionLocallyCached: true) + try itemMetadataManager.updateMetadata(itemMetadata) + return FileProviderItem(metadata: itemMetadata, + domainIdentifier: domainIdentifier, + newestVersionLocallyCached: true) } } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift index db91a48d7..5d4e96be5 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift @@ -37,7 +37,15 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, uploadTaskManager: UploadTaskManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, deleteItemHelper: DeleteItemHelper) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager, + uploadTaskManager: UploadTaskManager, + reparentTaskManager: ReparentTaskManager, + deletionTaskManager: DeletionTaskManager, + itemEnumerationTaskManager: ItemEnumerationTaskManager, + deleteItemHelper: DeleteItemHelper) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift index 6593c372a..7b85d9a3c 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation import Promises @@ -30,8 +31,13 @@ class ReparentTaskExecutor: WorkflowMiddleware { private let itemMetadataManager: ItemMetadataManager private let cachedFileManager: CachedFileManager private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, reparentTaskManager: ReparentTaskManager, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + reparentTaskManager: ReparentTaskManager, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.reparentTaskManager = reparentTaskManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift index a670cc153..2fc5abf38 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift @@ -8,6 +8,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation import Promises @@ -32,8 +33,14 @@ class UploadTaskExecutor: WorkflowMiddleware { let uploadTaskManager: UploadTaskManager let domainIdentifier: NSFileProviderDomainIdentifier let progressManager: ProgressManager + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, uploadTaskManager: UploadTaskManager, progressManager: ProgressManager = InMemoryProgressManager.shared) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + cachedFileManager: CachedFileManager, + itemMetadataManager: ItemMetadataManager, + uploadTaskManager: UploadTaskManager, + progressManager: ProgressManager = InMemoryProgressManager.shared) { self.domainIdentifier = domainIdentifier self.provider = provider self.cachedFileManager = cachedFileManager diff --git a/CryptomatorFileProvider/PermissionProvider.swift b/CryptomatorFileProvider/PermissionProvider.swift new file mode 100644 index 000000000..bcdbee887 --- /dev/null +++ b/CryptomatorFileProvider/PermissionProvider.swift @@ -0,0 +1,127 @@ +// +// PermissionProvider.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 18.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CocoaLumberjackSwift +import CryptomatorCommonCore +import Dependencies +import FileProvider +import Foundation + +public protocol PermissionProvider { + /** + Returns the permission for a given `item` at a given `domain`. + + The following restrictions can apply to any item: + - in case of an upload error it's only allowed to delete the item. + - in case of a free version only reading is allowed, except if the vault belongs to Cryptomator Hub and it has an active subscription state. + + The following capabilities hold for files: + - reading + - adding sub items + - content enumerating + - deleting + - renaming + - reparenting + + - Note: In case of an running upload, i.e. a creation of the folder in the cloud, the capabilities do not get restricted except if something listed above restricts all items of the vault. + + The following capabilities hold for files: + - reading + - writing + - deleting + - renaming + - reparenting + - Note: In case of an running upload for a file it's only allowed to read the item. To prevent additional modifications. + + */ + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities +} + +private enum PermissionProviderKey: DependencyKey { + static let liveValue: PermissionProvider = PermissionProviderImpl() + #if DEBUG + static let testValue: PermissionProvider = UnimplementedPermissionProvider() + #endif +} + +extension DependencyValues { + var permissionProvider: PermissionProvider { + get { self[PermissionProviderKey.self] } + set { self[PermissionProviderKey.self] = newValue } + } +} + +struct PermissionProviderImpl: PermissionProvider { + @Dependency(\.fullVersionChecker) private var fullVersionChecker + @Dependency(\.hubRepository) private var hubRepository + + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + if item.statusCode == .uploadError { + return .allowsDeleting + } + + let vaultID = domain.rawValue + let hubSubscriptionState: HubSubscriptionState? + do { + let hubVault = try hubRepository.getHubVault(vaultID: vaultID) + hubSubscriptionState = hubVault?.subscriptionState + } catch { + hubSubscriptionState = nil + DDLogError("Failed to retrieve possible hub vault for with id: \(vaultID)") + } + + if !fullVersionChecker.isFullVersion && hubSubscriptionState != .active { + return FileProviderItem.readOnlyCapabilities + } + if item.type == .folder { + return [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + } + if item.statusCode == .isUploading { + return FileProviderItem.readOnlyCapabilities + } + return [.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + } + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + if fullVersionChecker.isFullVersion { + return [.allowsAll] + } + guard let domain else { + return FileProviderItem.readOnlyCapabilities + } + let vaultID = domain.rawValue + let hubSubscriptionState: HubSubscriptionState? + do { + let hubVault = try hubRepository.getHubVault(vaultID: vaultID) + hubSubscriptionState = hubVault?.subscriptionState + } catch { + hubSubscriptionState = nil + DDLogError("Failed to retrieve possible hub vault for with id: \(vaultID)") + } + switch hubSubscriptionState { + case .active: + return [.allowsAll] + case .inactive, nil: + return FileProviderItem.readOnlyCapabilities + } + } +} + +#if DEBUG +struct UnimplementedPermissionProvider: PermissionProvider { + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + unimplemented("\(Self.self).getPermissions", placeholder: .allowsReading) + } + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + unimplemented("\(Self.self).getPermissionsForRootItem", placeholder: .allowsReading) + } +} +#endif diff --git a/CryptomatorFileProvider/RootFileProviderItem.swift b/CryptomatorFileProvider/RootFileProviderItem.swift index c46984e8b..fafb4dafb 100644 --- a/CryptomatorFileProvider/RootFileProviderItem.swift +++ b/CryptomatorFileProvider/RootFileProviderItem.swift @@ -19,12 +19,13 @@ public class RootFileProviderItem: NSObject, NSFileProviderItem { public let typeIdentifier = kUTTypeFolder as String public let documentSize: NSNumber? = nil public var capabilities: NSFileProviderItemCapabilities { - if fullVersionChecker.isFullVersion { - return [.allowsAll] - } else { - return FileProviderItem.readOnlyCapabilities - } + return permissionProvider.getPermissionsForRootItem(at: domain?.identifier) } - @Dependency(\.fullVersionChecker) private var fullVersionChecker + private let domain: NSFileProviderDomain? + @Dependency(\.permissionProvider) private var permissionProvider + + public init(domain: NSFileProviderDomain?) { + self.domain = domain + } } diff --git a/CryptomatorFileProvider/Workflow/WorkflowFactory.swift b/CryptomatorFileProvider/Workflow/WorkflowFactory.swift index 2ebe1387e..17a92c359 100644 --- a/CryptomatorFileProvider/Workflow/WorkflowFactory.swift +++ b/CryptomatorFileProvider/Workflow/WorkflowFactory.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation @@ -21,6 +22,7 @@ struct WorkflowFactory { let downloadTaskManager: DownloadTaskManager let dependencyFactory = WorkflowDependencyFactory() let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider func createWorkflow(for deletionTask: DeletionTask) -> Workflow<Void> { let taskExecutor = DeletionTaskExecutor(provider: provider, itemMetadataManager: itemMetadataManager) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift index a7882b36f..ca98e991a 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift @@ -9,6 +9,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { override func setUpWithError() throws { @@ -34,6 +35,9 @@ class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { ItemMetadata(id: 3, name: "TestFolder", type: .file, size: nil, parentID: 4, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Foo/TestFolder"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: 1, tagData: nil) ] metadataManagerMock.workingSetMetadata = mockMetadata + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let expectation = XCTestExpectation() adapter.enumerateItems(for: .workingSet, withPageToken: nil).then { itemList in XCTAssertEqual(mockMetadata.map { FileProviderItem(metadata: $0, domainIdentifier: .test) }, itemList.items) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift index 2c83892f7..e2f4fb8cd 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift @@ -12,6 +12,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { let itemID: Int64 = 2 @@ -26,6 +27,9 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { // MARK: LocalItemImport func testLocalItemImport() throws { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let fileURL = tmpDirectory.appendingPathComponent("ItemToBeImported.txt", isDirectory: false) let fileContent = "TestContent" try fileContent.write(to: fileURL, atomically: true, encoding: .utf8) diff --git a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift index 131daf148..912b7bc19 100644 --- a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift @@ -13,6 +13,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderEnumeratorTestCase: XCTestCase { var enumerationObserverMock: NSFileProviderEnumerationObserverMock! @@ -50,6 +51,10 @@ class FileProviderEnumeratorTestCase: XCTestCase { } func assertChangeObserverUpdated(deletedItems: [NSFileProviderItemIdentifier], updatedItems: [FileProviderItem], currentSyncAnchor: NSFileProviderSyncAnchor) { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([deletedItems], changeObserverMock.didDeleteItemsWithIdentifiersReceivedInvocations) let receivedUpdatedItems = changeObserverMock.didUpdateReceivedInvocations as? [[FileProviderItem]] XCTAssertEqual([updatedItems], receivedUpdatedItems) @@ -179,6 +184,10 @@ class FileProviderEnumeratorTests: FileProviderEnumeratorTestCase { } private func assertEnumerateItemObserverSucceeded(itemList: FileProviderItemList) { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([itemList.nextPageToken], enumerationObserverMock.finishEnumeratingUpToReceivedInvocations) let receivedInvocations = enumerationObserverMock.didEnumerateReceivedInvocations as? [[FileProviderItem]] XCTAssertEqual([items], receivedInvocations) diff --git a/CryptomatorFileProviderTests/FileProviderItemTests.swift b/CryptomatorFileProviderTests/FileProviderItemTests.swift index 1c4af1510..e8829ef6c 100644 --- a/CryptomatorFileProviderTests/FileProviderItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderItemTests.swift @@ -108,59 +108,19 @@ class FileProviderItemTests: XCTestCase { // MARK: Capabilities - func testUploadingItemRestrictsCapabilityToRead() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = true - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) + func testCapabilitiesArePassedThroughFromPermissionProvider() { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) let cloudPath = CloudPath("/test.txt") let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, item.capabilities) - } - - func testUploadingFolderDoesNotRestrictCapabilities() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = true - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) - - let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual([.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], item.capabilities) - } - - func testCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) - - let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, item.capabilities) - } - - func testFailedUploadItemCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) - let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) - } - - func testFailedUploadFolderCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) - - let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) + let capabilities: [NSFileProviderItemCapabilities] = [.allowsAddingSubItems, .allowsContentEnumerating, .allowsDeleting, .allowsReading, .allowsReparenting, .allowsWriting] + for capability in capabilities { + permissionProviderMock.getPermissionsForAtReturnValue = capability + XCTAssertEqual(capability, item.capabilities) + } } // MARK: Evict File From Cache Action diff --git a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift index 650d54507..4e54026a2 100644 --- a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift @@ -9,6 +9,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies @available(iOS 14.0, *) class FileProviderNotificatorTests: XCTestCase { @@ -97,6 +98,11 @@ class FileProviderNotificatorTests: XCTestCase { }) let actualItems = notificator.popUpdateContainerItems() as? [FileProviderItem] + + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([updatedItem], actualItems?.sorted()) XCTAssert(notificator.popUpdateWorkingSetItems().isEmpty) XCTAssert(notificator.getItemIdentifiersToDeleteFromWorkingSet().isEmpty) @@ -109,6 +115,9 @@ class FileProviderNotificatorTests: XCTestCase { } private func assertUpdateWorkingSetHasUpdatedItems() { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let actualItems = notificator.popUpdateWorkingSetItems() as? [FileProviderItem] XCTAssertEqual(updatedItems.sorted(), actualItems?.sorted()) } diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift index 05dc2be97..1c4ca9b14 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import Promises import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { override func setUpWithError() throws { @@ -201,6 +202,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { // MARK: Folder + // swiftlint:disable:next function_body_length func testFolderEnumeration() throws { let expectation = XCTestExpectation(description: "Folder Enumeration") @@ -222,6 +224,9 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectedSubFolderFileProviderItems = expectedItemMetadataInsideSubFolder.map { FileProviderItem(metadata: $0, domainIdentifier: .test) } let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> FileProviderItem in XCTAssertEqual(5, fileProviderItemList.items.count) @@ -283,6 +288,10 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false), domainIdentifier: .test), FileProviderItem(metadata: ItemMetadata(id: 7, name: "NewFileFromCloud", type: .file, size: 24, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/NewFileFromCloud"), isPlaceholderItem: false), domainIdentifier: .test)] + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> Promise<FileProviderItemList> in diff --git a/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift b/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift new file mode 100644 index 000000000..7571ceee7 --- /dev/null +++ b/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift @@ -0,0 +1,51 @@ +// +// PermissionProviderMock.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 19.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CryptomatorFileProvider +import FileProvider +import Foundation + +final class PermissionProviderMock: PermissionProvider { + // MARK: - getPermissions + + var getPermissionsForAtCallsCount = 0 + var getPermissionsForAtCalled: Bool { + getPermissionsForAtCallsCount > 0 + } + + var getPermissionsForAtReceivedArguments: (item: ItemMetadata, domain: NSFileProviderDomainIdentifier)? + var getPermissionsForAtReceivedInvocations: [(item: ItemMetadata, domain: NSFileProviderDomainIdentifier)] = [] + var getPermissionsForAtReturnValue: NSFileProviderItemCapabilities! + var getPermissionsForAtClosure: ((ItemMetadata, NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities)? + + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + getPermissionsForAtCallsCount += 1 + getPermissionsForAtReceivedArguments = (item: item, domain: domain) + getPermissionsForAtReceivedInvocations.append((item: item, domain: domain)) + return getPermissionsForAtClosure.map({ $0(item, domain) }) ?? getPermissionsForAtReturnValue + } + + // MARK: - getPermissionsForRootItem + + var getPermissionsForRootItemAtCallsCount = 0 + var getPermissionsForRootItemAtCalled: Bool { + getPermissionsForRootItemAtCallsCount > 0 + } + + var getPermissionsForRootItemAtReceivedDomain: NSFileProviderDomainIdentifier? + var getPermissionsForRootItemAtReceivedInvocations: [NSFileProviderDomainIdentifier?] = [] + var getPermissionsForRootItemAtReturnValue: NSFileProviderItemCapabilities! + var getPermissionsForRootItemAtClosure: ((NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities)? + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + getPermissionsForRootItemAtCallsCount += 1 + getPermissionsForRootItemAtReceivedDomain = domain + getPermissionsForRootItemAtReceivedInvocations.append(domain) + return getPermissionsForRootItemAtClosure.map({ $0(domain) }) ?? getPermissionsForRootItemAtReturnValue + } +} diff --git a/CryptomatorFileProviderTests/PermissionProviderImplTests.swift b/CryptomatorFileProviderTests/PermissionProviderImplTests.swift new file mode 100644 index 000000000..67fdb5a8e --- /dev/null +++ b/CryptomatorFileProviderTests/PermissionProviderImplTests.swift @@ -0,0 +1,137 @@ +// +// PermissionProviderImplTests.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 19.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import XCTest +@testable import CryptomatorCommonCore +@testable import CryptomatorFileProvider +@testable import Dependencies + +final class PermissionProviderImplTests: XCTestCase { + private static let defaultFolderCapabilities: NSFileProviderItemCapabilities = [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + private var fullVersionCheckerMock: FullVersionCheckerMock! + private var hubRepositoryMock: HubRepositoryMock! + private var permissionProvider: PermissionProviderImpl! + + override func setUpWithError() throws { + fullVersionCheckerMock = FullVersionCheckerMock() + hubRepositoryMock = HubRepositoryMock() + DependencyValues.mockDependency(\.hubRepository, with: hubRepositoryMock) + DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) + permissionProvider = PermissionProviderImpl() + } + + // MARK: Full Version + + func testUploadingItemRestrictsCapabilityToRead() { + fullVersionCheckerMock.isFullVersion = true + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testUploadingFolderDoesNotRestrictCapabilities() { + fullVersionCheckerMock.isFullVersion = true + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testFailedUploadItemCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, actualCapabilities) + } + + func testFailedUploadFolderCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, actualCapabilities) + } + + func testFullVersionNoActiveHubScriptionReturnsFullPermissionsForFile() { + fullVersionCheckerMock.isFullVersion = true + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .inactive) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual([.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], actualCapabilities) + } + + // MARK: Cryptomator Hub + + func testUploadingItemRestrictsCapabilityToReadWithActiveHubSubscription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testNoFullVersionNoActiveHubSubscriptionRestrictsToReadOnly() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .inactive) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testFolderCapabilitiesNoFullVersionActiveHubSubscription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testUploadingFolderDoesNotRestrictCapabilitiesForActiveHubSubsription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testNoFullVersionActiveHubScriptionReturnsFullPermissionsForFile() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual([.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], actualCapabilities) + } +} diff --git a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift index 2772c4de2..eee1b96de 100644 --- a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift +++ b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift @@ -11,6 +11,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class CacheManagingServiceSourceTests: XCTestCase { var serviceSource: CacheManagingServiceSource! @@ -57,6 +58,9 @@ class CacheManagingServiceSourceTests: XCTestCase { let expectation = XCTestExpectation() let cacheManagerMock = CachedFileManagerMock() cacheManagerFactoryMock.createCachedFileManagerForReturnValue = cacheManagerMock + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let domainIdentifier = NSFileProviderDomainIdentifier("Test-Domain") let itemID: Int64 = 2 let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: itemID) diff --git a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift index 034a0bca1..728b31357 100644 --- a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift +++ b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import GRDB import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class WorkingSetObserverTests: XCTestCase { var observer: WorkingSetObserver! @@ -31,6 +32,9 @@ class WorkingSetObserverTests: XCTestCase { XCTAssertEqual(1, notificatorMock.updateWorkingSetItemsCallsCount) let actualUpdatedItems = notificatorMock.updateWorkingSetItemsReceivedItems as? [FileProviderItem] + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading XCTAssertEqual(updatedItems.sorted(), actualUpdatedItems?.sorted()) XCTAssertEqual(1, notificatorMock.refreshWorkingSetCallsCount) } diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index ee8f385b9..2458a4f48 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -69,7 +69,7 @@ class FileProviderExtension: NSFileProviderExtension { // resolve the given identifier to a record in the model DDLogDebug("FPExt: item(for: \(identifier)) called") if identifier == .rootContainer || identifier.rawValue == "File Provider Storage" || identifier.rawValue == domain?.identifier.rawValue { - return RootFileProviderItem() + return RootFileProviderItem(domain: domain) } let adapter = try getAdapterWithWrappedError() return try adapter.item(for: identifier)