Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication refactoring #215

Merged
merged 18 commits into from
Jan 27, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions BuildaGitServer/Base/Authentication.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// Authentication.swift
// Buildasaur
//
// Created by Honza Dvorsky on 1/26/16.
// Copyright © 2016 Honza Dvorsky. All rights reserved.
//

import Foundation
import BuildaUtils

public struct ProjectAuthenticator {

public enum AuthType: String {
case PersonalToken
case OAuthToken
}

public let service: GitService
public let username: String
public let type: AuthType
public let secret: String

public init(service: GitService, username: String, type: AuthType, secret: String) {
self.service = service
self.username = username
self.type = type
self.secret = secret
}
}

public protocol KeychainStringSerializable {
static func fromString(value: String) throws -> Self
func toString() -> String
}

extension ProjectAuthenticator: KeychainStringSerializable {

public static func fromString(value: String) throws -> ProjectAuthenticator {

let comps = value.componentsSeparatedByString(":")
guard comps.count >= 4 else { throw Error.withInfo("Corrupted keychain string") }
guard let service = GitService(rawValue: comps[0]) else {
throw Error.withInfo("Unsupported service: \(comps[0])")
}
guard let type = ProjectAuthenticator.AuthType(rawValue: comps[2]) else {
throw Error.withInfo("Unsupported auth type: \(comps[2])")
}
//join the rest back in case we have ":" in the token
let remaining = comps.dropFirst(3).joinWithSeparator(":")
let auth = ProjectAuthenticator(service: service, username: comps[1], type: type, secret: remaining)
return auth
}

public func toString() -> String {

return [
self.service.rawValue,
self.username,
self.type.rawValue,
self.secret
].joinWithSeparator(":")
}
}
39 changes: 10 additions & 29 deletions BuildaGitServer/Base/BaseTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,22 @@ public protocol SourceServerType: BuildStatusCreator {
func getCommentsOfIssue(issueNumber: Int, repo: String, completion: (comments: [CommentType]?, error: ErrorType?) -> ())
}

public enum SourceServerOption {
case Token(String)
}

extension SourceServerOption: Hashable {
public var hashValue: Int {
switch self {
case .Token(_):
return 1
}
}
}

public func ==(lhs: SourceServerOption, rhs: SourceServerOption) -> Bool {

if case .Token(let lhsToken) = lhs, case .Token(let rhsToken) = rhs {
return lhsToken == rhsToken
}

return false
}

public class SourceServerFactory {

public init() { }

public func createServer(config: Set<SourceServerOption>) -> SourceServerType {
public func createServer(service: GitService, auth: ProjectAuthenticator?) -> SourceServerType {

if let auth = auth {
precondition(service == auth.service)
}

//TODO: generalize
if let tokenOption = config.first,
case .Token(let token) = tokenOption {
return GitHubFactory.server(token)
switch service {
case .GitHub:
return GitHubFactory.server(auth)
case .BitBucket:
fatalError("Not implemented yet")
}
preconditionFailure("Insufficient data provided to create a source server")
}
}

Expand Down
14 changes: 9 additions & 5 deletions BuildaGitServer/GitHub/GitHubEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ class GitHubEndpoints {
}

private let baseURL: String
private let token: String?
private let auth: ProjectAuthenticator?

init(baseURL: String, token: String?) {
init(baseURL: String, auth: ProjectAuthenticator?) {
self.baseURL = baseURL
self.token = token
self.auth = auth
}

private func endpointURL(endpoint: Endpoint, params: [String: String]? = nil) -> String {
Expand Down Expand Up @@ -152,8 +152,12 @@ class GitHubEndpoints {
let request = NSMutableURLRequest(URL: url)

request.HTTPMethod = method.rawValue
if let token = self.token {
request.setValue("token \(token)", forHTTPHeaderField:"Authorization")
if let auth = self.auth {

switch auth.type {
case .PersonalToken, .OAuthToken:
request.setValue("token \(auth.secret)", forHTTPHeaderField:"Authorization")
}
}

if let body = body {
Expand Down
6 changes: 3 additions & 3 deletions BuildaGitServer/GitHub/GitHubFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import Foundation

class GitHubFactory {

class func server(token: String?) -> GitHubServer {

class func server(auth: ProjectAuthenticator?) -> GitHubServer {
let baseURL = "https://api.github.com"
let endpoints = GitHubEndpoints(baseURL: baseURL, token: token)
let endpoints = GitHubEndpoints(baseURL: baseURL, auth: auth)

let server = GitHubServer(endpoints: endpoints)
return server
Expand Down
2 changes: 1 addition & 1 deletion BuildaGitServer/GitHub/GitHubServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class GitHubServer : GitServer {
init(endpoints: GitHubEndpoints, http: HTTP? = nil) {

self.endpoints = endpoints
super.init(http: http)
super.init(service: .GitHub, http: http)
}
}

Expand Down
25 changes: 25 additions & 0 deletions BuildaGitServer/GitServerPublic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,32 @@
import Foundation
import BuildaUtils

public enum GitService: String {
case GitHub = "github"
case BitBucket = "bitbucket"
// case GitLab = "gitlab"

public func prettyName() -> String {
switch self {
case .GitHub: return "GitHub"
case .BitBucket: return "BitBucket"
}
}

public func logoName() -> String {
switch self {
case .GitHub: return "github"
case .BitBucket: return "bitbucket"
}
}
}

public class GitServer : HTTPServer {
let service: GitService

init(service: GitService, http: HTTP? = nil) {
self.service = service
super.init(http: http)
}
}

7 changes: 2 additions & 5 deletions BuildaKit/NetworkUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ public class NetworkUtils {

public class func checkAvailabilityOfGitHubWithCurrentSettingsOfProject(project: Project, completion: (success: Bool, error: ErrorType?) -> ()) {

let token = project.config.value.serverAuthentication!
//TODO: have project spit out Set<SourceServerOption>

let options: Set<SourceServerOption> = [.Token(token)]
let server: SourceServerType = SourceServerFactory().createServer(options)
let auth = project.config.value.serverAuthentication
let server = SourceServerFactory().createServer(.GitHub, auth: auth)

let credentialValidationBlueprint = project.createSourceControlBlueprintForCredentialVerification()

Expand Down
63 changes: 61 additions & 2 deletions BuildaKit/PersistenceMigrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import BuildaUtils
import XcodeServerSDK
@testable import BuildaGitServer

public protocol MigratorType {
init(persistence: Persistence)
Expand Down Expand Up @@ -49,7 +50,8 @@ public class CompositeMigrator: MigratorType {
self.childMigrators = [
Migrator_v0_v1(persistence: persistence),
Migrator_v1_v2(persistence: persistence),
Migrator_v2_v3(persistence: persistence)
Migrator_v2_v3(persistence: persistence),
Migrator_v3_v4(persistence: persistence)
]
}

Expand Down Expand Up @@ -276,7 +278,7 @@ class Migrator_v1_v2: MigratorType {

/*
- ServerConfigs.json: password moved to the keychain
- Projects.json: github_token -> server_authentication, ssh_passphrase moved to keychain
- Projects.json: github_token -> oauth_tokens keychain, ssh_passphrase moved to keychain
- move any .log files to a separate folder called 'Logs'
*/
class Migrator_v2_v3: MigratorType {
Expand Down Expand Up @@ -381,3 +383,60 @@ class Migrator_v2_v3: MigratorType {
}
}

/*
- keychain oauth_tokens need to be prepended with the service, username etc.
- "token1234" -> "github:username:personaltoken:token1234"
- unfortunately we haven't kept the username anywhere, so we'll just put
- "GIT" there instead.
*/
class Migrator_v3_v4: MigratorType {

internal var persistence: Persistence
required init(persistence: Persistence) {
self.persistence = persistence
}

func isMigrationRequired() -> Bool {

return self.persistenceVersion() == 3
}

func attemptMigration() throws {

let pers = self.persistence

//migrate
self.migrateKeychainTokens()

//copy the rest
pers.copyFileToWriteLocation("Projects.json", isDirectory: false)
pers.copyFileToWriteLocation("ServerConfigs.json", isDirectory: false)
pers.copyFileToWriteLocation("Syncers.json", isDirectory: false)
pers.copyFileToWriteLocation("BuildTemplates", isDirectory: true)
pers.copyFileToWriteLocation("Triggers", isDirectory: true)
pers.copyFileToWriteLocation("Logs", isDirectory: true)

let config = self.config()
let mutableConfig = config.mutableCopy() as! NSMutableDictionary
mutableConfig[kPersistenceVersion] = 4

//save the updated config
pers.saveDictionary("Config.json", item: mutableConfig)
}

func migrateKeychainTokens() {

let tokenKeychain = SecurePersistence.sourceServerTokenKeychain()

tokenKeychain.readAll().forEach { (id, key) in
//all keys migrated are github personal tokens
let auth = ProjectAuthenticator(service: .GitHub, username: "GIT", type: .PersonalToken, secret: key)
let formatted = auth.toString()
tokenKeychain.writeIfNeeded(id, value: formatted)

precondition(tokenKeychain.read(id) == formatted)
}
}
}


5 changes: 3 additions & 2 deletions BuildaKit/ProjectConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import BuildaUtils
import BuildaGitServer

public struct ProjectConfig {

Expand All @@ -17,13 +18,13 @@ public struct ProjectConfig {
public var publicSSHKeyPath: String

public var sshPassphrase: String? //loaded from the keychain
public var serverAuthentication: String? //loaded from the keychain
public var serverAuthentication: ProjectAuthenticator? //loaded from the keychain

//creates a new default ProjectConfig
public init() {
self.id = Ref.new()
self.url = ""
self.serverAuthentication = ""
self.serverAuthentication = nil
self.privateSSHKeyPath = ""
self.publicSSHKeyPath = ""
self.sshPassphrase = nil
Expand Down
14 changes: 14 additions & 0 deletions BuildaKit/SecurePersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ final class SecurePersistence {
return val
}

func readAll() -> [(String, String)] {
var all: [(String, String)] = []
self.safe.read {
#if TESTING
let keychain = self.keychain
all = keychain.allKeys.map { ($0 as! String, keychain[$0 as! String] as! String) }
#else
let keychain = self.keychain
all = keychain.allKeys().map { ($0, keychain[$0]!) }
#endif
}
return all
}

func writeIfNeeded(key: String, value: String?) {
self.safe.write {
self.updateIfNeeded(key, value: value)
Expand Down
12 changes: 10 additions & 2 deletions BuildaKit/StorageManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
import BuildaUtils
import XcodeServerSDK
import ReactiveCocoa
import BuildaGitServer

public enum StorageManagerError: ErrorType {
case DuplicateServerConfig(XcodeServerConfig)
Expand All @@ -28,6 +29,7 @@ public class StorageManager {
private let persistence: Persistence

public init(persistence: Persistence) {

self.persistence = persistence
self.loadAllFromPersistence()
self.setupSaving()
Expand Down Expand Up @@ -174,7 +176,11 @@ public class StorageManager {
self.projectConfigs.value = allProjects
.map {
(var p: ProjectConfig) -> ProjectConfig in
p.serverAuthentication = tokenKeychain.read(p.keychainKey())
var auth: ProjectAuthenticator?
if let val = tokenKeychain.read(p.keychainKey()) {
auth = try? ProjectAuthenticator.fromString(val)
}
p.serverAuthentication = auth
p.sshPassphrase = passphraseKeychain.read(p.keychainKey())
return p
}.dictionarifyWithKey { $0.id }
Expand Down Expand Up @@ -232,7 +238,9 @@ public class StorageManager {
let tokenKeychain = SecurePersistence.sourceServerTokenKeychain()
let passphraseKeychain = SecurePersistence.sourceServerPassphraseKeychain()
configs.values.forEach {
tokenKeychain.writeIfNeeded($0.keychainKey(), value: $0.serverAuthentication)
if let auth = $0.serverAuthentication {
tokenKeychain.writeIfNeeded($0.keychainKey(), value: auth.toString())
}
passphraseKeychain.writeIfNeeded($0.keychainKey(), value: $0.sshPassphrase)
}
self.persistence.saveArray("Projects.json", items: projectConfigs)
Expand Down
Loading