diff --git a/lib/travis/api/v3/access_control/anonymous.rb b/lib/travis/api/v3/access_control/anonymous.rb index 98175a80..f2229bc5 100644 --- a/lib/travis/api/v3/access_control/anonymous.rb +++ b/lib/travis/api/v3/access_control/anonymous.rb @@ -8,5 +8,9 @@ module Travis::API::V3 def self.for_request(*) new end + + def admin_for(repository) + raise LoginRequired + end end end diff --git a/lib/travis/api/v3/access_control/application.rb b/lib/travis/api/v3/access_control/application.rb index 80e26d53..36102170 100644 --- a/lib/travis/api/v3/access_control/application.rb +++ b/lib/travis/api/v3/access_control/application.rb @@ -2,12 +2,13 @@ require 'travis/api/v3/access_control/generic' module Travis::API::V3 class AccessControl::Application < AccessControl::Generic - attr_reader :application_name, :config, :user + attr_reader :application_name, :config, :user, :user_control def initialize(application_name, user: nil) @application_name = application_name @config = Travis.config.applications[application_name] @user = user + @user_control = user ? AccessControl::User.new(user) : AccessControl::Generic.new raise ArgumentError, 'unknown application %p' % application_name unless config raise ArgumentError, 'cannot use %p without a user' % application_name if config.requires_user and not user end @@ -19,5 +20,12 @@ module Travis::API::V3 def full_access? config.full_access end + + def admin_for(repository) + return user_control.admin_for(repository) unless full_access? + admin = repository.users.where('permissions.admin = true'.freeze).order('users.synced_at DESC'.freeze).first + raise AdminAccessRequired, "no admin found for #{repository.slug}" unless admin + admin + end end end diff --git a/lib/travis/api/v3/access_control/generic.rb b/lib/travis/api/v3/access_control/generic.rb index d04b01d6..0253c18b 100644 --- a/lib/travis/api/v3/access_control/generic.rb +++ b/lib/travis/api/v3/access_control/generic.rb @@ -15,6 +15,10 @@ module Travis::API::V3 full_access? or dispatch(object) end + def admin_for(repository) + raise AdminAccessRequired, repository: repository + end + def user end diff --git a/lib/travis/api/v3/access_control/user.rb b/lib/travis/api/v3/access_control/user.rb index 5dbe81bd..38529f8a 100644 --- a/lib/travis/api/v3/access_control/user.rb +++ b/lib/travis/api/v3/access_control/user.rb @@ -15,6 +15,10 @@ module Travis::API::V3 true end + def admin_for(repository) + permission?(:admin, repository) ? user : super + end + protected def repository_writable?(repository) diff --git a/lib/travis/api/v3/extensions/encrypted_column.rb b/lib/travis/api/v3/extensions/encrypted_column.rb new file mode 100644 index 00000000..6818b887 --- /dev/null +++ b/lib/travis/api/v3/extensions/encrypted_column.rb @@ -0,0 +1,109 @@ +require 'securerandom' +require 'base64' + +module Travis::API::V3 + module Extensions + class EncryptedColumn + attr_reader :disable, :options + alias disabled? disable + + def initialize(options = {}) + @options = options || {} + @disable = self.options[:disable] + @key = self.options[:key] + end + + def enabled? + !disabled? + end + + def load(data) + return nil unless data + + data = data.to_s + + decrypt?(data) ? decrypt(data) : data + end + + def dump(data) + encrypt?(data) ? encrypt(data.to_s) : data + end + + def key + @key || config.key + end + + def iv + SecureRandom.hex(8) + end + + def prefix + '--ENCR--' + end + + def decrypt?(data) + data.present? && (!use_prefix? || prefix_used?(data)) + end + + def encrypt?(data) + data.present? && enabled? + end + + def prefix_used?(data) + data[0..7] == prefix + end + + def decrypt(data) + data = data[8..-1] if prefix_used?(data) + + data = decode data + + iv = data[-16..-1] + data = data[0..-17] + + aes = create_aes :decrypt, key.to_s, iv + + result = aes.update(data) + aes.final + end + + def encrypt(data) + iv = self.iv + + aes = create_aes :encrypt, key.to_s, iv + + encrypted = aes.update(data) + aes.final + + encrypted = "#{encrypted}#{iv}" + encrypted = encode encrypted + encrypted = "#{prefix}#{encrypted}" if use_prefix? + encrypted + end + + def use_prefix? + options.has_key?(:use_prefix) ? options[:use_prefix] : Travis::Features.feature_inactive?(:db_encryption_prefix) + end + + def create_aes(mode = :encrypt, key, iv) + aes = OpenSSL::Cipher::AES.new(256, :CBC) + + aes.send(mode) + aes.key = key + aes.iv = iv + + aes + end + + def config + Travis.config.encryption + end + + def decode(str) + Base64.strict_decode64 str + end + + def encode(str) + Base64.strict_encode64 str + end + end + end +end diff --git a/lib/travis/api/v3/github.rb b/lib/travis/api/v3/github.rb new file mode 100644 index 00000000..b1f501c2 --- /dev/null +++ b/lib/travis/api/v3/github.rb @@ -0,0 +1,44 @@ +require 'gh' + + +module Travis::API::V3 + class GitHub + DEFAULT_OPTIONS = { + client_id: Travis.config.oauth2.try(:client_id), + client_secret: Travis.config.oauth2.try(:client_secret), + user_agent: "Travis-API/3 Travis-CI/0.0.1 GH/#{GH::VERSION}", + origin: Travis.config.host, + api_url: Travis.config.github.api_url, + ssl: Travis.config.ssl.merge(Travis.config.github.ssl || {}).to_hash.compact + } + private_constant :DEFAULT_OPTIONS + + attr_reader :gh, :user + + def initialize(user = nil, token = nil) + if user.respond_to? :github_oauth_token + raise ServerError, 'no GitHub token for user' if user.github_oauth_token.blank? + token = user.github_oauth_token + end + + @user = user + @gh = GH.with(token: token, **DEFAULT_OPTIONS) + end + + def set_hook(repository, flag) + hooks_url = "repos/#{repository.slug}/hooks" + payload = { + name: 'travis'.freeze, + events: [:push, :pull_request, :issue_comment, :public, :member], + active: flag, + config: { domain: Travis.config.service_hook_url || '' } + } + + if hook = gh[hooks_url].detect { |hook| hook['name'.freeze] == 'travis'.freeze } + gh.patch(hook['_links'.freeze]['self'.freeze]['href'.freeze], payload) + else + gh.post(hooks_url, payload) + end + end + end +end \ No newline at end of file diff --git a/lib/travis/api/v3/models/token.rb b/lib/travis/api/v3/models/token.rb new file mode 100644 index 00000000..90965d0b --- /dev/null +++ b/lib/travis/api/v3/models/token.rb @@ -0,0 +1,14 @@ +module Travis::API::V3 + class Models::Token < Model + belongs_to :user + validate :token, presence: true + serialize :token, Extensions::EncryptedColumn.new(disable: true) + before_validation :generate_token, on: :create + + protected + + def generate_token + self.token = SecureRandom.base64(15).tr('+/=lIO0', 'pqrsxyz') + end + end +end diff --git a/lib/travis/api/v3/models/user.rb b/lib/travis/api/v3/models/user.rb index c07f3a4b..874bc908 100644 --- a/lib/travis/api/v3/models/user.rb +++ b/lib/travis/api/v3/models/user.rb @@ -1,9 +1,17 @@ module Travis::API::V3 class Models::User < Model has_many :memberships, dependent: :destroy - has_many :organizations, through: :memberships has_many :permissions, dependent: :destroy - has_many :repositories, through: :permissions has_many :emails, dependent: :destroy + has_many :tokens, dependent: :destroy + has_many :repositories, through: :permissions + has_many :organizations, through: :memberships + + + serialize :github_oauth_token, Extensions::EncryptedColumn.new(disable: true) + + def token + tokens.first_or_create.token + end end end diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index b0d77179..80e6a214 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -7,6 +7,9 @@ module Travis::API::V3 route '/repo/{repository.id}' get :find + post :enable, '/enable' + post :disable, '/disable' + resource :requests do route '/requests' get :find diff --git a/lib/travis/api/v3/service.rb b/lib/travis/api/v3/service.rb index 95aad833..affd37ff 100644 --- a/lib/travis/api/v3/service.rb +++ b/lib/travis/api/v3/service.rb @@ -13,12 +13,17 @@ module Travis::API::V3 @access_control = access_control @params = params @queries = {} + @github = {} end def query(type = self.class.result_type) @queries[type] ||= Queries[type].new(params, self.class.result_type) end + def github(user = nil) + @github[user] ||= GitHub.new(user) + end + def find(type = self.class.result_type, *args) not_found(true, type) unless object = query(type).find(*args) not_found(false, type) unless access_control.visible? object diff --git a/lib/travis/api/v3/services/repository/disable.rb b/lib/travis/api/v3/services/repository/disable.rb new file mode 100644 index 00000000..08a191bb --- /dev/null +++ b/lib/travis/api/v3/services/repository/disable.rb @@ -0,0 +1,15 @@ +module Travis::API::V3 + class Services::Repository::Disable < Service + def run!(activate = false) + raise LoginRequired unless access_control.logged_in? or access_control.full_access? + raise NotFound unless repository = find(:repository) + + admin = access_control.admin_for(repository) + + github(admin).set_hook(repository, activate) + repository.update_attributes(active: activate) + + repository + end + end +end diff --git a/lib/travis/api/v3/services/repository/enable.rb b/lib/travis/api/v3/services/repository/enable.rb new file mode 100644 index 00000000..5d85439b --- /dev/null +++ b/lib/travis/api/v3/services/repository/enable.rb @@ -0,0 +1,7 @@ +module Travis::API::V3 + class Services::Repository::Enable < Services::Repository::Disable + def run! + super(true) + end + end +end diff --git a/spec/v3/service_index_spec.rb b/spec/v3/service_index_spec.rb index 0925bb75..25f198ad 100644 --- a/spec/v3/service_index_spec.rb +++ b/spec/v3/service_index_spec.rb @@ -9,7 +9,9 @@ describe Travis::API::V3::ServiceIndex do describe "custom json entry point" do let(:expected_resources) {{ "repository" => { - "find" => [{"request-method"=>"GET", "uri-template"=>"#{path}repo/{repository.id}"}] }, + "find" => [{"request-method"=>"GET", "uri-template"=>"#{path}repo/{repository.id}"}], + "enable" => [{"request-method"=>"POST", "uri-template"=>"#{path}repo/{repository.id}/enable"}], + "disable" => [{"request-method"=>"POST", "uri-template"=>"#{path}repo/{repository.id}/disable"}] }, "repositories" => { "for_current_user" => [{"request-method"=>"GET", "uri-template"=>"#{path}repos"}] }, "branch" => {