-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Add token authentication support to SwiftPM #5838
Changes from all commits
22b05e5
658ade5
9b1558d
9d477f0
ad078d2
794dce7
ee27510
68e3a51
45fb652
e0ffeb0
e69f371
51700b1
7b9d74e
e522e8f
53f8036
b7fc62f
40b6bcf
e42d2f5
24e9801
d1268eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
// | ||
// This source file is part of the Swift open source project | ||
// | ||
// Copyright (c) 2021 Apple Inc. and the Swift project authors | ||
// Copyright (c) 2021-2022 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See http://swift.org/LICENSE.txt for license information | ||
|
@@ -22,6 +22,12 @@ public protocol AuthorizationProvider { | |
func authentication(for url: URL) -> (user: String, password: String)? | ||
} | ||
|
||
public protocol AuthorizationWriter { | ||
func addOrUpdate(for url: URL, user: String, password: String, persist: Bool, callback: @escaping (Result<Void, Error>) -> Void) | ||
|
||
func remove(for url: URL, callback: @escaping (Result<Void, Error>) -> Void) | ||
} | ||
|
||
public enum AuthorizationProviderError: Error { | ||
case invalidURLHost | ||
case notFound | ||
|
@@ -53,23 +59,30 @@ private extension URL { | |
|
||
// MARK: - netrc | ||
|
||
public struct NetrcAuthorizationProvider: AuthorizationProvider { | ||
public class NetrcAuthorizationProvider: AuthorizationProvider, AuthorizationWriter { | ||
// marked internal for testing | ||
internal let path: AbsolutePath | ||
private let fileSystem: FileSystem | ||
|
||
private let cache = ThreadSafeKeyValueStore<String, (user: String, password: String)>() | ||
|
||
public init(path: AbsolutePath, fileSystem: FileSystem) throws { | ||
self.path = path | ||
self.fileSystem = fileSystem | ||
// validate file is okay at the time of initializing the provider | ||
_ = try Self.load(fileSystem: fileSystem, path: path) | ||
} | ||
|
||
public mutating func addOrUpdate(for url: URL, user: String, password: String, callback: @escaping (Result<Void, Error>) -> Void) { | ||
public func addOrUpdate(for url: URL, user: String, password: String, persist: Bool = true, callback: @escaping (Result<Void, Error>) -> Void) { | ||
guard let machine = url.authenticationID else { | ||
return callback(.failure(AuthorizationProviderError.invalidURLHost)) | ||
} | ||
|
||
if !persist { | ||
self.cache[machine] = (user, password) | ||
return callback(.success(())) | ||
} | ||
|
||
// Same entry already exists, no need to add or update | ||
let netrc = try? Self.load(fileSystem: self.fileSystem, path: self.path) | ||
guard netrc?.machines.first(where: { $0.name.lowercased() == machine && $0.login == user && $0.password == password }) == nil else { | ||
|
@@ -81,8 +94,8 @@ public struct NetrcAuthorizationProvider: AuthorizationProvider { | |
try self.fileSystem.withLock(on: self.path, type: .exclusive) { | ||
let contents = try? self.fileSystem.readFileContents(self.path).contents | ||
try self.fileSystem.writeFileContents(self.path) { stream in | ||
// File does not exist yet | ||
if let contents = contents { | ||
// Write existing contents | ||
if let contents = contents, !contents.isEmpty { | ||
stream.write(contents) | ||
stream.write("\n") | ||
} | ||
|
@@ -97,8 +110,15 @@ public struct NetrcAuthorizationProvider: AuthorizationProvider { | |
} | ||
} | ||
|
||
public func remove(for url: URL, callback: @escaping (Result<Void, Error>) -> Void) { | ||
callback(.failure(AuthorizationProviderError.other("User must edit netrc file at \(self.path) manually to remove entries"))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't reliably update |
||
} | ||
|
||
public func authentication(for url: URL) -> (user: String, password: String)? { | ||
self.machine(for: url).map { (user: $0.login, password: $0.password) } | ||
if let machine = url.authenticationID, let cached = self.cache[machine] { | ||
return cached | ||
} | ||
return self.machine(for: url).map { (user: $0.login, password: $0.password) } | ||
} | ||
|
||
private func machine(for url: URL) -> Basics.Netrc.Machine? { | ||
|
@@ -133,17 +153,25 @@ public struct NetrcAuthorizationProvider: AuthorizationProvider { | |
// MARK: - Keychain | ||
|
||
#if canImport(Security) | ||
public struct KeychainAuthorizationProvider: AuthorizationProvider { | ||
public class KeychainAuthorizationProvider: AuthorizationProvider, AuthorizationWriter { | ||
private let observabilityScope: ObservabilityScope | ||
|
||
private let cache = ThreadSafeKeyValueStore<String, (user: String, password: String)>() | ||
|
||
public init(observabilityScope: ObservabilityScope) { | ||
self.observabilityScope = observabilityScope | ||
} | ||
|
||
public func addOrUpdate(for url: URL, user: String, password: String, callback: @escaping (Result<Void, Error>) -> Void) { | ||
public func addOrUpdate(for url: URL, user: String, password: String, persist: Bool = true, callback: @escaping (Result<Void, Error>) -> Void) { | ||
guard let server = url.authenticationID else { | ||
return callback(.failure(AuthorizationProviderError.invalidURLHost)) | ||
} | ||
|
||
if !persist { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this mainly for testing, or more broadly needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is general feature. |
||
self.cache[server] = (user, password) | ||
return callback(.success(())) | ||
} | ||
|
||
guard let passwordData = password.data(using: .utf8) else { | ||
return callback(.failure(AuthorizationProviderError.cannotEncodePassword)) | ||
} | ||
|
@@ -159,11 +187,30 @@ public struct KeychainAuthorizationProvider: AuthorizationProvider { | |
} | ||
} | ||
|
||
public func remove(for url: URL, callback: @escaping (Result<Void, Error>) -> Void) { | ||
guard let server = url.authenticationID else { | ||
return callback(.failure(AuthorizationProviderError.invalidURLHost)) | ||
} | ||
|
||
let `protocol` = self.protocol(for: url) | ||
|
||
do { | ||
try self.delete(server: server, protocol: `protocol`) | ||
callback(.success(())) | ||
} catch { | ||
callback(.failure(error)) | ||
} | ||
} | ||
|
||
public func authentication(for url: URL) -> (user: String, password: String)? { | ||
guard let server = url.authenticationID else { | ||
return nil | ||
} | ||
|
||
if let cached = self.cache[server] { | ||
return cached | ||
} | ||
|
||
do { | ||
guard let existingItem = try self.search(server: server, protocol: self.protocol(for: url)) as? [String: Any], | ||
let passwordData = existingItem[kSecValueData as String] as? Data, | ||
|
@@ -216,6 +263,16 @@ public struct KeychainAuthorizationProvider: AuthorizationProvider { | |
return true | ||
} | ||
|
||
private func delete(server: String, protocol: CFString) throws { | ||
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, | ||
kSecAttrServer as String: server, | ||
kSecAttrProtocol as String: `protocol`] | ||
let status = SecItemDelete(query as CFDictionary) | ||
guard status == errSecSuccess else { | ||
throw AuthorizationProviderError.other("Failed to delete credentials for server \(server) from keychain: status \(status)") | ||
} | ||
} | ||
|
||
private func search(server: String, protocol: CFString) throws -> CFTypeRef? { | ||
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, | ||
kSecAttrServer as String: server, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for adding credentials to memory so
RegistryClient
can read them for thelogin
request.