Commit 980b8292 authored by Imre Farkas's avatar Imre Farkas Committed by Andreas Brandl

spec/controllers/concerns/project_unauthorized_spec to use ExternalAuthorizationServiceHelpers

...instead of EEExternalAuthorizationServiceHelpers
parent fd00a835
......@@ -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: [],
......
# 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
ProjectUnauthorized.prepend(EE::ProjectUnauthorized)
......@@ -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
......
......@@ -305,6 +305,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,7 +22,14 @@ 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)
......
......@@ -2,14 +2,14 @@
module ValidatesClassificationLabel
def validate_classification_label(record, attribute_name)
return unless EE::Gitlab::ExternalAuthorization.enabled?
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 EE::Gitlab::ExternalAuthorization.access_allowed?(current_user, new_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)
......@@ -17,7 +17,7 @@ module ValidatesClassificationLabel
end
def rejection_reason_for_label(label)
reason_from_service = EE::Gitlab::ExternalAuthorization.rejection_reason(current_user, label).presence
reason_from_service = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label).presence
reason_from_service || _("Access to '%{classification_label}' not allowed") % { classification_label: label }
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?
......
- return unless License.feature_available?(:external_authorization_service)
%section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
......
......@@ -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
......
......@@ -8,7 +8,7 @@
= render "layouts/header/ee_license_banner"
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/nav/ee/classification_level_banner"
= render "layouts/nav/classification_level_banner"
= yield :flash_message
= render "shared/ping_consent"
- unless @hide_breadcrumbs
......
- if EE::Gitlab::ExternalAuthorization.enabled? && @project
- 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')
......
- if ::EE::Gitlab::ExternalAuthorization.enabled?
- if ::Gitlab::ExternalAuthorization.enabled?
.form-group
= f.label :external_authorization_classification_label, class: 'label-bold' do
= s_('ExternalAuthorizationService|Classification Label')
......
......@@ -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
......
---
title: Move "Authorize project access with external service" to Core
merge_request: 26823
author:
type: changed
......@@ -51,10 +51,6 @@
}
}
.classification-label {
background-color: $red-500;
}
.load-wrapper {
position: absolute;
background: $black-transparent;
......
# frozen_string_literal: true
module EE
module ProjectUnauthorized
extend ::Gitlab::Utils::Override
override :project_unauthorized_proc
def project_unauthorized_proc
lambda do |project|
if project
label = project.external_authorization_classification_label
rejection_reason = nil
unless EE::Gitlab::ExternalAuthorization.access_allowed?(current_user, label)
rejection_reason = EE::Gitlab::ExternalAuthorization.rejection_reason(current_user, label)
rejection_reason ||= _('External authorization denied access to this project')
end
if rejection_reason
access_denied!(rejection_reason)
end
end
end
end
end
end
......@@ -10,8 +10,8 @@ module EE
attrs += EE::ApplicationSettingsHelper.repository_mirror_attributes
end
if License.feature_available?(:external_authorization_service)
attrs += EE::ApplicationSettingsHelper.external_authorization_service_attributes
if License.feature_available?(:project_creation_level)
attrs << :default_project_creation
end
if License.feature_available?(:custom_project_templates)
......
......@@ -37,7 +37,6 @@ module EE
repository_size_limit
reset_approvals_on_push
service_desk_enabled
external_authorization_classification_label
ci_cd_only
use_custom_template
packages_enabled
......
......@@ -4,39 +4,6 @@ module EE
module ApplicationSettingsHelper
extend ::Gitlab::Utils::Override
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 pseudonymizer_enabled_help_text
_("Enable Pseudonymizer data collection")
end
......@@ -102,20 +69,8 @@ module EE
]
end
def self.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 self.possible_licensed_attributes
repository_mirror_attributes + external_authorization_service_attributes + %i[
repository_mirror_attributes + %i[
email_additional_text
file_template_project_id
]
......
......@@ -111,16 +111,6 @@ module EE
can?(current_user, :"change_#{rule}", @project)
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
def ci_cd_projects_available?
::License.feature_available?(:ci_cd_projects) && import_sources_enabled?
end
......
......@@ -60,43 +60,9 @@ module EE
presence: true,
if: :snowplow_enabled
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 :geo_node_allowed_ips, length: { maximum: 255 }, presence: true
validate :check_geo_node_allowed_ips
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
end
class_methods do
......@@ -238,12 +204,6 @@ module EE
EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT
end
def external_authorization_service_enabled
License.feature_available?(:external_authorization_service) && super
end
alias_method :external_authorization_service_enabled?,
:external_authorization_service_enabled
def custom_project_templates_enabled?
License.feature_available?(:custom_project_templates)
end
......
......@@ -56,23 +56,6 @@ module EE
super if supports_weight?
end
# The functionality here is duplicated from the `IssuePolicy` and the
# `EE::IssuePolicy` for better performace
#
# Make sure to keep this in sync with the policies.
override :readable_by?
def readable_by?(user)
return super if user.full_private_access?
super && ::EE::Gitlab::ExternalAuthorization
.access_allowed?(user, project.external_authorization_classification_label)
end
override :publicly_visible?
def publicly_visible?
super && !::EE::Gitlab::ExternalAuthorization.enabled?
end
def supports_weight?
project&.feature_available?(:issue_weights)
end
......
......@@ -477,13 +477,6 @@ module EE
::Gitlab::CurrentSettings.mirror_available
end
def external_authorization_classification_label
return unless License.feature_available?(:external_authorization_service)
super || ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
end
override :licensed_features
def licensed_features
return super unless License.current
......
......@@ -66,7 +66,6 @@ class License < ApplicationRecord
variable_environment_scope
reject_unsigned_commits
commit_committer_check
external_authorization_service
ci_cd_projects
protected_environments
custom_project_templates
......@@ -179,7 +178,6 @@ class License < ApplicationRecord
multiple_ldap_servers
object_storage
repository_size_limit
external_authorization_service
custom_project_templates
].freeze
......
......@@ -5,14 +5,6 @@ module EE
extend ActiveSupport::Concern
prepended do
condition(:external_authorization_enabled, scope: :global, score: 0) do
::EE::Gitlab::ExternalAuthorization.perform_check?
end
rule { external_authorization_enabled & ~admin & ~auditor }.policy do
prevent :read_cross_project
end
with_scope :user
condition(:auditor, score: 0) { @user&.auditor? }
......
......@@ -45,6 +45,7 @@ module EE
prevent :create_epic
prevent :admin_epic
prevent :update_epic
prevent :destroy_epic
end
rule { auditor }.enable :read_group
......
......@@ -30,15 +30,6 @@ module EE
with_scope :subject
condition(:packages_disabled) { !@subject.packages_enabled }
with_scope :subject
condition(:classification_label_authorized, score: 32) do
EE::Gitlab::ExternalAuthorization.access_allowed?(
@user,
@subject.external_authorization_classification_label,
@subject.full_path
)
end
with_scope :global
condition(:is_development) { Rails.env.development? }
......@@ -191,25 +182,6 @@ module EE
rule { owner | reporter }.enable :build_read_project
rule { ~can?(:read_cross_project) & ~classification_label_authorized }.policy do
# Preventing access here still allows the projects to be listed. Listing
# projects doesn't check the `:read_project` ability. But instead counts
# on the `project_authorizations` table.
#
# All other actions should explicitly check read project, which would
# trigger the `classification_label_authorized` condition.
#
# `: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
rule { archived }.policy do
READONLY_FEATURES_WHEN_ARCHIVED.each do |feature|
prevent(*::ProjectPolicy.create_update_admin_destroy(feature))
......
......@@ -3,7 +3,6 @@
module EE
module ApplicationSettings
module UpdateService
include ValidatesClassificationLabel
extend ::Gitlab::Utils::Override
extend ActiveSupport::Concern
......@@ -13,12 +12,6 @@ module EE
limit = params.delete(:repository_size_limit)
application_setting.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit
validate_classification_label(application_setting, :external_authorization_service_default_label)
if application_setting.errors.any?
return false
end
super
end
end
......
......@@ -4,7 +4,6 @@ module EE
module Projects
module CreateService
extend ::Gitlab::Utils::Override
include ValidatesClassificationLabel
override :execute
def execute
......@@ -25,7 +24,6 @@ module EE
project.mirror_user_id = mirror_user_id
end
validate_classification_label(project, :external_authorization_classification_label)
validate_namespace_used_with_template(project, group_with_project_templates_id)
end
......
......@@ -4,7 +4,6 @@ module EE
module Projects
module UpdateService
extend ::Gitlab::Utils::Override
include ValidatesClassificationLabel
include CleanupApprovers
override :execute
......@@ -26,8 +25,6 @@ module EE
if changing_storage_size?
project.change_repository_storage(params.delete(:repository_storage))
end
validate_classification_label(project, :external_authorization_classification_label)
end
if result[:status] == :success
......
......@@ -41,8 +41,6 @@ module EE
expose :mirror_trigger_builds, if: ->(project, _) { project.mirror? }
expose :only_mirror_protected_branches, if: ->(project, _) { project.mirror? }
expose :mirror_overwrites_diverged_branches, if: ->(project, _) { project.mirror? }
expose :external_authorization_classification_label,
if: ->(_, _) { License.feature_available?(:external_authorization_service) }
expose :packages_enabled, if: ->(project, _) { project.feature_available?(:packages) }
end
end
......@@ -158,9 +156,6 @@ module EE
expose(*EE::ApplicationSettingsHelper.repository_mirror_attributes, if: ->(_instance, _options) do
::License.feature_available?(:repository_mirrors)
end)
expose(*EE::ApplicationSettingsHelper.external_authorization_service_attributes, if: ->(_instance, _options) do
::License.feature_available?(:external_authorization_service)
end)
expose :email_additional_text, if: ->(_instance, _opts) { ::License.feature_available?(:email_additional_text) }
expose :file_template_project_id, if: ->(_instance, _opts) { ::License.feature_available?(:custom_file_templates) }
end
......
......@@ -16,12 +16,6 @@ module EE
attrs = attrs.except(*::EE::ApplicationSettingsHelper.repository_mirror_attributes)
end
unless ::License.feature_available?(:external_authorization_service)
attrs = attrs.except(
*::EE::ApplicationSettingsHelper.external_authorization_service_attributes
)
end
unless ::License.feature_available?(:email_additional_text)
attrs = attrs.except(:email_additional_text)
end
......
# frozen_string_literal: true
module EE
module Gitlab
module ExternalAuthorization
extend 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 = EE::Gitlab::ExternalAuthorization::Access.new(user, label).load!
::EE::Gitlab::ExternalAuthorization::Logger.log_access(access, project_path)
access
end
end
end
end
# frozen_string_literal: true
module EE
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 > 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 EE::Gitlab::ExternalAuthorization::RequestFailed => e
@access = false
@reason = e.message
@loaded_at = Time.now
end
def cache
@cache ||= Cache.new(@user, @label)
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module ExternalAuthorization
class Cache
VALIDITY_TIME = 6.hours
def initialize(user, label)
@user, @label = user, label
end
def load
@access, @reason, @refreshed_at = ::Gitlab::Redis::Cache.with do |redis|
redis.hmget(cache_key, :access, :reason, :refreshed_at)
end
[access, reason, refreshed_at]
end
def store(new_access, new_reason, new_refreshed_at)
::Gitlab::Redis::Cache.with do |redis|
redis.pipelined do
redis.mapped_hmset(
cache_key,
{
access: new_access.to_s,
reason: new_reason.to_s,
refreshed_at: new_refreshed_at.to_s
}
)
redis.expire(cache_key, VALIDITY_TIME)
end
end
end
private
def access
::Gitlab::Utils.to_boolean(@access)
end
def reason
# `nil` if the cached value was an empty string
return 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
end
# frozen_string_literal: true
Excon.defaults[:ssl_verify_peer] = false
module EE
module Gitlab
module ExternalAuthorization
class Client
include 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
)
EE::Gitlab::ExternalAuthorization::Response.new(response)
rescue Excon::Error => e
raise EE::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
end
# frozen_string_literal: true
module EE
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
end
# frozen_string_literal: true
module EE
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
end
# frozen_string_literal: true
module EE
module Gitlab
module ExternalAuthorization
class Response
include ::Gitlab::Utils::StrongMemoize
def initialize(excon_response)
@excon_response = excon_response
end
def valid?
@excon_response && [200, 401, 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
end
......@@ -81,23 +81,6 @@ describe Admin::ApplicationSettingsController do
it_behaves_like 'settings for licensed features'
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('ee/spec/fixtures/passphrase_x509_certificate.crt'),
external_auth_client_key: File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key'),
external_auth_client_key_pass: "5iveL!fe"
}
end
let(:feature) { :external_authorization_service }
it_behaves_like 'settings for licensed features'
end
context 'additional email footer' do
let(:settings) { { email_additional_text: 'scary legal footer' } }
let(:feature) { :email_additional_text }
......
......@@ -89,26 +89,6 @@ 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
get :index, params: { board_id: board }
expect(response).to have_gitlab_http_status(403)
end
it 'is successful for project boards' do
project_board = create(:board, project: project_1)
list_issues(user: user, board: project_board)
expect(response).to have_gitlab_http_status(200)
end
end
def list_issues(user:, board:, list: nil)
sign_in(user)
......
require 'spec_helper'
describe Dashboard::GroupsController do
include ExternalAuthorizationServiceHelpers
before do
sign_in create(:user)
end
describe '#index' do
it 'works when the external authorization service is enabled' do
enable_external_authorization_service_check
get :index
expect(response).to have_gitlab_http_status(200)
end
end
end
require 'spec_helper'
describe Dashboard::LabelsController do
before do
sign_in create(:user)
end
describe '#index' do
subject { get :index, format: :json }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Dashboard::MilestonesController do
before do
sign_in create(:user)
end
describe '#index' do
subject { get :index }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Dashboard::ProjectsController do
include ExternalAuthorizationServiceHelpers
before do
sign_in create(:user)
end
describe '#index' do
subject { get :index }
it '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
require 'spec_helper'
describe Dashboard::TodosController do
before do
sign_in create(:user)
end
describe '#index' do
subject { get :index }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Groups::AvatarsController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
before do
group.add_owner(user)
sign_in(user)
end
it 'works when external authorization service is enabled' do
enable_external_authorization_service_check
delete :destroy, params: { group_id: group }
expect(response).to have_gitlab_http_status(302)
end
end
......@@ -43,10 +43,6 @@ describe Groups::BoardsController do
end
end
it_behaves_like 'disabled when using an external authorization service' do
subject { list_boards }
end
it_behaves_like 'redirects to last visited board' do
let(:parent) { group }
end
......
require 'spec_helper'
describe Groups::ChildrenController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
before do
group.add_owner(user)
sign_in(user)
end
it 'works when external authorization service is enabled' do
enable_external_authorization_service_check
get :index, params: { group_id: group }, format: :json
expect(response).to have_gitlab_http_status(200)
end
end
......@@ -12,118 +12,47 @@ describe Groups::GroupMembersController do
sign_in(user)
end
context 'with external authorization enabled' do
before do
enable_external_authorization_service_check
describe 'POST #create' do
it 'creates an audit event' do
expect do
post :create, params: { group_id: group,
user_ids: user.id,
access_level: Gitlab::Access::GUEST }
end.to change(AuditEvent, :count).by(1)
end
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
describe 'DELETE #leave' do
context 'when member is not an owner' do
it 'creates an audit event' do
expect do
post :create, params: { group_id: group,
user_ids: user.id,
access_level: Gitlab::Access::GUEST }
end.to change(AuditEvent, :count).by(1)
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))
developer = create(:user)
group.add_developer(developer)
sign_in(developer)
post :request_access, params: { group_id: group }
expect(response).to have_gitlab_http_status(302)
expect { delete :leave, params: { group_id: group } }.to change(AuditEvent, :count).by(1)
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)
context 'when member is an owner' do
it 'does not create an audit event' do
expect { delete :leave, params: { group_id: group } }.not_to change(AuditEvent, :count)
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
context 'when member is not an owner' do
it 'creates an audit event' do
developer = create(:user)
group.add_developer(developer)
sign_in(developer)
expect { delete :leave, params: { group_id: group } }.to change(AuditEvent, :count).by(1)
end
end
context 'when member is an owner' do
it 'does not create an audit event' do
expect { delete :leave, params: { group_id: group } }.not_to change(AuditEvent, :count)
end
end
context 'when member requested access' do
it 'creates an audit event' do
requester = create(:user)
group.request_access(requester)
sign_in(requester)
context 'when member requested access' do
it 'creates an audit event' do
requester = create(:user)
group.request_access(requester)
sign_in(requester)
expect { delete :leave, params: { group_id: group } }.to change(AuditEvent, :count).by(1)
end
expect { delete :leave, params: { group_id: group } }.to change(AuditEvent, :count).by(1)
end
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
context 'with external authorization enabled' do
before do
enable_external_authorization_service_check
end
describe 'POST #override' do
......
......@@ -13,6 +13,7 @@ describe GroupsController do
describe 'external authorization' do
before do
group.add_owner(user)
sign_in(user)
end
context 'with external authorization service enabled' do
......@@ -20,60 +21,7 @@ describe GroupsController 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
context 'no license' do
it 'does not update the file_template_project_id successfully' do
project = create(:project, group: group)
......@@ -108,32 +56,6 @@ describe GroupsController do
end
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
......
require 'spec_helper'
describe Groups::LabelsController do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_owner(user)
sign_in(user)
end
describe 'GET #index' do
subject { get :index, params: { group_id: group.to_param } }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Groups::MilestonesController do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_owner(user)
sign_in(user)
end
describe 'GET #index' do
subject { get :index, params: { group_id: group.to_param } }
it_behaves_like 'disabled when using an external authorization service'
end
end
require 'spec_helper'
describe Groups::Settings::CiCdController do
include ExternalAuthorizationServiceHelpers
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_owner(user)
sign_in(user)
end
context 'with external authorization enabled' do
before do
enable_external_authorization_service_check
end
describe 'GET #show' do
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
end
require 'spec_helper'
describe Groups::VariablesController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:variable) { create(:ci_group_variable, group: group) }
before do
group.add_owner(user)
sign_in(user)
end
context 'with external authorization enabled' do
before do
enable_external_authorization_service_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
require 'spec_helper'
describe Projects::BoardsController do
include Rails.application.routes.url_helpers
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.add_maintainer(user)
allow(Ability).to receive(:allowed?).and_call_original
sign_in(user)
end
......@@ -25,10 +22,6 @@ describe Projects::BoardsController do
expect(parsed_response.length).to eq 2
end
it_behaves_like 'unauthorized when external service denies access' do
subject { list_boards }
end
it_behaves_like 'redirects to last visited board' do
let(:parent) { project }
end
......
require 'spec_helper'
describe Projects::IssuesController do
include Rails.application.routes.url_helpers
let(:namespace) { create(:group, :public) }
let(:project) { create(:project_empty_repo, :public, namespace: namespace) }
let(:user) { create(:user) }
describe 'GET #index' do
before do
sign_in user
project.add_developer(user)
end
it_behaves_like 'unauthorized when external service denies access' do
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
end
end
describe 'POST export_csv' do
let(:viewer) { user }
let(:issue) { create(:issue, project: project) }
......
require 'spec_helper'
describe ProjectsController do
include ExternalAuthorizationServiceHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
......@@ -279,29 +277,5 @@ describe ProjectsController do
end
end
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
require 'spec_helper'
describe SearchController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:note) { create(:note_on_issue, project: project) }
before do
sign_in(user)
end
context 'with external authorization service enabled' do
before do
enable_external_authorization_service_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
require 'spec_helper'
describe UsersController do
let(:user) { create(:user) }
before do
sign_in(user)
end
describe 'GET #snippets' do
subject { get :snippets, params: { username: user.username } }
it_behaves_like 'disabled when using an external authorization service'
end
describe 'GET #calendar_activities' do
subject { get :calendar_activities, params: { username: user.username } }
it_behaves_like 'disabled when using an external authorization service'
end
end
......@@ -11,46 +11,12 @@ describe 'The group page' do
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('Contribution Analytics')
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_ee_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
it 'shows the link to contribution analytics' do
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')
expect(page).to have_link('Contribution Analytics')
end
end
......
require 'spec_helper'
describe 'User page' do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
it 'shows the most recent activity' do
visit(user_path(user))
expect(page).to have_content('Most Recent Activity')
end
describe 'when external authorization is enabled' do
before do
enable_external_authorization_service_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
require 'spec_helper'
describe IssuesFinder do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:issue, project: project) }
let(:project_params) { { project_id: project.id } }
end
describe '#execute' do
include_context 'IssuesFinder context'
include_context 'IssuesFinder#execute context'
......
require 'spec_helper'
describe LabelsFinder do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:label, project: project) }
let(:project_params) { { project_id: project.id } }
end
end
require 'spec_helper'
describe MergeRequestsFinder do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:merge_request, source_project: project) }
let(:project_params) { { project_id: project.id } }
end
describe '#execute' do
include_context 'MergeRequestsFinder multiple projects with merge requests context'
......
require 'spec_helper'
describe SnippetsFinder do
include ExternalAuthorizationServiceHelpers
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:project_snippet, project: project) }
let(:project_params) { { project: project } }
end
context 'external authorization service enabled' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:snippet) { create(:project_snippet, :public, project: project) }
before do
project.add_maintainer(user)
end
it 'includes the result if the external service allows access' do
external_service_allow_access(user, project)
results = described_class.new(user, project: project).execute
expect(results).to contain_exactly(snippet)
end
it 'does not include any results if the external service denies access' do
external_service_deny_access(user, project)
results = described_class.new(user, project: project).execute
expect(results).to be_empty
end
end
context 'filter by project' do
set(:user) { create(:user) }
set(:group) { create(:group, :public) }
......
require 'spec_helper'
describe TodosFinder do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:todo, project: project, user: user) }
let(:project_params) { { project_id: project.id } }
end
end
......@@ -36,53 +36,6 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value("a" * (subject.email_additional_text_character_limit + 1)).for(:email_additional_text) }
end
describe 'when external authorization service is enabled' do
before do
stub_licensed_features(external_authorization_service: true)
setting.external_authorization_service_enabled = true
end
it { is_expected.not_to allow_value('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('ee/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('ee/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('ee/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
context 'when validating allowed_ips' do
where(:allowed_ips, :is_valid) do
"192.1.1.1" | true
......
require 'spec_helper'
describe EE::ProtectedRefAccess do
include ExternalAuthorizationServiceHelpers
included_in_classes = [ProtectedBranch::MergeAccessLevel,
ProtectedBranch::PushAccessLevel,
ProtectedTag::CreateAccessLevel]
......@@ -109,20 +108,4 @@ describe EE::ProtectedRefAccess do
end
end
end
describe '#check_access' do
subject(:protected_ref_access) do
create(:protected_branch, :maintainers_can_push).push_access_levels.first
end
let(:project) { protected_ref_access.project }
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
require 'spec_helper'
describe Issue do
using RSpec::Parameterized::TableSyntax
include ExternalAuthorizationServiceHelpers
using RSpec::Parameterized::TableSyntax
describe 'validations' do
subject { build(:issue) }
......@@ -131,50 +132,6 @@ describe Issue do
end
end
context 'when an external authentication service' do
before do
enable_external_authorization_service_check
end
describe '#publicly_visible?' do
it 'is `false` when an external authorization service is enabled' do
issue = build(:issue, project: build(:project, :public))
expect(issue).not_to be_publicly_visible
end
end
describe '#readable_by?' do
it 'checks the external service to determine if an issue is readable by a user' do
project = build(:project, :public,
external_authorization_classification_label: 'a-label')
issue = build(:issue, project: project)
user = build(:user)
expect(EE::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
expect(issue.readable_by?(user)).to be_falsy
end
it 'does not check the external webservice for admins' do
issue = build(:issue)
user = build(:admin)
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
issue.readable_by?(user)
end
it 'does not check the external webservice for auditors' do
issue = build(:issue)
user = build(:auditor)
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
issue.readable_by?(user)
end
end
end
describe 'relative positioning with group boards' do
let(:group) { create(:group) }
let!(:board) { create(:board, group: group) }
......@@ -380,4 +337,21 @@ describe Issue do
end
end
end
context 'when an external authentication service' do
before do
enable_external_authorization_service_check
end
describe '#visible_to_user?' do
it 'does not check the external webservice for auditors' do
issue = build(:issue)
user = build(:auditor)
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
issue.visible_to_user?(user)
end
end
end
end
......@@ -4,7 +4,6 @@ require 'spec_helper'
describe Project do
include ProjectForksHelper
include ExternalAuthorizationServiceHelpers
include ::EE::GeoHelpers
using RSpec::Parameterized::TableSyntax
......@@ -1242,43 +1241,6 @@ 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 `nil` if the feature is disabled' do
stub_licensed_features(external_authorization_service: false)
project = build(:project,
external_authorization_classification_label: 'hello')
expect(project.external_authorization_classification_label)
.to eq(nil)
end
it 'returns the classification label if it was configured on the project' do
enable_external_authorization_service_check
project = build(:project,
external_authorization_classification_label: 'hello')
expect(project.external_authorization_classification_label)
.to eq('hello')
end
it 'does not break when not stubbing the license check' do
enable_external_authorization_service_check
enable_namespace_license_check!
project = build(:project)
expect { project.external_authorization_classification_label }.not_to raise_error
end
end
describe '#licensed_features' do
let(:plan_license) { :free }
let(:global_license) { create(:license) }
......
......@@ -3,25 +3,12 @@ require 'spec_helper'
describe BasePolicy do
include ExternalAuthorizationServiceHelpers
let(:current_user) { create(:user) }
let(:user) { create(:user) }
subject { described_class.new(current_user, [user]) }
describe 'read cross project' do
it { is_expected.to be_allowed(:read_cross_project) }
context 'when an external authorization service is enabled' do
before do
enable_external_authorization_service_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
it 'allows auditors' do
expect(described_class.new(build(:auditor), nil)).to be_allowed(:read_cross_project)
end
......
......@@ -143,7 +143,7 @@ describe EpicPolicy do
end
it 'does not call external authorization service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
subject
end
......
require 'spec_helper'
describe IssuePolicy do
include ExternalAuthorizationServiceHelpers
context 'with external authorization enabled' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let(:policies) { described_class.new(user, issue) }
before do
enable_external_authorization_service_check
end
it 'can read the issue iid without accessing the external service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(policies).to be_allowed(:read_issue_iid)
end
end
end
require 'spec_helper'
describe MergeRequestPolicy do
include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
let(:guest) { create(:user) }
......@@ -111,21 +110,4 @@ describe MergeRequestPolicy do
end
end
end
context 'with external authorization enabled' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:policies) { described_class.new(user, merge_request) }
before do
enable_external_authorization_service_check
end
it 'can read the issue iid without accessing the external service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(policies).to be_allowed(:read_merge_request_iid)
end
end
end
......@@ -190,61 +190,17 @@ describe ProjectPolicy do
end
context 'reading a project' do
it 'allows access when a user has read access to the repo' do
expect(described_class.new(owner, project)).to be_allowed(:read_project)
expect(described_class.new(developer, project)).to be_allowed(:read_project)
expect(described_class.new(admin, project)).to be_allowed(:read_project)
end
it 'never checks the external service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(described_class.new(owner, project)).to be_allowed(:read_project)
end
context 'with an external authorization service' do
before do
enable_external_authorization_service_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(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(described_class.new(admin, project)).to be_allowed(:read_project)
end
it 'allows auditors' do
stub_licensed_features(auditor_user: true)
auditor = create(:user, :auditor)
expect(described_class.new(auditor, project)).to be_allowed(:read_project)
end
it 'prevents all but seeing a public project in a list when access is denied' do
[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(EE::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
......
require 'spec_helper'
describe API::Projects do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
describe 'GET /projects' do
it 'does not break on license checks' do
stub_licensed_features(external_authorization_service: true)
enable_namespace_license_check!
create(:project, :private, namespace: user.namespace)
......@@ -91,20 +88,6 @@ describe API::Projects do
describe 'PUT /projects/:id' do
let(:project) { create(:project, namespace: user.namespace) }
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
context 'when updating repository storage' do
let(:unknown_storage) { 'new-storage' }
let(:new_project) { create(:project, :repository, namespace: user.namespace) }
......@@ -307,67 +290,21 @@ describe API::Projects do
end
describe 'GET /projects/:id' do
context 'with external authorization' do
let(:project) do
create(:project,
namespace: user.namespace,
external_authorization_classification_label: 'the-label')
end
describe 'packages_enabled attribute' do
it 'exposed when the feature is available' do
stub_licensed_features(packages: true)
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)
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
expect(json_response).to have_key 'packages_enabled'
end
context 'when the external service denies access' do
before do
external_service_deny_access(user, project)
end
it 'not exposed when the feature is available' do
stub_licensed_features(packages: false)
it 'returns a 404' do
get api("/projects/#{project.id}", user)
get api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(404)
end
end
context 'it does not return the label when the feature is not available' do
before do
stub_licensed_features(external_authorization_service: false)
end
it 'does not include 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 be_nil
end
end
describe 'packages_enabled attribute' do
it 'exposed when the feature is available' do
stub_licensed_features(packages: true)
get api("/projects/#{project.id}", user)
expect(json_response).to have_key 'packages_enabled'
end
it 'not exposed when the feature is available' do
stub_licensed_features(packages: false)
get api("/projects/#{project.id}", user)
expect(json_response).not_to have_key 'packages_enabled'
end
expect(json_response).not_to have_key 'packages_enabled'
end
end
end
......
......@@ -95,23 +95,6 @@ describe API::Settings, 'EE Settings' do
it_behaves_like 'settings for licensed features'
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('ee/spec/fixtures/passphrase_x509_certificate.crt'),
external_auth_client_key: File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key'),
external_auth_client_key_pass: "5iveL!fe"
}
end
let(:feature) { :external_authorization_service }
it_behaves_like 'settings for licensed features'
end
context 'custom email footer' do
let(:settings) { { email_additional_text: 'this is a scary legal footer' } }
let(:feature) { :email_additional_text }
......
require 'spec_helper'
describe GroupChildEntity do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:request) { double('request') }
let(:entity) { described_class.new(object, request: request) }
subject(:json) { entity.as_json }
before do
allow(request).to receive(:current_user).and_return(user)
end
describe 'for a project with external authorization enabled' do
let(:object) do
create(:project, :with_avatar,
description: 'Awesomeness')
end
before do
enable_external_authorization_service_check
object.add_maintainer(user)
end
it 'does not hit the external authorization service' do
expect(EE::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
expect(json[:can_edit]).to eq(false)
end
end
end
require 'spec_helper'
describe ApplicationSettings::UpdateService do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:setting) { ApplicationSetting.create_from_defaults }
let(:service) { described_class.new(setting, user, opts) }
......@@ -64,37 +62,4 @@ 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(EE::Gitlab::ExternalAuthorization)
.to receive(:access_allowed?).with(user, 'new-label') { false }
described_class.new(setting, user, { external_authorization_service_default_label: 'new-label' }).execute
expect(setting.errors[:external_authorization_service_default_label]).to be_present
end
it 'saves the setting when the user has access to the label' do
expect(EE::Gitlab::ExternalAuthorization)
.to receive(:access_allowed?).with(user, 'new-label') { true }
described_class.new(setting, user, { external_authorization_service_default_label: 'new-label' }).execute
# Read the attribute directly to avoid the stub from
# `enable_external_authorization_service_check`
expect(setting[:external_authorization_service_default_label]).to eq('new-label')
end
it 'does not validate the label if it was not passed' do
expect(EE::Gitlab::ExternalAuthorization)
.not_to receive(:access_allowed?)
described_class.new(setting, user, { home_page_url: 'http://foo.bar' }).execute
end
end
end
......@@ -2,46 +2,8 @@ require 'spec_helper'
describe EE::NotificationService, :mailer do
include NotificationHelpers
include ExternalAuthorizationServiceHelpers
let(:subject) { NotificationService.new }
context 'with external authentication service' do
let(:issue) { create(:issue) }
let(:project) { issue.project }
let(:note) { create(:note, noteable: issue, project: project) }
let(:member) { create(:user) }
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).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).with(member.id, issue.id, nil).and_call_original
subject.new_issue(issue, member)
end
end
end
let(:subject) { NotificationService.new }
context 'service desk issues' do
before do
......
require 'spec_helper'
describe Projects::CreateService, '#execute' do
include ExternalAuthorizationServiceHelpers
let(:user) { create :user }
let(:opts) do
{
......@@ -260,42 +258,6 @@ describe Projects::CreateService, '#execute' do
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(EE::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(EE::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(EE::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)
described_class.new(user, opts).execute
end
......
......@@ -2,7 +2,6 @@ require 'spec_helper'
describe Projects::UpdateService, '#execute' do
include EE::GeoHelpers
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
......@@ -178,46 +177,6 @@ describe Projects::UpdateService, '#execute' do
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(EE::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(EE::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(EE::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(EE::Gitlab::ExternalAuthorization)
.not_to receive(:access_allowed?)
update_project(project, user, { name: 'New name' })
end
end
context 'with approval_rules' do
context 'when approval_rules is disabled' do
it "updates approval_rules' approvals_required" do
......
......@@ -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
......
# 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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment