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