Merge branch 'if-57131-external_auth_to_ce' into 'master'
CE port of Move "Authorize project access with external service" to Core See merge request gitlab-org/gitlab-ce!27056
This commit is contained in:
commit
18cd3e9540
|
@ -67,6 +67,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.classification-label {
|
||||
background-color: $red-500;
|
||||
}
|
||||
|
||||
.toggle-wrapper {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
|
|
@ -124,7 +124,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def visible_application_setting_attributes
|
||||
ApplicationSettingsHelper.visible_attributes + [
|
||||
[
|
||||
*::ApplicationSettingsHelper.visible_attributes,
|
||||
*::ApplicationSettingsHelper.external_authorization_service_attributes,
|
||||
:domain_blacklist_file,
|
||||
disabled_oauth_sign_in_sources: [],
|
||||
import_sources: [],
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProjectUnauthorized
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# EE would override this
|
||||
def project_unauthorized_proc
|
||||
# no-op
|
||||
lambda do |project|
|
||||
if project
|
||||
label = project.external_authorization_classification_label
|
||||
rejection_reason = nil
|
||||
|
||||
unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, label)
|
||||
rejection_reason = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label)
|
||||
rejection_reason ||= _('External authorization denied access to this project')
|
||||
end
|
||||
|
||||
if rejection_reason
|
||||
access_denied!(rejection_reason)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -345,6 +345,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
:container_registry_enabled,
|
||||
:default_branch,
|
||||
:description,
|
||||
:external_authorization_classification_label,
|
||||
:import_url,
|
||||
:issues_tracker,
|
||||
:issues_tracker_id,
|
||||
|
|
|
@ -119,6 +119,39 @@ module ApplicationSettingsHelper
|
|||
options_for_select(options, selected)
|
||||
end
|
||||
|
||||
def external_authorization_description
|
||||
_("If enabled, access to projects will be validated on an external service"\
|
||||
" using their classification label.")
|
||||
end
|
||||
|
||||
def external_authorization_timeout_help_text
|
||||
_("Time in seconds GitLab will wait for a response from the external "\
|
||||
"service. When the service does not respond in time, access will be "\
|
||||
"denied.")
|
||||
end
|
||||
|
||||
def external_authorization_url_help_text
|
||||
_("When leaving the URL blank, classification labels can still be "\
|
||||
"specified without disabling cross project features or performing "\
|
||||
"external authorization checks.")
|
||||
end
|
||||
|
||||
def external_authorization_client_certificate_help_text
|
||||
_("The X509 Certificate to use when mutual TLS is required to communicate "\
|
||||
"with the external authorization service. If left blank, the server "\
|
||||
"certificate is still validated when accessing over HTTPS.")
|
||||
end
|
||||
|
||||
def external_authorization_client_key_help_text
|
||||
_("The private key to use when a client certificate is provided. This value "\
|
||||
"is encrypted at rest.")
|
||||
end
|
||||
|
||||
def external_authorization_client_pass_help_text
|
||||
_("The passphrase required to decrypt the private key. This is optional "\
|
||||
"and the value is encrypted at rest.")
|
||||
end
|
||||
|
||||
def visible_attributes
|
||||
[
|
||||
:admin_notification_email,
|
||||
|
@ -238,6 +271,18 @@ module ApplicationSettingsHelper
|
|||
]
|
||||
end
|
||||
|
||||
def external_authorization_service_attributes
|
||||
[
|
||||
:external_auth_client_cert,
|
||||
:external_auth_client_key,
|
||||
:external_auth_client_key_pass,
|
||||
:external_authorization_service_default_label,
|
||||
:external_authorization_service_enabled,
|
||||
:external_authorization_service_timeout,
|
||||
:external_authorization_service_url
|
||||
]
|
||||
end
|
||||
|
||||
def expanded_by_default?
|
||||
Rails.env.test?
|
||||
end
|
||||
|
|
|
@ -303,6 +303,16 @@ module ProjectsHelper
|
|||
@path.present?
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
def get_project_nav_tabs(project, current_user)
|
||||
|
|
|
@ -213,6 +213,40 @@ class ApplicationSetting < ApplicationRecord
|
|||
|
||||
validate :terms_exist, if: :enforce_terms?
|
||||
|
||||
validates :external_authorization_service_default_label,
|
||||
presence: true,
|
||||
if: :external_authorization_service_enabled
|
||||
|
||||
validates :external_authorization_service_url,
|
||||
url: true, allow_blank: true,
|
||||
if: :external_authorization_service_enabled
|
||||
|
||||
validates :external_authorization_service_timeout,
|
||||
numericality: { greater_than: 0, less_than_or_equal_to: 10 },
|
||||
if: :external_authorization_service_enabled
|
||||
|
||||
validates :external_auth_client_key,
|
||||
presence: true,
|
||||
if: -> (setting) { setting.external_auth_client_cert.present? }
|
||||
|
||||
validates_with X509CertificateCredentialsValidator,
|
||||
certificate: :external_auth_client_cert,
|
||||
pkey: :external_auth_client_key,
|
||||
pass: :external_auth_client_key_pass,
|
||||
if: -> (setting) { setting.external_auth_client_cert.present? }
|
||||
|
||||
attr_encrypted :external_auth_client_key,
|
||||
mode: :per_attribute_iv,
|
||||
key: Settings.attr_encrypted_db_key_base_truncated,
|
||||
algorithm: 'aes-256-gcm',
|
||||
encode: true
|
||||
|
||||
attr_encrypted :external_auth_client_key_pass,
|
||||
mode: :per_attribute_iv,
|
||||
key: Settings.attr_encrypted_db_key_base_truncated,
|
||||
algorithm: 'aes-256-gcm',
|
||||
encode: true
|
||||
|
||||
before_validation :ensure_uuid!
|
||||
before_validation :strip_sentry_values
|
||||
|
||||
|
|
|
@ -208,7 +208,13 @@ class Issue < ApplicationRecord
|
|||
def visible_to_user?(user = nil)
|
||||
return false unless project && project.feature_available?(:issues, user)
|
||||
|
||||
user ? readable_by?(user) : publicly_visible?
|
||||
return publicly_visible? unless user
|
||||
|
||||
return false unless readable_by?(user)
|
||||
|
||||
user.full_private_access? ||
|
||||
::Gitlab::ExternalAuthorization.access_allowed?(
|
||||
user, project.external_authorization_classification_label)
|
||||
end
|
||||
|
||||
def check_for_spam?
|
||||
|
@ -276,7 +282,7 @@ class Issue < ApplicationRecord
|
|||
|
||||
# Returns `true` if this Issue is visible to everybody.
|
||||
def publicly_visible?
|
||||
project.public? && !confidential?
|
||||
project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled?
|
||||
end
|
||||
|
||||
def expire_etag_cache
|
||||
|
|
|
@ -2066,6 +2066,11 @@ class Project < ApplicationRecord
|
|||
fetch_branch_allows_collaboration(user, branch_name)
|
||||
end
|
||||
|
||||
def external_authorization_classification_label
|
||||
super || ::Gitlab::CurrentSettings.current_application_settings
|
||||
.external_authorization_service_default_label
|
||||
end
|
||||
|
||||
def licensed_features
|
||||
[]
|
||||
end
|
||||
|
|
|
@ -22,6 +22,13 @@ class BasePolicy < DeclarativePolicy::Base
|
|||
Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
|
||||
end
|
||||
|
||||
# This is prevented in some cases in `gitlab-ee`
|
||||
condition(:external_authorization_enabled, scope: :global, score: 0) do
|
||||
::Gitlab::ExternalAuthorization.perform_check?
|
||||
end
|
||||
|
||||
rule { external_authorization_enabled & ~full_private_access }.policy do
|
||||
prevent :read_cross_project
|
||||
end
|
||||
|
||||
rule { default }.enable :read_cross_project
|
||||
end
|
||||
|
|
|
@ -89,6 +89,15 @@ class ProjectPolicy < BasePolicy
|
|||
::Gitlab::CurrentSettings.current_application_settings.mirror_available
|
||||
end
|
||||
|
||||
with_scope :subject
|
||||
condition(:classification_label_authorized, score: 32) do
|
||||
::Gitlab::ExternalAuthorization.access_allowed?(
|
||||
@user,
|
||||
@subject.external_authorization_classification_label,
|
||||
@subject.full_path
|
||||
)
|
||||
end
|
||||
|
||||
# We aren't checking `:read_issue` or `:read_merge_request` in this case
|
||||
# because it could be possible for a user to see an issuable-iid
|
||||
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
|
||||
|
@ -417,6 +426,25 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
|
||||
|
||||
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.
|
||||
#
|
||||
# `:read_project_for_iids` is not prevented by this condition, as it is
|
||||
# used for cross-project reference checks.
|
||||
prevent :guest_access
|
||||
prevent :public_access
|
||||
prevent :public_user_access
|
||||
prevent :reporter_access
|
||||
prevent :developer_access
|
||||
prevent :maintainer_access
|
||||
prevent :owner_access
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def team_member?
|
||||
|
|
|
@ -2,9 +2,17 @@
|
|||
|
||||
module ApplicationSettings
|
||||
class UpdateService < ApplicationSettings::BaseService
|
||||
include ValidatesClassificationLabel
|
||||
|
||||
attr_reader :params, :application_setting
|
||||
|
||||
def execute
|
||||
validate_classification_label(application_setting, :external_authorization_service_default_label)
|
||||
|
||||
if application_setting.errors.any?
|
||||
return false
|
||||
end
|
||||
|
||||
update_terms(@params.delete(:terms))
|
||||
|
||||
if params.key?(:performance_bar_allowed_group_path)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ValidatesClassificationLabel
|
||||
def validate_classification_label(record, attribute_name)
|
||||
return unless ::Gitlab::ExternalAuthorization.enabled?
|
||||
return unless classification_label_change?(record, attribute_name)
|
||||
|
||||
new_label = params[attribute_name].presence
|
||||
new_label ||= ::Gitlab::CurrentSettings.current_application_settings
|
||||
.external_authorization_service_default_label
|
||||
|
||||
unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, new_label)
|
||||
reason = rejection_reason_for_label(new_label)
|
||||
message = s_('ClassificationLabelUnavailable|is unavailable: %{reason}') % { reason: reason }
|
||||
record.errors.add(attribute_name, message)
|
||||
end
|
||||
end
|
||||
|
||||
def rejection_reason_for_label(label)
|
||||
reason_from_service = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label).presence
|
||||
reason_from_service || _("Access to '%{classification_label}' not allowed") % { classification_label: label }
|
||||
end
|
||||
|
||||
def classification_label_change?(record, attribute_name)
|
||||
params.key?(attribute_name) || record.new_record?
|
||||
end
|
||||
end
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Projects
|
||||
class CreateService < BaseService
|
||||
include ValidatesClassificationLabel
|
||||
|
||||
def initialize(user, params)
|
||||
@current_user, @params = user, params.dup
|
||||
@skip_wiki = @params.delete(:skip_wiki)
|
||||
|
@ -45,6 +47,8 @@ module Projects
|
|||
relations_block&.call(@project)
|
||||
yield(@project) if block_given?
|
||||
|
||||
validate_classification_label(@project, :external_authorization_classification_label)
|
||||
|
||||
# If the block added errors, don't try to save the project
|
||||
return @project if @project.errors.any?
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Projects
|
||||
class UpdateService < BaseService
|
||||
include UpdateVisibilityLevel
|
||||
include ValidatesClassificationLabel
|
||||
|
||||
ValidationError = Class.new(StandardError)
|
||||
|
||||
|
@ -14,6 +15,8 @@ module Projects
|
|||
|
||||
yield if block_given?
|
||||
|
||||
validate_classification_label(project, :external_authorization_classification_label)
|
||||
|
||||
# If the block added errors, don't try to save the project
|
||||
return update_failed! if project.errors.any?
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# X509CertificateCredentialsValidator
|
||||
#
|
||||
# Custom validator to check if certificate-attribute was signed using the
|
||||
# private key stored in an attrebute.
|
||||
#
|
||||
# This can be used as an `ActiveModel::Validator` as follows:
|
||||
#
|
||||
# validates_with X509CertificateCredentialsValidator,
|
||||
# certificate: :client_certificate,
|
||||
# pkey: :decrypted_private_key,
|
||||
# pass: :decrypted_passphrase
|
||||
#
|
||||
#
|
||||
# Required attributes:
|
||||
# - certificate: The name of the accessor that returns the certificate to check
|
||||
# - pkey: The name of the accessor that returns the private key
|
||||
# Optional:
|
||||
# - pass: The name of the accessor that returns the passphrase to decrypt the
|
||||
# private key
|
||||
class X509CertificateCredentialsValidator < ActiveModel::Validator
|
||||
def initialize(*args)
|
||||
super
|
||||
|
||||
# We can't validate if we don't have a private key or certificate attributes
|
||||
# in which case this validator is useless.
|
||||
if options[:pkey].nil? || options[:certificate].nil?
|
||||
raise 'Provide at least `certificate` and `pkey` attribute names'
|
||||
end
|
||||
end
|
||||
|
||||
def validate(record)
|
||||
unless certificate = read_certificate(record)
|
||||
record.errors.add(options[:certificate], _('is not a valid X509 certificate.'))
|
||||
end
|
||||
|
||||
unless private_key = read_private_key(record)
|
||||
record.errors.add(options[:pkey], _('could not read private key, is the passphrase correct?'))
|
||||
end
|
||||
|
||||
return if private_key.nil? || certificate.nil?
|
||||
|
||||
unless certificate.public_key.fingerprint == private_key.public_key.fingerprint
|
||||
record.errors.add(options[:pkey], _('private key does not match certificate.'))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_private_key(record)
|
||||
OpenSSL::PKey.read(pkey(record).to_s, pass(record).to_s)
|
||||
rescue OpenSSL::PKey::PKeyError, ArgumentError
|
||||
# When the primary key could not be read, an ArgumentError is raised.
|
||||
# This hapens when the passed key is not valid or the passphrase is incorrect
|
||||
nil
|
||||
end
|
||||
|
||||
def read_certificate(record)
|
||||
OpenSSL::X509::Certificate.new(certificate(record).to_s)
|
||||
rescue OpenSSL::X509::CertificateError
|
||||
nil
|
||||
end
|
||||
|
||||
# rubocop:disable GitlabSecurity/PublicSend
|
||||
#
|
||||
# Allowing `#public_send` here because we don't want the validator to really
|
||||
# care about the names of the attributes or where they come from.
|
||||
#
|
||||
# The credentials are mostly stored encrypted so we need to go through the
|
||||
# accessors to get the values, `read_attribute` bypasses those.
|
||||
def certificate(record)
|
||||
record.public_send(options[:certificate])
|
||||
end
|
||||
|
||||
def pkey(record)
|
||||
record.public_send(options[:pkey])
|
||||
end
|
||||
|
||||
def pass(record)
|
||||
return unless options[:pass]
|
||||
|
||||
record.public_send(options[:pass])
|
||||
end
|
||||
# rubocop:enable GitlabSecurity/PublicSend
|
||||
end
|
|
@ -0,0 +1,51 @@
|
|||
%section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
= _('External authentication')
|
||||
%button.btn.js-settings-toggle{ type: 'button' }
|
||||
= expanded ? 'Collapse' : 'Expand'
|
||||
%p
|
||||
= _('External Classification Policy Authorization')
|
||||
.settings-content
|
||||
|
||||
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :external_authorization_service_enabled, class: 'form-check-input'
|
||||
= f.label :external_authorization_service_enabled, class: 'form-check-label' do
|
||||
= _('Enable classification control using an external service')
|
||||
%span.form-text.text-muted
|
||||
= external_authorization_description
|
||||
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/external_authorization')
|
||||
.form-group
|
||||
= f.label :external_authorization_service_url, _('Service URL'), class: 'label-bold'
|
||||
= f.text_field :external_authorization_service_url, class: 'form-control'
|
||||
%span.form-text.text-muted
|
||||
= external_authorization_url_help_text
|
||||
.form-group
|
||||
= f.label :external_authorization_service_timeout, _('External authorization request timeout'), class: 'label-bold'
|
||||
= f.number_field :external_authorization_service_timeout, class: 'form-control', min: 0.001, max: 10, step: 0.001
|
||||
%span.form-text.text-muted
|
||||
= external_authorization_timeout_help_text
|
||||
= f.label :external_auth_client_cert, _('Client authentication certificate'), class: 'label-bold'
|
||||
= f.text_area :external_auth_client_cert, class: 'form-control'
|
||||
%span.form-text.text-muted
|
||||
= external_authorization_client_certificate_help_text
|
||||
.form-group
|
||||
= f.label :external_auth_client_key, _('Client authentication key'), class: 'label-bold'
|
||||
= f.text_area :external_auth_client_key, class: 'form-control'
|
||||
%span.form-text.text-muted
|
||||
= external_authorization_client_key_help_text
|
||||
.form-group
|
||||
= f.label :external_auth_client_key_pass, _('Client authentication key password'), class: 'label-bold'
|
||||
= f.password_field :external_auth_client_key_pass, class: 'form-control'
|
||||
%span.form-text.text-muted
|
||||
= external_authorization_client_pass_help_text
|
||||
.form-group
|
||||
= f.label :external_authorization_service_default_label, _('Default classification label'), class: 'label-bold'
|
||||
= f.text_field :external_authorization_service_default_label, class: 'form-control'
|
||||
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
|
@ -68,7 +68,7 @@
|
|||
.settings-content
|
||||
= render 'terms'
|
||||
|
||||
= render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default?
|
||||
= render 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default?
|
||||
|
||||
%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
.alert-wrapper
|
||||
= render "layouts/broadcast"
|
||||
= render "layouts/header/read_only_banner"
|
||||
= render "layouts/nav/classification_level_banner"
|
||||
= yield :flash_message
|
||||
= render "shared/ping_consent"
|
||||
- unless @hide_breadcrumbs
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
- if ::Gitlab::ExternalAuthorization.enabled? && @project
|
||||
= content_for :header_content do
|
||||
%span.badge.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
|
|
@ -0,0 +1,8 @@
|
|||
- if ::Gitlab::ExternalAuthorization.enabled?
|
||||
.form-group
|
||||
= f.label :external_authorization_classification_label, class: 'label-bold' do
|
||||
= s_('ExternalAuthorizationService|Classification Label')
|
||||
%span.light (optional)
|
||||
= f.text_field :external_authorization_classification_label, class: "form-control"
|
||||
%span.form-text.text-muted
|
||||
= external_classification_label_help_message
|
|
@ -32,7 +32,7 @@
|
|||
%span.light (optional)
|
||||
= f.text_area :description, class: "form-control", rows: 3, maxlength: 250
|
||||
|
||||
= render_if_exists 'projects/classification_policy_settings', f: f
|
||||
= render 'projects/classification_policy_settings', f: f
|
||||
|
||||
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move "Authorize project access with external service" to Core
|
||||
merge_request: 26823
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,29 @@
|
|||
class AddExternalClassificationAuthorizationSettingsToApplictionSettings < ActiveRecord::Migration[4.2]
|
||||
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
|
|
@ -0,0 +1,11 @@
|
|||
class AddExternalAuthorizationServiceClassificationLabelToProjects < ActiveRecord::Migration[4.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :projects,
|
||||
:external_authorization_classification_label,
|
||||
:string
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
class AddExternalAuthorizationServiceTimeoutToApplicationSettings < ActiveRecord::Migration[4.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
# We can use the regular `add_column` with a default since `application_settings`
|
||||
# is a small table.
|
||||
add_column :application_settings,
|
||||
:external_authorization_service_timeout,
|
||||
:float,
|
||||
default: 0.5
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :application_settings, :external_authorization_service_timeout
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration[4.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :application_settings,
|
||||
:external_auth_client_cert, :text
|
||||
add_column :application_settings,
|
||||
:encrypted_external_auth_client_key, :text
|
||||
add_column :application_settings,
|
||||
:encrypted_external_auth_client_key_iv, :string
|
||||
add_column :application_settings,
|
||||
:encrypted_external_auth_client_key_pass, :string
|
||||
add_column :application_settings,
|
||||
:encrypted_external_auth_client_key_pass_iv, :string
|
||||
end
|
||||
end
|
10
db/schema.rb
10
db/schema.rb
|
@ -178,6 +178,15 @@ ActiveRecord::Schema.define(version: 20190326164045) do
|
|||
t.integer "local_markdown_version", default: 0, null: false
|
||||
t.integer "first_day_of_week", default: 0, 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"
|
||||
t.float "external_authorization_service_timeout", default: 0.5
|
||||
t.text "external_auth_client_cert"
|
||||
t.text "encrypted_external_auth_client_key"
|
||||
t.string "encrypted_external_auth_client_key_iv"
|
||||
t.string "encrypted_external_auth_client_key_pass"
|
||||
t.string "encrypted_external_auth_client_key_pass_iv"
|
||||
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
|
||||
end
|
||||
|
||||
|
@ -1756,6 +1765,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
|
|||
t.string "runners_token_encrypted"
|
||||
t.string "bfg_object_map"
|
||||
t.boolean "detected_repository_languages"
|
||||
t.string "external_authorization_classification_label"
|
||||
t.index ["ci_id"], name: "index_projects_on_ci_id", using: :btree
|
||||
t.index ["created_at"], name: "index_projects_on_created_at", using: :btree
|
||||
t.index ["creator_id"], name: "index_projects_on_creator_id", using: :btree
|
||||
|
|
|
@ -277,6 +277,7 @@ module API
|
|||
expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
|
||||
options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
|
||||
}
|
||||
expose :external_authorization_classification_label
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def self.preload_relation(projects_relation, options = {})
|
||||
|
@ -1120,6 +1121,8 @@ module API
|
|||
expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
|
||||
expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
|
||||
|
||||
expose(*::ApplicationSettingsHelper.external_authorization_service_attributes)
|
||||
|
||||
# support legacy names, can be removed in v5
|
||||
expose :password_authentication_enabled_for_web, as: :password_authentication_enabled
|
||||
expose :password_authentication_enabled_for_web, as: :signin_enabled
|
||||
|
|
|
@ -29,13 +29,13 @@ module API
|
|||
optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
|
||||
optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests'
|
||||
optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md"
|
||||
optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project'
|
||||
end
|
||||
|
||||
if Gitlab.ee?
|
||||
params :optional_project_params_ee do
|
||||
optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
|
||||
optional :approvals_before_merge, type: Integer, desc: 'How many approvers should approve merge request by default'
|
||||
optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project'
|
||||
optional :mirror, type: Boolean, desc: 'Enables pull mirroring in a project'
|
||||
optional :mirror_trigger_builds, type: Boolean, desc: 'Pull mirroring triggers builds'
|
||||
end
|
||||
|
@ -72,7 +72,8 @@ module API
|
|||
:tag_list,
|
||||
:visibility,
|
||||
:wiki_enabled,
|
||||
:avatar
|
||||
:avatar,
|
||||
:external_authorization_classification_label
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -168,7 +168,9 @@ module API
|
|||
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
|
||||
end
|
||||
|
||||
optional_attributes = ::ApplicationSettingsHelper.visible_attributes << :performance_bar_allowed_group_id
|
||||
optional_attributes = [*::ApplicationSettingsHelper.visible_attributes,
|
||||
*::ApplicationSettingsHelper.external_authorization_service_attributes,
|
||||
:performance_bar_allowed_group_id]
|
||||
|
||||
if Gitlab.ee?
|
||||
optional_attributes += EE::ApplicationSettingsHelper.possible_licensed_attributes
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ExternalAuthorization
|
||||
extend ExternalAuthorization::Config
|
||||
|
||||
RequestFailed = Class.new(StandardError)
|
||||
|
||||
def self.access_allowed?(user, label, project_path = nil)
|
||||
return true unless perform_check?
|
||||
return false unless user
|
||||
|
||||
access_for_user_to_label(user, label, project_path).has_access?
|
||||
end
|
||||
|
||||
def self.rejection_reason(user, label)
|
||||
return unless enabled?
|
||||
return unless user
|
||||
|
||||
access_for_user_to_label(user, label, nil).reason
|
||||
end
|
||||
|
||||
def self.access_for_user_to_label(user, label, project_path)
|
||||
if RequestStore.active?
|
||||
RequestStore.fetch("external_authorisation:user-#{user.id}:label-#{label}") do
|
||||
load_access(user, label, project_path)
|
||||
end
|
||||
else
|
||||
load_access(user, label, project_path)
|
||||
end
|
||||
end
|
||||
|
||||
def self.load_access(user, label, project_path)
|
||||
access = ::Gitlab::ExternalAuthorization::Access.new(user, label).load!
|
||||
::Gitlab::ExternalAuthorization::Logger.log_access(access, project_path)
|
||||
|
||||
access
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ExternalAuthorization
|
||||
class Access
|
||||
attr_reader :user,
|
||||
:reason,
|
||||
:loaded_at,
|
||||
:label,
|
||||
:load_type
|
||||
|
||||
def initialize(user, label)
|
||||
@user, @label = user, label
|
||||
end
|
||||
|
||||
def loaded?
|
||||
loaded_at && (loaded_at > ExternalAuthorization::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
|
||||
@load_type = :cache
|
||||
@access, @reason, @loaded_at = cache.load
|
||||
end
|
||||
|
||||
def load_from_service
|
||||
@load_type = :request
|
||||
response = Client.new(@user, @label).request_access
|
||||
@access = response.successful?
|
||||
@reason = response.reason
|
||||
@loaded_at = Time.now
|
||||
cache.store(@access, @reason, @loaded_at) if response.valid?
|
||||
rescue ::Gitlab::ExternalAuthorization::RequestFailed => e
|
||||
@access = false
|
||||
@reason = e.message
|
||||
@loaded_at = Time.now
|
||||
end
|
||||
|
||||
def cache
|
||||
@cache ||= ExternalAuthorization::Cache.new(@user, @label)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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 unless @reason.present?
|
||||
|
||||
@reason
|
||||
end
|
||||
|
||||
def refreshed_at
|
||||
# Don't try to parse a time if there was no cache
|
||||
return unless @refreshed_at.present?
|
||||
|
||||
Time.parse(@refreshed_at)
|
||||
end
|
||||
|
||||
def cache_key
|
||||
"external_authorization:user-#{@user.id}:label-#{@label}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Excon.defaults[:ssl_verify_peer] = false
|
||||
|
||||
module Gitlab
|
||||
module ExternalAuthorization
|
||||
class Client
|
||||
include ExternalAuthorization::Config
|
||||
|
||||
REQUEST_HEADERS = {
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
}.freeze
|
||||
|
||||
def initialize(user, label)
|
||||
@user, @label = user, label
|
||||
end
|
||||
|
||||
def request_access
|
||||
response = Excon.post(
|
||||
service_url,
|
||||
post_params
|
||||
)
|
||||
::Gitlab::ExternalAuthorization::Response.new(response)
|
||||
rescue Excon::Error => e
|
||||
raise ::Gitlab::ExternalAuthorization::RequestFailed.new(e)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def post_params
|
||||
params = { headers: REQUEST_HEADERS,
|
||||
body: body.to_json,
|
||||
connect_timeout: timeout,
|
||||
read_timeout: timeout,
|
||||
write_timeout: timeout }
|
||||
|
||||
if has_tls?
|
||||
params[:client_cert_data] = client_cert
|
||||
params[:client_key_data] = client_key
|
||||
params[:client_key_pass] = client_key_pass
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
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
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ExternalAuthorization
|
||||
module Config
|
||||
extend self
|
||||
|
||||
def timeout
|
||||
application_settings.external_authorization_service_timeout
|
||||
end
|
||||
|
||||
def service_url
|
||||
application_settings.external_authorization_service_url
|
||||
end
|
||||
|
||||
def enabled?
|
||||
application_settings.external_authorization_service_enabled
|
||||
end
|
||||
|
||||
def perform_check?
|
||||
enabled? && service_url.present?
|
||||
end
|
||||
|
||||
def client_cert
|
||||
application_settings.external_auth_client_cert
|
||||
end
|
||||
|
||||
def client_key
|
||||
application_settings.external_auth_client_key
|
||||
end
|
||||
|
||||
def client_key_pass
|
||||
application_settings.external_auth_client_key_pass
|
||||
end
|
||||
|
||||
def has_tls?
|
||||
client_cert.present? && client_key.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def application_settings
|
||||
::Gitlab::CurrentSettings.current_application_settings
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ExternalAuthorization
|
||||
class Logger < ::Gitlab::Logger
|
||||
def self.log_access(access, project_path)
|
||||
status = access.has_access? ? "GRANTED" : "DENIED"
|
||||
message = ["#{status} #{access.user.email} access to '#{access.label}'"]
|
||||
|
||||
message << "(#{project_path})" if project_path.present?
|
||||
message << "- #{access.load_type} #{access.loaded_at}" if access.load_type == :cache
|
||||
|
||||
info(message.join(' '))
|
||||
end
|
||||
|
||||
def self.file_name_noext
|
||||
'external-policy-access-control'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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, 403].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
|
|
@ -423,6 +423,9 @@ msgstr ""
|
|||
msgid "Access forbidden. Check your access level."
|
||||
msgstr ""
|
||||
|
||||
msgid "Access to '%{classification_label}' not allowed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Account"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1704,6 +1707,9 @@ msgstr ""
|
|||
msgid "CiVariable|Validation failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClassificationLabelUnavailable|is unavailable: %{reason}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1734,6 +1740,15 @@ msgstr ""
|
|||
msgid "Click to expand text"
|
||||
msgstr ""
|
||||
|
||||
msgid "Client authentication certificate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Client authentication key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Client authentication key password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clients"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2780,6 +2795,9 @@ msgstr ""
|
|||
msgid "Default artifacts expiration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Default classification label"
|
||||
msgstr ""
|
||||
|
||||
msgid "Default first day of the week"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3196,6 +3214,9 @@ msgstr ""
|
|||
msgid "Enable and configure Prometheus metrics."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable classification control using an external service"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable error tracking"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3625,12 +3646,33 @@ msgstr ""
|
|||
msgid "Explore public groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "External Classification Policy Authorization"
|
||||
msgstr ""
|
||||
|
||||
msgid "External URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "External Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "External authentication"
|
||||
msgstr ""
|
||||
|
||||
msgid "External authorization denied access to this project"
|
||||
msgstr ""
|
||||
|
||||
msgid "External authorization request timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "ExternalAuthorizationService|Classification Label"
|
||||
msgstr ""
|
||||
|
||||
msgid "ExternalAuthorizationService|Classification label"
|
||||
msgstr ""
|
||||
|
||||
msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
|
||||
msgstr ""
|
||||
|
||||
msgid "Facebook"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4346,6 +4388,9 @@ msgstr ""
|
|||
msgid "If enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "If enabled, access to projects will be validated on an external service using their classification label."
|
||||
msgstr ""
|
||||
|
||||
msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
|
||||
msgstr ""
|
||||
|
||||
|
@ -7382,6 +7427,9 @@ msgstr ""
|
|||
msgid "Service Templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Service URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Session duration (minutes)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8124,6 +8172,9 @@ msgstr ""
|
|||
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
|
||||
msgstr ""
|
||||
|
||||
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
|
||||
msgstr ""
|
||||
|
||||
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
|
||||
msgstr ""
|
||||
|
||||
|
@ -8205,6 +8256,9 @@ msgstr ""
|
|||
msgid "The name %{entryName} is already taken in this directory."
|
||||
msgstr ""
|
||||
|
||||
msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
|
||||
msgstr ""
|
||||
|
||||
msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8217,6 +8271,9 @@ msgstr ""
|
|||
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
|
||||
msgstr ""
|
||||
|
||||
msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
|
||||
msgstr ""
|
||||
|
||||
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
|
||||
msgstr ""
|
||||
|
||||
|
@ -8580,6 +8637,9 @@ msgstr ""
|
|||
msgid "Time estimate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
|
||||
msgstr ""
|
||||
|
||||
msgid "Time remaining"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9352,6 +9412,9 @@ msgstr ""
|
|||
msgid "When fast-forward merge is not possible, the user is given the option to rebase."
|
||||
msgstr ""
|
||||
|
||||
msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks."
|
||||
msgstr ""
|
||||
|
||||
msgid "When this merge request is accepted"
|
||||
msgid_plural "When these merge requests are accepted"
|
||||
msgstr[0] ""
|
||||
|
@ -9876,6 +9939,9 @@ msgstr ""
|
|||
msgid "connecting"
|
||||
msgstr ""
|
||||
|
||||
msgid "could not read private key, is the passphrase correct?"
|
||||
msgstr ""
|
||||
|
||||
msgid "customize"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9964,6 +10030,9 @@ msgstr ""
|
|||
msgid "index"
|
||||
msgstr ""
|
||||
|
||||
msgid "is not a valid X509 certificate."
|
||||
msgstr ""
|
||||
|
||||
msgid "issue boards"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10246,6 +10315,9 @@ msgstr ""
|
|||
msgid "private"
|
||||
msgstr ""
|
||||
|
||||
msgid "private key does not match certificate."
|
||||
msgstr ""
|
||||
|
||||
msgid "processing"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -92,6 +92,28 @@ describe Admin::ApplicationSettingsController do
|
|||
expect(response).to redirect_to(admin_application_settings_path)
|
||||
expect(ApplicationSetting.current.default_project_creation).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
|
||||
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',
|
||||
external_authorization_service_timeout: 3,
|
||||
external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'),
|
||||
external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'),
|
||||
external_auth_client_key_pass: "5iveL!fe"
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates settings when the feature is available' do
|
||||
put :update, params: { application_setting: settings }
|
||||
|
||||
settings.each do |attribute, value|
|
||||
expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #reset_registration_token' do
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Boards::IssuesController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:project) { create(:project, :private) }
|
||||
let(:board) { create(:board, project: project) }
|
||||
let(:user) { create(:user) }
|
||||
|
@ -136,6 +138,30 @@ describe Boards::IssuesController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with external authorization' do
|
||||
before do
|
||||
sign_in(user)
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
it 'returns a 403 for group boards' do
|
||||
group = create(:group)
|
||||
group_board = create(:board, group: group)
|
||||
|
||||
list_issues(user: user, board: group_board)
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
||||
it 'is successful for project boards' do
|
||||
project_board = create(:board, project: project)
|
||||
|
||||
list_issues(user: user, board: project_board)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
def list_issues(user:, board:, list: nil)
|
||||
sign_in(user)
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProjectUnauthorized do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
render_views
|
||||
|
||||
describe '#project_unauthorized_proc' do
|
||||
controller(::Projects::ApplicationController) do
|
||||
def show
|
||||
head :ok
|
||||
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, params: { namespace_id: project.namespace.to_param, id: project.to_param }
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'renders a 403 when the service denies access to the project' do
|
||||
external_service_deny_access(user, project)
|
||||
|
||||
get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param }
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
expect(response.body).to match("External authorization denied access to this project")
|
||||
end
|
||||
|
||||
it 'renders a 404 when the user cannot see the project at all' do
|
||||
other_project = create(:project, :private)
|
||||
|
||||
get :show, params: { namespace_id: other_project.namespace.to_param, id: other_project.to_param }
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Dashboard::GroupsController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
|
@ -11,33 +13,43 @@ describe Dashboard::GroupsController do
|
|||
expect(described_class).to include(GroupTree)
|
||||
end
|
||||
|
||||
it 'only includes projects the user is a member of' do
|
||||
member_of_group = create(:group)
|
||||
member_of_group.add_developer(user)
|
||||
create(:group, :public)
|
||||
describe '#index' do
|
||||
it 'only includes projects the user is a member of' do
|
||||
member_of_group = create(:group)
|
||||
member_of_group.add_developer(user)
|
||||
create(:group, :public)
|
||||
|
||||
get :index
|
||||
get :index
|
||||
|
||||
expect(assigns(:groups)).to contain_exactly(member_of_group)
|
||||
end
|
||||
|
||||
context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do
|
||||
let!(:top_level_result) { create(:group, name: 'chef-top') }
|
||||
let!(:top_level_a) { create(:group, name: 'top-a') }
|
||||
let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) }
|
||||
let!(:other_group) { create(:group, name: 'other') }
|
||||
|
||||
before do
|
||||
top_level_result.add_maintainer(user)
|
||||
top_level_a.add_maintainer(user)
|
||||
expect(assigns(:groups)).to contain_exactly(member_of_group)
|
||||
end
|
||||
|
||||
it 'renders only groups the user is a member of when searching hierarchy correctly' do
|
||||
get :index, params: { filter: 'chef' }, format: :json
|
||||
context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do
|
||||
let!(:top_level_result) { create(:group, name: 'chef-top') }
|
||||
let!(:top_level_a) { create(:group, name: 'top-a') }
|
||||
let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) }
|
||||
let!(:other_group) { create(:group, name: 'other') }
|
||||
|
||||
before do
|
||||
top_level_result.add_maintainer(user)
|
||||
top_level_a.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'renders only groups the user is a member of when searching hierarchy correctly' do
|
||||
get :index, params: { filter: 'chef' }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
all_groups = [top_level_result, top_level_a, sub_level_result_a]
|
||||
expect(assigns(:groups)).to contain_exactly(*all_groups)
|
||||
end
|
||||
end
|
||||
|
||||
it 'works when the external authorization service is enabled' do
|
||||
enable_external_authorization_service_check
|
||||
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
all_groups = [top_level_result, top_level_a, sub_level_result_a]
|
||||
expect(assigns(:groups)).to contain_exactly(*all_groups)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,13 +13,17 @@ describe Dashboard::LabelsController do
|
|||
describe "#index" do
|
||||
let!(:unrelated_label) { create(:label, project: create(:project, :public)) }
|
||||
|
||||
subject { get :index, format: :json }
|
||||
|
||||
it 'returns global labels for projects the user has a relationship with' do
|
||||
get :index, format: :json
|
||||
subject
|
||||
|
||||
expect(json_response).to be_kind_of(Array)
|
||||
expect(json_response.size).to eq(1)
|
||||
expect(json_response[0]["id"]).to be_nil
|
||||
expect(json_response[0]["title"]).to eq(label.title)
|
||||
end
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,5 +81,11 @@ describe Dashboard::MilestonesController do
|
|||
expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>")
|
||||
expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>")
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
subject { get :index }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,29 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Dashboard::ProjectsController do
|
||||
it_behaves_like 'authenticates sessionless user', :index, :atom
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
describe '#index' do
|
||||
context 'user not logged in' do
|
||||
it_behaves_like 'authenticates sessionless user', :index, :atom
|
||||
end
|
||||
|
||||
context 'user logged in' do
|
||||
before do
|
||||
sign_in create(:user)
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
it 'works when the external authorization service is enabled' do
|
||||
enable_external_authorization_service_check
|
||||
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'json requests' do
|
||||
render_views
|
||||
|
|
|
@ -105,6 +105,12 @@ describe Dashboard::TodosController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
subject { get :index }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH #restore' do
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Groups::AvatarsController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
|
||||
|
||||
|
@ -15,4 +17,12 @@ describe Groups::AvatarsController do
|
|||
expect(@group.avatar.present?).to be_falsey
|
||||
expect(@group).to be_valid
|
||||
end
|
||||
|
||||
it 'works when external authorization service is enabled' do
|
||||
enable_external_authorization_service_check
|
||||
|
||||
delete :destroy, params: { group_id: group }
|
||||
|
||||
expect(response).to have_gitlab_http_status(302)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,6 +82,10 @@ describe Groups::BoardsController do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service' do
|
||||
subject { list_boards }
|
||||
end
|
||||
|
||||
def list_boards(format: :html)
|
||||
get :index, params: { group_id: group }, format: format
|
||||
end
|
||||
|
@ -160,6 +164,10 @@ describe Groups::BoardsController do
|
|||
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)
|
||||
get :show, params: {
|
||||
group_id: group,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Groups::ChildrenController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:group) { create(:group, :public) }
|
||||
let(:user) { create(:user) }
|
||||
let!(:group_member) { create(:group_member, group: group, user: user) }
|
||||
|
@ -317,5 +319,15 @@ describe Groups::ChildrenController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
it 'works when external authorization service is enabled' do
|
||||
enable_external_authorization_service_check
|
||||
|
||||
get :index, params: { group_id: group }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
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) }
|
||||
|
||||
describe 'GET index' do
|
||||
it 'renders index with 200 status code' do
|
||||
|
@ -263,4 +266,87 @@ describe Groups::GroupMembersController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with external authorization enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
group.add_owner(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'is successful' do
|
||||
get :index, params: { group_id: group }
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
it 'is successful' do
|
||||
post :create, params: { 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,
|
||||
params: {
|
||||
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, params: { 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, params: { 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, params: { 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, params: { 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, params: { group_id: group, id: membership }
|
||||
|
||||
expect(response).to have_gitlab_http_status(302)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,6 +37,12 @@ describe Groups::LabelsController do
|
|||
expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title])
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
subject { get :index, params: { group_id: group.to_param } }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #toggle_subscription' do
|
||||
|
|
|
@ -80,6 +80,12 @@ describe Groups::MilestonesController do
|
|||
expect(response.content_type).to eq 'application/json'
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
subject { get :index, params: { group_id: group.to_param } }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#show' do
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Groups::Settings::CiCdController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:group) { create(:group) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
|
@ -33,6 +35,19 @@ describe Groups::Settings::CiCdController do
|
|||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'renders show with 200 status code' do
|
||||
get :show, params: { group_id: group }
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #reset_registration_token' do
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Groups::VariablesController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:group) { create(:group) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
|
@ -34,4 +36,36 @@ describe Groups::VariablesController do
|
|||
|
||||
include_examples 'PATCH #update updates variables'
|
||||
end
|
||||
|
||||
context 'with external authorization enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
let!(:variable) { create(:ci_group_variable, group: group) }
|
||||
|
||||
it 'is successful' do
|
||||
get :show, params: { 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,
|
||||
params: {
|
||||
group_id: group,
|
||||
variables_attributes: [{ id: variable.id, key: 'hello' }]
|
||||
},
|
||||
format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe GroupsController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:admin) { create(:admin) }
|
||||
let(:group) { create(:group, :public) }
|
||||
|
@ -665,4 +667,98 @@ describe GroupsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'external authorization' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'with external authorization service enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'is successful' do
|
||||
get :show, params: { id: group.to_param }
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'does not allow other formats' do
|
||||
get :show, params: { id: group.to_param }, format: :atom
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
it 'is successful' do
|
||||
get :edit, params: { 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, params: { 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, params: { 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, params: { id: group.to_param }
|
||||
|
||||
expect(response).to have_gitlab_http_status(302)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #activity' do
|
||||
subject { get :activity, params: { id: group.to_param } }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
|
||||
describe 'GET #issues' do
|
||||
subject { get :issues, params: { id: group.to_param } }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
|
||||
describe 'GET #merge_requests' do
|
||||
subject { get :merge_requests, params: { id: group.to_param } }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,6 +98,10 @@ describe Projects::BoardsController do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'unauthorized when external service denies access' do
|
||||
subject { list_boards }
|
||||
end
|
||||
|
||||
def list_boards(format: :html)
|
||||
get :index, params: {
|
||||
namespace_id: project.namespace,
|
||||
|
|
|
@ -127,6 +127,17 @@ describe Projects::IssuesController do
|
|||
expect(assigns(:issues).size).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
before do
|
||||
sign_in user
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'unauthorized when external service denies access' do
|
||||
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require('spec_helper')
|
||||
|
||||
describe ProjectsController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include ProjectForksHelper
|
||||
|
||||
let(:project) { create(:project) }
|
||||
|
@ -411,6 +412,37 @@ describe ProjectsController do
|
|||
|
||||
it_behaves_like 'updating a project'
|
||||
end
|
||||
|
||||
context 'as maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'unauthorized when external service denies access' do
|
||||
subject do
|
||||
put :update,
|
||||
params: {
|
||||
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
|
||||
|
||||
describe '#transfer' do
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe SearchController do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
|
@ -76,4 +78,41 @@ describe SearchController do
|
|||
expect(assigns[:search_objects].count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with external authorization service enabled' do
|
||||
let(:project) { create(:project, namespace: user.namespace) }
|
||||
let(:note) { create(:note_on_issue, project: project) }
|
||||
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'renders a 403 when no project is given' do
|
||||
get :show, params: { scope: 'notes', search: note.note }
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
||||
it 'renders a 200 when a project was set' do
|
||||
get :show, params: { 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 403 when no project is given' do
|
||||
get :autocomplete, params: { term: 'hello' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
||||
it 'renders a 200 when a project was set' do
|
||||
get :autocomplete, params: { project_id: project.id, term: 'hello' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -223,6 +223,12 @@ describe UsersController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
subject { get :calendar_activities, params: { username: user.username } }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
|
||||
def create_push_event
|
||||
push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user)
|
||||
EventCreateService.new.push(project, public_user, push_data)
|
||||
|
@ -286,6 +292,12 @@ describe UsersController do
|
|||
expect(JSON.parse(response.body)).to have_key('html')
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
subject { get :snippets, params: { username: user.username } }
|
||||
|
||||
it_behaves_like 'disabled when using an external authorization service'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #exists' do
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe '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_button('Projects')
|
||||
expect(page).to have_button('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_check
|
||||
visit dashboard_groups_path
|
||||
|
||||
within('.navbar') do
|
||||
expect(page).to have_button('Projects')
|
||||
expect(page).to have_button('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
|
|
@ -0,0 +1,58 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'The group page' do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
def expect_all_sidebar_links
|
||||
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('Issues')
|
||||
expect(page).to have_link('Merge Requests')
|
||||
expect(page).to have_link('Members')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'The sidebar' do
|
||||
it 'has all the expected links' do
|
||||
visit group_path(group)
|
||||
|
||||
expect_all_sidebar_links
|
||||
end
|
||||
|
||||
it 'shows all project features when policy control is enabled' do
|
||||
stub_application_setting(external_authorization_service_enabled: true)
|
||||
|
||||
visit group_path(group)
|
||||
|
||||
expect_all_sidebar_links
|
||||
end
|
||||
|
||||
it 'hides some links when an external authorization service configured with an url' do
|
||||
enable_external_authorization_service_check
|
||||
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
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Classification label on project pages' do
|
||||
let(:project) do
|
||||
create(:project, external_authorization_classification_label: 'authorized label')
|
||||
end
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_application_setting(external_authorization_service_enabled: true)
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'shows the classification label on the project page' do
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_content('authorized label')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
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_maintainer(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_check
|
||||
|
||||
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
|
|
@ -0,0 +1,128 @@
|
|||
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(:other_merge_request) do
|
||||
create(:merge_request, :closed,
|
||||
title: 'I am a merge request in another project',
|
||||
source_project: other_project)
|
||||
end
|
||||
let(:description_referencing_other_issue) do
|
||||
"Referencing: #{other_issue.to_reference(project)}, "\
|
||||
"a confidential issue #{confidential_issue.to_reference}, "\
|
||||
"a cross project confidential issue #{other_confidential_issue.to_reference(project)}, and "\
|
||||
"a cross project merge request #{other_merge_request.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_check
|
||||
end
|
||||
|
||||
it 'only hits the external service for the project the user is viewing' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(user, 'default_label', any_args).at_least(1).and_return(true)
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.not_to receive(:access_allowed?).with(user, 'other_label', any_args)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
it 'shows only the link to the cross project references' do
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
expect(page).to have_link("#{other_issue.to_reference(project)}")
|
||||
expect(page).to have_link("#{other_merge_request.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}']")
|
||||
expect(page).not_to have_content("#{other_merge_request.to_reference(project)} (#{other_merge_request.state})")
|
||||
expect(page).not_to have_xpath("//a[@title='#{other_merge_request.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
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Projects > Settings > External Authorization Classification Label setting' do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project_empty_repo) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'shows the field to set a classification label' do
|
||||
stub_application_setting(external_authorization_service_enabled: true)
|
||||
|
||||
visit edit_project_path(project)
|
||||
|
||||
expect(page).to have_selector('#project_external_authorization_classification_label')
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'User page' do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'with public profile' do
|
||||
|
@ -86,4 +88,24 @@ describe 'User page' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'most recent activity' do
|
||||
it 'shows the most recent activity' do
|
||||
visit(user_path(user))
|
||||
|
||||
expect(page).to have_content('Most Recent Activity')
|
||||
end
|
||||
|
||||
context 'when external authorization is enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
it 'hides the most recent activity' do
|
||||
visit(user_path(user))
|
||||
|
||||
expect(page).not_to have_content('Most Recent Activity')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -531,6 +531,13 @@ describe IssuesFinder do
|
|||
expect(issues.count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' 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
|
||||
end
|
||||
|
||||
describe '#row_count', :request_store do
|
||||
|
|
|
@ -226,5 +226,12 @@ describe LabelsFinder do
|
|||
expect(finder.execute).to eq [project_label_1]
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' 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
|
||||
end
|
||||
end
|
||||
|
|
|
@ -282,6 +282,13 @@ describe MergeRequestsFinder do
|
|||
expect(finder.row_count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' 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
|
||||
end
|
||||
|
||||
context 'when projects require different access levels for merge requests' do
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe SnippetsFinder do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include Gitlab::Allowable
|
||||
|
||||
describe '#initialize' do
|
||||
|
@ -164,4 +165,35 @@ describe SnippetsFinder do
|
|||
end
|
||||
|
||||
it_behaves_like 'snippet visibility'
|
||||
|
||||
context 'external authorization' do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let!(:snippet) { create(:project_snippet, :public, project: project) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a finder with external authorization service' do
|
||||
let!(:subject) { create(:project_snippet, project: project) }
|
||||
let(:project_params) { { project: project } }
|
||||
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
|
||||
|
|
|
@ -47,6 +47,13 @@ describe TodosFinder do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' 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
|
||||
end
|
||||
|
||||
describe '#sort' do
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIEpTCCAo0CAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
|
||||
MB4XDTE4MDMyMzE0MDIwOFoXDTE5MDMyMzE0MDIwOFowHTEbMBkGA1UEAwwSZ2l0
|
||||
bGFiLXBhc3NwaHJhc2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
|
||||
zpsWHOewP/khfDsLUWxaRCinrBzVJm2C01bVahKVR3g/JD4vEH901Wod9Pvbh/9e
|
||||
PEfE+YZmgSUUopbL3JUheMnyW416F43HKE/fPW4+QeuIEceuhCXg20eOXmvnWWNM
|
||||
0hXZh4hq69rwvMPREC/LkZy/QkTDKhJNLNAqAQu2AJ3C7Yga8hFQYEhx1hpfGtwD
|
||||
z/Nf3efat9WN/d6yW9hfJ98NCmImTm5l9Pc0YPNWCAf96vsqsNHBrTkFy6CQwkhH
|
||||
K1ynVYuqnHYxSc4FPCT5SAleD9gR/xFBAHb7pPy4yGxMSEmiWaMjjZCVPsghj1jM
|
||||
Ej77MTDL3U9LeDfiILhvZ+EeQxqPiFwwG2eaIn3ZEs2Ujvw7Z2VpG9VMcPTnB4jK
|
||||
ot6qPM1YXnkGWQ6iT0DTPS3h7zg1xIJXI5N2sI6GXuKrXXwZ1wPqzFLKPv+xBjp8
|
||||
P6dih+EImfReFi9zIO1LqGMY+XmRcqodsb6jzsmBimJkqBtatJM7FuUUUN56wiaj
|
||||
q9+BWbm+ZdQ2lvqndMljjUjTh6pNERfGAJgkNuLn3X9hXVE0TSpmn0nOgaL5izP3
|
||||
7FWUt0PTyGgK2zq9SEhZmK2TKckLkKMk/ZBBBVM/nrnjs72IlbsqdcVoTnApytZr
|
||||
xVYTj1hV7QlAfaU3w/M534qXDiy8+HfX5ksWQMtSklECAwEAATANBgkqhkiG9w0B
|
||||
AQUFAAOCAgEAMMhzSRq9PqCpui74nwjhmn8Dm2ky7A+MmoXNtk70cS/HWrjzaacb
|
||||
B/rxsAUp7f0pj4QMMM0ETMFpbNs8+NPd2FRY0PfWE4yyDpvZO2Oj1HZKLHX72Gjn
|
||||
K5KB9DYlVsXhGPfuFWXpxGWF2Az9hDWnj58M3DOAps+6tHuAtudQUuwf5ENQZWwE
|
||||
ySpr7yoHm1ykgl0Tsb9ZHi9qLrWRRMNYXRT+gvwP1bba8j9jOtjO/xYiIskwMPLM
|
||||
W8SFmQxbg0Cvi8Q89PB6zoTNOhPQyoyeSlw9meeZJHAMK2zxeglEm8C4EQ+I9Y6/
|
||||
yylM5/Sc55TjWAvRFgbsq+OozgMvffk/Q2fzcGF44J9DEQ7nrhmJxJ+X4enLknR5
|
||||
Hw4+WhdYA+bwjx3YZBNTh9/YMgNPYwQhf5gtcZGTd6X4j6qZfJ6CXBmhkC1Cbfyl
|
||||
yM7B7i4JAqPWMeDP50pXCgyKlwgw1JuFW+xkbkYQAj7wtggQ6z1Vjb5W8R8kYn9q
|
||||
LXClVtThEeSV5KkVwNX21aFcUs8qeQ+zsgKqpEyM5oILQQ1gDSxLTtrr2KuN+WJN
|
||||
wM0acwD45X7gA/aZYpCGkIgHIBq0zIDP1s6IqeebFJjW8lWofhRxOEWomWdRweJG
|
||||
N7qQ1WCTQxAPGAkDI8QPjaspvnAhFKmpBG/mR5IXLFKDbttu7WNdYDo=
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,54 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-128-CBC,79CCB506B0FD42A6F1BAE6D72E1CB20C
|
||||
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
|
||||
MB4XDTE4MDMxOTE1MjYzMloXDTE5MDMxOTE1MjYzMlowFDESMBAGA1UEAwwJbG9j
|
||||
YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+tcM7iphsLlR
|
||||
ccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzAn/eVU4jyVWkaBym6MHa8CiDOro9H
|
||||
OXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/2FAgFWzrB2HnYSShiN8tBeeDI5cJ
|
||||
ii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UTK37k2kbDQZ41rv1ng2w0AUZt0LRA
|
||||
NWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ
|
||||
+1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNkI+cyv0Gle6tk+CkOfE1m0CvNWlNg
|
||||
b8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5xMXpdUCsh22CZZHe/4SeFE64amkf
|
||||
1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q/nLdY8haMC6KOtpbAWvKX/Jqq0z1
|
||||
nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVrVef0pb2mfdtzjzUrYCP0PtnQExPB
|
||||
rocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8RSvAoEUs9VbPiUfN7WAyU1K1rTYH
|
||||
KV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRlq07Q5LDz33h9KXw1LZT8MWRinVJf
|
||||
RePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA
|
||||
Skp0tbvVsg3RG2pX0GP25j0ix+f78zG0+BJ6LiKGMoCIBtGKitfUjBg83ru/ILpa
|
||||
fpgrQpNQVUnGQ9tmpnqV605ZBBRUC1CRDsvUnyN6p7+yQAq6Fl+2ZKONHpPk+Bl4
|
||||
CIewgdkHjTwTpvIM/1DFVCz4R1FxNjY3uqOVcNDczMYEk2Pn2GZNNN35hUHHxWh4
|
||||
89ZvI+XKuRFZq3cDPA60PySeJJpCRScWGgnkdEX1gTtWH3WUlq9llxIvRexyNyzZ
|
||||
Yqvcfx5UT75/Pp+JPh9lpUCcKLHeUiadjkiLxu3IcrYa4gYx4lA8jgm7adNEahd0
|
||||
oMAHoO9DU6XMo7o6tnQH3xQv9RAbQanjuyJR9N7mwmc59bQ6mW+pxCk843GwT73F
|
||||
slseJ1nE1fQQQD7mn/KGjmeWtxY2ElUjTay9ff9/AgJeQYRW+oH0cSdo8WCpc2+G
|
||||
+LZtLWfBgFLHseRlmarSe2pP8KmbaTd3q7Bu0GekVQOxYcNX59Pj4muQZDVLh8aX
|
||||
mSQ+Ifts/ljT649MISHn2AZMR4+BUx63tFcatQhbAGGH5LeFdbaGcaVdsUVyZ9a2
|
||||
HBmFWNsgEPtcC+WmNzCXbv7jQsLAJXufKG5MnurJgNf/n5uKCmpGsEJDT/KF1k/3
|
||||
x9YnqM7zTyV6un+LS3HjEJvwQmqPWe+vFAeXWGCoWxE=
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,51 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,142 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 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(Gitlab::ExternalAuthorization::Client).to receive(:new) { 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 '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 ::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 '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
|
|
@ -0,0 +1,48 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 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
|
|
@ -0,0 +1,97 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ExternalAuthorization::Client do
|
||||
let(:user) { build(:user, email: 'dummy_user@example.com') }
|
||||
let(:dummy_url) { 'https://dummy.net/' }
|
||||
subject(:client) { described_class.new(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 'respects the the timeout' do
|
||||
stub_application_setting(
|
||||
external_authorization_service_timeout: 3
|
||||
)
|
||||
|
||||
expect(Excon).to receive(:post).with(dummy_url,
|
||||
hash_including(
|
||||
connect_timeout: 3,
|
||||
read_timeout: 3,
|
||||
write_timeout: 3
|
||||
))
|
||||
|
||||
client.request_access
|
||||
end
|
||||
|
||||
it 'adds the mutual tls params when they are present' do
|
||||
stub_application_setting(
|
||||
external_auth_client_cert: 'the certificate data',
|
||||
external_auth_client_key: 'the key data',
|
||||
external_auth_client_key_pass: 'open sesame'
|
||||
)
|
||||
expected_params = {
|
||||
client_cert_data: 'the certificate data',
|
||||
client_key_data: 'the key data',
|
||||
client_key_pass: 'open sesame'
|
||||
}
|
||||
|
||||
expect(Excon).to receive(:post).with(dummy_url, hash_including(expected_params))
|
||||
|
||||
client.request_access
|
||||
end
|
||||
|
||||
it 'returns an expected response' do
|
||||
expect(Excon).to receive(:post)
|
||||
|
||||
expect(client.request_access)
|
||||
.to be_kind_of(::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(::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
|
|
@ -0,0 +1,45 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ExternalAuthorization::Logger do
|
||||
let(:request_time) { Time.parse('2018-03-26 20:22:15') }
|
||||
|
||||
def fake_access(has_access, user, load_type = :request)
|
||||
access = double('access')
|
||||
allow(access).to receive_messages(user: user,
|
||||
has_access?: has_access,
|
||||
loaded_at: request_time,
|
||||
label: 'dummy_label',
|
||||
load_type: load_type)
|
||||
|
||||
access
|
||||
end
|
||||
|
||||
describe '.log_access' do
|
||||
it 'logs a nice message for an access request' do
|
||||
expected_message = "GRANTED admin@example.com access to 'dummy_label' (the/project/path)"
|
||||
fake_access = fake_access(true, build(:user, email: 'admin@example.com'))
|
||||
|
||||
expect(described_class).to receive(:info).with(expected_message)
|
||||
|
||||
described_class.log_access(fake_access, 'the/project/path')
|
||||
end
|
||||
|
||||
it 'does not trip without a project path' do
|
||||
expected_message = "DENIED admin@example.com access to 'dummy_label'"
|
||||
fake_access = fake_access(false, build(:user, email: 'admin@example.com'))
|
||||
|
||||
expect(described_class).to receive(:info).with(expected_message)
|
||||
|
||||
described_class.log_access(fake_access, nil)
|
||||
end
|
||||
|
||||
it 'adds the load time for cached accesses' do
|
||||
expected_message = "DENIED admin@example.com access to 'dummy_label' - cache #{request_time}"
|
||||
fake_access = fake_access(false, build(:user, email: 'admin@example.com'), :cache)
|
||||
|
||||
expect(described_class).to receive(:info).with(expected_message)
|
||||
|
||||
described_class.log_access(fake_access, nil)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ExternalAuthorization::Response do
|
||||
let(:excon_response) { double }
|
||||
subject(:response) { described_class.new(excon_response) }
|
||||
|
||||
describe '#valid?' do
|
||||
it 'is valid for 200, 401, and 403 responses' do
|
||||
[200, 401, 403].each do |status|
|
||||
allow(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
|
||||
|
||||
it 'is `false` if the status is 401 or 403' do
|
||||
[401, 403].each do |status|
|
||||
allow(excon_response).to receive(:status).and_return(status)
|
||||
|
||||
expect(response).not_to be_successful
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 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 always 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_check
|
||||
|
||||
expect(::Gitlab::ExternalAuthorization::Access)
|
||||
.to receive(:new).with(user, label).once.and_call_original
|
||||
|
||||
2.times { described_class.access_for_user_to_label(user, label, nil) }
|
||||
end
|
||||
|
||||
it 'logs the access request once per request' do
|
||||
expect(::Gitlab::ExternalAuthorization::Logger)
|
||||
.to receive(:log_access)
|
||||
.with(an_instance_of(::Gitlab::ExternalAuthorization::Access),
|
||||
'the/project/path')
|
||||
.once
|
||||
|
||||
2.times { described_class.access_for_user_to_label(user, label, 'the/project/path') }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -496,6 +496,7 @@ Project:
|
|||
- merge_requests_ff_only_enabled
|
||||
- merge_requests_rebase_enabled
|
||||
- jobs_cache_index
|
||||
- external_authorization_classification_label
|
||||
- pages_https_only
|
||||
Author:
|
||||
- name
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ApplicationSetting do
|
||||
let(:setting) { described_class.create_from_defaults }
|
||||
subject(:setting) { described_class.create_from_defaults }
|
||||
|
||||
it { include(CacheableAttributes) }
|
||||
it { include(ApplicationSettingImplementation) }
|
||||
|
@ -284,6 +284,52 @@ describe ApplicationSetting do
|
|||
expect(subject).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when external authorization service is enabled' do
|
||||
before do
|
||||
setting.external_authorization_service_enabled = true
|
||||
end
|
||||
|
||||
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.to allow_value('').for(:external_authorization_service_url) }
|
||||
it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) }
|
||||
it { is_expected.not_to allow_value(11).for(:external_authorization_service_timeout) }
|
||||
it { is_expected.not_to allow_value(0).for(:external_authorization_service_timeout) }
|
||||
it { is_expected.not_to allow_value('not a certificate').for(:external_auth_client_cert) }
|
||||
it { is_expected.to allow_value('').for(:external_auth_client_cert) }
|
||||
it { is_expected.to allow_value('').for(:external_auth_client_key) }
|
||||
|
||||
context 'when setting a valid client certificate for external authorization' do
|
||||
let(:certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') }
|
||||
|
||||
before do
|
||||
setting.external_auth_client_cert = certificate_data
|
||||
end
|
||||
|
||||
it 'requires a valid client key when a certificate is set' do
|
||||
expect(setting).not_to allow_value('fefefe').for(:external_auth_client_key)
|
||||
end
|
||||
|
||||
it 'requires a matching certificate' do
|
||||
other_private_key = File.read('spec/fixtures/x509_certificate_pk.key')
|
||||
|
||||
expect(setting).not_to allow_value(other_private_key).for(:external_auth_client_key)
|
||||
end
|
||||
|
||||
it 'the credentials are valid when the private key can be read and matches the certificate' do
|
||||
tls_attributes = [:external_auth_client_key_pass,
|
||||
:external_auth_client_key,
|
||||
:external_auth_client_cert]
|
||||
setting.external_auth_client_key = File.read('spec/fixtures/passphrase_x509_certificate_pk.key')
|
||||
setting.external_auth_client_key_pass = '5iveL!fe'
|
||||
|
||||
setting.validate
|
||||
|
||||
expect(setting.errors).not_to include(*tls_attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'restrict creating duplicates' do
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProtectedRefAccess do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
subject(:protected_ref_access) do
|
||||
create(:protected_branch, :maintainers_can_push).push_access_levels.first
|
||||
end
|
||||
|
@ -29,5 +31,15 @@ describe ProtectedRefAccess do
|
|||
|
||||
expect(protected_ref_access.check_access(developer)).to be_falsy
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
it 'is false if external authorization denies access' do
|
||||
maintainer = create(:user)
|
||||
project.add_maintainer(maintainer)
|
||||
external_service_deny_access(maintainer, project)
|
||||
|
||||
expect(protected_ref_access.check_access(maintainer)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Issue do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
describe "Associations" do
|
||||
it { is_expected.to belong_to(:milestone) }
|
||||
it { is_expected.to have_many(:assignees) }
|
||||
|
@ -779,4 +781,47 @@ describe Issue do
|
|||
it_behaves_like 'throttled touch' do
|
||||
subject { create(:issue, updated_at: 1.hour.ago) }
|
||||
end
|
||||
|
||||
context 'when an external authentication service' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
describe '#visible_to_user?' do
|
||||
it 'is `false` when an external authorization service is enabled' do
|
||||
issue = build(:issue, project: build(:project, :public))
|
||||
|
||||
expect(issue).not_to be_visible_to_user
|
||||
end
|
||||
|
||||
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(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
|
||||
expect(issue.visible_to_user?(user)).to be_falsy
|
||||
end
|
||||
|
||||
it 'does not check the external service if a user does not have access to the project' do
|
||||
project = build(:project, :private,
|
||||
external_authorization_classification_label: 'a-label')
|
||||
issue = build(:issue, project: project)
|
||||
user = build(:user)
|
||||
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
expect(issue.visible_to_user?(user)).to be_falsy
|
||||
end
|
||||
|
||||
it 'does not check the external webservice for admins' do
|
||||
issue = build(:issue)
|
||||
user = build(:admin)
|
||||
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
|
||||
issue.visible_to_user?(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
|||
describe Project do
|
||||
include ProjectForksHelper
|
||||
include GitHelpers
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
it_behaves_like 'having unique enum values'
|
||||
|
||||
|
@ -4417,6 +4418,25 @@ describe Project do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#external_authorization_classification_label' do
|
||||
it 'falls back to the default when none is configured' do
|
||||
enable_external_authorization_service_check
|
||||
|
||||
expect(build(:project).external_authorization_classification_label)
|
||||
.to eq('default_label')
|
||||
end
|
||||
|
||||
it 'returns the classification label if it was configured on the project' do
|
||||
enable_external_authorization_service_check
|
||||
|
||||
project = build(:project,
|
||||
external_authorization_classification_label: 'hello')
|
||||
|
||||
expect(project.external_authorization_classification_label)
|
||||
.to eq('hello')
|
||||
end
|
||||
end
|
||||
|
||||
describe "#pages_https_only?" do
|
||||
subject { build(:project) }
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe BasePolicy do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
describe '.class_for' do
|
||||
it 'detects policy class based on the subject ancestors' do
|
||||
expect(DeclarativePolicy.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy)
|
||||
|
@ -16,4 +18,25 @@ describe BasePolicy do
|
|||
expect(DeclarativePolicy.class_for(:global)).to eq(GlobalPolicy)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'read cross project' do
|
||||
let(:current_user) { create(:user) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { described_class.new(current_user, [user]) }
|
||||
|
||||
it { is_expected.to be_allowed(:read_cross_project) }
|
||||
|
||||
context 'when an external authorization service is enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe IssuePolicy do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:guest) { create(:user) }
|
||||
let(:author) { create(:user) }
|
||||
let(:assignee) { create(:user) }
|
||||
|
@ -204,4 +206,21 @@ describe IssuePolicy do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
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_check
|
||||
end
|
||||
|
||||
it 'can read the issue iid without accessing the external service' do
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
|
||||
expect(policies).to be_allowed(:read_issue_iid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe MergeRequestPolicy do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:guest) { create(:user) }
|
||||
let(:author) { create(:user) }
|
||||
let(:developer) { create(:user) }
|
||||
|
@ -47,4 +49,21 @@ describe MergeRequestPolicy do
|
|||
expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request)
|
||||
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_check
|
||||
end
|
||||
|
||||
it 'can read the issue iid without accessing the external service' do
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
|
||||
expect(policies).to be_allowed(:read_merge_request_iid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProjectPolicy do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include_context 'ProjectPolicy context'
|
||||
set(:guest) { create(:user) }
|
||||
set(:reporter) { create(:user) }
|
||||
|
@ -292,4 +293,56 @@ describe ProjectPolicy do
|
|||
projects: [clusterable])
|
||||
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(::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_check
|
||||
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(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
|
||||
expect(described_class.new(admin, project)).to be_allowed(:read_project)
|
||||
end
|
||||
|
||||
it 'prevents all but seeing a public project in a list when access is denied' do
|
||||
[developer, owner, build(:user), nil].each do |user|
|
||||
external_service_deny_access(user, project)
|
||||
policy = described_class.new(user, 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
|
||||
|
||||
it 'passes the full path to external authorization for logging purposes' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(owner, 'default_label', project.full_path).and_call_original
|
||||
|
||||
described_class.new(owner, project).allowed?(:read_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -46,6 +46,8 @@ shared_examples 'languages and percentages JSON response' do
|
|||
end
|
||||
|
||||
describe API::Projects do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:user3) { create(:user) }
|
||||
|
@ -1336,6 +1338,39 @@ describe API::Projects do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with external authorization' do
|
||||
let(:project) do
|
||||
create(:project,
|
||||
namespace: user.namespace,
|
||||
external_authorization_classification_label: 'the-label')
|
||||
end
|
||||
|
||||
context 'when the user has access to the project' do
|
||||
before do
|
||||
external_service_allow_access(user, project)
|
||||
end
|
||||
|
||||
it 'includes the label in the response' do
|
||||
get api("/projects/#{project.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(json_response['external_authorization_classification_label']).to eq('the-label')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the external service denies access' do
|
||||
before do
|
||||
external_service_deny_access(user, project)
|
||||
end
|
||||
|
||||
it 'returns a 404' do
|
||||
get api("/projects/#{project.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/users' do
|
||||
|
@ -1890,6 +1925,20 @@ describe API::Projects do
|
|||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when updating external classification' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
it 'updates the classification label' do
|
||||
put(api("/projects/#{project.id}", user), params: { external_authorization_classification_label: 'new label' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
|
||||
expect(project.reload.external_authorization_classification_label).to eq('new label')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/:id/archive' do
|
||||
|
|
|
@ -116,6 +116,39 @@ describe API::Settings, 'Settings' do
|
|||
expect(json_response['performance_bar_allowed_group_id']).to be_nil
|
||||
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',
|
||||
external_authorization_service_timeout: 9.99,
|
||||
external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'),
|
||||
external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'),
|
||||
external_auth_client_key_pass: "5iveL!fe"
|
||||
}
|
||||
end
|
||||
let(:attribute_names) { settings.keys.map(&:to_s) }
|
||||
|
||||
it 'includes the attributes in the API' do
|
||||
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), params: settings
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
settings.each do |attribute, value|
|
||||
expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "missing plantuml_url value when plantuml_enabled is true" do
|
||||
it "returns a blank parameter error message" do
|
||||
put api("/application/settings", admin), params: { plantuml_enabled: true }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe GroupChildEntity do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include Gitlab::Routing.url_helpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
@ -109,4 +110,22 @@ describe GroupChildEntity do
|
|||
|
||||
it_behaves_like 'group child json'
|
||||
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_check
|
||||
object.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'does not hit the external authorization service' do
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
|
||||
expect(json[:can_edit]).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ApplicationSettings::UpdateService do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:application_settings) { create(:application_setting) }
|
||||
let(:admin) { create(:user, :admin) }
|
||||
let(:params) { {} }
|
||||
|
@ -143,4 +145,37 @@ describe ApplicationSettings::UpdateService do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when external authorization is enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
it 'does not save the settings with an error if the service denies access' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(admin, 'new-label') { false }
|
||||
|
||||
described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute
|
||||
|
||||
expect(application_settings.errors[:external_authorization_service_default_label]).to be_present
|
||||
end
|
||||
|
||||
it 'saves the setting when the user has access to the label' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(admin, 'new-label') { true }
|
||||
|
||||
described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute
|
||||
|
||||
# Read the attribute directly to avoid the stub from
|
||||
# `enable_external_authorization_service_check`
|
||||
expect(application_settings[:external_authorization_service_default_label]).to eq('new-label')
|
||||
end
|
||||
|
||||
it 'does not validate the label if it was not passed' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.not_to receive(:access_allowed?)
|
||||
|
||||
described_class.new(application_settings, admin, { home_page_url: 'http://foo.bar' }).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ require 'spec_helper'
|
|||
|
||||
describe NotificationService, :mailer do
|
||||
include EmailSpec::Matchers
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include NotificationHelpers
|
||||
|
||||
let(:notification) { described_class.new }
|
||||
|
@ -2218,6 +2219,46 @@ describe NotificationService, :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with external authorization service' do
|
||||
let(:issue) { create(:issue) }
|
||||
let(:project) { issue.project }
|
||||
let(:note) { create(:note, noteable: issue, project: project) }
|
||||
let(:member) { create(:user) }
|
||||
|
||||
subject { NotificationService.new }
|
||||
|
||||
before do
|
||||
project.add_maintainer(member)
|
||||
member.global_notification_setting.update!(level: :watch)
|
||||
end
|
||||
|
||||
it 'sends email when the service is not enabled' do
|
||||
expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original
|
||||
|
||||
subject.new_issue(issue, member)
|
||||
end
|
||||
|
||||
context 'when the service is enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
it 'does not send an email' do
|
||||
expect(Notify).not_to receive(:new_issue_email)
|
||||
|
||||
subject.new_issue(issue, member)
|
||||
end
|
||||
|
||||
it 'still delivers email to admins' do
|
||||
member.update!(admin: true)
|
||||
|
||||
expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original
|
||||
|
||||
subject.new_issue(issue, member)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_team(project)
|
||||
@u_watcher = create_global_setting_for(create(:user), :watch)
|
||||
@u_participating = create_global_setting_for(create(:user), :participating)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::CreateService, '#execute' do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include GitHelpers
|
||||
|
||||
let(:gitlab_shell) { Gitlab::Shell.new }
|
||||
|
@ -344,6 +345,42 @@ describe Projects::CreateService, '#execute' do
|
|||
expect(rugged.config['gitlab.fullpath']).to eq project.full_path
|
||||
end
|
||||
|
||||
context 'with external authorization enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
it 'does not save the project with an error if the service denies access' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(user, 'new-label', any_args) { false }
|
||||
|
||||
project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' }))
|
||||
|
||||
expect(project.errors[:external_authorization_classification_label]).to be_present
|
||||
expect(project).not_to be_persisted
|
||||
end
|
||||
|
||||
it 'saves the project when the user has access to the label' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(user, 'new-label', any_args) { true }
|
||||
|
||||
project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' }))
|
||||
|
||||
expect(project).to be_persisted
|
||||
expect(project.external_authorization_classification_label).to eq('new-label')
|
||||
end
|
||||
|
||||
it 'does not save the project when the user has no access to the default label and no label is provided' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(user, 'default_label', any_args) { false }
|
||||
|
||||
project = create_project(user, opts)
|
||||
|
||||
expect(project.errors[:external_authorization_classification_label]).to be_present
|
||||
expect(project).not_to be_persisted
|
||||
end
|
||||
end
|
||||
|
||||
def create_project(user, opts)
|
||||
Projects::CreateService.new(user, opts).execute
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::UpdateService do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include ProjectForksHelper
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
@ -361,6 +362,46 @@ describe Projects::UpdateService do
|
|||
call_service
|
||||
end
|
||||
end
|
||||
|
||||
context 'with external authorization enabled' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
it 'does not save the project with an error if the service denies access' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(user, 'new-label') { false }
|
||||
|
||||
result = update_project(project, user, { external_authorization_classification_label: 'new-label' })
|
||||
|
||||
expect(result[:message]).to be_present
|
||||
expect(result[:status]).to eq(:error)
|
||||
end
|
||||
|
||||
it 'saves the new label if the service allows access' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(user, 'new-label') { true }
|
||||
|
||||
result = update_project(project, user, { external_authorization_classification_label: 'new-label' })
|
||||
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(project.reload.external_authorization_classification_label).to eq('new-label')
|
||||
end
|
||||
|
||||
it 'checks the default label when the classification label was cleared' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?).with(user, 'default_label') { true }
|
||||
|
||||
update_project(project, user, { external_authorization_classification_label: '' })
|
||||
end
|
||||
|
||||
it 'does not check the label when it does not change' do
|
||||
expect(::Gitlab::ExternalAuthorization)
|
||||
.not_to receive(:access_allowed?)
|
||||
|
||||
update_project(project, user, { name: 'New name' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#run_auto_devops_pipeline?' do
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
module ExternalAuthorizationServiceHelpers
|
||||
def enable_external_authorization_service_check
|
||||
stub_application_setting(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_check
|
||||
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(::Gitlab::ExternalAuthorization)
|
||||
.to receive(:access_allowed?)
|
||||
.with(user, classification_label, any_args)
|
||||
.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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue