bastille 0.0.1
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.
- data/.gitignore +18 -0
- data/.travis.yml +12 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +96 -0
- data/Rakefile +1 -0
- data/bastille.gemspec +33 -0
- data/bin/bastille +7 -0
- data/config.ru +4 -0
- data/features/bastille.feature +15 -0
- data/features/support/aruba.rb +7 -0
- data/features/support/octokit.rb +69 -0
- data/features/support/server.rb +55 -0
- data/features/token.feature +71 -0
- data/features/vault.feature +79 -0
- data/lib/bastille.rb +2 -0
- data/lib/bastille/cli.rb +29 -0
- data/lib/bastille/cli/common.rb +19 -0
- data/lib/bastille/cli/token.rb +69 -0
- data/lib/bastille/cli/vault.rb +87 -0
- data/lib/bastille/client.rb +131 -0
- data/lib/bastille/hub.rb +32 -0
- data/lib/bastille/server.rb +120 -0
- data/lib/bastille/space.rb +31 -0
- data/lib/bastille/store.rb +123 -0
- data/lib/bastille/version.rb +3 -0
- metadata +271 -0
data/lib/bastille.rb
ADDED
data/lib/bastille/cli.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'gibberish'
|
3
|
+
require 'highline'
|
4
|
+
require 'httparty'
|
5
|
+
require 'multi_json'
|
6
|
+
require 'octokit'
|
7
|
+
require 'thor'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'system_timer'
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'bastille/client'
|
16
|
+
require 'bastille/store'
|
17
|
+
|
18
|
+
require 'bastille/cli/common'
|
19
|
+
require 'bastille/cli/token'
|
20
|
+
require 'bastille/cli/vault'
|
21
|
+
|
22
|
+
module Bastille
|
23
|
+
module CLI
|
24
|
+
class Executable < Thor
|
25
|
+
register Token, :token, Token.usage, Token.description
|
26
|
+
register Vault, :vault, Vault.usage, Vault.description
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Bastille
|
2
|
+
module CLI
|
3
|
+
class Token < Thor
|
4
|
+
include Common
|
5
|
+
|
6
|
+
def self.usage
|
7
|
+
'token [TASK]'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.description
|
11
|
+
'Provides the user with tools to create and view their bastille token'
|
12
|
+
end
|
13
|
+
|
14
|
+
desc :new, 'Generates an OAuth token from github to authenticate against Bastille'
|
15
|
+
def new
|
16
|
+
if store.exist?
|
17
|
+
say 'Found a local token in ~/.bastille. Aborting new token generation. Run `bastille token delete` and run this command again to generate a new token.', :yellow
|
18
|
+
else
|
19
|
+
if yes? 'This action will require you to authenticate with Github. Are you sure you want to generate a new token?', :red
|
20
|
+
username = ask 'Github username: '
|
21
|
+
password = ask 'Password: ' do |q|
|
22
|
+
q.echo = false
|
23
|
+
end
|
24
|
+
domain = ask 'Where is the bastille server?: '
|
25
|
+
name = ask 'What should we call this bastille token? This can be anything: '
|
26
|
+
if store.generate(username, password, domain, name)
|
27
|
+
say 'Your token has been generated and authorized with github. It is stored in ~/.bastille. <3', :green
|
28
|
+
else
|
29
|
+
say 'The username and password entered do not match. Sorry. :(', :red
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
desc :show, 'Prints your credentials out to the commandline'
|
36
|
+
def show
|
37
|
+
if store.exist?
|
38
|
+
max_number_of_spaces = store.keys.map(&:to_s).sort { |a,b| a.length <=> b.length }.last.length + 1
|
39
|
+
store.each do |key, value|
|
40
|
+
say " #{key}#{' ' * (max_number_of_spaces - key.to_s.length)}: #{value}"
|
41
|
+
end
|
42
|
+
else
|
43
|
+
say 'There is no token.', :red
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
desc :delete, 'Deletes the token'
|
48
|
+
def delete
|
49
|
+
if yes? 'Are you sure you want to delete your token? This cannot be undone.'
|
50
|
+
store.delete!
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc :validate, 'Validates your token with the bastille server.'
|
55
|
+
def validate
|
56
|
+
if store.exist?
|
57
|
+
say 'Validating your token with the bastille server...', :green
|
58
|
+
if store.authenticate
|
59
|
+
say 'Your token is valid. \m/', :green
|
60
|
+
else
|
61
|
+
say "Github says you aren't who you say you are. o_O", :red
|
62
|
+
end
|
63
|
+
else
|
64
|
+
say 'Could not validate your token. There is no token at ~/.bastille. Try running `bastille token new` to generate a new token.', :red
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Bastille
|
2
|
+
module CLI
|
3
|
+
class Vault < Thor
|
4
|
+
include Common
|
5
|
+
|
6
|
+
def self.usage
|
7
|
+
'vault [TASK]'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.description
|
11
|
+
'Provides access to your vaults'
|
12
|
+
end
|
13
|
+
|
14
|
+
desc :list, 'List out existing vaults'
|
15
|
+
def list
|
16
|
+
if (response = Client.new(store).vaults).success?
|
17
|
+
response.body.sort.each do |owner, vaults|
|
18
|
+
say " #{owner}:"
|
19
|
+
vaults.sort.each do |vault|
|
20
|
+
say " #{vault}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
else
|
24
|
+
say response.error_message, :red
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'set [SPACE]:[VAULT] [KEY]=[VALUE]', 'Sets a key in the given vault'
|
29
|
+
def set(space_vault, key_value)
|
30
|
+
space, vault = space_vault.split(':')
|
31
|
+
return say('Expected a : delimited space and vault argument (ie. defunkt:resque)', :red) unless space && vault
|
32
|
+
key, value = key_value.split('=')
|
33
|
+
return say('Expected a key=value argument (ie. RAILS_ENV=production)', :red) unless key && value
|
34
|
+
|
35
|
+
response = Client.new(store).set(space, vault, key, value)
|
36
|
+
if response.success?
|
37
|
+
say "\"#{key} => #{value}\" has been added to the #{space}:#{vault} vault.", :green
|
38
|
+
else
|
39
|
+
say response.error_message, :red
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'get [SPACE]:[VAULT]', 'Retrieves the contents of a given vault'
|
44
|
+
def get(space_vault)
|
45
|
+
space, vault = space_vault.split(':')
|
46
|
+
return say('Expected a : delimited space and vault argument (ie. defunkt:resque)', :red) unless space && vault
|
47
|
+
|
48
|
+
response = Client.new(store).get(space, vault)
|
49
|
+
if response.success?
|
50
|
+
if response.body.empty?
|
51
|
+
say 'There are no keys in this vault.', :yellow
|
52
|
+
else
|
53
|
+
response.body.sort.each do |key, value|
|
54
|
+
say "#{key}=#{value}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
else
|
58
|
+
say response.error_message, :red
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
desc 'delete [SPACE]:[VAULT] (KEY)', 'Deletes the given vault, or removes the key from this vault if given.'
|
63
|
+
def delete(space_vault, key = nil)
|
64
|
+
space, vault = space_vault.split(':')
|
65
|
+
return say('Expected a : delimited space and vault argument (ie. defunkt:resque)', :red) unless space && vault
|
66
|
+
|
67
|
+
question = if key.nil?
|
68
|
+
"Are you sure you want to delete the #{space}:#{vault} vault?"
|
69
|
+
else
|
70
|
+
"Are you sure you want to remove the #{key} key from the #{space}:#{vault} vault?"
|
71
|
+
end
|
72
|
+
|
73
|
+
if yes?(question)
|
74
|
+
response = Client.new(store).delete(space, vault, key)
|
75
|
+
if response.success?
|
76
|
+
say response.body, :green
|
77
|
+
else
|
78
|
+
say response.error_message, :red
|
79
|
+
end
|
80
|
+
else
|
81
|
+
say 'OK, nothing was deleted.', :green
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Bastille
|
2
|
+
class Client
|
3
|
+
|
4
|
+
def initialize(store)
|
5
|
+
@store = store
|
6
|
+
end
|
7
|
+
|
8
|
+
def vaults
|
9
|
+
http Request.new(:get, '/vaults')
|
10
|
+
end
|
11
|
+
|
12
|
+
def set(space, vault, key, value)
|
13
|
+
check_for_cipher!(space, vault)
|
14
|
+
contents = get(space, vault).body || {}
|
15
|
+
contents.merge!(key => value)
|
16
|
+
request = Request.new(:put, "/vaults/#{space}/#{vault}", @store.key(space, vault), contents)
|
17
|
+
http request
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(space, vault)
|
21
|
+
check_for_cipher!(space, vault)
|
22
|
+
key = @store.key(space, vault)
|
23
|
+
http Request.new(:get, "/vaults/#{space}/#{vault}", key), key
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(space, vault, key)
|
27
|
+
check_for_cipher!(space, vault)
|
28
|
+
if key
|
29
|
+
contents = get(space, vault).body || {}
|
30
|
+
contents.delete(key)
|
31
|
+
request = Request.new(:put, "/vaults/#{space}/#{vault}", @store.key(space, vault), contents)
|
32
|
+
http request
|
33
|
+
else
|
34
|
+
http Request.new(:delete, "/vaults/#{space}/#{vault}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def authenticate!
|
39
|
+
http Request.new(:get, '/authenticate')
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def check_for_cipher!(space, vault)
|
45
|
+
existing_vaults = vaults.body
|
46
|
+
if existing_vaults[space] && existing_vaults[space].index(vault) && !@store.key(space, vault, :test)
|
47
|
+
raise 'You are trying to access a vault that you do not have a key for. Try adding the cipher to the cipher list in ~/.bastille'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def http(request, key = nil)
|
52
|
+
if [:get, :post, :put, :delete].include?(request.method)
|
53
|
+
url = domain + request.path
|
54
|
+
options = request.options.merge!(:headers => headers)
|
55
|
+
respond_to HTTParty.send(request.method, url, options), key
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def headers
|
60
|
+
{
|
61
|
+
'X-BASTILLE-USERNAME' => @store.username,
|
62
|
+
'X-BASTILLE-TOKEN' => @store.token
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def domain
|
67
|
+
@store.domain
|
68
|
+
end
|
69
|
+
|
70
|
+
def respond_to(response, key)
|
71
|
+
Response.new(response, key)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Request
|
76
|
+
attr_reader :method, :path
|
77
|
+
|
78
|
+
def initialize(method, path, key = nil, contents = nil)
|
79
|
+
@method = method
|
80
|
+
@path = path
|
81
|
+
@key = key
|
82
|
+
@contents = contents
|
83
|
+
end
|
84
|
+
|
85
|
+
def options
|
86
|
+
if @contents
|
87
|
+
if @key
|
88
|
+
cipher = Gibberish::AES.new(@key)
|
89
|
+
contents = MultiJson.dump(@contents)
|
90
|
+
contents = cipher.encrypt(contents)
|
91
|
+
contents = Base64.encode64(contents)
|
92
|
+
end
|
93
|
+
{ :body => { :contents => contents } }
|
94
|
+
else
|
95
|
+
{}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
class Response
|
102
|
+
SUCCESS_CODES = 200..299
|
103
|
+
|
104
|
+
def initialize(response, key = nil)
|
105
|
+
@response = response
|
106
|
+
@key = key
|
107
|
+
end
|
108
|
+
|
109
|
+
def body
|
110
|
+
contents = @response.body
|
111
|
+
if @key && success? && !@response.body.empty?
|
112
|
+
cipher = Gibberish::AES.new(@key)
|
113
|
+
contents = Base64.decode64(@response.body)
|
114
|
+
contents = cipher.decrypt(contents)
|
115
|
+
end
|
116
|
+
@body ||= MultiJson.load(contents)
|
117
|
+
end
|
118
|
+
|
119
|
+
def body=(body)
|
120
|
+
@body = body
|
121
|
+
end
|
122
|
+
|
123
|
+
def success?
|
124
|
+
SUCCESS_CODES.include?(@response.code.to_i)
|
125
|
+
end
|
126
|
+
|
127
|
+
def error_message
|
128
|
+
body.fetch('error')
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
data/lib/bastille/hub.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Bastille
|
2
|
+
class Hub
|
3
|
+
|
4
|
+
def initialize(username, token)
|
5
|
+
@login = username
|
6
|
+
@oauth = token
|
7
|
+
end
|
8
|
+
|
9
|
+
def authenticate!
|
10
|
+
client.ratelimit
|
11
|
+
true
|
12
|
+
rescue Octokit::Unauthorized
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def spaces
|
17
|
+
[@login] + client.organizations.collect(&:login)
|
18
|
+
end
|
19
|
+
|
20
|
+
def member_of_space?(space)
|
21
|
+
spaces.include?(space)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def client
|
27
|
+
raise Octokit::Unauthorized unless @login && @oauth
|
28
|
+
@client ||= Octokit::Client.new(:login => @login, :oauth_token => @oauth)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
require 'octokit'
|
3
|
+
require 'redis'
|
4
|
+
require 'redis/namespace'
|
5
|
+
require 'sinatra'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'system_timer'
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'bastille/hub'
|
13
|
+
require 'bastille/space'
|
14
|
+
|
15
|
+
module Bastille
|
16
|
+
class Server < Sinatra::Base
|
17
|
+
configure :production, :development do
|
18
|
+
enable :logging
|
19
|
+
set :raise_errors, Proc.new { false }
|
20
|
+
set :show_exceptions, false
|
21
|
+
end
|
22
|
+
|
23
|
+
before do
|
24
|
+
if authenticated?
|
25
|
+
pass
|
26
|
+
else
|
27
|
+
halt 401, MultiJson.dump(:error => "Github is saying that you aren't who you say you are. Try checking your credentials.")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
not_found do
|
32
|
+
status 404
|
33
|
+
MultiJson.dump(:error => "Could not find this action on the Bastille server.")
|
34
|
+
end
|
35
|
+
|
36
|
+
error do
|
37
|
+
MultiJson.dump(:error => "We're sorry. Looks like there was an error processing your request.")
|
38
|
+
end
|
39
|
+
|
40
|
+
get '/vaults' do
|
41
|
+
json = {}
|
42
|
+
spaces.each do |space|
|
43
|
+
json[space] = Space.new(space).all
|
44
|
+
end
|
45
|
+
MultiJson.dump(json)
|
46
|
+
end
|
47
|
+
|
48
|
+
put '/vaults/:space/:vault' do
|
49
|
+
space = params.fetch('space')
|
50
|
+
vault = params.fetch('vault')
|
51
|
+
contents = params.fetch('contents')
|
52
|
+
|
53
|
+
authorize_space_access!(space)
|
54
|
+
|
55
|
+
space = Space.new(space)
|
56
|
+
space.set(vault, contents)
|
57
|
+
MultiJson.dump('OK!')
|
58
|
+
end
|
59
|
+
|
60
|
+
get '/vaults/:space/:vault' do
|
61
|
+
space = params.fetch('space')
|
62
|
+
vault = params.fetch('vault')
|
63
|
+
|
64
|
+
authorize_space_access!(space)
|
65
|
+
|
66
|
+
space = Space.new(space)
|
67
|
+
space.get(vault)
|
68
|
+
end
|
69
|
+
|
70
|
+
delete '/vaults/:space/:vault' do
|
71
|
+
space = params.fetch('space')
|
72
|
+
vault = params.fetch('vault')
|
73
|
+
|
74
|
+
authorize_space_access!(space)
|
75
|
+
|
76
|
+
space = Space.new(space)
|
77
|
+
space.delete(vault)
|
78
|
+
MultiJson.dump('OK!')
|
79
|
+
end
|
80
|
+
|
81
|
+
get '/authenticate' do
|
82
|
+
MultiJson.dump('OK!')
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def authenticated?
|
88
|
+
logger.info "Authenticating #{username} with Github"
|
89
|
+
hub.authenticate!
|
90
|
+
end
|
91
|
+
|
92
|
+
def authorize_space_access!(space)
|
93
|
+
unless hub.member_of_space?(space)
|
94
|
+
error = <<-RESPONSE.gsub(/\s+/, ' ').strip
|
95
|
+
Github is saying that you are not the owner of this space.
|
96
|
+
Your spaces are #{spaces.inspect}
|
97
|
+
RESPONSE
|
98
|
+
|
99
|
+
halt 401, MultiJson.dump(:error => error)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def hub
|
104
|
+
@hub ||= Hub.new(username, token)
|
105
|
+
end
|
106
|
+
|
107
|
+
def username
|
108
|
+
@username ||= env['HTTP_X_BASTILLE_USERNAME']
|
109
|
+
end
|
110
|
+
|
111
|
+
def token
|
112
|
+
@token ||= env['HTTP_X_BASTILLE_TOKEN']
|
113
|
+
end
|
114
|
+
|
115
|
+
def spaces
|
116
|
+
@spaces ||= hub.spaces
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|