first stab at authorization
This commit is contained in:
parent
7baf61054c
commit
29e387140a
|
@ -15,11 +15,12 @@ require 'active_record'
|
|||
#
|
||||
# Requires TLS in production.
|
||||
class Travis::Api::App
|
||||
autoload :Responder, 'travis/api/app/responder'
|
||||
autoload :Endpoint, 'travis/api/app/endpoint'
|
||||
autoload :Extensions, 'travis/api/app/extensions'
|
||||
autoload :Helpers, 'travis/api/app/helpers'
|
||||
autoload :Middleware, 'travis/api/app/middleware'
|
||||
autoload :AccessToken, 'travis/api/api/access_token'
|
||||
autoload :Responder, 'travis/api/app/responder'
|
||||
autoload :Endpoint, 'travis/api/app/endpoint'
|
||||
autoload :Extensions, 'travis/api/app/extensions'
|
||||
autoload :Helpers, 'travis/api/app/helpers'
|
||||
autoload :Middleware, 'travis/api/app/middleware'
|
||||
|
||||
Rack.autoload :SSL, 'rack/ssl'
|
||||
|
||||
|
|
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>
|
||||
<meta charset="utf-8" />
|
||||
<title>Travis API documentation</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- 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-responsive.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/google-code-prettify/prettify.js"></script>
|
||||
|
@ -97,15 +95,15 @@ __END__
|
|||
</head>
|
||||
|
||||
<body onload="prettyPrint()">
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<header class="span12">
|
||||
<h1>The Travis API</h1>
|
||||
<p>All the routes, just waiting for you to build something awesome.</p>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="span3">
|
||||
<div class="page-header">
|
||||
|
@ -165,13 +163,18 @@ __END__
|
|||
<a href="#<%= endpoint['name'] %>"><%= endpoint['name'] %></a>
|
||||
</h1>
|
||||
</div>
|
||||
<%= docs_for endpoint %>
|
||||
<% unless endpoint['doc'].to_s.empty? %>
|
||||
<%= docs_for endpoint %>
|
||||
<hr>
|
||||
<% end %>
|
||||
<% endpoint['routes'].each do |route| %>
|
||||
<div class="route" id="<%= slug_for(route) %>">
|
||||
<pre><h3><%= route['verb'] %> <%= route['uri'] %></h3></pre>
|
||||
<p>
|
||||
<h5>Required autorization scope: <span class="label"><%= route['scope'] %></span></h5>
|
||||
</p>
|
||||
<% if route['scope'] %>
|
||||
<p>
|
||||
<h5>Required autorization scope: <span class="label"><%= route['scope'] %></span></h5>
|
||||
</p>
|
||||
<% end %>
|
||||
<%= docs_for route %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -19,7 +19,7 @@ class Travis::Api::App
|
|||
'uri' => (controller.prefix + route.http_path[1..-2]).gsub('//', '/'),
|
||||
'verb' => route.http_verb,
|
||||
'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] ||= {
|
||||
'name' => namespace.name,
|
||||
|
|
|
@ -7,6 +7,12 @@ class Travis::Api::App
|
|||
class Responder < Sinatra::Base
|
||||
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
|
||||
# We pull in certain protection middleware in App.
|
||||
# Being token based makes us invulnerable to common
|
||||
|
|
Loading…
Reference in New Issue
Block a user