rack-auth-travis 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/.simplecov +5 -0
- data/.travis.yml +11 -0
- data/Gemfile +5 -0
- data/Guardfile +7 -0
- data/LICENSE.md +22 -0
- data/README.md +39 -0
- data/Rakefile +17 -0
- data/bin/rack-auth-travis-envify +19 -0
- data/bin/rack-auth-travis-hash +22 -0
- data/example/Gemfile +9 -0
- data/example/Rakefile +48 -0
- data/example/config.ru +32 -0
- data/example/echoplex.rb +51 -0
- data/example/public/.gitkeep +0 -0
- data/example/views/index.erb +72 -0
- data/example/views/layout.erb +13 -0
- data/example/views/result.erb +17 -0
- data/lib/rack-auth-travis.rb +3 -0
- data/lib/rack/auth/travis.rb +153 -0
- data/rack-auth-travis.gemspec +28 -0
- data/spec/lib/rack/auth/travis_spec.rb +151 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support.rb +83 -0
- metadata +216 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 66e2120bb880c54b9c8fc6de7f4c71ba04e0eb8d
|
4
|
+
data.tar.gz: 93fc97948dcc29135dd0373c90087b6d94e9ca33
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5945ea2ec8ede2f9541208be47db7b521c3e3969da17e5d8c69aae8a453117fc909018963519929167c5ddac4283fb67e08a6ced1b06e02406beeeba581a00ad
|
7
|
+
data.tar.gz: 505976820d8c6c7edf49a485e263cb57aad7b550099e5777764c81a990c7cbc02a7428e7e632e36f992599d4e71d0687ec6a66b2d826bb53727132c711deb64b
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p247
|
data/.simplecov
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright © 2013 ModCloth, Inc.
|
2
|
+
|
3
|
+
## MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Rack::Auth::Travis
|
2
|
+
|
3
|
+
It's a Rack auth thing for Travis webhook requests!
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
Depend on it!
|
8
|
+
|
9
|
+
``` ruby
|
10
|
+
# Gemfile
|
11
|
+
gem 'rack-auth-travis'
|
12
|
+
```
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
Add it to your rack!
|
17
|
+
|
18
|
+
Take a look at the [example](./example) or do something like this:
|
19
|
+
|
20
|
+
``` ruby
|
21
|
+
# config.ru
|
22
|
+
require './my_fancy_app'
|
23
|
+
require 'rack-auth-travis'
|
24
|
+
|
25
|
+
use Rack::Auth::Travis
|
26
|
+
run MyFancyApp.new
|
27
|
+
```
|
28
|
+
|
29
|
+
When `use`'d without arguments, `Rack::Auth::Travis` will add an `ENV`-based
|
30
|
+
authenticator that will look for the following variables and use their values to
|
31
|
+
authenticate the `Authorization` header:
|
32
|
+
|
33
|
+
- `TRAVIS_AUTH_<uppercased-slug>` - generated from the "repo slug" with
|
34
|
+
non-alphanumeric characters replaced with underscores, e.g.
|
35
|
+
`octocat/Knife-Spoon` becomes `TRAVIS_AUTH_OCTOCAT_KNIFE_SPOON`
|
36
|
+
- `TRAVIS_AUTH_DEFAULT`
|
37
|
+
|
38
|
+
If one of these `ENV` variables exists, its value will be hashed with the "repo
|
39
|
+
slug" and compared to the `Authorization` header sent from Travis.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
7
|
+
t.rspec_opts = '-f doc'
|
8
|
+
end
|
9
|
+
|
10
|
+
load './example/Rakefile'
|
11
|
+
|
12
|
+
desc 'Run rubocop'
|
13
|
+
task :rubocop do
|
14
|
+
sh('rubocop --format simple') { |ok, _| ok || abort }
|
15
|
+
end
|
16
|
+
|
17
|
+
task default: [:rubocop, :spec]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim:fileencoding=utf-8
|
3
|
+
require 'json'
|
4
|
+
require 'rack-auth-travis'
|
5
|
+
|
6
|
+
if ARGV.include?('-h') || ARGV.include?('--help')
|
7
|
+
$stdout.puts <<-EOUSAGE.gsub(/^ {4}/, '')
|
8
|
+
Usage: #{File.basename($PROGRAM_NAME)} < repo-slug-token-map.json
|
9
|
+
|
10
|
+
Convert a JSON mapping of repo slugs to auth tokens into environment
|
11
|
+
variables usable by the Rack::Auth::Travis ENV-based authenticator.
|
12
|
+
EOUSAGE
|
13
|
+
exit 0
|
14
|
+
end
|
15
|
+
|
16
|
+
JSON.parse($stdin.read).each do |k, v|
|
17
|
+
$stdout.puts "#{Rack::Auth::Travis.repo_env_key(k)}=#{v}"
|
18
|
+
end
|
19
|
+
exit 0
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim:fileencoding=utf-8
|
3
|
+
require 'json'
|
4
|
+
require 'rack-auth-travis'
|
5
|
+
|
6
|
+
def usage
|
7
|
+
$stdout.puts <<-EOUSAGE.gsub(/^ {4}/, '')
|
8
|
+
Usage: #{File.basename($PROGRAM_NAME)} <repo-slug> <token>
|
9
|
+
|
10
|
+
Get the sha256 hash of a "repo slug" and Travis token just like the one
|
11
|
+
expected in the 'Authorization' header of a webhook request.
|
12
|
+
EOUSAGE
|
13
|
+
end
|
14
|
+
|
15
|
+
if ARGV.include?('-h') || ARGV.include?('--help') || !ARGV[0] || !ARGV[1]
|
16
|
+
usage
|
17
|
+
exit 0
|
18
|
+
end
|
19
|
+
|
20
|
+
owner_name, name = ARGV[0].split('/', 2)
|
21
|
+
$stdout.puts Rack::Auth::Travis.authz(owner_name, name, ARGV[1])
|
22
|
+
exit 0
|
data/example/Gemfile
ADDED
data/example/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
desc 'POST a valid payload to the example app (or anywhere else)'
|
4
|
+
task :payload do
|
5
|
+
require 'rack-auth-travis'
|
6
|
+
require 'net/http'
|
7
|
+
require_relative 'spec/support'
|
8
|
+
|
9
|
+
owner_name = ENV['OWNER_NAME'] || 'octocat'
|
10
|
+
name = ENV['NAME'] || 'Knife-Spoon'
|
11
|
+
ENV['TRAVIS_AUTH_DEFAULT'] ||= 'a' * 64
|
12
|
+
token = ENV['TRAVIS_AUTH_DEFAULT']
|
13
|
+
repo_env_key = Rack::Auth::Travis.repo_env_key("#{owner_name}/#{name}")
|
14
|
+
ENV[repo_env_key] ||= token
|
15
|
+
host = ENV['HOST'] || 'localhost'
|
16
|
+
port = Integer(ENV['PORT'] || 9292)
|
17
|
+
|
18
|
+
http = Net::HTTP.new(host, port)
|
19
|
+
req = Net::HTTP::Post.new('/')
|
20
|
+
req['Accept'] = 'application/json'
|
21
|
+
req['Content-Type'] = 'application/json'
|
22
|
+
req['Authorization'] = Rack::Auth::Travis.authz(owner_name, name, token)
|
23
|
+
req.body = JSON.pretty_generate(Support.valid_payload(owner_name, name))
|
24
|
+
|
25
|
+
$stdout.puts <<-EOF.gsub(/^ {4}/, '')
|
26
|
+
OWNER_NAME=#{owner_name}
|
27
|
+
NAME=#{name}
|
28
|
+
TRAVIS_AUTH_DEFAULT=#{token}
|
29
|
+
#{repo_env_key}=#{token}
|
30
|
+
HOST=#{host}
|
31
|
+
PORT=#{port}
|
32
|
+
|
33
|
+
Authorization: #{req['Authorization']}
|
34
|
+
|
35
|
+
EOF
|
36
|
+
http.set_debug_output($stdout)
|
37
|
+
reqbody = http.request(req).body
|
38
|
+
File.open('rack-output.html', 'w') { |f| f.puts reqbody }
|
39
|
+
$stdout.puts reqbody
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Run example application'
|
43
|
+
task :example do
|
44
|
+
Dir.chdir(File.expand_path('../', __FILE__)) do
|
45
|
+
ENV['BUNDLE_GEMFILE'] = nil
|
46
|
+
exec 'bundle exec rackup -I$PWD/../lib'
|
47
|
+
end
|
48
|
+
end
|
data/example/config.ru
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
require 'open-uri'
|
4
|
+
require 'rack/lint'
|
5
|
+
require 'rack/urlmap'
|
6
|
+
require 'rack-auth-travis'
|
7
|
+
require './echoplex'
|
8
|
+
|
9
|
+
ENV['TRAVIS_AUTH_DEFAULT'] ||= 'a' * 20
|
10
|
+
ENV['TRAVIS_REPO_OWNER_NAME'] ||= 'foo'
|
11
|
+
ENV['TRAVIS_REPO_NAME'] ||= 'bar'
|
12
|
+
|
13
|
+
sha256js = File.expand_path('../public/sha256.js', __FILE__)
|
14
|
+
|
15
|
+
if !File.exist?(sha256js)
|
16
|
+
File.open(sha256js, 'w') do |f|
|
17
|
+
open(
|
18
|
+
'http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha256.js'
|
19
|
+
) do |uri_f|
|
20
|
+
uri_f.each_line { |l| f.write l }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
app = Echoplex::App.new
|
26
|
+
protected_app = Rack::Auth::Travis.new(app, realm: 'echoplex')
|
27
|
+
|
28
|
+
run Rack::URLMap.new(
|
29
|
+
'/unprotected' => Rack::Lint.new(app),
|
30
|
+
'/protected' => Rack::Lint.new(protected_app),
|
31
|
+
'/' => Rack::Directory.new(File.expand_path('../public', __FILE__))
|
32
|
+
)
|
data/example/echoplex.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'sinatra/base'
|
5
|
+
|
6
|
+
module Echoplex
|
7
|
+
class App < Sinatra::Base
|
8
|
+
set :public_dir, File.expand_path('../public', __FILE__)
|
9
|
+
|
10
|
+
get '/' do
|
11
|
+
@owner_name = ENV['TRAVIS_REPO_OWNER_NAME']
|
12
|
+
@name = ENV['TRAVIS_REPO_NAME']
|
13
|
+
@token = ENV['TRAVIS_AUTH_DEFAULT']
|
14
|
+
@default_json = JSON.pretty_generate(
|
15
|
+
payload: { repository: { owner_name: @owner_name, name: @name } }
|
16
|
+
)
|
17
|
+
erb :index
|
18
|
+
end
|
19
|
+
|
20
|
+
post '/' do
|
21
|
+
rack_input, input_json, error = extract_post_data(request)
|
22
|
+
@authz = request.env['HTTP_AUTHORIZATION']
|
23
|
+
@input_length = rack_input.length
|
24
|
+
@owner_name = (
|
25
|
+
(input_json['payload'] || {})['repository'] || {}
|
26
|
+
)['owner_name']
|
27
|
+
@name = (
|
28
|
+
(input_json['payload'] || {})['repository'] || {}
|
29
|
+
)['name']
|
30
|
+
@error = error
|
31
|
+
@env = request.env
|
32
|
+
erb :result, layout: !request.xhr?
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def extract_post_data(request)
|
38
|
+
rack_input = request.env['rack.input'].read
|
39
|
+
input_json = {}
|
40
|
+
error = nil
|
41
|
+
|
42
|
+
begin
|
43
|
+
input_json = JSON.parse(rack_input)
|
44
|
+
rescue => e
|
45
|
+
error = "#{e.class.name} #{e.message}"
|
46
|
+
end
|
47
|
+
|
48
|
+
[rack_input, input_json, error]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
File without changes
|
@@ -0,0 +1,72 @@
|
|
1
|
+
<form action="protected" method="post">
|
2
|
+
<div>
|
3
|
+
<label for="owner_name">Repo Owner</label>
|
4
|
+
</div>
|
5
|
+
<div>
|
6
|
+
<input type="text" id="owner_name" name="owner_name"
|
7
|
+
placeholder="<github user or org>" value="<%= @owner_name %>"
|
8
|
+
size="40" autofocus />
|
9
|
+
</div>
|
10
|
+
<div>
|
11
|
+
<label for="name">Repo Name</label>
|
12
|
+
</div>
|
13
|
+
<div>
|
14
|
+
<input type="text" id="name" name="name"
|
15
|
+
placeholder="<github repo>" value="<%= @name %>"
|
16
|
+
size="40" />
|
17
|
+
</div>
|
18
|
+
<div>
|
19
|
+
<label for="token">Token</label>
|
20
|
+
</div>
|
21
|
+
<div>
|
22
|
+
<input type="text" id="token" name="token"
|
23
|
+
placeholder="<travis token>" value="<%= @token %>"
|
24
|
+
size="30" maxlength="20" />
|
25
|
+
</div>
|
26
|
+
<div>
|
27
|
+
<label for="payload">JSON payload</label>
|
28
|
+
</div>
|
29
|
+
<textarea id="payload" name="payload" rows="20" cols="80">
|
30
|
+
<%= @default_json %>
|
31
|
+
</textarea>
|
32
|
+
<div>
|
33
|
+
<button id="fake_submit">POST</button>
|
34
|
+
</div>
|
35
|
+
</form>
|
36
|
+
<script
|
37
|
+
src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js">
|
38
|
+
</script>
|
39
|
+
<script src="/sha256.js"></script>
|
40
|
+
<script type="text/javascript">
|
41
|
+
$( function () {
|
42
|
+
function travisAuthz ( ownerName, name, token ) {
|
43
|
+
var toHash = ownerName + '/' + name + token;
|
44
|
+
return CryptoJS.SHA256( toHash ).toString( CryptoJS.enc.Hex );
|
45
|
+
}
|
46
|
+
|
47
|
+
$( '#fake_submit' ).click( function ( event ) {
|
48
|
+
event.preventDefault();
|
49
|
+
var payload = $( '#payload' ).val();
|
50
|
+
console.log( 'sending payload %o', payload );
|
51
|
+
$.ajax({
|
52
|
+
url: 'protected',
|
53
|
+
type: 'POST',
|
54
|
+
headers: {
|
55
|
+
'Authorization': travisAuthz(
|
56
|
+
$( '#owner_name' ).val(),
|
57
|
+
$( '#name' ).val(),
|
58
|
+
$( '#token' ).val()
|
59
|
+
),
|
60
|
+
'Accept': 'text/html'
|
61
|
+
},
|
62
|
+
contentType: 'application/json',
|
63
|
+
data: payload,
|
64
|
+
dataType: 'html',
|
65
|
+
success: function( data ) {
|
66
|
+
window.history.pushState(null, "result", "protected");
|
67
|
+
$( '#content' ).html( data );
|
68
|
+
}
|
69
|
+
});
|
70
|
+
});
|
71
|
+
});
|
72
|
+
</script>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>rack-auth-travis example</title>
|
5
|
+
<style type="text/css">body { font-family: Menlo, monospace; }</style>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<h1>rack-auth-travis example</h1>
|
9
|
+
<div id="content">
|
10
|
+
<%= yield %>
|
11
|
+
</div>
|
12
|
+
</body>
|
13
|
+
</html>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<p><a id="redo" href="unprotected">again again!</a></p>
|
2
|
+
<dl>
|
3
|
+
<dt>authz</dt>
|
4
|
+
<dd><%= @authz.inspect %></dd>
|
5
|
+
<dt>rack input length</dt>
|
6
|
+
<dd><%= @input_length.inspect %></dd>
|
7
|
+
<dt>repo owner name</dt>
|
8
|
+
<dd><%= @owner_name.inspect %></dd>
|
9
|
+
<dt>repo name</dt>
|
10
|
+
<dd><%= @name.inspect %></dd>
|
11
|
+
<dt>error</dt>
|
12
|
+
<dd><%= @error.inspect %></dd>
|
13
|
+
</dl>
|
14
|
+
|
15
|
+
<% @env.sort.each do |k, v| %>
|
16
|
+
<!-- <%= k.to_s.gsub(/--/, '- - ') %>=<%= v.to_s.gsub(/--/, '- - ') %> -->
|
17
|
+
<% end %>
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'json'
|
5
|
+
require 'rack/auth/abstract/handler'
|
6
|
+
require 'rack/auth/abstract/request'
|
7
|
+
|
8
|
+
module Rack
|
9
|
+
module Auth
|
10
|
+
class Travis < ::Rack::Auth::AbstractHandler
|
11
|
+
VERSION = '0.1.0'
|
12
|
+
|
13
|
+
def self.authz(owner_name, name, token)
|
14
|
+
::Digest::SHA256.hexdigest([owner_name, name].join('/') + token)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.repo_env_key(repo_slug)
|
18
|
+
"TRAVIS_AUTH_#{repo_slug.gsub(/[^\p{Alnum}]/, '_').upcase}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.valid?(env)
|
22
|
+
::Rack::Auth::Travis::Request.new(env).valid?
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.default_authenticators
|
26
|
+
[
|
27
|
+
::Rack::Auth::Travis::ENVAuthenticator.new
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(app, config = {}, &authenticator)
|
32
|
+
@config = config
|
33
|
+
@config[:sources] ||= [:env]
|
34
|
+
super(app, config[:realm], &authenticator)
|
35
|
+
configure_authenticators
|
36
|
+
end
|
37
|
+
|
38
|
+
def call(env)
|
39
|
+
auth_req = Travis::Request.new(env, @authenticators)
|
40
|
+
return unauthorized unless auth_req.provided?
|
41
|
+
return bad_request unless auth_req.travis? && auth_req.json?
|
42
|
+
return @app.call(env) if auth_req.valid?
|
43
|
+
unauthorized
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_env_authenticator
|
47
|
+
ENVAuthenticator.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def configure_authenticators
|
51
|
+
@authenticators = []
|
52
|
+
@config[:sources].each do |source|
|
53
|
+
method_name = "build_#{source}_authenticator"
|
54
|
+
@authenticators << send(method_name) if respond_to?(method_name)
|
55
|
+
end
|
56
|
+
if @authenticator
|
57
|
+
@authenticators << DIYAuthenticator.new(@authenticator)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def challenge
|
62
|
+
%Q(Travis realm="#{realm}")
|
63
|
+
end
|
64
|
+
|
65
|
+
class DIYAuthenticator
|
66
|
+
def initialize(authenticator_block)
|
67
|
+
@authenticator_block = authenticator_block
|
68
|
+
end
|
69
|
+
|
70
|
+
def valid?(auth_req)
|
71
|
+
@authenticator_block.call(auth_req.repo_slug, auth_req.token)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class ENVAuthenticator
|
76
|
+
def valid?(auth_req)
|
77
|
+
[
|
78
|
+
Travis.repo_env_key(auth_req.repo_slug),
|
79
|
+
'TRAVIS_AUTH_DEFAULT'
|
80
|
+
].each do |k|
|
81
|
+
env_auth_token = ENV[k]
|
82
|
+
next unless env_auth_token
|
83
|
+
return true if auth_req.token == authz(auth_req, env_auth_token)
|
84
|
+
end
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
def authz(auth_req, env_auth_token)
|
89
|
+
Travis.authz(auth_req.owner_name, auth_req.name, env_auth_token)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Request < ::Rack::Auth::AbstractRequest
|
94
|
+
JSON_REGEXP = /^application\/([\w!#\$%&\*`\-\.\^~]*\+)?json$/i
|
95
|
+
|
96
|
+
def initialize(env, authenticators = Travis.default_authenticators)
|
97
|
+
super(env)
|
98
|
+
@authenticators = authenticators || []
|
99
|
+
end
|
100
|
+
|
101
|
+
def travis?
|
102
|
+
token =~ /[\da-f]{64}/i
|
103
|
+
end
|
104
|
+
|
105
|
+
def json?
|
106
|
+
request.env['CONTENT_TYPE'] =~ JSON_REGEXP
|
107
|
+
end
|
108
|
+
|
109
|
+
def valid?
|
110
|
+
return false unless provided? && travis? && json?
|
111
|
+
@authenticators.each do |authenticator|
|
112
|
+
return true if authenticator.valid?(self)
|
113
|
+
end
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
def owner_name
|
118
|
+
@owner ||= repository['owner_name']
|
119
|
+
end
|
120
|
+
|
121
|
+
def name
|
122
|
+
@name ||= repository['name']
|
123
|
+
end
|
124
|
+
|
125
|
+
def repo_slug
|
126
|
+
[owner_name, name].join('/')
|
127
|
+
end
|
128
|
+
|
129
|
+
def token
|
130
|
+
@token ||= parts.first.to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def repository
|
136
|
+
@repository ||= ((
|
137
|
+
JSON.parse(request_body) || {}
|
138
|
+
)['payload'] || {})['repository'] || {}
|
139
|
+
rescue
|
140
|
+
{}
|
141
|
+
end
|
142
|
+
|
143
|
+
def request_body
|
144
|
+
@request_body ||= begin
|
145
|
+
body = request.env['rack.input'].read
|
146
|
+
request.env['rack.input'].rewind
|
147
|
+
body
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
Gem::Specification.new do |spec|
|
3
|
+
spec.name = 'rack-auth-travis'
|
4
|
+
spec.version = '0.1.0'
|
5
|
+
spec.authors = ['Dan Buch']
|
6
|
+
spec.email = ['d.buch@modcloth.com']
|
7
|
+
spec.summary = %q{Rack auth for Travis CI webhook requests!}
|
8
|
+
spec.description = spec.summary
|
9
|
+
spec.homepage = 'https://github.com/modcloth-labs/rack-auth-travis'
|
10
|
+
spec.license = 'MIT'
|
11
|
+
|
12
|
+
spec.files = `git ls-files`.split($/)
|
13
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
14
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
15
|
+
spec.require_paths = ['lib']
|
16
|
+
|
17
|
+
spec.add_runtime_dependency 'rack'
|
18
|
+
|
19
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
20
|
+
spec.add_development_dependency 'guard-rspec'
|
21
|
+
spec.add_development_dependency 'puma'
|
22
|
+
spec.add_development_dependency 'rack-test'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec'
|
25
|
+
spec.add_development_dependency 'rubocop'
|
26
|
+
spec.add_development_dependency 'shotgun'
|
27
|
+
spec.add_development_dependency 'simplecov'
|
28
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'json'
|
5
|
+
require 'rack/lobster'
|
6
|
+
|
7
|
+
describe Rack::Auth::Travis do
|
8
|
+
include Rack::Test::Methods
|
9
|
+
|
10
|
+
let :unprotected_app do
|
11
|
+
Rack::Lobster.new
|
12
|
+
end
|
13
|
+
|
14
|
+
subject :protected_app do
|
15
|
+
described_class.new(unprotected_app)
|
16
|
+
end
|
17
|
+
|
18
|
+
let :owner do
|
19
|
+
"octocat#{rand(10..19)}"
|
20
|
+
end
|
21
|
+
|
22
|
+
let :repo do
|
23
|
+
"deathrace#{rand(2000..2999)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
let :repo_slug do
|
27
|
+
[owner, repo].join('/')
|
28
|
+
end
|
29
|
+
|
30
|
+
let :token do
|
31
|
+
Base64.encode64("#{rand(100..999)}#{rand(10e4..10e5)}")[0, 20]
|
32
|
+
end
|
33
|
+
|
34
|
+
let :valid_auth_header do
|
35
|
+
described_class.authz(owner, repo, token)
|
36
|
+
end
|
37
|
+
|
38
|
+
let :valid_payload do
|
39
|
+
Support.valid_payload(owner, repo)
|
40
|
+
end
|
41
|
+
|
42
|
+
let :valid_payload_json do
|
43
|
+
JSON.pretty_generate(valid_payload)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'inherits from Rack::Auth::AbstractHandler' do
|
47
|
+
subject.class.superclass.should == Rack::Auth::AbstractHandler
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'accepts config as a hash' do
|
51
|
+
described_class.new(unprotected_app, realm: 'foo').realm.should == 'foo'
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'provides an array of default authenticators' do
|
55
|
+
described_class.default_authenticators.should_not be_empty
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'provides a `.valid?` method for checking arbitrary request envs' do
|
59
|
+
described_class.valid?('HTTP_JUST_KIDDING' => '1').should be_false
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when initialized with an authenticator block' do
|
63
|
+
it 'adds a DIYAuthenticator' do
|
64
|
+
handler = described_class.new(unprotected_app) { |r, t| true }
|
65
|
+
authenticators = handler.instance_variable_get(:@authenticators)
|
66
|
+
authenticators.map do |a|
|
67
|
+
a.class.name
|
68
|
+
end.should include('Rack::Auth::Travis::DIYAuthenticator')
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'when the request is authenticated' do
|
72
|
+
def app
|
73
|
+
described_class.new(unprotected_app) { |r, t| true }
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'responds 200' do
|
77
|
+
post '/', valid_payload_json, {
|
78
|
+
'HTTP_AUTHORIZATION' => valid_auth_header,
|
79
|
+
'CONTENT_TYPE' => 'application/json'
|
80
|
+
}
|
81
|
+
last_response.status.should == 200
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when the payload is not valid JSON' do
|
87
|
+
def app
|
88
|
+
protected_app
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'does not explode' do
|
92
|
+
post '/', '{asd', {
|
93
|
+
'HTTP_AUTHORIZATION' => 'a' * 64,
|
94
|
+
'CONTENT_TYPE' => 'application/json'
|
95
|
+
}
|
96
|
+
(last_response.status < 500).should be_true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when initialized without config or block' do
|
101
|
+
it 'adds an ENVAuthenticator' do
|
102
|
+
handler = described_class.new(unprotected_app)
|
103
|
+
authenticators = handler.instance_variable_get(:@authenticators)
|
104
|
+
authenticators.map do |a|
|
105
|
+
a.class.name
|
106
|
+
end.should == ['Rack::Auth::Travis::ENVAuthenticator']
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'when no Authorization is provided' do
|
111
|
+
def app
|
112
|
+
protected_app
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'responds 401' do
|
116
|
+
post '/foo', '{}'
|
117
|
+
last_response.status.should == 401
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'when invalid Authorization is provided' do
|
122
|
+
def app
|
123
|
+
protected_app
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'responds 401' do
|
127
|
+
post '/', valid_payload_json, {
|
128
|
+
'HTTP_AUTHORIZATION' => 'a' * 64,
|
129
|
+
'CONTENT_TYPE' => 'application/json'
|
130
|
+
}
|
131
|
+
last_response.status.should == 401
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'when valid Authorization is provided' do
|
136
|
+
def app
|
137
|
+
protected_app
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'responds 200' do
|
141
|
+
env_key = described_class.repo_env_key(repo_slug)
|
142
|
+
ENV[env_key] = token
|
143
|
+
post '/', valid_payload_json, {
|
144
|
+
'HTTP_AUTHORIZATION' => valid_auth_header,
|
145
|
+
'CONTENT_TYPE' => 'application/json'
|
146
|
+
}
|
147
|
+
ENV[env_key] = nil
|
148
|
+
last_response.status.should == 200
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/support.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
class Support
|
4
|
+
PAYLOAD_TMPL = {
|
5
|
+
'payload' => {
|
6
|
+
'id' => 1,
|
7
|
+
'number' => 1,
|
8
|
+
'status' => nil,
|
9
|
+
'started_at' => nil,
|
10
|
+
'finished_at' => nil,
|
11
|
+
'status_message' => 'Passed',
|
12
|
+
'commit' => '62aae5f70ceee39123ef',
|
13
|
+
'branch' => 'master',
|
14
|
+
'message' => 'the commit message',
|
15
|
+
'compare_url' => (
|
16
|
+
'https://github.com/___OWNER___/minimal/compare/master...develop'
|
17
|
+
),
|
18
|
+
'committed_at' => '2011-11-11T11: 11: 11Z',
|
19
|
+
'committer_name' => 'Sven Fuchs',
|
20
|
+
'committer_email' => 'svenfuchs@artweb-design.de',
|
21
|
+
'author_name' => 'Sven Fuchs',
|
22
|
+
'author_email' => 'svenfuchs@artweb-design.de',
|
23
|
+
'repository' => {
|
24
|
+
'id' => 1,
|
25
|
+
'name' => '___REPO___',
|
26
|
+
'owner_name' => '___OWNER___',
|
27
|
+
'url' => 'http://github.com/___OWNER___/minimal'
|
28
|
+
},
|
29
|
+
'matrix' => [
|
30
|
+
{
|
31
|
+
'id' => 2,
|
32
|
+
'repository_id' => 1,
|
33
|
+
'number' => '1.1',
|
34
|
+
'state' => 'created',
|
35
|
+
'started_at' => nil,
|
36
|
+
'finished_at' => nil,
|
37
|
+
'config' => {
|
38
|
+
'notifications' => {
|
39
|
+
'webhooks' => [
|
40
|
+
'http://evome.fr/notifications',
|
41
|
+
'http://example.com/'
|
42
|
+
]
|
43
|
+
}
|
44
|
+
},
|
45
|
+
'status' => nil,
|
46
|
+
'log' => '',
|
47
|
+
'result' => nil,
|
48
|
+
'parent_id' => 1,
|
49
|
+
'commit' => '62aae5f70ceee39123ef',
|
50
|
+
'branch' => 'master',
|
51
|
+
'message' => 'the commit message',
|
52
|
+
'committed_at' => '2011-11-11T11: 11: 11Z',
|
53
|
+
'committer_name' => 'Sven Fuchs',
|
54
|
+
'committer_email' => 'svenfuchs@artweb-design.de',
|
55
|
+
'author_name' => 'Sven Fuchs',
|
56
|
+
'author_email' => 'svenfuchs@artweb-design.de',
|
57
|
+
'compare_url' => (
|
58
|
+
'https://github.com/___OWNER___/minimal/compare/master...develop'
|
59
|
+
)
|
60
|
+
}
|
61
|
+
]
|
62
|
+
}
|
63
|
+
}.freeze
|
64
|
+
|
65
|
+
def self.valid_payload(owner = 'foo', repo = 'bar')
|
66
|
+
PAYLOAD_TMPL.clone.tap do |payload|
|
67
|
+
substitute_payload_vars!(payload['payload'], owner, repo)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.substitute_payload_vars!(payload, owner, repo)
|
72
|
+
payload['compare_url'] = payload['compare_url'].sub(/___OWNER___/, owner)
|
73
|
+
payload['repository'].merge!(
|
74
|
+
'name' => repo,
|
75
|
+
'owner_name' => owner,
|
76
|
+
'url' => payload['repository']['url'].sub(/___OWNER___/, owner)
|
77
|
+
)
|
78
|
+
compare_url = payload['matrix'].first['compare_url']
|
79
|
+
payload['matrix'].first.merge!(
|
80
|
+
'compare_url' => compare_url.sub(/___OWNER___/, owner)
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-auth-travis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Buch
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard-rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: puma
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: shotgun
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: simplecov
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: Rack auth for Travis CI webhook requests!
|
154
|
+
email:
|
155
|
+
- d.buch@modcloth.com
|
156
|
+
executables:
|
157
|
+
- rack-auth-travis-envify
|
158
|
+
- rack-auth-travis-hash
|
159
|
+
extensions: []
|
160
|
+
extra_rdoc_files: []
|
161
|
+
files:
|
162
|
+
- .gitignore
|
163
|
+
- .rspec
|
164
|
+
- .rubocop.yml
|
165
|
+
- .ruby-version
|
166
|
+
- .simplecov
|
167
|
+
- .travis.yml
|
168
|
+
- Gemfile
|
169
|
+
- Guardfile
|
170
|
+
- LICENSE.md
|
171
|
+
- README.md
|
172
|
+
- Rakefile
|
173
|
+
- bin/rack-auth-travis-envify
|
174
|
+
- bin/rack-auth-travis-hash
|
175
|
+
- example/Gemfile
|
176
|
+
- example/Rakefile
|
177
|
+
- example/config.ru
|
178
|
+
- example/echoplex.rb
|
179
|
+
- example/public/.gitkeep
|
180
|
+
- example/views/index.erb
|
181
|
+
- example/views/layout.erb
|
182
|
+
- example/views/result.erb
|
183
|
+
- lib/rack-auth-travis.rb
|
184
|
+
- lib/rack/auth/travis.rb
|
185
|
+
- rack-auth-travis.gemspec
|
186
|
+
- spec/lib/rack/auth/travis_spec.rb
|
187
|
+
- spec/spec_helper.rb
|
188
|
+
- spec/support.rb
|
189
|
+
homepage: https://github.com/modcloth-labs/rack-auth-travis
|
190
|
+
licenses:
|
191
|
+
- MIT
|
192
|
+
metadata: {}
|
193
|
+
post_install_message:
|
194
|
+
rdoc_options: []
|
195
|
+
require_paths:
|
196
|
+
- lib
|
197
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - '>='
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
203
|
+
requirements:
|
204
|
+
- - '>='
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: '0'
|
207
|
+
requirements: []
|
208
|
+
rubyforge_project:
|
209
|
+
rubygems_version: 2.0.3
|
210
|
+
signing_key:
|
211
|
+
specification_version: 4
|
212
|
+
summary: Rack auth for Travis CI webhook requests!
|
213
|
+
test_files:
|
214
|
+
- spec/lib/rack/auth/travis_spec.rb
|
215
|
+
- spec/spec_helper.rb
|
216
|
+
- spec/support.rb
|