diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 5f2dac538..1b1636e24 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 21489B7C267CBA0E00BDE4AC /* ClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21489B7B267CBA0E00BDE4AC /* ClientConfiguration.swift */; }; 21489B80267CC39E00BDE4AC /* OrganisationalRulesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21489B7F267CC39E00BDE4AC /* OrganisationalRulesService.swift */; }; 21489B83267CC99C00BDE4AC /* OrganisationalRulesServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21489B82267CC99C00BDE4AC /* OrganisationalRulesServiceError.swift */; }; + 215002A32690B1DD00980DDD /* client_configuraion_with_unknown_flag.json in Resources */ = {isa = PBXBuildFile; fileRef = 215002A22690B1DD00980DDD /* client_configuraion_with_unknown_flag.json */; }; 215897E8267A553300423694 /* FilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215897E7267A553200423694 /* FilesManager.swift */; }; 2196A2202684B9BE001B9E00 /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2196A21F2684B9BE001B9E00 /* URLExtension.swift */; }; 21C7DEFC26669A3700C44800 /* CalendarExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C7DEFB26669A3700C44800 /* CalendarExtension.swift */; }; @@ -370,6 +371,7 @@ 21489B7B267CBA0E00BDE4AC /* ClientConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConfiguration.swift; sourceTree = ""; }; 21489B7F267CC39E00BDE4AC /* OrganisationalRulesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganisationalRulesService.swift; sourceTree = ""; }; 21489B82267CC99C00BDE4AC /* OrganisationalRulesServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganisationalRulesServiceError.swift; sourceTree = ""; }; + 215002A22690B1DD00980DDD /* client_configuraion_with_unknown_flag.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = client_configuraion_with_unknown_flag.json; sourceTree = ""; }; 215897E7267A553200423694 /* FilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesManager.swift; sourceTree = ""; }; 2196A21F2684B9BE001B9E00 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; 21C7DEFB26669A3700C44800 /* CalendarExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarExtension.swift; sourceTree = ""; }; @@ -818,6 +820,7 @@ 21EA3B2E26565B7400691848 /* client_configuraion.json */, 21EA3B3C26565B9800691848 /* client_configuraion_partly_empty.json */, 21EA3B3526565B8100691848 /* client_configuraion_empty.json */, + 215002A22690B1DD00980DDD /* client_configuraion_with_unknown_flag.json */, ); path = "Models Parsing"; sourceTree = ""; @@ -2055,6 +2058,7 @@ 9F976569267E18F30058419D /* client_configuraion.json in Resources */, 9F976576267E18F90058419D /* client_configuraion_partly_empty.json in Resources */, 9F97657D267E18FE0058419D /* client_configuraion_empty.json in Resources */, + 215002A32690B1DD00980DDD /* client_configuraion_with_unknown_flag.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift b/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift index 59d8ee368..59d226b14 100644 --- a/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift +++ b/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift @@ -16,8 +16,8 @@ import FlowCryptUI * - Tap on each row will navigate user to appropriate settings controller */ final class SettingsViewController: TableNodeViewController { - private enum Settings: Int, CaseIterable { - case backups, privacy, contacts, keys, atteseter, notifications, legal, experimental + private enum SettingsMenuItem: Int, CaseIterable { + case backups, privacy, contacts, keys, attester, notifications, legal, experimental var title: String { switch self { @@ -25,23 +25,38 @@ final class SettingsViewController: TableNodeViewController { case .privacy: return "settings_screen_security".localized case .contacts: return "settings_screen_contacts".localized case .keys: return "settings_screen_keys".localized - case .atteseter: return "settings_screen_attester".localized + case .attester: return "settings_screen_attester".localized case .notifications: return "settings_screen_notifications".localized case .legal: return "settings_screen_legal".localized case .experimental: return "settings_screen_experimental".localized } } + + static func filtered(with rules: OrganisationalRules) -> [SettingsMenuItem] { + var cases = SettingsMenuItem.allCases + + if !rules.canBackupKeys { + cases.removeAll(where: { $0 == .backups }) + } + + return cases + } } private let decorator: SettingsViewDecoratorType private let currentUser: User? + private let organisationalRules: OrganisationalRules + private let rows: [SettingsMenuItem] init( decorator: SettingsViewDecoratorType = SettingsViewDecorator(), - currentUser: User? = DataService.shared.currentUser + currentUser: User? = DataService.shared.currentUser, + organisationalRulesService: OrganisationalRulesServiceType = OrganisationalRulesService() ) { self.decorator = decorator self.currentUser = currentUser + self.organisationalRules = organisationalRulesService.getSavedOrganisationalRulesForCurrentUser() + self.rows = SettingsMenuItem.filtered(with: self.organisationalRules) super.init(node: TableNode()) } @@ -71,13 +86,14 @@ final class SettingsViewController: TableNodeViewController { extension SettingsViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { - Settings.allCases.count + rows.count } func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { { [weak self] in - guard let self = self, let setting = Settings(rawValue: indexPath.row) else { return ASCellNode() } + guard let self = self else { return ASCellNode() } + let setting = self.rows[indexPath.row] return SettingsCellNode( title: self.decorator.attributedSetting(setting.title), insets: self.decorator.insets @@ -86,7 +102,7 @@ extension SettingsViewController: ASTableDelegate, ASTableDataSource { } func tableNode(_: ASTableNode, didSelectRowAt indexPath: IndexPath) { - guard let setting = Settings(rawValue: indexPath.row) else { return assertionFailure() } + let setting = rows[indexPath.row] proceed(to: setting) } } @@ -94,7 +110,7 @@ extension SettingsViewController: ASTableDelegate, ASTableDataSource { // MARK: - Actions extension SettingsViewController { - private func proceed(to setting: Settings) { + private func proceed(to setting: SettingsMenuItem) { let viewController: UIViewController? switch setting { @@ -105,7 +121,8 @@ extension SettingsViewController { case .contacts: viewController = ContactsListViewController() case .backups: - guard let currentUser = currentUser else { + guard let currentUser = currentUser, + !organisationalRules.canBackupKeys else { viewController = nil return } diff --git a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift index fac5b7e4a..29b81d6b5 100644 --- a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift @@ -53,6 +53,7 @@ final class SetupInitialViewController: TableNodeViewController { private let user: UserId private let router: GlobalRouterType private let decorator: SetupViewDecorator + private let organisationalRules: OrganisationalRules private lazy var logger = Logger.nested(in: Self.self, with: .setup) @@ -60,12 +61,14 @@ final class SetupInitialViewController: TableNodeViewController { user: UserId, backupService: BackupServiceType = BackupService(), router: GlobalRouterType = GlobalRouter(), - decorator: SetupViewDecorator = SetupViewDecorator() + decorator: SetupViewDecorator = SetupViewDecorator(), + organisationalRulesService: OrganisationalRulesServiceType = OrganisationalRulesService() ) { self.user = user self.backupService = backupService self.router = router self.decorator = decorator + self.organisationalRules = organisationalRulesService.getSavedOrganisationalRulesForCurrentUser() super.init(node: TableNode()) } @@ -103,6 +106,12 @@ extension SetupInitialViewController { } private func searchBackups() { + if !organisationalRules.canBackupKeys { + logger.logInfo("Skipping backups searching because canBackupKeys == false") + proceedToSetupWith(keys: []) + return + } + logger.logInfo("Searching for backups in inbox") backupService.fetchBackupsFromInbox(for: user) diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index bbe7b3493..fc621af0d 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -113,12 +113,15 @@ extension EncryptedStorage: LogOutHandler { .filter { keys.map(\.primaryLongid).contains($0.longid) } let sessions = storage.objects(SessionObject.self) .filter { $0.email == email } + let clientConfigurations = storage.objects(ClientConfigurationObject.self) + .filter { $0.userEmail == email } try storage.write { storage.delete(keys) storage.delete(sessions) storage.delete(passPhrases) storage.delete(userToDelete) + storage.delete(clientConfigurations) } } } diff --git a/FlowCrypt/Functionality/Services/AppStartup.swift b/FlowCrypt/Functionality/Services/AppStartup.swift index 569f38c49..b54532e5b 100644 --- a/FlowCrypt/Functionality/Services/AppStartup.swift +++ b/FlowCrypt/Functionality/Services/AppStartup.swift @@ -27,8 +27,7 @@ struct AppStartup { try awaitPromise(self.setupCore()) try self.setupMigrationIfNeeded() try self.setupSession() - // Fetching of org rules is being called async in purpose we don't need to wait until it's fetched - self.getUserOrgRulesIfNeeded() + try self.getUserOrgRulesIfNeeded() }.then(on: .main) { self.chooseView(for: window, session: session) }.catch(on: .main) { error in @@ -96,9 +95,10 @@ struct AppStartup { } } - private func getUserOrgRulesIfNeeded() { + private func getUserOrgRulesIfNeeded() throws { if DataService.shared.isLoggedIn { - _ = OrganisationalRulesService().fetchOrganisationalRulesForCurrentUser() + let service = OrganisationalRulesService() + _ = try awaitPromise(service.fetchOrganisationalRulesForCurrentUser()) } } @@ -131,7 +131,7 @@ struct AppStartup { } private func showErrorAlert(with error: Error, on window: UIWindow, session: SessionType?) { - let alert = UIAlertController(title: "Startup Error", message: "\(error)", preferredStyle: .alert) + let alert = UIAlertController(title: "Startup Error", message: "\(error.localizedDescription)", preferredStyle: .alert) let retry = UIAlertAction(title: "Retry", style: .default) { _ in self.initializeApp(window: window, session: session) } diff --git a/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift b/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift index d8df4c477..e802ece1b 100644 --- a/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift +++ b/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift @@ -12,8 +12,21 @@ protocol EnterpriseServerApiType { func getActiveFesUrl(for email: String) -> Promise func getActiveFesUrlForCurrentUser() -> Promise - func getClientConfiguration(for email: String) -> Promise - func getClientConfigurationForCurrentUser() -> Promise + func getClientConfiguration(for email: String) -> Promise + func getClientConfigurationForCurrentUser() -> Promise +} + +enum EnterpriseServerApiError: Error { + case parse + case emailFormat +} +extension EnterpriseServerApiError: LocalizedError { + var errorDescription: String? { + switch self { + case .parse: return "organisational_rules_parse_error_description".localized + case .emailFormat: return "organisational_rules_email_format_error_description".localized + } + } } class EnterpriseServerApi: EnterpriseServerApiType { @@ -71,11 +84,14 @@ class EnterpriseServerApi: EnterpriseServerApiType { .recoverFromTimeOut(result: nil) } - func getClientConfiguration(for email: String) -> Promise { - Promise { resolve, _ in - guard let userDomain = email.recipientDomain, - !Configuration.publicEmailProviderDomains.contains(userDomain) else { - resolve(nil) + func getClientConfiguration(for email: String) -> Promise { + Promise { resolve, reject in + guard let userDomain = email.recipientDomain else { + reject(EnterpriseServerApiError.emailFormat) + return + } + if Configuration.publicEmailProviderDomains.contains(userDomain) { + resolve(.empty) return } let request = URLRequest.urlRequest( @@ -93,17 +109,18 @@ class EnterpriseServerApi: EnterpriseServerApiType { from: safeReponse.data ))?.clientConfiguration else { - resolve(nil) + reject(EnterpriseServerApiError.parse) return } resolve(clientConfiguration) } } - func getClientConfigurationForCurrentUser() -> Promise { + func getClientConfigurationForCurrentUser() -> Promise { guard let email = DataService.shared.currentUser?.email else { - return Promise { resolve, _ in - resolve(nil) + return Promise { _, reject in + assertionFailure("User has to be set while getting client configuration") + reject(AppErr.user("currentUser == nil")) } } return getClientConfiguration(for: email) diff --git a/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesService.swift b/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesService.swift index 20c0956e8..3f1d15ee6 100644 --- a/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesService.swift +++ b/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesService.swift @@ -12,6 +12,8 @@ import Promises protocol OrganisationalRulesServiceType { func fetchOrganisationalRulesForCurrentUser() -> Promise func fetchOrganisationalRules(for email: String) -> Promise + + func getSavedOrganisationalRulesForCurrentUser() -> OrganisationalRules } final class OrganisationalRulesService { @@ -41,26 +43,36 @@ extension OrganisationalRulesService: OrganisationalRulesServiceType { } func fetchOrganisationalRules(for email: String) -> Promise { - Promise { [weak self] resolve, reject in + Promise { [weak self] resolve, _ in guard let self = self else { throw AppErr.nilSelf } - guard let clientConfigurationResponse = try awaitPromise( - self.enterpriseServerApi.getClientConfiguration(for: email) - ) else { - reject(OrganisationalRulesServiceError.parse) - return - } - guard let organisationalRules = OrganisationalRules( - clientConfiguration: clientConfigurationResponse, - email: email - ) else { - reject(OrganisationalRulesServiceError.emailFormat) - return - } + let clientConfigurationResponse = try awaitPromise( + self.enterpriseServerApi.getClientConfiguration(for: email) + ) + + let organisationalRules = OrganisationalRules( + clientConfiguration: clientConfigurationResponse + ) self.clientConfigurationProvider.save(clientConfiguration: clientConfigurationResponse) resolve(organisationalRules) } + .recover { [weak self] error -> OrganisationalRules in + guard let self = self else { throw AppErr.nilSelf } + guard let clientConfig = self.clientConfigurationProvider.fetch() else { + throw error + } + return OrganisationalRules(clientConfiguration: clientConfig) + } + } + + func getSavedOrganisationalRulesForCurrentUser() -> OrganisationalRules { + guard let configuration = self.clientConfigurationProvider.fetch() else { + assertionFailure("There should not be a user without OrganisationalRules") + return OrganisationalRules(clientConfiguration: .empty) + } + + return OrganisationalRules(clientConfiguration: configuration) } } diff --git a/FlowCrypt/Models/ClientConfiguration.swift b/FlowCrypt/Models/ClientConfiguration.swift index 46b6e49c5..83bacf2a6 100644 --- a/FlowCrypt/Models/ClientConfiguration.swift +++ b/FlowCrypt/Models/ClientConfiguration.swift @@ -20,6 +20,13 @@ enum ClientConfigurationFlag: String, Codable { case useLegacyAttesterSubmit = "USE_LEGACY_ATTESTER_SUBMIT" case defaultRememberPassphrase = "DEFAULT_REMEMBER_PASS_PHRASE" case hideArmorMeta = "HIDE_ARMOR_META" + case forbidStoringPassphrase = "FORBID_STORING_PASS_PHRASE" + + case unknown + + init(from decoder: Decoder) throws { + self = try ClientConfigurationFlag(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown + } } struct ClientConfiguration: Codable, Equatable { @@ -32,6 +39,20 @@ struct ClientConfiguration: Codable, Equatable { let enforceKeygenExpireMonths: Int? } +// MARK: - Empty model +extension ClientConfiguration { + static var empty: ClientConfiguration { + return ClientConfiguration( + flags: [], + customKeyserverUrl: nil, + keyManagerUrl: nil, + disallowAttesterSearchForDomains: nil, + enforceKeygenAlgo: nil, + enforceKeygenExpireMonths: nil + ) + } +} + // MARK: - Map from realm model extension ClientConfiguration { init?(_ object: ClientConfigurationObject?) { diff --git a/FlowCrypt/Models/OrganisationalRule.swift b/FlowCrypt/Models/OrganisationalRule.swift index 57b029675..fa8747da1 100644 --- a/FlowCrypt/Models/OrganisationalRule.swift +++ b/FlowCrypt/Models/OrganisationalRule.swift @@ -13,19 +13,8 @@ import Foundation class OrganisationalRules { private let clientConfiguration: ClientConfiguration - let domain: String - init(clientConfiguration: ClientConfiguration, domain: String) { - self.clientConfiguration = clientConfiguration - self.domain = domain - } - - init?(clientConfiguration: ClientConfiguration, email: String) { - guard let recipientDomain = email.recipientDomain else { - return nil - } - - self.domain = recipientDomain + init(clientConfiguration: ClientConfiguration) { self.clientConfiguration = clientConfiguration } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 657fea5a8..81c300f9f 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -199,3 +199,7 @@ // Backup select keys screen "backup_select_key_screen_title" = "Select keys"; "backup_select_key_screen_no_selection" = "Please select keys to backup"; + +// Organisational rules error +"organisational_rules_parse_error_description" = "Couldn't parse data while getting organisational rules"; +"organisational_rules_email_format_error_description" = "Wrong user email format"; diff --git a/FlowCryptAppTests/Models Parsing/ClientConfigurationTests.swift b/FlowCryptAppTests/Models Parsing/ClientConfigurationTests.swift index 986d4066f..aa8da13b0 100644 --- a/FlowCryptAppTests/Models Parsing/ClientConfigurationTests.swift +++ b/FlowCryptAppTests/Models Parsing/ClientConfigurationTests.swift @@ -72,4 +72,23 @@ class ClientConfigurationTests: XCTestCase { XCTAssert(model?.keyManagerUrl == nil) XCTAssert(model?.disallowAttesterSearchForDomains == nil) } + + func test_client_configuraion_with_unknown_flag_json_parse() { + let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)).path(forResource: "client_configuraion_with_unknown_flag", ofType: "json")!) + let data = try! Data(contentsOf: urlPath, options: .dataReadingMapped) + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let model = try? decoder.decode(ClientConfiguration.self, from: data) + + XCTAssert(model?.flags != nil) + XCTAssert(model?.customKeyserverUrl != nil) + XCTAssert(model?.keyManagerUrl != nil) + XCTAssert(model?.disallowAttesterSearchForDomains != nil) + XCTAssert(model?.enforceKeygenAlgo != nil) + XCTAssert(model?.enforceKeygenExpireMonths != nil) + + XCTAssert(model?.flags?.contains(.unknown) == true) + } } diff --git a/FlowCryptAppTests/Models Parsing/client_configuraion_with_unknown_flag.json b/FlowCryptAppTests/Models Parsing/client_configuraion_with_unknown_flag.json new file mode 100644 index 000000000..785fb2caa --- /dev/null +++ b/FlowCryptAppTests/Models Parsing/client_configuraion_with_unknown_flag.json @@ -0,0 +1,8 @@ +{ + "flags": ["NO_PRV_BACKUP", "NO_KEY_MANAGER_PUB_LOOKUP", "ANY_RANDOM_NEW_FLAG_THAT_IS_NOT_PRESENT_IN_FLAGS_ENUM"], + "custom_keyserver_url": "https://hello", + "key_manager_url": "https://there", + "disallow_attester_search_for_domains": [], + "enforce_keygen_algo": "curve25519", + "enforce_keygen_expire_months": 12 +}