From b9c36d6c05e71c5a27a2e0530d1afcc162213ab9 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 20 Jan 2017 18:39:44 +0530 Subject: [PATCH 01/26] Allow a user to be an "Auditor" An auditor user is intended to be user with read-only access to all projects and groups. Access to the admin area and any project settings pages are disallowed This commit lays the initial groundwork for this concept - adding an `auditor` column to the `users` table, as well as a few supplements. --- app/models/user.rb | 7 +++++++ .../20170120123345_add_column_auditor_to_users.rb | 14 ++++++++++++++ db/schema.rb | 1 + spec/factories/users.rb | 4 ++++ 4 files changed, 26 insertions(+) create mode 100644 db/migrate/20170120123345_add_column_auditor_to_users.rb diff --git a/app/models/user.rb b/app/models/user.rb index 1209cdddc859..eb60f040c82d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -123,6 +123,7 @@ class User < ActiveRecord::Base validate :unique_email, if: ->(user) { user.email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? } + validate :cannot_be_admin_and_auditor validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :generate_password, on: :create @@ -453,6 +454,12 @@ def update_emails_with_primary_email end end + def cannot_be_admin_and_auditor + if admin? && auditor? + errors.add(:admin, "user cannot also be an Auditor.") + end + end + # Returns the groups a user has access to def authorized_groups union = Gitlab::SQL::Union. diff --git a/db/migrate/20170120123345_add_column_auditor_to_users.rb b/db/migrate/20170120123345_add_column_auditor_to_users.rb new file mode 100644 index 000000000000..e119506b2376 --- /dev/null +++ b/db/migrate/20170120123345_add_column_auditor_to_users.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddColumnAuditorToUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_column_with_default :users, :auditor, :boolean, default: false, allow_null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index c3728be053b4..e22210f6fc01 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1452,6 +1452,7 @@ t.string "incoming_email_token" t.string "organization" t.boolean "authorized_projects_populated" + t.boolean "auditor", default: false, null: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 67843ed8dd9f..ce9353e7342b 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -14,6 +14,10 @@ admin true end + trait :auditor do + auditor true + end + trait :external do external true end -- GitLab From d475c369777bb1b01703cb3bdb508540d8fd0344 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 20 Jan 2017 18:43:05 +0530 Subject: [PATCH 02/26] Allow an auditor user to access all projects / groups In addition, allow an auditor read-only permissions within a project. Collect all the permissions that an auditor is supposed to have in the `auditor_access` method. This _could_ be automated by dynamically listing all permissions that start with `read_`, but this is cleaner / more readable, especially since it's confined to this one location. --- app/finders/group_projects_finder.rb | 2 +- app/finders/issues_finder.rb | 2 +- app/finders/snippets_finder.rb | 2 +- app/models/project_feature.rb | 2 +- app/policies/global_policy.rb | 2 +- app/policies/group_policy.rb | 2 ++ app/policies/namespace_policy.rb | 1 + app/policies/project_policy.rb | 32 +++++++++++++++++++ app/policies/project_snippet_policy.rb | 6 +++- .../projects/notes/_notes_with_form.html.haml | 2 ++ 10 files changed, 47 insertions(+), 6 deletions(-) diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index aa8f4c1d0e4d..4607269813b9 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -18,7 +18,7 @@ def group_projects(current_user) projects = [] if current_user - if @group.users.include?(current_user) || current_user.admin? + if @group.users.include?(current_user) || current_user.admin? || current_user.auditor? projects << @group.projects unless only_shared projects << @group.shared_projects unless only_owned else diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 707eddd4d299..c320fdfe260f 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -33,7 +33,7 @@ def iid_pattern def self.not_restricted_by_confidentiality(user) return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? - return Issue.all if user.admin? + return Issue.all if user.admin? || user.auditor? Issue.where(' issues.confidential IS NULL diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index da6e6e87a6ff..cebcbe606648 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -44,7 +44,7 @@ def by_project(current_user, project, scope) snippets = project.snippets.fresh if current_user - include_private = project.team.member?(current_user) || current_user.admin? + include_private = project.team.member?(current_user) || current_user.admin? || current_user.auditor? by_scope(snippets, scope, include_private) else snippets.are_public diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 03194fc21413..70bb505b6381 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -83,7 +83,7 @@ def get_permission(user, level) when DISABLED false when PRIVATE - user && (project.team.member?(user) || user.admin?) + user && (project.team.member?(user) || user.admin? || user.auditor?) when ENABLED true else diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 3c2fbe6b56ba..e156bb76c13b 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -2,7 +2,7 @@ class GlobalPolicy < BasePolicy def rules return unless @user - can! :create_group if @user.can_create_group + can! :create_group if @user.can_create_group && !@user.auditor? can! :read_users_list end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 669b5e780e9f..a6be4ac2bd82 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -12,6 +12,7 @@ def rules can_read ||= globally_viewable can_read ||= member can_read ||= @user.admin? + can_read ||= @user.auditor? can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any? can! :read_group if can_read @@ -40,6 +41,7 @@ def rules def can_read_group? return true if @subject.public? return true if @user.admin? + return true if @user.auditor? return true if @subject.internal? && !@user.external? return true if @subject.users.include?(@user) diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb index 29bb357e00a8..8237eb7a510c 100644 --- a/app/policies/namespace_policy.rb +++ b/app/policies/namespace_policy.rb @@ -1,6 +1,7 @@ class NamespacePolicy < BasePolicy def rules return unless @user + return if @user.auditor? if @subject.owner == @user || @user.admin? can! :create_projects diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index e5ff006847c6..9e3d042f085b 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -8,6 +8,7 @@ def rules (project.group && project.group.has_owner?(user)) owner_access! if user.admin? || owner + auditor_access! if user.auditor? team_member_owner_access! if owner if project.public? || (project.internal? && !user.external?) @@ -182,6 +183,37 @@ def archived_access! cannot! :admin_merge_request end + # An auditor user has read-only access to all projects + def auditor_access! + can! :download_code + can! :download_wiki_code + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_issue + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_note + can! :read_cycle_analytics + can! :read_pipeline + can! :read_build + can! :read_commit_status + can! :read_build + can! :read_container_image + can! :read_pipeline + can! :read_environment + can! :read_deployment + can! :read_merge_request + can! :read_pages + can! :read_commit_status + can! :read_pipeline + can! :read_container_image + can! :read_merge_request + end + def disabled_features! repository_enabled = project.feature_available?(:repository, user) diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index 57acccfafd95..dd45309adb0d 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -3,12 +3,16 @@ def rules can! :read_project_snippet if @subject.public? return unless @user - if @user && @subject.author == @user || @user.admin? + if @user && (@subject.author == @user || @user.admin?) can! :read_project_snippet can! :update_project_snippet can! :admin_project_snippet end + if @user.auditor? + can! :read_project_snippet + end + if @subject.internal? && !@user.external? can! :read_project_snippet end diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index fbd2bff5bbb2..d5a539a14053 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -13,6 +13,8 @@ = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' .timeline-content.timeline-content-form = render "projects/notes/form", view: diff_view + - elsif current_user.present? && current_user.auditor? + -# Display nothing - else .disabled-comment.text-center .disabled-comment-text.inline -- GitLab From 9a3a4a5eed930d3d56fb3701bd687166ddbaabad Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 30 Jan 2017 13:02:48 +0530 Subject: [PATCH 03/26] Add specs covering the auditor user. All finders, policies and controllers that needed to be modified to include an `auditor` check are tested here --- .../admin/dashboard_controller_spec.rb | 26 ++++ spec/controllers/groups_controller_spec.rb | 44 ++++++ spec/controllers/projects_controller_spec.rb | 22 +++ spec/finders/group_projects_finder_spec.rb | 38 ++++++ spec/finders/issues_finder_spec.rb | 10 ++ spec/finders/snippets_finder_spec.rb | 12 ++ spec/models/project_feature_spec.rb | 9 ++ spec/models/user_spec.rb | 8 +- spec/policies/group_policy_spec.rb | 11 ++ spec/policies/namespace_policy_spec.rb | 60 +++++++++ spec/policies/project_policy_spec.rb | 22 +++ spec/policies/project_snippet_policy_spec.rb | 125 ++++++++++++++++++ 12 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 spec/controllers/admin/dashboard_controller_spec.rb create mode 100644 spec/policies/namespace_policy_spec.rb create mode 100644 spec/policies/project_snippet_policy_spec.rb diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/controllers/admin/dashboard_controller_spec.rb new file mode 100644 index 000000000000..29c545be43dd --- /dev/null +++ b/spec/controllers/admin/dashboard_controller_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Admin::DashboardController do + describe '#index' do + it "allows an admin user to access the page" do + sign_in(create(:user, :admin)) + get :index + + expect(response).to have_http_status(200) + end + + it "does not allow an auditor user to access the page" do + sign_in(create(:user, :auditor)) + get :index + + expect(response).to have_http_status(404) + end + + it "does not allow a regular user to access the page" do + sign_in(create(:user)) + get :index + + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index cad82a34fb0e..b61f0b906192 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -126,4 +126,48 @@ expect(assigns(:group).path).not_to eq('new_path') end end + + describe 'POST create' do + it 'allows creating a group' do + sign_in(user) + + expect do + post :create, group: { name: 'new_group', path: "new_group" } + end.to change { Group.count }.by(1) + + expect(response).to have_http_status(302) + end + + context 'authorization' do + it 'allows an admin to create a group' do + sign_in(create(:admin)) + + expect do + post :create, group: { name: 'new_group', path: "new_group" } + end.to change { Group.count }.by(1) + + expect(response).to have_http_status(302) + end + + it 'does not allow a user with "can_create_group" set to false to create a group' do + sign_in(create(:user, can_create_group: false)) + + expect do + post :create, group: { name: 'new_group', path: "new_group" } + end.not_to change { Group.count } + + expect(response).to have_http_status(404) + end + + it 'does not allow an auditor with "can_create_group" set to true to create a group' do + sign_in(create(:user, :auditor, can_create_group: true)) + + expect do + post :create, group: { name: 'new_group', path: "new_group" } + end.not_to change { Group.count } + + expect(response).to have_http_status(404) + end + end + end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 4378ae4950b4..2e1a0323a28d 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -425,4 +425,26 @@ expect(parsed_body["Commits"]).to include("123456") end end + + describe 'GET edit' do + it 'does not allow an auditor user to access the page' do + sign_in(create(:user, :auditor)) + + get :edit, + namespace_id: project.namespace.path, + id: project.path + + expect(response).to have_http_status(404) + end + + it 'allows an admin user to access the page' do + sign_in(create(:user, :admin)) + + get :edit, + namespace_id: project.namespace.path, + id: project.path + + expect(response).to have_http_status(200) + end + end end diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb index ef97b061ca7b..a59a404a1f52 100644 --- a/spec/finders/group_projects_finder_spec.rb +++ b/spec/finders/group_projects_finder_spec.rb @@ -76,6 +76,44 @@ end end + describe 'with an admin current user' do + let(:current_user) { create(:user, :admin) } + + context "only shared" do + subject { described_class.new(group, only_shared: true).execute(current_user) } + it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) } + end + + context "only owned" do + subject { described_class.new(group, only_owned: true).execute(current_user) } + it { is_expected.to eq([private_project, public_project]) } + end + + context "all" do + subject { described_class.new(group).execute(current_user) } + it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) } + end + end + + describe 'with an auditor current user' do + let(:current_user) { create(:user, :auditor) } + + context "only shared" do + subject { described_class.new(group, only_shared: true).execute(current_user) } + it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) } + end + + context "only owned" do + subject { described_class.new(group, only_owned: true).execute(current_user) } + it { is_expected.to eq([private_project, public_project]) } + end + + context "all" do + subject { described_class.new(group).execute(current_user) } + it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) } + end + end + describe "no user" do context "only shared" do subject { described_class.new(group, only_shared: true).execute(current_user) } diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index b19def2a3077..37eab1215658 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -258,6 +258,8 @@ describe '.not_restricted_by_confidentiality' do let(:authorized_user) { create(:user) } + let(:admin_user) { create(:user, :admin) } + let(:auditor_user) { create(:user, :auditor) } let(:project) { create(:empty_project, namespace: authorized_user.namespace) } let!(:public_issue) { create(:issue, project: project) } let!(:confidential_issue) { create(:issue, project: project, confidential: true) } @@ -273,5 +275,13 @@ it 'returns all issues for user authorized for the issues projects' do expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) end + + it 'returns all issues for an admin user' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, admin_user)).to include(public_issue, confidential_issue) + end + + it 'returns all issues for an auditor user' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, auditor_user)).to include(public_issue, confidential_issue) + end end end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 975e99c58072..a969434eecd4 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -127,5 +127,17 @@ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") expect(snippets).to include(@snippet1) end + + it "returns all snippets for admin users" do + user = create(:user, :admin) + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + expect(snippets).to include(@snippet1, @snippet2, @snippet3) + end + + it "returns all snippets for auditor users" do + user = create(:user, :auditor) + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + expect(snippets).to include(@snippet1, @snippet2, @snippet3) + end end end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 8589f1eb712d..2a188a8e0e57 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -52,6 +52,15 @@ expect(project.feature_available?(:issues, user)).to eq(true) end end + + it "returns true if user is an auditor" do + user.update_attribute(:auditor, true) + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end end context 'when feature is enabled for everyone' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 929200469e15..97a5fafac876 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -198,6 +198,12 @@ end end end + + it 'does not allow a user to be both an auditor and an admin' do + user = build(:user, :admin, :auditor) + + expect(user).to be_invalid + end end describe "non_ldap" do @@ -1415,7 +1421,7 @@ def add_user(access) it 'returns the projects when using an ActiveRecord relation' do projects = user. - projects_with_reporter_access_limited_to(Project.select(:id)) + projects_with_reporter_access_limited_to(Project.select(:id)) expect(projects).to eq([project1]) end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 5c34ff041529..887daa484b90 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -6,6 +6,7 @@ let(:developer) { create(:user) } let(:master) { create(:user) } let(:owner) { create(:user) } + let(:auditor) { create(:user, :auditor) } let(:admin) { create(:admin) } let(:group) { create(:group) } @@ -170,5 +171,15 @@ is_expected.to include(*owner_permissions) end end + + context 'auditor' do + let(:current_user) { auditor } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end end end diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb new file mode 100644 index 000000000000..b951dac2e833 --- /dev/null +++ b/spec/policies/namespace_policy_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe NamespacePolicy, models: true do + let(:user) { create(:user) } + let(:owner) { create(:user) } + let(:auditor) { create(:user, :auditor) } + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, owner: owner) } + + let(:owner_permissions) do + [ + :create_projects, + :admin_namespace + ] + end + + let(:admin_permissions) { owner_permissions } + + subject { described_class.abilities(current_user, namespace).to_set } + + context 'with no user' do + let(:current_user) { nil } + + it do + is_expected.to be_empty + end + end + + context 'regular user' do + let(:current_user) { user } + + it do + is_expected.to be_empty + end + end + + context 'owner' do + let(:current_user) { owner } + + it do + is_expected.to include(*owner_permissions) + end + end + + context 'auditor' do + let(:current_user) { auditor } + + it do + is_expected.to be_empty + end + end + + context 'admin' do + let(:current_user) { admin } + + it do + is_expected.to include(*owner_permissions) + end + end +end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index eeab9827d994..d477e6771231 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -6,6 +6,7 @@ let(:dev) { create(:user) } let(:master) { create(:user) } let(:owner) { create(:user) } + let(:auditor) { create(:user, :auditor) } let(:admin) { create(:admin) } let(:project) { create(:empty_project, :public, namespace: owner.namespace) } @@ -68,6 +69,16 @@ ] end + let(:auditor_permissions) do + [ + :download_code, :download_wiki_code, :read_project, :read_board, :read_list, + :read_wiki, :read_issue, :read_label, :read_milestone, :read_project_snippet, + :read_project_member, :read_note, :read_cycle_analytics, :read_pipeline, + :read_build, :read_commit_status, :read_container_image, :read_environment, + :read_deployment, :read_merge_request, :read_pages + ] + end + before do project.team << [guest, :guest] project.team << [master, :master] @@ -207,5 +218,16 @@ is_expected.to include(*owner_permissions) end end + + context 'auditor' do + let(:current_user) { auditor } + + it do + is_expected.not_to include(*developer_permissions) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + is_expected.to include(*auditor_permissions) + end + end end end diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb new file mode 100644 index 000000000000..6bbe89cf934a --- /dev/null +++ b/spec/policies/project_snippet_policy_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +describe ProjectSnippetPolicy, models: true do + let(:author_permissions) do + [ + :update_project_snippet, + :admin_project_snippet + ] + end + + subject { described_class.abilities(current_user, project_snippet).to_set } + + context 'public snippet' do + let(:project_snippet) { create(:project_snippet, :public) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + let(:current_user) { create(:user) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + end + + context 'internal snippet' do + let(:project_snippet) { create(:project_snippet, :internal) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + let(:current_user) { create(:user) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'external user' do + let(:current_user) { create(:user, :external) } + + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + end + + context 'private snippet' do + let(:project_snippet) { create(:project_snippet, :private) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + let(:current_user) { create(:user) } + + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'snippet author' do + let(:current_user) { create(:user) } + let(:project_snippet) { create(:project_snippet, :private, author: current_user) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.to include(*author_permissions) + end + end + + context 'project team member' do + let(:current_user) { create(:user) } + before { project_snippet.project.team << [current_user, :developer] } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'auditor user' do + let(:current_user) { create(:user, :auditor) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'admin user' do + let(:current_user) { create(:user, :admin) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.to include(*author_permissions) + end + end + end +end -- GitLab From c0136d620585515bb11d69670dbc6f370188519c Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 30 Jan 2017 15:45:41 +0530 Subject: [PATCH 04/26] Refactor project policy by removing duplicate declarations for readonly users. Don't repeat declarations that are common between anonymous and auditor users. --- app/policies/project_policy.rb | 72 ++++++++++++++-------------------- 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 9e3d042f085b..d3c25dd9001e 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -185,33 +185,12 @@ def archived_access! # An auditor user has read-only access to all projects def auditor_access! - can! :download_code - can! :download_wiki_code - can! :read_project - can! :read_board - can! :read_list - can! :read_wiki - can! :read_issue - can! :read_label - can! :read_milestone - can! :read_project_snippet - can! :read_project_member - can! :read_note - can! :read_cycle_analytics - can! :read_pipeline - can! :read_build - can! :read_commit_status + base_readonly_access! + can! :read_build - can! :read_container_image - can! :read_pipeline can! :read_environment can! :read_deployment - can! :read_merge_request can! :read_pages - can! :read_commit_status - can! :read_pipeline - can! :read_container_image - can! :read_merge_request end def disabled_features! @@ -260,25 +239,7 @@ def disabled_features! def anonymous_rules return unless project.public? - can! :read_project - can! :read_board - can! :read_list - can! :read_wiki - can! :read_label - can! :read_milestone - can! :read_project_snippet - can! :read_project_member - can! :read_merge_request - can! :read_note - can! :read_pipeline - can! :read_commit_status - can! :read_container_image - can! :download_code - can! :download_wiki_code - can! :read_cycle_analytics - - # NOTE: may be overridden by IssuePolicy - can! :read_issue + base_readonly_access! # Allow to read builds by anonymous user if guests are allowed can! :read_build if project.public_builds? @@ -311,4 +272,31 @@ def named_abilities(name) :"admin_#{name}" ] end + + private + + # A base set of abilities for read-only users, which + # is then augmented as necessary for anonymous and auditor + # users. + def base_readonly_access! + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :read_pipeline + can! :read_commit_status + can! :read_container_image + can! :download_code + can! :download_wiki_code + can! :read_cycle_analytics + + # NOTE: may be overridden by IssuePolicy + can! :read_issue + end end -- GitLab From b7784a2a606deba439a53c43887cb43ca2ace1ec Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 31 Jan 2017 13:39:47 +0530 Subject: [PATCH 05/26] Implement review comments from @jameslopez 1. Extract an `admin_or_auditor?` method to clean up multiple uses of `user.admin? || user.auditor?` 2. Follow the four phase test rule. 3. Clean up the `project_policy_spec` by using %i for literal symbols 4. A number of other minor improvements. --- app/finders/group_projects_finder.rb | 2 +- app/finders/issues_finder.rb | 2 +- app/finders/snippets_finder.rb | 2 +- app/models/project_feature.rb | 2 +- app/models/user.rb | 4 ++ spec/finders/group_projects_finder_spec.rb | 2 +- spec/finders/issues_finder_spec.rb | 2 +- spec/finders/snippets_finder_spec.rb | 20 ++++++ spec/policies/group_policy_spec.rb | 1 + spec/policies/namespace_policy_spec.rb | 27 ++----- spec/policies/project_policy_spec.rb | 74 ++++++++++---------- spec/policies/project_snippet_policy_spec.rb | 12 +--- 12 files changed, 76 insertions(+), 74 deletions(-) diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index 4607269813b9..63445211d7bd 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -18,7 +18,7 @@ def group_projects(current_user) projects = [] if current_user - if @group.users.include?(current_user) || current_user.admin? || current_user.auditor? + if @group.users.include?(current_user) || current_user.admin_or_auditor? projects << @group.projects unless only_shared projects << @group.shared_projects unless only_owned else diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index c320fdfe260f..752c7bf59dd3 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -33,7 +33,7 @@ def iid_pattern def self.not_restricted_by_confidentiality(user) return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? - return Issue.all if user.admin? || user.auditor? + return Issue.all if user.admin_or_auditor? Issue.where(' issues.confidential IS NULL diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index cebcbe606648..a292d5b537d5 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -44,7 +44,7 @@ def by_project(current_user, project, scope) snippets = project.snippets.fresh if current_user - include_private = project.team.member?(current_user) || current_user.admin? || current_user.auditor? + include_private = project.team.member?(current_user) || current_user.admin_or_auditor? by_scope(snippets, scope, include_private) else snippets.are_public diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 70bb505b6381..e0673098e37e 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -83,7 +83,7 @@ def get_permission(user, level) when DISABLED false when PRIVATE - user && (project.team.member?(user) || user.admin? || user.auditor?) + user && (project.team.member?(user) || user.admin_or_auditor?) when ENABLED true else diff --git a/app/models/user.rb b/app/models/user.rb index eb60f040c82d..e512800a573c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -538,6 +538,10 @@ def is_admin? admin end + def admin_or_auditor? + admin? || auditor? + end + def require_ssh_key? keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') end diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb index a59a404a1f52..4290adff94fc 100644 --- a/spec/finders/group_projects_finder_spec.rb +++ b/spec/finders/group_projects_finder_spec.rb @@ -77,7 +77,7 @@ end describe 'with an admin current user' do - let(:current_user) { create(:user, :admin) } + let(:current_user) { create(:admin) } context "only shared" do subject { described_class.new(group, only_shared: true).execute(current_user) } diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 37eab1215658..8e9c4c8077cf 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -258,7 +258,7 @@ describe '.not_restricted_by_confidentiality' do let(:authorized_user) { create(:user) } - let(:admin_user) { create(:user, :admin) } + let(:admin_user) { create(:admin) } let(:auditor_user) { create(:user, :auditor) } let(:project) { create(:empty_project, namespace: authorized_user.namespace) } let!(:public_issue) { create(:issue, project: project) } diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index a969434eecd4..94eba72f78ff 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -15,12 +15,14 @@ it "returns all private and internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :all) + expect(snippets).to include(snippet2, snippet3) expect(snippets).not_to include(snippet1) end it "returns all public snippets" do snippets = SnippetsFinder.new.execute(nil, filter: :all) + expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet1, snippet2) end @@ -46,6 +48,7 @@ it "returns all public and internal snippets" do snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) + expect(snippets).to include(snippet2, snippet3) expect(snippets).not_to include(snippet1) end @@ -58,23 +61,27 @@ it "returns private snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") + expect(snippets).to include(snippet1) expect(snippets).not_to include(snippet2, snippet3) end it "returns public snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") + expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet1, snippet2) end it "returns all snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) + expect(snippets).to include(snippet1, snippet2, snippet3) end it "returns only public snippets if unauthenticated user" do snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) + expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet2, snippet1) end @@ -89,54 +96,67 @@ it "returns public snippets for unauthorized user" do snippets = SnippetsFinder.new.execute(nil, filter: :by_project, project: project1) + expect(snippets).to include(@snippet3) expect(snippets).not_to include(@snippet1, @snippet2) end it "returns public and internal snippets for non project members" do snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + expect(snippets).to include(@snippet2, @snippet3) expect(snippets).not_to include(@snippet1) end it "returns public snippets for non project members" do snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public") + expect(snippets).to include(@snippet3) expect(snippets).not_to include(@snippet1, @snippet2) end it "returns internal snippets for non project members" do snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal") + expect(snippets).to include(@snippet2) expect(snippets).not_to include(@snippet1, @snippet3) end it "does not return private snippets for non project members" do snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + expect(snippets).not_to include(@snippet1, @snippet2, @snippet3) end it "returns all snippets for project members" do project1.team << [user, :developer] + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + expect(snippets).to include(@snippet1, @snippet2, @snippet3) end it "returns private snippets for project members" do project1.team << [user, :developer] + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + expect(snippets).to include(@snippet1) end it "returns all snippets for admin users" do user = create(:user, :admin) + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + expect(snippets).to include(@snippet1, @snippet2, @snippet3) end it "returns all snippets for auditor users" do user = create(:user, :auditor) + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + expect(snippets).to include(@snippet1, @snippet2, @snippet3) end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 887daa484b90..4f28ec22a62e 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -177,6 +177,7 @@ it do is_expected.to include(:read_group) + is_expected.to all(start_with("read")) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb index b951dac2e833..a0f7c66d87ba 100644 --- a/spec/policies/namespace_policy_spec.rb +++ b/spec/policies/namespace_policy_spec.rb @@ -7,12 +7,7 @@ let(:admin) { create(:admin) } let(:namespace) { create(:namespace, owner: owner) } - let(:owner_permissions) do - [ - :create_projects, - :admin_namespace - ] - end + let(:owner_permissions) { [:create_projects, :admin_namespace] } let(:admin_permissions) { owner_permissions } @@ -21,40 +16,30 @@ context 'with no user' do let(:current_user) { nil } - it do - is_expected.to be_empty - end + it { is_expected.to be_empty } end context 'regular user' do let(:current_user) { user } - it do - is_expected.to be_empty - end + it { is_expected.to be_empty } end context 'owner' do let(:current_user) { owner } - it do - is_expected.to include(*owner_permissions) - end + it { is_expected.to include(*owner_permissions) } end context 'auditor' do let(:current_user) { auditor } - it do - is_expected.to be_empty - end + it { is_expected.to be_empty } end context 'admin' do let(:current_user) { admin } - it do - is_expected.to include(*owner_permissions) - end + it { is_expected.to include(*owner_permissions) } end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index d477e6771231..0c82568dcdf2 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -11,71 +11,69 @@ let(:project) { create(:empty_project, :public, namespace: owner.namespace) } let(:guest_permissions) do - [ - :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label, - :read_milestone, :read_project_snippet, :read_project_member, - :read_note, :create_project, :create_issue, :create_note, - :upload_file + %i[ + read_project read_board read_list read_wiki read_issue read_label + read_milestone read_project_snippet read_project_member + read_note create_project create_issue create_note + upload_file ] end let(:reporter_permissions) do - [ - :download_code, :fork_project, :create_project_snippet, :update_issue, - :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build, - :read_container_image, :read_pipeline, :read_environment, :read_deployment, - :read_merge_request, :download_wiki_code + %i[ + download_code fork_project create_project_snippet update_issue + admin_issue admin_label admin_list read_commit_status read_build + read_container_image read_pipeline read_environment read_deployment + read_merge_request download_wiki_code ] end let(:team_member_reporter_permissions) do - [ - :build_download_code, :build_read_container_image - ] + %i[build_download_code build_read_container_image] end let(:developer_permissions) do - [ - :admin_merge_request, :update_merge_request, :create_commit_status, - :update_commit_status, :create_build, :update_build, :create_pipeline, - :update_pipeline, :create_merge_request, :create_wiki, :push_code, - :resolve_note, :create_container_image, :update_container_image, - :create_environment, :create_deployment + %i[ + admin_merge_request update_merge_request create_commit_status + update_commit_status create_build update_build create_pipeline + update_pipeline create_merge_request create_wiki push_code + resolve_note create_container_image update_container_image + create_environment create_deployment ] end let(:master_permissions) do - [ - :push_code_to_protected_branches, :update_project_snippet, :update_environment, - :update_deployment, :admin_milestone, :admin_project_snippet, - :admin_project_member, :admin_note, :admin_wiki, :admin_project, - :admin_commit_status, :admin_build, :admin_container_image, - :admin_pipeline, :admin_environment, :admin_deployment + %i[ + push_code_to_protected_branches update_project_snippet update_environment + update_deployment admin_milestone admin_project_snippet + admin_project_member admin_note admin_wiki admin_project + admin_commit_status admin_build admin_container_image + admin_pipeline admin_environment admin_deployment ] end let(:public_permissions) do - [ - :download_code, :fork_project, :read_commit_status, :read_pipeline, - :read_container_image, :build_download_code, :build_read_container_image, - :download_wiki_code + %i[ + download_code fork_project read_commit_status read_pipeline + read_container_image build_download_code build_read_container_image + download_wiki_code ] end let(:owner_permissions) do - [ - :change_namespace, :change_visibility_level, :rename_project, :remove_project, - :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue + %i[ + change_namespace change_visibility_level rename_project remove_project + archive_project remove_fork_project destroy_merge_request destroy_issue ] end let(:auditor_permissions) do - [ - :download_code, :download_wiki_code, :read_project, :read_board, :read_list, - :read_wiki, :read_issue, :read_label, :read_milestone, :read_project_snippet, - :read_project_member, :read_note, :read_cycle_analytics, :read_pipeline, - :read_build, :read_commit_status, :read_container_image, :read_environment, - :read_deployment, :read_merge_request, :read_pages + %i[ + download_code download_wiki_code read_project read_board read_list + read_wiki read_issue read_label read_milestone read_project_snippet + read_project_member read_note read_cycle_analytics read_pipeline + read_build read_commit_status read_container_image read_environment + read_deployment read_merge_request read_pages ] end diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index 6bbe89cf934a..7b7c622ac31b 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ProjectSnippetPolicy, models: true do + let(:current_user) { create(:user) } + let(:author_permissions) do [ :update_project_snippet, @@ -23,8 +25,6 @@ end context 'regular user' do - let(:current_user) { create(:user) } - it do is_expected.to include(:read_project_snippet) is_expected.not_to include(*author_permissions) @@ -45,8 +45,6 @@ end context 'regular user' do - let(:current_user) { create(:user) } - it do is_expected.to include(:read_project_snippet) is_expected.not_to include(*author_permissions) @@ -76,8 +74,6 @@ end context 'regular user' do - let(:current_user) { create(:user) } - it do is_expected.not_to include(:read_project_snippet) is_expected.not_to include(*author_permissions) @@ -85,7 +81,6 @@ end context 'snippet author' do - let(:current_user) { create(:user) } let(:project_snippet) { create(:project_snippet, :private, author: current_user) } it do @@ -95,7 +90,6 @@ end context 'project team member' do - let(:current_user) { create(:user) } before { project_snippet.project.team << [current_user, :developer] } it do @@ -114,7 +108,7 @@ end context 'admin user' do - let(:current_user) { create(:user, :admin) } + let(:current_user) { create(:admin) } it do is_expected.to include(:read_project_snippet) -- GitLab From e04bd02388010c8999eef729c5f09c6ba578ae5b Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 31 Jan 2017 13:46:47 +0530 Subject: [PATCH 06/26] Add `project_policy` to `flayignore` The `ProjectPolicy` is meant to be declarative in nature, and it isn't necessarily a good idea to remove all duplication here. That level of indirection would _hurt_ readability, rather than improve it. --- .flayignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.flayignore b/.flayignore index 0689ddc1eb8b..80682c5b237e 100644 --- a/.flayignore +++ b/.flayignore @@ -3,3 +3,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb lib/gitlab/diff/position_tracer.rb app/controllers/projects/approver_groups_controller.rb app/controllers/projects/approvers_controller.rb +app/policies/project_policy.rb \ No newline at end of file -- GitLab From ea629571d76ef988d488da5338f0daf02a827153 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 2 Feb 2017 15:01:12 +0530 Subject: [PATCH 07/26] Add CHANGELOG --- changelogs/unreleased-ee/1439-read-only-user.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased-ee/1439-read-only-user.yml diff --git a/changelogs/unreleased-ee/1439-read-only-user.yml b/changelogs/unreleased-ee/1439-read-only-user.yml new file mode 100644 index 000000000000..b80db5272f0b --- /dev/null +++ b/changelogs/unreleased-ee/1439-read-only-user.yml @@ -0,0 +1,4 @@ +--- +title: Read-only "auditor" user role +merge_request: 998 +author: -- GitLab From 2bacfd48f8343963de27ca608c3eaabc2875b67a Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 2 Feb 2017 15:15:44 +0530 Subject: [PATCH 08/26] Add documentation around auditor users. --- doc/README.md | 1 + doc/administration/auditor_users.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 doc/administration/auditor_users.md diff --git a/doc/README.md b/doc/README.md index 6991c4ca3f51..e26ddb13cc1c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -77,6 +77,7 @@ - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability. - [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab. - [Repository restrictions](user/admin_area/settings/account_and_limit_settings.md#repository-size-limit) Define size restrictions for your repositories to limit the space they occupy in your storage device. Includes LFS objects. +- [Auditor users](administration/auditor_users.md) Create auditor users, with read-only access to the entire system. ## Contributor documentation diff --git a/doc/administration/auditor_users.md b/doc/administration/auditor_users.md new file mode 100644 index 000000000000..6757f2ac06a1 --- /dev/null +++ b/doc/administration/auditor_users.md @@ -0,0 +1,20 @@ +# Auditor Users + +>**Note:** [Introduced][998] in GitLab 8.16. + +With Gitlab Enterprise Edition Premium, you can create *auditor* users, who +are given read-only access to all projects, groups, and other resources on the +GitLab instance. + +First and foremost, an auditor user can perform all the actions that a regular user can. +In projects that the auditor user owns, or has been added to, they can be added to +groups, mentioned in comments, or have issues assigned to them. The one exception is +that auditor users cannot _create_ projects or groups. + +In addition, the auditor will be granted read-only access to all other projects/groups/etc. +on the GitLab instance. + +The `auditor` role is _not_ a read-only version of the `admin` role. The auditor will not be +able to access the project settings pages, or the Admin Area. + +[998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998 -- GitLab From ac299a0ecf94bff9e87e6a98bdc59a34e8b11c73 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 2 Feb 2017 18:54:54 +0530 Subject: [PATCH 09/26] Allow auditor users to create groups and projects. 1. Projects under the groups they belong to, or under their own personal namespace. They cannot create projects under groups they don't have explicit control over. 2. Since we're thinking of auditor users as "regular users with readonly access to everything they wouldn't normally see", it makes sense to let them do anything a regular user would do, including creating projects and groups. --- app/policies/global_policy.rb | 2 +- app/policies/namespace_policy.rb | 1 - spec/controllers/groups_controller_spec.rb | 6 +++--- spec/policies/namespace_policy_spec.rb | 10 +++++++++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index e156bb76c13b..3c2fbe6b56ba 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -2,7 +2,7 @@ class GlobalPolicy < BasePolicy def rules return unless @user - can! :create_group if @user.can_create_group && !@user.auditor? + can! :create_group if @user.can_create_group can! :read_users_list end end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb index 8237eb7a510c..29bb357e00a8 100644 --- a/app/policies/namespace_policy.rb +++ b/app/policies/namespace_policy.rb @@ -1,7 +1,6 @@ class NamespacePolicy < BasePolicy def rules return unless @user - return if @user.auditor? if @subject.owner == @user || @user.admin? can! :create_projects diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index b61f0b906192..418205e5db7c 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -159,14 +159,14 @@ expect(response).to have_http_status(404) end - it 'does not allow an auditor with "can_create_group" set to true to create a group' do + it 'allows an auditor with "can_create_group" set to true to create a group' do sign_in(create(:user, :auditor, can_create_group: true)) expect do post :create, group: { name: 'new_group', path: "new_group" } - end.not_to change { Group.count } + end.to change { Group.count }.by(1) - expect(response).to have_http_status(404) + expect(response).to have_http_status(302) end end end diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb index a0f7c66d87ba..b7a9f05a7d6a 100644 --- a/spec/policies/namespace_policy_spec.rb +++ b/spec/policies/namespace_policy_spec.rb @@ -34,7 +34,15 @@ context 'auditor' do let(:current_user) { auditor } - it { is_expected.to be_empty } + context 'owner' do + let(:namespace) { create(:namespace, owner: auditor) } + + it { is_expected.to include(*owner_permissions) } + end + + context 'non-owner' do + it { is_expected.to be_empty } + end end context 'admin' do -- GitLab From 6c0c3d9f33ee6e29bb4c6e7c35b78d7f72e37c61 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 3 Feb 2017 10:12:36 +0530 Subject: [PATCH 10/26] Implement review comments from maintainer (@DouweM) 1. `add_column_with_default` needs a `down` block 2. Add specs for the auditor user to `spec/features/security`. This directory contains a series of feature specs to test the access various user roles have to various project/admin pages, which is the perfect place to test auditor user access. 3. Other minor changes (views, typos) --- .../projects/notes/_notes_with_form.html.haml | 4 +-- ...70120123345_add_column_auditor_to_users.rb | 6 +++- doc/administration/auditor_users.md | 2 +- spec/features/security/admin_access_spec.rb | 3 ++ .../security/dashboard_access_spec.rb | 9 ++++++ .../security/group/internal_access_spec.rb | 5 ++++ .../security/group/private_access_spec.rb | 5 ++++ .../security/group/public_access_spec.rb | 5 ++++ spec/features/security/profile_access_spec.rb | 6 ++++ .../security/project/internal_access_spec.rb | 30 +++++++++++++++++++ .../security/project/private_access_spec.rb | 26 ++++++++++++++++ .../security/project/public_access_spec.rb | 30 +++++++++++++++++++ .../project/snippet/internal_access_spec.rb | 6 ++++ .../project/snippet/private_access_spec.rb | 4 +++ .../project/snippet/public_access_spec.rb | 8 +++++ spec/support/matchers/access_matchers.rb | 2 ++ 16 files changed, 146 insertions(+), 5 deletions(-) diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index d5a539a14053..08c73d94a099 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -13,9 +13,7 @@ = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' .timeline-content.timeline-content-form = render "projects/notes/form", view: diff_view - - elsif current_user.present? && current_user.auditor? - -# Display nothing - - else + - elsif !current_user .disabled-comment.text-center .disabled-comment-text.inline Please diff --git a/db/migrate/20170120123345_add_column_auditor_to_users.rb b/db/migrate/20170120123345_add_column_auditor_to_users.rb index e119506b2376..11e2b6160f24 100644 --- a/db/migrate/20170120123345_add_column_auditor_to_users.rb +++ b/db/migrate/20170120123345_add_column_auditor_to_users.rb @@ -8,7 +8,11 @@ class AddColumnAuditorToUsers < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_column_with_default :users, :auditor, :boolean, default: false, allow_null: false end + + def down + remove_column :users, :auditor + end end diff --git a/doc/administration/auditor_users.md b/doc/administration/auditor_users.md index 6757f2ac06a1..ff2aae301f9b 100644 --- a/doc/administration/auditor_users.md +++ b/doc/administration/auditor_users.md @@ -1,6 +1,6 @@ # Auditor Users ->**Note:** [Introduced][998] in GitLab 8.16. +>**Note:** [Introduced][998] in GitLab 8.17. With Gitlab Enterprise Edition Premium, you can create *auditor* users, who are given read-only access to all projects, groups, and other resources on the diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb index e180ca53eb51..52a63a9ebecc 100644 --- a/spec/features/security/admin_access_spec.rb +++ b/spec/features/security/admin_access_spec.rb @@ -9,6 +9,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for :user } it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_denied_for :auditor } end describe "GET /admin/users" do @@ -17,6 +18,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for :user } it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_denied_for :auditor } end describe "GET /admin/hooks" do @@ -25,5 +27,6 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for :user } it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_denied_for :auditor } end end diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index 40f773956d19..8f5e2d7cb8a9 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -8,6 +8,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -16,6 +17,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -24,6 +26,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -32,6 +35,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -40,6 +44,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_allowed_for :visitor } end @@ -53,6 +58,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end end @@ -60,12 +66,14 @@ describe "GET /projects/new" do it { expect(new_project_path).to be_allowed_for :admin } it { expect(new_project_path).to be_allowed_for :user } + it { expect(new_project_path).to be_allowed_for :auditor } it { expect(new_project_path).to be_denied_for :visitor } end describe "GET /groups/new" do it { expect(new_group_path).to be_allowed_for :admin } it { expect(new_group_path).to be_allowed_for :user } + it { expect(new_group_path).to be_allowed_for :auditor } it { expect(new_group_path).to be_denied_for :visitor } end @@ -74,6 +82,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb index 87cce32d6c63..1036f65b37f2 100644 --- a/spec/features/security/group/internal_access_spec.rb +++ b/spec/features/security/group/internal_access_spec.rb @@ -22,6 +22,7 @@ subject { group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -37,6 +38,7 @@ subject { issues_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -52,6 +54,7 @@ subject { merge_requests_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -67,6 +70,7 @@ subject { group_group_members_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -82,6 +86,7 @@ subject { edit_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_denied_for(:master).of(group) } it { is_expected.to be_denied_for(:developer).of(group) } diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb index 1d6b3e77c221..52a84278e8c5 100644 --- a/spec/features/security/group/private_access_spec.rb +++ b/spec/features/security/group/private_access_spec.rb @@ -22,6 +22,7 @@ subject { group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -37,6 +38,7 @@ subject { issues_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -52,6 +54,7 @@ subject { merge_requests_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -67,6 +70,7 @@ subject { group_group_members_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -82,6 +86,7 @@ subject { edit_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_denied_for(:master).of(group) } it { is_expected.to be_denied_for(:developer).of(group) } diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb index d7d761772692..5532d9790d1a 100644 --- a/spec/features/security/group/public_access_spec.rb +++ b/spec/features/security/group/public_access_spec.rb @@ -22,6 +22,7 @@ subject { group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -37,6 +38,7 @@ subject { issues_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -52,6 +54,7 @@ subject { merge_requests_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -67,6 +70,7 @@ subject { group_group_members_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:master).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -82,6 +86,7 @@ subject { edit_group_path(group) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_denied_for(:master).of(group) } it { is_expected.to be_denied_for(:developer).of(group) } diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb index c19678ab3813..86fc58d93066 100644 --- a/spec/features/security/profile_access_spec.rb +++ b/spec/features/security/profile_access_spec.rb @@ -8,6 +8,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -16,6 +17,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -24,6 +26,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -32,6 +35,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -40,6 +44,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end @@ -48,6 +53,7 @@ it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :auditor } it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 5c3fe62379fa..b601aa03acb1 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -16,6 +16,7 @@ subject { namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -30,6 +31,7 @@ subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -44,6 +46,7 @@ subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -58,6 +61,7 @@ subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -72,6 +76,7 @@ subject { namespace_project_compare_index_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -86,6 +91,7 @@ subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -101,6 +107,7 @@ subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -115,6 +122,7 @@ subject { edit_namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -129,6 +137,7 @@ subject { namespace_project_deploy_keys_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -143,6 +152,7 @@ subject { namespace_project_issues_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -158,6 +168,7 @@ subject { edit_namespace_project_issue_path(project.namespace, project, issue) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -172,6 +183,7 @@ subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -186,6 +198,7 @@ subject { new_namespace_project_snippet_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -200,6 +213,7 @@ subject { namespace_project_merge_requests_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -214,6 +228,7 @@ subject { new_namespace_project_merge_request_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -233,6 +248,7 @@ end it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -252,6 +268,7 @@ end it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -266,6 +283,7 @@ subject { namespace_project_settings_integrations_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -280,6 +298,7 @@ subject { namespace_project_pipelines_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -295,6 +314,7 @@ subject { namespace_project_pipeline_path(project.namespace, project, pipeline) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -312,6 +332,7 @@ before { project.update(public_builds: true) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -326,6 +347,7 @@ before { project.update(public_builds: false) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -346,6 +368,7 @@ before { project.update(public_builds: true) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -360,6 +383,7 @@ before { project.update(public_builds: false) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -375,6 +399,7 @@ subject { namespace_project_environments_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -390,6 +415,7 @@ subject { namespace_project_environment_path(project.namespace, project, environment) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -404,6 +430,7 @@ subject { new_namespace_project_environment_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -427,6 +454,7 @@ it { is_expected.to be_denied_for(:admin) } it { is_expected.to be_denied_for(:guest).of(project) } it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_denied_for(:visitor) } end @@ -438,6 +466,7 @@ it { is_expected.to be_denied_for(:admin) } it { is_expected.to be_denied_for(:guest).of(project) } it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_denied_for(:visitor) } end end @@ -451,6 +480,7 @@ subject { namespace_project_container_registry_index_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 828d7bc496e6..e42ea69ebd22 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -16,6 +16,7 @@ subject { namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -30,6 +31,7 @@ subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -44,6 +46,7 @@ subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -58,6 +61,7 @@ subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -72,6 +76,7 @@ subject { namespace_project_compare_index_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -86,6 +91,7 @@ subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -101,6 +107,7 @@ subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))} it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -115,6 +122,7 @@ subject { edit_namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -129,6 +137,7 @@ subject { namespace_project_deploy_keys_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -143,6 +152,7 @@ subject { namespace_project_issues_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -158,6 +168,7 @@ subject { edit_namespace_project_issue_path(project.namespace, project, issue) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -172,6 +183,7 @@ subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -186,6 +198,7 @@ subject { namespace_project_merge_requests_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -205,6 +218,7 @@ end it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -224,6 +238,7 @@ end it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -238,6 +253,7 @@ subject { namespace_project_settings_integrations_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -252,6 +268,7 @@ subject { namespace_project_pipelines_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -279,6 +296,7 @@ subject { namespace_project_pipeline_path(project.namespace, project, pipeline) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -305,6 +323,7 @@ subject { namespace_project_builds_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -333,6 +352,7 @@ subject { namespace_project_build_path(project.namespace, project, build.id) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -364,6 +384,7 @@ subject { namespace_project_environments_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -379,6 +400,7 @@ subject { namespace_project_environment_path(project.namespace, project, environment) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -393,6 +415,7 @@ subject { new_namespace_project_environment_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -416,6 +439,7 @@ it { is_expected.to be_denied_for(:admin) } it { is_expected.to be_denied_for(:guest).of(project) } it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_denied_for(:visitor) } end @@ -427,6 +451,7 @@ it { is_expected.to be_denied_for(:admin) } it { is_expected.to be_denied_for(:guest).of(project) } it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_denied_for(:visitor) } end end @@ -440,6 +465,7 @@ subject { namespace_project_container_registry_index_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 0a5ed2aa36db..f121ba36a43c 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -16,6 +16,7 @@ subject { namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -30,6 +31,7 @@ subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -44,6 +46,7 @@ subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -58,6 +61,7 @@ subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -72,6 +76,7 @@ subject { namespace_project_compare_index_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -86,6 +91,7 @@ subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -100,6 +106,7 @@ subject { namespace_project_pipelines_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -115,6 +122,7 @@ subject { namespace_project_pipeline_path(project.namespace, project, pipeline) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -132,6 +140,7 @@ before { project.update(public_builds: true) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -146,6 +155,7 @@ before { project.update(public_builds: false) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -166,6 +176,7 @@ before { project.update(public_builds: true) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -180,6 +191,7 @@ before { project.update(public_builds: false) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -195,6 +207,7 @@ subject { namespace_project_environments_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -210,6 +223,7 @@ subject { namespace_project_environment_path(project.namespace, project, environment) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -224,6 +238,7 @@ subject { new_namespace_project_environment_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -240,6 +255,7 @@ subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -253,6 +269,7 @@ subject { edit_namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -267,6 +284,7 @@ subject { namespace_project_deploy_keys_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -281,6 +299,7 @@ subject { namespace_project_issues_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -296,6 +315,7 @@ subject { edit_namespace_project_issue_path(project.namespace, project, issue) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -310,6 +330,7 @@ subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -324,6 +345,7 @@ subject { new_namespace_project_snippet_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -338,6 +360,7 @@ subject { namespace_project_merge_requests_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -352,6 +375,7 @@ subject { new_namespace_project_merge_request_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -371,6 +395,7 @@ end it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -390,6 +415,7 @@ end it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -404,6 +430,7 @@ subject { namespace_project_settings_integrations_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_denied_for(:developer).of(project) } @@ -427,6 +454,7 @@ it { is_expected.to be_denied_for(:admin) } it { is_expected.to be_denied_for(:guest).of(project) } it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_denied_for(:visitor) } end @@ -438,6 +466,7 @@ it { is_expected.to be_denied_for(:admin) } it { is_expected.to be_denied_for(:guest).of(project) } it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_denied_for(:visitor) } end end @@ -451,6 +480,7 @@ subject { namespace_project_container_registry_index_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb index 2659b3ee3ec3..26dd7e0c0b47 100644 --- a/spec/features/security/project/snippet/internal_access_spec.rb +++ b/spec/features/security/project/snippet/internal_access_spec.rb @@ -12,6 +12,7 @@ subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -26,6 +27,7 @@ subject { new_namespace_project_snippet_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -41,6 +43,7 @@ subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -55,6 +58,7 @@ subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -71,6 +75,7 @@ subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -85,6 +90,7 @@ subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb index 6eb9f163bd5b..bb7ffcccc2be 100644 --- a/spec/features/security/project/snippet/private_access_spec.rb +++ b/spec/features/security/project/snippet/private_access_spec.rb @@ -11,6 +11,7 @@ subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -25,6 +26,7 @@ subject { new_namespace_project_snippet_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -39,6 +41,7 @@ subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -53,6 +56,7 @@ subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb index f3329d0bc960..45b8ca8093df 100644 --- a/spec/features/security/project/snippet/public_access_spec.rb +++ b/spec/features/security/project/snippet/public_access_spec.rb @@ -13,6 +13,7 @@ subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -27,6 +28,7 @@ subject { new_namespace_project_snippet_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_denied_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -42,6 +44,7 @@ subject { namespace_project_snippet_path(project.namespace, project, public_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -56,6 +59,7 @@ subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -70,6 +74,7 @@ subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -86,6 +91,7 @@ subject { raw_namespace_project_snippet_path(project.namespace, project, public_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -100,6 +106,7 @@ subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } @@ -114,6 +121,7 @@ subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:master).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb index ceddb6565961..63cdfc73310c 100644 --- a/spec/support/matchers/access_matchers.rb +++ b/spec/support/matchers/access_matchers.rb @@ -15,6 +15,8 @@ def emulate_user(user, membership = nil) logout when :admin login_as(create(:admin)) + when :auditor + login_as(create(:user, :auditor)) when :external login_as(create(:user, external: true)) when User -- GitLab From 44b47218b43620dc2757723c8e31673d10ae1ac0 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 3 Feb 2017 20:44:00 +0530 Subject: [PATCH 11/26] Hide the auditor user feature behind an EE add-on. 1. The add-on is named `GitLab_Auditor_User` 2. An auditor user cannot be created if the addon is not present. 3. `auditor?` always returns `false` if the addon is not present. --- app/models/user.rb | 13 +++++++++++ spec/factories/licenses.rb | 7 +++++- spec/models/user_spec.rb | 48 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index e512800a573c..b6d49daada82 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -124,6 +124,7 @@ class User < ActiveRecord::Base validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? } validate :cannot_be_admin_and_auditor + validate :auditor_requires_license_add_on validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :generate_password, on: :create @@ -460,6 +461,12 @@ def cannot_be_admin_and_auditor end end + def auditor_requires_license_add_on + unless ::License.current && ::License.current.add_on?('GitLab_Auditor_User') + errors.add(:auditor, 'user cannot be created without the "GitLab_Auditor_User" addon') + end + end + # Returns the groups a user has access to def authorized_groups union = Gitlab::SQL::Union. @@ -538,6 +545,12 @@ def is_admin? admin end + def auditor? + @license_allows_auditors ||= (::License.current && ::License.current.add_on?('GitLab_Auditor_User')) + + @license_allows_auditors && self.auditor + end + def admin_or_auditor? admin? || auditor? end diff --git a/spec/factories/licenses.rb b/spec/factories/licenses.rb index bfc483943778..4faff875637f 100644 --- a/spec/factories/licenses.rb +++ b/spec/factories/licenses.rb @@ -6,7 +6,12 @@ { "Name" => FFaker::Name.name } end restrictions do - { add_ons: { 'GitLab_FileLocks' => 1 } } + { + add_ons: { + 'GitLab_FileLocks' => 1, + 'GitLab_Auditor_User' => 1 + } + } end notify_users_at { |l| l.expires_at } notify_admins_at { |l| l.expires_at } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 97a5fafac876..a1c042611091 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1492,4 +1492,52 @@ def add_user(access) expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true) end end + + describe 'the GitLab_Auditor_User add-on' do + context 'creating an auditor user' do + it "does not allow creating an auditor user if the addon isn't enabled" do + allow_any_instance_of(License).to receive(:add_ons).and_return({}) + + expect(build(:user, :auditor)).to be_invalid + end + + it "does not allow creating an auditor user if no license is present" do + allow(License).to receive(:current).and_return nil + + expect(build(:user, :auditor)).to be_invalid + end + + it "allows creating an auditor user if the addon is enabled" do + allow_any_instance_of(License).to receive(:add_ons).and_return({ 'GitLab_Auditor_User' => 1 }) + + expect(build(:user, :auditor)).to be_valid + end + end + + context '#auditor?' do + it "returns true for an auditor user if the addon is enabled" do + allow_any_instance_of(License).to receive(:add_ons).and_return({ 'GitLab_Auditor_User' => 1 }) + + expect(build(:user, :auditor)).to be_auditor + end + + it "returns false for an auditor user if the addon is not enabled" do + allow_any_instance_of(License).to receive(:add_ons).and_return({}) + + expect(build(:user, :auditor)).not_to be_auditor + end + + it "returns false for an auditor user if a license is not present" do + allow(License).to receive(:current).and_return nil + + expect(build(:user, :auditor)).not_to be_auditor + end + + it "returns false for a non-auditor user even if the addon is present" do + allow_any_instance_of(License).to receive(:add_ons).and_return({ 'GitLab_Auditor_User' => 1 }) + + expect(build(:user)).not_to be_auditor + end + end + end end -- GitLab From 4f022d9ac53c5016bfbf110fd53975fb94cc9bd5 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 3 Feb 2017 13:55:54 -0600 Subject: [PATCH 12/26] Added UI elements Changed the access checkboxes to radio buttons as to only allow one particular type of user active at all times. The new user types are as follows: * Regular * Auditor * Admin --- app/controllers/admin/users_controller.rb | 2 +- app/helpers/path_locks_helper.rb | 4 +++ app/models/user.rb | 17 ++++++++++ app/views/admin/users/_form.html.haml | 38 +++++++++++++++++------ app/views/admin/users/_head.html.haml | 2 ++ 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index c788e6ea59a2..6acdc5d59dc0 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -175,7 +175,7 @@ def user_params def user_params_ce [ - :admin, + :access_level, :avatar, :bio, :can_create_group, diff --git a/app/helpers/path_locks_helper.rb b/app/helpers/path_locks_helper.rb index b1ded86232cd..1e06881abfd3 100644 --- a/app/helpers/path_locks_helper.rb +++ b/app/helpers/path_locks_helper.rb @@ -7,6 +7,10 @@ def license_allows_file_locks? @license_allows_file_locks ||= (::License.current && ::License.current.add_on?('GitLab_FileLocks')) end + def license_allows_auditor_user? + @license_allows_auditor_user ||= (::License.current && ::License.current.add_on?('GitLab_Auditor_User')) + end + def text_label_for_lock(file_lock, path) if file_lock.path == path "Locked by #{file_lock.user.name}" diff --git a/app/models/user.rb b/app/models/user.rb index b6d49daada82..10e31bd11ac1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -957,6 +957,23 @@ def record_activity Gitlab::UserActivities::ActivitySet.record(self) end + def access_level + if admin? + :admin + elsif auditor? + :auditor + else + :regular + end + end + + def access_level=(new_level) + # new_level can be a symbol or a string + new_level = new_level.to_s + self.admin = (new_level == :admin) + self.auditor = (new_level == :auditor) + end + private def ci_projects_union diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 6ed2ff07bbac..f4a6ba8fba9b 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -52,17 +52,37 @@ .form-group = f.label :admin, class: 'control-label' - - if current_user == @user - .col-sm-10= f.check_box :admin, disabled: true - .col-sm-10 You cannot remove your own admin rights. - - else - .col-sm-10= f.check_box :admin - + .col-sm-10 + = f.radio_button :access_level, :regular, checked: true + = label_tag :regular do + Regular + %p.light + Regular users have access to their groups and projects + - if license_allows_auditor_user? + = f.radio_button :access_level, :auditor + = label_tag :auditor do + Audit + %p.light + Auditors have read-only access to all groups, projects and users + - if current_user == @user + = f.radio_button :access_level, :admin + = label_tag :admin do + Admin + %p.light + You cannot remove your own admin rights + -else + = f.radio_button :access_level, :admin + = label_tag :admin do + Admin + %p.light + Administrators have access to all groups, projects and users and can manage all features in this installation .form-group = f.label :external, class: 'control-label' - .col-sm-10= f.check_box :external - .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. - + .col-sm-10 + = f.check_box :external do + External + %p.light + External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. %fieldset %legend Profile .form-group diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 9984e733956a..ea71cf0968f9 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -4,6 +4,8 @@ %span.cred (Blocked) - if @user.admin %span.cred (Admin) + - if @user.auditor + %span.cred (Auditor) .pull-right - unless @user == current_user || @user.blocked? -- GitLab From cdc87fed6041f9c5992692b5c90382d367490ea4 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Feb 2017 00:31:27 +0530 Subject: [PATCH 13/26] Implement backend review comments from @DouweM. Mainly related to increasing compatibility with CE, and trying to avoid merge conflicts. 1. Create an `EE::AuditorUser` module with auditor-specific methods. Mixed into the `User` model. 2. Create an `EE::User` module with EE-specific user methods. Mixed into the `User` model. 3. Don't block creation of regular users when the auditor addon is disabled (bug in original implementation). --- app/helpers/auditor_user_helper.rb | 5 ++++ app/helpers/path_locks_helper.rb | 4 --- app/models/ee/auditor_user.rb | 41 ++++++++++++++++++++++++++++ app/models/ee/user.rb | 26 ++++++++++++++++++ app/models/user.rb | 43 ++---------------------------- app/policies/project_policy.rb | 4 +-- spec/models/user_spec.rb | 6 +++++ 7 files changed, 82 insertions(+), 47 deletions(-) create mode 100644 app/helpers/auditor_user_helper.rb create mode 100644 app/models/ee/auditor_user.rb create mode 100644 app/models/ee/user.rb diff --git a/app/helpers/auditor_user_helper.rb b/app/helpers/auditor_user_helper.rb new file mode 100644 index 000000000000..88094373b0a0 --- /dev/null +++ b/app/helpers/auditor_user_helper.rb @@ -0,0 +1,5 @@ +module AuditorUserHelper + def license_allows_auditor_user? + @license_allows_auditor_user ||= (::License.current && ::License.current.add_on?('GitLab_Auditor_User')) + end +end diff --git a/app/helpers/path_locks_helper.rb b/app/helpers/path_locks_helper.rb index 1e06881abfd3..b1ded86232cd 100644 --- a/app/helpers/path_locks_helper.rb +++ b/app/helpers/path_locks_helper.rb @@ -7,10 +7,6 @@ def license_allows_file_locks? @license_allows_file_locks ||= (::License.current && ::License.current.add_on?('GitLab_FileLocks')) end - def license_allows_auditor_user? - @license_allows_auditor_user ||= (::License.current && ::License.current.add_on?('GitLab_Auditor_User')) - end - def text_label_for_lock(file_lock, path) if file_lock.path == path "Locked by #{file_lock.user.name}" diff --git a/app/models/ee/auditor_user.rb b/app/models/ee/auditor_user.rb new file mode 100644 index 000000000000..703a483fcead --- /dev/null +++ b/app/models/ee/auditor_user.rb @@ -0,0 +1,41 @@ +module EE + # AuditorUser EE mixin + # + # This module is intended to encapsulate EE-specific model logic + # related to auditor (readonly access to all projects) users. It + # is prepended to the `User` model. + module AuditorUser + extend ActiveSupport::Concern + include AuditorUserHelper + + included do + # We aren't using the `auditor?` method for the `if` condition here + # because `auditor?` returns `false` when the `auditor` column is `true` + # and the auditor add-on absent. We want to run this validation + # regardless of the add-on's presence, so we need to check the `auditor` + # column directly. + validate :auditor_requires_license_add_on, if: :auditor + validate :cannot_be_admin_and_auditor + end + + def cannot_be_admin_and_auditor + if admin? && auditor? + errors.add(:admin, "user cannot also be an Auditor.") + end + end + + def auditor_requires_license_add_on + unless license_allows_auditor_user? + errors.add(:auditor, 'user cannot be created without the "GitLab_Auditor_User" addon') + end + end + + def auditor? + license_allows_auditor_user? && self.auditor + end + + def admin_or_auditor? + admin? || auditor? + end + end +end diff --git a/app/models/ee/user.rb b/app/models/ee/user.rb new file mode 100644 index 000000000000..0952316ff609 --- /dev/null +++ b/app/models/ee/user.rb @@ -0,0 +1,26 @@ +module EE + # User EE mixin + # + # This module is intended to encapsulate EE-specific model logic + # and be prepended in the `User` model + module User + extend ActiveSupport::Concern + + def access_level + if admin? + :admin + elsif auditor? + :auditor + else + :regular + end + end + + def access_level=(new_level) + # new_level can be a symbol or a string + new_level = new_level.to_s + self.admin = (new_level == :admin) + self.auditor = (new_level == :auditor) + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 10e31bd11ac1..bf78f74f626b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -10,6 +10,8 @@ class User < ActiveRecord::Base include CaseSensitivity include TokenAuthenticatable prepend EE::GeoAwareAvatar + prepend EE::User + prepend EE::AuditorUser DEFAULT_NOTIFICATION_LEVEL = :participating @@ -123,8 +125,6 @@ class User < ActiveRecord::Base validate :unique_email, if: ->(user) { user.email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? } - validate :cannot_be_admin_and_auditor - validate :auditor_requires_license_add_on validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :generate_password, on: :create @@ -455,18 +455,6 @@ def update_emails_with_primary_email end end - def cannot_be_admin_and_auditor - if admin? && auditor? - errors.add(:admin, "user cannot also be an Auditor.") - end - end - - def auditor_requires_license_add_on - unless ::License.current && ::License.current.add_on?('GitLab_Auditor_User') - errors.add(:auditor, 'user cannot be created without the "GitLab_Auditor_User" addon') - end - end - # Returns the groups a user has access to def authorized_groups union = Gitlab::SQL::Union. @@ -545,16 +533,6 @@ def is_admin? admin end - def auditor? - @license_allows_auditors ||= (::License.current && ::License.current.add_on?('GitLab_Auditor_User')) - - @license_allows_auditors && self.auditor - end - - def admin_or_auditor? - admin? || auditor? - end - def require_ssh_key? keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') end @@ -957,23 +935,6 @@ def record_activity Gitlab::UserActivities::ActivitySet.record(self) end - def access_level - if admin? - :admin - elsif auditor? - :auditor - else - :regular - end - end - - def access_level=(new_level) - # new_level can be a symbol or a string - new_level = new_level.to_s - self.admin = (new_level == :admin) - self.auditor = (new_level == :auditor) - end - private def ci_projects_union diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index d3c25dd9001e..bb03de3fbd0f 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -276,8 +276,8 @@ def named_abilities(name) private # A base set of abilities for read-only users, which - # is then augmented as necessary for anonymous and auditor - # users. + # is then augmented as necessary for anonymous and other + # read-only users. def base_readonly_access! can! :read_project can! :read_board diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a1c042611091..9407e0d3f08c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1512,6 +1512,12 @@ def add_user(access) expect(build(:user, :auditor)).to be_valid end + + it "allows creating a regular user if the addon isn't enabled" do + allow_any_instance_of(License).to receive(:add_ons).and_return({}) + + expect(build(:user)).to be_valid + end end context '#auditor?' do -- GitLab From 9b071cd95ca9bf912c9850ce3f8477efc66ead48 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Feb 2017 01:03:53 +0530 Subject: [PATCH 14/26] Fix the admin users UI. 1. The edit user page allows making a user an admin or an auditor. This creates a virtual attribute called `access_level` which can have `regular`, `admin`, or `auditor` as valid values. 2. The `access_level=` method was broken, which led to the page not accepting changes to a user's access level. 3. This commit fixes the issue and adds specs for `access_level=` --- app/controllers/admin/users_controller.rb | 4 +- app/models/ee/user.rb | 11 ++-- spec/models/user_spec.rb | 71 +++++++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 6acdc5d59dc0..f0f3e3cfd084 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -175,7 +175,6 @@ def user_params def user_params_ce [ - :access_level, :avatar, :bio, :can_create_group, @@ -203,7 +202,8 @@ def user_params_ce def user_params_ee [ - :note + :note, + :access_level ] end end diff --git a/app/models/ee/user.rb b/app/models/ee/user.rb index 0952316ff609..5ca6b0d92de4 100644 --- a/app/models/ee/user.rb +++ b/app/models/ee/user.rb @@ -17,10 +17,13 @@ def access_level end def access_level=(new_level) - # new_level can be a symbol or a string - new_level = new_level.to_s - self.admin = (new_level == :admin) - self.auditor = (new_level == :auditor) + new_level = new_level.to_sym + return unless [:admin, :auditor, :regular].include?(new_level) + + self.admin = self.auditor = false + + self.admin = true if new_level == :admin + self.auditor = true if new_level == :auditor end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9407e0d3f08c..18217e49cc27 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1545,5 +1545,76 @@ def add_user(access) expect(build(:user)).not_to be_auditor end end + + context 'access_level=' do + let(:user) { build(:user) } + + it 'does nothing for an invalid access level' do + user.access_level = :invalid_access_level + + expect(user.access_level).to eq(:regular) + expect(user.admin).to be false + expect(user.auditor).to be false + end + + it "assigns the 'admin' access level" do + user.access_level = :admin + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + expect(user.auditor).to be false + end + + it "assigns the 'auditor' access level" do + user.access_level = :auditor + + expect(user.access_level).to eq(:auditor) + expect(user.admin).to be false + expect(user.auditor).to be true + end + + it "assigns the 'auditor' access level" do + user.access_level = :regular + + expect(user.access_level).to eq(:regular) + expect(user.admin).to be false + expect(user.auditor).to be false + end + + it "clears the 'admin' access level when a user is made an auditor" do + user.access_level = :admin + user.access_level = :auditor + + expect(user.access_level).to eq(:auditor) + expect(user.admin).to be false + expect(user.auditor).to be true + end + + it "clears the 'auditor' access level when a user is made an admin" do + user.access_level = :auditor + user.access_level = :admin + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + expect(user.auditor).to be false + end + + it "doesn't clear existing access levels when an invalid access level is passed in" do + user.access_level = :admin + user.access_level = :invalid_access_level + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + expect(user.auditor).to be false + end + + it "accepts string values in addition to symbols" do + user.access_level = 'admin' + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + expect(user.auditor).to be false + end + end end end -- GitLab From c18709098c065fe2f4c006cf0296f4177a5d1cb7 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Feb 2017 08:22:25 +0530 Subject: [PATCH 15/26] Fix build. There were failures related to cross-spec contamination of the `License` mocks. --- spec/models/user_spec.rb | 26 +++++++++++++++++++------- spec/requests/api/license_spec.rb | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 18217e49cc27..d6ea2ca11252 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1494,9 +1494,15 @@ def add_user(access) end describe 'the GitLab_Auditor_User add-on' do + let(:license) { build(:license) } + + before do + allow(::License).to receive(:current).and_return(license) + end + context 'creating an auditor user' do it "does not allow creating an auditor user if the addon isn't enabled" do - allow_any_instance_of(License).to receive(:add_ons).and_return({}) + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { false } expect(build(:user, :auditor)).to be_invalid end @@ -1508,13 +1514,13 @@ def add_user(access) end it "allows creating an auditor user if the addon is enabled" do - allow_any_instance_of(License).to receive(:add_ons).and_return({ 'GitLab_Auditor_User' => 1 }) + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { true } expect(build(:user, :auditor)).to be_valid end it "allows creating a regular user if the addon isn't enabled" do - allow_any_instance_of(License).to receive(:add_ons).and_return({}) + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { false } expect(build(:user)).to be_valid end @@ -1522,25 +1528,25 @@ def add_user(access) context '#auditor?' do it "returns true for an auditor user if the addon is enabled" do - allow_any_instance_of(License).to receive(:add_ons).and_return({ 'GitLab_Auditor_User' => 1 }) + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { true } expect(build(:user, :auditor)).to be_auditor end it "returns false for an auditor user if the addon is not enabled" do - allow_any_instance_of(License).to receive(:add_ons).and_return({}) + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { false } expect(build(:user, :auditor)).not_to be_auditor end it "returns false for an auditor user if a license is not present" do - allow(License).to receive(:current).and_return nil + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { false } expect(build(:user, :auditor)).not_to be_auditor end it "returns false for a non-auditor user even if the addon is present" do - allow_any_instance_of(License).to receive(:add_ons).and_return({ 'GitLab_Auditor_User' => 1 }) + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { true } expect(build(:user)).not_to be_auditor end @@ -1549,6 +1555,12 @@ def add_user(access) context 'access_level=' do let(:user) { build(:user) } + before do + # `auditor?` returns true only when the user is an auditor _and_ the auditor license + # add-on is present. We aren't testing this here, so we can assume that the add-on exists. + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { true } + end + it 'does nothing for an invalid access level' do user.access_level = :invalid_access_level diff --git a/spec/requests/api/license_spec.rb b/spec/requests/api/license_spec.rb index d5fe60e0a6de..9063380ce2e9 100644 --- a/spec/requests/api/license_spec.rb +++ b/spec/requests/api/license_spec.rb @@ -17,7 +17,7 @@ expect(Date.parse(json_response['expires_at'])).to eq Date.today + 11.months expect(json_response['active_users']).to eq 1 expect(json_response['licensee']).not_to be_empty - expect(json_response['add_ons']).to eq({ 'GitLab_FileLocks' => 1 }) + expect(json_response['add_ons']).to eq({ 'GitLab_FileLocks' => 1, 'GitLab_Auditor_User' => 1 }) end it 'denies access if not admin' do -- GitLab From 67666a3c14d88de3639eb8b4df7a900589a67994 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Mon, 6 Feb 2017 10:32:37 -0600 Subject: [PATCH 16/26] Added and fixed tests on the admin_users_spec.rb --- app/models/user.rb | 4 ++++ app/views/admin/users/_form.html.haml | 2 +- spec/features/admin/admin_users_spec.rb | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index bf78f74f626b..ce2f54bc815b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -533,6 +533,10 @@ def is_admin? admin end + def is_auditor? + auditor + end + def require_ssh_key? keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') end diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index f4a6ba8fba9b..b1adc411a7a9 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -70,7 +70,7 @@ Admin %p.light You cannot remove your own admin rights - -else + - else = f.radio_button :access_level, :admin = label_tag :admin do Admin diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index a586f8d31843..482b98da9f96 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -211,7 +211,7 @@ def expect_two_factor_status(status) fill_in "user_email", with: "bigbang@mail.com" fill_in "user_password", with: "AValidPassword1" fill_in "user_password_confirmation", with: "AValidPassword1" - check "user_admin" + choose "user_access_level_admin" click_button "Save changes" end @@ -228,6 +228,20 @@ def expect_two_factor_status(status) end end + describe "Update user account type" do + before do + allow_any_instance_of(AuditorUserHelper).to receive(:license_allows_auditor_user?).and_return(true) + choose "user_access_level_auditor" + click_button "Save changes" + end + + it "changes account type to be auditor" do + user.reload + expect(user.is_admin?).to be_falsey + expect(user.is_auditor?).to be_truthy + end + end + describe 'update username to non ascii char' do it do fill_in 'user_username', with: '\u3042\u3044' -- GitLab From d0ba5e4bf86b1173ab01b84157890c56e4af1950 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Feb 2017 22:28:29 +0530 Subject: [PATCH 17/26] Make the auditor user UI more compatible with GitLab CE. Use partials and backport code where necessary. --- app/controllers/admin/users_controller.rb | 1 + app/models/user.rb | 4 -- .../admin/users/_access_levels_ee.html.haml | 44 +++++++++++++++++++ app/views/admin/users/_form.html.haml | 43 +----------------- spec/features/admin/admin_users_spec.rb | 5 ++- 5 files changed, 49 insertions(+), 48 deletions(-) create mode 100644 app/views/admin/users/_access_levels_ee.html.haml diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f0f3e3cfd084..871cea67ff22 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -175,6 +175,7 @@ def user_params def user_params_ce [ + :admin, :avatar, :bio, :can_create_group, diff --git a/app/models/user.rb b/app/models/user.rb index ce2f54bc815b..bf78f74f626b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -533,10 +533,6 @@ def is_admin? admin end - def is_auditor? - auditor - end - def require_ssh_key? keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') end diff --git a/app/views/admin/users/_access_levels_ee.html.haml b/app/views/admin/users/_access_levels_ee.html.haml new file mode 100644 index 000000000000..9f570c3b210a --- /dev/null +++ b/app/views/admin/users/_access_levels_ee.html.haml @@ -0,0 +1,44 @@ +%fieldset + %legend Access + .form-group + = f.label :projects_limit, class: 'control-label' + .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' + + .form-group + = f.label :can_create_group, class: 'control-label' + .col-sm-10= f.check_box :can_create_group + + .form-group + = f.label :admin, class: 'control-label' + .col-sm-10 + = f.radio_button :access_level, :regular, checked: true + = label_tag :regular do + Regular + %p.light + Regular users have access to their groups and projects + - if license_allows_auditor_user? + = f.radio_button :access_level, :auditor + = label_tag :auditor do + Audit + %p.light + Auditors have read-only access to all groups, projects and users + - if current_user == @user + = f.radio_button :access_level, :admin + = label_tag :admin do + Admin + %p.light + You cannot remove your own admin rights + - else + = f.radio_button :access_level, :admin + = label_tag :admin do + Admin + %p.light + Administrators have access to all groups, projects and users and can manage all features in this installation + + .form-group + = f.label :external, class: 'control-label' + .col-sm-10 + = f.check_box :external do + External + %p.light + External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index b1adc411a7a9..de2bd5e0bb96 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -40,49 +40,8 @@ = f.label :password_confirmation, class: 'control-label' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' - %fieldset - %legend Access - .form-group - = f.label :projects_limit, class: 'control-label' - .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' - - .form-group - = f.label :can_create_group, class: 'control-label' - .col-sm-10= f.check_box :can_create_group + = render partial: 'access_levels_ee', locals: { f: f } - .form-group - = f.label :admin, class: 'control-label' - .col-sm-10 - = f.radio_button :access_level, :regular, checked: true - = label_tag :regular do - Regular - %p.light - Regular users have access to their groups and projects - - if license_allows_auditor_user? - = f.radio_button :access_level, :auditor - = label_tag :auditor do - Audit - %p.light - Auditors have read-only access to all groups, projects and users - - if current_user == @user - = f.radio_button :access_level, :admin - = label_tag :admin do - Admin - %p.light - You cannot remove your own admin rights - - else - = f.radio_button :access_level, :admin - = label_tag :admin do - Admin - %p.light - Administrators have access to all groups, projects and users and can manage all features in this installation - .form-group - = f.label :external, class: 'control-label' - .col-sm-10 - = f.check_box :external do - External - %p.light - External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. %fieldset %legend Profile .form-group diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 482b98da9f96..e9a684b2011d 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -237,8 +237,9 @@ def expect_two_factor_status(status) it "changes account type to be auditor" do user.reload - expect(user.is_admin?).to be_falsey - expect(user.is_auditor?).to be_truthy + + expect(user).not_to be_admin + expect(user).to be_auditor end end -- GitLab From 7847c63051d673938f090bc0d54c626863e3bed4 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Mon, 6 Feb 2017 12:56:59 -0600 Subject: [PATCH 18/26] Added example image to the auditor documentation --- doc/administration/auditor_access_form.png | Bin 0 -> 35710 bytes doc/administration/auditor_users.md | 10 +++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 doc/administration/auditor_access_form.png diff --git a/doc/administration/auditor_access_form.png b/doc/administration/auditor_access_form.png new file mode 100644 index 0000000000000000000000000000000000000000..08e3eeac541718a6b05a52ca640a6cf12677fa2e GIT binary patch literal 35710 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 WH_pkxcFQRvkesKhpUXO@geCwcaE=E6 literal 0 HcmV?d00001 diff --git a/doc/administration/auditor_users.md b/doc/administration/auditor_users.md index ff2aae301f9b..c5520f7fb254 100644 --- a/doc/administration/auditor_users.md +++ b/doc/administration/auditor_users.md @@ -2,13 +2,13 @@ >**Note:** [Introduced][998] in GitLab 8.17. -With Gitlab Enterprise Edition Premium, you can create *auditor* users, who -are given read-only access to all projects, groups, and other resources on the +With Gitlab Enterprise Edition Premium, you can create *auditor* users, who +are given read-only access to all projects, groups, and other resources on the GitLab instance. First and foremost, an auditor user can perform all the actions that a regular user can. In projects that the auditor user owns, or has been added to, they can be added to -groups, mentioned in comments, or have issues assigned to them. The one exception is +groups, mentioned in comments, or have issues assigned to them. The one exception is that auditor users cannot _create_ projects or groups. In addition, the auditor will be granted read-only access to all other projects/groups/etc. @@ -17,4 +17,8 @@ on the GitLab instance. The `auditor` role is _not_ a read-only version of the `admin` role. The auditor will not be able to access the project settings pages, or the Admin Area. +A user access level can be set to ‘Auditor’ in the Admin Area + +![Admin Area Form](auditor_access_form.png) + [998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998 -- GitLab From ed94edeb5d0353b04ce2cb2097d2cfe94f046d0a Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Mon, 6 Feb 2017 13:30:04 -0600 Subject: [PATCH 19/26] Corrected documentation to reflect the latest changes to view Changed ratio button label from Audit to Auditor, also changed "Admin" label to "Access level" --- .../admin/users/_access_levels_ee.html.haml | 4 ++-- doc/administration/auditor_access_form.png | Bin 35710 -> 36800 bytes doc/administration/auditor_users.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/admin/users/_access_levels_ee.html.haml b/app/views/admin/users/_access_levels_ee.html.haml index 9f570c3b210a..4802f40c4ed4 100644 --- a/app/views/admin/users/_access_levels_ee.html.haml +++ b/app/views/admin/users/_access_levels_ee.html.haml @@ -9,7 +9,7 @@ .col-sm-10= f.check_box :can_create_group .form-group - = f.label :admin, class: 'control-label' + = f.label :access_level, class: 'control-label' .col-sm-10 = f.radio_button :access_level, :regular, checked: true = label_tag :regular do @@ -19,7 +19,7 @@ - if license_allows_auditor_user? = f.radio_button :access_level, :auditor = label_tag :auditor do - Audit + Auditor %p.light Auditors have read-only access to all groups, projects and users - if current_user == @user diff --git a/doc/administration/auditor_access_form.png b/doc/administration/auditor_access_form.png index 08e3eeac541718a6b05a52ca640a6cf12677fa2e..390df04392da02d548e634333b92df611b66ae75 100644 GIT binary patch literal 36800 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 aS&tvC|44&#btrZ(bm6=cvflbq!oL7wRoRdL literal 35710 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 WH_pkxcFQRvkesKhpUXO@geCwcaE=E6 diff --git a/doc/administration/auditor_users.md b/doc/administration/auditor_users.md index c5520f7fb254..fbcc18eb3cf5 100644 --- a/doc/administration/auditor_users.md +++ b/doc/administration/auditor_users.md @@ -17,7 +17,7 @@ on the GitLab instance. The `auditor` role is _not_ a read-only version of the `admin` role. The auditor will not be able to access the project settings pages, or the Admin Area. -A user access level can be set to ‘Auditor’ in the Admin Area +A user's access level can be set to ‘Auditor’ in the Admin Area ![Admin Area Form](auditor_access_form.png) -- GitLab From 42279725f6e1a84bffb0c53c5b16888dbe91b56c Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 7 Feb 2017 01:01:05 +0530 Subject: [PATCH 20/26] Implement final round of review comments from @DouweM. Refactor the EE::User module --- app/models/ee/user.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/models/ee/user.rb b/app/models/ee/user.rb index 5ca6b0d92de4..c30a4f327d85 100644 --- a/app/models/ee/user.rb +++ b/app/models/ee/user.rb @@ -17,13 +17,11 @@ def access_level end def access_level=(new_level) - new_level = new_level.to_sym - return unless [:admin, :auditor, :regular].include?(new_level) + new_level = new_level.to_s + return unless %w(admin auditor regular).include?(new_level) - self.admin = self.auditor = false - - self.admin = true if new_level == :admin - self.auditor = true if new_level == :auditor + self.admin = (new_level == 'admin') + self.auditor = (new_level == 'auditor') end end end -- GitLab From d2f5ee21c6ad7ecc3bcc3157758683749a0e0071 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 7 Feb 2017 01:17:01 +0530 Subject: [PATCH 21/26] Delete `EE::AuditorUser` - There's no real need to have this module _and_ `EE::User` - This commit moves the contents of `EE::AuditorUser` to `EE::User` --- app/models/ee/auditor_user.rb | 41 ----------------------------------- app/models/ee/user.rb | 31 ++++++++++++++++++++++++++ app/models/user.rb | 1 - 3 files changed, 31 insertions(+), 42 deletions(-) delete mode 100644 app/models/ee/auditor_user.rb diff --git a/app/models/ee/auditor_user.rb b/app/models/ee/auditor_user.rb deleted file mode 100644 index 703a483fcead..000000000000 --- a/app/models/ee/auditor_user.rb +++ /dev/null @@ -1,41 +0,0 @@ -module EE - # AuditorUser EE mixin - # - # This module is intended to encapsulate EE-specific model logic - # related to auditor (readonly access to all projects) users. It - # is prepended to the `User` model. - module AuditorUser - extend ActiveSupport::Concern - include AuditorUserHelper - - included do - # We aren't using the `auditor?` method for the `if` condition here - # because `auditor?` returns `false` when the `auditor` column is `true` - # and the auditor add-on absent. We want to run this validation - # regardless of the add-on's presence, so we need to check the `auditor` - # column directly. - validate :auditor_requires_license_add_on, if: :auditor - validate :cannot_be_admin_and_auditor - end - - def cannot_be_admin_and_auditor - if admin? && auditor? - errors.add(:admin, "user cannot also be an Auditor.") - end - end - - def auditor_requires_license_add_on - unless license_allows_auditor_user? - errors.add(:auditor, 'user cannot be created without the "GitLab_Auditor_User" addon') - end - end - - def auditor? - license_allows_auditor_user? && self.auditor - end - - def admin_or_auditor? - admin? || auditor? - end - end -end diff --git a/app/models/ee/user.rb b/app/models/ee/user.rb index c30a4f327d85..a437ffdda569 100644 --- a/app/models/ee/user.rb +++ b/app/models/ee/user.rb @@ -5,6 +5,37 @@ module EE # and be prepended in the `User` model module User extend ActiveSupport::Concern + include AuditorUserHelper + + included do + # We aren't using the `auditor?` method for the `if` condition here + # because `auditor?` returns `false` when the `auditor` column is `true` + # and the auditor add-on absent. We want to run this validation + # regardless of the add-on's presence, so we need to check the `auditor` + # column directly. + validate :auditor_requires_license_add_on, if: :auditor + validate :cannot_be_admin_and_auditor + end + + def cannot_be_admin_and_auditor + if admin? && auditor? + errors.add(:admin, "user cannot also be an Auditor.") + end + end + + def auditor_requires_license_add_on + unless license_allows_auditor_user? + errors.add(:auditor, 'user cannot be created without the "GitLab_Auditor_User" addon') + end + end + + def auditor? + license_allows_auditor_user? && self.auditor + end + + def admin_or_auditor? + admin? || auditor? + end def access_level if admin? diff --git a/app/models/user.rb b/app/models/user.rb index bf78f74f626b..8237a1ec5428 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,7 +11,6 @@ class User < ActiveRecord::Base include TokenAuthenticatable prepend EE::GeoAwareAvatar prepend EE::User - prepend EE::AuditorUser DEFAULT_NOTIFICATION_LEVEL = :participating -- GitLab From 61a1d1fce9a643291aeac4fceaf926e9b0707f43 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Mon, 6 Feb 2017 15:45:56 -0600 Subject: [PATCH 22/26] Fixed radio buttons conditioning to not allow admins change their own permissions --- .../admin/users/_access_levels_ee.html.haml | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/app/views/admin/users/_access_levels_ee.html.haml b/app/views/admin/users/_access_levels_ee.html.haml index 4802f40c4ed4..3d31f60cd5b0 100644 --- a/app/views/admin/users/_access_levels_ee.html.haml +++ b/app/views/admin/users/_access_levels_ee.html.haml @@ -11,29 +11,22 @@ .form-group = f.label :access_level, class: 'control-label' .col-sm-10 - = f.radio_button :access_level, :regular, checked: true + = f.radio_button :access_level, :regular, disabled: (current_user == @user && @user.is_admin?) = label_tag :regular do Regular %p.light Regular users have access to their groups and projects - if license_allows_auditor_user? - = f.radio_button :access_level, :auditor + = f.radio_button :access_level, :auditor, disabled: (current_user == @user && @user.is_admin?) = label_tag :auditor do Auditor %p.light Auditors have read-only access to all groups, projects and users - - if current_user == @user - = f.radio_button :access_level, :admin - = label_tag :admin do - Admin - %p.light - You cannot remove your own admin rights - - else - = f.radio_button :access_level, :admin - = label_tag :admin do - Admin - %p.light - Administrators have access to all groups, projects and users and can manage all features in this installation + = f.radio_button :access_level, :admin + = label_tag :admin do + Admin + %p.light + Administrators have access to all groups, projects and users and can manage all features in this installation .form-group = f.label :external, class: 'control-label' -- GitLab From 632ee6187552c9bac9974064e89ca3bc3643d1bc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 6 Feb 2017 16:59:48 -0600 Subject: [PATCH 23/26] Prepare for backport --- app/controllers/admin/users_controller.rb | 5 +- app/models/ee/user.rb | 18 -- app/models/user.rb | 18 ++ ..._ee.html.haml => _access_levels.html.haml} | 13 +- app/views/admin/users/_form.html.haml | 2 +- spec/models/user_spec.rb | 154 +++++++++--------- 6 files changed, 108 insertions(+), 102 deletions(-) rename app/views/admin/users/{_access_levels_ee.html.haml => _access_levels.html.haml} (74%) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 871cea67ff22..6acdc5d59dc0 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -175,7 +175,7 @@ def user_params def user_params_ce [ - :admin, + :access_level, :avatar, :bio, :can_create_group, @@ -203,8 +203,7 @@ def user_params_ce def user_params_ee [ - :note, - :access_level + :note ] end end diff --git a/app/models/ee/user.rb b/app/models/ee/user.rb index a437ffdda569..40b3f3178c5b 100644 --- a/app/models/ee/user.rb +++ b/app/models/ee/user.rb @@ -36,23 +36,5 @@ def auditor? def admin_or_auditor? admin? || auditor? end - - def access_level - if admin? - :admin - elsif auditor? - :auditor - else - :regular - end - end - - def access_level=(new_level) - new_level = new_level.to_s - return unless %w(admin auditor regular).include?(new_level) - - self.admin = (new_level == 'admin') - self.auditor = (new_level == 'auditor') - end end end diff --git a/app/models/user.rb b/app/models/user.rb index 8237a1ec5428..8e2463e7aaaf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -934,6 +934,24 @@ def record_activity Gitlab::UserActivities::ActivitySet.record(self) end + def access_level + if admin? + :admin + elsif auditor? + :auditor + else + :regular + end + end + + def access_level=(new_level) + new_level = new_level.to_s + return unless %w(admin auditor regular).include?(new_level) + + self.admin = (new_level == 'admin') + self.auditor = (new_level == 'auditor') + end + private def ci_projects_union diff --git a/app/views/admin/users/_access_levels_ee.html.haml b/app/views/admin/users/_access_levels.html.haml similarity index 74% rename from app/views/admin/users/_access_levels_ee.html.haml rename to app/views/admin/users/_access_levels.html.haml index 3d31f60cd5b0..b3e76ddaf4dc 100644 --- a/app/views/admin/users/_access_levels_ee.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -11,22 +11,29 @@ .form-group = f.label :access_level, class: 'control-label' .col-sm-10 - = f.radio_button :access_level, :regular, disabled: (current_user == @user && @user.is_admin?) + - editing_current_user = (current_user == @user) + + = f.radio_button :access_level, :regular, disabled: editing_current_user = label_tag :regular do Regular %p.light Regular users have access to their groups and projects + - if license_allows_auditor_user? - = f.radio_button :access_level, :auditor, disabled: (current_user == @user && @user.is_admin?) + = f.radio_button :access_level, :auditor, disabled: editing_current_user = label_tag :auditor do Auditor %p.light Auditors have read-only access to all groups, projects and users - = f.radio_button :access_level, :admin + + = f.radio_button :access_level, :admin, disabled: editing_current_user = label_tag :admin do Admin %p.light Administrators have access to all groups, projects and users and can manage all features in this installation + - if editing_current_user + %p.light + You cannot remove your own admin rights. .form-group = f.label :external, class: 'control-label' diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index de2bd5e0bb96..bbe8f7d91143 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -40,7 +40,7 @@ = f.label :password_confirmation, class: 'control-label' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' - = render partial: 'access_levels_ee', locals: { f: f } + = render partial: 'access_levels', locals: { f: f } %fieldset %legend Profile diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d6ea2ca11252..014d70acfab4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1493,6 +1493,83 @@ def add_user(access) end end + describe '#access_level=' do + let(:user) { build(:user) } + + before do + # `auditor?` returns true only when the user is an auditor _and_ the auditor license + # add-on is present. We aren't testing this here, so we can assume that the add-on exists. + allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { true } + end + + it 'does nothing for an invalid access level' do + user.access_level = :invalid_access_level + + expect(user.access_level).to eq(:regular) + expect(user.admin).to be false + expect(user.auditor).to be false + end + + it "assigns the 'admin' access level" do + user.access_level = :admin + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + expect(user.auditor).to be false + end + + it "assigns the 'auditor' access level" do + user.access_level = :auditor + + expect(user.access_level).to eq(:auditor) + expect(user.admin).to be false + expect(user.auditor).to be true + end + + it "assigns the 'auditor' access level" do + user.access_level = :regular + + expect(user.access_level).to eq(:regular) + expect(user.admin).to be false + expect(user.auditor).to be false + end + + it "clears the 'admin' access level when a user is made an auditor" do + user.access_level = :admin + user.access_level = :auditor + + expect(user.access_level).to eq(:auditor) + expect(user.admin).to be false + expect(user.auditor).to be true + end + + it "clears the 'auditor' access level when a user is made an admin" do + user.access_level = :auditor + user.access_level = :admin + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + expect(user.auditor).to be false + end + + it "doesn't clear existing access levels when an invalid access level is passed in" do + user.access_level = :admin + user.access_level = :invalid_access_level + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + expect(user.auditor).to be false + end + + it "accepts string values in addition to symbols" do + user.access_level = 'admin' + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + expect(user.auditor).to be false + end + end + describe 'the GitLab_Auditor_User add-on' do let(:license) { build(:license) } @@ -1551,82 +1628,5 @@ def add_user(access) expect(build(:user)).not_to be_auditor end end - - context 'access_level=' do - let(:user) { build(:user) } - - before do - # `auditor?` returns true only when the user is an auditor _and_ the auditor license - # add-on is present. We aren't testing this here, so we can assume that the add-on exists. - allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Auditor_User') { true } - end - - it 'does nothing for an invalid access level' do - user.access_level = :invalid_access_level - - expect(user.access_level).to eq(:regular) - expect(user.admin).to be false - expect(user.auditor).to be false - end - - it "assigns the 'admin' access level" do - user.access_level = :admin - - expect(user.access_level).to eq(:admin) - expect(user.admin).to be true - expect(user.auditor).to be false - end - - it "assigns the 'auditor' access level" do - user.access_level = :auditor - - expect(user.access_level).to eq(:auditor) - expect(user.admin).to be false - expect(user.auditor).to be true - end - - it "assigns the 'auditor' access level" do - user.access_level = :regular - - expect(user.access_level).to eq(:regular) - expect(user.admin).to be false - expect(user.auditor).to be false - end - - it "clears the 'admin' access level when a user is made an auditor" do - user.access_level = :admin - user.access_level = :auditor - - expect(user.access_level).to eq(:auditor) - expect(user.admin).to be false - expect(user.auditor).to be true - end - - it "clears the 'auditor' access level when a user is made an admin" do - user.access_level = :auditor - user.access_level = :admin - - expect(user.access_level).to eq(:admin) - expect(user.admin).to be true - expect(user.auditor).to be false - end - - it "doesn't clear existing access levels when an invalid access level is passed in" do - user.access_level = :admin - user.access_level = :invalid_access_level - - expect(user.access_level).to eq(:admin) - expect(user.admin).to be true - expect(user.auditor).to be false - end - - it "accepts string values in addition to symbols" do - user.access_level = 'admin' - - expect(user.access_level).to eq(:admin) - expect(user.admin).to be true - expect(user.auditor).to be false - end - end end end -- GitLab From 37bcdc913061959bc35d2dc6d0e4a22b14e90adf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 6 Feb 2017 19:16:54 -0600 Subject: [PATCH 24/26] Prevent licenses table from getting cleaned up --- spec/support/db_cleaner.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 26e8bf5c0274..6b0028a6089b 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -12,7 +12,7 @@ end config.before(:each, truncate: true) do - DatabaseCleaner.strategy = :truncation + DatabaseCleaner.strategy = :truncation, { except: ['licenses'] } end config.before(:each) do -- GitLab From 79497949129b5b1515309d7949826e3791808ca7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 6 Feb 2017 20:28:17 -0600 Subject: [PATCH 25/26] List all groups/projects for auditors on explore pages --- app/finders/group_projects_finder.rb | 2 +- lib/gitlab/visibility_level.rb | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index 63445211d7bd..3b9a421b1187 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -18,7 +18,7 @@ def group_projects(current_user) projects = [] if current_user - if @group.users.include?(current_user) || current_user.admin_or_auditor? + if @group.users.include?(current_user) projects << @group.projects unless only_shared projects << @group.shared_projects unless only_owned else diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index c7953af29ddb..ba9140c16fac 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -13,7 +13,19 @@ module VisibilityLevel scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } scope :non_public_only, -> { where.not(visibility_level: PUBLIC) } - scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only } + scope :public_to_user, -> (user) do + if user + if user.admin_or_auditor? + all + elsif !user.external? + public_and_internal_only + else + public_only + end + else + public_only + end + end end PRIVATE = 0 unless const_defined?(:PRIVATE) -- GitLab From 7e3ec6e83b75811722402a5035db7c48bedcca29 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 6 Feb 2017 22:08:51 -0600 Subject: [PATCH 26/26] Use random group name to prevent conflicts --- spec/services/groups/update_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 4a045b78187b..1459c745828a 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -76,7 +76,7 @@ end context 'rename group' do - let!(:service) { described_class.new(internal_group, user, path: 'new_path') } + let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) } before do internal_group.add_user(user, Gitlab::Access::MASTER) -- GitLab