Configure iPhone Git commit identity (#76)
This commit is contained in:
@@ -45,3 +45,12 @@ cargo test --workspace
|
|||||||
|
|
||||||
For Apple project changes, also run `xcodegen generate` from `apple/` and build
|
For Apple project changes, also run `xcodegen generate` from `apple/` and build
|
||||||
the affected simulator targets.
|
the affected simulator targets.
|
||||||
|
|
||||||
|
### Simulator biometric validation
|
||||||
|
|
||||||
|
Before testing biometric unlock, enable Biometric Unlock once in the iPhone
|
||||||
|
app so its protected passphrase is enrolled. In Simulator, use **Features >
|
||||||
|
Face ID > Enrolled**, trigger an IronStorage unlock, then choose **Matching
|
||||||
|
Face**. Verify the app changes from locked to unlocked. Use **Non-matching
|
||||||
|
Face** to test rejection, and clear **Enrolled** to test unavailable or changed
|
||||||
|
biometric enrollment.
|
||||||
|
|||||||
@@ -625,6 +625,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func generateEntryEditorPassword(editor: UInt64, length: UInt32?, noSymbols: Bool) throws -> MobileEntryEditorPage
|
func generateEntryEditorPassword(editor: UInt64, length: UInt32?, noSymbols: Bool) throws -> MobileEntryEditorPage
|
||||||
|
|
||||||
|
func gitIdentity() throws -> MobileGitIdentity
|
||||||
|
|
||||||
func manualLock() throws
|
func manualLock() throws
|
||||||
|
|
||||||
func performEntryMutation(request: MobileMutationRequest) throws -> MobileMutationOutcome
|
func performEntryMutation(request: MobileMutationRequest) throws -> MobileMutationOutcome
|
||||||
@@ -643,6 +645,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func setBiometricUnlock(enabled: Bool) throws -> MobileAuthenticationState
|
func setBiometricUnlock(enabled: Bool) throws -> MobileAuthenticationState
|
||||||
|
|
||||||
|
func setGitIdentity(name: String, email: String) throws -> MobileGitIdentity
|
||||||
|
|
||||||
func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64) throws -> MobileTotpDetail
|
func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64) throws -> MobileTotpDetail
|
||||||
|
|
||||||
func state() throws -> MobileAuthenticationState
|
func state() throws -> MobileAuthenticationState
|
||||||
@@ -829,6 +833,15 @@ open func generateEntryEditorPassword(editor: UInt64, length: UInt32?, noSymbols
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func gitIdentity()throws -> MobileGitIdentity {
|
||||||
|
return try FfiConverterTypeMobileGitIdentity_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_git_identity(
|
||||||
|
self.uniffiCloneHandle(),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func manualLock()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
open func manualLock()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(
|
||||||
@@ -925,6 +938,17 @@ open func setBiometricUnlock(enabled: Bool)throws -> MobileAuthenticationState
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func setGitIdentity(name: String, email: String)throws -> MobileGitIdentity {
|
||||||
|
return try FfiConverterTypeMobileGitIdentity_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_set_git_identity(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterString.lower(name),
|
||||||
|
FfiConverterString.lower(email),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64)throws -> MobileTotpDetail {
|
open func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64)throws -> MobileTotpDetail {
|
||||||
return try FfiConverterTypeMobileTotpDetail_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileTotpDetail_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -2437,6 +2461,60 @@ public func FfiConverterTypeMobileEntrySection_lower(_ value: MobileEntrySection
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobileGitIdentity: Equatable, Hashable {
|
||||||
|
public var name: String
|
||||||
|
public var email: String
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(name: String, email: String) {
|
||||||
|
self.name = name
|
||||||
|
self.email = email
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileGitIdentity: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileGitIdentity: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileGitIdentity {
|
||||||
|
return
|
||||||
|
try MobileGitIdentity(
|
||||||
|
name: FfiConverterString.read(from: &buf),
|
||||||
|
email: FfiConverterString.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileGitIdentity, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.name, into: &buf)
|
||||||
|
FfiConverterString.write(value.email, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileGitIdentity_lift(_ buf: RustBuffer) throws -> MobileGitIdentity {
|
||||||
|
return try FfiConverterTypeMobileGitIdentity.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileGitIdentity_lower(_ value: MobileGitIdentity) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileGitIdentity.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct MobileHomeChange: Equatable, Hashable {
|
public struct MobileHomeChange: Equatable, Hashable {
|
||||||
public var id: String
|
public var id: String
|
||||||
public var title: String
|
public var title: String
|
||||||
@@ -6864,6 +6942,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_generate_entry_editor_password() != 34289) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_generate_entry_editor_password() != 34289) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_git_identity() != 29496) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock() != 57220) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock() != 57220) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -6891,6 +6972,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock() != 9486) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock() != 9486) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_git_identity() != 62371) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared() != 57472) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared() != 57472) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,6 +308,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_entry_page(ui
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_generate_entry_editor_password(uint64_t ptr, uint64_t editor, RustBuffer length, int8_t no_symbols, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_generate_entry_editor_password(uint64_t ptr, uint64_t editor, RustBuffer length, int8_t no_symbols, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_GIT_IDENTITY
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_GIT_IDENTITY
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_git_identity(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
||||||
void uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
void uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
@@ -353,6 +358,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_save_entry_ed
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric_unlock(uint64_t ptr, int8_t enabled, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric_unlock(uint64_t ptr, int8_t enabled, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_GIT_IDENTITY
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_GIT_IDENTITY
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_git_identity(uint64_t ptr, RustBuffer name, RustBuffer email, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_totp_watch_shared(uint64_t ptr, RustBuffer path, int8_t shared, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_totp_watch_shared(uint64_t ptr, RustBuffer path, int8_t shared, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
||||||
@@ -990,6 +1000,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_entry_pag
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_GENERATE_ENTRY_EDITOR_PASSWORD
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_GENERATE_ENTRY_EDITOR_PASSWORD
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_generate_entry_editor_password(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_generate_entry_editor_password(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_GIT_IDENTITY
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_GIT_IDENTITY
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_git_identity(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
||||||
@@ -1044,6 +1060,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_save_entr
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_GIT_IDENTITY
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_GIT_IDENTITY
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_git_identity(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
||||||
|
|||||||
@@ -712,6 +712,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
private let authentication: MobileAuthentication?
|
private let authentication: MobileAuthentication?
|
||||||
private let keyTransfer = try? mobileKeyTransfer()
|
private let keyTransfer = try? mobileKeyTransfer()
|
||||||
private var state: MobileAuthenticationState?
|
private var state: MobileAuthenticationState?
|
||||||
|
private var gitIdentity: MobileGitIdentity?
|
||||||
private var preferenceTask: Task<Void, Never>?
|
private var preferenceTask: Task<Void, Never>?
|
||||||
private var loadTask: Task<Void, Never>?
|
private var loadTask: Task<Void, Never>?
|
||||||
private var loadGeneration = 0
|
private var loadGeneration = 0
|
||||||
@@ -753,14 +754,14 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
page.state == .ready ? 3 : 0
|
page.state == .ready ? 4 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
numberOfRowsInSection section: Int
|
numberOfRowsInSection section: Int
|
||||||
) -> Int {
|
) -> Int {
|
||||||
section == 0 ? 2 : (section == 1 ? 1 : 2)
|
section == 1 ? 2 : (section == 3 ? 2 : 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
@@ -768,8 +769,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
titleForHeaderInSection section: Int
|
titleForHeaderInSection section: Int
|
||||||
) -> String? {
|
) -> String? {
|
||||||
switch section {
|
switch section {
|
||||||
case 0: "GPG Key Transfer"
|
case 0: "Git Commit Identity"
|
||||||
case 1: "Secure Unlock"
|
case 1: "GPG Key Transfer"
|
||||||
|
case 2: "Secure Unlock"
|
||||||
default: "Authentication Session"
|
default: "Authentication Session"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,9 +781,12 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
titleForFooterInSection section: Int
|
titleForFooterInSection section: Int
|
||||||
) -> String? {
|
) -> String? {
|
||||||
if section == 0 {
|
if section == 0 {
|
||||||
return "Scan or display ASCII-armored GPG keys. Private-key transfers require explicit confirmation and passphrase validation."
|
return "Used as the author for commits created by the iPhone app."
|
||||||
}
|
}
|
||||||
if section == 1 {
|
if section == 1 {
|
||||||
|
return "Scan or display ASCII-armored GPG keys. Private-key transfers require explicit confirmation and passphrase validation."
|
||||||
|
}
|
||||||
|
if section == 2 {
|
||||||
return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
|
return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
|
||||||
}
|
}
|
||||||
return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
|
return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
|
||||||
@@ -794,6 +799,13 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||||
var content = cell.defaultContentConfiguration()
|
var content = cell.defaultContentConfiguration()
|
||||||
if indexPath.section == 0 {
|
if indexPath.section == 0 {
|
||||||
|
content.image = UIImage(systemName: "person.crop.circle")
|
||||||
|
content.text = gitIdentity?.name ?? "Git Commit Identity"
|
||||||
|
content.secondaryText = gitIdentity?.email ?? "Set the commit author name and email"
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
cell.isUserInteractionEnabled = authentication != nil
|
||||||
|
cell.contentView.alpha = authentication == nil ? 0.45 : 1
|
||||||
|
} else if indexPath.section == 1 {
|
||||||
let importing = indexPath.row == 0
|
let importing = indexPath.row == 0
|
||||||
content.image = UIImage(systemName: importing ? "qrcode.viewfinder" : "qrcode")
|
content.image = UIImage(systemName: importing ? "qrcode.viewfinder" : "qrcode")
|
||||||
content.text = importing ? "Import GPG Key" : "Export GPG Key"
|
content.text = importing ? "Import GPG Key" : "Export GPG Key"
|
||||||
@@ -803,7 +815,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
cell.accessoryType = .disclosureIndicator
|
cell.accessoryType = .disclosureIndicator
|
||||||
cell.isUserInteractionEnabled = keyTransfer != nil
|
cell.isUserInteractionEnabled = keyTransfer != nil
|
||||||
cell.contentView.alpha = keyTransfer == nil ? 0.45 : 1
|
cell.contentView.alpha = keyTransfer == nil ? 0.45 : 1
|
||||||
} else if indexPath.section == 1 {
|
} else if indexPath.section == 2 {
|
||||||
content.image = UIImage(systemName: "faceid")
|
content.image = UIImage(systemName: "faceid")
|
||||||
content.text = "Biometric Unlock"
|
content.text = "Biometric Unlock"
|
||||||
content.secondaryText = state?.biometricUnlockEnabled == true
|
content.secondaryText = state?.biometricUnlockEnabled == true
|
||||||
@@ -841,6 +853,10 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
if indexPath.section == 0 {
|
if indexPath.section == 0 {
|
||||||
|
editGitIdentity()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if indexPath.section == 1 {
|
||||||
guard let keyTransfer else { return }
|
guard let keyTransfer else { return }
|
||||||
if indexPath.row == 0 {
|
if indexPath.row == 0 {
|
||||||
requestKeyScanner(keyTransfer)
|
requestKeyScanner(keyTransfer)
|
||||||
@@ -852,7 +868,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard indexPath.section == 2, indexPath.row == 1, let authentication else { return }
|
guard indexPath.section == 3, indexPath.row == 1, let authentication else { return }
|
||||||
do {
|
do {
|
||||||
try authentication.manualLock()
|
try authentication.manualLock()
|
||||||
refreshState()
|
refreshState()
|
||||||
@@ -970,6 +986,81 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
present(alert, animated: true)
|
present(alert, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func editGitIdentity() {
|
||||||
|
guard let authentication else {
|
||||||
|
presentAuthenticationFailure(.unavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "Git Commit Identity",
|
||||||
|
message: "Enter the author used for commits created on this iPhone.",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addTextField { [gitIdentity] field in
|
||||||
|
field.placeholder = "Name"
|
||||||
|
field.text = gitIdentity?.name
|
||||||
|
field.textContentType = .name
|
||||||
|
field.autocapitalizationType = .words
|
||||||
|
field.clearButtonMode = .whileEditing
|
||||||
|
}
|
||||||
|
alert.addTextField { [gitIdentity] field in
|
||||||
|
field.placeholder = "Email"
|
||||||
|
field.text = gitIdentity?.email
|
||||||
|
field.textContentType = .emailAddress
|
||||||
|
field.keyboardType = .emailAddress
|
||||||
|
field.autocapitalizationType = .none
|
||||||
|
field.autocorrectionType = .no
|
||||||
|
field.clearButtonMode = .whileEditing
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Save", style: .default) { [weak self, weak alert] _ in
|
||||||
|
guard
|
||||||
|
let self,
|
||||||
|
let fields = alert?.textFields,
|
||||||
|
fields.count == 2
|
||||||
|
else { return }
|
||||||
|
saveGitIdentity(
|
||||||
|
authentication,
|
||||||
|
name: fields[0].text ?? "",
|
||||||
|
email: fields[1].text ?? ""
|
||||||
|
)
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveGitIdentity(
|
||||||
|
_ authentication: MobileAuthentication,
|
||||||
|
name: String,
|
||||||
|
email: String
|
||||||
|
) {
|
||||||
|
preferenceTask?.cancel()
|
||||||
|
preferenceTask = Task { [weak self] in
|
||||||
|
let result = await Task.detached(priority: .userInitiated) {
|
||||||
|
do {
|
||||||
|
return Result<MobileGitIdentity, AuthenticationFailure>.success(
|
||||||
|
try authentication.setGitIdentity(name: name, email: email)
|
||||||
|
)
|
||||||
|
} catch let error as MobileAuthenticationFfiError {
|
||||||
|
return .failure(AuthenticationFailure(error))
|
||||||
|
} catch {
|
||||||
|
return .failure(.unexpected)
|
||||||
|
}
|
||||||
|
}.value
|
||||||
|
guard !Task.isCancelled, let self else { return }
|
||||||
|
switch result {
|
||||||
|
case let .success(identity):
|
||||||
|
gitIdentity = identity
|
||||||
|
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
|
||||||
|
UIAccessibility.post(
|
||||||
|
notification: .announcement,
|
||||||
|
argument: "Git commit identity updated"
|
||||||
|
)
|
||||||
|
case let .failure(failure):
|
||||||
|
presentAuthenticationFailure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func authenticationDidChange() {
|
@objc private func authenticationDidChange() {
|
||||||
refreshState()
|
refreshState()
|
||||||
}
|
}
|
||||||
@@ -1014,6 +1105,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
|
|
||||||
private func refreshState() {
|
private func refreshState() {
|
||||||
state = try? authentication?.state()
|
state = try? authentication?.state()
|
||||||
|
gitIdentity = try? authentication?.gitIdentity()
|
||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,6 +567,12 @@ pub struct MobileAuthenticationState {
|
|||||||
pub remaining_seconds: u64,
|
pub remaining_seconds: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobileGitIdentity {
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
pub enum MobileMutationAction {
|
pub enum MobileMutationAction {
|
||||||
Move,
|
Move,
|
||||||
@@ -1328,6 +1334,30 @@ impl MobileAuthentication {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_identity(&self) -> Result<MobileGitIdentity, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.git_identity()
|
||||||
|
.map(|identity| MobileGitIdentity {
|
||||||
|
name: identity.name().to_owned(),
|
||||||
|
email: identity.email().to_owned(),
|
||||||
|
})
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_git_identity(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
) -> Result<MobileGitIdentity, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.set_git_identity(name, email)
|
||||||
|
.map(|identity| MobileGitIdentity {
|
||||||
|
name: identity.name().to_owned(),
|
||||||
|
email: identity.email().to_owned(),
|
||||||
|
})
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unlock_entry(
|
pub fn unlock_entry(
|
||||||
&self,
|
&self,
|
||||||
path: String,
|
path: String,
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ use cap_tempfile::TempFile;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::authentication::{AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT};
|
use crate::{
|
||||||
use crate::mobile::MobileTab;
|
authentication::{AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT},
|
||||||
use crate::presentation::{ClipboardTimeout, DEFAULT_CLIPBOARD_TIMEOUT};
|
git::GitIdentity,
|
||||||
use crate::repository::EntryPath;
|
mobile::MobileTab,
|
||||||
|
presentation::{ClipboardTimeout, DEFAULT_CLIPBOARD_TIMEOUT},
|
||||||
|
repository::EntryPath,
|
||||||
|
};
|
||||||
|
|
||||||
const APPLICATION_DIRECTORY: &str = "ironstorage";
|
const APPLICATION_DIRECTORY: &str = "ironstorage";
|
||||||
const CONFIG_FILE: &str = "config.toml";
|
const CONFIG_FILE: &str = "config.toml";
|
||||||
@@ -40,6 +43,7 @@ pub struct Config {
|
|||||||
mobile_tab: MobileTab,
|
mobile_tab: MobileTab,
|
||||||
mobile_home_refreshed_at: Option<i64>,
|
mobile_home_refreshed_at: Option<i64>,
|
||||||
watch_shared_totp_entries: BTreeSet<EntryPath>,
|
watch_shared_totp_entries: BTreeSet<EntryPath>,
|
||||||
|
git_identity: GitIdentity,
|
||||||
git_remotes: Vec<GitRemote>,
|
git_remotes: Vec<GitRemote>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +149,10 @@ impl Config {
|
|||||||
&self.git_remotes
|
&self.git_remotes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_identity(&self) -> &GitIdentity {
|
||||||
|
&self.git_identity
|
||||||
|
}
|
||||||
|
|
||||||
pub fn settings(&self) -> ConfigSettings {
|
pub fn settings(&self) -> ConfigSettings {
|
||||||
let editor = self.editor.as_ref().map(|editor| {
|
let editor = self.editor.as_ref().map(|editor| {
|
||||||
std::iter::once(editor.program.clone())
|
std::iter::once(editor.program.clone())
|
||||||
@@ -272,6 +280,35 @@ impl Config {
|
|||||||
validate_config(self.source.clone(), document, raw)?.persist()
|
validate_config(self.source.clone(), document, raw)?.persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_git_identity(&self, identity: &GitIdentity) -> Result<(), ConfigError> {
|
||||||
|
let mut document = self.current_document()?;
|
||||||
|
let root = document
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or_else(|| ConfigError::Malformed {
|
||||||
|
path: self.source.clone(),
|
||||||
|
})?;
|
||||||
|
let git = root
|
||||||
|
.entry("git")
|
||||||
|
.or_insert_with(|| toml::Value::Table(toml::Table::new()))
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or(ConfigError::InvalidField { field: "git" })?;
|
||||||
|
git.insert(
|
||||||
|
"user_name".to_owned(),
|
||||||
|
toml::Value::String(identity.name().to_owned()),
|
||||||
|
);
|
||||||
|
git.insert(
|
||||||
|
"user_email".to_owned(),
|
||||||
|
toml::Value::String(identity.email().to_owned()),
|
||||||
|
);
|
||||||
|
let raw = document
|
||||||
|
.clone()
|
||||||
|
.try_into::<RawConfig>()
|
||||||
|
.map_err(|_| ConfigError::Malformed {
|
||||||
|
path: self.source.clone(),
|
||||||
|
})?;
|
||||||
|
validate_config(self.source.clone(), document, raw)?.persist()
|
||||||
|
}
|
||||||
|
|
||||||
fn current_document(&self) -> Result<toml::Value, ConfigError> {
|
fn current_document(&self) -> Result<toml::Value, ConfigError> {
|
||||||
Self::load(Some(&self.source)).map(|config| config.document)
|
Self::load(Some(&self.source)).map(|config| config.document)
|
||||||
}
|
}
|
||||||
@@ -930,6 +967,8 @@ enum RawEditor {
|
|||||||
#[derive(Default, Deserialize)]
|
#[derive(Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct RawGit {
|
struct RawGit {
|
||||||
|
user_name: Option<String>,
|
||||||
|
user_email: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
remotes: Vec<RawGitRemote>,
|
remotes: Vec<RawGitRemote>,
|
||||||
}
|
}
|
||||||
@@ -1019,6 +1058,19 @@ fn validate_config(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<BTreeSet<_>, _>>()?;
|
.collect::<Result<BTreeSet<_>, _>>()?;
|
||||||
|
let git_identity = match (raw.git.user_name, raw.git.user_email) {
|
||||||
|
(None, None) => GitIdentity::ironstorage(),
|
||||||
|
(Some(name), Some(email)) => {
|
||||||
|
GitIdentity::new(name, email).map_err(|_| ConfigError::InvalidField {
|
||||||
|
field: "git.user_identity",
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(ConfigError::InvalidField {
|
||||||
|
field: "git.user_identity",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
let git_remotes = validate_remotes(raw.git.remotes)?;
|
let git_remotes = validate_remotes(raw.git.remotes)?;
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
@@ -1034,6 +1086,7 @@ fn validate_config(
|
|||||||
mobile_tab,
|
mobile_tab,
|
||||||
mobile_home_refreshed_at,
|
mobile_home_refreshed_at,
|
||||||
watch_shared_totp_entries,
|
watch_shared_totp_entries,
|
||||||
|
git_identity,
|
||||||
git_remotes,
|
git_remotes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1228,7 +1281,7 @@ fn validate_known_fields(value: &toml::Value, source: &Path) -> Result<(), Confi
|
|||||||
let git = git.as_table().ok_or_else(|| ConfigError::Malformed {
|
let git = git.as_table().ok_or_else(|| ConfigError::Malformed {
|
||||||
path: source.to_owned(),
|
path: source.to_owned(),
|
||||||
})?;
|
})?;
|
||||||
validate_table(git, "git", &["remotes"])?;
|
validate_table(git, "git", &["user_name", "user_email", "remotes"])?;
|
||||||
if let Some(remotes) = git.get("remotes") {
|
if let Some(remotes) = git.get("remotes") {
|
||||||
let remotes = remotes.as_array().ok_or_else(|| ConfigError::Malformed {
|
let remotes = remotes.as_array().ok_or_else(|| ConfigError::Malformed {
|
||||||
path: source.to_owned(),
|
path: source.to_owned(),
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ struct ActiveMobileLease {
|
|||||||
|
|
||||||
struct MobileAuthenticationStatus {
|
struct MobileAuthenticationStatus {
|
||||||
biometric_unlock_enabled: bool,
|
biometric_unlock_enabled: bool,
|
||||||
|
git_identity: GitIdentity,
|
||||||
active: Option<ActiveMobileLease>,
|
active: Option<ActiveMobileLease>,
|
||||||
next_editor_id: u64,
|
next_editor_id: u64,
|
||||||
editors: BTreeMap<u64, MobileEntryDraft>,
|
editors: BTreeMap<u64, MobileEntryDraft>,
|
||||||
@@ -226,6 +227,7 @@ impl MobileAuthentication {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
status: Mutex::new(MobileAuthenticationStatus {
|
status: Mutex::new(MobileAuthenticationStatus {
|
||||||
biometric_unlock_enabled: config.biometric_unlock_enabled(),
|
biometric_unlock_enabled: config.biometric_unlock_enabled(),
|
||||||
|
git_identity: config.git_identity().clone(),
|
||||||
active: None,
|
active: None,
|
||||||
next_editor_id: 0,
|
next_editor_id: 0,
|
||||||
editors: BTreeMap::new(),
|
editors: BTreeMap::new(),
|
||||||
@@ -394,6 +396,29 @@ impl MobileAuthentication {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_identity(&self) -> Result<GitIdentity, MobileAuthenticationError> {
|
||||||
|
Ok(self.status()?.git_identity.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_git_identity(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
) -> Result<GitIdentity, MobileAuthenticationError> {
|
||||||
|
let identity = GitIdentity::new(name, email).map_err(|_| {
|
||||||
|
MobileAuthenticationError::new(
|
||||||
|
MobileAuthenticationErrorKind::Configuration,
|
||||||
|
"Git Identity Is Invalid",
|
||||||
|
"Enter a non-empty name and email without line breaks or angle brackets.",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
self.config
|
||||||
|
.update_git_identity(&identity)
|
||||||
|
.map_err(config_error)?;
|
||||||
|
self.status()?.git_identity = identity.clone();
|
||||||
|
Ok(identity)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn touch_user_activity(&self) -> Result<(), MobileAuthenticationError> {
|
pub fn touch_user_activity(&self) -> Result<(), MobileAuthenticationError> {
|
||||||
let status = self.status()?;
|
let status = self.status()?;
|
||||||
let active = status.active.as_ref().ok_or_else(|| {
|
let active = status.active.as_ref().ok_or_else(|| {
|
||||||
@@ -483,16 +508,17 @@ impl MobileAuthentication {
|
|||||||
&self,
|
&self,
|
||||||
request: MobileMutationRequest,
|
request: MobileMutationRequest,
|
||||||
) -> Result<MobileMutationOutcome, MobileAuthenticationError> {
|
) -> Result<MobileMutationOutcome, MobileAuthenticationError> {
|
||||||
let (handle, key) = {
|
let (handle, key, identity) = {
|
||||||
let status = self.status()?;
|
let status = self.status()?;
|
||||||
let active = status.active.as_ref().ok_or_else(locked_error)?;
|
let active = status.active.as_ref().ok_or_else(locked_error)?;
|
||||||
(active.handle.clone(), active.key.clone())
|
(
|
||||||
};
|
active.handle.clone(),
|
||||||
let mut committer = AutomaticTreeCommitter::for_source(
|
active.key.clone(),
|
||||||
&self.repository,
|
status.git_identity.clone(),
|
||||||
&request.source,
|
|
||||||
GitIdentity::ironstorage(),
|
|
||||||
)
|
)
|
||||||
|
};
|
||||||
|
let mut committer =
|
||||||
|
AutomaticTreeCommitter::for_source(&self.repository, &request.source, identity)
|
||||||
.map_err(git_mutation_error)?;
|
.map_err(git_mutation_error)?;
|
||||||
let has_open_editor = self.editor_state(&request.source)?.0;
|
let has_open_editor = self.editor_state(&request.source)?.0;
|
||||||
let editors = if has_open_editor && request.discard_editor {
|
let editors = if has_open_editor && request.discard_editor {
|
||||||
@@ -632,8 +658,8 @@ impl MobileAuthentication {
|
|||||||
document
|
document
|
||||||
.replace_field_value(EntryFieldId::from_value(field), value.into_bytes())
|
.replace_field_value(EntryFieldId::from_value(field), value.into_bytes())
|
||||||
.map_err(document_error)?;
|
.map_err(document_error)?;
|
||||||
let mut committer =
|
let identity = self.git_identity()?;
|
||||||
AutomaticEntryCommitter::for_entry(&self.repository, path, GitIdentity::ironstorage())
|
let mut committer = AutomaticEntryCommitter::for_entry(&self.repository, path, identity)
|
||||||
.map_err(|error| entry_detail("Password Entry Could Not Be Saved", error))?;
|
.map_err(|error| entry_detail("Password Entry Could Not Be Saved", error))?;
|
||||||
EntryDocumentService::new(&self.repository, &self.keys)
|
EntryDocumentService::new(&self.repository, &self.keys)
|
||||||
.save_recoverable(&document, None, &mut committer)
|
.save_recoverable(&document, None, &mut committer)
|
||||||
@@ -795,11 +821,9 @@ impl MobileAuthentication {
|
|||||||
return Err(editor_error(error));
|
return Err(editor_error(error));
|
||||||
}
|
}
|
||||||
let path = draft.document().path().to_string();
|
let path = draft.document().path().to_string();
|
||||||
let mut committer = match AutomaticEntryCommitter::for_entry(
|
let identity = self.git_identity()?;
|
||||||
&self.repository,
|
let mut committer =
|
||||||
&path,
|
match AutomaticEntryCommitter::for_entry(&self.repository, &path, identity) {
|
||||||
GitIdentity::ironstorage(),
|
|
||||||
) {
|
|
||||||
Ok(committer) => committer,
|
Ok(committer) => committer,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
self.restore_editor(editor, draft)?;
|
self.restore_editor(editor, draft)?;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use std::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, ConfigError, GitRemote},
|
config::{Config, ConfigError, GitRemote},
|
||||||
git::{
|
git::{
|
||||||
GitChangeKind, GitCommitActivity, GitDivergence, GitError, GitIdentity,
|
GitChangeKind, GitCommitActivity, GitDivergence, GitError, GitOperationControl,
|
||||||
GitOperationControl, GitProgressPhase, GitRepository, PullOutcome,
|
GitProgressPhase, GitRepository, PullOutcome,
|
||||||
},
|
},
|
||||||
repository::{Repository, RepositoryError},
|
repository::{Repository, RepositoryError},
|
||||||
secret_store::{
|
secret_store::{
|
||||||
@@ -343,7 +343,7 @@ impl MobileHomeStorage {
|
|||||||
let config = Config::load(None).map_err(MobileHomeError::from_config)?;
|
let config = Config::load(None).map_err(MobileHomeError::from_config)?;
|
||||||
let repository =
|
let repository =
|
||||||
Repository::open(config.vault()).map_err(MobileHomeError::from_repository)?;
|
Repository::open(config.vault()).map_err(MobileHomeError::from_repository)?;
|
||||||
let git = GitRepository::open(&repository, GitIdentity::ironstorage())
|
let git = GitRepository::open(&repository, config.git_identity().clone())
|
||||||
.map_err(MobileHomeError::from_git)?;
|
.map_err(MobileHomeError::from_git)?;
|
||||||
let remote = config
|
let remote = config
|
||||||
.git_remote(None)
|
.git_remote(None)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use ironstorage::{
|
|||||||
authentication::{DEFAULT_AUTHENTICATION_TIMEOUT, MAX_AUTHENTICATION_TIMEOUT},
|
authentication::{DEFAULT_AUTHENTICATION_TIMEOUT, MAX_AUTHENTICATION_TIMEOUT},
|
||||||
config::{ConfigError, ConfigLoader, EditorSource},
|
config::{ConfigError, ConfigLoader, EditorSource},
|
||||||
desktop::DesktopStorage,
|
desktop::DesktopStorage,
|
||||||
|
git::GitIdentity,
|
||||||
mobile::MobileTab,
|
mobile::MobileTab,
|
||||||
repository::EntryPath,
|
repository::EntryPath,
|
||||||
};
|
};
|
||||||
@@ -98,6 +99,38 @@ fn explicit_relative_configuration_resolves_deterministically() -> TestResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn git_commit_identity_defaults_validates_and_persists() -> TestResult {
|
||||||
|
let fixture = ConfigurationFixture::new()?;
|
||||||
|
fixture.write_explicit(fixture.valid_contents())?;
|
||||||
|
let config = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||||
|
assert_eq!(config.git_identity(), &GitIdentity::ironstorage());
|
||||||
|
|
||||||
|
let identity = GitIdentity::new("Alice Example", "alice@example.test")?;
|
||||||
|
config.update_git_identity(&identity)?;
|
||||||
|
let reloaded = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||||
|
assert_eq!(reloaded.git_identity(), &identity);
|
||||||
|
assert_eq!(reloaded.git_remotes(), config.git_remotes());
|
||||||
|
let contents = fs::read_to_string(fixture.explicit_path())?;
|
||||||
|
assert!(contents.contains("user_name = \"Alice Example\""));
|
||||||
|
assert!(contents.contains("user_email = \"alice@example.test\""));
|
||||||
|
|
||||||
|
fixture.write_explicit(&fixture.valid_contents().replace(
|
||||||
|
"[[git.remotes]]",
|
||||||
|
"[git]\nuser_name = \"Alice Example\"\n\n[[git.remotes]]",
|
||||||
|
))?;
|
||||||
|
assert_eq!(
|
||||||
|
fixture
|
||||||
|
.loader()
|
||||||
|
.load(Some(&fixture.explicit_path()))
|
||||||
|
.expect_err("partial Git identity must be rejected"),
|
||||||
|
ConfigError::InvalidField {
|
||||||
|
field: "git.user_identity"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn watch_totp_selection_persists_only_in_application_configuration() -> TestResult {
|
fn watch_totp_selection_persists_only_in_application_configuration() -> TestResult {
|
||||||
let fixture = ConfigurationFixture::new()?;
|
let fixture = ConfigurationFixture::new()?;
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ clipboard_timeout_seconds = 45
|
|||||||
[security]
|
[security]
|
||||||
inactivity_timeout_seconds = 120
|
inactivity_timeout_seconds = 120
|
||||||
|
|
||||||
|
[git]
|
||||||
|
user_name = "Alice Example"
|
||||||
|
user_email = "alice@example.test"
|
||||||
|
|
||||||
[[git.remotes]]
|
[[git.remotes]]
|
||||||
name = "origin"
|
name = "origin"
|
||||||
url = "https://git.example.test/alice/password-store.git"
|
url = "https://git.example.test/alice/password-store.git"
|
||||||
@@ -49,6 +53,10 @@ Commands are split into an executable and argument vector and are never passed
|
|||||||
through a shell. Only the CLI `edit` adapter may eventually launch this
|
through a shell. Only the CLI `edit` adapter may eventually launch this
|
||||||
configured editor.
|
configured editor.
|
||||||
|
|
||||||
|
`git.user_name` and `git.user_email` set the author for commits created by
|
||||||
|
IronStorage. Both must be present together and cannot contain line breaks or
|
||||||
|
angle brackets. The built-in IronStorage identity is used when both are absent.
|
||||||
|
|
||||||
Git remotes are HTTPS-only. URLs containing user information, passwords,
|
Git remotes are HTTPS-only. URLs containing user information, passwords,
|
||||||
queries, or fragments are rejected. `server_id` and `application_id` are opaque
|
queries, or fragments are rejected. `server_id` and `application_id` are opaque
|
||||||
references used to retrieve credentials from the operating-system secret
|
references used to retrieve credentials from the operating-system secret
|
||||||
|
|||||||
Reference in New Issue
Block a user