github-auth 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 103b97306c0adbb26e02a782f02c8244557fb665
4
+ data.tar.gz: b95cbdc7fb511a1346bbf7bb222034c8a1103f60
5
+ SHA512:
6
+ metadata.gz: 4ad87cd1b81d17f581e209cfc4f190a5135f7e8b141fd421968e51ff221d8bf25083701efde8cf45b1affc6e1343e13cefc23c2be4099fc62aed2125bdeb0227
7
+ data.tar.gz: 03ec6d0356ecb5ba1c12f56e7edbd5ab284bfa151aab0d1453d001bc9acac248bce461b70749f17072e18ea87a428c9be9a824d0cc2ecb39bda9c5eacc406ba3
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.swp
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in github-auth.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Chris Hunt
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # gh-auth
2
+ [![Travis CI](https://travis-ci.org/chrishunt/github-auth.png)](https://travis-ci.org/chrishunt/github-auth)
3
+ [![Coverage Status](https://coveralls.io/repos/chrishunt/github-auth/badge.png?branch=master)](https://coveralls.io/r/chrishunt/github-auth)
4
+ [![Code Climate](https://codeclimate.com/github/chrishunt/github-auth.png)](https://codeclimate.com/github/chrishunt/github-auth)
5
+
6
+ ## Description
7
+
8
+ If you decide to [`#pairwithme`](https://twitter.com/search?q=%23pairwithme),
9
+ we'll probably be SSHing into my laptop, your laptop, or some laptop in the
10
+ sky. Since I'd rather not send you a password over email or Skype, we'll use
11
+ public key authentication.
12
+
13
+ `gh-auth` allows you to easily add and remove any Github user's public ssh keys
14
+ from your [`authorized_keys`](http://en.wikipedia.org/wiki/Ssh-agent) file.
15
+
16
+ Let's say you'd like to pair with me, just run:
17
+
18
+ ```bash
19
+ $ gh-auth add chrishunt
20
+ Adding 2 key(s) to '/Users/chris/.ssh/authorized_keys'
21
+ ```
22
+
23
+ Now I can ssh into your machine! That was easy. When we're done working, you
24
+ can revoke my access with:
25
+
26
+ ```bash
27
+ $ gh-auth remove chrishunt
28
+ Removing 2 key(s) from '/Users/chris/.ssh/authorized_keys'
29
+ ```
30
+
31
+ You can add and remove any number of users at the same time.
32
+
33
+ ```bash
34
+ $ gh-auth add chrishunt zachmargolis
35
+ Adding 4 key(s) to '/Users/chris/.ssh/authorized_keys'
36
+
37
+ $ gh-auth remove chrishunt
38
+ Removing 2 key(s) from '/Users/chris/.ssh/authorized_keys'
39
+
40
+ $ gh-auth remove zachmargolis
41
+ Removing 2 key(s) from '/Users/chris/.ssh/authorized_keys'
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```bash
47
+ usage: gh-auth [add|remove] <username>
48
+ ```
49
+
50
+ ## Installation
51
+
52
+ Install the `github-auth` gem:
53
+
54
+ ```bash
55
+ $ gem install github-auth
56
+ ```
57
+
58
+ ### SSH Public Key Authentication (Mac OS X)
59
+
60
+ Public key authentication works with Mac OS by default, but you'll need to get
61
+ your ssh server running. This is done by ticking 'Remote Login' in the
62
+ 'Sharing' panel of System Preferences.
63
+
64
+ ![]('https://raw.github.com/chrishunt/github-auth/master/img/mac-os-ssh-sharing.png')
65
+
66
+ Now that SSH is running, make sure you have the correct permissions set for
67
+ your authorized keys.
68
+
69
+ ```bash
70
+ $ chmod 700 ~/.ssh
71
+ $ chmod 600 ~/.ssh/authorized_keys
72
+ ```
73
+
74
+ ### Verification
75
+
76
+ If you'd like to verify that everything is working as expected, you can test
77
+ right from your machine.
78
+
79
+ First, authorized yourself for ssh. (Make sure to replace 'chrishunt' with
80
+ *your* Github username)
81
+
82
+ ```bash
83
+ $ gh-auth add chrishunt
84
+ Adding 2 key(s) to '/Users/chris/.ssh/authorized_keys'
85
+ ```
86
+
87
+ Next, open an SSH session to your machine with public key authentication. It
88
+ should work just fine.
89
+
90
+ ```bash
91
+ $ ssh -o PreferredAuthentications=publickey localhost
92
+
93
+ (localhost)$
94
+ ```
95
+
96
+ Now remove your public keys from the keys file:
97
+
98
+ ```bash
99
+ $ gh-auth remove chrishunt
100
+ Removing 2 key(s) from '/Users/chris/.ssh/authorized_keys'
101
+ ```
102
+
103
+ You should no longer be able to login to the machine since the keys have been
104
+ removed.
105
+
106
+ ```bash
107
+ $ ssh -o PreferredAuthentications=publickey localhost
108
+
109
+ > Permission denied (publickey,keyboard-interactive)
110
+ ```
111
+
112
+ ## Contributing
113
+
114
+ 1. Fork it
115
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
116
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
117
+ 4. Run the tests (`bundle exec rake spec`)
118
+ 5. Push to the branch (`git push origin my-new-feature`)
119
+ 6. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc 'Run all tests'
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = '--color --order random'
7
+ end
8
+
9
+ task default: :spec
data/bin/gh-auth ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'github/auth'
4
+
5
+ Github::Auth::CLI.new(ARGV).execute
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'github/auth/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'github-auth'
8
+ spec.version = Github::Auth::VERSION
9
+ spec.authors = ['Chris Hunt']
10
+ spec.email = ['c@chrishunt.co']
11
+ spec.description = %q{SSH key management for Github users}
12
+ spec.summary = %q{SSH key management for Github users}
13
+ spec.homepage = 'https://github.com/chrishunt/github-auth'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.3'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_development_dependency 'coveralls'
25
+ spec.add_development_dependency 'sinatra'
26
+ spec.add_development_dependency 'thin'
27
+
28
+ spec.add_runtime_dependency 'httparty'
29
+ end
Binary file
@@ -0,0 +1,4 @@
1
+ require 'github/auth/version'
2
+ require 'github/auth/keys_client'
3
+ require 'github/auth/keys_file'
4
+ require 'github/auth/cli'
@@ -0,0 +1,100 @@
1
+ module Github::Auth
2
+ class CLI
3
+ attr_reader :command, :usernames
4
+
5
+ COMMANDS = %w(add remove)
6
+
7
+ def initialize(argv)
8
+ @command = argv.shift
9
+ @usernames = argv
10
+ end
11
+
12
+ def execute
13
+ if COMMANDS.include?(command) && !usernames.empty?
14
+ send command
15
+ else
16
+ print_usage
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def add
23
+ on_keys_file :write!,
24
+ "Adding #{keys.count} key(s) to '#{keys_file.path}'"
25
+ end
26
+
27
+ def remove
28
+ on_keys_file :delete!,
29
+ "Removing #{keys.count} key(s) from '#{keys_file.path}'"
30
+ end
31
+
32
+ def on_keys_file(action, message)
33
+ puts message
34
+ rescue_keys_file_errors { keys_file.send action, keys }
35
+ end
36
+
37
+ def rescue_keys_file_errors
38
+ yield
39
+ rescue KeysFile::PermissionDeniedError
40
+ print_permission_denied
41
+ rescue KeysFile::FileDoesNotExistError
42
+ print_file_does_not_exist
43
+ end
44
+
45
+ def print_usage
46
+ puts "usage: gh-auth [#{COMMANDS.join '|'}] <username>"
47
+ end
48
+
49
+ def print_permission_denied
50
+ puts 'Permission denied!'
51
+ puts
52
+ puts "Make sure you have write permissions for '#{keys_file.path}'"
53
+ end
54
+
55
+ def print_file_does_not_exist
56
+ puts "Keys file does not exist!"
57
+ puts
58
+ puts "Create one now and try again:"
59
+ puts
60
+ puts " $ touch #{keys_file.path}"
61
+ end
62
+
63
+ def print_github_user_does_not_exist(username)
64
+ puts "Github user '#{username}' does not exist"
65
+ end
66
+
67
+ def print_github_unavailable
68
+ puts "Github appears to be unavailable :("
69
+ puts
70
+ puts "https://status.github.com"
71
+ end
72
+
73
+ def keys
74
+ @keys ||= usernames.map { |username| keys_for username }.flatten.compact
75
+ end
76
+
77
+ def keys_for(username)
78
+ Github::Auth::KeysClient.new(
79
+ hostname: github_hostname,
80
+ username: username
81
+ ).keys
82
+ rescue Github::Auth::KeysClient::GithubUserDoesNotExistError
83
+ print_github_user_does_not_exist username
84
+ rescue Github::Auth::KeysClient::GithubUnavailableError
85
+ print_github_unavailable
86
+ end
87
+
88
+ def keys_file
89
+ Github::Auth::KeysFile.new path: keys_file_path
90
+ end
91
+
92
+ def keys_file_path
93
+ Github::Auth::KeysFile::DEFAULT_PATH
94
+ end
95
+
96
+ def github_hostname
97
+ Github::Auth::KeysClient::DEFAULT_HOSTNAME
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,44 @@
1
+ require 'httparty'
2
+
3
+ module Github::Auth
4
+ class KeysClient
5
+ attr_reader :username, :hostname
6
+
7
+ UsernameRequiredError = Class.new StandardError
8
+ GithubUnavailableError = Class.new StandardError
9
+ GithubUserDoesNotExistError = Class.new StandardError
10
+
11
+ DEFAULT_HOSTNAME = 'https://api.github.com'
12
+
13
+ DEFAULT_OPTIONS = {
14
+ username: nil,
15
+ hostname: DEFAULT_HOSTNAME
16
+ }
17
+
18
+ def initialize(options = {})
19
+ options = DEFAULT_OPTIONS.merge options
20
+ raise UsernameRequiredError unless options.fetch :username
21
+
22
+ @username = options.fetch :username
23
+ @hostname = options.fetch :hostname
24
+ end
25
+
26
+ def keys
27
+ @keys ||= Array(github_response).map { |entry| entry.fetch 'key' }
28
+ end
29
+
30
+ private
31
+
32
+ def github_response
33
+ response = http_client.get "#{hostname}/users/#{username}/keys"
34
+ raise GithubUserDoesNotExistError if response.code == 404
35
+ response.parsed_response
36
+ rescue SocketError, Errno::ECONNREFUSED => e
37
+ raise GithubUnavailableError.new e
38
+ end
39
+
40
+ def http_client
41
+ HTTParty
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,57 @@
1
+ module Github::Auth
2
+ class KeysFile
3
+ attr_reader :path
4
+
5
+ PermissionDeniedError = Class.new StandardError
6
+ FileDoesNotExistError = Class.new StandardError
7
+
8
+ DEFAULT_PATH = '~/.ssh/authorized_keys'
9
+
10
+ def initialize(options = {})
11
+ @path = File.expand_path(options[:path] || DEFAULT_PATH)
12
+ end
13
+
14
+ def write!(keys)
15
+ append_keys_file do |keys_file|
16
+ Array(keys).each do |key|
17
+ keys_file.write "\n#{key}" unless keys_file_content.include? key
18
+ end
19
+ end
20
+ end
21
+
22
+ def delete!(keys)
23
+ new_content = keys_file_content_without keys
24
+
25
+ write_keys_file { |keys_file| keys_file.write new_content }
26
+ end
27
+
28
+ private
29
+
30
+ def append_keys_file(&block)
31
+ with_keys_file 'a', block
32
+ end
33
+
34
+ def write_keys_file(&block)
35
+ with_keys_file 'w', block
36
+ end
37
+
38
+ def with_keys_file(mode, block)
39
+ File.open(path, mode) { |keys_file| block.call keys_file }
40
+ rescue Errno::EACCES => e
41
+ raise PermissionDeniedError.new e
42
+ rescue Errno::ENOENT => e
43
+ raise FileDoesNotExistError.new e
44
+ end
45
+
46
+ def keys_file_content
47
+ File.read path
48
+ end
49
+
50
+ def keys_file_content_without(keys)
51
+ keys_file_content.tap do |content|
52
+ Array(keys).each { |key| content.gsub! key, '' }
53
+ content.strip!
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ module Github
2
+ module Auth
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'support/mock_github_server'
3
+ require 'github/auth'
4
+
5
+ describe Github::Auth::CLI do
6
+ with_mock_github_server do |mock_server_hostname|
7
+ let(:hostname) { mock_server_hostname }
8
+ let(:keys_file) { Tempfile.new 'authorized_keys' }
9
+ let(:keys) { Github::Auth::MockGithubServer::KEYS }
10
+
11
+ after { keys_file.unlink }
12
+
13
+ def cli(argv)
14
+ described_class.new(argv).tap do |cli|
15
+ cli.stub(
16
+ github_hostname: hostname,
17
+ keys_file_path: keys_file.path
18
+ )
19
+ end
20
+ end
21
+
22
+ it 'adds and removes keys from the keys file' do
23
+ cli(%w(add chrishunt)).execute
24
+
25
+ keys_file.read.tap do |content|
26
+ keys.each { |key| expect(content).to include key }
27
+ end
28
+
29
+ cli(%w(remove chrishunt)).execute
30
+
31
+ expect(keys_file.read).to be_empty
32
+
33
+ keys_file.unlink
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+ require 'support/mock_github_server'
3
+ require 'github/auth'
4
+
5
+ describe Github::Auth::KeysClient do
6
+ it 'fetches all keys for the given github user' do
7
+ with_mock_github_server do |hostname, keys|
8
+ client = described_class.new(
9
+ username: 'chrishunt',
10
+ hostname: hostname
11
+ )
12
+
13
+ expect(client.keys).to eq keys
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ require 'tempfile'
2
+ require 'spec_helper'
3
+ require 'github/auth'
4
+
5
+ describe Github::Auth::KeysFile do
6
+ it 'writes and deletes keys from the keys file' do
7
+ tempfile = Tempfile.new 'authorized_keys'
8
+ keys_file = described_class.new path: tempfile.path
9
+ keys = %w(abc123 def456)
10
+
11
+ keys_file.write! keys
12
+ expect(tempfile.read).to include keys.join("\n")
13
+
14
+ keys_file.delete! keys.first
15
+ expect(tempfile.read).to_not include keys.first
16
+
17
+ keys_file.delete! keys.last
18
+ expect(tempfile.read).to_not include keys.last
19
+
20
+ expect(tempfile.read).to be_empty
21
+ end
22
+ end
@@ -0,0 +1,2 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
@@ -0,0 +1,40 @@
1
+ require 'httparty'
2
+ require 'sinatra/base'
3
+ require 'json'
4
+
5
+ module Github::Auth
6
+ class MockGithubServer < Sinatra::Base
7
+ KEYS = %w(abc234 def456)
8
+
9
+ set :port, 8001
10
+
11
+ get '/' do
12
+ 'success'
13
+ end
14
+
15
+ get '/users/chrishunt/keys' do
16
+ content_type :json
17
+
18
+ [
19
+ { 'id' => 123, 'key' => KEYS[0] },
20
+ { 'id' => 456, 'key' => KEYS[1] }
21
+ ].to_json
22
+ end
23
+ end
24
+ end
25
+
26
+ def with_mock_github_server
27
+ hostname = "http://localhost:#{Github::Auth::MockGithubServer.port}"
28
+ Thread.new { Github::Auth::MockGithubServer.run! }
29
+
30
+ while true
31
+ begin
32
+ HTTParty.get(hostname)
33
+ break
34
+ rescue Errno::ECONNREFUSED
35
+ # Do nothing, try again
36
+ end
37
+ end
38
+
39
+ yield hostname, Github::Auth::MockGithubServer::KEYS
40
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'github/auth/cli'
3
+
4
+ describe Github::Auth::CLI do
5
+ let(:argv) { [] }
6
+
7
+ subject { described_class.new argv }
8
+
9
+ describe '#execute' do
10
+ shared_examples_for 'a method that prints usage' do
11
+ it 'prints the usage' do
12
+ subject.should_receive(:print_usage)
13
+ subject.execute
14
+ end
15
+ end
16
+
17
+ context 'when missing a command' do
18
+ let(:argv) { [] }
19
+ it_should_behave_like 'a method that prints usage'
20
+ end
21
+
22
+ context 'with an invalid command' do
23
+ let(:argv) { ['invalid'] }
24
+ it_should_behave_like 'a method that prints usage'
25
+ end
26
+
27
+ context 'when no usernames are provide' do
28
+ let(:argv) { ['add'] }
29
+ it_should_behave_like 'a method that prints usage'
30
+ end
31
+
32
+ context 'with a valid action and usernames' do
33
+ let(:action) { 'add' }
34
+ let(:argv) { [action, 'chrishunt'] }
35
+
36
+ it 'calls the method matching the action name' do
37
+ subject.should_receive(action)
38
+ subject.execute
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+ require 'github/auth/keys_client'
3
+
4
+ describe Github::Auth::KeysClient do
5
+ subject { described_class.new username: username }
6
+
7
+ let(:username) { 'chrishunt' }
8
+ let(:http_client) { stub('HttpClient', get: response) }
9
+ let(:response_code) { 200 }
10
+ let(:parsed_response) { nil }
11
+ let(:response) {
12
+ stub('HTTParty::Response', {
13
+ code: response_code,
14
+ parsed_response: parsed_response
15
+ })
16
+ }
17
+
18
+ before { subject.stub(http_client: http_client) }
19
+
20
+ describe '#initialize' do
21
+ it 'requires a username' do
22
+ expect {
23
+ described_class.new
24
+ }.to raise_error Github::Auth::KeysClient::UsernameRequiredError
25
+
26
+ expect {
27
+ described_class.new username: nil
28
+ }.to raise_error Github::Auth::KeysClient::UsernameRequiredError
29
+ end
30
+
31
+ it 'saves the username' do
32
+ keys_client = described_class.new username: username
33
+ expect(keys_client.username).to eq username
34
+ end
35
+ end
36
+
37
+ describe '#keys' do
38
+ it 'requests keys from the Github API' do
39
+ http_client.should_receive(:get).with(
40
+ "https://api.github.com/users/#{username}/keys"
41
+ )
42
+ subject.keys
43
+ end
44
+
45
+ it 'memoizes the response' do
46
+ http_client.should_receive(:get).once
47
+ 2.times { subject.keys }
48
+ end
49
+
50
+ context 'when the github user has keys' do
51
+ let(:parsed_response) {[
52
+ { 'id' => 123, 'key' => 'BLAHBLAH' },
53
+ { 'id' => 456, 'key' => 'FLARBBLU' }
54
+ ]}
55
+
56
+ it 'returns the keys' do
57
+ expected_keys = parsed_response.map { |entry| entry.fetch 'key' }
58
+ expect(subject.keys).to eq expected_keys
59
+ end
60
+ end
61
+
62
+ context 'when the github user does not have keys' do
63
+ let(:parsed_response) { [] }
64
+
65
+ it 'returns an empty array' do
66
+ expect(subject.keys).to eq []
67
+ end
68
+ end
69
+
70
+ context 'when the github user does not exist' do
71
+ let(:response_code) { 404 }
72
+
73
+ it 'raises GithubUserDoesNotExistError' do
74
+ expect {
75
+ subject.keys
76
+ }.to raise_error Github::Auth::KeysClient::GithubUserDoesNotExistError
77
+ end
78
+ end
79
+
80
+ context 'when there is an issue connecting to Github' do
81
+ [SocketError, Errno::ECONNREFUSED].each do |exception|
82
+ before { http_client.stub(:get).and_raise exception }
83
+
84
+ it 'raises a GithubUnavailableError' do
85
+ expect {
86
+ subject.keys
87
+ }.to raise_error Github::Auth::KeysClient::GithubUnavailableError
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+ require 'github/auth/keys_file'
4
+
5
+ describe Github::Auth::KeysFile do
6
+ subject { described_class.new path: path }
7
+
8
+ let(:keys) { %w(abc123 def456) }
9
+ let(:keys_file) { Tempfile.new 'authorized_keys' }
10
+ let(:path) { keys_file.path }
11
+
12
+ after { keys_file.unlink } # clean up, delete tempfile
13
+
14
+ describe '#initialize' do
15
+ context 'without a path' do
16
+ let(:path) { nil }
17
+
18
+ it 'has a default path' do
19
+ expect(subject.path).to_not be_nil
20
+ end
21
+ end
22
+
23
+ context 'with a custom path' do
24
+ let(:path) { '/foo/bar/baz' }
25
+
26
+ it 'saves the custom path' do
27
+ expect(subject.path).to eq path
28
+ end
29
+ end
30
+
31
+ context 'with an unexpanded path' do
32
+ let(:path) { '~/my/home/dir' }
33
+
34
+ it 'expands the path' do
35
+ expect(subject.path).to_not include '~'
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '#write!' do
41
+ it 'writes each key to the keys file' do
42
+ subject.write! keys
43
+ file_content = keys_file.read
44
+ keys.each { |key| expect(file_content).to include key }
45
+ end
46
+
47
+ context 'with a single key' do
48
+ let(:key) { 'abc123' }
49
+
50
+ it 'writes the single key to the keys file' do
51
+ subject.write! key
52
+ expect(keys_file.read).to include key
53
+ end
54
+ end
55
+
56
+ context 'with existing keys in the keys file' do
57
+ let(:existing_keys) { %w(ghi789 jkl123) }
58
+
59
+ before do
60
+ keys_file.write existing_keys.join("\n")
61
+ keys_file.rewind
62
+ end
63
+
64
+ it 'preserves the existing keys' do
65
+ subject.write! keys
66
+ file_lines = keys_file.readlines
67
+ existing_keys.each { |key| expect(file_lines).to include "#{key}\n" }
68
+ end
69
+
70
+ it 'does not write duplicate keys into the keys file' do
71
+ subject.write! existing_keys.first
72
+ expect(keys_file.readlines.count).to eq existing_keys.count
73
+ end
74
+ end
75
+
76
+ context 'when the keys file is readonly' do
77
+ before { File.chmod(0400, keys_file) }
78
+
79
+ it 'raises PermissionDeniedError' do
80
+ expect {
81
+ subject.write! keys
82
+ }.to raise_error Github::Auth::KeysFile::PermissionDeniedError
83
+ end
84
+ end
85
+
86
+ context 'when the keys file does not exist' do
87
+ let(:path) { 'not/a/real/file/path' }
88
+
89
+ it 'raises FileDoesNotExistError' do
90
+ expect {
91
+ subject.write! keys
92
+ }.to raise_error Github::Auth::KeysFile::FileDoesNotExistError
93
+ end
94
+ end
95
+ end
96
+
97
+ describe '#delete!' do
98
+ before do
99
+ keys_file.write keys.join("\n")
100
+ keys_file.rewind
101
+ end
102
+
103
+ context 'when the keys file has the key' do
104
+ let(:key) { keys[0] }
105
+ let(:other_key) { keys[1] }
106
+
107
+ it 'removes the key from the keys file' do
108
+ subject.delete! key
109
+ expect(keys_file.read).to_not include key
110
+ end
111
+
112
+ it 'does not remove the other key from the keys file' do
113
+ subject.delete! key
114
+ expect(keys_file.read).to include other_key
115
+ end
116
+
117
+ it 'does not leave blank lines' do
118
+ subject.delete! [key, other_key]
119
+ blank_lines = keys_file.readlines.select { |line| line =~ /^$\n/ }
120
+
121
+ expect(blank_lines).to be_empty
122
+ end
123
+ end
124
+
125
+ context 'when the keys file does not have the key' do
126
+ let(:key) { 'not-in-the-keys-file' }
127
+
128
+ it 'does not modify the keys file' do
129
+ original_keys_file = keys_file.read
130
+ keys_file.rewind
131
+
132
+ subject.delete! key
133
+
134
+ expect(keys_file.read).to eq original_keys_file
135
+ end
136
+ end
137
+ end
138
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: github-auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Hunt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: coveralls
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sinatra
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: thin
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: httparty
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: SSH key management for Github users
112
+ email:
113
+ - c@chrishunt.co
114
+ executables:
115
+ - gh-auth
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - .gitignore
120
+ - .travis.yml
121
+ - Gemfile
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - bin/gh-auth
126
+ - github-auth.gemspec
127
+ - img/mac-os-ssh-sharing.png
128
+ - lib/github/auth.rb
129
+ - lib/github/auth/cli.rb
130
+ - lib/github/auth/keys_client.rb
131
+ - lib/github/auth/keys_file.rb
132
+ - lib/github/auth/version.rb
133
+ - spec/acceptance/github/auth/cli_spec.rb
134
+ - spec/acceptance/github/auth/keys_client_spec.rb
135
+ - spec/acceptance/github/auth/keys_file_spec.rb
136
+ - spec/spec_helper.rb
137
+ - spec/support/mock_github_server.rb
138
+ - spec/unit/github/auth/cli_spec.rb
139
+ - spec/unit/github/auth/keys_client_spec.rb
140
+ - spec/unit/github/auth/keys_file_spec.rb
141
+ homepage: https://github.com/chrishunt/github-auth
142
+ licenses:
143
+ - MIT
144
+ metadata: {}
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - '>='
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 2.0.3
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: SSH key management for Github users
165
+ test_files:
166
+ - spec/acceptance/github/auth/cli_spec.rb
167
+ - spec/acceptance/github/auth/keys_client_spec.rb
168
+ - spec/acceptance/github/auth/keys_file_spec.rb
169
+ - spec/spec_helper.rb
170
+ - spec/support/mock_github_server.rb
171
+ - spec/unit/github/auth/cli_spec.rb
172
+ - spec/unit/github/auth/keys_client_spec.rb
173
+ - spec/unit/github/auth/keys_file_spec.rb