Commit abfb082a authored by Bob Van Landuyt's avatar Bob Van Landuyt

Authorize project access with external service

When this feature is enabled, users will not be able to see any cross
project references.

When a user tries to view a project we will first validate if the user
has access to the classification label assigned to the project.

When no classification label is assigned to a project, a default label
is used. This default can be defined in the settings.
parent fb4c0b7e
class Projects::ApplicationController < ApplicationController class Projects::ApplicationController < ApplicationController
prepend EE::Projects::ApplicationController
include RoutableActions include RoutableActions
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
......
...@@ -213,7 +213,11 @@ class Issue < ActiveRecord::Base ...@@ -213,7 +213,11 @@ class Issue < ActiveRecord::Base
.preload(preload) .preload(preload)
.reorder('issue_link_id') .reorder('issue_link_id')
Ability.issues_readable_by_user(related_issues, current_user) cross_project_filter = -> (issues) { issues.where(project: project) }
Ability.issues_readable_by_user(
related_issues, current_user,
filters: { read_cross_project: cross_project_filter }
)
end end
# Returns boolean if a related branch exists for the current issue # Returns boolean if a related branch exists for the current issue
......
require_dependency 'declarative_policy' require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base class BasePolicy < DeclarativePolicy::Base
prepend EE::BasePolicy
desc "User is an instance admin" desc "User is an instance admin"
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:admin) { @user&.admin? } condition(:admin) { @user&.admin? }
......
...@@ -29,7 +29,7 @@ module Boards ...@@ -29,7 +29,7 @@ module Boards
{ project_ids: [parent.id], group_ids: [parent.group&.id] } { project_ids: [parent.id], group_ids: [parent.group&.id] }
end end
milestone = MilestonesFinder.new(finder_params).execute.find_by_id(milestone_id) milestone = MilestonesFinder.new(finder_params).find_by(id: milestone_id)
params[:milestone_id] = milestone&.id params[:milestone_id] = milestone&.id
end end
......
...@@ -927,5 +927,8 @@ ...@@ -927,5 +927,8 @@
.col-sm-10 .col-sm-10
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
- if License.feature_available?(:external_authorization_service)
= render partial: 'external_authorization_service_form', locals: { f: f }
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
= render "layouts/header/ee_license_banner" = render "layouts/header/ee_license_banner"
= render "layouts/broadcast" = render "layouts/broadcast"
= render 'layouts/header/ee/geo_secondary_banner' = render 'layouts/header/ee/geo_secondary_banner'
= render "layouts/nav/ee/classification_level_banner"
= yield :flash_message = yield :flash_message
- unless @hide_breadcrumbs - unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs" = render "layouts/nav/breadcrumbs"
......
...@@ -37,6 +37,8 @@ ...@@ -37,6 +37,8 @@
%span.light (optional) %span.light (optional)
= f.text_area :description, class: "form-control", rows: 3, maxlength: 250 = f.text_area :description, class: "form-control", rows: 3, maxlength: 250
= render 'projects/ee/classification_policy_settings', f: f
- unless @project.empty_repo? - unless @project.empty_repo?
.form-group .form-group
= f.label :default_branch, "Default Branch", class: 'label-light' = f.label :default_branch, "Default Branch", class: 'label-light'
......
...@@ -180,6 +180,9 @@ ActiveRecord::Schema.define(version: 20180215143644) do ...@@ -180,6 +180,9 @@ ActiveRecord::Schema.define(version: 20180215143644) do
t.boolean "mirror_available", default: true, null: false t.boolean "mirror_available", default: true, null: false
t.string "auto_devops_domain" t.string "auto_devops_domain"
t.integer "default_project_creation", default: 2, null: false t.integer "default_project_creation", default: 2, null: false
t.boolean "external_authorization_service_enabled", default: false, null: false
t.string "external_authorization_service_url"
t.string "external_authorization_service_default_label"
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
...@@ -1914,6 +1917,7 @@ ActiveRecord::Schema.define(version: 20180215143644) do ...@@ -1914,6 +1917,7 @@ ActiveRecord::Schema.define(version: 20180215143644) do
t.boolean "pull_mirror_available_overridden" t.boolean "pull_mirror_available_overridden"
t.integer "jobs_cache_index" t.integer "jobs_cache_index"
t.boolean "mirror_overwrites_diverged_branches" t.boolean "mirror_overwrites_diverged_branches"
t.string "external_authorization_classification_label"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
...@@ -76,3 +76,6 @@ ...@@ -76,3 +76,6 @@
justify-content: center; justify-content: center;
} }
} }
.classification-label {
background-color: $common-red;
}
...@@ -12,6 +12,10 @@ module EE ...@@ -12,6 +12,10 @@ module EE
attrs << :default_project_creation attrs << :default_project_creation
end end
if License.feature_available?(:external_authorization_service)
attrs += EE::ApplicationSettingsHelper.external_authorization_service_attributes
end
attrs attrs
end end
end end
......
module EE module EE
module Boards module Boards
module IssuesController module IssuesController
extend ActiveSupport::Concern
include ControllerWithCrossProjectAccessCheck
prepended do
requires_cross_project_access if: -> { board.group_board? }
end
def issues_finder def issues_finder
return super unless board.group_board? return super unless board.group_board?
......
module EE module EE
module Groups module Groups
module ApplicationController module ApplicationController
extend ActiveSupport::Concern
def check_group_feature_available!(feature) def check_group_feature_available!(feature)
render_404 unless group.feature_available?(feature) render_404 unless group.feature_available?(feature)
end end
......
module EE module EE
module Groups module Groups
module GroupMembersController module GroupMembersController
extend ActiveSupport::Concern
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def override def override
@group_member = @group.group_members.find(params[:id]) @group_member = @group.group_members.find(params[:id])
......
module EE module EE
module GroupsController module GroupsController
extend ActiveSupport::Concern
def group_params_attributes def group_params_attributes
super + group_params_ee super + group_params_ee
end end
......
module EE
module Projects
module ApplicationController
extend ::Gitlab::Utils::Override
override :handle_not_found_or_authorized
def handle_not_found_or_authorized(project)
return super unless project
label = project.external_authorization_classification_label
rejection_reason = nil
unless EE::Gitlab::ExternalAuthorization.access_allowed?(current_user, label)
rejection_reason = EE::Gitlab::ExternalAuthorization.rejection_reason(current_user, label)
rejection_reason ||= _('External authorization denied access to this project')
end
if rejection_reason
access_denied!(rejection_reason)
else
super
end
end
end
end
end
...@@ -20,6 +20,7 @@ module EE ...@@ -20,6 +20,7 @@ module EE
mirror mirror
mirror_trigger_builds mirror_trigger_builds
mirror_user_id mirror_user_id
external_authorization_classification_label
] ]
end end
end end
......
...@@ -34,5 +34,17 @@ module EE ...@@ -34,5 +34,17 @@ module EE
:mirror_capacity_threshold :mirror_capacity_threshold
] ]
end end
def self.external_authorization_service_attributes
[
:external_authorization_service_enabled,
:external_authorization_service_url,
:external_authorization_service_default_label
]
end
def self.possible_licensed_attributes
repository_mirror_attributes + external_authorization_service_attributes
end
end end
end end
...@@ -10,5 +10,27 @@ module EE ...@@ -10,5 +10,27 @@ module EE
super super
end end
end end
private
def get_group_sidebar_links
links = super
if can?(current_user, :read_cross_project)
if @group.feature_available?(:contribution_analytics) || show_promotions?
links << :contribution_analytics
end
if @group.feature_available?(:group_issue_boards)
links << :boards
end
if @group.feature_available?(:epics)
links << :epics
end
end
links
end
end end
end end
...@@ -5,5 +5,15 @@ module EE ...@@ -5,5 +5,15 @@ module EE
can?(current_user, :"change_#{rule}", @project) can?(current_user, :"change_#{rule}", @project)
end end
def external_classification_label_help_message
default_label = ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
s_(
"ExternalAuthorizationService|When no classification label is set the "\
"default label `%{default_label}` will be used."
) % { default_label: default_label }
end
end end
end end
...@@ -185,6 +185,12 @@ module Elastic ...@@ -185,6 +185,12 @@ module Elastic
# documents gated by that project feature - e.g., "issues". The feature's # documents gated by that project feature - e.g., "issues". The feature's
# visibility level must be taken into account. # visibility level must be taken into account.
def project_ids_query(user, project_ids, public_and_internal_projects, feature = nil) def project_ids_query(user, project_ids, public_and_internal_projects, feature = nil)
# When reading cross project is not allowed, only allow searching a
# a single project, so the `:read_*` ability is only checked once.
unless Ability.allowed?(user, :read_cross_project)
project_ids = [] if project_ids.is_a?(Array) && project_ids.size > 1
end
# At least one condition must be present, so pick no projects for # At least one condition must be present, so pick no projects for
# anonymous users. # anonymous users.
# Pick private, internal and public projects the user is a member of. # Pick private, internal and public projects the user is a member of.
......
...@@ -66,7 +66,7 @@ module Elastic ...@@ -66,7 +66,7 @@ module Elastic
bool: { bool: {
should: [ should: [
{ term: { author_id: user.id } }, { term: { author_id: user.id } },
{ terms: { project_id: user.authorized_projects.pluck(:id) } }, { terms: { project_id: authorized_project_ids_for_user(user) } },
{ {
bool: { bool: {
filter: { terms: { visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL] } }, filter: { terms: { visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL] } },
...@@ -88,6 +88,14 @@ module Elastic ...@@ -88,6 +88,14 @@ module Elastic
query_hash[:query][:bool][:filter] = filter query_hash[:query][:bool][:filter] = filter
query_hash query_hash
end end
def self.authorized_project_ids_for_user(user)
if Ability.allowed?(user, :read_cross_project)
user.authorized_projects.pluck(:id)
else
[]
end
end
end end
end end
end end
...@@ -39,6 +39,15 @@ module EE ...@@ -39,6 +39,15 @@ module EE
validates :elasticsearch_aws_region, validates :elasticsearch_aws_region,
presence: { message: "can't be blank when using aws hosted elasticsearch" }, presence: { message: "can't be blank when using aws hosted elasticsearch" },
if: ->(setting) { setting.elasticsearch_indexing? && setting.elasticsearch_aws? } if: ->(setting) { setting.elasticsearch_indexing? && setting.elasticsearch_aws? }
validates :external_authorization_service_url,
:external_authorization_service_default_label,
presence: true,
if: :external_authorization_service_enabled?
validates :external_authorization_service_url,
url: true,
if: :external_authorization_service_enabled?
end end
module ClassMethods module ClassMethods
...@@ -103,6 +112,12 @@ module EE ...@@ -103,6 +112,12 @@ module EE
} }
end end
def external_authorization_service_enabled
License.feature_available?(:external_authorization_service) && super
end
alias_method :external_authorization_service_enabled?,
:external_authorization_service_enabled
private private
def mirror_max_delay_in_minutes def mirror_max_delay_in_minutes
......
module EE module EE
module Issue module Issue
extend ::Gitlab::Utils::Override
# override # override
def check_for_spam? def check_for_spam?
author.support_bot? || super author.support_bot? || super
...@@ -34,6 +36,23 @@ module EE ...@@ -34,6 +36,23 @@ module EE
super if supports_weight? super if supports_weight?
end end
# The functionality here is duplicated from the `IssuePolicy` and the
# `EE::IssuePolicy` for better performace
#
# Make sure to keep this in sync with the policies.
override :readable_by?
def readable_by?(user)
return super if user.full_private_access?
super && ::EE::Gitlab::ExternalAuthorization
.access_allowed?(user, project.external_authorization_classification_label)
end
override :publicly_visible?
def publicly_visible?
super && !::EE::Gitlab::ExternalAuthorization.enabled?
end
def supports_weight? def supports_weight?
project&.feature_available?(:issue_weights) project&.feature_available?(:issue_weights)
end end
......
...@@ -457,6 +457,13 @@ module EE ...@@ -457,6 +457,13 @@ module EE
::Gitlab::CurrentSettings.mirror_available ::Gitlab::CurrentSettings.mirror_available
end end
def external_authorization_classification_label
return nil unless feature_available?(:external_authorization_service)
super || ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
end
private private
def set_override_pull_mirror_available def set_override_pull_mirror_available
......
...@@ -53,6 +53,7 @@ class License < ActiveRecord::Base ...@@ -53,6 +53,7 @@ class License < ActiveRecord::Base
reject_unsigned_commits reject_unsigned_commits
commit_committer_check commit_committer_check
project_creation_level project_creation_level
external_authorization_service
].freeze ].freeze
EEU_FEATURES = EEP_FEATURES + %i[ EEU_FEATURES = EEP_FEATURES + %i[
...@@ -133,6 +134,7 @@ class License < ActiveRecord::Base ...@@ -133,6 +134,7 @@ class License < ActiveRecord::Base
multiple_ldap_servers multiple_ldap_servers
object_storage object_storage
repository_size_limit repository_size_limit
external_authorization_service
].freeze ].freeze
validate :valid_license validate :valid_license
......
module EE
module BasePolicy
extend ActiveSupport::Concern
prepended do
condition(:external_authorization_enabled, scope: :global, score: 0) do
::EE::Gitlab::ExternalAuthorization.enabled?
end
rule { external_authorization_enabled & ~admin & ~auditor }.policy do
prevent :read_cross_project
end
end
end
end
...@@ -15,6 +15,14 @@ module EE ...@@ -15,6 +15,14 @@ module EE
with_scope :subject with_scope :subject
condition(:deploy_board_disabled) { !@subject.feature_available?(:deploy_board) } condition(:deploy_board_disabled) { !@subject.feature_available?(:deploy_board) }
with_scope :subject
condition(:classification_label_authorized, score: 32) do
EE::Gitlab::ExternalAuthorization.access_allowed?(
@user,
@subject.external_authorization_classification_label
)
end
with_scope :global with_scope :global
condition(:is_development) { Rails.env.development? } condition(:is_development) { Rails.env.development? }
...@@ -97,6 +105,22 @@ module EE ...@@ -97,6 +105,22 @@ module EE
rule { admin | (commit_committer_check_disabled_globally & can?(:master_access)) }.enable :change_commit_committer_check rule { admin | (commit_committer_check_disabled_globally & can?(:master_access)) }.enable :change_commit_committer_check
rule { owner | reporter }.enable :build_read_project rule { owner | reporter }.enable :build_read_project
rule { ~can?(:read_cross_project) & ~classification_label_authorized }.policy do
# Preventing access here still allows the projects to be listed. Listing
# projects doesn't check the `:read_project` ability. But instead counts
# on the `project_authorizations` table.
#
# All other actions should explicitly check read project, which would
# trigger the `classification_label_authorized` condition.
prevent :guest_access
prevent :public_access
prevent :public_user_access
prevent :reporter_access
prevent :developer_access
prevent :master_access
prevent :owner_access
end
end end
end end
end end
class EpicPolicy < BasePolicy class EpicPolicy < BasePolicy
delegate { @subject.group } delegate { @subject.group }
rule { can?(:read_epic) }.enable :read_epic_iid
end end
- if License.feature_available?(:external_authorization_service)
%fieldset
%legend
= _('External Classification Policy Authorization')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :external_authorization_service_enabled do
= f.check_box :external_authorization_service_enabled
Enable classification control using an external service
%span.help-block
If enabled, access to projects will be validated on an external service
using their classification label.
= link_to icon('question-circle'), help_page_path('#')
.form-group
= f.label :external_authorization_service_url, _('Service URL'), class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :external_authorization_service_url, class: 'form-control'
.form-group
= f.label :external_authorization_service_default_label, _('Default classification label'), class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :external_authorization_service_default_label, class: 'form-control'
- if EE::Gitlab::ExternalAuthorization.enabled? && @project
= content_for :header_content do
%span.label.color-label.classification-label.has-tooltip{ title: s_('ExternalAuthorizationService|Classification label') }
= sprite_icon('lock-open', size: 8, css_class: 'inline')
= @project.external_authorization_classification_label
- return unless group.feature_available?(:epics) - return unless group_sidebar_link?(:epics)
- epics = EpicsFinder.new(current_user, group_id: @group.id).execute - epics = EpicsFinder.new(current_user, group_id: @group.id).execute
- epics_items = ['epics#show', 'epics#index', 'roadmap#show'] - epics_items = ['epics#show', 'epics#index', 'roadmap#show']
......
- if ::EE::Gitlab::ExternalAuthorization.enabled?
.form-group
= f.label :external_authorization_classification_label, class: 'label-light' do
= s_('ExternalAuthorizationService|Classification Label')
%span.light (optional)
= f.text_field :external_authorization_classification_label, class: "form-control"
%span.help-block
= external_classification_label_help_message
class AddExternalClassificationAuthorizationSettingsToApplictionSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :application_settings,
:external_authorization_service_enabled,
:boolean,
default: false
add_column :application_settings,
:external_authorization_service_url,
:string
add_column :application_settings,
:external_authorization_service_default_label,
:string
end
def down
remove_column :application_settings,
:external_authorization_service_default_label
remove_column :application_settings,
:external_authorization_service_url
remove_column :application_settings,
:external_authorization_service_enabled
end
end
class AddExternalAuthorizationServiceClassificationLabelToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :projects,
:external_authorization_classification_label,
:string
end
end
module EE
module Gitlab
module ExternalAuthorization
RequestFailed = Class.new(StandardError)
def self.access_allowed?(user, label)
return true unless enabled?
return false unless user
access_for_user_to_label(user, label).has_access?
end
def self.rejection_reason(user, label)
return nil unless enabled?
return nil unless user
access_for_user_to_label(user, label).reason
end
def self.access_for_user_to_label(user, label)
if RequestStore.active?
RequestStore.fetch("external_authorisation:user-#{user.id}:label-#{label}") do
EE::Gitlab::ExternalAuthorization::Access.new(user, label).load!
end
else
EE::Gitlab::ExternalAuthorization::Access.new(user, label).load!
end
end
def self.enabled?
::Gitlab::CurrentSettings
.current_application_settings
.external_authorization_service_enabled?
end
def self.service_url
::Gitlab::CurrentSettings
.current_application_settings
.external_authorization_service_url
end
end
end
end
module EE
module Gitlab
module ExternalAuthorization
class Access
attr_reader :access, :reason, :loaded_at
def initialize(user, label)
@user, @label = user, label
end
def loaded?
loaded_at && (loaded_at > Cache::VALIDITY_TIME.ago)
end
def has_access?
@access
end
def load!
load_from_cache
load_from_service unless loaded?
self
end
private
def load_from_cache
@access, @reason, @loaded_at = cache.load
end
def load_from_service
response = Client.build(@user, @label).request_access
@access = response.successful?
@reason = response.reason
@loaded_at = Time.now
cache.store(@access, @reason, @loaded_at) if response.valid?
rescue EE::Gitlab::ExternalAuthorization::RequestFailed => e
@access = false
@reason = e.message
@loaded_at = Time.now
end
def cache
@cache ||= Cache.new(@user, @label)
end
end
end
end
end
module EE
module Gitlab
module ExternalAuthorization
class Cache
VALIDITY_TIME = 6.hours
def initialize(user, label)
@user, @label = user, label
end
def load
@access, @reason, @refreshed_at = ::Gitlab::Redis::Cache.with do |redis|
redis.hmget(cache_key, :access, :reason, :refreshed_at)
end
[access, reason, refreshed_at]
end
def store(new_access, new_reason, new_refreshed_at)
::Gitlab::Redis::Cache.with do |redis|
redis.pipelined do
redis.mapped_hmset(
cache_key,
{
access: new_access.to_s,
reason: new_reason.to_s,
refreshed_at: new_refreshed_at.to_s
}
)
redis.expire(cache_key, VALIDITY_TIME)
end
end
end
private
def access
::Gitlab::Utils.to_boolean(@access)
end
def reason
# `nil` if the cached value was an empty string
return nil unless @reason.present?
@reason
end
def refreshed_at
# Don't try to parse a time if there was no cache
return nil unless @refreshed_at.present?
Time.parse(@refreshed_at)
end
def cache_key
"external_authorization:user-#{@user.id}:label-#{@label}"
end
end
end
end
end
module EE
module Gitlab
module ExternalAuthorization
class Client
REQUEST_HEADERS = {
'Content-Type' => 'application/json',
'Accept' => 'application/json'
}.freeze
TIMEOUT = 0.5
def self.build(user, label)
new(
::EE::Gitlab::ExternalAuthorization.service_url,
user,
label
)
end
def initialize(url, user, label)
@url, @user, @label = url, user, label
end
def request_access
response = Excon.post(
@url,
headers: REQUEST_HEADERS,
body: body.to_json,
connect_timeout: TIMEOUT,
read_timeout: TIMEOUT,
write_timeout: TIMEOUT
)
EE::Gitlab::ExternalAuthorization::Response.new(response)
rescue Excon::Error => e
raise EE::Gitlab::ExternalAuthorization::RequestFailed.new(e)
end
private
def body
@body ||= begin
body = {
user_identifier: @user.email,
project_classification_label: @label
}
if @user.ldap_identity
body[:user_ldap_dn] = @user.ldap_identity.extern_uid
end
body
end
end
end
end
end
end
module EE
module Gitlab
module ExternalAuthorization
class Response
include ::Gitlab::Utils::StrongMemoize
def initialize(excon_response)
@excon_response = excon_response
end
def valid?
@excon_response && [200, 401].include?(@excon_response.status)
end
def successful?
valid? && @excon_response.status == 200
end
def reason
parsed_response['reason'] if parsed_response
end
private
def parsed_response
strong_memoize(:parsed_response) { parse_response! }
end
def parse_response!
JSON.parse(@excon_response.body)
rescue JSON::JSONError
# The JSON response is optional, so don't fail when it's missing
nil
end
end
end
end
end
...@@ -47,38 +47,50 @@ describe Admin::ApplicationSettingsController do ...@@ -47,38 +47,50 @@ describe Admin::ApplicationSettingsController do
expect(ApplicationSetting.current.elasticsearch_url).to contain_exactly(settings[:elasticsearch_url]) expect(ApplicationSetting.current.elasticsearch_url).to contain_exactly(settings[:elasticsearch_url])
end end
it 'does not update mirror settings when repository mirrors unlicensed' do shared_examples 'settings for licensed features' do
stub_licensed_features(repository_mirrors: false) it 'does not update settings when licesed feature is not available' do
stub_licensed_features(feature => false)
attribute_names = settings.keys.map(&:to_s)
settings = { expect { put :update, application_setting: settings }
mirror_max_delay: 12, .not_to change { ApplicationSetting.current.reload.attributes.slice(*attribute_names) }
mirror_max_capacity: 2,
mirror_capacity_threshold: 2
}
settings.each do |setting, _value|
expect do
put :update, application_setting: settings
end.not_to change(ApplicationSetting.current.reload, setting)
end end
end
it 'updates mirror settings when repository mirrors is licensed' do it 'updates settings when the feature is available' do
stub_licensed_features(repository_mirrors: true) stub_licensed_features(feature => true)
mirror_delay = (Gitlab::Mirror.min_delay_upper_bound / 60) + 1 put :update, application_setting: settings
settings = { settings.each do |attribute, value|
mirror_max_delay: mirror_delay, expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
mirror_max_capacity: 2, end
mirror_capacity_threshold: 2 end
} end
put :update, application_setting: settings context 'mirror settings' do
let(:settings) do
{
mirror_max_delay: (Gitlab::Mirror.min_delay_upper_bound / 60) + 1,
mirror_max_capacity: 200,
mirror_capacity_threshold: 2
}
end
let(:feature) { :repository_mirrors }
settings.each do |setting, value| it_behaves_like 'settings for licensed features'
expect(ApplicationSetting.current.public_send(setting)).to eq(value) end
context 'external policy classification settings' do
let(:settings) do
{
external_authorization_service_enabled: true,
external_authorization_service_url: 'https://custom.service/',
external_authorization_service_default_label: 'default'
}
end end
let(:feature) { :external_authorization_service }
it_behaves_like 'settings for licensed features'
end end
it 'updates the default_project_creation for string value' do it 'updates the default_project_creation for string value' do
......
require 'spec_helper' require 'spec_helper'
describe Boards::IssuesController do describe Boards::IssuesController do
include ExternalAuthorizationServiceHelpers
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project_1) { create(:project, namespace: group) } let(:project_1) { create(:project, namespace: group) }
let(:project_2) { create(:project, namespace: group) } let(:project_2) { create(:project, namespace: group) }
...@@ -77,6 +79,7 @@ describe Boards::IssuesController do ...@@ -77,6 +79,7 @@ describe Boards::IssuesController do
context 'with unauthorized user' do context 'with unauthorized user' do
before do before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false) allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
end end
...@@ -87,6 +90,26 @@ describe Boards::IssuesController do ...@@ -87,6 +90,26 @@ describe Boards::IssuesController do
end end
end end
context 'with external authorization' do
before do
sign_in(user)
enable_external_authorization_service
end
it 'returns a 404 for group boards' do
get :index, board_id: board
expect(response).to have_gitlab_http_status(404)
end
it 'is successful for project boards' do
project_board = create(:board, project: project_1)
list_issues(user: user, board: project_board)
expect(response).to have_gitlab_http_status(200)
end
end
def list_issues(user:, board:, list: nil) def list_issues(user:, board:, list: nil)
sign_in(user) sign_in(user)
......
require 'spec_helper'
describe Dashboard::GroupsController do
include ExternalAuthorizationServiceHelpers
before do
sign_in create(:user)
end
describe '#index' do
it 'works when the external authorization service is enabled' do
enable_external_authorization_service
get :index
expect(response).to have_gitlab_http_status(200)
end
end
end
require 'spec_helper'
describe Dashboard::LabelsController do
before do
sign_in create(:user)
end
describe '#index' do
subject { get :index, format: :json }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Dashboard::MilestonesController do
before do
sign_in create(:user)
end
describe '#index' do
subject { get :index }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Dashboard::ProjectsController do
include ExternalAuthorizationServiceHelpers
before do
sign_in create(:user)
end
describe '#index' do
subject { get :index }
it 'it works when the external authorization service is enabled' do
enable_external_authorization_service
get :index
expect(response).to have_gitlab_http_status(200)
end
end
end
require 'spec_helper'
describe Dashboard::TodosController do
before do
sign_in create(:user)
end
describe '#index' do
subject { get :index }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe EE::Projects::ApplicationController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
before do
sign_in user
end
render_views
describe '#handle_not_found_or_authorized' do
controller(::Projects::ApplicationController) do
def show
render nothing: true
end
end
let(:project) { create(:project) }
before do
project.add_developer(user)
end
it 'renders a 200 when the service allows access to the project' do
external_service_allow_access(user, project)
get :show, namespace_id: project.namespace.to_param, id: project.to_param
expect(response).to have_gitlab_http_status(200)
end
it 'renders a 404 when the service denies access to the project' do
external_service_deny_access(user, project)
get :show, namespace_id: project.namespace.to_param, id: project.to_param
expect(response).to have_gitlab_http_status(404)
expect(response.body).to match("External authorization denied access to this project")
end
end
end
...@@ -93,4 +93,10 @@ describe Groups::AnalyticsController do ...@@ -93,4 +93,10 @@ describe Groups::AnalyticsController do
expect { get :show, group_id: group.path }.not_to exceed_query_limit(control_count) expect { get :show, group_id: group.path }.not_to exceed_query_limit(control_count)
end end
end end
describe 'GET #index' do
subject { get :show, group_id: group.to_param }
it_behaves_like 'disabled when using an external authorization service'
end
end end
require 'spec_helper'
describe Groups::AvatarsController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
before do
group.add_owner(user)
sign_in(user)
end
it 'works when external authorization service is enabled' do
enable_external_authorization_service
delete :destroy, group_id: group
expect(response).to have_gitlab_http_status(302)
end
end
...@@ -2,9 +2,10 @@ require 'spec_helper' ...@@ -2,9 +2,10 @@ require 'spec_helper'
describe Groups::BoardsController do describe Groups::BoardsController do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
allow(Ability).to receive(:allowed?).and_call_original
group.add_master(user) group.add_master(user)
sign_in(user) sign_in(user)
stub_licensed_features(group_issue_boards: true) stub_licensed_features(group_issue_boards: true)
...@@ -64,6 +65,10 @@ describe Groups::BoardsController do ...@@ -64,6 +65,10 @@ describe Groups::BoardsController do
end end
end end
it_behaves_like 'disabled when using an external authorization service' do
subject { list_boards }
end
def list_boards(format: :html) def list_boards(format: :html)
get :index, group_id: group, format: format get :index, group_id: group, format: format
end end
...@@ -125,6 +130,10 @@ describe Groups::BoardsController do ...@@ -125,6 +130,10 @@ describe Groups::BoardsController do
end end
end end
it_behaves_like 'disabled when using an external authorization service' do
subject { read_board board: board }
end
def read_board(board:, format: :html) def read_board(board:, format: :html)
get :show, group_id: group, get :show, group_id: group,
id: board.to_param, id: board.to_param,
......
require 'spec_helper'
describe Groups::ChildrenController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
before do
group.add_owner(user)
sign_in(user)
end
it 'works when external authorization service is enabled' do
enable_external_authorization_service
get :index, group_id: group, format: :json
expect(response).to have_gitlab_http_status(200)
end
end
...@@ -75,6 +75,10 @@ describe Groups::EpicsController do ...@@ -75,6 +75,10 @@ describe Groups::EpicsController do
expect(assigns(:epics).current_page).to eq(last_page) expect(assigns(:epics).current_page).to eq(last_page)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it_behaves_like 'disabled when using an external authorization service' do
subject { get :index, group_id: group }
end
end end
context 'when format is JSON' do context 'when format is JSON' do
...@@ -133,6 +137,14 @@ describe Groups::EpicsController do ...@@ -133,6 +137,14 @@ describe Groups::EpicsController do
expect(response.content_type).to eq 'text/html' expect(response.content_type).to eq 'text/html'
end end
end end
it_behaves_like 'disabled when using an external authorization service' do
subject { show_epic }
before do
group.add_developer(user)
end
end
end end
context 'when format is JSON' do context 'when format is JSON' do
...@@ -188,6 +200,12 @@ describe Groups::EpicsController do ...@@ -188,6 +200,12 @@ describe Groups::EpicsController do
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end
it_behaves_like 'disabled when using an external authorization service' do
before do
group.add_developer(user)
end
end
end end
describe '#create' do describe '#create' do
...@@ -216,6 +234,8 @@ describe Groups::EpicsController do ...@@ -216,6 +234,8 @@ describe Groups::EpicsController do
expect(JSON.parse(response.body)).to eq({ 'web_url' => group_epic_path(group, Epic.last) }) expect(JSON.parse(response.body)).to eq({ 'web_url' => group_epic_path(group, Epic.last) })
end end
it_behaves_like 'disabled when using an external authorization service'
end end
context 'when required parameter is missing' do context 'when required parameter is missing' do
......
require 'spec_helper'
describe Groups::GroupMembersController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
let(:membership) { create(:group_member, group: group) }
before do
group.add_owner(user)
sign_in(user)
end
context 'with external authorization enabled' do
before do
enable_external_authorization_service
end
describe 'GET #index' do
it 'is successful' do
get :index, group_id: group
expect(response).to have_gitlab_http_status(200)
end
end
describe 'POST #create' do
it 'is successful' do
post :create, group_id: group, users: user, access_level: Gitlab::Access::GUEST
expect(response).to have_gitlab_http_status(302)
end
end
describe 'PUT #update' do
it 'is successful' do
put :update,
group_member: { access_level: Gitlab::Access::GUEST },
group_id: group,
id: membership,
format: :js
expect(response).to have_gitlab_http_status(200)
end
end
describe 'DELETE #destroy' do
it 'is successful' do
delete :destroy, group_id: group, id: membership
expect(response).to have_gitlab_http_status(302)
end
end
describe 'POST #destroy' do
it 'is successful' do
sign_in(create(:user))
post :request_access, group_id: group
expect(response).to have_gitlab_http_status(302)
end
end
describe 'POST #approve_request_access' do
it 'is successful' do
access_request = create(:group_member, :access_request, group: group)
post :approve_access_request, group_id: group, id: access_request
expect(response).to have_gitlab_http_status(302)
end
end
describe 'DELETE #leave' do
it 'is successful' do
group.add_owner(create(:user))
delete :leave, group_id: group
expect(response).to have_gitlab_http_status(302)
end
end
describe 'POST #resend_invite' do
it 'is successful' do
post :resend_invite, group_id: group, id: membership
expect(response).to have_gitlab_http_status(302)
end
end
describe 'POST #override' do
let(:group) { create(:group_with_ldap_group_link) }
it 'is successful' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :override_group_member, membership) { true }
post :override,
group_id: group,
id: membership,
group_member: { override: true },
format: :js
expect(response).to have_gitlab_http_status(200)
end
end
end
end
require 'spec_helper'
describe GroupsController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
before do
group.add_owner(user)
sign_in(user)
end
context 'with external authorization service enabled' do
before do
enable_external_authorization_service
end
describe 'GET #show' do
it 'is successful' do
get :show, id: group.to_param
expect(response).to have_gitlab_http_status(200)
end
it 'does not allow other formats' do
get :show, id: group.to_param, format: :atom
expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET #edit' do
it 'is successful' do
get :edit, id: group.to_param
expect(response).to have_gitlab_http_status(200)
end
end
describe 'GET #new' do
it 'is successful' do
get :new
expect(response).to have_gitlab_http_status(200)
end
end
describe 'GET #index' do
it 'is successful' do
get :index
# Redirects to the dashboard
expect(response).to have_gitlab_http_status(302)
end
end
describe 'POST #create' do
it 'creates a group' do
expect do
post :create, group: { name: 'a name', path: 'a-name' }
end.to change { Group.count }.by(1)
end
end
describe 'PUT #update' do
it 'updates a group' do
expect do
put :update, id: group.to_param, group: { name: 'world' }
end.to change { group.reload.name }
end
end
describe 'DELETE #destroy' do
it 'deletes the group' do
delete :destroy, id: group.to_param
expect(response).to have_gitlab_http_status(302)
end
end
end
describe 'GET #activity' do
subject { get :activity, id: group.to_param }
it_behaves_like 'disabled when using an external authorization service'
end
describe 'GET #issues' do
subject { get :issues, id: group.to_param }
it_behaves_like 'disabled when using an external authorization service'
end
describe 'GET #merge_requests' do
subject { get :merge_requests, id: group.to_param }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Groups::LabelsController do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_owner(user)
sign_in(user)
end
describe 'GET #index' do
subject { get :index, group_id: group.to_param }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Groups::MilestonesController do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_owner(user)
sign_in(user)
end
describe 'GET #index' do
subject { get :index, group_id: group.to_param }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Groups::Settings::CiCdController do
include ExternalAuthorizationServiceHelpers
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_master(user)
sign_in(user)
end
context 'with external authorization enabled' do
before do
enable_external_authorization_service
end
describe 'GET #show' do
it 'renders show with 200 status code' do
get :show, group_id: group
expect(response).to have_gitlab_http_status(200)
end
end
end
end
require 'spec_helper'
describe Groups::VariablesController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:variable) { create(:ci_group_variable, group: group) }
before do
group.add_owner(user)
sign_in(user)
end
context 'with external authorization enabled' do
before do
enable_external_authorization_service
end
describe 'GET #show' do
let!(:variable) { create(:ci_group_variable, group: group) }
it 'is successful' do
get :show, group_id: group, format: :json
expect(response).to have_gitlab_http_status(200)
end
end
describe 'PATCH #update' do
let!(:variable) { create(:ci_group_variable, group: group) }
let(:owner) { group }
it 'is successful' do
patch :update,
group_id: group,
variables_attributes: [{ id: variable.id, key: 'hello' }],
format: :json
expect(response).to have_gitlab_http_status(200)
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Projects::BoardsController do describe Projects::BoardsController do
include Rails.application.routes.url_helpers
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
project.add_master(user) project.add_master(user)
allow(Ability).to receive(:allowed?).and_call_original
sign_in(user) sign_in(user)
end end
...@@ -22,6 +25,10 @@ describe Projects::BoardsController do ...@@ -22,6 +25,10 @@ describe Projects::BoardsController do
expect(parsed_response.length).to eq 2 expect(parsed_response.length).to eq 2
end end
it_behaves_like 'unauthorized when external service denies access' do
subject { list_boards }
end
def list_boards(format: :html) def list_boards(format: :html)
get :index, namespace_id: project.namespace, get :index, namespace_id: project.namespace,
project_id: project, project_id: project,
......
require('spec_helper') require('spec_helper')
describe Projects::IssuesController do describe Projects::IssuesController do
include Rails.application.routes.url_helpers
let(:namespace) { create(:group, :public) } let(:namespace) { create(:group, :public) }
let(:project) { create(:project_empty_repo, :public, namespace: namespace) } let(:project) { create(:project_empty_repo, :public, namespace: namespace) }
let(:user) { create(:user) }
describe 'GET #index' do
before do
sign_in user
project.add_developer(user)
end
it_behaves_like 'unauthorized when external service denies access' do
subject { get :index, namespace_id: project.namespace, project_id: project }
end
end
describe 'POST export_csv' do describe 'POST export_csv' do
let(:user) { create(:user) }
let(:viewer) { user } let(:viewer) { user }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:globally_licensed) { false } let(:globally_licensed) { false }
......
require 'spec_helper' require 'spec_helper'
describe ProjectsController do describe ProjectsController do
include ExternalAuthorizationServiceHelpers
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -42,7 +44,7 @@ describe ProjectsController do ...@@ -42,7 +44,7 @@ describe ProjectsController do
stub_licensed_features(repository_mirrors: false) stub_licensed_features(repository_mirrors: false)
end end
it 'has mirror enabled in new project' do it 'has mirror disabled in new project' do
post :create, project: params post :create, project: params
created_project = Project.find_by_path('foo') created_project = Project.find_by_path('foo')
...@@ -53,10 +55,6 @@ describe ProjectsController do ...@@ -53,10 +55,6 @@ describe ProjectsController do
end end
describe 'PUT #update' do describe 'PUT #update' do
before do
controller.instance_variable_set(:@project, project)
end
it 'updates EE attributes' do it 'updates EE attributes' do
params = { params = {
repository_size_limit: 1024 repository_size_limit: 1024
...@@ -64,8 +62,9 @@ describe ProjectsController do ...@@ -64,8 +62,9 @@ describe ProjectsController do
put :update, put :update,
namespace_id: project.namespace, namespace_id: project.namespace,
id: project.id, id: project,
project: params project: params
project.reload
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(302)
params.except(:repository_size_limit).each do |param, value| params.except(:repository_size_limit).each do |param, value|
...@@ -84,8 +83,9 @@ describe ProjectsController do ...@@ -84,8 +83,9 @@ describe ProjectsController do
put :update, put :update,
namespace_id: project.namespace, namespace_id: project.namespace,
id: project.id, id: project,
project: params project: params
project.reload
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(302)
expect(project.approver_groups.pluck(:group_id)).to contain_exactly(params[:approver_group_ids]) expect(project.approver_groups.pluck(:group_id)).to contain_exactly(params[:approver_group_ids])
...@@ -100,8 +100,9 @@ describe ProjectsController do ...@@ -100,8 +100,9 @@ describe ProjectsController do
put :update, put :update,
namespace_id: project.namespace, namespace_id: project.namespace,
id: project.id, id: project,
project: params project: params
project.reload
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(302)
params.each do |param, value| params.each do |param, value|
...@@ -119,8 +120,9 @@ describe ProjectsController do ...@@ -119,8 +120,9 @@ describe ProjectsController do
put :update, put :update,
namespace_id: project.namespace, namespace_id: project.namespace,
id: project.id, id: project,
project: params project: params
project.reload
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(302)
expect(project.service_desk_enabled).to eq(true) expect(project.service_desk_enabled).to eq(true)
...@@ -131,7 +133,8 @@ describe ProjectsController do ...@@ -131,7 +133,8 @@ describe ProjectsController do
{ {
mirror: true, mirror: true,
mirror_trigger_builds: true, mirror_trigger_builds: true,
mirror_user_id: user.id mirror_user_id: user.id,
import_url: 'https://example.com'
} }
end end
...@@ -143,12 +146,14 @@ describe ProjectsController do ...@@ -143,12 +146,14 @@ describe ProjectsController do
it 'updates repository mirror attributes' do it 'updates repository mirror attributes' do
put :update, put :update,
namespace_id: project.namespace, namespace_id: project.namespace,
id: project.id, id: project,
project: params project: params
project.reload
params.each do |param, value| expect(project.mirror).to eq(true)
expect(project.public_send(param)).to eq(value) expect(project.mirror_trigger_builds).to eq(true)
end expect(project.mirror_user).to eq(user)
expect(project.import_url).to eq('https://example.com')
end end
end end
...@@ -162,12 +167,59 @@ describe ProjectsController do ...@@ -162,12 +167,59 @@ describe ProjectsController do
expect do expect do
put :update, put :update,
namespace_id: project.namespace, namespace_id: project.namespace,
id: project.id, id: project,
project: params project: params
project.reload
end.not_to change(project, param) end.not_to change(project, param)
end end
end end
end end
end end
context 'external authaurization service attributes' do
def update_classification_label
put :update,
namespace_id: project.namespace,
id: project,
project: { external_authorization_classification_label: 'new_label' }
project.reload
end
it 'updates the project classification label' do
external_service_allow_access(user, project)
expect { update_classification_label }
.to change(project, :external_authorization_classification_label).to('new_label')
end
it 'does not update the project classification label when the feature is not available' do
stub_licensed_features(external_authorization_service: false)
expect { update_classification_label }
.not_to change(project, :external_authorization_classification_label)
end
end
it_behaves_like 'unauthorized when external service denies access' do
subject do
put :update,
namespace_id: project.namespace,
id: project,
project: { description: 'Hello world' }
project.reload
end
it 'updates when the service allows access' do
external_service_allow_access(user, project)
expect { subject }.to change(project, :description)
end
it 'does not update when the service rejects access' do
external_service_deny_access(user, project)
expect { subject }.not_to change(project, :description)
end
end
end end
end end
require 'spec_helper'
describe SearchController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:note) { create(:note_on_issue, project: project) }
before do
sign_in(user)
end
context 'with external authorization service enabled' do
before do
enable_external_authorization_service
end
describe 'GET #show' do
it 'renders a 404 when no project is given' do
get :show, scope: 'notes', search: note.note
expect(response).to have_gitlab_http_status(404)
end
it 'renders a 200 when a project was set' do
get :show, project_id: project.id, scope: 'notes', search: note.note
expect(response).to have_gitlab_http_status(200)
end
end
describe 'GET #autocomplete' do
it 'renders a 404 when no project is given' do
get :autocomplete, term: 'hello'
expect(response).to have_gitlab_http_status(404)
end
it 'renders a 200 when a project was set' do
get :autocomplete, project_id: project.id, term: 'hello'
expect(response).to have_gitlab_http_status(200)
end
end
end
end
require 'spec_helper'
describe UsersController do
let(:user) { create(:user) }
before do
sign_in(user)
end
describe 'GET #snippets' do
subject { get :snippets, username: user.username }
it_behaves_like 'disabled when using an external authorization service'
end
describe 'GET #calendar_activities' do
subject { get :calendar_activities, username: user.username }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
feature 'The group dashboard' do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
before do
sign_in user
end
describe 'The top navigation' do
it 'has all the expected links' do
visit dashboard_groups_path
within('.navbar') do
expect(page).to have_link('Projects')
expect(page).to have_link('Groups')
expect(page).to have_link('Activity')
expect(page).to have_link('Milestones')
expect(page).to have_link('Snippets')
end
end
it 'hides some links when an external authorization service is enabled' do
enable_external_authorization_service
visit dashboard_groups_path
within('.navbar') do
expect(page).to have_link('Projects')
expect(page).to have_link('Groups')
expect(page).not_to have_link('Activity')
expect(page).not_to have_link('Milestones')
expect(page).to have_link('Snippets')
end
end
end
end
require 'spec_helper'
feature 'The group page' do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
before do
sign_in user
group.add_owner(user)
end
describe 'The sidebar' do
it 'has all the expected links' do
visit group_path(group)
within('.nav-sidebar') do
expect(page).to have_link('Overview')
expect(page).to have_link('Details')
expect(page).to have_link('Activity')
expect(page).to have_link('Contribution Analytics')
expect(page).to have_link('Issues')
expect(page).to have_link('Merge Requests')
expect(page).to have_link('Members')
end
end
it 'hides some links when an external authorization service is enabled' do
enable_external_authorization_service
visit group_path(group)
within('.nav-sidebar') do
expect(page).to have_link('Overview')
expect(page).to have_link('Details')
expect(page).not_to have_link('Activity')
expect(page).not_to have_link('Contribution Analytics')
expect(page).not_to have_link('Issues')
expect(page).not_to have_link('Merge Requests')
expect(page).to have_link('Members')
end
end
context 'when epics are available' do
before do
stub_licensed_features(epics: true)
end
it 'shows the link to epics' do
visit group_path(group)
within('.nav-sidebar') do
expect(page).to have_link('Epics')
end
end
it 'hides the epics link when an external authorization service is enabled' do
enable_external_authorization_service
visit group_path(group)
within('.nav-sidebar') do
expect(page).not_to have_link('Epics')
end
end
end
end
end
require 'spec_helper'
describe 'Classification label on project pages' do
include ExternalAuthorizationServiceHelpers
let(:project) do
create(:project, external_authorization_classification_label: 'authorized label')
end
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
end
it 'shows the classification label on the project page when the feature is enabled' do
external_service_allow_access(user, project)
visit project_path(project)
expect(page).to have_content('authorized label')
end
it 'does not show the classification label when the feature is not enabled' do
stub_licensed_features(external_authorization_service: false)
visit project_path(project)
expect(page).not_to have_content('authorized label')
end
end
require 'spec_helper'
describe 'listing forks of a project' do
include ProjectForksHelper
include ExternalAuthorizationServiceHelpers
let(:source) { create(:project, :public, :repository) }
let!(:fork) { fork_project(source, nil, repository: true) }
let(:user) { create(:user) }
before do
source.add_master(user)
sign_in(user)
end
it 'shows the forked project in the list with commit as description' do
visit project_forks_path(source)
page.within('li.project-row') do
expect(page).to have_content(fork.full_name)
expect(page).to have_css('a.commit-row-message')
end
end
it 'does not show the commit message when an external authorization service is used' do
enable_external_authorization_service
visit project_forks_path(source)
page.within('li.project-row') do
expect(page).to have_content(fork.full_name)
expect(page).not_to have_css('a.commit-row-message')
end
end
end
require 'spec_helper'
describe 'viewing an issue with cross project references' do
include ExternalAuthorizationServiceHelpers
include Gitlab::Routing.url_helpers
let(:user) { create(:user) }
let(:other_project) do
create(:project, :public,
external_authorization_classification_label: 'other_label')
end
let(:other_issue) do
create(:issue, :closed,
title: 'I am in another project',
project: other_project)
end
let(:other_confidential_issue) do
create(:issue, :confidential, :closed,
title: 'I am in another project and confidential',
project: other_project)
end
let(:description_referencing_other_issue) do
"Referencing: #{other_issue.to_reference(project)}, and "\
"a confidential issue #{confidential_issue.to_reference}"\
"a cross project confidential issue #{other_confidential_issue.to_reference(project)}"
end
let(:project) { create(:project) }
let(:issue) do
create(:issue,
project: project,
description: description_referencing_other_issue )
end
let(:confidential_issue) do
create(:issue, :confidential, :closed,
title: "I am in the same project and confidential",
project: project)
end
before do
project.add_developer(user)
sign_in(user)
end
it 'shows all information related to the cross project reference' do
visit project_issue_path(project, issue)
expect(page).to have_link("#{other_issue.to_reference(project)} (#{other_issue.state})")
expect(page).to have_xpath("//a[@title='#{other_issue.title}']")
end
it 'shows a link to the confidential issue in the same project' do
visit project_issue_path(project, issue)
expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})")
expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']")
end
it 'does not show the link to a cross project confidential issue when the user does not have access' do
visit project_issue_path(project, issue)
expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})")
expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
end
it 'shows the link to a cross project confidential issue when the user has access' do
other_project.add_developer(user)
visit project_issue_path(project, issue)
expect(page).to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})")
expect(page).to have_xpath("//a[@title='#{other_confidential_issue.title}']")
end
context 'when an external authorization service is enabled' do
before do
enable_external_authorization_service
end
it 'only hits the external service for the project the user is viewing' do
expect(EE::Gitlab::ExternalAuthorization)
.to receive(:access_allowed?).with(user, 'default_label').at_least(1).and_return(true)
visit project_issue_path(project, issue)
end
it 'shows only the link to the cross project reference' do
visit project_issue_path(project, issue)
expect(page).to have_link("#{other_issue.to_reference(project)}")
expect(page).not_to have_content("#{other_issue.to_reference(project)} (#{other_issue.state})")
expect(page).not_to have_xpath("//a[@title='#{other_issue.title}']")
end
it 'does not link a cross project confidential issue if the user does not have access' do
visit project_issue_path(project, issue)
expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)}")
expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
end
it 'links a cross project confidential issue without exposing information when the user has access' do
other_project.add_developer(user)
visit project_issue_path(project, issue)
expect(page).to have_link("#{other_confidential_issue.to_reference(project)}")
expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
end
it 'shows a link to the confidential issue in the same project' do
visit project_issue_path(project, issue)
expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})")
expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']")
end
end
end
require 'spec_helper'
describe 'Project settings > [EE] repository' do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:project) { create(:project_empty_repo) }
before do
project.add_master(user)
sign_in(user)
end
it 'shows the field to set a classification label when the feature is enabled' do
external_service_allow_access(user, project)
visit edit_project_path(project)
expect(page).to have_selector('#project_external_authorization_classification_label')
end
it 'shows the field to set a classification label when the feature is unavailable' do
stub_licensed_features(external_authorization_service: false)
visit edit_project_path(project)
expect(page).not_to have_selector('#project_external_authorization_classification_label')
end
end
require 'spec_helper'
describe 'User page' do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
it 'shows the most recent activity' do
visit(user_path(user))
expect(page).to have_content('Most Recent Activity')
end
describe 'when external authorization is enabled' do
before do
enable_external_authorization_service
end
it 'hides the most recent activity' do
visit(user_path(user))
expect(page).not_to have_content('Most Recent Activity')
end
end
end
require 'spec_helper'
describe IssuesFinder do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:issue, project: project) }
let(:project_params) { { project_id: project.id } }
end
end
require 'spec_helper'
describe LabelsFinder do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:label, project: project) }
let(:project_params) { { project_id: project.id } }
end
end
require 'spec_helper'
describe MergeRequestsFinder do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:merge_request, source_project: project) }
let(:project_params) { { project_id: project.id } }
end
end
require 'spec_helper'
describe SnippetsFinder do
include ExternalAuthorizationServiceHelpers
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:project_snippet, project: project) }
let(:project_params) { { project: project } }
end
context 'external authorization service enabled' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:snippet) { create(:project_snippet, :public, project: project) }
before do
project.add_master(user)
end
it 'includes the result if the external service allows access' do
external_service_allow_access(user, project)
results = described_class.new(user, project: project).execute
expect(results).to contain_exactly(snippet)
end
it 'does not include any results if the external service denies access' do
external_service_deny_access(user, project)
results = described_class.new(user, project: project).execute
expect(results).to be_empty
end
end
end
require 'spec_helper'
describe TodosFinder do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:todo, project: project, user: user) }
let(:project_params) { { project_id: project.id } }
end
end
require 'spec_helper'
describe GroupsHelper do
describe '#group_sidebar_links' do
let(:user) { create(:user) }
let(:group) { create(:group) }
before do
allow(helper).to receive(:current_user) { user }
helper.instance_variable_set(:@group, group)
allow(helper).to receive(:can?).with(user, :admin_group, group) { false }
end
it 'shows the licenced cross project features when the user can read cross project' do
expect(helper).to receive(:can?).with(user, :read_cross_project).at_least(1) { true }
stub_licensed_features(contribution_analytics: true,
group_issue_boards: true,
epics: true)
expect(helper.group_sidebar_links).to include(:contribution_analytics, :boards, :epics)
end
end
end
require 'spec_helper'
describe EE::Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
subject(:access) { described_class.new(build(:user), 'dummy_label') }
describe '#loaded?' do
it 'is `true` when it was loaded recently' do
Timecop.freeze do
allow(access).to receive(:loaded_at).and_return(5.minutes.ago)
expect(access).to be_loaded
end
end
it 'is `false` when there is no loading time' do
expect(access).not_to be_loaded
end
it 'is `false` when there the result was loaded a long time ago' do
Timecop.freeze do
allow(access).to receive(:loaded_at).and_return(2.weeks.ago)
expect(access).not_to be_loaded
end
end
end
describe 'load!' do
let(:fake_client) { double('ExternalAuthorization::Client') }
let(:fake_response) do
double(
'Response',
'successful?' => true,
'valid?' => true,
'reason' => nil
)
end
before do
allow(access).to receive(:load_from_cache)
allow(fake_client).to receive(:request_access).and_return(fake_response)
allow(EE::Gitlab::ExternalAuthorization::Client).to receive(:build) { fake_client }
end
context 'when loading from the webservice' do
it 'loads from the webservice it the cache was empty' do
expect(access).to receive(:load_from_cache)
expect(access).to receive(:load_from_service).and_call_original
access.load!
expect(access).to be_loaded
end
it 'assigns the accessibility, reason and loaded_at' do
allow(fake_response).to receive(:successful?).and_return(false)
allow(fake_response).to receive(:reason).and_return('Inaccessible label')
access.load!
expect(access.reason).to eq('Inaccessible label')
expect(access).not_to have_access
expect(access.loaded_at).not_to be_nil
end
it 'returns itself' do
expect(access.load!).to eq(access)
end
it 'it stores the result in redis' do
Timecop.freeze do
fake_cache = double
expect(fake_cache).to receive(:store).with(true, nil, Time.now)
expect(access).to receive(:cache).and_return(fake_cache)
access.load!
end
end
context 'when the request fails' do
before do
allow(fake_client).to receive(:request_access) do
raise EE::Gitlab::ExternalAuthorization::RequestFailed.new('Service unavailable')
end
end
it 'is loaded' do
access.load!
expect(access).to be_loaded
end
it 'assigns the correct accessibility, reason and loaded_at' do
access.load!
expect(access.reason).to eq('Service unavailable')
expect(access).not_to have_access
expect(access.loaded_at).not_to be_nil
end
it 'it does not store the result in redis' do
fake_cache = double
expect(fake_cache).not_to receive(:store)
allow(access).to receive(:cache).and_return(fake_cache)
access.load!
end
end
end
context 'When loading from cache' do
let(:fake_cache) { double('ExternalAuthorization::Cache') }
before do
allow(access).to receive(:cache).and_return(fake_cache)
end
it 'does not load from the webservice' do
Timecop.freeze do
expect(fake_cache).to receive(:load).and_return([true, nil, Time.now])
expect(access).to receive(:load_from_cache).and_call_original
expect(access).not_to receive(:load_from_service)
access.load!
end
end
it 'loads from the webservice when the cached result was too old' do
Timecop.freeze do
expect(fake_cache).to receive(:load).and_return([true, nil, 2.days.ago])
expect(access).to receive(:load_from_cache).and_call_original
expect(access).to receive(:load_from_service).and_call_original
allow(fake_cache).to receive(:store)
access.load!
end
end
end
end
end
require 'spec_helper'
describe EE::Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do
let(:user) { build_stubbed(:user) }
let(:cache_key) { "external_authorization:user-#{user.id}:label-dummy_label" }
subject(:cache) { described_class.new(user, 'dummy_label') }
def read_from_redis(key)
Gitlab::Redis::Cache.with do |redis|
redis.hget(cache_key, key)
end
end
def set_in_redis(key, value)
Gitlab::Redis::Cache.with do |redis|
redis.hmset(cache_key, key, value)
end
end
describe '#load' do
it 'reads stored info from redis' do
Timecop.freeze do
set_in_redis(:access, false)
set_in_redis(:reason, 'Access denied for now')
set_in_redis(:refreshed_at, Time.now)
access, reason, refreshed_at = cache.load
expect(access).to eq(false)
expect(reason).to eq('Access denied for now')
expect(refreshed_at).to be_within(1.second).of(Time.now)
end
end
end
describe '#store' do
it 'sets the values in redis' do
Timecop.freeze do
cache.store(true, 'the reason', Time.now)
expect(read_from_redis(:access)).to eq('true')
expect(read_from_redis(:reason)).to eq('the reason')
expect(read_from_redis(:refreshed_at)).to eq(Time.now.to_s)
end
end
end
end
require 'spec_helper'
describe EE::Gitlab::ExternalAuthorization::Client do
let(:user) { build(:user, email: 'dummy_user@example.com') }
let(:dummy_url) { 'https://dummy.net/' }
subject(:client) { described_class.build(user, 'dummy_label') }
before do
stub_application_setting(external_authorization_service_url: dummy_url)
end
describe '#request_access' do
it 'performs requests to the configured endpoint' do
expect(Excon).to receive(:post).with(dummy_url, any_args)
client.request_access
end
it 'adds the correct params for the user to the body of the request' do
expected_body = {
user_identifier: 'dummy_user@example.com',
project_classification_label: 'dummy_label'
}.to_json
expect(Excon).to receive(:post)
.with(dummy_url, hash_including(body: expected_body))
client.request_access
end
it 'returns an expected response' do
expect(Excon).to receive(:post)
expect(client.request_access)
.to be_kind_of(::EE::Gitlab::ExternalAuthorization::Response)
end
it 'wraps exceptions if the request fails' do
expect(Excon).to receive(:post) { raise Excon::Error.new('the request broke') }
expect { client.request_access }
.to raise_error(EE::Gitlab::ExternalAuthorization::RequestFailed)
end
describe 'for ldap users' do
let(:user) do
create(:omniauth_user,
email: 'dummy_user@example.com',
extern_uid: 'external id',
provider: 'ldapprovider')
end
it 'includes the ldap dn for ldap users' do
expected_body = {
user_identifier: 'dummy_user@example.com',
project_classification_label: 'dummy_label',
user_ldap_dn: 'external id'
}.to_json
expect(Excon).to receive(:post)
.with(dummy_url, hash_including(body: expected_body))
client.request_access
end
end
end
end
require 'spec_helper'
describe EE::Gitlab::ExternalAuthorization::Response do
let(:excon_response) { double }
subject(:response) { described_class.new(excon_response) }
describe '#valid?' do
it 'is valid for 200 & 401 responses' do
[200, 401].each do |status|
expect(excon_response).to receive(:status).and_return(status)
expect(response).to be_valid
end
end
it "is invalid for other statuses" do
expect(excon_response).to receive(:status).and_return(500)
expect(response).not_to be_valid
end
end
describe '#reason' do
it 'returns a reason if it was included in the response body' do
expect(excon_response).to receive(:body).and_return({ reason: 'Not authorized' }.to_json)
expect(response.reason).to eq('Not authorized')
end
it 'returns nil when there was no body' do
expect(excon_response).to receive(:body).and_return('')
expect(response.reason).to eq(nil)
end
end
describe '#successful?' do
it 'is `true` if the status is 200' do
allow(excon_response).to receive(:status).and_return(200)
expect(response).to be_successful
end
end
end
require 'spec_helper'
describe EE::Gitlab::ExternalAuthorization, :request_store do
include ExternalAuthorizationServiceHelpers
let(:user) { build(:user) }
let(:label) { 'dummy_label' }
describe '#access_allowed?' do
it 'is always true when the feature is disabled' do
# Not using `stub_application_setting` because the method is prepended in
# `EE::ApplicationSetting` which breaks when using `any_instance`
# https://gitlab.com/gitlab-org/gitlab-ce/issues/33587
expect(::Gitlab::CurrentSettings.current_application_settings)
.to receive(:external_authorization_service_enabled?) { false }
expect(described_class).not_to receive(:access_for_user_to_label)
expect(described_class.access_allowed?(user, label)).to be_truthy
end
end
describe '#rejection_reason' do
it 'is alwaus nil when the feature is disabled' do
expect(::Gitlab::CurrentSettings.current_application_settings)
.to receive(:external_authorization_service_enabled?) { false }
expect(described_class).not_to receive(:access_for_user_to_label)
expect(described_class.rejection_reason(user, label)).to be_nil
end
end
describe '#access_for_user_to_label' do
it 'only loads the access once per request' do
enable_external_authorization_service
expect(EE::Gitlab::ExternalAuthorization::Access)
.to receive(:new).with(user, label).once.and_call_original
2.times { described_class.access_for_user_to_label(user, label) }
end
end
end
require 'spec_helper' require 'spec_helper'
describe ApplicationSetting do describe ApplicationSetting do
let(:setting) { described_class.create_from_defaults } subject(:setting) { described_class.create_from_defaults }
describe 'validations' do describe 'validations' do
it { is_expected.to allow_value(100).for(:mirror_max_delay) } it { is_expected.to allow_value(100).for(:mirror_max_delay) }
...@@ -23,6 +23,18 @@ describe ApplicationSetting do ...@@ -23,6 +23,18 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(1.0).for(:mirror_capacity_threshold) } it { is_expected.not_to allow_value(1.0).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(-1).for(:mirror_capacity_threshold) } it { is_expected.not_to allow_value(-1).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(subject.mirror_max_capacity + 1).for(:mirror_capacity_threshold) } it { is_expected.not_to allow_value(subject.mirror_max_capacity + 1).for(:mirror_capacity_threshold) }
describe 'when external authorization service is enabled' do
before do
stub_licensed_features(external_authorization_service: true)
setting.external_authorization_service_enabled = true
end
it { is_expected.not_to allow_value(nil).for(:external_authorization_service_url) }
it { is_expected.not_to allow_value('not a URL').for(:external_authorization_service_url) }
it { is_expected.to allow_value('https://example.com').for(:external_authorization_service_url) }
it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) }
end
end end
describe '#should_check_namespace_plan?' do describe '#should_check_namespace_plan?' do
......
...@@ -44,4 +44,9 @@ describe Issue, elastic: true do ...@@ -44,4 +44,9 @@ describe Issue, elastic: true do
expect(issue.as_indexed_json).to eq(expected_hash) expect(issue.as_indexed_json).to eq(expected_hash)
end end
it_behaves_like 'no results when the user cannot read cross project' do
let(:record1) { create(:issue, project: project, title: 'test-issue') }
let(:record2) { create(:issue, project: project2, title: 'test-issue') }
end
end end
...@@ -54,4 +54,9 @@ describe MergeRequest, elastic: true do ...@@ -54,4 +54,9 @@ describe MergeRequest, elastic: true do
expect(merge_request.as_indexed_json).to eq(expected_hash) expect(merge_request.as_indexed_json).to eq(expected_hash)
end end
it_behaves_like 'no results when the user cannot read cross project' do
let(:record1) { create(:merge_request, source_project: project, title: 'test-mr') }
let(:record2) { create(:merge_request, source_project: project2, title: 'test-mr') }
end
end end
...@@ -45,4 +45,9 @@ describe Milestone, elastic: true do ...@@ -45,4 +45,9 @@ describe Milestone, elastic: true do
expect(milestone.as_indexed_json).to eq(expected_hash) expect(milestone.as_indexed_json).to eq(expected_hash)
end end
it_behaves_like 'no results when the user cannot read cross project' do
let(:record1) { create(:milestone, project: project, title: 'test-milestone') }
let(:record2) { create(:milestone, project: project2, title: 'test-milestone') }
end
end end
...@@ -155,6 +155,13 @@ describe Note, elastic: true do ...@@ -155,6 +155,13 @@ describe Note, elastic: true do
end end
end end
it_behaves_like 'no results when the user cannot read cross project' do
let(:issue1) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project2) }
let(:record1) { create :note, note: 'test-note', project: issue1.project, noteable: issue1 }
let(:record2) { create :note, note: 'test-note', project: issue2.project, noteable: issue2 }
end
def create_notes_for(issue, note) def create_notes_for(issue, note)
create :note, note: note, project: issue.project, noteable: issue create :note, note: note, project: issue.project, noteable: issue
create :note, project: issue.project, noteable: issue create :note, project: issue.project, noteable: issue
......
...@@ -80,6 +80,22 @@ describe Snippet, elastic: true do ...@@ -80,6 +80,22 @@ describe Snippet, elastic: true do
expect(result.records).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet] expect(result.records).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
end end
end end
describe 'when the user cannot read cross project' do
before do
allow(Ability).to receive(:allowed?).and_call_original
end
it 'returns public, internal snippets, but not project snippets' do
member = create(:user)
project.add_developer(member)
expect(Ability).to receive(:allowed?).with(member, :read_cross_project) { false }
result = described_class.elastic_search_code('password', options: { user: member })
expect(result.records).to match_array [public_snippet, internal_snippet]
end
end
end end
it 'searches snippets by title and file_name' do it 'searches snippets by title and file_name' do
......
require 'spec_helper' require 'spec_helper'
describe EE::ProtectedRefAccess do describe EE::ProtectedRefAccess do
include ExternalAuthorizationServiceHelpers
included_in_classes = [ProtectedBranch::MergeAccessLevel, included_in_classes = [ProtectedBranch::MergeAccessLevel,
ProtectedBranch::PushAccessLevel, ProtectedBranch::PushAccessLevel,
ProtectedTag::CreateAccessLevel] ProtectedTag::CreateAccessLevel]
...@@ -71,4 +72,20 @@ describe EE::ProtectedRefAccess do ...@@ -71,4 +72,20 @@ describe EE::ProtectedRefAccess do
end end
end end
end end
describe '#check_access' do
subject(:protected_ref_access) do
create(:protected_branch, :masters_can_push).push_access_levels.first
end
let(:project) { protected_ref_access.project }
it 'is false if external authorization denies access' do
master = create(:user)
project.add_master(master)
external_service_deny_access(master, project)
expect(protected_ref_access.check_access(master)).to be_falsey
end
end
end end
...@@ -2,6 +2,7 @@ require 'spec_helper' ...@@ -2,6 +2,7 @@ require 'spec_helper'
describe Issue do describe Issue do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
include ExternalAuthorizationServiceHelpers
describe '#allows_multiple_assignees?' do describe '#allows_multiple_assignees?' do
it 'does not allow multiple assignees without license' do it 'does not allow multiple assignees without license' do
...@@ -41,4 +42,48 @@ describe Issue do ...@@ -41,4 +42,48 @@ describe Issue do
it { is_expected.to eq(expected) } it { is_expected.to eq(expected) }
end end
end end
context 'when an external authentication service' do
before do
enable_external_authorization_service
end
describe '#publicly_visible?' do
it 'is `false` when an external authorization service is enabled' do
issue = build(:issue, project: build(:project, :public))
expect(issue).not_to be_publicly_visible
end
end
describe '#readable_by?' do
it 'checks the external service to determine if an issue is readable by a user' do
project = build(:project, :public,
external_authorization_classification_label: 'a-label')
issue = build(:issue, project: project)
user = build(:user)
expect(EE::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
expect(issue.readable_by?(user)).to be_falsy
end
it 'does not check the external webservice for admins' do
issue = build(:issue)
user = build(:admin)
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
issue.readable_by?(user)
end
it 'does not check the external webservice for auditors' do
issue = build(:issue)
user = build(:auditor)
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
issue.readable_by?(user)
end
end
end
end end
require 'spec_helper' require 'spec_helper'
describe Project do describe Project do
include ExternalAuthorizationServiceHelpers
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
describe 'associations' do describe 'associations' do
...@@ -1155,4 +1156,44 @@ describe Project do ...@@ -1155,4 +1156,44 @@ describe Project do
expect(projects).not_to include(project2) expect(projects).not_to include(project2)
end end
end end
describe '#external_authorization_classification_label' do
it 'falls back to the default when none is configured' do
enable_external_authorization_service
expect(build(:project).external_authorization_classification_label)
.to eq('default_label')
end
it 'returns `nil` if the feature is disabled' do
stub_licensed_features(external_authorization_service: false)
project = build(:project,
external_authorization_classification_label: 'hello')
expect(project.external_authorization_classification_label)
.to eq(nil)
end
it 'returns the classification label if it was configured on the project' do
enable_external_authorization_service
project = build(:project,
external_authorization_classification_label: 'hello')
expect(project.external_authorization_classification_label)
.to eq('hello')
end
end
describe '#user_can_push_to_empty_repo?' do
it 'returns false when the external service denies access' do
user = create(:user)
project = create(:project)
project.add_master(user)
external_service_deny_access(user, project)
expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
end
end
end end
require 'spec_helper'
describe BasePolicy do
include ExternalAuthorizationServiceHelpers
let(:current_user) { create(:user) }
let(:user) { create(:user) }
subject { described_class.new(current_user, [user]) }
describe 'read cross project' do
it { is_expected.to be_allowed(:read_cross_project) }
context 'when an external authorization service is enabled' do
before do
enable_external_authorization_service
end
it { is_expected.not_to be_allowed(:read_cross_project) }
it 'allows admins' do
expect(described_class.new(build(:admin), nil)).to be_allowed(:read_cross_project)
end
it 'allows auditors' do
expect(described_class.new(build(:auditor), nil)).to be_allowed(:read_cross_project)
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe EpicPolicy do describe EpicPolicy do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
def permissions(user, group) def permissions(user, group)
...@@ -16,7 +17,7 @@ describe EpicPolicy do ...@@ -16,7 +17,7 @@ describe EpicPolicy do
group.add_owner(user) group.add_owner(user)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_disallowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_disallowed(:read_epic, :read_epic_iid, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
end end
...@@ -30,18 +31,18 @@ describe EpicPolicy do ...@@ -30,18 +31,18 @@ describe EpicPolicy do
it 'anonymous user can not read epics' do it 'anonymous user can not read epics' do
expect(permissions(nil, group)) expect(permissions(nil, group))
.to be_disallowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_disallowed(:read_epic, :read_epic_iid, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
it 'user who is not a group member can not read epics' do it 'user who is not a group member can not read epics' do
expect(permissions(user, group)) expect(permissions(user, group))
.to be_disallowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_disallowed(:read_epic, :read_epic_iid, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
it 'guest group member can only read epics' do it 'guest group member can only read epics' do
group.add_guest(user) group.add_guest(user)
expect(permissions(user, group)).to be_allowed(:read_epic) expect(permissions(user, group)).to be_allowed(:read_epic, :read_epic_iid)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic) expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
...@@ -50,14 +51,14 @@ describe EpicPolicy do ...@@ -50,14 +51,14 @@ describe EpicPolicy do
expect(permissions(user, group)).to be_disallowed(:destroy_epic) expect(permissions(user, group)).to be_disallowed(:destroy_epic)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :read_epic_iid, :update_epic, :admin_epic, :create_epic)
end end
it 'only group owner can destroy epics' do it 'only group owner can destroy epics' do
group.add_owner(user) group.add_owner(user)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :read_epic_iid, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
end end
...@@ -66,18 +67,18 @@ describe EpicPolicy do ...@@ -66,18 +67,18 @@ describe EpicPolicy do
it 'anonymous user can not read epics' do it 'anonymous user can not read epics' do
expect(permissions(nil, group)) expect(permissions(nil, group))
.to be_disallowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_disallowed(:read_epic, :read_epic_iid, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
it 'user who is not a group member can only read epics' do it 'user who is not a group member can only read epics' do
expect(permissions(user, group)).to be_allowed(:read_epic) expect(permissions(user, group)).to be_allowed(:read_epic, :read_epic_iid)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic) expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
it 'guest group member can only read epics' do it 'guest group member can only read epics' do
group.add_guest(user) group.add_guest(user)
expect(permissions(user, group)).to be_allowed(:read_epic) expect(permissions(user, group)).to be_allowed(:read_epic, :read_epic_iid)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic) expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
...@@ -86,14 +87,14 @@ describe EpicPolicy do ...@@ -86,14 +87,14 @@ describe EpicPolicy do
expect(permissions(user, group)).to be_disallowed(:destroy_epic) expect(permissions(user, group)).to be_disallowed(:destroy_epic)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :read_epic_iid, :update_epic, :admin_epic, :create_epic)
end end
it 'only group owner can destroy epics' do it 'only group owner can destroy epics' do
group.add_owner(user) group.add_owner(user)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :read_epic_iid, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
end end
...@@ -101,19 +102,19 @@ describe EpicPolicy do ...@@ -101,19 +102,19 @@ describe EpicPolicy do
let(:group) { create(:group, :public) } let(:group) { create(:group, :public) }
it 'anonymous user can only read epics' do it 'anonymous user can only read epics' do
expect(permissions(nil, group)).to be_allowed(:read_epic) expect(permissions(nil, group)).to be_allowed(:read_epic, :read_epic_iid)
expect(permissions(nil, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic) expect(permissions(nil, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
it 'user who is not a group member can only read epics' do it 'user who is not a group member can only read epics' do
expect(permissions(user, group)).to be_allowed(:read_epic) expect(permissions(user, group)).to be_allowed(:read_epic, :read_epic_iid)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic) expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
it 'guest group member can only read epics' do it 'guest group member can only read epics' do
group.add_guest(user) group.add_guest(user)
expect(permissions(user, group)).to be_allowed(:read_epic) expect(permissions(user, group)).to be_allowed(:read_epic, :read_epic_iid)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic) expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
...@@ -122,15 +123,32 @@ describe EpicPolicy do ...@@ -122,15 +123,32 @@ describe EpicPolicy do
expect(permissions(user, group)).to be_disallowed(:destroy_epic) expect(permissions(user, group)).to be_disallowed(:destroy_epic)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :read_epic_iid, :update_epic, :admin_epic, :create_epic)
end end
it 'only group owner can destroy epics' do it 'only group owner can destroy epics' do
group.add_owner(user) group.add_owner(user)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :read_epic_iid, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
end end
end end
context 'when external authorization is enabled' do
let(:group) { create(:group) }
before do
enable_external_authorization_service
group.add_owner(user)
end
it 'does not allow any epic permissions' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(permissions(user, group))
.not_to be_allowed(:read_epic, :read_epic_iid, :update_epic,
:destroy_epic, :admin_epic, :create_epic)
end
end
end end
require 'spec_helper'
describe IssuePolicy do
include ExternalAuthorizationServiceHelpers
context 'with external authorization enabled' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let(:policies) { described_class.new(user, issue) }
before do
enable_external_authorization_service
end
it 'can read the issue iid without accessing the external service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(policies).to be_allowed(:read_issue_iid)
end
end
end
require 'spec_helper' require 'spec_helper'
describe MergeRequestPolicy do describe MergeRequestPolicy do
include ExternalAuthorizationServiceHelpers
let(:guest) { create(:user) } let(:guest) { create(:user) }
let(:developer) { create(:user) } let(:developer) { create(:user) }
let(:master) { create(:user) } let(:master) { create(:user) }
...@@ -108,4 +110,21 @@ describe MergeRequestPolicy do ...@@ -108,4 +110,21 @@ describe MergeRequestPolicy do
end end
end end
end end
context 'with external authorization enabled' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:policies) { described_class.new(user, merge_request) }
before do
enable_external_authorization_service
end
it 'can read the issue iid without accessing the external service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(policies).to be_allowed(:read_merge_request_iid)
end
end
end end
require 'spec_helper' require 'spec_helper'
describe ProjectPolicy do describe ProjectPolicy do
include ExternalAuthorizationServiceHelpers
set(:owner) { create(:user) } set(:owner) { create(:user) }
set(:admin) { create(:admin) } set(:admin) { create(:admin) }
set(:developer) { create(:user) } set(:developer) { create(:user) }
...@@ -121,4 +123,57 @@ describe ProjectPolicy do ...@@ -121,4 +123,57 @@ describe ProjectPolicy do
end end
end end
end end
context 'reading a project' do
it 'allows access when a user has read access to the repo' do
expect(described_class.new(owner, project)).to be_allowed(:read_project)
expect(described_class.new(developer, project)).to be_allowed(:read_project)
expect(described_class.new(admin, project)).to be_allowed(:read_project)
end
it 'never checks the external service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(described_class.new(owner, project)).to be_allowed(:read_project)
end
context 'with an external authorization service' do
before do
enable_external_authorization_service
end
it 'allows access when the external service allows it' do
external_service_allow_access(owner, project)
external_service_allow_access(developer, project)
expect(described_class.new(owner, project)).to be_allowed(:read_project)
expect(described_class.new(developer, project)).to be_allowed(:read_project)
end
it 'does not check the external service for admins and allows access' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(described_class.new(admin, project)).to be_allowed(:read_project)
end
it 'allows auditors' do
stub_licensed_features(auditor_user: true)
auditor = create(:user, :auditor)
expect(described_class.new(auditor, project)).to be_allowed(:read_project)
end
it 'prevents all but seeing a public project in a list when access is denied' do
external_service_deny_access(owner, project)
external_service_deny_access(developer, project)
[developer, owner, create(:user), nil].each do |user|
policy = described_class.new(owner, project)
expect(policy).not_to be_allowed(:read_project)
expect(policy).not_to be_allowed(:owner_access)
expect(policy).not_to be_allowed(:change_namespace)
end
end
end
end
end end
...@@ -19,41 +19,75 @@ describe API::Settings, 'EE Settings' do ...@@ -19,41 +19,75 @@ describe API::Settings, 'EE Settings' do
end end
end end
context 'when the repository mirrors feature is not available' do shared_examples 'settings for licensed features' do
let(:attribute_names) { settings.keys.map(&:to_s) }
before do before do
stub_licensed_features(repository_mirrors: false) # Make sure the settings exist before the specs
get api("/application/settings", admin) get api("/application/settings", admin)
end end
it 'hides repository mirror attributes when the feature is available' do context 'when the feature is not available' do
get api("/application/settings", admin) before do
stub_licensed_features(feature => false)
end
expect(response).to have_gitlab_http_status(200) it 'hides the attributes in the API' do
expect(json_response.keys).not_to include('mirror_max_capacity') get api("/application/settings", admin)
end
it 'does not update repository mirror attributes' do expect(response).to have_gitlab_http_status(200)
expect { put api("/application/settings", admin), mirror_max_capacity: 15 } attribute_names.each do |attribute|
.not_to change(ApplicationSetting.current.reload, :mirror_max_capacity) expect(json_response.keys).not_to include(attribute)
end end
end end
context 'when the repository mirrors feature is available' do it 'does not update application settings' do
before do expect { put api("/application/settings", admin), settings }
stub_licensed_features(repository_mirrors: true) .not_to change { ApplicationSetting.current.reload.attributes.slice(*attribute_names) }
end
end end
it 'has repository mirror attributes when the feature is available' do context 'when the feature is available' do
get api("/application/settings", admin) before do
stub_licensed_features(feature => true)
end
expect(response).to have_gitlab_http_status(200) it 'includes the attributes in the API' do
expect(json_response.keys).to include('mirror_max_capacity') get api("/application/settings", admin)
expect(response).to have_gitlab_http_status(200)
attribute_names.each do |attribute|
expect(json_response.keys).to include(attribute)
end
end
it 'allows updating the settings' do
put api("/application/settings", admin), settings
settings.each do |attribute, value|
expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
end
end
end end
end
context 'mirroring settings' do
let(:settings) { { mirror_max_capacity: 15 } }
let(:feature) { :repository_mirrors }
it 'updates repository mirror attributes' do it_behaves_like 'settings for licensed features'
put api("/application/settings", admin), mirror_max_capacity: 15 end
expect(ApplicationSetting.current.reload.mirror_max_capacity).to eq(15) context 'external policy classification settings' do
let(:settings) do
{
external_authorization_service_enabled: true,
external_authorization_service_url: 'https://custom.service/',
external_authorization_service_default_label: 'default'
}
end end
let(:feature) { :external_authorization_service }
it_behaves_like 'settings for licensed features'
end end
end end
require 'spec_helper'
describe GroupChildEntity do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:request) { double('request') }
let(:entity) { described_class.new(object, request: request) }
subject(:json) { entity.as_json }
before do
allow(request).to receive(:current_user).and_return(user)
end
describe 'for a project with external authorization enabled' do
let(:object) do
create(:project, :with_avatar,
description: 'Awesomeness')
end
before do
enable_external_authorization_service
object.add_master(user)
end
it 'does not hit the external authorization service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(json[:can_edit]).to eq(false)
end
end
end
require 'spec_helper' require 'spec_helper'
describe EE::NotificationService do describe EE::NotificationService, :mailer do
include ExternalAuthorizationServiceHelpers
let(:subject) { NotificationService.new } let(:subject) { NotificationService.new }
before do context 'with external authentication service' do
allow(Notify).to receive(:service_desk_new_note_email) let(:issue) { create(:issue) }
.with(kind_of(Integer), kind_of(Integer)).and_return(double(deliver_later: true)) let(:project) { issue.project }
let(:note) { create(:note, noteable: issue, project: project) }
let(:member) { create(:user) }
allow(::EE::Gitlab::ServiceDesk).to receive(:enabled?).and_return(true) before do
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true } project.add_master(member)
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true } member.global_notification_setting.update!(level: :watch)
end end
def should_email! it 'sends email when the service is not enabled' do
expect(Notify).to receive(:service_desk_new_note_email).with(issue.id, kind_of(Integer)) expect(Notify).to receive(:new_issue_email).with(member.id, issue.id, nil).and_call_original
end
def should_not_email! subject.new_issue(issue, member)
expect(Notify).not_to receive(:service_desk_new_note_email) end
end
def execute! context 'when the service is enabled' do
subject.send_service_desk_notification(note) before do
end enable_external_authorization_service
end
it 'does not send an email' do
expect(Notify).not_to receive(:new_issue_email)
def self.it_should_email! subject.new_issue(issue, member)
it 'sends the email' do end
should_email!
execute! it 'still delivers email to admins' do
member.update!(admin: true)
expect(Notify).to receive(:new_issue_email).with(member.id, issue.id, nil).and_call_original
subject.new_issue(issue, member)
end
end end
end end
def self.it_should_not_email! context 'service desk issues' do
it 'doesn\'t send the email' do before do
should_not_email! allow(Notify).to receive(:service_desk_new_note_email)
execute! .with(kind_of(Integer), kind_of(Integer)).and_return(double(deliver_later: true))
allow(::EE::Gitlab::ServiceDesk).to receive(:enabled?).and_return(true)
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
end end
end
let(:issue) { create(:issue, author: User.support_bot) } def should_email!
let(:project) { issue.project } expect(Notify).to receive(:service_desk_new_note_email).with(issue.id, kind_of(Integer))
let(:note) { create(:note, noteable: issue, project: project) } end
context 'a non-service-desk issue' do def should_not_email!
it_should_not_email! expect(Notify).not_to receive(:service_desk_new_note_email)
end end
context 'a service-desk issue' do def execute!
before do subject.send_service_desk_notification(note)
issue.update!(service_desk_reply_to: 'service.desk@example.com')
project.update!(service_desk_enabled: true)
end end
it_should_email! def self.it_should_email!
it 'sends the email' do
should_email!
execute!
end
end
context 'where the project has disabled the feature' do def self.it_should_not_email!
before do it 'doesn\'t send the email' do
project.update(service_desk_enabled: false) should_not_email!
execute!
end end
end
let(:issue) { create(:issue, author: User.support_bot) }
let(:project) { issue.project }
let(:note) { create(:note, noteable: issue, project: project) }
context 'a non-service-desk issue' do
it_should_not_email! it_should_not_email!
end end
context 'when the license doesn\'t allow service desk' do context 'a service-desk issue' do
before do before do
expect(EE::Gitlab::ServiceDesk).to receive(:enabled?).and_return(false) issue.update!(service_desk_reply_to: 'service.desk@example.com')
project.update!(service_desk_enabled: true)
end end
it_should_not_email! it_should_email!
end
context 'when the support bot has unsubscribed' do context 'where the project has disabled the feature' do
before do before do
issue.unsubscribe(User.support_bot, project) project.update(service_desk_enabled: false)
end
it_should_not_email!
end end
it_should_not_email! context 'when the license doesn\'t allow service desk' do
before do
expect(EE::Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
end
it_should_not_email!
end
context 'when the support bot has unsubscribed' do
before do
issue.unsubscribe(User.support_bot, project)
end
it_should_not_email!
end
end end
end end
end end
module ExternalAuthorizationServiceHelpers
def enable_external_authorization_service
stub_licensed_features(external_authorization_service: true)
# Not using `stub_application_setting` because the method is prepended in
# `EE::ApplicationSetting` which breaks when using `any_instance`
# https://gitlab.com/gitlab-org/gitlab-ce/issues/33587
allow(::Gitlab::CurrentSettings.current_application_settings)
.to receive(:external_authorization_service_enabled) { true }
allow(::Gitlab::CurrentSettings.current_application_settings)
.to receive(:external_authorization_service_enabled?) { true }
stub_application_setting(external_authorization_service_url: 'https://authorize.me')
stub_application_setting(external_authorization_service_default_label: 'default_label')
stub_request(:post, "https://authorize.me").to_return(status: 200)
end
def external_service_set_access(allowed, user, project)
enable_external_authorization_service
classification_label = ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
# Reload the project so cached licensed features are reloaded
if project
classification_label = Project.find(project.id).external_authorization_classification_label
end
allow(EE::Gitlab::ExternalAuthorization)
.to receive(:access_allowed?)
.with(user, classification_label)
.and_return(allowed)
end
def external_service_allow_access(user, project = nil)
external_service_set_access(true, user, project)
end
def external_service_deny_access(user, project = nil)
external_service_set_access(false, user, project)
end
end
require 'spec_helper'
shared_examples 'disabled when using an external authorization service' do
include ExternalAuthorizationServiceHelpers
it 'works when the feature is not enabled' do
subject
expect(response).to be_success
end
it 'renders a 404 with a message when the feature is enabled' do
enable_external_authorization_service
subject
expect(response).to have_gitlab_http_status(404)
end
end
shared_examples 'unauthorized when external service denies access' do
include ExternalAuthorizationServiceHelpers
it 'allows access when the authorization service allows it' do
external_service_allow_access(user, project)
subject
# Account for redirects after updates
expect(response.status).to be_between(200, 302)
end
it 'allows access when the authorization service denies it' do
external_service_deny_access(user, project)
subject
expect(response).to have_gitlab_http_status(404)
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment