mirror of https://github.com/cirruslabs/tart.git
tart login: verify credentials (#102)
* tart login: verify credentials * Make DictionaryCredentialsProvider fileprivate to Login.swift
This commit is contained in:
parent
afa6b7b46c
commit
f3068b9055
|
|
@ -10,9 +10,21 @@ struct Login: AsyncParsableCommand {
|
|||
|
||||
func run() async throws {
|
||||
do {
|
||||
let (user, password) = try Credentials.retrieveStdin()
|
||||
let (user, password) = try StdinCredentials.retrieve()
|
||||
let credentialsProvider = DictionaryCredentialsProvider([
|
||||
host: (user, password)
|
||||
])
|
||||
|
||||
try Credentials.store(host: host, user: user, password: password)
|
||||
do {
|
||||
let registry = try Registry(host: host, namespace: "", credentialsProvider: credentialsProvider)
|
||||
try await registry.ping()
|
||||
} catch {
|
||||
print("invalid credentials: \(error)")
|
||||
|
||||
Foundation.exit(1)
|
||||
}
|
||||
|
||||
try KeychainCredentialsProvider().store(host: host, user: user, password: password)
|
||||
|
||||
Foundation.exit(0)
|
||||
} catch {
|
||||
|
|
@ -22,3 +34,19 @@ struct Login: AsyncParsableCommand {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class DictionaryCredentialsProvider: CredentialsProvider {
|
||||
var credentials: Dictionary<String, (String, String)>
|
||||
|
||||
init(_ credentials: Dictionary<String, (String, String)>) {
|
||||
self.credentials = credentials
|
||||
}
|
||||
|
||||
func retrieve(host: String) throws -> (String, String)? {
|
||||
credentials[host]
|
||||
}
|
||||
|
||||
func store(host: String, user: String, password: String) throws {
|
||||
credentials[host] = (user, password)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum CredentialsError: Error {
|
||||
case CredentialRequired(which: String)
|
||||
case CredentialTooLong(message: String)
|
||||
}
|
||||
|
||||
class Credentials {
|
||||
static func retrieveKeychain(host: String) throws -> (String, String)? {
|
||||
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
|
||||
kSecAttrServer as String: host,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecReturnData as String: true,
|
||||
kSecAttrLabel as String: "Tart Credentials",
|
||||
]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
|
||||
if status != errSecSuccess {
|
||||
if status == errSecItemNotFound {
|
||||
return nil
|
||||
}
|
||||
|
||||
throw RegistryError.AuthFailed(why: "Keychain returned unsuccessful status \(status)")
|
||||
}
|
||||
|
||||
guard let item = item as? [String: Any],
|
||||
let user = item[kSecAttrAccount as String] as? String,
|
||||
let passwordData = item[kSecValueData as String] as? Data,
|
||||
let password = String(data: passwordData, encoding: .utf8)
|
||||
else {
|
||||
throw RegistryError.AuthFailed(why: "Keychain item has unexpected format")
|
||||
}
|
||||
|
||||
return (user, password)
|
||||
}
|
||||
|
||||
static func retrieveStdin() throws -> (String, String) {
|
||||
let user = try readStdinCredential(name: "username", prompt: "User: ", isSensitive: false)
|
||||
let password = try readStdinCredential(name: "password", prompt: "Password: ", isSensitive: true)
|
||||
|
||||
return (user, password)
|
||||
}
|
||||
|
||||
private static func readStdinCredential(name: String, prompt: String, maxCharacters: Int = 255, isSensitive: Bool) throws -> String {
|
||||
var buf = [CChar](repeating: 0, count: maxCharacters + 1 /* sentinel */ + 1 /* NUL */)
|
||||
guard let rawCredential = readpassphrase(prompt, &buf, buf.count, isSensitive ? RPP_ECHO_OFF : RPP_ECHO_ON) else {
|
||||
throw CredentialsError.CredentialRequired(which: name)
|
||||
}
|
||||
|
||||
let credential = String(cString: rawCredential).trimmingCharacters(in: .newlines)
|
||||
|
||||
if credential.count > maxCharacters {
|
||||
throw CredentialsError.CredentialTooLong(
|
||||
message: "\(name) should contain no more than \(maxCharacters) characters")
|
||||
}
|
||||
|
||||
return credential
|
||||
}
|
||||
|
||||
static func store(host: String, user: String, password: String) throws {
|
||||
let attributes: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: user,
|
||||
kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
|
||||
kSecAttrServer as String: host,
|
||||
kSecValueData as String: password,
|
||||
kSecAttrLabel as String: "Tart Credentials",
|
||||
]
|
||||
|
||||
let status = SecItemAdd(attributes as CFDictionary, nil)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess, errSecDuplicateItem:
|
||||
return
|
||||
default:
|
||||
throw RegistryError.AuthFailed(why: "Keychain returned unsuccessful status \(status)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import Foundation
|
||||
|
||||
enum CredentialsProviderError: Error {
|
||||
case Failed(message: String)
|
||||
}
|
||||
|
||||
protocol CredentialsProvider {
|
||||
func retrieve(host: String) throws -> (String, String)?
|
||||
func store(host: String, user: String, password: String) throws
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import Foundation
|
||||
|
||||
class KeychainCredentialsProvider: CredentialsProvider {
|
||||
func retrieve(host: String) throws -> (String, String)? {
|
||||
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
|
||||
kSecAttrServer as String: host,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecReturnData as String: true,
|
||||
kSecAttrLabel as String: "Tart Credentials",
|
||||
]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
|
||||
if status != errSecSuccess {
|
||||
if status == errSecItemNotFound {
|
||||
return nil
|
||||
}
|
||||
|
||||
throw CredentialsProviderError.Failed(message: "Keychain returned unsuccessful status \(status)")
|
||||
}
|
||||
|
||||
guard let item = item as? [String: Any],
|
||||
let user = item[kSecAttrAccount as String] as? String,
|
||||
let passwordData = item[kSecValueData as String] as? Data,
|
||||
let password = String(data: passwordData, encoding: .utf8)
|
||||
else {
|
||||
throw CredentialsProviderError.Failed(message: "Keychain item has unexpected format")
|
||||
}
|
||||
|
||||
return (user, password)
|
||||
}
|
||||
|
||||
func store(host: String, user: String, password: String) throws {
|
||||
let attributes: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: user,
|
||||
kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
|
||||
kSecAttrServer as String: host,
|
||||
kSecValueData as String: password,
|
||||
kSecAttrLabel as String: "Tart Credentials",
|
||||
]
|
||||
|
||||
let status = SecItemAdd(attributes as CFDictionary, nil)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess, errSecDuplicateItem:
|
||||
return
|
||||
default:
|
||||
throw CredentialsProviderError.Failed(message: "Keychain returned unsuccessful status \(status)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import Foundation
|
||||
|
||||
enum StdinCredentialsError: Error {
|
||||
case CredentialRequired(which: String)
|
||||
case CredentialTooLong(message: String)
|
||||
}
|
||||
|
||||
class StdinCredentials {
|
||||
static func retrieve() throws -> (String, String) {
|
||||
let user = try readStdinCredential(name: "username", prompt: "User: ", isSensitive: false)
|
||||
let password = try readStdinCredential(name: "password", prompt: "Password: ", isSensitive: true)
|
||||
|
||||
return (user, password)
|
||||
}
|
||||
|
||||
private static func readStdinCredential(name: String, prompt: String, maxCharacters: Int = 255, isSensitive: Bool) throws -> String {
|
||||
var buf = [CChar](repeating: 0, count: maxCharacters + 1 /* sentinel */ + 1 /* NUL */)
|
||||
guard let rawCredential = readpassphrase(prompt, &buf, buf.count, isSensitive ? RPP_ECHO_OFF : RPP_ECHO_ON) else {
|
||||
throw StdinCredentialsError.CredentialRequired(which: name)
|
||||
}
|
||||
|
||||
let credential = String(cString: rawCredential).trimmingCharacters(in: .newlines)
|
||||
|
||||
if credential.count > maxCharacters {
|
||||
throw StdinCredentialsError.CredentialTooLong(
|
||||
message: "\(name) should contain no more than \(maxCharacters) characters")
|
||||
}
|
||||
|
||||
return credential
|
||||
}
|
||||
}
|
||||
|
|
@ -79,24 +79,33 @@ class Registry {
|
|||
try! httpClient.syncShutdown()
|
||||
}
|
||||
|
||||
var baseURL: URL
|
||||
var namespace: String
|
||||
let baseURL: URL
|
||||
let namespace: String
|
||||
let credentialsProvider: CredentialsProvider
|
||||
|
||||
var currentAuthToken: TokenResponse? = nil
|
||||
|
||||
init(urlComponents: URLComponents, namespace: String) throws {
|
||||
init(urlComponents: URLComponents,
|
||||
namespace: String,
|
||||
credentialsProvider: CredentialsProvider = KeychainCredentialsProvider()
|
||||
) throws {
|
||||
baseURL = urlComponents.url!
|
||||
self.namespace = namespace
|
||||
self.credentialsProvider = credentialsProvider
|
||||
}
|
||||
|
||||
convenience init(host: String, namespace: String) throws {
|
||||
convenience init(
|
||||
host: String,
|
||||
namespace: String,
|
||||
credentialsProvider: CredentialsProvider = KeychainCredentialsProvider()
|
||||
) throws {
|
||||
var baseURLComponents = URLComponents()
|
||||
|
||||
baseURLComponents.scheme = "https"
|
||||
baseURLComponents.host = host
|
||||
baseURLComponents.path = "/v2/"
|
||||
|
||||
try self.init(urlComponents: baseURLComponents, namespace: namespace)
|
||||
try self.init(urlComponents: baseURLComponents, namespace: namespace, credentialsProvider: credentialsProvider)
|
||||
}
|
||||
|
||||
func ping() async throws {
|
||||
|
|
@ -285,7 +294,7 @@ class Registry {
|
|||
|
||||
var headers: Dictionary<String, String> = Dictionary()
|
||||
|
||||
if let (user, password) = try Credentials.retrieveKeychain(host: baseURL.host!) {
|
||||
if let (user, password) = try credentialsProvider.retrieve(host: baseURL.host!) {
|
||||
let encodedCredentials = "\(user):\(password)".data(using: .utf8)?.base64EncodedString()
|
||||
headers["Authorization"] = "Basic \(encodedCredentials!)"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue