diff --git a/lib/travis/api/v3.rb b/lib/travis/api/v3.rb index 727d62ae..221df59b 100644 --- a/lib/travis/api/v3.rb +++ b/lib/travis/api/v3.rb @@ -30,7 +30,6 @@ module Travis WrongCredentials = ClientError .create('access denied', status: 403) LoginRequired = ClientError .create('login required', status: 403) InsufficientAccess = ClientError .create(status: 403) - PushAccessRequired = InsufficientAccess .create('push access required') WrongParams = ClientError .create('wrong parameters') ServerError = Error .create(status: 500) NotImplemented = ServerError .create('request not (yet) implemented', status: 501) diff --git a/lib/travis/api/v3/access_control/generic.rb b/lib/travis/api/v3/access_control/generic.rb index c4883ff3..4e5755cd 100644 --- a/lib/travis/api/v3/access_control/generic.rb +++ b/lib/travis/api/v3/access_control/generic.rb @@ -36,6 +36,11 @@ module Travis::API::V3 list.select { |r| visible?(r) } end + def permissions(object) + return unless factory = permission_class(object.class) + factory.new(self, object) + end + protected def build_visible?(build) @@ -78,13 +83,22 @@ module Travis::API::V3 send(method, object) if respond_to?(method, true) end - @@method_for_cache = Tool::ThreadLocal.new + + @@unknown_permission = Object.new + @@permission_class_cache = Tool::ThreadLocal.new + @@method_for_cache = Tool::ThreadLocal.new + + def permission_class(klass) + result = @@permission_class_cache[klass] ||= Permissions[normailze_type(klass), false] || @@unknown_permission + result unless result == @@unknown_permission + end def method_for(type, method) - @@method_for_cache[[type, method]] ||= begin - prefix = type.name.sub(/^Travis::API::V3::Models::/, ''.freeze).underscore - "#{prefix}_#{method}" - end + @@method_for_cache[[type, method]] ||= "#{normailze_type(type)}_#{method}" + end + + def normailze_type(type) + type.name.sub(/^Travis::API::V3::Models::/, ''.freeze).underscore.to_sym end end end diff --git a/lib/travis/api/v3/access_control/scoped.rb b/lib/travis/api/v3/access_control/scoped.rb index 5bf1df90..33b8017d 100644 --- a/lib/travis/api/v3/access_control/scoped.rb +++ b/lib/travis/api/v3/access_control/scoped.rb @@ -2,18 +2,27 @@ require 'travis/api/v3/access_control/generic' module Travis::API::V3 class AccessControl::Scoped < AccessControl::Generic - attr_accessor :unscoped, :owner_name, :name + attr_accessor :unscoped, :anonymous, :owner_name, :name def initialize(scope, unscoped) @owner_name, @name = scope.split(?/.freeze, 2) @unscoped = unscoped + @anonymous = AccessControl::Anonymous.new end protected def private_repository_visible?(repository) + scope_repository(repository).visible?(repository) + end + + def repository_writable?(repository) + scope_repository(repository).writable?(repository) + end + + def scope_repository(repository, method = caller_locations.first.base_label) return false if name and repository.name != name - unscoped.visible?(repository) if repository.owner_name == owner_name + repository.owner_name == owner_name ? unscoped : anonymous end end end diff --git a/lib/travis/api/v3/access_control/user.rb b/lib/travis/api/v3/access_control/user.rb index f1979dfa..c6a2dd25 100644 --- a/lib/travis/api/v3/access_control/user.rb +++ b/lib/travis/api/v3/access_control/user.rb @@ -2,12 +2,12 @@ require 'travis/api/v3/access_control/generic' module Travis::API::V3 class AccessControl::User < AccessControl::Generic - attr_reader :user, :permissions + attr_reader :user, :access_permissions def initialize(user) - user = Models::User.find(user.id) if user.is_a? ::User - @user = user - @permissions = user.permissions.where(user_id: user.id) + user = Models::User.find(user.id) if user.is_a? ::User + @user = user + @access_permissions = user.permissions.where(user_id: user.id) super() end @@ -20,7 +20,7 @@ module Travis::API::V3 end def visible_repositories(list) - list.where('repositories.private = false OR repositories.id IN (?)'.freeze, permissions.map(&:repository_id)) + list.where('repositories.private = false OR repositories.id IN (?)'.freeze, access_permissions.map(&:repository_id)) end protected @@ -35,7 +35,7 @@ module Travis::API::V3 def permission?(type, id) id = id.id if id.is_a? ::Repository - permissions.where(type => true, :repository_id => id).any? + access_permissions.where(type => true, :repository_id => id).any? end end end diff --git a/lib/travis/api/v3/permissions.rb b/lib/travis/api/v3/permissions.rb new file mode 100644 index 00000000..8b4c58b7 --- /dev/null +++ b/lib/travis/api/v3/permissions.rb @@ -0,0 +1,5 @@ +module Travis::API::V3 + module Permissions + extend ConstantResolver + end +end diff --git a/lib/travis/api/v3/permissions/generic.rb b/lib/travis/api/v3/permissions/generic.rb new file mode 100644 index 00000000..5648adfd --- /dev/null +++ b/lib/travis/api/v3/permissions/generic.rb @@ -0,0 +1,60 @@ +module Travis::API::V3 + class Permissions::Generic + def self.access_rights + @access_rights ||= begin + rights = superclass.respond_to?(:access_rights) ? superclass.access_rights.dup : {} + public_instance_methods(false).each do |method| + next unless method.to_s =~ /^([^_].+)\?$/ + rights[$1.to_sym] = method + end + rights + end + end + + # for any public method defined with a question mark in the end, it defines a method with an + # exclamation mark that will raise an InsufficientAccess error if the question mark version + # returns false + def self.method_added(method_name) + super + + return unless public_method_defined?(method_name) + return unless method_name.to_s =~ /^([^_].+)\?$/ + + permission = $1 + type = name[/[^:]+$/].underscore + + class_eval <<-RUBY + def #{permission}! + return self if #{permission}? + payload = { + resource_type: "#{type}".freeze, + permission: "#{permission}".freeze + } + payload[:#{type}] = object if read? + raise InsufficientAccess.new('operation requires #{permission} access to #{type}', payload) + end + RUBY + end + + attr_accessor :access_control, :object + + def initialize(access_control, object) + @access_control = access_control + @object = object + end + + def read? + access_control.visible? object + end + + def to_h + self.class.access_rights.map { |k,v| [k,!!public_send(v)] }.to_h + end + + private + + def write? + access_control.writable? object + end + end +end diff --git a/lib/travis/api/v3/permissions/repository.rb b/lib/travis/api/v3/permissions/repository.rb new file mode 100644 index 00000000..b89d6ae4 --- /dev/null +++ b/lib/travis/api/v3/permissions/repository.rb @@ -0,0 +1,15 @@ +module Travis::API::V3 + class Permissions::Repository < Permissions::Generic + def enable? + write? + end + + def disable? + write? + end + + def create_request? + write? + end + end +end diff --git a/lib/travis/api/v3/renderer/model_renderer.rb b/lib/travis/api/v3/renderer/model_renderer.rb index 491aa627..81b4a864 100644 --- a/lib/travis/api/v3/renderer/model_renderer.rb +++ b/lib/travis/api/v3/renderer/model_renderer.rb @@ -43,7 +43,7 @@ module Travis::API::V3 @script_name = script_name @include = include @included = included - @access_control = access_control + @access_control = access_control || AccessControl::Anonymous.new end def href @@ -74,13 +74,17 @@ module Travis::API::V3 nested_included = included + [model] modes = {} + if permissions = access_control.permissions(model) and (representation != :minimal or include? :@permissions) + result[:@permissions] = permissions.to_h + end + if include.any? excepted_type = result[:@type].to_s fields = fields.dup end include.each do |qualified_field| - raise WrongParams, 'illegal format for include parameter'.freeze unless /\A(?\w+)\.(?\w+)\Z$/ =~ qualified_field + raise WrongParams, 'illegal format for include parameter'.freeze unless /\A(?\w+)\.(?@?\w+)\Z$/ =~ qualified_field next if prefix != excepted_type raise WrongParams, 'no field %p to include'.freeze % qualified_field unless self.class.available_attributes.include?(field) diff --git a/lib/travis/api/v3/services/repository/disable.rb b/lib/travis/api/v3/services/repository/disable.rb index 08a191bb..c904351e 100644 --- a/lib/travis/api/v3/services/repository/disable.rb +++ b/lib/travis/api/v3/services/repository/disable.rb @@ -3,6 +3,7 @@ module Travis::API::V3 def run!(activate = false) raise LoginRequired unless access_control.logged_in? or access_control.full_access? raise NotFound unless repository = find(:repository) + check_access(repository) admin = access_control.admin_for(repository) @@ -11,5 +12,9 @@ module Travis::API::V3 repository end + + def check_access(repository) + access_control.permissions(repository).disable! + end end end diff --git a/lib/travis/api/v3/services/repository/enable.rb b/lib/travis/api/v3/services/repository/enable.rb index 5d85439b..5d43bbe7 100644 --- a/lib/travis/api/v3/services/repository/enable.rb +++ b/lib/travis/api/v3/services/repository/enable.rb @@ -3,5 +3,9 @@ module Travis::API::V3 def run! super(true) end + + def check_access(repository) + access_control.permissions(repository).enable! + end end end diff --git a/lib/travis/api/v3/services/requests/create.rb b/lib/travis/api/v3/services/requests/create.rb index 2ff6f156..40a299c5 100644 --- a/lib/travis/api/v3/services/requests/create.rb +++ b/lib/travis/api/v3/services/requests/create.rb @@ -8,9 +8,9 @@ module Travis::API::V3 params "request", "user", :config, :message, :branch def run - raise LoginRequired unless access_control.logged_in? or access_control.full_access? - raise NotFound unless repository = find(:repository) - raise PushAccessRequired, repository: repository unless access_control.writable?(repository) + raise LoginRequired unless access_control.logged_in? or access_control.full_access? + raise NotFound unless repository = find(:repository) + access_control.permissions(repository).create_request! user = find(:user) if access_control.full_access? and params_for? 'user'.freeze user ||= access_control.user diff --git a/spec/v3/services/owner/find_spec.rb b/spec/v3/services/owner/find_spec.rb index 90152fd8..b5dd0c82 100644 --- a/spec/v3/services/owner/find_spec.rb +++ b/spec/v3/services/owner/find_spec.rb @@ -39,6 +39,11 @@ describe Travis::API::V3::Services::Owner::Find do "repositories" => [{ "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", + "@permissions" => { + "read" => true, + "enable" => false, + "disable" => false, + "create_request"=> false}, "id" => repo.id, "name" => "example-repo", "slug" => "example-org/example-repo", @@ -76,6 +81,11 @@ describe Travis::API::V3::Services::Owner::Find do "repositories" => [{ "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", + "@permissions" => { + "read" => true, + "enable" => false, + "disable" => false, + "create_request"=> false}, "id" => repo.id, "name" => "example-repo", "slug" => "example-org/example-repo", diff --git a/spec/v3/services/owner/repositories_spec.rb b/spec/v3/services/owner/repositories_spec.rb index 9f47fecb..77285bfc 100644 --- a/spec/v3/services/owner/repositories_spec.rb +++ b/spec/v3/services/owner/repositories_spec.rb @@ -18,6 +18,11 @@ describe Travis::API::V3::Services::Owner::Repositories do "repositories" => [{ "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", + "@permissions" => { + "read" => true, + "enable" => false, + "disable" => false, + "create_request"=> false}, "id" => repo.id, "name" => "minimal", "slug" => "svenfuchs/minimal", diff --git a/spec/v3/services/repositories/for_current_user_spec.rb b/spec/v3/services/repositories/for_current_user_spec.rb index 67068f69..26fe6151 100644 --- a/spec/v3/services/repositories/for_current_user_spec.rb +++ b/spec/v3/services/repositories/for_current_user_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' describe Travis::API::V3::Services::Repositories::ForCurrentUser do let(:repo) { Repository.by_slug('svenfuchs/minimal').first } - let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) } - let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} - before { Permission.create(repository: repo, user: repo.owner, pull: true) } - before { repo.update_attribute(:private, true) } - after { repo.update_attribute(:private, false) } + let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} + before { Permission.create(repository: repo, user: repo.owner, pull: true, push: true) } + before { repo.update_attribute(:private, true) } + after { repo.update_attribute(:private, false) } describe "private repository, private API, authenticated as user with access" do before { get("/v3/repos", {}, headers) } @@ -18,6 +18,11 @@ describe Travis::API::V3::Services::Repositories::ForCurrentUser do "repositories" => [{ "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", + "@permissions" => { + "read" => true, + "enable" => true, + "disable" => true, + "create_request"=> true}, "id" => repo.id, "name" => "minimal", "slug" => "svenfuchs/minimal", diff --git a/spec/v3/services/repository/find_spec.rb b/spec/v3/services/repository/find_spec.rb index b92df8d0..d0d6d431 100644 --- a/spec/v3/services/repository/find_spec.rb +++ b/spec/v3/services/repository/find_spec.rb @@ -10,6 +10,11 @@ describe Travis::API::V3::Services::Repository::Find do example { expect(parsed_body).to be == { "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", + "@permissions" => { + "read" => true, + "enable" => false, + "disable" => false, + "create_request"=> false}, "id" => repo.id, "name" => "minimal", "slug" => "svenfuchs/minimal", @@ -95,6 +100,11 @@ describe Travis::API::V3::Services::Repository::Find do example { expect(parsed_body).to be == { "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", + "@permissions" => { + "read" => true, + "enable" => false, + "disable" => false, + "create_request"=> false}, "id" => repo.id, "name" => "minimal", "slug" => "svenfuchs/minimal", @@ -165,6 +175,11 @@ describe Travis::API::V3::Services::Repository::Find do example { expect(parsed_body).to be == { "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", + "@permissions" => { + "read" => true, + "enable" => true, + "disable" => true, + "create_request"=> true}, "id" => repo.id, "name" => "minimal", "slug" => "svenfuchs/minimal", @@ -241,6 +256,11 @@ describe Travis::API::V3::Services::Repository::Find do example { expect(parsed_body).to be == { "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", + "@permissions" => { + "read" => true, + "enable" => true, + "disable" => true, + "create_request"=> true}, "id" => repo.id, "name" => "minimal", "slug" => "svenfuchs/minimal", diff --git a/spec/v3/services/requests/create_spec.rb b/spec/v3/services/requests/create_spec.rb index 62b32089..b316ca82 100644 --- a/spec/v3/services/requests/create_spec.rb +++ b/spec/v3/services/requests/create_spec.rb @@ -49,8 +49,10 @@ describe Travis::API::V3::Services::Requests::Create do example { expect(last_response.status).to be == 403 } example { expect(JSON.load(body)).to be == { "@type" => "error", - "error_type" => "push_access_required", - "error_message" => "push access required", + "error_type" => "insufficient_access", + "error_message" => "operation requires create_request access to repository", + "resource_type" => "repository", + "permission" => "create_request", "repository" => { "@type" => "repository", "@href" => "/repo/#{repo.id}",