first stab at authorization
This commit is contained in:
parent
7baf61054c
commit
29e387140a
|
@ -15,6 +15,7 @@ require 'active_record'
|
||||||
#
|
#
|
||||||
# Requires TLS in production.
|
# Requires TLS in production.
|
||||||
class Travis::Api::App
|
class Travis::Api::App
|
||||||
|
autoload :AccessToken, 'travis/api/api/access_token'
|
||||||
autoload :Responder, 'travis/api/app/responder'
|
autoload :Responder, 'travis/api/app/responder'
|
||||||
autoload :Endpoint, 'travis/api/app/endpoint'
|
autoload :Endpoint, 'travis/api/app/endpoint'
|
||||||
autoload :Extensions, 'travis/api/app/extensions'
|
autoload :Extensions, 'travis/api/app/extensions'
|
||||||
|
|
50
lib/travis/api/app/access_token.rb
Normal file
50
lib/travis/api/app/access_token.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
require 'travis/api/app'
|
||||||
|
require 'securerandom'
|
||||||
|
require 'redis'
|
||||||
|
|
||||||
|
class Travis::Api::App
|
||||||
|
class AccessToken
|
||||||
|
attr_reader :token, :scopes, :user_id
|
||||||
|
|
||||||
|
def self.create(options = {})
|
||||||
|
new(options).tap(&:save)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.find_by_token(token)
|
||||||
|
user_id, app_id, *scopes = redis.lrange(key(token), 0, -1)
|
||||||
|
new(token: token, scopes: scopes, user_id: user_id) if user_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(options = {})
|
||||||
|
raise ArgumentError, 'must supply either user_id or user' unless options[:user] ^ options[:user_id]
|
||||||
|
@token = options[:token] || SecureRandom.urlsafe_base64(64)
|
||||||
|
@scopes = Array(options[:scopes] || options[:scope])
|
||||||
|
@user = options[:user]
|
||||||
|
@user_id = options[:user_id] || @user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def save
|
||||||
|
key = key(token)
|
||||||
|
redis.del(key)
|
||||||
|
redis.rpush(key, [user_id, nil, *scopes].map(&))
|
||||||
|
end
|
||||||
|
|
||||||
|
def user
|
||||||
|
@user ||= User.find(user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
module Helpers
|
||||||
|
private
|
||||||
|
def redis
|
||||||
|
Thread.current[:redis] ||= ::Redis.connect(url: Travis.config.redis.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def key(token)
|
||||||
|
"t:#{token}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
include Helpers
|
||||||
|
extend Helpers
|
||||||
|
end
|
||||||
|
end
|
92
lib/travis/api/app/endpoint/authorization.rb
Normal file
92
lib/travis/api/app/endpoint/authorization.rb
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
require 'travis/api/app'
|
||||||
|
|
||||||
|
class Travis::Api::App
|
||||||
|
class Endpoint
|
||||||
|
# You need to get hold of an access token in order to reach any
|
||||||
|
# endpoint requiring authorization.
|
||||||
|
# There are three ways to get hold of such a token: OAuth2, via a GitHub
|
||||||
|
# token you may already have or with Cross-Origin Window Messages.
|
||||||
|
#
|
||||||
|
# ## OAuth2
|
||||||
|
#
|
||||||
|
# API authorization is done via a subset of OAuth2 and is largely compatible
|
||||||
|
# with the [GitHub process](http://developer.github.com/v3/oauth/).
|
||||||
|
# Be aware that Travis CI will in turn use OAuth2 to authenticate (and
|
||||||
|
# authorize) against GitHub.
|
||||||
|
#
|
||||||
|
# This is the recommended way for third-party web apps.
|
||||||
|
#
|
||||||
|
# ## GitHub Token
|
||||||
|
#
|
||||||
|
# If you already have a GitHub token with the same or greater scope than
|
||||||
|
# the tokens used by Travis CI, you can easily exchange it for a access
|
||||||
|
# token. Travis will not store the GitHub token and only use it for a single
|
||||||
|
# request to resolve the associated user and scopes.
|
||||||
|
#
|
||||||
|
# This is the recommended way for GitHub applications that also want Travis
|
||||||
|
# integration.
|
||||||
|
#
|
||||||
|
# ## Cross-Origin Window Messages
|
||||||
|
#
|
||||||
|
# This is the recommended way for the official client. We might improve the
|
||||||
|
# authorization flow to support third-party clients in the future, too.
|
||||||
|
class Authorization < Endpoint
|
||||||
|
set prefix: '/auth', default_scope: :private
|
||||||
|
|
||||||
|
# Parameters:
|
||||||
|
#
|
||||||
|
# * **client_id**: your App's client id (required)
|
||||||
|
# * **redirect_uri**: URL to redirect to
|
||||||
|
# * **scope**: requested access scope
|
||||||
|
# * **state**: should be random string to prevent CSRF attacks
|
||||||
|
get '/authorize' do
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parameters:
|
||||||
|
#
|
||||||
|
# * **client_id**: your App's client id (required)
|
||||||
|
# * **client_secret**: your App's client secret (required)
|
||||||
|
# * **code**: code retrieved from redirect from [/authorize](#/authorize) (required)
|
||||||
|
# * **redirect_uri**: URL to redirect to
|
||||||
|
# * **state**: same value sent to [/authorize](#/authorize)
|
||||||
|
post '/access_token' do
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parameters:
|
||||||
|
#
|
||||||
|
# * **token**: GitHub token for checking authorization (required)
|
||||||
|
post '/github' do
|
||||||
|
data = GH.with(token: params[:token].to_s) { GH['user'] }
|
||||||
|
scopes = parse_scopes data.headers['x-oauth-scopes']
|
||||||
|
user = User.find_by_login(data['login'])
|
||||||
|
|
||||||
|
halt 403, 'not a Travis user' if user.nil?
|
||||||
|
halt 403, 'insufficient access' unless acceptable? scopes
|
||||||
|
|
||||||
|
{ 'access_token' => generate_token(user) }
|
||||||
|
end
|
||||||
|
|
||||||
|
error Faraday::Error::ClientError do
|
||||||
|
halt 401, 'could not resolve github token'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse_scopes(data)
|
||||||
|
data.gsub(/\s/,'').split(',') if data
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_token
|
||||||
|
token = SecureRandom.urlsafe_base64(64)
|
||||||
|
scopes = parse_scopes(params[:scope]) || Array(settings.default_scope)
|
||||||
|
token
|
||||||
|
end
|
||||||
|
|
||||||
|
def acceptable?(scopes)
|
||||||
|
scopes.include? 'public_repo' or scopes.include? 'repo'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -52,11 +52,9 @@ __END__
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Travis API documentation</title>
|
<title>Travis API documentation</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
|
|
||||||
<!-- we might wanna change this -->
|
<!-- we might wanna change this -->
|
||||||
<link href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css" rel="stylesheet" />
|
<link href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css" rel="stylesheet" />
|
||||||
<link href="http://twitter.github.com/bootstrap/assets/css/bootstrap-responsive.css" rel="stylesheet" />
|
|
||||||
<link href="http://twitter.github.com/bootstrap/assets/js/google-code-prettify/prettify.css" rel="stylesheet" />
|
<link href="http://twitter.github.com/bootstrap/assets/js/google-code-prettify/prettify.css" rel="stylesheet" />
|
||||||
<script src="http://twitter.github.com/bootstrap/assets/js/jquery.js"></script>
|
<script src="http://twitter.github.com/bootstrap/assets/js/jquery.js"></script>
|
||||||
<script src="http://twitter.github.com/bootstrap/assets/js/google-code-prettify/prettify.js"></script>
|
<script src="http://twitter.github.com/bootstrap/assets/js/google-code-prettify/prettify.js"></script>
|
||||||
|
@ -97,15 +95,15 @@ __END__
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body onload="prettyPrint()">
|
<body onload="prettyPrint()">
|
||||||
<div class="container-fluid">
|
<div class="container">
|
||||||
<div class="row-fluid">
|
<div class="row">
|
||||||
<header class="span12">
|
<header class="span12">
|
||||||
<h1>The Travis API</h1>
|
<h1>The Travis API</h1>
|
||||||
<p>All the routes, just waiting for you to build something awesome.</p>
|
<p>All the routes, just waiting for you to build something awesome.</p>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row-fluid">
|
<div class="row">
|
||||||
|
|
||||||
<aside class="span3">
|
<aside class="span3">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
@ -165,13 +163,18 @@ __END__
|
||||||
<a href="#<%= endpoint['name'] %>"><%= endpoint['name'] %></a>
|
<a href="#<%= endpoint['name'] %>"><%= endpoint['name'] %></a>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<% unless endpoint['doc'].to_s.empty? %>
|
||||||
<%= docs_for endpoint %>
|
<%= docs_for endpoint %>
|
||||||
|
<hr>
|
||||||
|
<% end %>
|
||||||
<% endpoint['routes'].each do |route| %>
|
<% endpoint['routes'].each do |route| %>
|
||||||
<div class="route" id="<%= slug_for(route) %>">
|
<div class="route" id="<%= slug_for(route) %>">
|
||||||
<pre><h3><%= route['verb'] %> <%= route['uri'] %></h3></pre>
|
<pre><h3><%= route['verb'] %> <%= route['uri'] %></h3></pre>
|
||||||
|
<% if route['scope'] %>
|
||||||
<p>
|
<p>
|
||||||
<h5>Required autorization scope: <span class="label"><%= route['scope'] %></span></h5>
|
<h5>Required autorization scope: <span class="label"><%= route['scope'] %></span></h5>
|
||||||
</p>
|
</p>
|
||||||
|
<% end %>
|
||||||
<%= docs_for route %>
|
<%= docs_for route %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -19,7 +19,7 @@ class Travis::Api::App
|
||||||
'uri' => (controller.prefix + route.http_path[1..-2]).gsub('//', '/'),
|
'uri' => (controller.prefix + route.http_path[1..-2]).gsub('//', '/'),
|
||||||
'verb' => route.http_verb,
|
'verb' => route.http_verb,
|
||||||
'doc' => route.docstring,
|
'doc' => route.docstring,
|
||||||
'scope' => /scope\W+(\w+)/.match(route.source).try(:[], 1) || 'public'
|
'scope' => /scope\W+(\w+)/.match(route.source).try(:[], 1)
|
||||||
}
|
}
|
||||||
endpoint = endpoints[controller.prefix] ||= {
|
endpoint = endpoints[controller.prefix] ||= {
|
||||||
'name' => namespace.name,
|
'name' => namespace.name,
|
||||||
|
|
|
@ -7,6 +7,12 @@ class Travis::Api::App
|
||||||
class Responder < Sinatra::Base
|
class Responder < Sinatra::Base
|
||||||
register Extensions::SmartConstants
|
register Extensions::SmartConstants
|
||||||
|
|
||||||
|
error NotImplementedError do
|
||||||
|
content_type :txt
|
||||||
|
status 501
|
||||||
|
"This feature has not yet been implemented. Sorry :(\n\nPull Requests welcome!"
|
||||||
|
end
|
||||||
|
|
||||||
configure do
|
configure do
|
||||||
# We pull in certain protection middleware in App.
|
# We pull in certain protection middleware in App.
|
||||||
# Being token based makes us invulnerable to common
|
# Being token based makes us invulnerable to common
|
||||||
|
|
Loading…
Reference in New Issue
Block a user