Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-10401] Implemented SSH Key type handling #1092

Merged
merged 4 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ enum FeatureFlag: String, CaseIterable, Codable {
/// A feature flag for the create account flow.
case nativeCreateAccountFlow = "native-create-account-flow"

case sshKeyVaultItem = "ssh-key-vault-item"

// MARK: Test Flags

/// A test feature flag that isn't remotely configured and has no initial value.
Expand Down Expand Up @@ -81,6 +83,7 @@ enum FeatureFlag: String, CaseIterable, Codable {
.importLoginsFlow,
.nativeCarouselFlow,
.nativeCreateAccountFlow,
.sshKeyVaultItem,
.testLocalFeatureFlag,
.testLocalInitialBoolFlag,
.testLocalInitialIntFlag,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ final class FeatureFlagTests: BitwardenTestCase {
XCTAssertEqual(filtered, [])
}

/// `getter:isRemotelyConfigured` returns the correct value for each flag.
func test_isRemotelyConfigured() {
XCTAssertTrue(FeatureFlag.emailVerification.isRemotelyConfigured)
XCTAssertTrue(FeatureFlag.testRemoteInitialBoolFlag.isRemotelyConfigured)
XCTAssertTrue(FeatureFlag.testRemoteInitialIntFlag.isRemotelyConfigured)
XCTAssertTrue(FeatureFlag.testRemoteInitialStringFlag.isRemotelyConfigured)

XCTAssertFalse(FeatureFlag.enableAuthenticatorSync.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.enableCipherKeyEncryption.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.importLoginsFlow.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.nativeCarouselFlow.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.nativeCreateAccountFlow.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.sshKeyVaultItem.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.testLocalFeatureFlag.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.testLocalInitialBoolFlag.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.testLocalInitialIntFlag.isRemotelyConfigured)
XCTAssertFalse(FeatureFlag.testLocalInitialStringFlag.isRemotelyConfigured)
}

/// `name` formats the raw value of a feature flag
func test_name() {
XCTAssertEqual(FeatureFlag.testLocalFeatureFlag.name, "Test Local Feature Flag")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
}
}

extension AttachmentView: Identifiable {}

Check warning on line 43 in BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift

View workflow job for this annotation

GitHub Actions / Test

extension declares a conformance of imported type 'AttachmentView' to imported protocol 'Identifiable'; this will not behave correctly if the owners of 'BitwardenSdk' introduce this conformance in the future

extension CipherCardModel {
init(card: BitwardenSdk.Card) {
Expand Down Expand Up @@ -330,9 +330,9 @@
}
}

extension BitwardenSdk.CipherListView: Identifiable {}

Check warning on line 333 in BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift

View workflow job for this annotation

GitHub Actions / Test

extension declares a conformance of imported type 'CipherListView' to imported protocol 'Identifiable'; this will not behave correctly if the owners of 'BitwardenSdk' introduce this conformance in the future

extension BitwardenSdk.CipherView: Identifiable {

Check warning on line 335 in BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift

View workflow job for this annotation

GitHub Actions / Test

extension declares a conformance of imported type 'CipherView' to imported protocol 'Identifiable'; this will not behave correctly if the owners of 'BitwardenSdk' introduce this conformance in the future
/// Initializes a new `CipherView` based on a `Fido2CredentialNewView`
/// - Parameters:
/// - fido2CredentialNewView: The `Fido2CredentialNewView` for the Fido2 creation flow
Expand Down Expand Up @@ -390,8 +390,7 @@
case .identity:
self = .identity
case .sshKey:
// TODO: PM-10401 set self = .sshKey when SDK is ready.
self = .init(rawValue: 5)!
self = .sshKey
}
}
}
Expand All @@ -407,7 +406,7 @@
}
}

extension BitwardenSdk.Fido2Credential: Identifiable, @unchecked Sendable {

Check warning on line 409 in BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift

View workflow job for this annotation

GitHub Actions / Test

extension declares a conformance of imported type 'Fido2Credential' to imported protocols 'Identifiable', 'Sendable'; this will not behave correctly if the owners of 'BitwardenSdk' introduce this conformance in the future
public var id: String { credentialId }

init(cipherLoginFido2Credential model: CipherLoginFido2Credential) {
Expand All @@ -429,7 +428,7 @@
}
}

extension BitwardenSdk.Fido2CredentialView: @unchecked Sendable {}

Check warning on line 431 in BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift

View workflow job for this annotation

GitHub Actions / Test

extension declares a conformance of imported type 'Fido2CredentialView' to imported protocol 'Sendable'; this will not behave correctly if the owners of 'BitwardenSdk' introduce this conformance in the future

extension BitwardenSdk.Fido2CredentialAutofillView: @unchecked Sendable {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ import XCTest

@testable import BitwardenShared

// MARK: - BitwardenSdk.CipherType

class BitwardenSdkVaultBitwardenCipherTypeTests: BitwardenTestCase { // swiftlint:disable:this type_name
// MARK: Tests

/// `init(type:)` initializes the SDK cipher type based on the cipher type.
func test_init_byCipherType() {
XCTAssertEqual(BitwardenSdk.CipherType(.login), .login)
XCTAssertEqual(BitwardenSdk.CipherType(.card), .card)
XCTAssertEqual(BitwardenSdk.CipherType(.identity), .identity)
XCTAssertEqual(BitwardenSdk.CipherType(.secureNote), .secureNote)
XCTAssertEqual(BitwardenSdk.CipherType(.sshKey), .sshKey)
}
}

// MARK: - Cipher

class BitwardenSdkVaultCipherTests: BitwardenTestCase {
Expand Down
12 changes: 12 additions & 0 deletions BitwardenShared/Core/Vault/Models/Enum/CipherType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ extension CipherType {
self = .login
case .secureNote:
self = .secureNote
case .sshKey:
self = .sshKey
case .collection,
.folder,
.noFolder,
Expand Down Expand Up @@ -61,4 +63,14 @@ extension CipherType: Menuable {
extension CipherType {
/// These are the cases of `CipherType` that the user can use to create a cipher.
static let canCreateCases: [CipherType] = [.login, .card, .identity, .secureNote]

/// The allowed custom field types per cipher type.
var allowedFieldTypes: [FieldType] {
switch self {
case .card, .identity, .login:
return [.text, .hidden, .boolean, .linked]
case .secureNote, .sshKey:
return [.text, .hidden, .boolean]
}
}
}
11 changes: 11 additions & 0 deletions BitwardenShared/Core/Vault/Models/Enum/CipherTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ import XCTest
class CipherTypeTests: BitwardenTestCase {
// MARK: Tests

/// `getter:allowedFieldTypes` return the correct `FielldType` array for the given cipher type..
func test_allowedFieldTypes() {
XCTAssertEqual(CipherType.login.allowedFieldTypes, [.text, .hidden, .boolean, .linked])
XCTAssertEqual(CipherType.card.allowedFieldTypes, [.text, .hidden, .boolean, .linked])
XCTAssertEqual(CipherType.identity.allowedFieldTypes, [.text, .hidden, .boolean, .linked])
XCTAssertEqual(CipherType.secureNote.allowedFieldTypes, [.text, .hidden, .boolean])
XCTAssertEqual(CipherType.sshKey.allowedFieldTypes, [.text, .hidden, .boolean])
}

/// `localizedName` returns the correct values.
func test_localizedName() {
XCTAssertEqual(CipherType.card.localizedName, Localizations.typeCard)
XCTAssertEqual(CipherType.identity.localizedName, Localizations.typeIdentity)
XCTAssertEqual(CipherType.login.localizedName, Localizations.typeLogin)
XCTAssertEqual(CipherType.secureNote.localizedName, Localizations.typeSecureNote)
XCTAssertEqual(CipherType.sshKey.localizedName, Localizations.sshKey)
}

/// `init` with a `VaultListGroup` produces the correct value.
Expand All @@ -21,6 +31,7 @@ class CipherTypeTests: BitwardenTestCase {
XCTAssertEqual(CipherType(group: .identity), .identity)
XCTAssertEqual(CipherType(group: .login), .login)
XCTAssertEqual(CipherType(group: .secureNote), .secureNote)
XCTAssertEqual(CipherType(group: .sshKey), .sshKey)
XCTAssertNil(CipherType(group: .trash))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ struct CipherRequestModel: JSONRequestBody {
/// Secure note data if the cipher is a secure note.
let secureNote: CipherSecureNoteModel?

/// SSH key data if the cipher is an SSH Key.
let sshKey: CipherSSHKeyModel?

/// The type of the cipher.
let type: CipherType
}
Expand Down Expand Up @@ -85,6 +88,7 @@ extension CipherRequestModel {
passwordHistory: cipher.passwordHistory?.map(CipherPasswordHistoryModel.init),
reprompt: CipherRepromptType(type: cipher.reprompt),
secureNote: cipher.secureNote.map(CipherSecureNoteModel.init),
sshKey: cipher.sshKey.map(CipherSSHKeyModel.init),
type: CipherType(type: cipher.type)
)
}
Expand Down
23 changes: 21 additions & 2 deletions BitwardenShared/Core/Vault/Repositories/VaultRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,11 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length
? { $0.deletedDate == nil }
: { $0.deletedDate != nil }

let isSSHKeyVaultItemEnabled: Bool = await configService.getFeatureFlag(.sshKeyVaultItem)
let sshKeyFilter: (CipherView) -> Bool = { cipher in
cipher.type != .sshKey || isSSHKeyVaultItemEnabled
}

return try await cipherService.ciphersPublisher().asyncTryMap { ciphers -> [CipherView] in
// Convert the Ciphers to CipherViews and filter appropriately.
let matchingCiphers = try await ciphers.asyncMap { cipher in
Expand All @@ -522,6 +527,7 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length
.filter { cipher in
filterType.cipherFilter(cipher) &&
isMatchingCipher(cipher) &&
sshKeyFilter(cipher) &&
(cipherFilter?(cipher) ?? true)
}

Expand Down Expand Up @@ -739,6 +745,8 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length
items = activeCiphers.filter { $0.folderId == nil }.compactMap(VaultListItem.init)
case .secureNote:
items = activeCiphers.filter { $0.type == .secureNote }.compactMap(VaultListItem.init)
case .sshKey:
items = activeCiphers.filter { $0.type == .sshKey }.compactMap(VaultListItem.init)
case .totp:
items = try await totpListItems(from: activeCiphers, filter: filter)
case .trash:
Expand Down Expand Up @@ -830,7 +838,11 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length
.filter(filter.cipherFilter)
.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }

let activeCiphers = ciphers.filter { $0.deletedDate == nil }
let isSSHKeyVaultItemFlagEnabled: Bool = await configService.getFeatureFlag(.sshKeyVaultItem)
let activeCiphers = ciphers.filter { cipher in
cipher.deletedDate == nil
&& (isSSHKeyVaultItemFlagEnabled || cipher.type != .sshKey)
}

let folders = try await clientService.vault().folders()
.decryptList(folders: folders)
Expand Down Expand Up @@ -882,13 +894,18 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length
let typesLoginCount = activeCiphers.lazy.filter { $0.type == .login }.count
let typesSecureNoteCount = activeCiphers.lazy.filter { $0.type == .secureNote }.count

let types = [
var types = [
VaultListItem(id: "Types.Logins", itemType: .group(.login, typesLoginCount)),
VaultListItem(id: "Types.Cards", itemType: .group(.card, typesCardCount)),
VaultListItem(id: "Types.Identities", itemType: .group(.identity, typesIdentityCount)),
VaultListItem(id: "Types.SecureNotes", itemType: .group(.secureNote, typesSecureNoteCount)),
]

if isSSHKeyVaultItemFlagEnabled {
let typesSSHKeyCount = activeCiphers.lazy.filter { $0.type == .sshKey }.count
types.append(VaultListItem(id: "Types.SSHKeys", itemType: .group(.sshKey, typesSSHKeyCount)))
}

return [
VaultListSection(id: "TOTP", items: totpItems, name: Localizations.totp),
VaultListSection(id: "Favorites", items: ciphersFavorites, name: Localizations.favorites),
Expand Down Expand Up @@ -1292,6 +1309,8 @@ extension DefaultVaultRepository: VaultRepository {
return cipher.folderId == nil
case .secureNote:
return cipher.type == .secureNote
case .sshKey:
return cipher.type == .sshKey
case .totp:
return cipher.type == .login
&& cipher.login?.totp != nil
Expand Down
Loading
Loading