Skip to content

Commit 6cfe749

Browse files
authored
[PM-18935] show key connector domain (#1529)
1 parent 8ce8f80 commit 6cfe749

29 files changed

+492
-50
lines changed

BitwardenShared/Core/Auth/Repositories/AuthRepository.swift

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ protocol AuthRepository: AnyObject {
4545
///
4646
func clearPins() async throws
4747

48+
/// Convert new user to key connector.
49+
///
50+
func convertNewUserToKeyConnector(keyConnectorURL: URL, orgIdentifier: String) async throws
51+
4852
/// Create new account for a JIT sso user .
4953
///
5054
func createNewSsoUser(orgIdentifier: String, rememberDevice: Bool) async throws
@@ -107,6 +111,12 @@ protocol AuthRepository: AnyObject {
107111
///
108112
func isUserManagedByOrganization() async throws -> Bool
109113

114+
/// User leaves organization
115+
///
116+
/// - Parameters:
117+
/// - organizationId: The ID of the organization the user is leaving.
118+
func leaveOrganization(organizationId: String) async throws
119+
110120
/// Locks the user's vault and clears decrypted data from memory
111121
/// - Parameters:
112122
/// - userId: The userId of the account to lock. Defaults to active account if nil
@@ -565,6 +575,13 @@ extension DefaultAuthRepository: AuthRepository {
565575
}
566576
}
567577

578+
func convertNewUserToKeyConnector(keyConnectorURL: URL, orgIdentifier: String) async throws {
579+
try await keyConnectorService.convertNewUserToKeyConnector(
580+
keyConnectorUrl: keyConnectorURL,
581+
orgIdentifier: orgIdentifier
582+
)
583+
}
584+
568585
func createNewSsoUser(orgIdentifier: String, rememberDevice: Bool) async throws {
569586
let account = try await stateService.getActiveAccount()
570587
let enrollStatus = try await organizationAPIService.getOrganizationAutoEnrollStatus(identifier: orgIdentifier)
@@ -735,6 +752,10 @@ extension DefaultAuthRepository: AuthRepository {
735752
try await keyConnectorService.migrateUser(password: password)
736753
}
737754

755+
func leaveOrganization(organizationId: String) async throws {
756+
try await organizationAPIService.leaveOrganization(organizationId: organizationId)
757+
}
758+
738759
func logout(userId: String?, userInitiated: Bool) async throws {
739760
let userId = try await stateService.getAccountIdOrActiveId(userId: userId)
740761

@@ -952,18 +973,7 @@ extension DefaultAuthRepository: AuthRepository {
952973
func unlockVaultWithKeyConnectorKey(keyConnectorURL: URL, orgIdentifier: String) async throws {
953974
let account = try await stateService.getActiveAccount()
954975

955-
let encryptionKeys: AccountEncryptionKeys
956-
do {
957-
encryptionKeys = try await stateService.getAccountEncryptionKeys(userId: account.profile.userId)
958-
} catch StateServiceError.noEncryptedPrivateKey {
959-
// If the private key doesn't exist, this is a new user and we need to convert them to
960-
// use key connector.
961-
try await keyConnectorService.convertNewUserToKeyConnector(
962-
keyConnectorUrl: keyConnectorURL,
963-
orgIdentifier: orgIdentifier
964-
)
965-
encryptionKeys = try await stateService.getAccountEncryptionKeys(userId: account.profile.userId)
966-
}
976+
let encryptionKeys = try await stateService.getAccountEncryptionKeys(userId: account.profile.userId)
967977

968978
guard let encryptedUserKey = encryptionKeys.encryptedUserKey else { throw StateServiceError.noEncUserKey }
969979

BitwardenShared/Core/Auth/Repositories/AuthRepositoryTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,20 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
19551955
stateService.activeAccount = .fixture()
19561956
stateService.getAccountEncryptionKeysError = StateServiceError.noEncryptedPrivateKey
19571957

1958+
await assertAsyncThrows(error: StateServiceError.noEncryptedPrivateKey) {
1959+
try await subject.unlockVaultWithKeyConnectorKey(
1960+
keyConnectorURL: URL(string: "https://example.com")!,
1961+
orgIdentifier: "org-id"
1962+
)
1963+
}
1964+
1965+
await assertAsyncDoesNotThrow {
1966+
try await subject.convertNewUserToKeyConnector(
1967+
keyConnectorURL: URL(string: "https://example.com")!,
1968+
orgIdentifier: "org-id"
1969+
)
1970+
}
1971+
19581972
await assertAsyncDoesNotThrow {
19591973
try await subject.unlockVaultWithKeyConnectorKey(
19601974
keyConnectorURL: URL(string: "https://example.com")!,
@@ -1975,6 +1989,33 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
19751989
XCTAssertTrue(vaultTimeoutService.unlockVaultHadUserInteraction)
19761990
}
19771991

1992+
/// `convertNewUserToKeyConnector()` converts a new user to use key connector.
1993+
func test_convertNewUserToKeyconnector() async {
1994+
clientService.mockCrypto.initializeUserCryptoResult = .success(())
1995+
keyConnectorService.convertNewUserToKeyConnectorHandler = { [weak self] in
1996+
self?.stateService.accountEncryptionKeys["1"] = AccountEncryptionKeys(
1997+
encryptedPrivateKey: "private",
1998+
encryptedUserKey: "user"
1999+
)
2000+
self?.stateService.getAccountEncryptionKeysError = nil
2001+
}
2002+
keyConnectorService.getMasterKeyFromKeyConnectorResult = .success("key")
2003+
stateService.activeAccount = .fixture()
2004+
stateService.getAccountEncryptionKeysError = StateServiceError.noEncryptedPrivateKey
2005+
2006+
await assertAsyncDoesNotThrow {
2007+
try await subject.convertNewUserToKeyConnector(
2008+
keyConnectorURL: URL(string: "https://example.com")!,
2009+
orgIdentifier: "org-id"
2010+
)
2011+
}
2012+
XCTAssertTrue(keyConnectorService.convertNewUserToKeyConnectorCalled)
2013+
XCTAssertEqual(keyConnectorService.convertNewUserToKeyConnectorOrganizationId,
2014+
"org-id")
2015+
XCTAssertEqual(keyConnectorService.convertNewUserToKeyConnectorKeyConnectorUrl,
2016+
URL(string: "https://example.com"))
2017+
}
2018+
19782019
/// `unlockVaultWithKeyConnectorKey()` throws an error if the user is missing an encrypted user key.
19792020
func test_unlockVaultWithKeyConnectorKey_missingEncryptedUserKey() async {
19802021
stateService.activeAccount = .fixture()

BitwardenShared/Core/Auth/Repositories/TestHelpers/MockAuthRepository.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
3434
var isPinUnlockAvailableResult: Result<Bool, Error> = .success(false)
3535
var isUserManagedByOrganizationResult: Result<Bool, Error> = .success(false)
3636
var pinUnlockAvailabilityResult: Result<[String: Bool], Error> = .success([:])
37+
var leaveOrganizationCalled = false
38+
var leaveOrganizationOrganizationId: String?
39+
var leaveOrganizationResult: Result<Void, Error> = .success(())
3740
var lockVaultUserId: String?
3841
var logoutCalled = false
3942
var logoutUserId: String?
@@ -81,6 +84,11 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
8184
var unlockVaultWithKeyConnectorKeyConnectorURL: URL? // swiftlint:disable:this identifier_name
8285
var unlockVaultWithKeyConnectorOrgIdentifier: String?
8386
var unlockVaultWithKeyConnectorKeyResult: Result<Void, Error> = .success(())
87+
88+
var convertNewUserToKeyConnectorKeyCalled = false
89+
var convertNewUserToKeyConnectorKeyConnectorURL: URL? // swiftlint:disable:this identifier_name
90+
var convertNewUserToKeyConnectorOrgIdentifier: String? // swiftlint:disable:this identifier_name
91+
var convertNewUserToKeyConnectorKeyResult: Result<Void, Error> = .success(())
8492
var unlockVaultWithNeverlockKeyCalled = false
8593
var unlockVaultWithNeverlockResult: Result<Void, Error> = .success(())
8694
var verifyOtpOpt: String?
@@ -138,6 +146,13 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
138146
clearPinsCalled = true
139147
}
140148

149+
func convertNewUserToKeyConnector(keyConnectorURL: URL, orgIdentifier: String) async throws {
150+
convertNewUserToKeyConnectorKeyCalled = true
151+
convertNewUserToKeyConnectorKeyConnectorURL = keyConnectorURL
152+
convertNewUserToKeyConnectorOrgIdentifier = orgIdentifier
153+
try convertNewUserToKeyConnectorKeyResult.get()
154+
}
155+
141156
func createNewSsoUser(orgIdentifier: String, rememberDevice: Bool) async throws {
142157
createNewSsoUserOrgIdentifier = orgIdentifier
143158
createNewSsoUserRememberDevice = rememberDevice
@@ -231,6 +246,12 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
231246
return try passwordStrengthResult.get()
232247
}
233248

249+
func leaveOrganization(organizationId: String) async throws {
250+
leaveOrganizationCalled = true
251+
leaveOrganizationOrganizationId = organizationId
252+
try leaveOrganizationResult.get()
253+
}
254+
234255
func lockVault(userId: String?, isManuallyLocking: Bool) async {
235256
lockVaultUserId = userId
236257
hasManuallyLocked = isManuallyLocking

BitwardenShared/Core/Auth/Services/API/Organization/OrganizationAPIService.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ protocol OrganizationAPIService {
2727
/// - Parameter email: The user's email address
2828
/// - Returns: A `SingleSignOnDomainsVerifiedResponse` with the verified domains list.
2929
func getSingleSignOnVerifiedDomains(email: String) async throws -> SingleSignOnDomainsVerifiedResponse
30+
31+
/// Performs the API request to leave an organization.
32+
///
33+
/// - Parameters:
34+
/// - organizationId: The organization identifier for the organization the user wants to leave.
35+
///
36+
func leaveOrganization(
37+
organizationId: String
38+
) async throws
3039
}
3140

3241
extension APIService: OrganizationAPIService {
@@ -45,4 +54,10 @@ extension APIService: OrganizationAPIService {
4554
func getSingleSignOnVerifiedDomains(email: String) async throws -> SingleSignOnDomainsVerifiedResponse {
4655
try await apiUnauthenticatedService.send(SingleSignOnDomainsVerifiedRequest(email: email))
4756
}
57+
58+
func leaveOrganization(organizationId: String) async throws {
59+
_ = try await apiService.send(
60+
OrganizationLeaveRequest(identifier: organizationId)
61+
)
62+
}
4863
}

BitwardenShared/Core/Auth/Services/API/Organization/OrganizationAPIServiceTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,13 @@ class OrganizationAPIServiceTests: BitwardenTestCase {
9393
)
9494
)
9595
}
96+
97+
/// `leaveOrganization(organizationId:)` successfully runs the leaveOrganization
98+
func test_leaveOrganization() async throws {
99+
client.result = .httpSuccess(testData: .emptyResponse)
100+
101+
await assertAsyncDoesNotThrow {
102+
try await subject.leaveOrganization(organizationId: "ORG_IDENTIFIER")
103+
}
104+
}
96105
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Networking
2+
3+
// MARK: - OrganizationLeaveRequest
4+
5+
/// A networking request to leave an organization.
6+
struct OrganizationLeaveRequest: Request {
7+
typealias Response = EmptyResponse
8+
9+
// MARK: Properties
10+
11+
/// The identifier for the organization.
12+
let identifier: String
13+
14+
/// The HTTP method for this request.
15+
var method: HTTPMethod { .post }
16+
17+
/// The URL path for this request.
18+
var path: String { "/organizations/\(identifier)/leave" }
19+
}

BitwardenShared/Core/Platform/Services/TestHelpers/MockKeyConnectorService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ class MockKeyConnectorService: KeyConnectorService {
66
var convertNewUserToKeyConnectorCalled = false
77
var convertNewUserToKeyConnectorHandler: (() -> Void)?
88
var convertNewUserToKeyConnectorResult: Result<Void, Error> = .success(())
9+
var convertNewUserToKeyConnectorKeyConnectorUrl: URL?
10+
var convertNewUserToKeyConnectorOrganizationId: String?
911

1012
var getManagingOrganizationResult: Result<Organization?, Error> = .success(nil)
1113

@@ -18,6 +20,8 @@ class MockKeyConnectorService: KeyConnectorService {
1820

1921
func convertNewUserToKeyConnector(keyConnectorUrl: URL, orgIdentifier: String) async throws {
2022
convertNewUserToKeyConnectorCalled = true
23+
convertNewUserToKeyConnectorKeyConnectorUrl = keyConnectorUrl
24+
convertNewUserToKeyConnectorOrganizationId = orgIdentifier
2125
convertNewUserToKeyConnectorHandler?()
2226
return try convertNewUserToKeyConnectorResult.get()
2327
}

BitwardenShared/Core/Vault/Services/SyncService.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,13 @@ protocol SyncServiceDelegate: AnyObject {
8181

8282
/// The user needs to remove their master password so they can be migrated to use Key Connector.
8383
///
84-
/// - Parameter organizationName: The organization's name that requires Key Connector.
84+
/// - Parameters:
85+
/// - organizationName: The organization's name that requires Key Connector.
86+
/// - organizationId: The organization's id that requires Key Connector.
87+
/// - keyConnectorUrl: The organization's Key Connector domain.
8588
///
8689
@MainActor
87-
func removeMasterPassword(organizationName: String)
90+
func removeMasterPassword(organizationName: String, organizationId: String, keyConnectorUrl: String)
8891

8992
/// The user's security stamp changed.
9093
///
@@ -223,6 +226,11 @@ class DefaultSyncService: SyncService {
223226
guard !forceSync, let lastSyncTime = try await stateService.getLastSyncTime(userId: userId) else {
224227
return true
225228
}
229+
230+
if try await keyConnectorService.userNeedsMigration() {
231+
return true
232+
}
233+
226234
guard lastSyncTime.addingTimeInterval(Constants.minimumSyncInterval) < timeProvider.presentTime else {
227235
return false
228236
}
@@ -292,8 +300,13 @@ extension DefaultSyncService {
292300
try await checkVaultTimeoutPolicy()
293301

294302
if try await keyConnectorService.userNeedsMigration(),
295-
let organization = try await keyConnectorService.getManagingOrganization() {
296-
await delegate?.removeMasterPassword(organizationName: organization.name)
303+
let organization = try await keyConnectorService.getManagingOrganization(),
304+
let keyConnectorUrl = organization.keyConnectorUrl {
305+
await delegate?.removeMasterPassword(
306+
organizationName: organization.name,
307+
organizationId: organization.id,
308+
keyConnectorUrl: keyConnectorUrl
309+
)
297310
}
298311

299312
await delegate?.onFetchSyncSucceeded(userId: userId)

BitwardenShared/Core/Vault/Services/SyncServiceTests.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class SyncServiceTests: BitwardenTestCase {
208208
let priorSyncDate = Date(year: 2022, month: 1, day: 1)
209209
stateService.lastSyncTimeByUserId["1"] = priorSyncDate
210210
cipherService.replaceCiphersError = BitwardenTestError.example
211+
keyConnectorService.userNeedsMigrationResult = .success(false)
211212

212213
await assertAsyncThrows(error: BitwardenTestError.example) {
213214
try await subject.fetchSync(forceSync: false)
@@ -253,6 +254,7 @@ class SyncServiceTests: BitwardenTestCase {
253254
stateService.lastSyncTimeByUserId["1"] = try XCTUnwrap(
254255
lastSync
255256
)
257+
keyConnectorService.userNeedsMigrationResult = .success(false)
256258

257259
try await subject.fetchSync(forceSync: false)
258260

@@ -274,6 +276,7 @@ class SyncServiceTests: BitwardenTestCase {
274276
client.result = .httpFailure(BitwardenTestError.example)
275277
stateService.activeAccount = .fixture()
276278
stateService.lastSyncTimeByUserId["1"] = lastSyncTime
279+
keyConnectorService.userNeedsMigrationResult = .success(false)
277280

278281
try await subject.fetchSync(forceSync: false)
279282

@@ -296,6 +299,7 @@ class SyncServiceTests: BitwardenTestCase {
296299
stateService.lastSyncTimeByUserId["1"] = try XCTUnwrap(
297300
lastSync
298301
)
302+
keyConnectorService.userNeedsMigrationResult = .success(false)
299303

300304
try await subject.fetchSync(forceSync: false)
301305

@@ -315,6 +319,7 @@ class SyncServiceTests: BitwardenTestCase {
315319
stateService.lastSyncTimeByUserId["1"] = try XCTUnwrap(
316320
timeProvider.presentTime.addingTimeInterval(-(Constants.minimumSyncInterval - 1))
317321
)
322+
keyConnectorService.userNeedsMigrationResult = .success(false)
318323

319324
try await subject.fetchSync(forceSync: false)
320325

@@ -444,14 +449,16 @@ class SyncServiceTests: BitwardenTestCase {
444449
/// Connector.
445450
func test_fetchSync_removeMasterPassword() async throws {
446451
client.result = .httpSuccess(testData: .syncWithProfile)
447-
keyConnectorService.getManagingOrganizationResult = .success(.fixture(name: "Example Org"))
452+
keyConnectorService.getManagingOrganizationResult = .success(
453+
.fixture(keyConnectorUrl: "htttp://example.com/", name: "Example Org"))
448454
keyConnectorService.userNeedsMigrationResult = .success(true)
449455
stateService.activeAccount = .fixture()
450456

451457
try await subject.fetchSync(forceSync: false)
452458

453459
XCTAssertTrue(syncServiceDelegate.removeMasterPasswordCalled)
454460
XCTAssertEqual(syncServiceDelegate.removeMasterPasswordOrganizationName, "Example Org")
461+
XCTAssertEqual(syncServiceDelegate.removeMasterPasswordKeyConnectorUrl, "htttp://example.com/")
455462
}
456463

457464
/// `fetchSync()` throws an error if checking if the user needs to be migrated fails.
@@ -775,13 +782,17 @@ class MockSyncServiceDelegate: SyncServiceDelegate {
775782
var securityStampChangedUserId: String?
776783
var setMasterPasswordCalled = false
777784
var setMasterPasswordOrgId: String?
785+
var removeMasterPasswordOrganizationId: String?
786+
var removeMasterPasswordKeyConnectorUrl: String?
778787

779788
func onFetchSyncSucceeded(userId: String) async {
780789
onFetchSyncSucceededCalledWithuserId = userId
781790
}
782791

783-
func removeMasterPassword(organizationName: String) {
792+
func removeMasterPassword(organizationName: String, organizationId: String, keyConnectorUrl: String) {
784793
removeMasterPasswordOrganizationName = organizationName
794+
removeMasterPasswordOrganizationId = organizationId
795+
removeMasterPasswordKeyConnectorUrl = keyConnectorUrl
785796
removeMasterPasswordCalled = true
786797
}
787798

0 commit comments

Comments
 (0)