bastille 0.0.1

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