aws-auth 0.9.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.
data/README ADDED
@@ -0,0 +1 @@
1
+ Rack middleware that provides AWS (Amazon Web Services) style authentication.
@@ -0,0 +1,8 @@
1
+ $:.unshift "./lib"
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/gempackagetask'
5
+ require 'aws-auth'
6
+ require 'aws-auth/tasks'
7
+ require 'bundler'
8
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,4 @@
1
+ ---
2
+ :auth:
3
+ :database: db/aws-users.db
4
+ :adapter: sqlite3
Binary file
@@ -0,0 +1,24 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :users do |t|
5
+ t.column :id, :integer, :null => false
6
+ t.column :login, :string, :limit => 40
7
+ t.column :password, :string, :limit => 40
8
+ t.column :email, :string, :limit => 64
9
+ t.column :key, :string, :limit => 64
10
+ t.column :secret, :string, :limit => 64
11
+ t.column :created_at, :datetime
12
+ t.column :updated_at, :timestamp
13
+ t.column :activated_at, :datetime
14
+ t.column :superuser, :integer, :default => 0
15
+ t.column :deleted, :integer, :default => 0
16
+ end
17
+ end
18
+
19
+ def self.down
20
+ drop_table :users
21
+ end
22
+
23
+ end
24
+
Binary file
@@ -0,0 +1,126 @@
1
+ require 'rubygems'
2
+ require 'rack'
3
+ require 'active_record'
4
+ require 'base64'
5
+ require 'digest/sha1'
6
+ require 'openssl'
7
+ require 'sinatra'
8
+
9
+ module AWSAuth
10
+ class Base
11
+
12
+ VERSION = "0.9.0"
13
+ ROOT_DIR = ENV['AWS_ROOT_DIR'] || File.join(File.dirname(__FILE__),'..')
14
+ DEFAULT_PASSWORD = "testp@ss"
15
+
16
+ def self.config
17
+ @@config ||= load_config()
18
+ end
19
+
20
+ def self.config_path=(val)
21
+ @@config_path = val
22
+ end
23
+
24
+ def initialize(app)
25
+ @app = app
26
+ AWSAuth::User.establish_connection(AWSAuth::Base.config[:auth])
27
+ end
28
+
29
+ def call(env)
30
+ date_s = env['HTTP_X_AMZ_DATE'] || env['HTTP_DATE']
31
+ request = Rack::Request.new(env)
32
+ auth = Rack::Auth::Basic::Request.new(env)
33
+
34
+ # Basic Auth
35
+ if auth.provided? && auth.basic?
36
+ user = AWSAuth::User.find_by_login(auth.credentials[0])
37
+ unless user.nil?
38
+ if user.password == AWSAuth::Base.hmac_sha1( auth.credentials[1], user.secret )
39
+ env['AWS_AUTH_USER'] = user
40
+ end
41
+ end
42
+ # Route 53
43
+ elsif env['HTTP_X_AMZN_AUTHORIZATION'] =~ /^AWS3-HTTPS AWSAccessKeyId=(.*),Algorithm=HmacSHA256,Signature=(.*)$/
44
+ access_key = $1
45
+ signature = $2
46
+ user = AWSAuth::User.find_by_key(access_key)
47
+ unless user.nil?
48
+ hmac = HMAC::SHA256.new(user.secret)
49
+ hmac.update(date_s)
50
+ if Base64.encode64(hmac.digest).chomp == signature
51
+ env['AWS_AUTH_USER'] = user
52
+ end
53
+ end
54
+ # S3
55
+ elsif env['HTTP_AUTHORIZATION'] =~ /^AWS (\w+):(.+)$/ || !request['Signature'].nil?
56
+ meta, amz = {}, {}
57
+ env.each do |k,v|
58
+ k = k.downcase.gsub('_', '-')
59
+ amz[$1] = v.strip if k =~ /^http-x-amz-([-\w]+)$/
60
+ meta[$1] = v if k =~ /^http-x-amz-meta-([-\w]+)$/
61
+ end
62
+
63
+ auth, key_s, secret_s = *env['HTTP_AUTHORIZATION'].to_s.match(/^AWS (\w+):(.+)$/)
64
+ if request.params.has_key?('Signature') and Time.at(request['Expires'].to_i) >= Time.now
65
+ key_s, secret_s, date_s = request['AWSAccessKeyId'], request['Signature'], request['Expires']
66
+ end
67
+ uri = env['PATH_INFO']
68
+ uri += "?" + env['QUERY_STRING'] if %w[acl versioning torrent].include?(env['QUERY_STRING'])
69
+ canonical = [env['REQUEST_METHOD'], env['HTTP_CONTENT_MD5'], env['CONTENT_TYPE'],
70
+ date_s, uri]
71
+ amz.sort.each do |k, v|
72
+ canonical[-1,0] = "x-amz-#{k}:#{v}"
73
+ end
74
+
75
+ user = AWSAuth::User.find_by_key key_s
76
+ unless (user and secret_s != AWSAuth::Base.hmac_sha1(user.secret, canonical.map{|v|v.to_s.strip} * "\n")) || (user and user.deleted == 1)
77
+ env['AWS_AUTH_USER'] = user
78
+ end
79
+ end
80
+
81
+ @app.call(env)
82
+ end
83
+
84
+ def self.generate_secret
85
+ abc = %{ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz}
86
+ (1..40).map { abc[rand(abc.size),1] }.join
87
+ end
88
+
89
+ def self.generate_key
90
+ abc = %{ABCDEF0123456789}
91
+ (1..20).map { abc[rand(abc.size),1] }.join
92
+ end
93
+
94
+ def self.hmac_sha1(key, s)
95
+ ipad = [].fill(0x36, 0, 64)
96
+ opad = [].fill(0x5C, 0, 64)
97
+ key = key.unpack("C*")
98
+ if key.length < 64 then
99
+ key += [].fill(0, 0, 64-key.length)
100
+ end
101
+
102
+ inner = []
103
+ 64.times { |i| inner.push(key[i] ^ ipad[i]) }
104
+ inner += s.unpack("C*")
105
+
106
+ outer = []
107
+ 64.times { |i| outer.push(key[i] ^ opad[i]) }
108
+ outer = outer.pack("c*")
109
+ outer += Digest::SHA1.digest(inner.pack("c*"))
110
+
111
+ return Base64::encode64(Digest::SHA1.digest(outer)).chomp
112
+ end
113
+
114
+ protected
115
+ def self.load_config()
116
+ @@config_path ||= File.join(File.dirname(__FILE__), '../aws-auth.yml')
117
+ return YAML::load(File.read(ENV['AWS_AUTH_PATH'])) if ENV['AWS_AUTH_PATH'] && File.exists?(ENV['AWS_AUTH_PATH'])
118
+ return YAML::load(File.read(File.expand_path("~/.aws-auth.yml"))) if File.exists?(File.expand_path("~/.aws-auth.yml"))
119
+ return YAML::load(File.read(@@config_path)) if @@config_path && File.exists?(@@config_path)
120
+ end
121
+
122
+ end
123
+ end
124
+
125
+ require File.join(File.dirname(__FILE__),'aws-auth/user')
126
+ require File.join(File.dirname(__FILE__),'aws-auth/admin')
@@ -0,0 +1,270 @@
1
+ module AWS
2
+
3
+ class Admin < Sinatra::Base
4
+
5
+ POST = %{if(!this.title||confirm(this.title+'?')){var f = document.createElement('form'); this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; f.submit();}return false;}
6
+
7
+ set :sessions, :on
8
+ enable :inline_templates
9
+
10
+ @@navigation_tabs = [["users","/control/users",true],["profile","/control/profile"],["logout","/control/logout"]]
11
+ @@home_page = "/control/buckets"
12
+
13
+ def self.add_tab(name, path, admin_only=false)
14
+ t = [name,path]
15
+ t << admin_only unless admin_only == false
16
+ @@navigation_tabs = [t] + @@navigation_tabs
17
+ end
18
+
19
+ def self.tabs
20
+ @@navigation_tabs
21
+ end
22
+
23
+ def self.home_page
24
+ @@home_page
25
+ end
26
+
27
+ def self.home_page=(val)
28
+ @@home_page = val
29
+ end
30
+
31
+ before do
32
+ ActiveRecord::Base.verify_active_connections!
33
+ end
34
+
35
+ get '/?' do
36
+ login_required
37
+ redirect @@home_page
38
+ end
39
+
40
+ get %r{^/s/(.*)} do
41
+ expires 500, :public
42
+ open(File.join(AWSAuth::Base::ROOT_DIR,'public', params[:captures].first))
43
+ end
44
+
45
+ get '/login' do
46
+ r :login, "Login"
47
+ end
48
+
49
+ post '/login' do
50
+ @user = AWSAuth::User.find_by_login params[:login]
51
+ if @user
52
+ if @user.password == AWSAuth::Base.hmac_sha1( params[:password], @user.secret )
53
+ session[:user_id] = @user.id
54
+ redirect @@home_page
55
+ else
56
+ @user.errors.add(:password, 'is incorrect')
57
+ end
58
+ else
59
+ @user = AWSAuth::User.new
60
+ @user.errors.add(:login, 'not found')
61
+ end
62
+ r :login, "Login"
63
+ end
64
+
65
+ get '/logout' do
66
+ session[:user_id] = nil
67
+ redirect '/control'
68
+ end
69
+
70
+ get "/profile/?" do
71
+ login_required
72
+ @usero = @user
73
+ r :profile, "Your Profile"
74
+ end
75
+
76
+ post "/profile/?" do
77
+ login_required
78
+ @user.update_attributes(params['user'])
79
+ @usero = @user
80
+ r :profile, "Your Profile"
81
+ end
82
+
83
+ get "/users/?" do
84
+ login_required
85
+ only_superusers
86
+ @usero = AWSAuth::User.new
87
+ @users = AWSAuth::User.find :all, :conditions => ['deleted != 1'], :order => 'login'
88
+ r :users, "User List"
89
+ end
90
+
91
+ post "/users/?" do
92
+ login_required
93
+ only_superusers
94
+ @usero = AWSAuth::User.new params['user'].merge(:activated_at => Time.now)
95
+ if @usero.valid?
96
+ @usero.save()
97
+ redirect "/control/users"
98
+ else
99
+ @users = AWSAuth::User.find :all, :conditions => ['deleted != 1'], :order => 'login'
100
+ r :users, "User List"
101
+ end
102
+ end
103
+
104
+ get "/users/:login/?" do
105
+ login_required
106
+ only_superusers
107
+ @usero = AWSAuth::User.find_by_login params[:login]
108
+ r :profile, @usero.login
109
+ end
110
+
111
+ post "/users/:login/?" do
112
+ login_required
113
+ only_superusers
114
+ @usero = AWSAuth::User.find_by_login params[:login]
115
+
116
+ # if were not changing passwords remove blank values
117
+ if params['user']['password'].blank? && params['user']['password_confirmation'].blank?
118
+ params['user'].delete('password')
119
+ params['user'].delete('password_confirmation')
120
+ end
121
+
122
+ if @usero.update_attributes(params['user'])
123
+ redirect "/control/users/#{@usero.login}"
124
+ else
125
+ r :profile, @usero.login
126
+ end
127
+ end
128
+
129
+ post "/users/delete/:login/?" do
130
+ login_required
131
+ only_superusers
132
+ @usero = AWSAuth::User.find_by_login params[:login]
133
+ if @usero.id == @user.id
134
+ # FIXME: notify user they cannot delete themselves
135
+ else
136
+ @usero.destroy
137
+ end
138
+ redirect "/control/users"
139
+ end
140
+
141
+ protected
142
+ def login_required
143
+ @user = AWSAuth::User.find(session[:user_id]) unless session[:user_id].nil?
144
+ redirect '/control/login' if @user.nil?
145
+ end
146
+
147
+ def r(name, title, layout = :layout)
148
+ @title = title
149
+ haml name, :layout => layout
150
+ end
151
+
152
+ def errors_for(model)
153
+ ret = ""
154
+ if model.errors.size > 0
155
+ ret += "<ul class=\"errors\">"
156
+ model.errors.each_full do |error|
157
+ ret += "<li>#{error}</li>"
158
+ end
159
+ ret += "</ul>"
160
+ end
161
+ ret
162
+ end
163
+
164
+ def only_superusers
165
+ redirect '/control/login' unless @user.superuser?
166
+ end
167
+
168
+ end
169
+
170
+ end
171
+
172
+ __END__
173
+
174
+ @@ layout
175
+ %html
176
+ %head
177
+ %title Control Center &raquo; #{@title}
178
+ %script{ :language => "JavaScript", :type => "text/javascript", :src => "/control/s/js/prototype.js" }
179
+ %style{ :type => "text/css" }
180
+ @import '/control/s/css/control.css';
181
+ %body
182
+ %div#page
183
+ - if @user and not @login
184
+ %div.menu
185
+ %ul
186
+ %li
187
+ - for tab in AWS::Admin.tabs
188
+ - if tab[2].nil? || (tab[2] && @user.superuser?)
189
+ %a{ :href => tab[1] }= tab[0]
190
+ %div#header
191
+ %h1 Control Center
192
+ %h2 #{@title}
193
+ %div#content
194
+ = yield
195
+
196
+ @@ login
197
+ %form.create{ :method => "post" }
198
+ %div.required
199
+ %label{ :for => "login" } User
200
+ %input#login{ :type => "text", :name => "login" }
201
+ %div.required
202
+ %label{ :for => "password" } Password
203
+ %input#password{ :type => "password", :name => "password" }
204
+ %input#loggo{ :type => "submit", :value => "Login", :name => "loggo" }
205
+
206
+ @@ users
207
+ %table
208
+ %thead
209
+ %tr
210
+ %th Login
211
+ %th Activated On
212
+ %th Actions
213
+ %tbody
214
+ - @users.each do |user|
215
+ %tr
216
+ %th
217
+ %a{ :href => "/control/users/#{user.login}" } #{user.login}
218
+ %td #{user.activated_at}
219
+ %td
220
+ %a{ :href => "/control/users/delete/#{user.login}", :onclick => POST, :title => "Delete user #{user.login}" } Delete
221
+ %h3 Create a User
222
+ %form.create{ :action => "/control/users", :method => "post" }
223
+ = preserve errors_for(@usero)
224
+ %div.required
225
+ %label{ :for => "user[login]" } Login
226
+ %input.large{ :type => "text", :value => @usero.login, :name => "user[login]" }
227
+ %div.required.inline
228
+ %label{ :for => "user[superuser]" } Is a super-admin?
229
+ %input{ :type => "checkbox", :name => "user[superuser]", :value => @usero.superuser }
230
+ %div.required
231
+ %label{ :for => "user[password]" } Password
232
+ %input.fixed{ :type => "password", :name => "user[password]" }
233
+ %div.required
234
+ %label{ :for => "user[password_confirmation]" } Password again
235
+ %input.fixed{ :type => "password", :name => "user[password_confirmation]" }
236
+ %div.required
237
+ %label{ :for => "user[email]" } Email
238
+ %input{ :type => "text", :value => @usero.email, :name => "user[email]" }
239
+ %div.required
240
+ %label{ :for => "user[key]" } Key (must be unique)
241
+ %input.fixed.long{ :type => "text", :value => (@usero.key || AWSAuth::Base.generate_key), :name => "user[key]" }
242
+ %div.required
243
+ %label{ :for => "user[secret]" } Secret
244
+ %input.fixed.long{ :type => "text", :value => (@usero.secret || AWSAuth::Base.generate_secret), :name => "user[secret]" }
245
+ %input.newuser{ :type => "submit", :value => "Create", :name => "newuser" }
246
+
247
+ @@ profile
248
+ %form.create{ :method => "post" }
249
+ = preserve errors_for(@usero)
250
+ - if @user.superuser?
251
+ %div.required.inline
252
+ %label{ :for => "user[superuser]" } Is a super-admin?
253
+ %input{ :type => "hidden", :name => "user[superuser]", :value => 0 }
254
+ %input{ :type => "checkbox", :name => "user[superuser]", :value => 1, :checked => @usero.superuser? }
255
+ %div.required
256
+ %label{ :for => "user[password]" } Password
257
+ %input.fixed{ :type => "password", :name => "user[password]" }
258
+ %div.required
259
+ %label{ :for => "user[password_confirmation]" } Password again
260
+ %input.fixed{ :type => "password", :name => "user[password_confirmation]" }
261
+ %div.required
262
+ %label{ :for => "user[email]" } Email
263
+ %input{ :type => "text", :value => @usero.email, :name => "user[email]" }
264
+ %div.required
265
+ %label{ :for => "key" } Key
266
+ %h4 #{@usero.key}
267
+ %div.required
268
+ %label{ :for => "secret" } Secret
269
+ %h4 #{@usero.secret}
270
+ %input#saveuser{ :type => "submit", :value => "Save", :name => "saveuser" }
@@ -0,0 +1,28 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/gempackagetask'
4
+ require File.join(File.dirname(__FILE__), '../aws-auth')
5
+
6
+ namespace :auth do
7
+ task :environment do
8
+ ActiveRecord::Base.establish_connection(AWSAuth::Base.config[:auth])
9
+ end
10
+
11
+ desc "Migrate the database"
12
+ task(:migrate => :environment) do
13
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
14
+ ActiveRecord::Migration.verbose = true
15
+
16
+ out_dir = File.dirname(AWSAuth::Base.config[:auth][:database])
17
+ FileUtils.mkdir_p(out_dir) unless File.exists?(out_dir)
18
+
19
+ ActiveRecord::Migrator.migrate(File.join(AWSAuth::Base::ROOT_DIR, 'db', 'migrate'), ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
20
+ num_users = AWSAuth::User.count || 0
21
+ if num_users == 0
22
+ puts "** No users found, creating the `admin' user."
23
+ AWSAuth::User.create :login => "admin", :password => AWSAuth::Base::DEFAULT_PASSWORD,
24
+ :email => "admin@parkplace.net", :key => AWSAuth::Base.generate_key(), :secret => AWSAuth::Base.generate_secret(),
25
+ :activated_at => Time.now, :superuser => 1
26
+ end
27
+ end
28
+ end