Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")!,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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")!)
Expand All @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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")!,
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions BitwardenKit/Core/Platform/Models/Domain/ServerConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
}
5 changes: 5 additions & 0 deletions BitwardenKit/Core/Platform/Models/Enum/RegionType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -36,20 +39,23 @@ 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?,
environment: EnvironmentServerConfigResponseModel?,
featureStates: [String: AnyCodable]?,
gitHash: String?,
server: ThirdPartyConfigResponseModel?,
settings: ServerSettingsResponseModel? = nil,
version: String,
) {
self.communication = communication
self.environment = environment
self.featureStates = featureStates
self.gitHash = gitHash
self.server = server
self.settings = settings
self.version = version
}
}
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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"
Expand All @@ -46,6 +49,8 @@ extension RegionType {
switch self {
case .europe:
.defaultEU
case .gov:
.defaultGov
case .unitedStates:
.defaultUS
case .internal, .selfHosted:
Expand All @@ -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"
Expand All @@ -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
}
}
Expand Down
Loading
Loading