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/lib/bastille.rb ADDED
@@ -0,0 +1,2 @@
1
+ module Bastille
2
+ end
@@ -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,19 @@
1
+ module Bastille
2
+ module CLI
3
+ module Common
4
+ private
5
+
6
+ def ask(*args, &block)
7
+ highline.ask(*args, &block)
8
+ end
9
+
10
+ def store
11
+ @store ||= Store.new
12
+ end
13
+
14
+ def highline
15
+ @highline ||= HighLine.new
16
+ end
17
+ end
18
+ end
19
+ 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
@@ -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