first stab at authorization

This commit is contained in:
Konstantin Haase 2012-07-28 19:47:45 +02:00
parent 7baf61054c
commit 29e387140a
6 changed files with 167 additions and 15 deletions

View File

@ -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'

View 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

View 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

View File

@ -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 %>

View File

@ -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,

View File

@ -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