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__
All the routes, just waiting for you to build something awesome.