diff --git a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLData.swift b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLData.swift index df01a888af..f9f1372ac3 100644 --- a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLData.swift +++ b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLData.swift @@ -124,10 +124,14 @@ public extension EnvironmentURLData { /// The base url for send sharing. var sendShareURL: URL? { - guard region != .unitedStates else { - return URL(string: "https://send.bitwarden.com/#")! + switch region { + case .unitedStates: + URL(string: "https://send.bitwarden.com/#")! + case .gov: + URL(string: "https://send.bitwarden-gov.com/#")! + case .europe, .internal, .selfHosted: + subpageURL(additionalPath: "send") } - return subpageURL(additionalPath: "send") } /// The base url for the settings screen. @@ -196,4 +200,15 @@ public extension EnvironmentURLData { notifications: URL(string: "https://notifications.bitwarden.eu")!, webVault: URL(string: "https://vault.bitwarden.eu")!, ) + + /// The default URLs for the US government cloud (FedRAMP) region. + static let defaultGov = EnvironmentURLData( + api: URL(string: "https://api.bitwarden-gov.com")!, + base: URL(string: "https://vault.bitwarden-gov.com")!, + events: URL(string: "https://events.bitwarden-gov.com")!, + icons: URL(string: "https://icons.bitwarden-gov.com")!, + identity: URL(string: "https://identity.bitwarden-gov.com")!, + notifications: URL(string: "https://notifications.bitwarden-gov.com")!, + webVault: URL(string: "https://vault.bitwarden-gov.com")!, + ) } diff --git a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLDataTests.swift b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLDataTests.swift index acb916655f..e433e6a5c5 100644 --- a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLDataTests.swift +++ b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLDataTests.swift @@ -60,6 +60,23 @@ class EnvironmentURLDataTests: XCTestCase { ) } + /// `defaultGov` returns the properly configured `EnvironmentURLData` + /// with the default Urls for the government cloud (FedRAMP) region. + func test_defaultGov() { + XCTAssertEqual( + EnvironmentURLData.defaultGov, + EnvironmentURLData( + api: URL(string: "https://api.bitwarden-gov.com")!, + base: URL(string: "https://vault.bitwarden-gov.com")!, + events: URL(string: "https://events.bitwarden-gov.com")!, + icons: URL(string: "https://icons.bitwarden-gov.com")!, + identity: URL(string: "https://identity.bitwarden-gov.com")!, + notifications: URL(string: "https://notifications.bitwarden-gov.com")!, + webVault: URL(string: "https://vault.bitwarden-gov.com")!, + ), + ) + } + /// `importItemsURL` returns the import items URL for the base URL. func test_importItemsURL_baseURL() { let subject = EnvironmentURLData(base: URL(string: "https://vault.example.com")) @@ -130,6 +147,12 @@ class EnvironmentURLDataTests: XCTestCase { XCTAssertTrue(subject.region == .europe) } + /// `region` returns `.gov` if base URL is the same as the default for the government cloud (FedRAMP) region. + func test_region_gov() { + let subject = EnvironmentURLData(base: URL(string: "https://vault.bitwarden-gov.com")!) + XCTAssertTrue(subject.region == .gov) + } + /// `region` returns `.selfHosted` if base URL is neither the default for US nor for EU. func test_region_selfHost() { let subject = EnvironmentURLData(base: URL(string: "https://example.com")!) @@ -148,6 +171,12 @@ class EnvironmentURLDataTests: XCTestCase { XCTAssertEqual(subject.sendShareURL?.absoluteString, "https://vault.bitwarden.eu/#/send") } + /// `sendShareURL` returns the send URL for the government cloud (FedRAMP) region. + func test_sendShareURL_gov() { + let subject = EnvironmentURLData.defaultGov + XCTAssertEqual(subject.sendShareURL?.absoluteString, "https://send.bitwarden-gov.com/#") + } + /// `sendShareURL` returns the send URL for the base URL. func test_sendShareURL_baseURL() { let subject = EnvironmentURLData(base: URL(string: "https://vault.example.com")) diff --git a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLs.swift b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLs.swift index 892aa37db2..c05653b2ec 100644 --- a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLs.swift +++ b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLs.swift @@ -129,6 +129,7 @@ public extension EnvironmentURLs { let environmentURLData: EnvironmentURLData = switch environmentURLData.region { case .europe: .defaultEU case .unitedStates: .defaultUS + case .gov: .defaultGov case .internal, .selfHosted: environmentURLData } diff --git a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLsTests+GovTests.swift b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLsTests+GovTests.swift new file mode 100644 index 0000000000..cea4428ad9 --- /dev/null +++ b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLsTests+GovTests.swift @@ -0,0 +1,75 @@ +import XCTest + +@testable import BitwardenKit + +extension EnvironmentURLsTests { + // MARK: Tests + + /// `init(environmentURLData:)` sets the URLs from the passed data when such data is the default + /// government cloud (FedRAMP) region. + func test_init_environmentURLData_defaultGov() { + let subject = EnvironmentURLs( + environmentURLData: EnvironmentURLData.defaultGov, + ) + XCTAssertEqual( + subject, + EnvironmentURLs( + apiURL: URL(string: "https://api.bitwarden-gov.com")!, + baseURL: URL(string: "https://vault.bitwarden-gov.com")!, + changeEmailURL: URL(string: "https://vault.bitwarden-gov.com/#/settings/account")!, + eventsURL: URL(string: "https://events.bitwarden-gov.com")!, + fillAssistRulesURL: URL(string: "https://github.com/bitwarden/map-the-web/releases/latest/download")!, + iconsURL: URL(string: "https://icons.bitwarden-gov.com")!, + identityURL: URL(string: "https://identity.bitwarden-gov.com")!, + importItemsURL: URL(string: "https://vault.bitwarden-gov.com/#/tools/import")!, + manageSubscriptionURL: URL(string: "https://vault.bitwarden-gov.com/#/settings/subscription")!, + proxyCookieRedirectConnectorURL: URL( + string: "https://vault.bitwarden-gov.com/proxy-cookie-redirect-connector.html", + )!, + recoveryCodeURL: URL(string: "https://vault.bitwarden-gov.com/#/recover-2fa")!, + sendShareURL: URL(string: "https://send.bitwarden-gov.com/#")!, + settingsURL: URL(string: "https://vault.bitwarden-gov.com/#/settings")!, + setUpTwoFactorURL: URL(string: "https://vault.bitwarden-gov.com/#/settings/security/two-factor")!, + upgradeToPremiumURL: URL( + // swiftlint:disable:next line_length + string: "https://vault.bitwarden-gov.com/#/settings/subscription/premium?callToAction=upgradeToPremium", + )!, + webVaultURL: URL(string: "https://vault.bitwarden-gov.com")!, + ), + ) + } + + /// `init(environmentURLData:)` defaults to the pre-defined government cloud (FedRAMP) URLs if the + /// base URL matches the gov environment. + func test_init_environmentURLData_baseURL_gov() { + let subject = EnvironmentURLs( + environmentURLData: EnvironmentURLData(base: URL(string: "https://vault.bitwarden-gov.com")!), + ) + XCTAssertEqual( + subject, + EnvironmentURLs( + apiURL: URL(string: "https://api.bitwarden-gov.com")!, + baseURL: URL(string: "https://vault.bitwarden-gov.com")!, + changeEmailURL: URL(string: "https://vault.bitwarden-gov.com/#/settings/account")!, + eventsURL: URL(string: "https://events.bitwarden-gov.com")!, + fillAssistRulesURL: URL(string: "https://github.com/bitwarden/map-the-web/releases/latest/download")!, + iconsURL: URL(string: "https://icons.bitwarden-gov.com")!, + identityURL: URL(string: "https://identity.bitwarden-gov.com")!, + importItemsURL: URL(string: "https://vault.bitwarden-gov.com/#/tools/import")!, + manageSubscriptionURL: URL(string: "https://vault.bitwarden-gov.com/#/settings/subscription")!, + proxyCookieRedirectConnectorURL: URL( + string: "https://vault.bitwarden-gov.com/proxy-cookie-redirect-connector.html", + )!, + recoveryCodeURL: URL(string: "https://vault.bitwarden-gov.com/#/recover-2fa")!, + sendShareURL: URL(string: "https://send.bitwarden-gov.com/#")!, + settingsURL: URL(string: "https://vault.bitwarden-gov.com/#/settings")!, + setUpTwoFactorURL: URL(string: "https://vault.bitwarden-gov.com/#/settings/security/two-factor")!, + upgradeToPremiumURL: URL( + // swiftlint:disable:next line_length + string: "https://vault.bitwarden-gov.com/#/settings/subscription/premium?callToAction=upgradeToPremium", + )!, + webVaultURL: URL(string: "https://vault.bitwarden-gov.com")!, + ), + ) + } +} diff --git a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLsTests.swift b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLsTests.swift index fcdf734efa..3ef651e5a0 100644 --- a/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLsTests.swift +++ b/BitwardenKit/Core/Platform/Models/Domain/EnvironmentURLsTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import BitwardenKit -class EnvironmentURLsTests: BitwardenTestCase { +class EnvironmentURLsTests: BitwardenTestCase { // swiftlint:disable:this type_body_length // MARK: Tests /// `init(environmentURLData:)` sets the URLs from the passed data when such data is the default US. diff --git a/BitwardenKit/Core/Platform/Models/Domain/ServerConfig.swift b/BitwardenKit/Core/Platform/Models/Domain/ServerConfig.swift index 8306b46dcd..8625c9aaf7 100644 --- a/BitwardenKit/Core/Platform/Models/Domain/ServerConfig.swift +++ b/BitwardenKit/Core/Platform/Models/Domain/ServerConfig.swift @@ -26,6 +26,9 @@ public struct ServerConfig: Equatable, Codable, Sendable { /// Third party server information. public let server: ThirdPartyServerConfig? + /// Server-level settings. + public let settings: ServerSettings? + /// The version of the server. public let version: String @@ -36,6 +39,7 @@ public struct ServerConfig: Equatable, Codable, Sendable { featureStates = responseModel.featureStates ?? [:] gitHash = responseModel.gitHash server = responseModel.server.map(ThirdPartyServerConfig.init) + settings = responseModel.settings.map(ServerSettings.init) version = responseModel.version } @@ -177,3 +181,27 @@ public struct ServerCommunicationBootstrapSettings: Equatable, Codable, Sendable cookieDomain = responseModel.cookieDomain } } + +// MARK: - ServerSettings + +/// Domain model for server-level settings. +public struct ServerSettings: Equatable, Codable, Sendable { + // MARK: Properties + + /// Whether user registration is disabled on this server. + public let disableUserRegistration: Bool + + // MARK: Initialization + + /// Creates a new `ServerSettings`. + /// + /// - Parameter disableUserRegistration: Whether user registration is disabled on this server. + /// + public init(disableUserRegistration: Bool) { + self.disableUserRegistration = disableUserRegistration + } + + public init(responseModel: ServerSettingsResponseModel) { + disableUserRegistration = responseModel.disableUserRegistration + } +} diff --git a/BitwardenKit/Core/Platform/Models/Enum/RegionType.swift b/BitwardenKit/Core/Platform/Models/Enum/RegionType.swift index 07cfdb1ca9..91c36faebf 100644 --- a/BitwardenKit/Core/Platform/Models/Enum/RegionType.swift +++ b/BitwardenKit/Core/Platform/Models/Enum/RegionType.swift @@ -10,6 +10,9 @@ public enum RegionType: CaseIterable, Sendable { /// The European region. case europe + /// The government cloud (FedRAMP) region. + case gov + /// A self-hosted instance. case selfHosted @@ -29,6 +32,8 @@ public extension RegionType { self = .unitedStates } else if baseURL == EnvironmentURLData.defaultEU.base { self = .europe + } else if baseURL == EnvironmentURLData.defaultGov.base { + self = .gov } else if let baseURL, let host = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)?.host, host == "bitwarden.pw" || host.hasSuffix(".bitwarden.pw") { diff --git a/BitwardenKit/Core/Platform/Services/API/Fixtures/APITestData+Config.swift b/BitwardenKit/Core/Platform/Services/API/Fixtures/APITestData+Config.swift index 3631c5403e..7bc0e3cd79 100644 --- a/BitwardenKit/Core/Platform/Services/API/Fixtures/APITestData+Config.swift +++ b/BitwardenKit/Core/Platform/Services/API/Fixtures/APITestData+Config.swift @@ -4,4 +4,9 @@ import TestHelpers public extension APITestData { /// A valid server configuration to produce a `ConfigResponseModel`. static let validServerConfig = loadFromJsonBundle(resource: "ValidServerConfig") + + /// A valid server configuration with `disableUserRegistration` set to `true`. + static let validServerConfigDisableRegistration = loadFromJsonBundle( + resource: "ValidServerConfigDisableRegistration", + ) } diff --git a/BitwardenKit/Core/Platform/Services/API/Fixtures/ValidServerConfigDisableRegistration.json b/BitwardenKit/Core/Platform/Services/API/Fixtures/ValidServerConfigDisableRegistration.json new file mode 100644 index 0000000000..16828ee1d3 --- /dev/null +++ b/BitwardenKit/Core/Platform/Services/API/Fixtures/ValidServerConfigDisableRegistration.json @@ -0,0 +1,18 @@ +{ + "version": "2024.4.2", + "gitHash": "75238191", + "server": null, + "environment": { + "cloudRegion": "US", + "vault": "https://vault.qa.bitwarden.pw", + "api": "https://api.qa.bitwarden.pw", + "identity": "https://identity.qa.bitwarden.pw", + "notifications": "https://notifications.qa.bitwarden.pw", + "sso": "https://sso.qa.bitwarden.pw" + }, + "settings": { + "disableUserRegistration": true + }, + "featureStates": {}, + "object": "config" +} diff --git a/BitwardenKit/Core/Platform/Services/API/Response/ConfigResponseModel.swift b/BitwardenKit/Core/Platform/Services/API/Response/ConfigResponseModel.swift index 7363d3223c..a0c8374eaa 100644 --- a/BitwardenKit/Core/Platform/Services/API/Response/ConfigResponseModel.swift +++ b/BitwardenKit/Core/Platform/Services/API/Response/ConfigResponseModel.swift @@ -23,6 +23,9 @@ public struct ConfigResponseModel: Equatable, JSONResponse { /// Third party server information. public let server: ThirdPartyConfigResponseModel? + /// Server-level settings. + public let settings: ServerSettingsResponseModel? + /// The version of the server. public let version: String @@ -36,6 +39,7 @@ public struct ConfigResponseModel: Equatable, JSONResponse { /// - featureStates: Feature flags to configure the client. /// - gitHash: The git hash of the server. /// - server: Third party server information. + /// - settings: Server-level settings. /// - version: The version of the server. public init( communication: CommunicationSettingsResponseModel?, @@ -43,6 +47,7 @@ public struct ConfigResponseModel: Equatable, JSONResponse { featureStates: [String: AnyCodable]?, gitHash: String?, server: ThirdPartyConfigResponseModel?, + settings: ServerSettingsResponseModel? = nil, version: String, ) { self.communication = communication @@ -50,6 +55,7 @@ public struct ConfigResponseModel: Equatable, JSONResponse { self.featureStates = featureStates self.gitHash = gitHash self.server = server + self.settings = settings self.version = version } } @@ -144,6 +150,23 @@ public struct ThirdPartyConfigResponseModel: Equatable, JSONResponse { } } +// MARK: - ServerSettingsResponseModel + +/// API response model for server-level settings in a configuration response. +public struct ServerSettingsResponseModel: Equatable, JSONResponse { + /// Whether user registration is disabled on this server. + public let disableUserRegistration: Bool + + // MARK: Initializers + + /// Initializes a `ServerSettingsResponseModel`. + /// + /// - Parameter disableUserRegistration: Whether user registration is disabled on this server. + public init(disableUserRegistration: Bool) { + self.disableUserRegistration = disableUserRegistration + } +} + // MARK: - EnvironmentServerConfigResponseModel /// API response model for the environment URLs in a configuration response. diff --git a/BitwardenResources/Localizations/en.lproj/Localizable.strings b/BitwardenResources/Localizations/en.lproj/Localizable.strings index 02107ed7d6..adb38d70a2 100644 --- a/BitwardenResources/Localizations/en.lproj/Localizable.strings +++ b/BitwardenResources/Localizations/en.lproj/Localizable.strings @@ -39,6 +39,7 @@ "RemoveAccount" = "Remove account"; "RemoveAccountConfirmation" = "Are you sure you want to remove this account?"; "AccountAlreadyAdded" = "Account already added"; +"AccountCreationNotAllowed" = "Account creation is not allowed for this server."; "SwitchToAlreadyAddedAccountConfirmation" = "Would you like to switch to it now?"; "MasterPassword" = "Master password"; "MoreOptions" = "More options"; @@ -563,6 +564,7 @@ "ThereAreNoBankAccountsInYourVault" = "There are no bank accounts in your vault"; "US" = "US"; "EU" = "EU"; +"Gov" = "Gov"; "SelfHosted" = "Self-hosted"; "UpdateWeakMasterPasswordWarning" = "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."; "CurrentMasterPasswordRequired" = "Current master password (required)"; diff --git a/BitwardenShared/Core/Platform/Models/Enum/RegionType.swift b/BitwardenShared/Core/Platform/Models/Enum/RegionType.swift index 0b968c9185..72d73098b7 100644 --- a/BitwardenShared/Core/Platform/Models/Enum/RegionType.swift +++ b/BitwardenShared/Core/Platform/Models/Enum/RegionType.swift @@ -15,6 +15,7 @@ extension RegionType { var authCallbackHost: String? { switch self { case .europe: "bitwarden.eu" + case .gov: "bitwarden-gov.com" case .internal: "bitwarden.pw" case .selfHosted: nil case .unitedStates: "bitwarden.com" @@ -25,6 +26,7 @@ extension RegionType { var localizedName: String { switch self { case .europe: Localizations.eu + case .gov: Localizations.gov case .internal: "Internal" // Internal only, hidden from the region picker and not user facing. case .selfHosted: Localizations.selfHosted case .unitedStates: Localizations.us @@ -35,6 +37,7 @@ extension RegionType { var baseURLDescription: String { switch self { case .europe: "bitwarden.eu" + case .gov: "bitwarden-gov.com" // `.internal` is not user facing, so it mirrors self-hosted's description. case .internal, .selfHosted: Localizations.selfHosted case .unitedStates: "bitwarden.com" @@ -46,6 +49,8 @@ extension RegionType { switch self { case .europe: .defaultEU + case .gov: + .defaultGov case .unitedStates: .defaultUS case .internal, .selfHosted: @@ -57,6 +62,7 @@ extension RegionType { var errorReporterName: String { switch self { case .europe: "EU" + case .gov: "Gov" case .internal: "Internal" case .selfHosted: "Self-Hosted" case .unitedStates: "US" @@ -68,7 +74,7 @@ extension RegionType { /// offered as a selectable option. var isUserSelectable: Bool { switch self { - case .europe, .selfHosted, .unitedStates: true + case .europe, .gov, .selfHosted, .unitedStates: true case .internal: false } } diff --git a/BitwardenShared/Core/Platform/Models/Enum/RegionTypeTests.swift b/BitwardenShared/Core/Platform/Models/Enum/RegionTypeTests.swift index f66da379bb..b368136a35 100644 --- a/BitwardenShared/Core/Platform/Models/Enum/RegionTypeTests.swift +++ b/BitwardenShared/Core/Platform/Models/Enum/RegionTypeTests.swift @@ -10,6 +10,7 @@ class RegionTypeTests: BitwardenTestCase { /// `authCallbackHost` returns the apex host for Cloud regions and `nil` for self-hosted. func test_authCallbackHost() { XCTAssertEqual(RegionType.europe.authCallbackHost, "bitwarden.eu") + XCTAssertEqual(RegionType.gov.authCallbackHost, "bitwarden-gov.com") XCTAssertEqual(RegionType.internal.authCallbackHost, "bitwarden.pw") XCTAssertNil(RegionType.selfHosted.authCallbackHost) XCTAssertEqual(RegionType.unitedStates.authCallbackHost, "bitwarden.com") @@ -18,6 +19,7 @@ class RegionTypeTests: BitwardenTestCase { /// `getter:localizedName` returns the correct values. func test_localizedName() { XCTAssertEqual(RegionType.europe.localizedName, Localizations.eu) + XCTAssertEqual(RegionType.gov.localizedName, Localizations.gov) XCTAssertEqual(RegionType.internal.localizedName, "Internal") XCTAssertEqual(RegionType.selfHosted.localizedName, Localizations.selfHosted) XCTAssertEqual(RegionType.unitedStates.localizedName, Localizations.us) @@ -26,6 +28,7 @@ class RegionTypeTests: BitwardenTestCase { /// `getter:baseURLDescription` returns the correct values. func test_baseURLDescription() { XCTAssertEqual(RegionType.europe.baseURLDescription, "bitwarden.eu") + XCTAssertEqual(RegionType.gov.baseURLDescription, "bitwarden-gov.com") XCTAssertEqual(RegionType.internal.baseURLDescription, Localizations.selfHosted) XCTAssertEqual(RegionType.selfHosted.baseURLDescription, Localizations.selfHosted) XCTAssertEqual(RegionType.unitedStates.baseURLDescription, "bitwarden.com") @@ -34,6 +37,7 @@ class RegionTypeTests: BitwardenTestCase { /// `getter:defaultURLs` returns the correct values. func test_defaultURLs() { XCTAssertEqual(RegionType.europe.defaultURLs?.api?.absoluteString, "https://api.bitwarden.eu") + XCTAssertEqual(RegionType.gov.defaultURLs?.api?.absoluteString, "https://api.bitwarden-gov.com") XCTAssertNil(RegionType.internal.defaultURLs) XCTAssertNil(RegionType.selfHosted.defaultURLs) XCTAssertEqual(RegionType.unitedStates.defaultURLs?.api?.absoluteString, "https://api.bitwarden.com") @@ -42,6 +46,7 @@ class RegionTypeTests: BitwardenTestCase { /// `getter:errorReporterName` returns the correct values. func test_errorReporterName() { XCTAssertEqual(RegionType.europe.errorReporterName, "EU") + XCTAssertEqual(RegionType.gov.errorReporterName, "Gov") XCTAssertEqual(RegionType.internal.errorReporterName, "Internal") XCTAssertEqual(RegionType.selfHosted.errorReporterName, "Self-Hosted") XCTAssertEqual(RegionType.unitedStates.errorReporterName, "US") @@ -50,6 +55,7 @@ class RegionTypeTests: BitwardenTestCase { /// `getter:isUserSelectable` is true for the user-facing regions and false for internal. func test_isUserSelectable() { XCTAssertTrue(RegionType.europe.isUserSelectable) + XCTAssertTrue(RegionType.gov.isUserSelectable) XCTAssertFalse(RegionType.internal.isUserSelectable) XCTAssertTrue(RegionType.selfHosted.isUserSelectable) XCTAssertTrue(RegionType.unitedStates.isUserSelectable) @@ -57,6 +63,6 @@ class RegionTypeTests: BitwardenTestCase { /// `userSelectableCases` returns the user-facing regions in display order, excluding internal. func test_userSelectableCases() { - XCTAssertEqual(RegionType.userSelectableCases, [.unitedStates, .europe, .selfHosted]) + XCTAssertEqual(RegionType.userSelectableCases, [.unitedStates, .europe, .gov, .selfHosted]) } } diff --git a/BitwardenShared/Core/Platform/Services/EnvironmentServiceTests+GovTests.swift b/BitwardenShared/Core/Platform/Services/EnvironmentServiceTests+GovTests.swift new file mode 100644 index 0000000000..5f0e15720b --- /dev/null +++ b/BitwardenShared/Core/Platform/Services/EnvironmentServiceTests+GovTests.swift @@ -0,0 +1,40 @@ +import BitwardenKit +import XCTest + +@testable import BitwardenShared +@testable import BitwardenSharedMocks + +extension EnvironmentServiceTests { + // MARK: Tests + + /// `loadURLsForActiveAccount()` handles government cloud (FedRAMP) URLs + func test_loadURLsForActiveAccount_gov() async { + let urls = EnvironmentURLData.defaultGov + let account = Account.fixture(settings: .fixture(environmentURLs: urls)) + stateService.activeAccount = account + stateService.environmentURLs = [account.profile.userId: urls] + + await subject.loadURLsForActiveAccount() + + XCTAssertEqual(subject.apiURL, URL(string: "https://api.bitwarden-gov.com")) + XCTAssertEqual(subject.baseURL, URL(string: "https://vault.bitwarden-gov.com")) + XCTAssertEqual(subject.changeEmailURL, URL(string: "https://vault.bitwarden-gov.com/#/settings/account")) + XCTAssertEqual(subject.eventsURL, URL(string: "https://events.bitwarden-gov.com")) + XCTAssertEqual(subject.iconsURL, URL(string: "https://icons.bitwarden-gov.com")) + XCTAssertEqual(subject.identityURL, URL(string: "https://identity.bitwarden-gov.com")) + XCTAssertEqual(subject.importItemsURL, URL(string: "https://vault.bitwarden-gov.com/#/tools/import")) + // swiftlint:disable:next line_length + XCTAssertEqual(subject.proxyCookieRedirectConnectorURL, URL(string: "https://vault.bitwarden-gov.com/proxy-cookie-redirect-connector.html")) + XCTAssertEqual(subject.recoveryCodeURL, URL(string: "https://vault.bitwarden-gov.com/#/recover-2fa")) + XCTAssertEqual(subject.region, .gov) + XCTAssertEqual(subject.sendShareURL, URL(string: "https://send.bitwarden-gov.com/#")) + XCTAssertEqual(subject.settingsURL, URL(string: "https://vault.bitwarden-gov.com/#/settings")) + // swiftlint:disable:next line_length + XCTAssertEqual(subject.setUpTwoFactorURL, URL(string: "https://vault.bitwarden-gov.com/#/settings/security/two-factor")) + XCTAssertEqual(subject.webVaultURL, URL(string: "https://vault.bitwarden-gov.com")) + XCTAssertEqual(stateService.preAuthEnvironmentURLs, urls) + + XCTAssertEqual(errorReporter.region?.region, "Gov") + XCTAssertEqual(errorReporter.region?.isPreAuth, false) + } +} diff --git a/BitwardenShared/UI/Auth/Extensions/Alert+Auth.swift b/BitwardenShared/UI/Auth/Extensions/Alert+Auth.swift index c1bbaba437..062f80ab25 100644 --- a/BitwardenShared/UI/Auth/Extensions/Alert+Auth.swift +++ b/BitwardenShared/UI/Auth/Extensions/Alert+Auth.swift @@ -79,6 +79,21 @@ extension Alert { ) } + /// An alert notifying the user that account creation is not allowed for the current server. + /// + /// - Parameter action: The action to perform when the user dismisses the alert. + /// - Returns: An alert notifying the user that account creation is not allowed. + /// + static func registrationDisabled(action: @escaping () async -> Void) -> Alert { + Alert( + title: Localizations.anErrorHasOccurred, + message: Localizations.accountCreationNotAllowed, + alertActions: [ + AlertAction(title: Localizations.ok, style: .default) { _ in await action() }, + ], + ) + } + /// Display the options to log out of, lock, or remove the selected profile switcher item. /// /// - Parameters: diff --git a/BitwardenShared/UI/Auth/Extensions/AlertAuthTests.swift b/BitwardenShared/UI/Auth/Extensions/AlertAuthTests.swift index 77e6f1de7e..f993966ab6 100644 --- a/BitwardenShared/UI/Auth/Extensions/AlertAuthTests.swift +++ b/BitwardenShared/UI/Auth/Extensions/AlertAuthTests.swift @@ -382,4 +382,18 @@ class AlertAuthTests: BitwardenTestCase { // swiftlint:disable:this type_body_le try await subject.tapAction(title: Localizations.yes) XCTAssertTrue(actionCalled) } + + /// `registrationDisabled(action:)` builds the correct alert and fires the action on OK. + func test_registrationDisabled() async throws { + var actionCalled = false + let subject = Alert.registrationDisabled { actionCalled = true } + + XCTAssertEqual(subject.title, Localizations.anErrorHasOccurred) + XCTAssertEqual(subject.message, Localizations.accountCreationNotAllowed) + XCTAssertEqual(subject.alertActions.count, 1) + XCTAssertEqual(subject.alertActions[0].title, Localizations.ok) + + try await subject.tapAction(title: Localizations.ok) + XCTAssertTrue(actionCalled) + } } diff --git a/BitwardenShared/UI/Auth/Landing/LandingProcessor.swift b/BitwardenShared/UI/Auth/Landing/LandingProcessor.swift index 4f4c749837..3d50a2e19d 100644 --- a/BitwardenShared/UI/Auth/Landing/LandingProcessor.swift +++ b/BitwardenShared/UI/Auth/Landing/LandingProcessor.swift @@ -58,6 +58,14 @@ class LandingProcessor: StateProcessor! var delegate: MockStartRegistrationDelegate! + var environmentService: MockEnvironmentService! var errorReporter: MockErrorReporter! - var subject: StartRegistrationProcessor! var stateService: MockStateService! - var environmentService: MockEnvironmentService! + var subject: StartRegistrationProcessor! // MARK: Setup & Teardown @@ -29,6 +30,7 @@ class StartRegistrationProcessorTests: BitwardenTestCase { // swiftlint:disable: super.setUp() authRepository = MockAuthRepository() client = MockHTTPClient() + configService = MockConfigService() coordinator = MockCoordinator() delegate = MockStartRegistrationDelegate() environmentService = MockEnvironmentService() @@ -40,6 +42,7 @@ class StartRegistrationProcessorTests: BitwardenTestCase { // swiftlint:disable: delegate: delegate, services: ServiceContainer.withMocks( authRepository: authRepository, + configService: configService, environmentService: environmentService, errorReporter: errorReporter, httpClient: client, @@ -53,16 +56,17 @@ class StartRegistrationProcessorTests: BitwardenTestCase { // swiftlint:disable: super.tearDown() authRepository = nil client = nil + configService = nil coordinator = nil environmentService = nil errorReporter = nil - subject = nil stateService = nil + subject = nil } // MARK: Tests - /// `perform(_:)` with `.regionTapped` navigates to the region selection screen. + /// `perform(_:)` with `.regionTapped` shows US, EU, Self-Hosted — Gov is excluded. @MainActor func test_perform_regionTapped() async throws { await subject.perform(.regionTapped) @@ -70,7 +74,8 @@ class StartRegistrationProcessorTests: BitwardenTestCase { // swiftlint:disable: var alert = try XCTUnwrap(coordinator.alertShown.last) XCTAssertEqual(alert.title, Localizations.creatingOn) XCTAssertNil(alert.message) - XCTAssertEqual(alert.alertActions.count, 4) + XCTAssertEqual(alert.alertActions.count, 4) // US + EU + Self-Hosted + Cancel (no Gov) + XCTAssertNil(alert.alertActions.first(where: { $0.title == "bitwarden-gov.com" })) XCTAssertEqual(alert.alertActions[0].title, "bitwarden.com") try await alert.tapAction(title: "bitwarden.com") @@ -89,6 +94,70 @@ class StartRegistrationProcessorTests: BitwardenTestCase { // swiftlint:disable: XCTAssertEqual(coordinator.routes.last, .selfHosted(currentRegion: .europe)) } + /// When `disableUserRegistration` becomes true while the view is visible, an alert is shown + /// and tapping OK navigates back. + @MainActor + func test_configPublisher_disableUserRegistration_true_whileVisible() async throws { + await subject.perform(.appeared) + + configService.configSubject.send(MetaServerConfig( + isPreAuth: true, + userId: nil, + serverConfig: ServerConfig( + date: Date(), + responseModel: ConfigResponseModel( + communication: nil, + environment: nil, + featureStates: [:], + gitHash: nil, + server: nil, + settings: ServerSettingsResponseModel(disableUserRegistration: true), + version: "2024.4.0", + ), + ), + )) + waitFor(coordinator.alertShown.last?.message == Localizations.accountCreationNotAllowed) + + let alert = try XCTUnwrap(coordinator.alertShown.last) + XCTAssertEqual(alert.title, Localizations.anErrorHasOccurred) + XCTAssertEqual(alert.alertActions.count, 1) + try await alert.tapAction(title: Localizations.ok) + XCTAssertEqual(coordinator.routes.last, .dismiss) + } + + /// When `disableUserRegistration` becomes true but the view is not visible, no alert is shown. + @MainActor + func test_configPublisher_disableUserRegistration_viewNotVisible_noAlert() async throws { + // viewIsVisible is false — `.appeared` has not been called + configService.configSubject.send(MetaServerConfig( + isPreAuth: true, + userId: nil, + serverConfig: ServerConfig( + date: Date(), + responseModel: ConfigResponseModel( + communication: nil, + environment: nil, + featureStates: [:], + gitHash: nil, + server: nil, + settings: ServerSettingsResponseModel(disableUserRegistration: true), + version: "2024.4.0", + ), + ), + )) + try await Task.sleep(nanoseconds: 10_000_000) + XCTAssertTrue(coordinator.alertShown.isEmpty) + } + + /// `setRegion` triggers a config refresh so the processor can react to `disableUserRegistration`. + @MainActor + func test_setRegion_callsRefreshConfig() async { + await subject.setRegion(.europe, .defaultEU) + waitFor(configService.configMocker.called) + XCTAssertEqual(configService.configMocker.invokedParam?.forceRefresh, true) + XCTAssertEqual(configService.configMocker.invokedParam?.isPreAuth, true) + } + /// `perform(_:)` with `.startRegistration` sets preAuthUrls for the given email and navigates to check email. @MainActor func test_perform_startRegistration_setPreAuthUrls_checkEmail() async throws { diff --git a/BitwardenShared/UI/Auth/Utilities/RegionHelper.swift b/BitwardenShared/UI/Auth/Utilities/RegionHelper.swift index 14c292c317..90416c7711 100644 --- a/BitwardenShared/UI/Auth/Utilities/RegionHelper.swift +++ b/BitwardenShared/UI/Auth/Utilities/RegionHelper.swift @@ -35,19 +35,30 @@ class RegionHelper { /// Builds an alert for region selection and navigates to the alert. /// - func presentRegionSelectorAlert(title: String, currentRegion: RegionType?) async { - let actions = RegionType.userSelectableCases.map { region in - AlertAction(title: region.baseURLDescription, style: .default) { _ in - if let urls = region.defaultURLs { - await self.delegate?.setRegion(region, urls) - } else { - await self.coordinator.navigate( - to: .selfHosted(currentRegion: currentRegion ?? .unitedStates), - context: self.delegate, - ) + /// - Parameters: + /// - title: The title of the alert. + /// - currentRegion: The currently selected region. + /// - excludingRegions: Regions to omit from the picker. Defaults to empty (all selectable regions shown). + /// + func presentRegionSelectorAlert( + title: String, + currentRegion: RegionType?, + excludingRegions: [RegionType] = [], + ) async { + let actions = RegionType.userSelectableCases + .filter { !excludingRegions.contains($0) } + .map { region in + AlertAction(title: region.baseURLDescription, style: .default) { _ in + if let urls = region.defaultURLs { + await self.delegate?.setRegion(region, urls) + } else { + await self.coordinator.navigate( + to: .selfHosted(currentRegion: currentRegion ?? .unitedStates), + context: self.delegate, + ) + } } } - } let cancelAction = AlertAction(title: Localizations.cancel, style: .cancel) let alert = Alert( title: title, diff --git a/BitwardenShared/UI/Auth/Utilities/RegionHelperTests.swift b/BitwardenShared/UI/Auth/Utilities/RegionHelperTests.swift index fd34cac48b..96581fe835 100644 --- a/BitwardenShared/UI/Auth/Utilities/RegionHelperTests.swift +++ b/BitwardenShared/UI/Auth/Utilities/RegionHelperTests.swift @@ -47,7 +47,7 @@ class RegionHelperTests: BitwardenTestCase { let alert = try XCTUnwrap(coordinator.alertShown.last) XCTAssertEqual(alert.title, Localizations.creatingOn) XCTAssertNil(alert.message) - XCTAssertEqual(alert.alertActions.count, 4) + XCTAssertEqual(alert.alertActions.count, 5) XCTAssertEqual(alert.alertActions[0].title, "bitwarden.com") try await alert.tapAction(title: "bitwarden.com") @@ -64,7 +64,7 @@ class RegionHelperTests: BitwardenTestCase { let alert = try XCTUnwrap(coordinator.alertShown.last) XCTAssertEqual(alert.title, Localizations.creatingOn) XCTAssertNil(alert.message) - XCTAssertEqual(alert.alertActions.count, 4) + XCTAssertEqual(alert.alertActions.count, 5) XCTAssertEqual(alert.alertActions[1].title, "bitwarden.eu") try await alert.tapAction(title: "bitwarden.eu") @@ -73,6 +73,23 @@ class RegionHelperTests: BitwardenTestCase { XCTAssertEqual(regionDelegate.setRegionUrls, RegionType.europe.defaultURLs) } + /// `presentRegionSelectorAlert(title:currentRegion)` shows alert and tap bitwarden-gov.com. + @MainActor + func test_presentRegionSelectorAlert_gov() async throws { + await subject.presentRegionSelectorAlert(title: Localizations.creatingOn, currentRegion: .unitedStates) + + let alert = try XCTUnwrap(coordinator.alertShown.last) + XCTAssertEqual(alert.title, Localizations.creatingOn) + XCTAssertNil(alert.message) + XCTAssertEqual(alert.alertActions.count, 5) + + XCTAssertEqual(alert.alertActions[2].title, "bitwarden-gov.com") + try await alert.tapAction(title: "bitwarden-gov.com") + XCTAssertTrue(regionDelegate.setRegionCalled) + XCTAssertEqual(regionDelegate.setRegionType, .gov) + XCTAssertEqual(regionDelegate.setRegionUrls, RegionType.gov.defaultURLs) + } + /// `presentRegionSelectorAlert(title:currentRegion)` shows alert and tap selfhosted. @MainActor func test_presentRegionSelectorAlert_selfHosted() async throws { @@ -81,9 +98,9 @@ class RegionHelperTests: BitwardenTestCase { let alert = try XCTUnwrap(coordinator.alertShown.last) XCTAssertEqual(alert.title, Localizations.creatingOn) XCTAssertNil(alert.message) - XCTAssertEqual(alert.alertActions.count, 4) + XCTAssertEqual(alert.alertActions.count, 5) - XCTAssertEqual(alert.alertActions[2].title, Localizations.selfHosted) + XCTAssertEqual(alert.alertActions[3].title, Localizations.selfHosted) try await alert.tapAction(title: Localizations.selfHosted) XCTAssertEqual(coordinator.routes.last, .selfHosted(currentRegion: .europe)) } @@ -96,15 +113,15 @@ class RegionHelperTests: BitwardenTestCase { let alert = try XCTUnwrap(coordinator.alertShown.last) XCTAssertEqual(alert.title, Localizations.loggingInOn) XCTAssertNil(alert.message) - XCTAssertEqual(alert.alertActions.count, 4) + XCTAssertEqual(alert.alertActions.count, 5) - XCTAssertEqual(alert.alertActions[2].title, Localizations.selfHosted) + XCTAssertEqual(alert.alertActions[3].title, Localizations.selfHosted) try await alert.tapAction(title: Localizations.selfHosted) XCTAssertEqual(coordinator.routes.last, .selfHosted(currentRegion: .unitedStates)) } /// `presentRegionSelectorAlert(title:currentRegion:)` offers exactly the user-selectable regions - /// (US, EU, self-hosted) plus cancel — the internal (`bitwarden.pw`) region is detected + /// (US, EU, Gov, self-hosted) plus cancel — the internal (`bitwarden.pw`) region is detected /// automatically and must never appear in the picker. @MainActor func test_presentRegionSelectorAlert_excludesInternal() async throws { @@ -112,7 +129,24 @@ class RegionHelperTests: BitwardenTestCase { let alert = try XCTUnwrap(coordinator.alertShown.last) let actionTitles = alert.alertActions.map(\.title) - XCTAssertEqual(actionTitles, ["bitwarden.com", "bitwarden.eu", Localizations.selfHosted, Localizations.cancel]) + XCTAssertEqual( + actionTitles, + ["bitwarden.com", "bitwarden.eu", "bitwarden-gov.com", Localizations.selfHosted, Localizations.cancel], + ) + } + + /// `presentRegionSelectorAlert(title:currentRegion:excludingRegions:)` omits excluded regions. + @MainActor + func test_presentRegionSelectorAlert_excludingRegions() async throws { + await subject.presentRegionSelectorAlert( + title: Localizations.creatingOn, + currentRegion: .unitedStates, + excludingRegions: [.gov], + ) + + let alert = try XCTUnwrap(coordinator.alertShown.last) + XCTAssertEqual(alert.alertActions.count, 4) // US + EU + Self-Hosted + Cancel + XCTAssertNil(alert.alertActions.first(where: { $0.title == "bitwarden-gov.com" })) } /// `loadRegion()` with pre auth region as nil default to us @@ -142,6 +176,15 @@ class RegionHelperTests: BitwardenTestCase { XCTAssertEqual(regionDelegate.setRegionUrls, RegionType.europe.defaultURLs) } + /// `loadRegion()` with pre auth region + func test_loadRegion_gov() async throws { + stateService.preAuthEnvironmentURLs = .defaultGov + await subject.loadRegion() + XCTAssertTrue(regionDelegate.setRegionCalled) + XCTAssertEqual(regionDelegate.setRegionType, .gov) + XCTAssertEqual(regionDelegate.setRegionUrls, RegionType.gov.defaultURLs) + } + /// `loadRegion()` with pre auth region func test_loadRegion_selfHosted() async throws { stateService.preAuthEnvironmentURLs = EnvironmentURLData(base: URL(string: "https://selfhosted.com"))