bastille 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|