github-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +119 -0
- data/Rakefile +9 -0
- data/bin/gh-auth +5 -0
- data/github-auth.gemspec +29 -0
- data/img/mac-os-ssh-sharing.png +0 -0
- data/lib/github/auth.rb +4 -0
- data/lib/github/auth/cli.rb +100 -0
- data/lib/github/auth/keys_client.rb +44 -0
- data/lib/github/auth/keys_file.rb +57 -0
- data/lib/github/auth/version.rb +5 -0
- data/spec/acceptance/github/auth/cli_spec.rb +36 -0
- data/spec/acceptance/github/auth/keys_client_spec.rb +16 -0
- data/spec/acceptance/github/auth/keys_file_spec.rb +22 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/mock_github_server.rb +40 -0
- data/spec/unit/github/auth/cli_spec.rb +42 -0
- data/spec/unit/github/auth/keys_client_spec.rb +92 -0
- data/spec/unit/github/auth/keys_file_spec.rb +138 -0
- metadata +173 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
|
+
[](https://travis-ci.org/chrishunt/github-auth)
|
3
|
+
[](https://coveralls.io/r/chrishunt/github-auth)
|
4
|
+
[](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
|
+

|
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
data/bin/gh-auth
ADDED
data/github-auth.gemspec
ADDED
@@ -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
|
data/lib/github/auth.rb
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|