Commit 37c40143 authored by http://jneen.net/'s avatar http://jneen.net/

convert all the policies to DeclarativePolicy

parent e5aad75a
......@@ -56,24 +56,26 @@ class Ability
end
end
def allowed?(user, action, subject = :global)
allowed(user, subject).include?(action)
end
def allowed?(user, action, subject = :global, opts = {})
if subject.is_a?(Hash)
opts, subject = subject, :global
end
def allowed(user, subject = :global)
return BasePolicy::RuleSet.none if subject.nil?
return uncached_allowed(user, subject) unless RequestStore.active?
policy = policy_for(user, subject)
user_key = user ? user.id : 'anonymous'
subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}"
key = "/ability/#{user_key}/#{subject_key}"
RequestStore[key] ||= uncached_allowed(user, subject).freeze
case opts[:scope]
when :user
DeclarativePolicy.user_scope { policy.can?(action) }
when :subject
DeclarativePolicy.subject_scope { policy.can?(action) }
else
policy.can?(action)
end
end
private
def uncached_allowed(user, subject)
BasePolicy.class_for(subject).abilities(user, subject)
def policy_for(user, subject = :global)
cache = RequestStore.active? ? RequestStore : {}
DeclarativePolicy.policy_for(user, subject, cache: cache)
end
end
end
......@@ -51,8 +51,11 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user)
access_level = public_send(ProjectFeature.access_level_attribute(feature))
get_permission(user, access_level)
get_permission(user, access_level(feature))
end
def access_level(feature)
public_send(ProjectFeature.access_level_attribute(feature))
end
def builds_enabled?
......
class BasePolicy
class RuleSet
attr_reader :can_set, :cannot_set
def initialize(can_set, cannot_set)
@can_set = can_set
@cannot_set = cannot_set
end
require 'declarative_policy'
delegate :size, to: :to_set
class BasePolicy < DeclarativePolicy::Base
desc "User is an instance admin"
with_options scope: :user, score: 0
condition(:admin) { @user&.admin? }
def self.empty
new(Set.new, Set.new)
end
with_options scope: :user, score: 0
condition(:external_user) { @user.nil? || @user.external? }
def self.none
empty.freeze
end
def can?(ability)
@can_set.include?(ability) && !@cannot_set.include?(ability)
end
def include?(ability)
can?(ability)
end
def to_set
@can_set - @cannot_set
end
def merge(other)
@can_set.merge(other.can_set)
@cannot_set.merge(other.cannot_set)
end
def can!(*abilities)
@can_set.merge(abilities)
end
def cannot!(*abilities)
@cannot_set.merge(abilities)
end
def freeze
@can_set.freeze
@cannot_set.freeze
super
end
end
def self.abilities(user, subject)
new(user, subject).abilities
end
def self.class_for(subject)
return GlobalPolicy if subject == :global
raise ArgumentError, 'no policy for nil' if subject.nil?
if subject.class.try(:presenter?)
subject = subject.subject
end
subject.class.ancestors.each do |klass|
next unless klass.name
begin
policy_class = "#{klass.name}Policy".constantize
# NOTE: the < operator here tests whether policy_class
# inherits from BasePolicy
return policy_class if policy_class < BasePolicy
rescue NameError
nil
end
end
raise "no policy for #{subject.class.name}"
end
attr_reader :user, :subject
def initialize(user, subject)
@user = user
@subject = subject
end
def abilities
return RuleSet.none if @user && @user.blocked?
return anonymous_abilities if @user.nil?
collect_rules { rules }
end
def anonymous_abilities
collect_rules { anonymous_rules }
end
def anonymous_rules
rules
end
def rules
raise NotImplementedError
end
def delegate!(new_subject)
@rule_set.merge(Ability.allowed(@user, new_subject))
end
def can?(rule)
@rule_set.can?(rule)
end
def can!(*rules)
@rule_set.can!(*rules)
end
def cannot!(*rules)
@rule_set.cannot!(*rules)
end
private
def collect_rules(&b)
@rule_set = RuleSet.empty
yield
@rule_set
end
with_options scope: :user, score: 0
condition(:can_create_group) { @user&.can_create_group }
end
module Ci
class BuildPolicy < CommitStatusPolicy
alias_method :build, :subject
def rules
super
# If we can't read build we should also not have that
# ability when looking at this in context of commit_status
%w[read create update admin].each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end
if can?(:update_build) && protected_action?
cannot! :update_build
end
end
private
def protected_action?
return false unless build.action?
condition(:protected_action) do
next false unless @subject.action?
!::Gitlab::UserAccess
.new(user, project: build.project)
.can_merge_to_branch?(build.ref)
.new(@user, project: @subject.project)
.can_merge_to_branch?(@subject.ref)
end
rule { protected_action }.prevent :update_build
end
end
module Ci
class PipelinePolicy < BasePolicy
def rules
delegate! @subject.project
end
delegate { @subject.project }
end
end
module Ci
class RunnerPolicy < BasePolicy
def rules
return unless @user
with_options scope: :subject, score: 0
condition(:shared) { @subject.is_shared? }
can! :assign_runner if @user.admin?
with_options scope: :subject, score: 0
condition(:locked, scope: :subject) { @subject.locked? }
return if @subject.is_shared? || @subject.locked?
condition(:authorized_runner) { @user.ci_authorized_runners.include?(@subject) }
can! :assign_runner if @user.ci_authorized_runners.include?(@subject)
end
rule { anonymous }.prevent_all
rule { admin | authorized_runner }.enable :assign_runner
rule { ~admin & shared }.prevent :assign_runner
rule { ~admin & locked }.prevent :assign_runner
end
end
module Ci
class TriggerPolicy < BasePolicy
def rules
delegate! @subject.project
if can?(:admin_build)
can! :admin_trigger if @subject.owner.blank? ||
@subject.owner == @user
can! :manage_trigger
end
end
delegate { @subject.project }
with_options scope: :subject, score: 0
condition(:legacy) { @subject.legacy? }
with_score 0
condition(:is_owner) { @user && @subject.owner_id == @user.id }
rule { ~can?(:admin_build) }.prevent :admin_trigger
rule { legacy | is_owner }.enable :admin_trigger
rule { can?(:admin_build) }.enable :manage_trigger
end
end
class CommitStatusPolicy < BasePolicy
def rules
delegate! @subject.project
delegate { @subject.project }
%w[read create update admin].each do |action|
rule { ~can?(:"#{action}_commit_status") }.prevent :"#{action}_build"
end
end
class DeployKeyPolicy < BasePolicy
def rules
return unless @user
with_options scope: :subject, score: 0
condition(:private_deploy_key) { @subject.private? }
can! :update_deploy_key if @user.admin?
condition(:has_deploy_key) { @user.project_deploy_keys.exists?(id: @subject.id) }
if @subject.private? && @user.project_deploy_keys.exists?(id: @subject.id)
can! :update_deploy_key
end
end
rule { anonymous }.prevent_all
rule { admin }.enable :update_deploy_key
rule { private_deploy_key & has_deploy_key }.enable :update_deploy_key
end
class DeploymentPolicy < BasePolicy
def rules
delegate! @subject.project
end
delegate { @subject.project }
end
class EnvironmentPolicy < BasePolicy
alias_method :environment, :subject
delegate { @subject.project }
def rules
delegate! environment.project
if can?(:create_deployment) && environment.stop_action?
can! :stop_environment if can_play_stop_action?
end
condition(:stop_action_allowed) do
@subject.stop_action? && can?(:update_build, @subject.stop_action)
end
private
def can_play_stop_action?
Ability.allowed?(user, :update_build, environment.stop_action)
end
rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment
end
class ExternalIssuePolicy < BasePolicy
def rules
delegate! @subject.project
end
delegate { @subject.project }
end
class GlobalPolicy < BasePolicy
def rules
return unless @user
desc "User is blocked"
with_options scope: :user, score: 0
condition(:blocked) { @user.blocked? }
can! :create_group if @user.can_create_group
can! :read_users_list
desc "User is an internal user"
with_options scope: :user, score: 0
condition(:internal) { @user.internal? }
unless @user.blocked? || @user.internal?
can! :log_in unless @user.access_locked?
can! :access_api
can! :access_git
can! :receive_notifications
can! :use_quick_actions
end
desc "User's access has been locked"
with_options scope: :user, score: 0
condition(:access_locked) { @user.access_locked? }
rule { anonymous }.prevent_all
rule { default }.policy do
enable :read_users_list
enable :log_in
enable :access_api
enable :access_git
enable :receive_notifications
enable :use_quick_actions
end
rule { blocked | internal }.policy do
prevent :log_in
prevent :access_api
prevent :access_git
prevent :receive_notifications
prevent :use_quick_actions
end
rule { can_create_group }.policy do
enable :create_group
end
rule { access_locked }.policy do
prevent :log_in
end
end
class GroupLabelPolicy < BasePolicy
def rules
delegate! @subject.group
end
delegate { @subject.group }
end
class GroupMemberPolicy < BasePolicy
def rules
return unless @user
delegate :group
target_user = @subject.user
group = @subject.group
with_scope :subject
condition(:last_owner) { @subject.group.last_owner?(@subject.user) }
return if group.last_owner?(target_user)
desc "Membership is users' own"
with_score 0
condition(:is_target_user) { @user && @subject.user_id == @user.id }
can_manage = Ability.allowed?(@user, :admin_group_member, group)
rule { anonymous }.prevent_all
rule { last_owner }.prevent_all
if can_manage
can! :update_group_member
can! :destroy_group_member
elsif @user == target_user
can! :destroy_group_member
end
additional_rules!
rule { can?(:admin_group_member) }.policy do
enable :update_group_member
enable :destroy_group_member
end
def additional_rules!
# This is meant to be overriden in EE
rule { is_target_user }.policy do
enable :destroy_group_member
end
end
class GroupPolicy < BasePolicy
def rules
can! :read_group if @subject.public?
return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
access_level = @subject.max_member_access_for_user(@user)
owner = access_level >= GroupMember::OWNER
master = access_level >= GroupMember::MASTER
reporter = access_level >= GroupMember::REPORTER
can_read = false
can_read ||= globally_viewable
can_read ||= access_level >= GroupMember::GUEST
can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
can! :read_group if can_read
if reporter
can! :admin_label
end
# Only group masters and group owners can create new projects
if master
can! :create_projects
can! :admin_milestones
end
# Only group owner and administrators can admin group
if owner
can! :admin_group
can! :admin_namespace
can! :admin_group_member
can! :change_visibility_level
can! :create_subgroup if @user.can_create_group
end
if globally_viewable && @subject.request_access_enabled && access_level == GroupMember::NO_ACCESS
can! :request_access
end
end
desc "Group is public"
with_options scope: :subject, score: 0
condition(:public_group) { @subject.public? }
with_score 0
condition(:logged_in_viewable) { @user && @subject.internal? && !@user.external? }
condition(:has_access) { access_level != GroupMember::NO_ACCESS }
def can_read_group?
return true if @subject.public?
return true if @user.admin?
return true if @subject.internal? && !@user.external?
return true if @subject.users.include?(@user)
condition(:guest) { access_level >= GroupMember::GUEST }
condition(:owner) { access_level >= GroupMember::OWNER }
condition(:master) { access_level >= GroupMember::MASTER }
condition(:reporter) { access_level >= GroupMember::REPORTER }
condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end
with_options scope: :subject, score: 0
condition(:request_access_enabled) { @subject.request_access_enabled }
rule { public_group } .enable :read_group
rule { logged_in_viewable }.enable :read_group
rule { guest } .enable :read_group
rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
rule { reporter }.enable :admin_label
rule { master }.policy do
enable :create_projects
enable :admin_milestones
end
rule { owner }.policy do
enable :admin_group
enable :admin_namespace
enable :admin_group_member
enable :change_visibility_level
end
rule { owner & can_create_group }.enable :create_subgroup
rule { public_group | logged_in_viewable }.enable :view_globally
rule { default }.enable(:request_access)
rule { ~request_access_enabled }.prevent :request_access
rule { ~can?(:view_globally) }.prevent :request_access
rule { has_access }.prevent :request_access
def access_level
return GroupMember::NO_ACCESS if @user.nil?
@access_level ||= @subject.max_member_access_for_user(@user)
end
end
class IssuablePolicy < BasePolicy
def action_name
@subject.class.name.underscore
end
delegate { @subject.project }
def rules
if @user && @subject.assignee_or_author?(@user)
can! :"read_#{action_name}"
can! :"update_#{action_name}"
end
desc "User is the assignee or author"
condition(:assignee_or_author) do
@user && @subject.assignee_or_author?(@user)
end
delegate! @subject.project
rule { assignee_or_author }.policy do
enable :read_issue
enable :update_issue
enable :read_merge_request
enable :update_merge_request
end
end
......@@ -3,25 +3,17 @@ class IssuePolicy < IssuablePolicy
# Make sure to sync this class checks with issue.rb to avoid security problems.
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
def issue
@subject
desc "User can read confidential issues"
condition(:can_read_confidential) do
@user && IssueCollection.new([@subject]).visible_to(@user).any?
end
def rules
super
desc "Issue is confidential"
condition(:confidential, scope: :subject) { @subject.confidential? }
if @subject.confidential? && !can_read_confidential?
cannot! :read_issue
cannot! :update_issue
cannot! :admin_issue
end
end
private
def can_read_confidential?
return false unless @user
IssueCollection.new([@subject]).visible_to(@user).any?
rule { confidential & ~can_read_confidential }.policy do
prevent :read_issue
prevent :update_issue
prevent :admin_issue
end
end
class NamespacePolicy < BasePolicy
def rules
return unless @user
rule { anonymous }.prevent_all
if @subject.owner == @user || @user.admin?
can! :create_projects
can! :admin_namespace
end
condition(:owner) { @subject.owner == @user }
rule { owner | admin }.policy do
enable :create_projects
enable :admin_namespace
end
end
class NilPolicy < BasePolicy
rule { default }.prevent_all
end
class NotePolicy < BasePolicy
def rules
delegate! @subject.project
delegate { @subject.project }
return unless @user
condition(:is_author) { @user && @subject.author == @user }
condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? }
condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id }
if @subject.author == @user
can! :read_note
can! :update_note
can! :admin_note
can! :resolve_note
end
condition(:editable, scope: :subject) { @subject.editable? }
if @subject.for_merge_request? &&
@subject.noteable.author == @user
can! :resolve_note
end
rule { ~editable | anonymous }.prevent :edit_note
rule { is_author | admin }.enable :edit_note
rule { can?(:master_access) }.enable :edit_note
rule { is_author }.policy do
enable :read_note
enable :update_note
enable :admin_note
enable :resolve_note
end
rule { for_merge_request & is_noteable_author }.policy do
enable :resolve_note
end
end
class PersonalSnippetPolicy < BasePolicy
def rules
can! :read_personal_snippet if @subject.public?
return unless @user
condition(:public_snippet, scope: :subject) { @subject.public? }
condition(:is_author) { @user && @subject.author == @user }
condition(:internal_snippet, scope: :subject) { @subject.internal? }
if @subject.public?
can! :comment_personal_snippet
end
rule { public_snippet }.policy do
enable :read_personal_snippet
enable :comment_personal_snippet
end
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
can! :destroy_personal_snippet
can! :admin_personal_snippet
can! :comment_personal_snippet
end
rule { is_author }.policy do
enable :read_personal_snippet
enable :update_personal_snippet
enable :destroy_personal_snippet
enable :admin_personal_snippet
enable :comment_personal_snippet
end
unless @user.external?
can! :create_personal_snippet
end
rule { ~anonymous }.enable :create_personal_snippet
rule { external_user }.prevent :create_personal_snippet
if @subject.internal? && !@user.external?
can! :read_personal_snippet
can! :comment_personal_snippet
end
rule { internal_snippet & ~external_user }.policy do
enable :read_personal_snippet
enable :comment_personal_snippet
end
rule { anonymous }.prevent :comment_personal_snippet
end
class ProjectLabelPolicy < BasePolicy
def rules
delegate! @subject.project
end
delegate { @subject.project }
end
class ProjectMemberPolicy < BasePolicy
def rules
# anonymous users have no abilities here
return unless @user
delegate { @subject.project }
target_user = @subject.user
project = @subject.project
condition(:target_is_owner, scope: :subject) { @subject.user == @subject.project.owner }
condition(:target_is_self) { @user && @subject.user == @user }
return if target_user == project.owner
rule { anonymous }.prevent_all
rule { target_is_owner }.prevent_all
can_manage = Ability.allowed?(@user, :admin_project_member, project)
if can_manage
can! :update_project_member
can! :destroy_project_member
end
if @user == target_user
can! :destroy_project_member
end
rule { can?(:admin_project_member) }.policy do
enable :update_project_member
enable :destroy_project_member
end
rule { target_is_self }.enable :destroy_project_member
end
This diff is collapsed.
class ProjectSnippetPolicy < BasePolicy
def rules
# We have to check both project feature visibility and a snippet visibility and take the stricter one
# This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573
return unless @subject.project.feature_available?(:snippets, @user)
return unless Ability.allowed?(@user, :read_project, @subject.project)
can! :read_project_snippet if @subject.public?
return unless @user
if @user && (@subject.author == @user || @user.admin?)
can! :read_project_snippet
can! :update_project_snippet
can! :admin_project_snippet
end
if @subject.internal? && !@user.external?
can! :read_project_snippet
end
if @subject.project.team.member?(@user)
can! :read_project_snippet
end
delegate :project
desc "Snippet is public"
condition(:public_snippet, scope: :subject) { @subject.public? }
condition(:private_snippet, scope: :subject) { @subject.private? }
condition(:public_project, scope: :subject) { @subject.project.public? }
condition(:is_author) { @user && @subject.author == @user }
condition(:internal, scope: :subject) { @subject.internal? }
# We have to check both project feature visibility and a snippet visibility and take the stricter one
# This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573
rule { ~can?(:read_project) }.policy do
prevent :read_project_snippet
prevent :update_project_snippet
prevent :admin_project_snippet
end
# we have to use this complicated prevent because the delegated project policy
# is overly greedy in allowing :read_project_snippet, since it doesn't have any
# information about the snippet. However, :read_project_snippet on the *project*
# is used to hide/show various snippet-related controls, so we can't just move
# all of the handling here.
rule do
all?(private_snippet | (internal & external_user),
~project.guest,
~admin,
~is_author)
end.prevent :read_project_snippet
rule { internal & ~is_author & ~admin }.policy do
prevent :update_project_snippet
prevent :admin_project_snippet
end
rule { public_snippet }.enable :read_project_snippet
rule { is_author | admin }.policy do
enable :read_project_snippet
enable :update_project_snippet
enable :admin_project_snippet
end
end
class UserPolicy < BasePolicy
include Gitlab::CurrentSettings
def rules
can! :read_user if @user || !restricted_public_level?
desc "The application is restricted from public visibility"
condition(:restricted_public_level, scope: :global) do
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
if @user
if @user.admin? || @subject == @user
can! :destroy_user
end
desc "The current user is the user in question"
condition(:user_is_self, score: 0) { @subject == @user }
cannot! :destroy_user if @subject.ghost?
end
end
desc "This is the ghost user"
condition(:subject_ghost, scope: :subject, score: 0) { @subject.ghost? }
def restricted_public_level?
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
rule { ~restricted_public_level }.enable :read_user
rule { ~anonymous }.enable :read_user
rule { user_is_self | admin }.enable :destroy_user
rule { subject_ghost }.prevent :destroy_user
end
module Gitlab
module Allowable
def can?(user, action, subject = :global)
Ability.allowed?(user, action, subject)
def can?(*args)
Ability.allowed?(*args)
end
end
end
......@@ -155,8 +155,8 @@ describe ProjectPolicy, models: true do
end
it do
is_expected.not_to include(:read_build)
is_expected.to include(:read_pipeline)
expect_disallowed(:read_build)
expect_allowed(:read_pipeline)
end
end
end
......
......@@ -119,7 +119,7 @@ describe ProjectSnippetPolicy, models: true do
context 'snippet author' do
let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) }
subject { described_class(regular_user, snippet) }
subject { described_class.new(regular_user, snippet) }
it do
expect_allowed(:read_project_snippet)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment