diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 3692d5049..460ff4641 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -11,6 +11,11 @@ 04B472951ECE29F600B8266F /* MyMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B472921ECE29F600B8266F /* MyMenuViewController.swift */; }; 04B472961ECE29F600B8266F /* SideMenuNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B472931ECE29F600B8266F /* SideMenuNavigationController.swift */; }; 211392A5266511E6009202EC /* PubLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 211392A4266511E6009202EC /* PubLookup.swift */; }; + 21489B78267CB42400BDE4AC /* ClientConfigurationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21489B77267CB42400BDE4AC /* ClientConfigurationProvider.swift */; }; + 21489B7A267CB4DF00BDE4AC /* ClientConfigurationObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21489B79267CB4DF00BDE4AC /* ClientConfigurationObject.swift */; }; + 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 */; }; 21C7DEFC26669A3700C44800 /* CalendarExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C7DEFB26669A3700C44800 /* CalendarExtension.swift */; }; 21C7DEFE26669CE100C44800 /* DateFormattingExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56BD3723438C7000A7371A /* DateFormattingExtensions.swift */; }; 21C7DF0526697DA500C44800 /* PromiseKitExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C7DF0426697DA500C44800 /* PromiseKitExtension.swift */; }; @@ -112,10 +117,10 @@ 9F976507267E165D0058419D /* ZBase32EncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F836CB2652A38700B2448C /* ZBase32EncodingTests.swift */; }; 9F97650E267E16620058419D /* WKDURLsConstructorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F836D22652A46E00B2448C /* WKDURLsConstructorTests.swift */; }; 9F97653D267E17C90058419D /* LocalStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F003D9D25EA910B00EB38C0 /* LocalStorageTests.swift */; }; - 9F976556267E186D0058419D /* DomainRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21EA3B2226565B5D00691848 /* DomainRulesTests.swift */; }; - 9F976569267E18F30058419D /* domain_rules.json in Resources */ = {isa = PBXBuildFile; fileRef = 21EA3B2E26565B7400691848 /* domain_rules.json */; }; - 9F976576267E18F90058419D /* domain_rules_partly_empty.json in Resources */ = {isa = PBXBuildFile; fileRef = 21EA3B3C26565B9800691848 /* domain_rules_partly_empty.json */; }; - 9F97657D267E18FE0058419D /* domain_rules_empty.json in Resources */ = {isa = PBXBuildFile; fileRef = 21EA3B3526565B8100691848 /* domain_rules_empty.json */; }; + 9F976556267E186D0058419D /* ClientConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21EA3B2226565B5D00691848 /* ClientConfigurationTests.swift */; }; + 9F976569267E18F30058419D /* client_configuraion.json in Resources */ = {isa = PBXBuildFile; fileRef = 21EA3B2E26565B7400691848 /* client_configuraion.json */; }; + 9F976576267E18F90058419D /* client_configuraion_partly_empty.json in Resources */ = {isa = PBXBuildFile; fileRef = 21EA3B3C26565B9800691848 /* client_configuraion_partly_empty.json */; }; + 9F97657D267E18FE0058419D /* client_configuraion_empty.json in Resources */ = {isa = PBXBuildFile; fileRef = 21EA3B3526565B8100691848 /* client_configuraion_empty.json */; }; 9F976584267E194F0058419D /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DAD60722E4588800F2C4CD /* TestData.swift */; }; 9F976585267E194F0058419D /* FlowCryptCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DAD5FD22E4574B00F2C4CD /* FlowCryptCoreTests.swift */; }; 9F976592267E19880058419D /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DAD60722E4588800F2C4CD /* TestData.swift */; }; @@ -354,16 +359,21 @@ 113F04B20ECC35FC59A81A6C /* Pods-FlowCryptTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptTests.release.xcconfig"; path = "Target Support Files/Pods-FlowCryptTests/Pods-FlowCryptTests.release.xcconfig"; sourceTree = ""; }; 11C1375F41411882DC4C9431 /* Pods-FlowCryptUIApplication.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.release.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.release.xcconfig"; sourceTree = ""; }; 211392A4266511E6009202EC /* PubLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubLookup.swift; sourceTree = ""; }; + 21489B77267CB42400BDE4AC /* ClientConfigurationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConfigurationProvider.swift; sourceTree = ""; }; + 21489B79267CB4DF00BDE4AC /* ClientConfigurationObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConfigurationObject.swift; sourceTree = ""; }; + 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 = ""; }; 21C7DEFB26669A3700C44800 /* CalendarExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarExtension.swift; sourceTree = ""; }; 21C7DF0426697DA500C44800 /* PromiseKitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseKitExtension.swift; sourceTree = ""; }; 21C7DF08266C0D8F00C44800 /* EnterpriseServerApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterpriseServerApi.swift; sourceTree = ""; }; 21C7DF0A266C0E3600C44800 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 21CE25E52650070300ADFF4B /* WKDURLsConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKDURLsConstructor.swift; sourceTree = ""; }; 21EA3B15265647C400691848 /* OrganisationalRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganisationalRule.swift; sourceTree = ""; }; - 21EA3B2226565B5D00691848 /* DomainRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainRulesTests.swift; sourceTree = ""; }; - 21EA3B2E26565B7400691848 /* domain_rules.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = domain_rules.json; sourceTree = ""; }; - 21EA3B3526565B8100691848 /* domain_rules_empty.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = domain_rules_empty.json; sourceTree = ""; }; - 21EA3B3C26565B9800691848 /* domain_rules_partly_empty.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = domain_rules_partly_empty.json; sourceTree = ""; }; + 21EA3B2226565B5D00691848 /* ClientConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConfigurationTests.swift; sourceTree = ""; }; + 21EA3B2E26565B7400691848 /* client_configuraion.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = client_configuraion.json; sourceTree = ""; }; + 21EA3B3526565B8100691848 /* client_configuraion_empty.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = client_configuraion_empty.json; sourceTree = ""; }; + 21EA3B3C26565B9800691848 /* client_configuraion_partly_empty.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = client_configuraion_partly_empty.json; sourceTree = ""; }; 21EFF61E265A5C6700AB0B71 /* WKDURLsApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKDURLsApi.swift; sourceTree = ""; }; 21F836B52652A26B00B2448C /* DataExntensions+ZBase32Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataExntensions+ZBase32Encoding.swift"; sourceTree = ""; }; 21F836CB2652A38700B2448C /* ZBase32EncodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZBase32EncodingTests.swift; sourceTree = ""; }; @@ -730,6 +740,7 @@ 9F0C3C132316E69300299985 /* User.swift */, 9F9AAFFC2383E216000A00F1 /* Document.swift */, 21EA3B15265647C400691848 /* OrganisationalRule.swift */, + 21489B7B267CBA0E00BDE4AC /* ClientConfiguration.swift */, ); path = Models; sourceTree = ""; @@ -754,6 +765,16 @@ path = SideMenu; sourceTree = ""; }; + 21489B81267CC3BC00BDE4AC /* Organisational Rules Service */ = { + isa = PBXGroup; + children = ( + 21489B77267CB42400BDE4AC /* ClientConfigurationProvider.swift */, + 21489B7F267CC39E00BDE4AC /* OrganisationalRulesService.swift */, + 21489B82267CC99C00BDE4AC /* OrganisationalRulesServiceError.swift */, + ); + path = "Organisational Rules Service"; + sourceTree = ""; + }; 21CE25D32650034500ADFF4B /* WKDURLs */ = { isa = PBXGroup; children = ( @@ -766,10 +787,10 @@ 21EA3B2126565B4100691848 /* Models Parsing */ = { isa = PBXGroup; children = ( - 21EA3B2226565B5D00691848 /* DomainRulesTests.swift */, - 21EA3B2E26565B7400691848 /* domain_rules.json */, - 21EA3B3C26565B9800691848 /* domain_rules_partly_empty.json */, - 21EA3B3526565B8100691848 /* domain_rules_empty.json */, + 21EA3B2226565B5D00691848 /* ClientConfigurationTests.swift */, + 21EA3B2E26565B7400691848 /* client_configuraion.json */, + 21EA3B3C26565B9800691848 /* client_configuraion_partly_empty.json */, + 21EA3B3526565B8100691848 /* client_configuraion_empty.json */, ); path = "Models Parsing"; sourceTree = ""; @@ -935,14 +956,15 @@ 9F31AB9F232C071700CF87EA /* GlobalRouter.swift */, 32DCAC088C8BFFFAF08853AC /* AttesterApi.swift */, 32DCAC9C0512037018F434A1 /* BackendApi.swift */, - 21C7DF08266C0D8F00C44800 /* EnterpriseServerApi.swift */, 21EFF61E265A5C6700AB0B71 /* WKDURLsApi.swift */, + 21C7DF08266C0D8F00C44800 /* EnterpriseServerApi.swift */, D274724024F97C5C006BA6EF /* CacheService.swift */, C132B9CA1EC2DE6400763715 /* GeneralConstants.swift */, 9FB22CFD25715DDF0026EE64 /* Key Services */, 9F41FA1C25372C2D003B970D /* Backup Services */, D227C0E4250538190070F805 /* Folders Services */, D27B911724EFE787002DF0A1 /* Contacts Services */, + 21489B81267CC3BC00BDE4AC /* Organisational Rules Service */, ); path = Services; sourceTree = ""; @@ -956,6 +978,7 @@ D274724324FD1932006BA6EF /* FolderObject.swift */, 04B4728B1ECE29D200B8266F /* KeyInfo.swift */, D2F41370243CC76E0066AFB5 /* SessionObject.swift */, + 21489B79267CB4DF00BDE4AC /* ClientConfigurationObject.swift */, ); path = "Realm Models"; sourceTree = ""; @@ -1989,9 +2012,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9F976569267E18F30058419D /* domain_rules.json in Resources */, - 9F976576267E18F90058419D /* domain_rules_partly_empty.json in Resources */, - 9F97657D267E18FE0058419D /* domain_rules_empty.json in Resources */, + 9F976569267E18F30058419D /* client_configuraion.json in Resources */, + 9F976576267E18F90058419D /* client_configuraion_partly_empty.json in Resources */, + 9F97657D267E18FE0058419D /* client_configuraion_empty.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2316,7 +2339,7 @@ 9FC4116B2681186D004C0A69 /* KeyMethodsTest.swift in Sources */, 9F97653D267E17C90058419D /* LocalStorageTests.swift in Sources */, 9F9764F4267E15CC0058419D /* ExtensionTests.swift in Sources */, - 9F976556267E186D0058419D /* DomainRulesTests.swift in Sources */, + 9F976556267E186D0058419D /* ClientConfigurationTests.swift in Sources */, 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2350,6 +2373,7 @@ 9F4163E6266520B600106194 /* CommonNodesInputs.swift in Sources */, 04B472951ECE29F600B8266F /* MyMenuViewController.swift in Sources */, C132B9B41EC2DBD800763715 /* AppDelegate.swift in Sources */, + 21489B7C267CBA0E00BDE4AC /* ClientConfiguration.swift in Sources */, 9F976592267E19880058419D /* TestData.swift in Sources */, 04B472961ECE29F600B8266F /* SideMenuNavigationController.swift in Sources */, 5A39F441239EF17F001F4607 /* LegalViewControllersProvider.swift in Sources */, @@ -2380,6 +2404,7 @@ D952B71D1ED0CB2500E5C02B /* MessageViewController.swift in Sources */, 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */, 5ADEDCBC23A4329000EC495E /* PublicKeyDetailViewController.swift in Sources */, + 21489B80267CC39E00BDE4AC /* OrganisationalRulesService.swift in Sources */, D28655932423B4EE0066F52E /* MyMenuViewDecorator.swift in Sources */, 04B4728D1ECE29D200B8266F /* KeyInfo.swift in Sources */, 9F3EF32F23B172D300FA0CEF /* SearchViewController.swift in Sources */, @@ -2450,6 +2475,7 @@ 9F9362192573D10E0009912F /* Imap+Message.swift in Sources */, 32DCA9C61ABB3234649B374E /* CoreHost.swift in Sources */, 9F41FA28253B75F4003B970D /* BackupSelectKeyViewController.swift in Sources */, + 21489B83267CC99C00BDE4AC /* OrganisationalRulesServiceError.swift in Sources */, D2F6D1402435008500DB4065 /* SessionCredentialsProvider.swift in Sources */, 9F9ABC8723AC1EAA00D560E3 /* MessageContext.swift in Sources */, 9F0C3C102316DD5B00299985 /* GoogleUserService.swift in Sources */, @@ -2475,6 +2501,7 @@ 9F6EE1552597399D0059BA51 /* BackupProvider.swift in Sources */, 9FEED1D2230DAD1E00700F8E /* InboxViewModel.swift in Sources */, 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */, + 21489B78267CB42400BDE4AC /* ClientConfigurationProvider.swift in Sources */, 9FF0671C25520D9D00FCC9E6 /* MailProvider.swift in Sources */, 9FE743072347AA54005E2DBB /* MainNavigationController.swift in Sources */, 9F5C2A8B257E6C4900DE9B4B /* ImapError.swift in Sources */, @@ -2490,6 +2517,7 @@ D2E26F7024F266F300612AF1 /* ContactDetailViewController.swift in Sources */, 9FDF3656235A22DA00614596 /* AppReset.swift in Sources */, 5A39F43F239EE7D2001F4607 /* SegmentedViewController.swift in Sources */, + 21489B7A267CB4DF00BDE4AC /* ClientConfigurationObject.swift in Sources */, 5A5C234B23A042520015E705 /* WebViewController.swift in Sources */, 9F5C2A7E257E64D500DE9B4B /* MessageOperationsProvider.swift in Sources */, 9FC7EB76266EB67B00F3BF5D /* EncryptedStorageProtocols.swift in Sources */, diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index c1a0d7b2f..bdc5a6703 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -91,7 +91,6 @@ final class InboxViewController: ASDKViewController { setupUI() setupNavigationBar() fetchAndRenderEmails(nil) - checkFES() } override func viewWillAppear(_ animated: Bool) { @@ -218,7 +217,7 @@ extension InboxViewController { // insert new messages let indexesToInsert = messageContext.messages .enumerated() - .map { (index, _) -> Int in + .map { index, _ -> Int in let indexInTableView = index + count return indexInTableView } @@ -245,14 +244,6 @@ extension InboxViewController { showAlert(error: error, message: "message_failed_load".localized) } } - - private func checkFES() { - enterpriseServerApi.getActiveFesUrlForCurrentUser() - .then(on: .main) { [weak self] urlString in - guard let urlString = urlString else { return } - self?.showToast("FES at \(urlString) not supported on iOS yet") - } - } } // MARK: - Action handlers diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index 564b747e1..132d8e4cd 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -37,7 +37,7 @@ final class EncryptedStorage: EncryptedStorageType { var version: SchemaVersion { switch self { case .initial: - return SchemaVersion(appVersion: "0.2.0", dbSchemaVersion: 1) + return SchemaVersion(appVersion: "0.2.0", dbSchemaVersion: 4) } } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index c1b5d6970..ff4d50434 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -79,7 +79,7 @@ final class MessageService { isEmail: true ) - let isWrongPassPhraseError = decrypted.blocks.first(where: { (block) -> Bool in + let isWrongPassPhraseError = decrypted.blocks.first(where: { block -> Bool in guard let errorBlock = block.decryptErr, case .needPassphrase = errorBlock.error.type else { return false } diff --git a/FlowCrypt/Functionality/Services/AppStartup.swift b/FlowCrypt/Functionality/Services/AppStartup.swift index a433a3ba3..458d0e22c 100644 --- a/FlowCrypt/Functionality/Services/AppStartup.swift +++ b/FlowCrypt/Functionality/Services/AppStartup.swift @@ -22,6 +22,8 @@ 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() }.then(on: .main) { self.chooseView(for: window, session: session) }.catch(on: .main) { error in @@ -85,6 +87,12 @@ struct AppStartup { } } + private func getUserOrgRulesIfNeeded() { + if DataService.shared.isLoggedIn { + _ = OrganisationalRulesService().fetchOrganisationalRulesForCurrentUser() + } + } + private func makeUserIdForSetup(session: SessionType) -> UserId? { guard let currentUser = DataService.shared.currentUser else { return nil diff --git a/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift b/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift index 861660669..d8df4c477 100644 --- a/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift +++ b/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift @@ -11,6 +11,9 @@ import Promises protocol EnterpriseServerApiType { func getActiveFesUrl(for email: String) -> Promise func getActiveFesUrlForCurrentUser() -> Promise + + func getClientConfiguration(for email: String) -> Promise + func getClientConfigurationForCurrentUser() -> Promise } class EnterpriseServerApi: EnterpriseServerApiType { @@ -22,6 +25,14 @@ class EnterpriseServerApi: EnterpriseServerApiType { static let serviceNeededValue = "enterprise-server" } + private struct ClientConfigurationContainer: Codable { + let clientConfiguration: ClientConfiguration + + private enum CodingKeys: String, CodingKey { + case clientConfiguration = "clientConfiguration" + } + } + func getActiveFesUrlForCurrentUser() -> Promise { guard let email = DataService.shared.currentUser?.email else { return Promise { resolve, _ in @@ -59,4 +70,42 @@ class EnterpriseServerApi: EnterpriseServerApiType { .timeout(Constants.getActiveFesTimeout) .recoverFromTimeOut(result: nil) } + + func getClientConfiguration(for email: String) -> Promise { + Promise { resolve, _ in + guard let userDomain = email.recipientDomain, + !Configuration.publicEmailProviderDomains.contains(userDomain) else { + resolve(nil) + return + } + let request = URLRequest.urlRequest( + with: "https://fes.\(userDomain)/api/v1/client-configuration?domain=\(userDomain)", + method: .get, + body: nil + ) + let response = try? awaitPromise(URLSession.shared.call(request)) + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + guard let safeReponse = response, + let clientConfiguration = (try? decoder.decode( + ClientConfigurationContainer.self, + from: safeReponse.data + ))?.clientConfiguration + else { + resolve(nil) + return + } + resolve(clientConfiguration) + } + } + + func getClientConfigurationForCurrentUser() -> Promise { + guard let email = DataService.shared.currentUser?.email else { + return Promise { resolve, _ in + resolve(nil) + } + } + return getClientConfiguration(for: email) + } } diff --git a/FlowCrypt/Functionality/Services/Organisational Rules Service/ClientConfigurationProvider.swift b/FlowCrypt/Functionality/Services/Organisational Rules Service/ClientConfigurationProvider.swift new file mode 100644 index 000000000..9f55d6942 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Organisational Rules Service/ClientConfigurationProvider.swift @@ -0,0 +1,41 @@ +// +// ClientConfigurationProvider.swift +// FlowCrypt +// +// Created by Yevhen Kyivskyi on 18.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Promises +import RealmSwift + +protocol ClientConfigurationProviderType { + func fetch() -> ClientConfiguration? + func removeClientConfiguration() + func save(clientConfiguration: ClientConfiguration) +} + +struct ClientConfigurationProvider: CacheServiceType { + let storage: CacheStorage + let clientConfigurationCache: CacheService + + init(storage: @escaping @autoclosure CacheStorage) { + self.storage = storage + self.clientConfigurationCache = CacheService(storage: storage()) + } +} + +extension ClientConfigurationProvider: ClientConfigurationProviderType { + func fetch() -> ClientConfiguration? { + ClientConfiguration(clientConfigurationCache.getAllForActiveUser()?.first) + } + + func removeClientConfiguration() { + clientConfigurationCache.removeAllForActiveUser() + } + + func save(clientConfiguration: ClientConfiguration) { + clientConfigurationCache.save(ClientConfigurationObject(clientConfiguration, user: EncryptedStorage().activeUser)) + } +} diff --git a/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesService.swift b/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesService.swift new file mode 100644 index 000000000..20c0956e8 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesService.swift @@ -0,0 +1,66 @@ +// +// OrganisationalRulesService.swift +// FlowCrypt +// +// Created by Yevhen Kyivskyi on 18.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Promises + +protocol OrganisationalRulesServiceType { + func fetchOrganisationalRulesForCurrentUser() -> Promise + func fetchOrganisationalRules(for email: String) -> Promise +} + +final class OrganisationalRulesService { + + private let enterpriseServerApi: EnterpriseServerApiType + private let clientConfigurationProvider: ClientConfigurationProviderType + + init( + storage: @escaping @autoclosure CacheStorage = DataService.shared.storage, + enterpriseServerApi: EnterpriseServerApiType = EnterpriseServerApi() + ) { + self.enterpriseServerApi = enterpriseServerApi + self.clientConfigurationProvider = ClientConfigurationProvider(storage: storage()) + } +} + +// MARK: - OrganisationalRulesServiceType +extension OrganisationalRulesService: OrganisationalRulesServiceType { + + func fetchOrganisationalRulesForCurrentUser() -> Promise { + guard let currentUser = DataService.shared.currentUser else { + return Promise { _, reject in + reject(OrganisationalRulesServiceError.noCurrentUser) + } + } + return fetchOrganisationalRules(for: currentUser.email) + } + + func fetchOrganisationalRules(for email: String) -> Promise { + Promise { [weak self] resolve, reject 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 + } + + self.clientConfigurationProvider.save(clientConfiguration: clientConfigurationResponse) + + resolve(organisationalRules) + } + } +} diff --git a/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesServiceError.swift b/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesServiceError.swift new file mode 100644 index 000000000..43ef64888 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Organisational Rules Service/OrganisationalRulesServiceError.swift @@ -0,0 +1,15 @@ +// +// OrganisationalRulesServiceError.swift +// FlowCrypt +// +// Created by Yevhen Kyivskyi on 18.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation + +enum OrganisationalRulesServiceError: Error { + case noCurrentUser + case parse + case emailFormat +} diff --git a/FlowCrypt/Models/ClientConfiguration.swift b/FlowCrypt/Models/ClientConfiguration.swift new file mode 100644 index 000000000..46b6e49c5 --- /dev/null +++ b/FlowCrypt/Models/ClientConfiguration.swift @@ -0,0 +1,62 @@ +// +// ClientConfiguration.swift +// FlowCrypt +// +// Created by Yevhen Kyivskyi on 18.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation + +enum ClientConfigurationFlag: String, Codable { + + case noPrivateKeyBackup = "NO_PRV_BACKUP" + case noPrivateKeyCreate = "NO_PRV_CREATE" + case noKeyManagerPubLookup = "NO_KEY_MANAGER_PUB_LOOKUP" + case privateKeyAutoimportOrAutogen = "PRV_AUTOIMPORT_OR_AUTOGEN" + case passphraseQuietAutogen = "PASS_PHRASE_QUIET_AUTOGEN" + case enforceAttesterSubmit = "ENFORCE_ATTESTER_SUBMIT" + case noAttesterSubmit = "NO_ATTESTER_SUBMIT" + case useLegacyAttesterSubmit = "USE_LEGACY_ATTESTER_SUBMIT" + case defaultRememberPassphrase = "DEFAULT_REMEMBER_PASS_PHRASE" + case hideArmorMeta = "HIDE_ARMOR_META" +} + +struct ClientConfiguration: Codable, Equatable { + + let flags: [ClientConfigurationFlag]? + let customKeyserverUrl: String? + let keyManagerUrl: String? + let disallowAttesterSearchForDomains: [String]? + let enforceKeygenAlgo: String? + let enforceKeygenExpireMonths: Int? +} + +// MARK: - Map from realm model +extension ClientConfiguration { + init?(_ object: ClientConfigurationObject?) { + guard let unwrappedObject = object else { + return nil + } + + var decodedFlags: [String]? + if let flagsData = object?.flags { + decodedFlags = try? JSONDecoder().decode([String].self, from: flagsData) + } + + var decodedDisallowAttesterSearchForDomains: [String]? + if let disallowAttesterSearchForDomainsData = object?.disallowAttesterSearchForDomains { + decodedDisallowAttesterSearchForDomains = try? JSONDecoder() + .decode([String].self, from: disallowAttesterSearchForDomainsData) + } + + self.init( + flags: decodedFlags?.compactMap(ClientConfigurationFlag.init), + customKeyserverUrl: unwrappedObject.customKeyserverUrl, + keyManagerUrl: unwrappedObject.keyManagerUrl, + disallowAttesterSearchForDomains: decodedDisallowAttesterSearchForDomains, + enforceKeygenAlgo: unwrappedObject.enforceKeygenAlgo, + enforceKeygenExpireMonths: unwrappedObject.enforceKeygenExpireMonths + ) + } +} diff --git a/FlowCrypt/Models/OrganisationalRule.swift b/FlowCrypt/Models/OrganisationalRule.swift index 7c009be16..57b029675 100644 --- a/FlowCrypt/Models/OrganisationalRule.swift +++ b/FlowCrypt/Models/OrganisationalRule.swift @@ -8,114 +8,90 @@ import Foundation -enum DomainRulesFlag: String, Codable { - - case noPRVBackup = "NO_PRV_BACKUP" - case noPRVCreate = "NO_PRV_CREATE" - case noKeyManagerPubLookup = "NO_KEY_MANAGER_PUB_LOOKUP" - case PRVAutoimportOrAutogen = "PRV_AUTOIMPORT_OR_AUTOGEN" - case passphraseQuietAutogen = "PASS_PHRASE_QUIET_AUTOGEN" - case enforceAttesterSubmit = "ENFORCE_ATTESTER_SUBMIT" - case noAttesterSubmit = "NO_ATTESTER_SUBMIT" - case useLegacyAttesterSubmit = "USE_LEGACY_ATTESTER_SUBMIT" - case defaultRememberPassphrase = "DEFAULT_REMEMBER_PASS_PHRASE" - case hideArmorMeta = "HIDE_ARMOR_META" -} - -struct DomainRules: Codable, Equatable { - - let flags: [DomainRulesFlag]? - let customKeyserverUrl: String? - let keyManagerUrl: String? - let disallowAttesterSearchForDomains: [String]? - let enforceKeygenAlgo: String? - let enforceKeygenExpireMonths: Int? -} - /// Organisational rules, set domain-wide, and delivered from FlowCrypt Backend /// These either enforce, alter or forbid various behavior to fit customer needs class OrganisationalRules { - private let domainRules: DomainRules + private let clientConfiguration: ClientConfiguration let domain: String - init(domainRules: DomainRules, domain: String) { - self.domainRules = domainRules + init(clientConfiguration: ClientConfiguration, domain: String) { + self.clientConfiguration = clientConfiguration self.domain = domain } - init?(domainRules: DomainRules, email: String) { + init?(clientConfiguration: ClientConfiguration, email: String) { guard let recipientDomain = email.recipientDomain else { return nil } self.domain = recipientDomain - self.domainRules = domainRules + self.clientConfiguration = clientConfiguration } /// Internal company SKS-like public key server to trust above Attester var customSksPubkeyServer: String? { - domainRules.customKeyserverUrl + clientConfiguration.customKeyserverUrl } /// an internal org FlowCrypt Email Key Manager instance, can manage both public and private keys /// use this method when using for PRV sync var keyManagerUrlForPrivateKeys: String? { - domainRules.keyManagerUrl + clientConfiguration.keyManagerUrl } /// an internal org FlowCrypt Email Key Manager instance, can manage both public and private keys /// use this method when using for PUB sync var keyManagerUrlForPublicKeys: String? { - (domainRules.flags ?? []).contains(.noKeyManagerPubLookup) + (clientConfiguration.flags ?? []).contains(.noKeyManagerPubLookup) ? nil - : domainRules.keyManagerUrl + : clientConfiguration.keyManagerUrl } /// use when finding out if EKM is in use, to change functionality without actually neededing the EKM var isUsingKeyManager: Bool { - domainRules.keyManagerUrl != nil + clientConfiguration.keyManagerUrl != nil } /// Enforce a key algo for keygen, eg rsa2048,rsa4096,curve25519 var enforcedKeygenAlgo: String? { - domainRules.enforceKeygenAlgo + clientConfiguration.enforceKeygenAlgo } /// Some orgs want to have newly generated keys include self-signatures that expire some time in the future. var getEnforcedKeygenExpirationMonths: Int? { - domainRules.enforceKeygenExpireMonths + clientConfiguration.enforceKeygenExpireMonths } /// Some orgs expect 100% of their private keys to be imported from elsewhere (and forbid keygen in the extension) var canCreateKeys: Bool { - !(domainRules.flags ?? []).contains(.noPRVCreate) + !(clientConfiguration.flags ?? []).contains(.noPrivateKeyCreate) } /// Some orgs want to forbid backing up of public keys (such as inbox or other methods) var canBackupKeys: Bool { - !(domainRules.flags ?? []).contains(.noPRVBackup) + !(clientConfiguration.flags ?? []).contains(.noPrivateKeyBackup) } /// (normally, during setup, if a public key is submitted to Attester and there is /// a conflicting key already submitted, the issue will be skipped) /// Some orgs want to make sure that their public key gets submitted to attester and conflict errors are NOT ignored: var mustSubmitAttester: Bool { - (domainRules.flags ?? []).contains(.enforceAttesterSubmit) + (clientConfiguration.flags ?? []).contains(.enforceAttesterSubmit) } /// Normally, during setup, "remember pass phrase" is unchecked /// This option will cause "remember pass phrase" option to be checked by default /// This behavior is also enabled as a byproduct of PASS_PHRASE_QUIET_AUTOGEN var shouldRememberPassphraseByDefault: Bool { - (domainRules.flags ?? []).contains(.defaultRememberPassphrase) || mustAutogenPassPhraseQuietly + (clientConfiguration.flags ?? []).contains(.defaultRememberPassphrase) || mustAutogenPassPhraseQuietly } /// This is to be used for customers who run their own FlowCrypt Email Key Manager /// If a key can be found on FEKM, it will be auto imported /// If not, it will be autogenerated and stored there var mustAutoImportOrAutogenPrvWithKeyManager: Bool { - if !(domainRules.flags ?? []).contains(.PRVAutoimportOrAutogen) { + if !(clientConfiguration.flags ?? []).contains(.privateKeyAutoimportOrAutogen) { return false } @@ -131,29 +107,29 @@ class OrganisationalRules { /// The pass phrase will NOT be displayed to user, and it will never be asked of the user /// This creates the smoothest user experience, for organisations that use full-disk-encryption and don't need pass phrase protection var mustAutogenPassPhraseQuietly: Bool { - (domainRules.flags ?? []).contains(.passphraseQuietAutogen) + (clientConfiguration.flags ?? []).contains(.passphraseQuietAutogen) } /// Some orgs prefer to forbid publishing public keys publicly var canSubmitPubToAttester: Bool { - !(domainRules.flags ?? []).contains(.noAttesterSubmit) + !(clientConfiguration.flags ?? []).contains(.noAttesterSubmit) } /// Some orgs have a list of email domains where they do NOT want such emails to be looked up on public sources (such as Attester) /// This is because they already have other means to obtain public keys for these domains, such as from their own internal keyserver func canLookupThisRecipientOnAttester(recipient email: String) -> Bool { - !(domainRules.disallowAttesterSearchForDomains ?? []).contains(email.recipientDomain ?? "") + !(clientConfiguration.disallowAttesterSearchForDomains ?? []).contains(email.recipientDomain ?? "") } /// Some orgs use flows that are only implemented in POST /initial/legacy_submit and not in POST /pub/email@corp.co: /// -> enforcing that submitted keys match customer key server /// Until the newer endpoint is ready, this flag will point users in those orgs to the original endpoint var useLegacyAttesterSubmit: Bool { - (domainRules.flags ?? []).contains(.useLegacyAttesterSubmit) + (clientConfiguration.flags ?? []).contains(.useLegacyAttesterSubmit) } /// With this option, sent messages won't have any comment/version in armor, imported keys get imported without armor var shouldHideArmorMeta: Bool { - (domainRules.flags ?? []).contains(.hideArmorMeta) + (clientConfiguration.flags ?? []).contains(.hideArmorMeta) } } diff --git a/FlowCrypt/Models/Realm Models/ClientConfigurationObject.swift b/FlowCrypt/Models/Realm Models/ClientConfigurationObject.swift new file mode 100644 index 000000000..08d830006 --- /dev/null +++ b/FlowCrypt/Models/Realm Models/ClientConfigurationObject.swift @@ -0,0 +1,71 @@ +// +// ClientConfigurationObject.swift +// FlowCrypt +// +// Created by Yevhen Kyivskyi on 18.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import RealmSwift + +final class ClientConfigurationObject: Object { + + @objc dynamic var flags: Data? + @objc dynamic var customKeyserverUrl: String? + @objc dynamic var keyManagerUrl: String? + @objc dynamic var disallowAttesterSearchForDomains: Data? + @objc dynamic var enforceKeygenAlgo: String? + @objc dynamic var enforceKeygenExpireMonths: Int = -1 + @objc dynamic var user: UserObject? + @objc dynamic var userEmail: String? + + convenience init( + flags: [String]?, + customKeyserverUrl: String?, + keyManagerUrl: String?, + disallowAttesterSearchForDomains: [String]?, + enforceKeygenAlgo: String?, + enforceKeygenExpireMonths: Int?, + user: UserObject? + ) { + self.init() + if let flags = flags { + self.flags = try? JSONEncoder().encode(flags) + } + self.customKeyserverUrl = customKeyserverUrl + self.keyManagerUrl = keyManagerUrl + if let disallowAttesterSearchForDomains = disallowAttesterSearchForDomains { + self.disallowAttesterSearchForDomains = try? JSONEncoder().encode(disallowAttesterSearchForDomains) + } + self.enforceKeygenAlgo = enforceKeygenAlgo + self.enforceKeygenExpireMonths = enforceKeygenExpireMonths ?? -1 + self.user = user + self.userEmail = user?.email + } + + convenience init( + _ clientConfiguration: ClientConfiguration, + user: UserObject? + ) { + self.init( + flags: clientConfiguration.flags?.map(\.rawValue), + customKeyserverUrl: clientConfiguration.customKeyserverUrl, + keyManagerUrl: clientConfiguration.keyManagerUrl, + disallowAttesterSearchForDomains: clientConfiguration.disallowAttesterSearchForDomains, + enforceKeygenAlgo: clientConfiguration.enforceKeygenAlgo, + enforceKeygenExpireMonths: clientConfiguration.enforceKeygenExpireMonths, + user: user + ) + } + + override class func primaryKey() -> String? { + "userEmail" + } +} + +extension ClientConfigurationObject: CachedObject { + var identifier: String { userEmail ?? "" } + + var activeUser: UserObject? { user } +} diff --git a/FlowCryptAppTests/Models Parsing/DomainRulesTests.swift b/FlowCryptAppTests/Models Parsing/ClientConfigurationTests.swift similarity index 76% rename from FlowCryptAppTests/Models Parsing/DomainRulesTests.swift rename to FlowCryptAppTests/Models Parsing/ClientConfigurationTests.swift index 898edad9b..986d4066f 100644 --- a/FlowCryptAppTests/Models Parsing/DomainRulesTests.swift +++ b/FlowCryptAppTests/Models Parsing/ClientConfigurationTests.swift @@ -1,5 +1,5 @@ // -// DomainRulesTests.swift +// ClientConfigurationTests.swift // FlowCryptTests // // Created by Yevhen Kyivskyi on 20.05.2021. @@ -9,7 +9,7 @@ import XCTest @testable import FlowCrypt -class DomainRulesTests: XCTestCase { +class ClientConfigurationTests: XCTestCase { override func setUp() { // Put setup code here. This method is called before the invocation of each test method in the class. @@ -19,14 +19,14 @@ class DomainRulesTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func test_complete_domain_rules_json_parse() { - let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)).path(forResource: "domain_rules", ofType: "json")!) + func test_complete_client_configuraion_json_parse() { + let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)).path(forResource: "client_configuraion", ofType: "json")!) let data = try! Data(contentsOf: urlPath, options: .dataReadingMapped) let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let model = try? decoder.decode(DomainRules.self, from: data) + let model = try? decoder.decode(ClientConfiguration.self, from: data) XCTAssert(model?.flags != nil) XCTAssert(model?.customKeyserverUrl != nil) @@ -36,14 +36,14 @@ class DomainRulesTests: XCTestCase { XCTAssert(model?.enforceKeygenExpireMonths != nil) } - func test_partial_domain_rules_json_parse() { - let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)).path(forResource: "domain_rules_partly_empty", ofType: "json")!) + func test_partial_client_configuraion_json_parse() { + let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)).path(forResource: "client_configuraion_partly_empty", ofType: "json")!) let data = try! Data(contentsOf: urlPath, options: .dataReadingMapped) let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let model = try? decoder.decode(DomainRules.self, from: data) + let model = try? decoder.decode(ClientConfiguration.self, from: data) XCTAssert(model?.flags != nil) XCTAssert(model?.enforceKeygenAlgo != nil) @@ -54,14 +54,14 @@ class DomainRulesTests: XCTestCase { XCTAssert(model?.disallowAttesterSearchForDomains == nil) } - func test_empty_domain_rules_json_parse() { - let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)).path(forResource: "domain_rules_empty", ofType: "json")!) + func test_empty_client_configuraion_json_parse() { + let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)).path(forResource: "client_configuraion_empty", ofType: "json")!) let data = try! Data(contentsOf: urlPath, options: .dataReadingMapped) let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let model = try? decoder.decode(DomainRules.self, from: data) + let model = try? decoder.decode(ClientConfiguration.self, from: data) XCTAssert(model != nil) diff --git a/FlowCryptAppTests/Models Parsing/domain_rules.json b/FlowCryptAppTests/Models Parsing/client_configuraion.json similarity index 100% rename from FlowCryptAppTests/Models Parsing/domain_rules.json rename to FlowCryptAppTests/Models Parsing/client_configuraion.json diff --git a/FlowCryptAppTests/Models Parsing/domain_rules_empty.json b/FlowCryptAppTests/Models Parsing/client_configuraion_empty.json similarity index 100% rename from FlowCryptAppTests/Models Parsing/domain_rules_empty.json rename to FlowCryptAppTests/Models Parsing/client_configuraion_empty.json diff --git a/FlowCryptAppTests/Models Parsing/domain_rules_partly_empty.json b/FlowCryptAppTests/Models Parsing/client_configuraion_partly_empty.json similarity index 100% rename from FlowCryptAppTests/Models Parsing/domain_rules_partly_empty.json rename to FlowCryptAppTests/Models Parsing/client_configuraion_partly_empty.json