Skip to content

Commit

Permalink
[PM-12405] Added new endpoint for Organization SSO verified domains (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
fedemkr authored Nov 13, 2024
1 parent e8a9381 commit 8608a71
Show file tree
Hide file tree
Showing 26 changed files with 430 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation
import Networking

// MARK: - SingleSignOnDomainsVerifiedRequestModel

/// API request model for getting the single sign on verified domains of a user from their email.
///
struct SingleSignOnDomainsVerifiedRequestModel: JSONRequestBody {
// MARK: Properties

/// The email of the user to check for single sign on details of.
let email: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ struct SingleSignOnDetailsResponse: JSONResponse, Equatable {

/// Whether single sign on is available for the user.
let ssoAvailable: Bool

/// The date the domain was verified.
let verifiedDate: Date?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import Networking

// MARK: - SingleSignOnDomainsVerifiedResponse

/// The response returned from the API when requesting the single sign on verified domains.
///
struct SingleSignOnDomainsVerifiedResponse: JSONResponse, Equatable {
// MARK: Types

/// Key names used for encoding and decoding.
enum CodingKeys: String, CodingKey {
case verifiedDomains = "data"
}

// MARK: Properties

/// The verified domains for the organization single sign on.
let verifiedDomains: [SingleSignOnDomainVerifiedDetailResponse]?
}

/// The response returned from the API when requesting the single sign on verified domain for a specific domain.
///
struct SingleSignOnDomainVerifiedDetailResponse: JSONResponse, Equatable {
static let decoder = JSONDecoder.pascalOrSnakeCaseDecoder

// MARK: Properties

/// The domain name.
let domainName: String?

/// The organization identifier for the user.
let organizationIdentifier: String?

/// The organization name.
let organizationName: String?
}
28 changes: 28 additions & 0 deletions BitwardenShared/Core/Auth/Repositories/AuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ protocol AuthRepository: AnyObject {
///
func getFingerprintPhrase() async throws -> String

/// Gets the organization identifier by email for the single sign on process.
/// - Parameter email: The email to get the organization identifier.
/// - Returns: The organization identifier, if any that complies with verified domains. Also `nil` if empty.
func getSingleSignOnOrganizationIdentifier(email: String) async throws -> String?

/// Check if the user has a master password
///
func hasMasterPassword() async throws -> Bool
Expand Down Expand Up @@ -571,6 +576,29 @@ extension DefaultAuthRepository: AuthRepository {
)
}

func getSingleSignOnOrganizationIdentifier(email: String) async throws -> String? {
guard !email.isEmpty else {
return nil
}

guard await configService.getFeatureFlag(.refactorSsoDetailsEndpoint) else {
let response = try await organizationAPIService.getSingleSignOnDetails(email: email)

// If there is already an organization identifier associated with the user's email,
// attempt to start the single sign on process with that identifier.
guard response.ssoAvailable,
response.verifiedDate != nil,
let organizationIdentifier = response.organizationIdentifier,
!organizationIdentifier.isEmpty else {
return nil
}
return organizationIdentifier
}

let verifiedDomainsResponse = try await organizationAPIService.getSingleSignOnVerifiedDomains(email: email)
return verifiedDomainsResponse.verifiedDomains?.first?.organizationIdentifier?.nilIfEmpty
}

func hasMasterPassword() async throws -> Bool {
let account = try await getAccount()
guard let decryptionOptions = account.profile.userDecryptionOptions else { return true }
Expand Down
129 changes: 129 additions & 0 deletions BitwardenShared/Core/Auth/Repositories/AuthRepositoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,135 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
}
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns the organization identifier when
/// the feature flag `.refactorSsoDetailsEndpoint` is off.
func test_getSingleSignOnOrganizationIdentifier_successFeatureFlagOff() async throws {
client.result = .httpSuccess(testData: .singleSignOnDetails)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertEqual(orgId, "TeamLivefront")
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when email is empty.
func test_getSingleSignOnOrganizationIdentifier_emptyEmail() async throws {
client.result = .httpSuccess(testData: .singleSignOnDetails)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when
/// the feature flag `.refactorSsoDetailsEndpoint` is off and SSO not available in the response.
func test_getSingleSignOnOrganizationIdentifier_ssoNotAvailableFeatureFlagOff() async throws {
client.result = .httpSuccess(testData: .singleSignOnDetailsNotAvailable)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when
/// the feature flag `.refactorSsoDetailsEndpoint` is off and no verified date in the response.
func test_getSingleSignOnOrganizationIdentifier_noVerifiedDateFeatureFlagOff() async throws {
client.result = .httpSuccess(testData: .singleSignOnDetailsNoVerifiedDate)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when
/// the feature flag `.refactorSsoDetailsEndpoint` is off and no organization identifier in the response.
func test_getSingleSignOnOrganizationIdentifier_noOrgIdFeatureFlagOff() async throws {
client.result = .httpSuccess(testData: .singleSignOnDetailsNoOrgId)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when
/// the feature flag `.refactorSsoDetailsEndpoint` is off and organization identifier is empty in the response.
func test_getSingleSignOnOrganizationIdentifier_orgIdEmptyFeatureFlagOff() async throws {
client.result = .httpSuccess(testData: .singleSignOnDetailsOrgIdEmpty)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` throws when calling the API
/// and the feature flag `.refactorSsoDetailsEndpoint` is off.
func test_getSingleSignOnOrganizationIdentifier_throwsFeatureFlagOff() async throws {
client.result = .httpFailure(BitwardenTestError.example)

await assertAsyncThrows(error: BitwardenTestError.example) {
_ = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
}
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns the organization identifier when
/// the feature flag `.refactorSsoDetailsEndpoint` is on.
@MainActor
func test_getSingleSignOnOrganizationIdentifier_successFeatureFlagOn() async throws {
configService.featureFlagsBool[.refactorSsoDetailsEndpoint] = true
client.result = .httpSuccess(testData: .singleSignOnDomainsVerified)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertEqual(orgId, "TestID")
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns the first organization identifier when
/// the feature flag `.refactorSsoDetailsEndpoint` is on and there are multiple results in response.
@MainActor
func test_getSingleSignOnOrganizationIdentifier_successInMultipleFeatureFlagOn() async throws {
configService.featureFlagsBool[.refactorSsoDetailsEndpoint] = true
client.result = .httpSuccess(testData: .singleSignOnDomainsVerifiedMultiple)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertEqual(orgId, "TestID")
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when
/// the feature flag `.refactorSsoDetailsEndpoint` is on and there is no data.
@MainActor
func test_getSingleSignOnOrganizationIdentifier_noDataFeatureFlagOn() async throws {
configService.featureFlagsBool[.refactorSsoDetailsEndpoint] = true
client.result = .httpSuccess(testData: .singleSignOnDomainsVerifiedNoData)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when
/// the feature flag `.refactorSsoDetailsEndpoint` is on and data array is empty.
@MainActor
func test_getSingleSignOnOrganizationIdentifier_emptyDataFeatureFlagOn() async throws {
configService.featureFlagsBool[.refactorSsoDetailsEndpoint] = true
client.result = .httpSuccess(testData: .singleSignOnDomainsVerifiedEmptyData)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when
/// the feature flag `.refactorSsoDetailsEndpoint` is on and there is no organization identifier.
@MainActor
func test_getSingleSignOnOrganizationIdentifier_noOrgIdFeatureFlagOn() async throws {
configService.featureFlagsBool[.refactorSsoDetailsEndpoint] = true
client.result = .httpSuccess(testData: .singleSignOnDomainsVerifiedNoOrgId)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` returns `nil` when
/// the feature flag `.refactorSsoDetailsEndpoint` is on and empty organization identifier.
@MainActor
func test_getSingleSignOnOrganizationIdentifier_emptyOrgIdFeatureFlagOn() async throws {
configService.featureFlagsBool[.refactorSsoDetailsEndpoint] = true
client.result = .httpSuccess(testData: .singleSignOnDomainsVerifiedEmptyOrgId)
let orgId = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
XCTAssertNil(orgId)
}

/// `getSingleSignOnOrganizationIdentifier(email:)` throws when calling the API
/// and the feature flag `.refactorSsoDetailsEndpoint` is on.
@MainActor
func test_getSingleSignOnOrganizationIdentifier_throwsFeatureFlagOn() async throws {
configService.featureFlagsBool[.refactorSsoDetailsEndpoint] = true
client.result = .httpFailure(BitwardenTestError.example)

await assertAsyncThrows(error: BitwardenTestError.example) {
_ = try await subject.getSingleSignOnOrganizationIdentifier(email: "foo@bar.com")
}
}

/// `hasMasterPassword` returns if user has masterpassword.
func test_hasMasterPassword_true_normal_user() async throws {
stateService.activeAccount = Account.fixture()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
var activeAccount: Account?
var altAccounts = [Account]()
var getAccountError: Error?
var getSSOOrganizationIdentifierByResult: Result<String?, Error> = .success(nil)
var hasManuallyLocked = false
var hasMasterPasswordResult = Result<Bool, Error>.success(true)
var isLockedResult: Result<Bool, Error> = .success(true)
Expand Down Expand Up @@ -167,6 +168,10 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
)
}

func getSingleSignOnOrganizationIdentifier(email: String) async throws -> String? {
try getSSOOrganizationIdentifierByResult.get()
}

func hasMasterPassword() async throws -> Bool {
try hasMasterPasswordResult.get()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,21 @@ extension APITestData {
resource: "OrganizationAutoEnrollStatusDisabled"
)
static let organizationKeys = loadFromJsonBundle(resource: "OrganizationKeys")
static let singleSignOnDetails = loadFromJsonBundle(resource: "singleSignOnDetails")
static let singleSignOnDetails = loadFromJsonBundle(resource: "SingleSignOnDetails")
static let singleSignOnDetailsNoOrgId = loadFromJsonBundle(resource: "SingleSignOnDetailsNoOrgId")
static let singleSignOnDetailsNotAvailable = loadFromJsonBundle(resource: "SingleSignOnDetailsNotAvailable")
static let singleSignOnDetailsNoVerifiedDate = loadFromJsonBundle(resource: "SingleSignOnDetailsNoVerifiedDate")
static let singleSignOnDetailsOrgIdEmpty = loadFromJsonBundle(resource: "SingleSignOnDetailsOrgIdEmpty")
static let singleSignOnDomainsVerified = loadFromJsonBundle(resource: "SingleSignOnDomainsVerified")
static let singleSignOnDomainsVerifiedEmptyData = loadFromJsonBundle(
resource: "SingleSignOnDomainsVerifiedEmptyData"
)
static let singleSignOnDomainsVerifiedEmptyOrgId = loadFromJsonBundle(
resource: "SingleSignOnDomainsVerifiedEmptyOrgId"
)
static let singleSignOnDomainsVerifiedNoData = loadFromJsonBundle(resource: "SingleSignOnDomainsVerifiedNoData")
static let singleSignOnDomainsVerifiedNoOrgId = loadFromJsonBundle(resource: "SingleSignOnDomainsVerifiedNoOrgId")
static let singleSignOnDomainsVerifiedMultiple = loadFromJsonBundle(
resource: "SingleSignOnDomainsVerifiedMultiple"
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"OrganizationIdentifier": "TeamLivefront",
"SsoAvailable": true,
"VerifiedDate": "2000-01-01T00:00:00.00Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"SsoAvailable": true,
"VerifiedDate": "2000-01-01T00:00:00.00Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@
"OrganizationIdentifier": "TeamLivefront",
"SsoAvailable": true
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"OrganizationIdentifier": "TeamLivefront",
"SsoAvailable": false,
"VerifiedDate": "2000-01-01T00:00:00.00Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"OrganizationIdentifier": "",
"SsoAvailable": true,
"VerifiedDate": "2000-01-01T00:00:00.00Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": [
{
"OrganizationName": "TestName",
"OrganizationIdentifier": "TestID",
"DomainName": "domain"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"data": [
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": [
{
"OrganizationName": "TestName",
"OrganizationIdentifier": "",
"DomainName": "domain"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": [
{
"OrganizationName": "TestName",
"OrganizationIdentifier": "TestID",
"DomainName": "domain"
},
{
"OrganizationName": "Org2",
"OrganizationIdentifier": "TestOrg2",
"DomainName": "domain"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"data": [
{
"OrganizationName": "TestName",
"DomainName": "domain"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ protocol OrganizationAPIService {
/// - Returns: A `SingleSignOnDetailsResponse`.
///
func getSingleSignOnDetails(email: String) async throws -> SingleSignOnDetailsResponse

/// Checks for the verified organization domains of an email for single sign on purposes.
/// - Parameter email: The user's email address
/// - Returns: A `SingleSignOnDomainsVerifiedResponse` with the verified domains list.
func getSingleSignOnVerifiedDomains(email: String) async throws -> SingleSignOnDomainsVerifiedResponse
}

extension APIService: OrganizationAPIService {
Expand All @@ -36,4 +41,8 @@ extension APIService: OrganizationAPIService {
func getSingleSignOnDetails(email: String) async throws -> SingleSignOnDetailsResponse {
try await apiUnauthenticatedService.send(SingleSignOnDetailsRequest(email: email))
}

func getSingleSignOnVerifiedDomains(email: String) async throws -> SingleSignOnDomainsVerifiedResponse {
try await apiUnauthenticatedService.send(SingleSignOnDomainsVerifiedRequest(email: email))
}
}
Loading

0 comments on commit 8608a71

Please sign in to comment.