Remove handling account ID in app, prevent changing settings when signed in (#2804)

This PR

  - Addresses the Apple clients part of #2791 
  - Fixes #2668

The Settings UI change of separating Logs and Advanced settings is not
part of this PR.
This commit is contained in:
Roopesh Chander
2023-12-07 18:45:42 +05:30
committed by GitHub
parent 6d1c962c83
commit e9cd4623fd
12 changed files with 223 additions and 316 deletions

View File

@@ -19,15 +19,8 @@ final class AuthViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
func signInButtonTapped() async {
guard let accountId = authStore.tunnelStore.tunnelAuthStatus.accountId(),
!accountId.isEmpty
else {
settingsUndefined()
return
}
do {
try await authStore.signIn(accountId: accountId)
try await authStore.signIn()
} catch {
dump(error)
}

View File

@@ -60,7 +60,9 @@ import SwiftUI
}
func signOutButtonTapped() {
appStore.auth.signOut()
Task {
await appStore.auth.signOut()
}
}
func startTunnel() async {
@@ -88,7 +90,7 @@ import SwiftUI
Section(header: Text("Authentication")) {
Group {
switch self.model.loginStatus {
case .signedIn(_, let actorName):
case .signedIn(let actorName):
HStack {
Text(actorName.isEmpty ? "Signed in" : "Signed in as")
Spacer()

View File

@@ -20,14 +20,16 @@ public final class SettingsViewModel: ObservableObject {
@Dependency(\.authStore) private var authStore
@Published var accountSettings: AccountSettings
var tunnelAuthStatus: TunnelAuthStatus {
authStore.tunnelStore.tunnelAuthStatus
}
@Published var advancedSettings: AdvancedSettings
public var onSettingsSaved: () -> Void = unimplemented()
private var cancellables = Set<AnyCancellable>()
public init() {
accountSettings = AccountSettings()
advancedSettings = AdvancedSettings.defaultValue
loadSettings()
}
@@ -35,11 +37,10 @@ public final class SettingsViewModel: ObservableObject {
func loadSettings() {
Task {
authStore.tunnelStore.$tunnelAuthStatus
.filter { $0.isInitialized }
.first { $0.isInitialized }
.receive(on: RunLoop.main)
.sink { [weak self] tunnelAuthStatus in
guard let self = self else { return }
self.accountSettings = AccountSettings(accountId: tunnelAuthStatus.accountId() ?? "")
self.advancedSettings =
authStore.tunnelStore.advancedSettings() ?? AdvancedSettings.defaultValue
}
@@ -47,27 +48,22 @@ public final class SettingsViewModel: ObservableObject {
}
}
func saveAccountSettings() {
Task {
let accountId = await authStore.loginStatus.accountId
if accountId == accountSettings.accountId {
// Not changed
await MainActor.run {
accountSettings.isSavedToDisk = true
}
return
}
try await updateTunnelAuthStatus(accountId: accountSettings.accountId)
await MainActor.run {
accountSettings.isSavedToDisk = true
}
}
}
func saveAdvancedSettings() {
let isChanged = (authStore.tunnelStore.advancedSettings() != advancedSettings)
guard isChanged else {
advancedSettings.isSavedToDisk = true
return
}
Task {
guard let authBaseURL = URL(string: advancedSettings.authBaseURLString) else {
fatalError("Saved authBaseURL is invalid")
if case .signedIn = self.tunnelAuthStatus {
await authStore.signOut()
}
let authBaseURLString = advancedSettings.authBaseURLString
guard URL(string: authBaseURLString) != nil else {
logger.error(
"Not saving advanced settings because authBaseURL '\(authBaseURLString, privacy: .public)' is invalid"
)
return
}
do {
try await authStore.tunnelStore.saveAdvancedSettings(advancedSettings)
@@ -77,30 +73,6 @@ public final class SettingsViewModel: ObservableObject {
await MainActor.run {
advancedSettings.isSavedToDisk = true
}
var isChanged = false
if isChanged {
try await updateTunnelAuthStatus(
accountId: authStore.tunnelStore.tunnelAuthStatus.accountId() ?? "")
}
}
}
// updateTunnelAuthStatus:
// When the authBaseURL or the accountId changes, we should update the signed-in-ness.
// This is done by searching the keychain for an entry with the authBaseURL+accountId
// combination. If an entry was found, we consider that entry to mean we're logged in.
func updateTunnelAuthStatus(accountId: String) async throws {
let tunnelAuthStatus: TunnelAuthStatus = await {
if accountId.isEmpty {
return .accountNotSetup
} else {
return await authStore.tunnelAuthStatusForAccount(accountId: accountId)
}
}()
do {
try await authStore.tunnelStore.saveAuthStatus(tunnelAuthStatus)
} catch {
logger.error("Error saving auth status to tunnel store: \(error, privacy: .public)")
}
}
}
@@ -111,7 +83,26 @@ public struct SettingsView: View {
@ObservedObject var model: SettingsViewModel
@Environment(\.dismiss) var dismiss
enum ConfirmationAlertContinueAction: Int {
case none
case saveAdvancedSettings
case saveAllSettingsAndDismiss
func performAction(on view: SettingsView) {
switch self {
case .none:
break
case .saveAdvancedSettings:
view.saveAdvancedSettings()
case .saveAllSettingsAndDismiss:
view.saveAllSettingsAndDismiss()
}
}
}
@State private var isExportingLogs = false
@State private var isShowingConfirmationAlert = false
@State private var confirmationAlertContinueAction: ConfirmationAlertContinueAction = .none
#if os(iOS)
@State private var logTempZipFileURL: URL?
@@ -142,13 +133,6 @@ public struct SettingsView: View {
#if os(iOS)
NavigationView {
TabView {
accountTab
.tabItem {
Image(systemName: "person.3.fill")
Text("Account")
}
.badge(model.accountSettings.isValid ? nil : "!")
advancedTab
.tabItem {
Image(systemName: "slider.horizontal.3")
@@ -159,12 +143,16 @@ public struct SettingsView: View {
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
self.saveSettings()
let action = ConfirmationAlertContinueAction.saveAllSettingsAndDismiss
if case .signedIn = model.tunnelAuthStatus {
self.confirmationAlertContinueAction = action
self.isShowingConfirmationAlert = true
} else {
action.performAction(on: self)
}
}
.disabled(
(model.accountSettings.isSavedToDisk && model.advancedSettings.isSavedToDisk)
|| !model.accountSettings.isValid
|| !model.advancedSettings.isValid
(model.advancedSettings.isSavedToDisk || !model.advancedSettings.isValid)
)
}
ToolbarItem(placement: .navigationBarLeading) {
@@ -176,13 +164,26 @@ public struct SettingsView: View {
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
}
.alert(
"Saving settings will sign you out",
isPresented: $isShowingConfirmationAlert,
presenting: confirmationAlertContinueAction,
actions: { confirmationAlertContinueAction in
Button("Cancel", role: .cancel) {
// Nothing to do
}
Button("Continue") {
confirmationAlertContinueAction.performAction(on: self)
}
},
message: { _ in
Text("Changing settings will sign you out and disconnect you from resources")
}
)
#elseif os(macOS)
VStack {
TabView {
accountTab
.tabItem {
Text("Account")
}
advancedTab
.tabItem {
Text("Advanced")
@@ -190,86 +191,28 @@ public struct SettingsView: View {
}
.padding(20)
}
.alert(
"Saving settings will sign you out",
isPresented: $isShowingConfirmationAlert,
presenting: confirmationAlertContinueAction,
actions: { confirmationAlertContinueAction in
Button("Cancel", role: .cancel) {
// Nothing to do
}
Button("Continue", role: .destructive) {
confirmationAlertContinueAction.performAction(on: self)
}
},
message: { _ in
Text("Changing settings will sign you out and disconnect you from resources")
}
)
.onDisappear(perform: { self.loadSettings() })
#else
#error("Unsupported platform")
#endif
}
private var accountTab: some View {
#if os(macOS)
VStack {
Spacer()
Form {
Section(
content: {
HStack(spacing: 15) {
Spacer()
Text("Account ID:")
TextField(
"",
text: Binding(
get: { model.accountSettings.accountId },
set: { model.accountSettings.accountId = $0 }
),
prompt: Text(PlaceholderText.accountId)
)
.frame(maxWidth: 240)
.onSubmit {
self.model.saveAccountSettings()
}
Spacer()
}
},
footer: {
Text(FootnoteText.forAccount)
.foregroundStyle(.secondary)
}
)
Button(
"Apply",
action: {
self.model.saveAccountSettings()
}
)
.disabled(
model.accountSettings.isSavedToDisk
|| !model.accountSettings.isValid
)
.padding(.top, 5)
}
Spacer()
}
#elseif os(iOS)
VStack {
Form {
Section(
content: {
HStack(spacing: 15) {
Text("Account ID")
.foregroundStyle(.secondary)
TextField(
PlaceholderText.accountId,
text: Binding(
get: { model.accountSettings.accountId },
set: { model.accountSettings.accountId = $0 }
)
)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.submitLabel(.done)
}
},
header: { Text("Account") },
footer: { Text(FootnoteText.forAccount) }
)
}
}
#else
#error("Unsupported platform")
#endif
}
private var advancedTab: some View {
#if os(macOS)
VStack {
@@ -311,7 +254,13 @@ public struct SettingsView: View {
Button(
"Apply",
action: {
self.model.saveAdvancedSettings()
let action = ConfirmationAlertContinueAction.saveAdvancedSettings
if case .signedIn = model.tunnelAuthStatus {
self.confirmationAlertContinueAction = action
self.isShowingConfirmationAlert = true
} else {
action.performAction(on: self)
}
}
)
.disabled(model.advancedSettings.isSavedToDisk || !model.advancedSettings.isValid)
@@ -429,8 +378,11 @@ public struct SettingsView: View {
#endif
}
func saveSettings() {
model.saveAccountSettings()
func saveAdvancedSettings() {
model.saveAdvancedSettings()
}
func saveAllSettingsAndDismiss() {
model.saveAdvancedSettings()
dismiss()
}

View File

@@ -55,10 +55,6 @@ import SwiftUINavigationCore
defer { bindDestination() }
if case .accountNotSetup = appStore.tunnel.tunnelAuthStatus {
destination = .undefinedSettingsAlert(.undefinedSettings)
}
appStore.auth.$loginStatus
.receive(on: mainQueue)
.sink(receiveValue: { [weak self] loginStatus in

View File

@@ -25,7 +25,7 @@ public actor Keychain {
public typealias PersistentRef = Data
public struct TokenAttributes {
let authURLString: String
let authBaseURLString: String
let actorName: String
}
@@ -56,7 +56,7 @@ public actor Keychain {
kSecClass: kSecClassGenericPassword,
kSecAttrLabel: "Firezone access token (\(tokenAttributes.actorName))",
kSecAttrDescription: "Firezone access token",
kSecAttrService: tokenAttributes.authURLString,
kSecAttrService: tokenAttributes.authBaseURLString,
// The UUID uniquifies this item in the keychain
kSecAttrAccount: "\(tokenAttributes.actorName): \(UUID().uuidString)",
kSecValueData: token.data(using: .utf8) as Any,
@@ -72,7 +72,7 @@ public actor Keychain {
kSecClass: kSecClassGenericPassword,
kSecAttrLabel: "Firezone access token (\(tokenAttributes.actorName))",
kSecAttrDescription: "Firezone access token",
kSecAttrService: tokenAttributes.authURLString,
kSecAttrService: tokenAttributes.authBaseURLString,
// The UUID uniquifies this item in the keychain
kSecAttrAccount: "\(tokenAttributes.actorName): \(UUID().uuidString)",
kSecValueData: token.data(using: .utf8) as Any,
@@ -100,7 +100,7 @@ public actor Keychain {
let checkForStaleItemsQuery =
[
kSecClass: kSecClassGenericPassword,
kSecAttrService: tokenAttributes.authURLString,
kSecAttrService: tokenAttributes.authBaseURLString,
kSecMatchLimit: kSecMatchLimitAll,
kSecReturnPersistentRef: true,
] as [CFString: Any]
@@ -217,7 +217,7 @@ public actor Keychain {
.startIndex..<(account.lastIndex(of: ":")
?? account.endIndex)])
let attributes = TokenAttributes(
authURLString: service,
authBaseURLString: service,
actorName: actorName)
continuation.resume(returning: attributes)
return
@@ -244,14 +244,14 @@ public actor Keychain {
}
}
func search(authURLString: String) async -> PersistentRef? {
func search(authBaseURLString: String) async -> PersistentRef? {
return await withCheckedContinuation { [weak self] continuation in
self?.workQueue.async {
let query =
[
kSecClass: kSecClassGenericPassword,
kSecAttrDescription: "Firezone access token",
kSecAttrService: authURLString,
kSecAttrService: authBaseURLString,
kSecReturnPersistentRef: true,
] as [CFString: Any]
var result: CFTypeRef?

View File

@@ -12,7 +12,7 @@ struct KeychainStorage: Sendable {
@Sendable (Keychain.Token, Keychain.TokenAttributes) async throws -> Keychain.PersistentRef
var delete: @Sendable (Keychain.PersistentRef) async throws -> Void
var loadAttributes: @Sendable (Keychain.PersistentRef) async -> Keychain.TokenAttributes?
var searchByAuthURL: @Sendable (URL) async -> Keychain.PersistentRef?
var searchByAuthBaseURL: @Sendable (URL) async -> Keychain.PersistentRef?
}
extension KeychainStorage: DependencyKey {
@@ -23,7 +23,7 @@ extension KeychainStorage: DependencyKey {
store: { try await keychain.store(token: $0, tokenAttributes: $1) },
delete: { try await keychain.delete(persistentRef: $0) },
loadAttributes: { await keychain.loadAttributes(persistentRef: $0) },
searchByAuthURL: { await keychain.search(authURLString: $0.absoluteString) }
searchByAuthBaseURL: { await keychain.search(authBaseURLString: $0.absoluteString) }
)
}
@@ -45,7 +45,7 @@ extension KeychainStorage: DependencyKey {
loadAttributes: { ref in
storage.value[ref]?.1
},
searchByAuthURL: { _ in
searchByAuthBaseURL: { _ in
nil
}
)

View File

@@ -7,5 +7,4 @@
import Foundation
enum FirezoneError: Error {
case missingTeamId
}

View File

@@ -6,25 +6,6 @@
import Foundation
struct AccountSettings {
var accountId: String = "" {
didSet { if oldValue != accountId { isSavedToDisk = false } }
}
var isSavedToDisk = true
var isValid: Bool {
!accountId.isEmpty
&& accountId.unicodeScalars.allSatisfy { Self.teamIdAllowedCharacterSet.contains($0) }
}
static let teamIdAllowedCharacterSet: CharacterSet = {
var pathAllowed = CharacterSet.urlPathAllowed
pathAllowed.remove("/")
return pathAllowed
}()
}
struct AdvancedSettings: Equatable {
var authBaseURLString: String {
didSet { if oldValue != authBaseURLString { isSavedToDisk = false } }
@@ -65,3 +46,9 @@ struct AdvancedSettings: Equatable {
// Note: To see what the connlibLogFilterString values mean, see:
// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html
}
extension AdvancedSettings: CustomStringConvertible {
var description: String {
"(\(authBaseURLString), \(apiURLString), \(connlibLogFilterString))"
}
}

View File

@@ -36,7 +36,7 @@ final class AppStore: ObservableObject {
Task {
do {
try await tunnel.stop()
auth.signOut()
await auth.signOut()
} catch {
logger.error("\(#function): Error stopping tunnel: \(String(describing: error))")
}

View File

@@ -29,25 +29,17 @@ final class AuthStore: ObservableObject {
enum LoginStatus: CustomStringConvertible {
case uninitialized
case signedOut(accountId: String?)
case signedIn(accountId: String, actorName: String)
var accountId: String? {
switch self {
case .uninitialized: return nil
case .signedOut(let accountId): return accountId
case .signedIn(let accountId, _): return accountId
}
}
case signedOut
case signedIn(actorName: String)
var description: String {
switch self {
case .uninitialized:
return "uninitialized"
case .signedOut(let accountId):
return "signedOut(accountId: \(accountId ?? "nil"))"
case .signedIn(let accountId, let actorName):
return "signedIn(accountId: \(accountId), actorName: \(actorName))"
case .signedOut:
return "signedOut"
case .signedIn(let actorName):
return "signedIn(actorName: \(actorName))"
}
}
}
@@ -96,22 +88,6 @@ final class AuthStore: ObservableObject {
.sink { [weak self] status in
guard let self = self else { return }
Task {
if status == .disconnected {
self.logger.log("\(#function): Disconnected")
if let tsEvent = TunnelShutdownEvent.loadFromDisk() {
self.logger.log(
"\(#function): Tunnel shutdown event: \(tsEvent, privacy: .public)"
)
switch tsEvent.action {
case .signoutImmediately:
self.signOut()
case .retryThenSignout:
self.retryStartTunnel()
}
} else {
self.logger.log("\(#function): Tunnel shutdown event not found")
}
}
if status == .connected {
self.resetReconnectionAttemptsRemaining()
}
@@ -134,51 +110,35 @@ final class AuthStore: ObservableObject {
switch tunnelAuthStatus {
case .tunnelUninitialized:
return .uninitialized
case .accountNotSetup:
return .signedOut(accountId: nil)
case .signedOut(_, let tunnelAccountId):
return .signedOut(accountId: tunnelAccountId)
case .signedIn(let tunnelAuthBaseURL, let tunnelAccountId, let tokenReference):
case .signedOut:
return .signedOut
case .signedIn(let tunnelAuthBaseURL, let tokenReference):
guard self.authBaseURL == tunnelAuthBaseURL else {
return .signedOut(accountId: tunnelAccountId)
return .signedOut
}
let tunnelPortalURLString = self.authURL(accountId: tunnelAccountId).absoluteString
let tunnelBaseURLString = self.authBaseURL.absoluteString
guard let tokenAttributes = await keychain.loadAttributes(tokenReference),
tunnelPortalURLString == tokenAttributes.authURLString
tunnelBaseURLString == tokenAttributes.authBaseURLString
else {
return .signedOut(accountId: tunnelAccountId)
return .signedOut
}
return .signedIn(accountId: tunnelAccountId, actorName: tokenAttributes.actorName)
return .signedIn(actorName: tokenAttributes.actorName)
}
}
func signIn(accountId: String) async throws {
logger.trace("\(#function)")
let portalURL = authURL(accountId: accountId)
let authResponse = try await auth.signIn(portalURL)
let attributes = Keychain.TokenAttributes(
authURLString: portalURL.absoluteString, actorName: authResponse.actorName ?? "")
let tokenRef = try await keychain.store(authResponse.token, attributes)
try await tunnelStore.saveAuthStatus(
.signedIn(authBaseURL: self.authBaseURL, accountId: accountId, tokenReference: tokenRef))
}
func signIn() async throws {
logger.trace("\(#function)")
guard case .signedOut(let accountId) = self.loginStatus, let accountId = accountId,
!accountId.isEmpty
else {
logger.log("No account-id found in tunnel")
throw FirezoneError.missingTeamId
}
let authResponse = try await auth.signIn(self.authBaseURL)
let attributes = Keychain.TokenAttributes(
authBaseURLString: self.authBaseURL.absoluteString, actorName: authResponse.actorName ?? "")
let tokenRef = try await keychain.store(authResponse.token, attributes)
try await signIn(accountId: accountId)
try await tunnelStore.saveAuthStatus(
.signedIn(authBaseURL: self.authBaseURL, tokenReference: tokenRef))
}
func signOut() {
func signOut() async {
logger.trace("\(#function)")
guard case .signedIn = self.tunnelStore.tunnelAuthStatus else {
@@ -186,10 +146,13 @@ final class AuthStore: ObservableObject {
return
}
Task {
if let tokenRef = try await tunnelStore.stopAndSignOut() {
do {
try await tunnelStore.stop()
if let tokenRef = try await tunnelStore.signOut() {
try await keychain.delete(tokenRef)
}
} catch {
logger.error("\(#function): Error signing out: \(error, privacy: .public)")
}
resetReconnectionAttemptsRemaining()
@@ -208,7 +171,21 @@ final class AuthStore: ObservableObject {
try await tunnelStore.start()
} catch {
logger.error("\(#function): Error starting tunnel: \(String(describing: error))")
self.retryStartTunnel()
if let tsEvent = TunnelShutdownEvent.loadFromDisk() {
self.logger.log(
"\(#function): Tunnel shutdown event: \(tsEvent, privacy: .public)"
)
switch tsEvent.action {
case .signoutImmediately:
Task {
await self.signOut()
}
case .retryThenSignout:
self.retryStartTunnel()
}
} else {
self.logger.log("\(#function): Tunnel shutdown event not found")
}
}
}
}
@@ -227,7 +204,9 @@ final class AuthStore: ObservableObject {
self.startTunnel()
}
} else {
self.signOut()
Task {
await self.signOut()
}
}
}
@@ -235,14 +214,7 @@ final class AuthStore: ObservableObject {
logger.log("\(#function): Login status: \(self.loginStatus)")
switch self.loginStatus {
case .signedIn:
Task {
do {
try await tunnelStore.start()
} catch {
logger.error("\(#function): Error starting tunnel: \(String(describing: error))")
self.retryStartTunnel()
}
}
self.startTunnel()
case .signedOut:
Task {
do {
@@ -250,6 +222,10 @@ final class AuthStore: ObservableObject {
} catch {
logger.error("\(#function): Error stopping tunnel: \(String(describing: error))")
}
if tunnelStore.tunnelAuthStatus != .signedOut {
// Bring tunnelAuthStatus in line, in case it's out of touch with the login status
try await tunnelStore.saveAuthStatus(.signedOut)
}
}
case .uninitialized:
break
@@ -260,16 +236,11 @@ final class AuthStore: ObservableObject {
self.reconnectionAttemptsRemaining = Self.maxReconnectionAttemptCount
}
func tunnelAuthStatusForAccount(accountId: String) async -> TunnelAuthStatus {
let portalURL = authURL(accountId: accountId)
if let tokenRef = await keychain.searchByAuthURL(portalURL) {
return .signedIn(authBaseURL: authBaseURL, accountId: accountId, tokenReference: tokenRef)
func tunnelAuthStatus(for authBaseURL: URL) async -> TunnelAuthStatus {
if let tokenRef = await keychain.searchByAuthBaseURL(authBaseURL) {
return .signedIn(authBaseURL: authBaseURL, tokenReference: tokenRef)
} else {
return .signedOut(authBaseURL: authBaseURL, accountId: accountId)
return .signedOut
}
}
func authURL(accountId: String) -> URL {
self.authBaseURL.appendingPathComponent(accountId)
}
}

View File

@@ -12,12 +12,21 @@ import OSLog
enum TunnelStoreError: Error {
case tunnelCouldNotBeStarted
case tunnelCouldNotBeStopped
case cannotSaveToTunnelWhenConnected
case cannotSignOutWhenConnected
case stopAlreadyBeingAttempted
}
public struct TunnelProviderKeys {
static let keyAuthBaseURLString = "authBaseURLString"
static let keyAccountId = "accountId"
public static let keyConnlibLogFilter = "connlibLogFilter"
// The following key is not added to the tunnel provider.
// The key is defined so that the key can be removed if it exists
// in a tunnel provider configuration. A tunnel configuration
// created by an earlier version of the app could have this
// key, and we use this key definition to remove it.
static let keyAccountId = "accountId"
}
final class TunnelStore: ObservableObject {
@@ -80,7 +89,7 @@ final class TunnelStore: ObservableObject {
try await tunnel.saveToPreferences()
Self.logger.log("\(#function): Tunnel created")
self.tunnel = tunnel
self.tunnelAuthStatus = .accountNotSetup
self.tunnelAuthStatus = .signedOut
}
setupTunnelObservers()
Self.logger.log("\(#function): TunnelStore initialized")
@@ -90,24 +99,35 @@ final class TunnelStore: ObservableObject {
}
func saveAuthStatus(_ tunnelAuthStatus: TunnelAuthStatus) async throws {
Self.logger.log("TunnelStore.\(#function) \(tunnelAuthStatus, privacy: .public)")
guard let tunnel = tunnel else {
fatalError("Tunnel not initialized yet")
}
try await stop()
let tunnelStatus = tunnel.connection.status
if tunnelStatus == .connected || tunnelStatus == .connecting {
throw TunnelStoreError.cannotSaveToTunnelWhenConnected
}
try await tunnel.loadFromPreferences()
try await tunnel.saveAuthStatus(tunnelAuthStatus)
self.tunnelAuthStatus = tunnelAuthStatus
}
func saveAdvancedSettings(_ advancedSettings: AdvancedSettings) async throws {
Self.logger.log("TunnelStore.\(#function) \(advancedSettings, privacy: .public)")
guard let tunnel = tunnel else {
fatalError("Tunnel not initialized yet")
}
try await stop()
let tunnelStatus = tunnel.connection.status
if tunnelStatus == .connected || tunnelStatus == .connecting {
throw TunnelStoreError.cannotSaveToTunnelWhenConnected
}
try await tunnel.loadFromPreferences()
try await tunnel.saveAdvancedSettings(advancedSettings)
self.tunnelAuthStatus = tunnel.authStatus()
}
func advancedSettings() -> AdvancedSettings? {
@@ -165,6 +185,10 @@ final class TunnelStore: ObservableObject {
return
}
guard self.stopTunnelContinuation == nil else {
throw TunnelStoreError.stopAlreadyBeingAttempted
}
TunnelStore.logger.trace("\(#function)")
let status = tunnel.connection.status
@@ -177,23 +201,20 @@ final class TunnelStore: ObservableObject {
}
}
func stopAndSignOut() async throws -> Keychain.PersistentRef? {
func signOut() async throws -> Keychain.PersistentRef? {
guard let tunnel = tunnel else {
Self.logger.log("\(#function): TunnelStore is not initialized")
return nil
}
TunnelStore.logger.trace("\(#function)")
let status = tunnel.connection.status
if status == .connected || status == .connecting {
let session = castToSession(tunnel.connection)
session.stopTunnel()
let tunnelStatus = tunnel.connection.status
if tunnelStatus == .connected || tunnelStatus == .connecting {
throw TunnelStoreError.cannotSignOutWhenConnected
}
if case .signedIn(let authBaseURL, let accountId, let tokenReference) = self.tunnelAuthStatus {
if case .signedIn(_, let tokenReference) = self.tunnelAuthStatus {
do {
try await saveAuthStatus(.signedOut(authBaseURL: authBaseURL, accountId: accountId))
try await saveAuthStatus(.signedOut)
} catch {
TunnelStore.logger.trace(
"\(#function): Error saving signed out auth status: \(error)"
@@ -324,9 +345,8 @@ final class TunnelStore: ObservableObject {
enum TunnelAuthStatus: Equatable, CustomStringConvertible {
case tunnelUninitialized
case accountNotSetup
case signedOut(authBaseURL: URL, accountId: String)
case signedIn(authBaseURL: URL, accountId: String, tokenReference: Data)
case signedOut
case signedIn(authBaseURL: URL, tokenReference: Data)
var isInitialized: Bool {
switch self {
@@ -335,27 +355,14 @@ enum TunnelAuthStatus: Equatable, CustomStringConvertible {
}
}
func accountId() -> String? {
switch self {
case .tunnelUninitialized, .accountNotSetup:
return nil
case .signedOut(_, let accountId):
return accountId
case .signedIn(_, let accountId, _):
return accountId
}
}
var description: String {
switch self {
case .tunnelUninitialized:
return "tunnel uninitialized"
case .accountNotSetup:
return "account not setup"
case .signedOut(let authBaseURL, let accountId):
return "signedOut(authBaseURL: \(authBaseURL), accountId: \(accountId))"
case .signedIn(let authBaseURL, let accountId, _):
return "signedIn(authBaseURL: \(authBaseURL), accountId: \(accountId))"
case .signedOut:
return "signedOut"
case .signedIn(let authBaseURL, _):
return "signedIn(authBaseURL: \(authBaseURL))"
}
}
}
@@ -389,19 +396,18 @@ extension NETunnelProviderManager {
}
return URL(string: urlString)
}()
let accountId = providerConfig[TunnelProviderKeys.keyAccountId] as? String
let tokenRef = protocolConfiguration.passwordReference
if let authBaseURL = authBaseURL, let accountId = accountId {
if let authBaseURL = authBaseURL {
if let tokenRef = tokenRef {
return .signedIn(authBaseURL: authBaseURL, accountId: accountId, tokenReference: tokenRef)
return .signedIn(authBaseURL: authBaseURL, tokenReference: tokenRef)
} else {
return .signedOut(authBaseURL: authBaseURL, accountId: accountId)
return .signedOut
}
} else {
return .accountNotSetup
return .signedOut
}
}
return .accountNotSetup
return .signedOut
}
func saveAuthStatus(_ authStatus: TunnelAuthStatus) async throws {
@@ -409,17 +415,18 @@ extension NETunnelProviderManager {
var providerConfig: [String: Any] = protocolConfiguration.providerConfiguration ?? [:]
switch authStatus {
case .tunnelUninitialized, .accountNotSetup:
case .tunnelUninitialized:
protocolConfiguration.passwordReference = nil
break
case .signedOut(let authBaseURL, let accountId):
case .signedOut:
protocolConfiguration.passwordReference = nil
break
case .signedIn(let authBaseURL, let tokenReference):
providerConfig[TunnelProviderKeys.keyAuthBaseURLString] = authBaseURL.absoluteString
providerConfig[TunnelProviderKeys.keyAccountId] = accountId
case .signedIn(let authBaseURL, let accountId, let tokenReference):
providerConfig[TunnelProviderKeys.keyAuthBaseURLString] = authBaseURL.absoluteString
providerConfig[TunnelProviderKeys.keyAccountId] = accountId
protocolConfiguration.passwordReference = tokenReference
}
providerConfig.removeValue(forKey: TunnelProviderKeys.keyAccountId)
protocolConfiguration.providerConfiguration = providerConfig
ensureTunnelConfigurationIsValid()

View File

@@ -41,7 +41,7 @@
private var connectingAnimationTimer: Timer?
let settingsViewModel: SettingsViewModel
private var loginStatus: AuthStore.LoginStatus = .signedOut(accountId: nil)
private var loginStatus: AuthStore.LoginStatus = .signedOut
private var tunnelStatus: NEVPNStatus = .invalid
public init(settingsViewModel: SettingsViewModel) {
@@ -210,8 +210,6 @@
Task {
do {
try await appStore?.auth.signIn()
} catch FirezoneError.missingTeamId {
openSettingsWindow()
} catch {
logger.error("Error signing in: \(String(describing: error))")
}
@@ -219,7 +217,9 @@
}
@objc private func signOutButtonTapped() {
appStore?.auth.signOut()
Task {
await appStore?.auth.signOut()
}
}
@objc private func settingsButtonTapped() {
@@ -316,7 +316,7 @@
signInMenuItem.target = self
signInMenuItem.isEnabled = true
signOutMenuItem.isHidden = true
case .signedIn(_, let actorName):
case .signedIn(let actorName):
signInMenuItem.title = actorName.isEmpty ? "Signed in" : "Signed in as \(actorName)"
signInMenuItem.target = nil
signOutMenuItem.isHidden = false