From 29e387140a34c558e0405ef7bd48a014ac765d4e Mon Sep 17 00:00:00 2001 From: Konstantin Haase <konstantin.mailinglists@googlemail.com> Date: Sat, 28 Jul 2012 19:47:45 +0200 Subject: [PATCH] first stab at authorization --- lib/travis/api/app.rb | 11 +-- lib/travis/api/app/access_token.rb | 50 +++++++++++ lib/travis/api/app/endpoint/authorization.rb | 92 ++++++++++++++++++++ lib/travis/api/app/endpoint/documentation.rb | 21 +++-- lib/travis/api/app/endpoint/endpoints.rb | 2 +- lib/travis/api/app/responder.rb | 6 ++ 6 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 lib/travis/api/app/access_token.rb create mode 100644 lib/travis/api/app/endpoint/authorization.rb diff --git a/lib/travis/api/app.rb b/lib/travis/api/app.rb index 65b7e3e0..542d845f 100644 --- a/lib/travis/api/app.rb +++ b/lib/travis/api/app.rb @@ -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' diff --git a/lib/travis/api/app/access_token.rb b/lib/travis/api/app/access_token.rb new file mode 100644 index 00000000..c69caaeb --- /dev/null +++ b/lib/travis/api/app/access_token.rb @@ -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 diff --git a/lib/travis/api/app/endpoint/authorization.rb b/lib/travis/api/app/endpoint/authorization.rb new file mode 100644 index 00000000..3a37c81b --- /dev/null +++ b/lib/travis/api/app/endpoint/authorization.rb @@ -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 diff --git a/lib/travis/api/app/endpoint/documentation.rb b/lib/travis/api/app/endpoint/documentation.rb index d79cbf5a..9131e5d3 100644 --- a/lib/travis/api/app/endpoint/documentation.rb +++ b/lib/travis/api/app/endpoint/documentation.rb @@ -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 %> diff --git a/lib/travis/api/app/endpoint/endpoints.rb b/lib/travis/api/app/endpoint/endpoints.rb index 8f395588..588c31fc 100644 --- a/lib/travis/api/app/endpoint/endpoints.rb +++ b/lib/travis/api/app/endpoint/endpoints.rb @@ -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, diff --git a/lib/travis/api/app/responder.rb b/lib/travis/api/app/responder.rb index d119e8f0..db20f7ef 100644 --- a/lib/travis/api/app/responder.rb +++ b/lib/travis/api/app/responder.rb @@ -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