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 ...@@ -56,24 +56,26 @@ class Ability
end end
end end
def allowed?(user, action, subject = :global) def allowed?(user, action, subject = :global, opts = {})
allowed(user, subject).include?(action) if subject.is_a?(Hash)
end opts, subject = subject, :global
end
def allowed(user, subject = :global) policy = policy_for(user, subject)
return BasePolicy::RuleSet.none if subject.nil?
return uncached_allowed(user, subject) unless RequestStore.active?
user_key = user ? user.id : 'anonymous' case opts[:scope]
subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}" when :user
key = "/ability/#{user_key}/#{subject_key}" DeclarativePolicy.user_scope { policy.can?(action) }
RequestStore[key] ||= uncached_allowed(user, subject).freeze when :subject
DeclarativePolicy.subject_scope { policy.can?(action) }
else
policy.can?(action)
end
end end
private def policy_for(user, subject = :global)
cache = RequestStore.active? ? RequestStore : {}
def uncached_allowed(user, subject) DeclarativePolicy.policy_for(user, subject, cache: cache)
BasePolicy.class_for(subject).abilities(user, subject)
end end
end end
end end
...@@ -51,8 +51,11 @@ class ProjectFeature < ActiveRecord::Base ...@@ -51,8 +51,11 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user) def feature_available?(feature, user)
access_level = public_send(ProjectFeature.access_level_attribute(feature)) get_permission(user, access_level(feature))
get_permission(user, access_level) end
def access_level(feature)
public_send(ProjectFeature.access_level_attribute(feature))
end end
def builds_enabled? def builds_enabled?
......
class BasePolicy require 'declarative_policy'
class RuleSet
attr_reader :can_set, :cannot_set
def initialize(can_set, cannot_set)
@can_set = can_set
@cannot_set = cannot_set
end
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 with_options scope: :user, score: 0
new(Set.new, Set.new) condition(:external_user) { @user.nil? || @user.external? }
end
def self.none with_options scope: :user, score: 0
empty.freeze condition(:can_create_group) { @user&.can_create_group }
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
end end
module Ci module Ci
class BuildPolicy < CommitStatusPolicy class BuildPolicy < CommitStatusPolicy
alias_method :build, :subject condition(:protected_action) do
next false unless @subject.action?
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?
!::Gitlab::UserAccess !::Gitlab::UserAccess
.new(user, project: build.project) .new(@user, project: @subject.project)
.can_merge_to_branch?(build.ref) .can_merge_to_branch?(@subject.ref)
end end
rule { protected_action }.prevent :update_build
end end
end end
module Ci module Ci
class PipelinePolicy < BasePolicy class PipelinePolicy < BasePolicy
def rules delegate { @subject.project }
delegate! @subject.project
end
end end
end end
module Ci module Ci
class RunnerPolicy < BasePolicy class RunnerPolicy < BasePolicy
def rules with_options scope: :subject, score: 0
return unless @user 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) rule { anonymous }.prevent_all
end rule { admin | authorized_runner }.enable :assign_runner
rule { ~admin & shared }.prevent :assign_runner
rule { ~admin & locked }.prevent :assign_runner
end end
end end
module Ci module Ci
class TriggerPolicy < BasePolicy class TriggerPolicy < BasePolicy
def rules delegate { @subject.project }
delegate! @subject.project
with_options scope: :subject, score: 0
if can?(:admin_build) condition(:legacy) { @subject.legacy? }
can! :admin_trigger if @subject.owner.blank? ||
@subject.owner == @user with_score 0
can! :manage_trigger condition(:is_owner) { @user && @subject.owner_id == @user.id }
end
end rule { ~can?(:admin_build) }.prevent :admin_trigger
rule { legacy | is_owner }.enable :admin_trigger
rule { can?(:admin_build) }.enable :manage_trigger
end end
end end
class CommitStatusPolicy < BasePolicy 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
end end
class DeployKeyPolicy < BasePolicy class DeployKeyPolicy < BasePolicy
def rules with_options scope: :subject, score: 0
return unless @user 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) rule { anonymous }.prevent_all
can! :update_deploy_key
end rule { admin }.enable :update_deploy_key
end rule { private_deploy_key & has_deploy_key }.enable :update_deploy_key
end end
class DeploymentPolicy < BasePolicy class DeploymentPolicy < BasePolicy
def rules delegate { @subject.project }
delegate! @subject.project
end
end end
class EnvironmentPolicy < BasePolicy class EnvironmentPolicy < BasePolicy
alias_method :environment, :subject delegate { @subject.project }
def rules condition(:stop_action_allowed) do
delegate! environment.project @subject.stop_action? && can?(:update_build, @subject.stop_action)
if can?(:create_deployment) && environment.stop_action?
can! :stop_environment if can_play_stop_action?
end
end end
private rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment
def can_play_stop_action?
Ability.allowed?(user, :update_build, environment.stop_action)
end
end end
class ExternalIssuePolicy < BasePolicy class ExternalIssuePolicy < BasePolicy
def rules delegate { @subject.project }
delegate! @subject.project
end
end end
class GlobalPolicy < BasePolicy class GlobalPolicy < BasePolicy
def rules desc "User is blocked"
return unless @user with_options scope: :user, score: 0
condition(:blocked) { @user.blocked? }
can! :create_group if @user.can_create_group desc "User is an internal user"
can! :read_users_list with_options scope: :user, score: 0
condition(:internal) { @user.internal? }
unless @user.blocked? || @user.internal? desc "User's access has been locked"
can! :log_in unless @user.access_locked? with_options scope: :user, score: 0
can! :access_api condition(:access_locked) { @user.access_locked? }
can! :access_git
can! :receive_notifications rule { anonymous }.prevent_all
can! :use_quick_actions
end 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
end end
class GroupLabelPolicy < BasePolicy class GroupLabelPolicy < BasePolicy
def rules delegate { @subject.group }
delegate! @subject.group
end
end end
class GroupMemberPolicy < BasePolicy class GroupMemberPolicy < BasePolicy
def rules delegate :group
return unless @user
target_user = @subject.user with_scope :subject
group = @subject.group 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 rule { can?(:admin_group_member) }.policy do
can! :update_group_member enable :update_group_member
can! :destroy_group_member enable :destroy_group_member
elsif @user == target_user
can! :destroy_group_member
end
additional_rules!
end end
def additional_rules! rule { is_target_user }.policy do
# This is meant to be overriden in EE enable :destroy_group_member
end end
end end
class GroupPolicy < BasePolicy class GroupPolicy < BasePolicy
def rules desc "Group is public"
can! :read_group if @subject.public? with_options scope: :subject, score: 0
return unless @user condition(:public_group) { @subject.public? }
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) with_score 0
access_level = @subject.max_member_access_for_user(@user) condition(:logged_in_viewable) { @user && @subject.internal? && !@user.external? }
owner = access_level >= GroupMember::OWNER
master = access_level >= GroupMember::MASTER condition(:has_access) { access_level != GroupMember::NO_ACCESS }
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
def can_read_group? condition(:guest) { access_level >= GroupMember::GUEST }
return true if @subject.public? condition(:owner) { access_level >= GroupMember::OWNER }
return true if @user.admin? condition(:master) { access_level >= GroupMember::MASTER }
return true if @subject.internal? && !@user.external? condition(:reporter) { access_level >= GroupMember::REPORTER }
return true if @subject.users.include?(@user)
condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end 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 end
class IssuablePolicy < BasePolicy class IssuablePolicy < BasePolicy
def action_name delegate { @subject.project }
@subject.class.name.underscore
end
def rules desc "User is the assignee or author"
if @user && @subject.assignee_or_author?(@user) condition(:assignee_or_author) do
can! :"read_#{action_name}" @user && @subject.assignee_or_author?(@user)
can! :"update_#{action_name}" end
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
end end
...@@ -3,25 +3,17 @@ class IssuePolicy < IssuablePolicy ...@@ -3,25 +3,17 @@ class IssuePolicy < IssuablePolicy
# Make sure to sync this class checks with issue.rb to avoid security problems. # Make sure to sync this class checks with issue.rb to avoid security problems.
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
def issue desc "User can read confidential issues"
@subject condition(:can_read_confidential) do
@user && IssueCollection.new([@subject]).visible_to(@user).any?
end end
def rules desc "Issue is confidential"
super condition(:confidential, scope: :subject) { @subject.confidential? }
if @subject.confidential? && !can_read_confidential? rule { confidential & ~can_read_confidential }.policy do
cannot! :read_issue prevent :read_issue
cannot! :update_issue prevent :update_issue
cannot! :admin_issue prevent :admin_issue
end
end
private
def can_read_confidential?
return false unless @user
IssueCollection.new([@subject]).visible_to(@user).any?
end end
end end
class NamespacePolicy < BasePolicy class NamespacePolicy < BasePolicy
def rules rule { anonymous }.prevent_all
return unless @user
if @subject.owner == @user || @user.admin? condition(:owner) { @subject.owner == @user }
can! :create_projects
can! :admin_namespace rule { owner | admin }.policy do
end enable :create_projects
enable :admin_namespace
end end
end end
class NilPolicy < BasePolicy
rule { default }.prevent_all
end
class NotePolicy < BasePolicy 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 condition(:editable, scope: :subject) { @subject.editable? }
can! :read_note
can! :update_note
can! :admin_note
can! :resolve_note
end
if @subject.for_merge_request? && rule { ~editable | anonymous }.prevent :edit_note
@subject.noteable.author == @user rule { is_author | admin }.enable :edit_note
can! :resolve_note rule { can?(:master_access) }.enable :edit_note
end
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
end end
class PersonalSnippetPolicy < BasePolicy class PersonalSnippetPolicy < BasePolicy
def rules condition(:public_snippet, scope: :subject) { @subject.public? }
can! :read_personal_snippet if @subject.public? condition(:is_author) { @user && @subject.author == @user }
return unless @user condition(:internal_snippet, scope: :subject) { @subject.internal? }
if @subject.public? rule { public_snippet }.policy do
can! :comment_personal_snippet enable :read_personal_snippet
end enable :comment_personal_snippet
end
if @subject.author == @user rule { is_author }.policy do
can! :read_personal_snippet enable :read_personal_snippet
can! :update_personal_snippet enable :update_personal_snippet
can! :destroy_personal_snippet enable :destroy_personal_snippet
can! :admin_personal_snippet enable :admin_personal_snippet
can! :comment_personal_snippet enable :comment_personal_snippet
end end
unless @user.external? rule { ~anonymous }.enable :create_personal_snippet
can! :create_personal_snippet rule { external_user }.prevent :create_personal_snippet
end
if @subject.internal? && !@user.external? rule { internal_snippet & ~external_user }.policy do
can! :read_personal_snippet enable :read_personal_snippet
can! :comment_personal_snippet enable :comment_personal_snippet
end
end end
rule { anonymous }.prevent :comment_personal_snippet
end end
class ProjectLabelPolicy < BasePolicy class ProjectLabelPolicy < BasePolicy
def rules delegate { @subject.project }
delegate! @subject.project
end
end end
class ProjectMemberPolicy < BasePolicy class ProjectMemberPolicy < BasePolicy
def rules delegate { @subject.project }
# anonymous users have no abilities here
return unless @user
target_user = @subject.user condition(:target_is_owner, scope: :subject) { @subject.user == @subject.project.owner }
project = @subject.project 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) rule { can?(:admin_project_member) }.policy do
enable :update_project_member
if can_manage enable :destroy_project_member
can! :update_project_member
can! :destroy_project_member
end
if @user == target_user
can! :destroy_project_member
end
end end
rule { target_is_self }.enable :destroy_project_member
end end
This diff is collapsed.
class ProjectSnippetPolicy < BasePolicy class ProjectSnippetPolicy < BasePolicy
def rules delegate :project
# 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 desc "Snippet is public"
return unless @subject.project.feature_available?(:snippets, @user) condition(:public_snippet, scope: :subject) { @subject.public? }
return unless Ability.allowed?(@user, :read_project, @subject.project) condition(:private_snippet, scope: :subject) { @subject.private? }
condition(:public_project, scope: :subject) { @subject.project.public? }
can! :read_project_snippet if @subject.public?
return unless @user condition(:is_author) { @user && @subject.author == @user }
if @user && (@subject.author == @user || @user.admin?) condition(:internal, scope: :subject) { @subject.internal? }
can! :read_project_snippet
can! :update_project_snippet # We have to check both project feature visibility and a snippet visibility and take the stricter one
can! :admin_project_snippet # This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573
end rule { ~can?(:read_project) }.policy do
prevent :read_project_snippet
if @subject.internal? && !@user.external? prevent :update_project_snippet
can! :read_project_snippet prevent :admin_project_snippet
end end
if @subject.project.team.member?(@user) # we have to use this complicated prevent because the delegated project policy
can! :read_project_snippet # is overly greedy in allowing :read_project_snippet, since it doesn't have any
end # 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
end end
class UserPolicy < BasePolicy class UserPolicy < BasePolicy
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
def rules desc "The application is restricted from public visibility"
can! :read_user if @user || !restricted_public_level? condition(:restricted_public_level, scope: :global) do
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
if @user desc "The current user is the user in question"
if @user.admin? || @subject == @user condition(:user_is_self, score: 0) { @subject == @user }
can! :destroy_user
end
cannot! :destroy_user if @subject.ghost? desc "This is the ghost user"
end condition(:subject_ghost, scope: :subject, score: 0) { @subject.ghost? }
end
def restricted_public_level? rule { ~restricted_public_level }.enable :read_user
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) rule { ~anonymous }.enable :read_user
end
rule { user_is_self | admin }.enable :destroy_user
rule { subject_ghost }.prevent :destroy_user
end end
module Gitlab module Gitlab
module Allowable module Allowable
def can?(user, action, subject = :global) def can?(*args)
Ability.allowed?(user, action, subject) Ability.allowed?(*args)
end end
end end
end end
...@@ -155,8 +155,8 @@ describe ProjectPolicy, models: true do ...@@ -155,8 +155,8 @@ describe ProjectPolicy, models: true do
end end
it do it do
is_expected.not_to include(:read_build) expect_disallowed(:read_build)
is_expected.to include(:read_pipeline) expect_allowed(:read_pipeline)
end end
end end
end end
......
...@@ -119,7 +119,7 @@ describe ProjectSnippetPolicy, models: true do ...@@ -119,7 +119,7 @@ describe ProjectSnippetPolicy, models: true do
context 'snippet author' do context 'snippet author' do
let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } 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 it do
expect_allowed(:read_project_snippet) 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