Skip to content
This repository has been archived by the owner on May 22, 2018. It is now read-only.

Commit

Permalink
Merge pull request #183 from ptoomey3/new_github_authorizations_api
Browse files Browse the repository at this point in the history
add support for new GitHub authorizations API using fingerprint
  • Loading branch information
MikeMcQuaid committed May 18, 2015
2 parents 0c3e039 + 8389630 commit 0213484
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 7 deletions.
2 changes: 1 addition & 1 deletion boxen.gemspec
Expand Up @@ -16,7 +16,7 @@ Gem::Specification.new do |gem|

gem.add_dependency "ansi", "~> 1.4"
gem.add_dependency "hiera", "~> 1.0"
gem.add_dependency "highline", "~> 1.6"
gem.add_dependency "highline", "~> 1.6.0"
gem.add_dependency "json_pure", [">= 1.7.7", "< 2.0"]
gem.add_dependency "librarian-puppet", "~> 1.0.0"
gem.add_dependency "octokit", "~> 2.7", ">= 2.7.1"
Expand Down
53 changes: 47 additions & 6 deletions lib/boxen/preflight/creds.rb
@@ -1,6 +1,8 @@
require "boxen/preflight"
require "highline"
require "octokit"
require "digest"
require "socket"

# HACK: Unless this is `false`, HighLine has some really bizarre
# problems with empty/expended streams at bizarre intervals.
Expand Down Expand Up @@ -68,12 +70,27 @@ def run
fetch_login_and_password
tokens = get_tokens

unless auth = tokens.detect { |a| a.note == "Boxen" }
auth = tmp_api.create_authorization \
:note => "Boxen",
:scopes => %w(repo user),
:headers => headers
end
# Boxen now supports the updated GitHub Authorizations API by using a unique
# `fingerprint` for each Boxen installation for a user. We delete any older
# authorization that does not make use of `fingerprint` so that the "legacy"
# authorization doesn't persist in the user's list of personal access
# tokens.
legacy_auth = tokens.detect { |a| a.note == "Boxen" && a.fingerprint == nil }
tmp_api.delete_authorization(legacy_auth.id, :headers => headers) if legacy_auth

# The updated GitHub authorizations API, in order to improve security, no
# longer returns a plaintext `token` for existing authorizations. So, if an
# authorization already exists for this machine we need to first delete it
# so that we can create a new one.
auth = tokens.detect { |a| a.note == note && a.fingerprint == fingerprint }
tmp_api.delete_authorization(auth.id, :headers => headers) if auth

auth = tmp_api.create_authorization(
:note => note,
:scopes => %w(repo user),
:fingerprint => fingerprint,
:headers => headers
)

config.token = auth.token

Expand Down Expand Up @@ -105,4 +122,28 @@ def fetch_from_env(thing)
warn "Oh, looks like you've provided your #{thing} as environmental variable..."
found
end

def fingerprint
@fingerprint ||= begin
# See Apple technical note TN1103, "Uniquely Identifying a Macintosh
# Computer."
serial_number_match_data = IO.popen(
["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"]
).read.match(/"IOPlatformSerialNumber" = "([[:alnum:]]+)"/)
if serial_number_match_data
# The fingerprint must be unique across all personal access tokens for a
# given user. We prefix the serial number with the application name to
# differentiate between any other personal access token that uses the
# Mac serial number for the fingerprint.
Digest::SHA256.hexdigest("Boxen: #{serial_number_match_data[1]}")
else
abort "Sorry, I was unable to obtain your Mac's serial number.",
"Boxen requires access to your Mac's serial number in order to generate a unique GitHub personal access token."
end
end
end

def note
@note ||= "Boxen: #{Socket.gethostname}"
end
end
97 changes: 97 additions & 0 deletions test/boxen_preflight_creds_test.rb
Expand Up @@ -3,6 +3,9 @@
require 'boxen/preflight/creds'

class BoxenPreflightCredsTest < Boxen::Test
# Make a struct to use for stubbing out authorization objects.
Struct.new("Authorization", :id, :note, :fingerprint, :token)

def setup
@config = Boxen::Config.new do |c|
c.user = 'mojombo'
Expand Down Expand Up @@ -77,4 +80,98 @@ def test_fetch_login_when_password_is_given_in_env
assert_equal "l", @config.login
assert_equal "p", preflight.instance_variable_get(:@password)
end

def test_run_with_existing_token
preflight = Boxen::Preflight::Creds.new @config
note = "App1"
fingerprint = "Fingerprint1"
existing_token = Struct::Authorization.new(1, note, fingerprint, "Token1")

preflight.stubs(:fetch_login_and_password).returns("")
preflight.stubs(:get_tokens).returns([existing_token])
preflight.stubs(:note).returns(note)
preflight.stubs(:fingerprint).returns(fingerprint)
preflight.stubs(:ok?).returns(true)
preflight.tmp_api.expects(:delete_authorization).with(existing_token.id, :headers => {})
preflight.tmp_api.expects(:create_authorization).with(
:note => note,
:fingerprint => fingerprint,
:scopes => %w(repo user),
:headers => {}
).returns(existing_token)

preflight.run
end

def test_run_with_no_existing_token
preflight = Boxen::Preflight::Creds.new @config
note = "App1"
fingerprint = "Fingerprint1"
new_token = Struct::Authorization.new(1, note, fingerprint, "Token1")

preflight.stubs(:fetch_login_and_password).returns("")
preflight.stubs(:get_tokens).returns([])
preflight.stubs(:note).returns(note)
preflight.stubs(:fingerprint).returns(fingerprint)
preflight.stubs(:ok?).returns(true)
preflight.tmp_api.expects(:delete_authorization).never
preflight.tmp_api.expects(:create_authorization).with(
:note => note,
:fingerprint => fingerprint,
:scopes => %w(repo user),
:headers => {}
).returns(new_token)

preflight.run
end

def test_run_does_not_delete_unrelated_tokens
preflight = Boxen::Preflight::Creds.new @config
note = "App1"
fingerprint = "Fingerprint1"
new_token = Struct::Authorization.new(1, note, fingerprint, "Token1")
unrelated_token = Struct::Authorization.new(2, "App2", fingerprint, "Token2")
unrelated_token_with_fingerprint = Struct::Authorization.new(3, "App3", "Fingerprint3", "Token3")

preflight.stubs(:fetch_login_and_password).returns("")
preflight.stubs(:get_tokens).returns(
[unrelated_token, unrelated_token_with_fingerprint]
)
preflight.stubs(:note).returns(note)
preflight.stubs(:fingerprint).returns(fingerprint)
preflight.stubs(:ok?).returns(true)
preflight.tmp_api.expects(:delete_authorization).never
preflight.tmp_api.expects(:create_authorization).with(
:note => note,
:fingerprint => fingerprint,
:scopes => %w(repo user),
:headers => {}
).returns(new_token)

preflight.run
end

def test_run_does_delete_legacy_token
preflight = Boxen::Preflight::Creds.new @config
note = "App1"
fingerprint = "Fingerprint1"
existing_token = Struct::Authorization.new(1, note, fingerprint, "Token1")
legacy_token = Struct::Authorization.new(2, "Boxen", nil, "Token2")

preflight.stubs(:fetch_login_and_password).returns("")
preflight.stubs(:get_tokens).returns([existing_token, legacy_token])
preflight.stubs(:note).returns(note)
preflight.stubs(:fingerprint).returns(fingerprint)
preflight.stubs(:ok?).returns(true)
preflight.tmp_api.expects(:delete_authorization).with(2, :headers => {})
preflight.tmp_api.expects(:delete_authorization).with(1, :headers => {})
preflight.tmp_api.expects(:create_authorization).with(
:note => note,
:fingerprint => fingerprint,
:scopes => %w(repo user),
:headers => {}
).returns(existing_token)

preflight.run
end
end

0 comments on commit 0213484

Please sign in to comment.