Get rid of SecretsManager. It wasn’t thread-safe, and it existed only for tests (and it wasn’t thread-safe for tests either). Pass SecretsProvider parameter where it’s needed.

This commit is contained in:
Brent Simmons 2024-03-10 22:22:41 -07:00
parent 13403df8f1
commit 78047fcaf7
31 changed files with 119 additions and 118 deletions

View File

@ -258,7 +258,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return delegate.refreshProgress return delegate.refreshProgress
} }
init(dataFolder: String, type: AccountType, accountID: String, transport: Transport? = nil) { init(dataFolder: String, type: AccountType, accountID: String, secretsProvider: SecretsProvider, transport: Transport? = nil) {
switch type { switch type {
case .onMyMac: case .onMyMac:
self.delegate = LocalAccountDelegate() self.delegate = LocalAccountDelegate()
@ -267,17 +267,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
case .feedbin: case .feedbin:
self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport) self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport)
case .feedly: case .feedly:
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment) self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment, secretsProvider: secretsProvider)
case .newsBlur: case .newsBlur:
self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport) self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport)
case .freshRSS: case .freshRSS:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .freshRSS) self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .freshRSS, secretsProvider: secretsProvider)
case .inoreader: case .inoreader:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .inoreader) self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .inoreader, secretsProvider: secretsProvider)
case .bazQux: case .bazQux:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .bazQux) self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .bazQux, secretsProvider: secretsProvider)
case .theOldReader: case .theOldReader:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .theOldReader) self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .theOldReader, secretsProvider: secretsProvider)
} }
self.delegate.accountMetadata = metadata self.delegate.accountMetadata = metadata
@ -367,29 +367,29 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
try CredentialsManager.removeCredentials(type: type, server: server, username: username) try CredentialsManager.removeCredentials(type: type, server: server, username: username)
} }
public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> Void) { public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void) {
switch type { switch type {
case .feedbin: case .feedbin:
FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, secretsProvider: secretsProvider, completion: completion)
case .newsBlur: case .newsBlur:
NewsBlurAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) NewsBlurAccountDelegate.validateCredentials(transport: transport, credentials: credentials, secretsProvider: secretsProvider, completion: completion)
case .freshRSS, .inoreader, .bazQux, .theOldReader: case .freshRSS, .inoreader, .bazQux, .theOldReader:
ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion) ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, secretsProvider: secretsProvider, completion: completion)
default: default:
break break
} }
} }
internal static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient { internal static func oauthAuthorizationClient(for type: AccountType, secretsProvider: SecretsProvider) -> OAuthAuthorizationClient {
switch type { switch type {
case .feedly: case .feedly:
return FeedlyAccountDelegate.environment.oauthAuthorizationClient return FeedlyAccountDelegate.environment.oauthAuthorizationClient(secretsProvider: secretsProvider)
default: default:
fatalError("\(type) is not a client for OAuth authorization code granting.") fatalError("\(type) is not a client for OAuth authorization code granting.")
} }
} }
public static func oauthAuthorizationCodeGrantRequest(for type: AccountType) -> URLRequest { public static func oauthAuthorizationCodeGrantRequest(for type: AccountType, secretsProvider: SecretsProvider) -> URLRequest {
let grantingType: OAuthAuthorizationGranting.Type let grantingType: OAuthAuthorizationGranting.Type
switch type { switch type {
case .feedly: case .feedly:
@ -398,13 +398,14 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fatalError("\(type) does not support OAuth authorization code granting.") fatalError("\(type) does not support OAuth authorization code granting.")
} }
return grantingType.oauthAuthorizationCodeGrantRequest() return grantingType.oauthAuthorizationCodeGrantRequest(secretsProvider: secretsProvider)
} }
public static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, public static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse,
client: OAuthAuthorizationClient, client: OAuthAuthorizationClient,
accountType: AccountType, accountType: AccountType,
transport: Transport = URLSession.webserviceTransport(), transport: Transport = URLSession.webserviceTransport(),
secretsProvider: SecretsProvider,
completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) { completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
let grantingType: OAuthAuthorizationGranting.Type let grantingType: OAuthAuthorizationGranting.Type
@ -415,7 +416,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fatalError("\(accountType) does not support OAuth authorization code granting.") fatalError("\(accountType) does not support OAuth authorization code granting.")
} }
grantingType.requestOAuthAccessToken(with: response, transport: transport, completion: completion) grantingType.requestOAuthAccessToken(with: response, transport: transport, secretsProvider: secretsProvider, completion: completion)
} }
public func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { public func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {

View File

@ -52,7 +52,7 @@ protocol AccountDelegate {
func accountWillBeDeleted(_ account: Account) func accountWillBeDeleted(_ account: Account)
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void)
/// Suspend all network activity /// Suspend all network activity
func suspendNetwork() func suspendNetwork()

View File

@ -12,6 +12,7 @@ import RSWeb
import Articles import Articles
import ArticlesDatabase import ArticlesDatabase
import Database import Database
import Secrets
// Main thread only. // Main thread only.
@ -29,6 +30,8 @@ public final class AccountManager: UnreadCountProvider {
private let defaultAccountFolderName = "OnMyMac" private let defaultAccountFolderName = "OnMyMac"
private let defaultAccountIdentifier = "OnMyMac" private let defaultAccountIdentifier = "OnMyMac"
private let secretsProvider: SecretsProvider
public var isSuspended = false public var isSuspended = false
public var isUnreadCountsInitialized: Bool { public var isUnreadCountsInitialized: Bool {
for account in activeAccounts { for account in activeAccounts {
@ -94,9 +97,11 @@ public final class AccountManager: UnreadCountProvider {
return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray) return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray)
} }
public init(accountsFolder: String) { public init(accountsFolder: String, secretsProvider: SecretsProvider) {
self.accountsFolder = accountsFolder
self.accountsFolder = accountsFolder
self.secretsProvider = secretsProvider
// The local "On My Mac" account must always exist, even if it's empty. // The local "On My Mac" account must always exist, even if it's empty.
let localAccountFolder = (accountsFolder as NSString).appendingPathComponent("OnMyMac") let localAccountFolder = (accountsFolder as NSString).appendingPathComponent("OnMyMac")
do { do {
@ -107,7 +112,7 @@ public final class AccountManager: UnreadCountProvider {
abort() abort()
} }
defaultAccount = Account(dataFolder: localAccountFolder, type: .onMyMac, accountID: defaultAccountIdentifier) defaultAccount = Account(dataFolder: localAccountFolder, type: .onMyMac, accountID: defaultAccountIdentifier, secretsProvider: secretsProvider)
accountsDictionary[defaultAccount.accountID] = defaultAccount accountsDictionary[defaultAccount.accountID] = defaultAccount
readAccountsFromDisk() readAccountsFromDisk()
@ -134,7 +139,7 @@ public final class AccountManager: UnreadCountProvider {
abort() abort()
} }
let account = Account(dataFolder: accountFolder, type: type, accountID: accountID) let account = Account(dataFolder: accountFolder, type: type, accountID: accountID, secretsProvider: secretsProvider)
accountsDictionary[accountID] = account accountsDictionary[accountID] = account
var userInfo = [String: Any]() var userInfo = [String: Any]()
@ -430,7 +435,7 @@ private extension AccountManager {
} }
func loadAccount(_ accountSpecifier: AccountSpecifier) -> Account? { func loadAccount(_ accountSpecifier: AccountSpecifier) -> Account? {
return Account(dataFolder: accountSpecifier.folderPath, type: accountSpecifier.type, accountID: accountSpecifier.identifier) return Account(dataFolder: accountSpecifier.folderPath, type: accountSpecifier.type, accountID: accountSpecifier.identifier, secretsProvider: secretsProvider)
} }
func loadAccount(_ filename: String) -> Account? { func loadAccount(_ filename: String) -> Account? {

View File

@ -451,7 +451,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
articlesZone.resetChangeToken() articlesZone.resetChangeToken()
} }
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) { static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: (Result<Credentials?, Error>) -> Void) {
return completion(.success(nil)) return completion(.success(nil))
} }

View File

@ -583,7 +583,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
func accountWillBeDeleted(_ account: Account) { func accountWillBeDeleted(_ account: Account) {
} }
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> Void) { static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void) {
let caller = FeedbinAPICaller(transport: transport) let caller = FeedbinAPICaller(transport: transport)
caller.credentials = credentials caller.credentials = credentials

View File

@ -37,12 +37,12 @@ final class FeedlyAPICaller {
return components return components
} }
var oauthAuthorizationClient: OAuthAuthorizationClient { func oauthAuthorizationClient(secretsProvider: SecretsProvider) -> OAuthAuthorizationClient {
switch self { switch self {
case .sandbox: case .sandbox:
return .feedlySandboxClient return .feedlySandboxClient
case .cloud: case .cloud:
return .feedlyCloudClient return OAuthAuthorizationClient.feedlyCloudClient(secretsProvider: secretsProvider)
} }
} }
} }
@ -50,11 +50,13 @@ final class FeedlyAPICaller {
private let transport: Transport private let transport: Transport
private let baseUrlComponents: URLComponents private let baseUrlComponents: URLComponents
private let uriComponentAllowed: CharacterSet private let uriComponentAllowed: CharacterSet
private let secretsProvider: SecretsProvider
init(transport: Transport, api: API) { init(transport: Transport, api: API, secretsProvider: SecretsProvider) {
self.transport = transport self.transport = transport
self.baseUrlComponents = api.baseUrlComponents self.baseUrlComponents = api.baseUrlComponents
self.secretsProvider = secretsProvider
var urlHostAllowed = CharacterSet.urlHostAllowed var urlHostAllowed = CharacterSet.urlHostAllowed
urlHostAllowed.remove("+") urlHostAllowed.remove("+")
uriComponentAllowed = urlHostAllowed uriComponentAllowed = urlHostAllowed

View File

@ -25,11 +25,11 @@ public struct FeedlyOAuthAccessTokenResponse: Decodable, OAuthAccessTokenRespons
} }
extension FeedlyAccountDelegate: OAuthAuthorizationGranting { extension FeedlyAccountDelegate: OAuthAuthorizationGranting {
private static let oauthAuthorizationGrantScope = "https://cloud.feedly.com/subscriptions" private static let oauthAuthorizationGrantScope = "https://cloud.feedly.com/subscriptions"
static func oauthAuthorizationCodeGrantRequest() -> URLRequest { static func oauthAuthorizationCodeGrantRequest(secretsProvider: SecretsProvider) -> URLRequest {
let client = environment.oauthAuthorizationClient let client = environment.oauthAuthorizationClient(secretsProvider: secretsProvider)
let authorizationRequest = OAuthAuthorizationRequest(clientId: client.id, let authorizationRequest = OAuthAuthorizationRequest(clientId: client.id,
redirectUri: client.redirectUri, redirectUri: client.redirectUri,
scope: oauthAuthorizationGrantScope, scope: oauthAuthorizationGrantScope,
@ -38,12 +38,12 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting {
return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest, baseUrlComponents: baseURLComponents) return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest, baseUrlComponents: baseURLComponents)
} }
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) { static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, secretsProvider: SecretsProvider, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
let client = environment.oauthAuthorizationClient let client = environment.oauthAuthorizationClient(secretsProvider: secretsProvider)
let request = OAuthAccessTokenRequest(authorizationResponse: response, let request = OAuthAccessTokenRequest(authorizationResponse: response,
scope: oauthAuthorizationGrantScope, scope: oauthAuthorizationGrantScope,
client: client) client: client)
let caller = FeedlyAPICaller(transport: transport, api: environment) let caller = FeedlyAPICaller(transport: transport, api: environment, secretsProvider: secretsProvider)
caller.requestAccessToken(request) { result in caller.requestAccessToken(request) { result in
switch result { switch result {
case .success(let response): case .success(let response):

View File

@ -67,15 +67,15 @@ final class FeedlyAccountDelegate: AccountDelegate {
private weak var currentSyncAllOperation: MainThreadOperation? private weak var currentSyncAllOperation: MainThreadOperation?
private let operationQueue = MainThreadOperationQueue() private let operationQueue = MainThreadOperationQueue()
init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API) { init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API, secretsProvider: SecretsProvider) {
// Many operations have their own operation queues, such as the sync all operation. // Many operations have their own operation queues, such as the sync all operation.
// Making this a serial queue at this higher level of abstraction means we can ensure, // Making this a serial queue at this higher level of abstraction means we can ensure,
// for example, a `FeedlyRefreshAccessTokenOperation` occurs before a `FeedlySyncAllOperation`, // for example, a `FeedlyRefreshAccessTokenOperation` occurs before a `FeedlySyncAllOperation`,
// improving our ability to debug, reason about and predict the behaviour of the code. // improving our ability to debug, reason about and predict the behaviour of the code.
if let transport = transport { if let transport = transport {
self.caller = FeedlyAPICaller(transport: transport, api: api) self.caller = FeedlyAPICaller(transport: transport, api: api, secretsProvider: secretsProvider)
} else { } else {
let sessionConfiguration = URLSessionConfiguration.default let sessionConfiguration = URLSessionConfiguration.default
@ -92,12 +92,12 @@ final class FeedlyAccountDelegate: AccountDelegate {
} }
let session = URLSession(configuration: sessionConfiguration) let session = URLSession(configuration: sessionConfiguration)
self.caller = FeedlyAPICaller(transport: session, api: api) self.caller = FeedlyAPICaller(transport: session, api: api, secretsProvider: secretsProvider)
} }
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
self.database = SyncDatabase(databaseFilePath: databaseFilePath) self.database = SyncDatabase(databaseFilePath: databaseFilePath)
self.oauthAuthorizationClient = api.oauthAuthorizationClient self.oauthAuthorizationClient = api.oauthAuthorizationClient(secretsProvider: secretsProvider)
self.caller.delegate = self self.caller.delegate = self
} }
@ -539,7 +539,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
MainThreadOperationQueue.shared.add(logout) MainThreadOperationQueue.shared.add(logout)
} }
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) { static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void) {
assertionFailure("An `account` instance should enqueue an \(FeedlyRefreshAccessTokenOperation.self) instead.") assertionFailure("An `account` instance should enqueue an \(FeedlyRefreshAccessTokenOperation.self) instead.")
completion(.success(credentials)) completion(.success(credentials))
} }

View File

@ -9,6 +9,7 @@
import Foundation import Foundation
import AuthenticationServices import AuthenticationServices
import RSCore import RSCore
import Secrets
public protocol OAuthAccountAuthorizationOperationDelegate: AnyObject { public protocol OAuthAccountAuthorizationOperationDelegate: AnyObject {
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account)
@ -42,17 +43,19 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
private let accountType: AccountType private let accountType: AccountType
private let oauthClient: OAuthAuthorizationClient private let oauthClient: OAuthAuthorizationClient
private var session: ASWebAuthenticationSession? private var session: ASWebAuthenticationSession?
private let secretsProvider: SecretsProvider
public init(accountType: AccountType) {
public init(accountType: AccountType, secretsProvider: SecretsProvider) {
self.accountType = accountType self.accountType = accountType
self.oauthClient = Account.oauthAuthorizationClient(for: accountType) self.secretsProvider = secretsProvider
self.oauthClient = Account.oauthAuthorizationClient(for: accountType, secretsProvider: secretsProvider)
} }
public func run() { public func run() {
assert(presentationAnchor != nil, "\(self) outlived presentation anchor.") assert(presentationAnchor != nil, "\(self) outlived presentation anchor.")
let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType) let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType, secretsProvider: secretsProvider)
guard let url = request.url else { guard let url = request.url else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
self.didEndAuthentication(url: nil, error: URLError(.badURL)) self.didEndAuthentication(url: nil, error: URLError(.badURL))
@ -113,7 +116,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
let response = try OAuthAuthorizationResponse(url: url, client: oauthClient) let response = try OAuthAuthorizationResponse(url: url, client: oauthClient)
Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, completion: didEndRequestingAccessToken(_:)) Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, secretsProvider: secretsProvider, completion: didEndRequestingAccessToken(_:))
} catch is ASWebAuthenticationSessionError { } catch is ASWebAuthenticationSessionError {
didFinish() // Primarily, cancellation. didFinish() // Primarily, cancellation.

View File

@ -11,14 +11,14 @@ import Secrets
extension OAuthAuthorizationClient { extension OAuthAuthorizationClient {
static var feedlyCloudClient: OAuthAuthorizationClient { static func feedlyCloudClient(secretsProvider: SecretsProvider) -> OAuthAuthorizationClient {
/// Models private NetNewsWire client secrets. /// Models private NetNewsWire client secrets.
/// These placeholders are substituted at build time using a Run Script phase with build settings. /// These placeholders are substituted at build time using a Run Script phase with build settings.
/// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code /// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code
return OAuthAuthorizationClient(id: SecretsManager.provider.feedlyClientId, return OAuthAuthorizationClient(id: secretsProvider.feedlyClientId,
redirectUri: "netnewswire://auth/feedly", redirectUri: "netnewswire://auth/feedly",
state: nil, state: nil,
secret: SecretsManager.provider.feedlyClientSecret) secret: secretsProvider.feedlyClientSecret)
} }
static var feedlySandboxClient: OAuthAuthorizationClient { static var feedlySandboxClient: OAuthAuthorizationClient {

View File

@ -17,7 +17,7 @@ public struct OAuthAuthorizationClient: Equatable {
public var redirectUri: String public var redirectUri: String
public var state: String? public var state: String?
public var secret: String public var secret: String
public init(id: String, redirectUri: String, state: String?, secret: String) { public init(id: String, redirectUri: String, state: String?, secret: String) {
self.id = id self.id = id
self.redirectUri = redirectUri self.redirectUri = redirectUri
@ -167,7 +167,7 @@ public protocol OAuthAuthorizationCodeGrantRequesting {
protocol OAuthAuthorizationGranting: AccountDelegate { protocol OAuthAuthorizationGranting: AccountDelegate {
static func oauthAuthorizationCodeGrantRequest() -> URLRequest static func oauthAuthorizationCodeGrantRequest(secretsProvider: SecretsProvider) -> URLRequest
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, secretsProvider: SecretsProvider, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ())
} }

View File

@ -197,7 +197,7 @@ final class LocalAccountDelegate: AccountDelegate {
func accountWillBeDeleted(_ account: Account) { func accountWillBeDeleted(_ account: Account) {
} }
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) { static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: (Result<Credentials?, Error>) -> Void) {
return completion(.success(nil)) return completion(.success(nil))
} }

View File

@ -612,7 +612,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
caller.logout() { _ in } caller.logout() { _ in }
} }
class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> ()) { class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> ()) {
let caller = NewsBlurAPICaller(transport: transport) let caller = NewsBlurAPICaller(transport: transport)
caller.credentials = credentials caller.credentials = credentials
caller.validateCredentials() { result in caller.validateCredentials() { result in

View File

@ -74,12 +74,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0) var refreshProgress = DownloadProgress(numberOfTasks: 0)
init(dataFolder: String, transport: Transport?, variant: ReaderAPIVariant) { init(dataFolder: String, transport: Transport?, variant: ReaderAPIVariant, secretsProvider: SecretsProvider) {
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
database = SyncDatabase(databaseFilePath: databaseFilePath) database = SyncDatabase(databaseFilePath: databaseFilePath)
if transport != nil { if transport != nil {
caller = ReaderAPICaller(transport: transport!) caller = ReaderAPICaller(transport: transport!, secretsProvider: secretsProvider)
} else { } else {
let sessionConfiguration = URLSessionConfiguration.default let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
@ -94,7 +94,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
} }
caller = ReaderAPICaller(transport: URLSession(configuration: sessionConfiguration)) caller = ReaderAPICaller(transport: URLSession(configuration: sessionConfiguration), secretsProvider: secretsProvider)
} }
caller.variant = variant caller.variant = variant
@ -636,13 +636,13 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
func accountWillBeDeleted(_ account: Account) { func accountWillBeDeleted(_ account: Account) {
} }
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) { static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void) {
guard let endpoint = endpoint else { guard let endpoint = endpoint else {
completion(.failure(TransportError.noURL)) completion(.failure(TransportError.noURL))
return return
} }
let caller = ReaderAPICaller(transport: transport) let caller = ReaderAPICaller(transport: transport, secretsProvider: secretsProvider)
caller.credentials = credentials caller.credentials = credentials
caller.validateCredentials(endpoint: endpoint) { result in caller.validateCredentials(endpoint: endpoint) { result in
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@ -48,6 +48,7 @@ final class ReaderAPICaller: NSObject {
} }
private var transport: Transport! private var transport: Transport!
private let secretsProvider: SecretsProvider
private let uriComponentAllowed: CharacterSet private let uriComponentAllowed: CharacterSet
private var accessToken: String? private var accessToken: String?
@ -77,9 +78,10 @@ final class ReaderAPICaller: NSObject {
} }
} }
init(transport: Transport) { init(transport: Transport, secretsProvider: SecretsProvider) {
self.transport = transport self.transport = transport
self.secretsProvider = secretsProvider
var urlHostAllowed = CharacterSet.urlHostAllowed var urlHostAllowed = CharacterSet.urlHostAllowed
urlHostAllowed.remove("+") urlHostAllowed.remove("+")
urlHostAllowed.remove("&") urlHostAllowed.remove("&")
@ -693,8 +695,8 @@ private extension ReaderAPICaller {
func addVariantHeaders(_ request: inout URLRequest) { func addVariantHeaders(_ request: inout URLRequest) {
if variant == .inoreader { if variant == .inoreader {
request.addValue(SecretsManager.provider.inoreaderAppId, forHTTPHeaderField: "AppId") request.addValue(secretsProvider.inoreaderAppId, forHTTPHeaderField: "AppId")
request.addValue(SecretsManager.provider.inoreaderAppKey, forHTTPHeaderField: "AppKey") request.addValue(secretsProvider.inoreaderAppKey, forHTTPHeaderField: "AppKey")
} }
} }

View File

@ -19,10 +19,6 @@ class FeedlyTestSupport {
var refreshToken = Credentials(type: .oauthRefreshToken, username: "Test", secret: "t3st-refresh-tok3n") var refreshToken = Credentials(type: .oauthRefreshToken, username: "Test", secret: "t3st-refresh-tok3n")
var transport = TestTransport() var transport = TestTransport()
init() {
SecretsManager.provider = FeedlyTestSecrets()
}
func makeMockNetworkStack() -> (TestTransport, FeedlyAPICaller) { func makeMockNetworkStack() -> (TestTransport, FeedlyAPICaller) {
let caller = FeedlyAPICaller(transport: transport, api: .sandbox) let caller = FeedlyAPICaller(transport: transport, api: .sandbox)
caller.credentials = accessToken caller.credentials = accessToken

View File

@ -108,6 +108,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
#endif #endif
private var themeImportPath: String? private var themeImportPath: String?
private let secretsProvider = Secrets()
override init() { override init() {
NSWindow.allowsAutomaticWindowTabbing = false NSWindow.allowsAutomaticWindowTabbing = false
@ -119,8 +120,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
crashReporter.enable() crashReporter.enable()
#endif #endif
SecretsManager.provider = Secrets() AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider)
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!)
ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!) ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)

View File

@ -1257,7 +1257,7 @@ private extension MainWindowController {
} }
func startArticleExtractorForCurrentLink() { func startArticleExtractorForCurrentLink() {
if let link = currentLink, let extractor = ArticleExtractor(link) { if let link = currentLink, let extractor = ArticleExtractor(link, secretsProvider: Secrets()) {
extractor.delegate = self extractor.delegate = self
extractor.process() extractor.process()
articleExtractor = extractor articleExtractor = extractor

View File

@ -79,8 +79,8 @@ class AccountsFeedbinWindowController: NSWindowController {
progressIndicator.startAnimation(self) progressIndicator.startAnimation(self)
let credentials = Credentials(type: .basic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue) let credentials = Credentials(type: .basic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in Account.validateCredentials(type: .feedbin, credentials: credentials, secretsProvider: Secrets()) { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
self.actionButton.isEnabled = true self.actionButton.isEnabled = true

View File

@ -76,7 +76,7 @@ class AccountsNewsBlurWindowController: NSWindowController {
progressIndicator.startAnimation(self) progressIndicator.startAnimation(self)
let credentials = Credentials(type: .newsBlurBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue) let credentials = Credentials(type: .newsBlurBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
Account.validateCredentials(type: .newsBlur, credentials: credentials) { [weak self] result in Account.validateCredentials(type: .newsBlur, credentials: credentials, secretsProvider: Secrets()) { [weak self] result in
guard let self = self else { return } guard let self = self else { return }

View File

@ -174,7 +174,7 @@ extension AccountsPreferencesViewController: AccountsPreferencesAddAccountDelega
accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!) accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!)
addAccountWindowController = accountsReaderAPIWindowController addAccountWindowController = accountsReaderAPIWindowController
case .feedly: case .feedly:
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly, secretsProvider: Secrets())
addAccount.delegate = self addAccount.delegate = self
addAccount.presentationAnchor = self.view.window! addAccount.presentationAnchor = self.view.window!
runAwaitingFeedlyLoginAlertModal(forLifetimeOf: addAccount) runAwaitingFeedlyLoginAlertModal(forLifetimeOf: addAccount)

View File

@ -131,8 +131,8 @@ class AccountsReaderAPIWindowController: NSWindowController {
progressIndicator.startAnimation(self) progressIndicator.startAnimation(self)
let credentials = Credentials(type: .readerBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue) let credentials = Credentials(type: .readerBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
Account.validateCredentials(type: accountType, credentials: credentials, endpoint: apiURL) { [weak self] result in Account.validateCredentials(type: accountType, credentials: credentials, endpoint: apiURL, secretsProvider: Secrets()) { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
self.actionButton.isEnabled = true self.actionButton.isEnabled = true

View File

@ -2,20 +2,23 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "Secrets", name: "Secrets",
platforms: [.macOS(.v14), .iOS(.v17)], platforms: [.macOS(.v14), .iOS(.v17)],
products: [ products: [
.library( .library(
name: "Secrets", name: "Secrets",
type: .dynamic, type: .dynamic,
targets: ["Secrets"] targets: ["Secrets"]
) )
], ],
dependencies: [], dependencies: [],
targets: [ targets: [
.target( .target(
name: "Secrets", name: "Secrets",
dependencies: [] dependencies: [],
) swiftSettings: [
] .enableExperimentalFeature("StrictConcurrency")
]
)
]
) )

View File

@ -1,12 +0,0 @@
//
// SecretsManager.swift
//
//
// Created by Maurice Parker on 7/30/20.
//
import Foundation
public class SecretsManager {
public static var provider: SecretsProvider!
}

View File

@ -34,15 +34,15 @@ class ArticleExtractor {
private var url: URL! private var url: URL!
public init?(_ articleLink: String) { public init?(_ articleLink: String, secretsProvider: SecretsProvider) {
self.articleLink = articleLink self.articleLink = articleLink
let clientURL = "https://extract.feedbin.com/parser" let clientURL = "https://extract.feedbin.com/parser"
let username = SecretsManager.provider.mercuryClientId let username = secretsProvider.mercuryClientId
let signiture = articleLink.hmacUsingSHA1(key: SecretsManager.provider.mercuryClientSecret) let signature = articleLink.hmacUsingSHA1(key: secretsProvider.mercuryClientSecret)
if let base64URL = articleLink.data(using: .utf8)?.base64EncodedString() { if let base64URL = articleLink.data(using: .utf8)?.base64EncodedString() {
let fullURL = "\(clientURL)/\(username)/\(signiture)?base64_url=\(base64URL)" let fullURL = "\(clientURL)/\(username)/\(signature)?base64_url=\(base64URL)"
if let url = URL(string: fullURL) { if let url = URL(string: fullURL) {
self.url = url self.url = url
return return

View File

@ -30,6 +30,8 @@ class FeedbinAccountViewController: UITableViewController {
weak var account: Account? weak var account: Account?
weak var delegate: AddAccountDismissDelegate? weak var delegate: AddAccountDismissDelegate?
var secretsProvider: SecretsProvider!
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupFooter() setupFooter()
@ -120,7 +122,7 @@ class FeedbinAccountViewController: UITableViewController {
setNavigationEnabled(to: false) setNavigationEnabled(to: false)
let credentials = Credentials(type: .basic, username: trimmedEmail, secret: password) let credentials = Credentials(type: .basic, username: trimmedEmail, secret: password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in Account.validateCredentials(type: .feedbin, credentials: credentials, secretsProvider: secretsProvider) { result in
self.toggleActivityIndicatorAnimation(visible: false) self.toggleActivityIndicatorAnimation(visible: false)
self.setNavigationEnabled(to: true) self.setNavigationEnabled(to: true)

View File

@ -29,7 +29,7 @@ class NewsBlurAccountViewController: UITableViewController {
weak var account: Account? weak var account: Account?
weak var delegate: AddAccountDismissDelegate? weak var delegate: AddAccountDismissDelegate?
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupFooter() setupFooter()
@ -105,7 +105,7 @@ class NewsBlurAccountViewController: UITableViewController {
disableNavigation() disableNavigation()
let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: password) let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: password)
Account.validateCredentials(type: .newsBlur, credentials: basicCredentials) { result in Account.validateCredentials(type: .newsBlur, credentials: basicCredentials, secretsProvider: Secrets()) { result in
self.stopAnimatingActivityIndicator() self.stopAnimatingActivityIndicator()
self.enableNavigation() self.enableNavigation()
@ -147,7 +147,6 @@ class NewsBlurAccountViewController: UITableViewController {
case .failure(let error): case .failure(let error):
self.showError(error.localizedDescription) self.showError(error.localizedDescription)
} }
} }
} }

View File

@ -157,7 +157,7 @@ class ReaderAPIAccountViewController: UITableViewController {
disableNavigation() disableNavigation()
let credentials = Credentials(type: .readerBasic, username: trimmedUsername, secret: password) let credentials = Credentials(type: .readerBasic, username: trimmedUsername, secret: password)
Account.validateCredentials(type: type, credentials: credentials, endpoint: url) { result in Account.validateCredentials(type: type, credentials: credentials, endpoint: url, secretsProvider: Secrets()) { result in
self.stopAnimatingActivityIndicator() self.stopAnimatingActivityIndicator()
self.enableNavigation() self.enableNavigation()
@ -199,7 +199,6 @@ class ReaderAPIAccountViewController: UITableViewController {
case .failure(let error): case .failure(let error):
self.showError(error.localizedDescription) self.showError(error.localizedDescription)
} }
} }
} }

View File

@ -58,16 +58,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
var isSyncArticleStatusRunning = false var isSyncArticleStatusRunning = false
var isWaitingForSyncTasks = false var isWaitingForSyncTasks = false
private var secretsProvider = Secrets()
override init() { override init() {
super.init() super.init()
appDelegate = self appDelegate = self
SecretsManager.provider = Secrets()
let documentFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let documentFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString
let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7))) let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7)))
AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath) AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath, secretsProvider: secretsProvider)
let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString
let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7))) let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7)))
ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath) ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath)

View File

@ -657,7 +657,7 @@ private extension WebViewController {
func startArticleExtractor() { func startArticleExtractor() {
guard articleExtractor == nil else { return } guard articleExtractor == nil else { return }
if let link = article?.preferredLink, let extractor = ArticleExtractor(link) { if let link = article?.preferredLink, let extractor = ArticleExtractor(link, secretsProvider: Secrets()) {
extractor.delegate = self extractor.delegate = self
extractor.process() extractor.process()
articleExtractor = extractor articleExtractor = extractor

View File

@ -197,7 +197,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
addViewController.delegate = self addViewController.delegate = self
present(navController, animated: true) present(navController, animated: true)
case .feedly: case .feedly:
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly, secretsProvider: Secrets())
addAccount.delegate = self addAccount.delegate = self
addAccount.presentationAnchor = self.view.window! addAccount.presentationAnchor = self.view.window!
MainThreadOperationQueue.shared.add(addAccount) MainThreadOperationQueue.shared.add(addAccount)