Merge pull request #171 from travis-ci/rkh-v3-permissions

[WiP] v3: expose access permissions
This commit is contained in:
Konstantin Haase 2015-04-29 11:40:59 +02:00
commit 02d212c1e4
16 changed files with 183 additions and 26 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
module Travis::API::V3
module Permissions
extend ConstantResolver
end
end

View File

@ -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

View File

@ -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

View File

@ -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(?<prefix>\w+)\.(?<field>\w+)\Z$/ =~ qualified_field
raise WrongParams, 'illegal format for include parameter'.freeze unless /\A(?<prefix>\w+)\.(?<field>@?\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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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}",