Commit 3962b00b authored by Andreas Brandl's avatar Andreas Brandl

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!26823
parents 30fa3cbd d9d7237d
......@@ -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
......@@ -343,6 +343,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,
......@@ -237,6 +270,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
......
......@@ -230,7 +230,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?
......@@ -298,7 +304,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
......
......@@ -2062,6 +2062,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)
......
# 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?
......
# 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
%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
......
- 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
- 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
......
---
title: Move "Authorize project access with external service" to Core
merge_request: 26823
author:
type: changed
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
class AddExternalAuthorizationServiceClassificationLabelToProjects < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :projects,
:external_authorization_classification_label,
:string
end
end
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
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
......@@ -177,6 +177,15 @@ ActiveRecord::Schema.define(version: 20190325165127) do
t.string "runners_registration_token_encrypted"
t.integer "local_markdown_version", default: 0, null: false
t.integer "first_day_of_week", default: 0, 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
......@@ -1754,6 +1763,7 @@ ActiveRecord::Schema.define(version: 20190325165127) 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 = {})
......@@ -1116,6 +1117,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
......
......@@ -167,7 +167,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
# 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
# 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
# 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
# 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
# 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
# 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
......@@ -393,6 +393,9 @@ msgstr ""
msgid "Access forbidden. Check your access level."
msgstr ""
msgid "Access to '%{classification_label}' not allowed"
msgstr ""
msgid "Account"
msgstr ""
......@@ -1656,6 +1659,9 @@ msgstr ""
msgid "CiVariable|Validation failed"
msgstr ""
msgid "ClassificationLabelUnavailable|is unavailable: %{reason}"
msgstr ""
msgid "Clear"
msgstr ""
......@@ -1686,6 +1692,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 ""
......@@ -2723,6 +2738,9 @@ msgstr ""
msgid "Default artifacts expiration"
msgstr ""
msgid "Default classification label"
msgstr ""
msgid "Default first day of the week"
msgstr ""
......@@ -3133,6 +3151,9 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
msgid "Enable classification control using an external service"
msgstr ""
msgid "Enable error tracking"
msgstr ""
......@@ -3559,12 +3580,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 ""
......@@ -4271,6 +4313,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 ""
......@@ -7241,6 +7286,9 @@ msgstr ""
msgid "Service Templates"
msgstr ""
msgid "Service URL"
msgstr ""
msgid "Session duration (minutes)"
msgstr ""
......@@ -7956,6 +8004,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 ""
......@@ -8031,6 +8082,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 ""
......@@ -8043,6 +8097,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 ""
......@@ -8409,6 +8466,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 ""
......@@ -9181,6 +9241,9 @@ msgstr ""
msgid "When enabled, users cannot use GitLab until the terms have been accepted."
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:"
msgstr ""
......@@ -9685,6 +9748,9 @@ msgstr ""
msgid "connecting"
msgstr ""
msgid "could not read private key, is the passphrase correct?"
msgstr ""
msgid "customize"
msgstr ""
......@@ -9773,6 +9839,9 @@ msgstr ""
msgid "index"
msgstr ""
msgid "is not a valid X509 certificate."
msgstr ""
msgid "issue boards"
msgstr ""
......@@ -10055,6 +10124,9 @@ msgstr ""
msgid "private"
msgstr ""
msgid "private key does not match certificate."
msgstr ""
msgid "processing"
msgstr ""
......
......@@ -85,6 +85,28 @@ describe Admin::ApplicationSettingsController do
expect(response).to redirect_to(admin_application_settings_path)
expect(ApplicationSetting.current.receive_max_input_size).to eq(1024)
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
......
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)
......
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
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
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') }
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)
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 'renders only groups the user is a member of when searching hierarchy correctly' do
get :index, params: { filter: 'chef' }, format: :json
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
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
......
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,
......
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
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
......
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
......
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
require 'spec_helper'
describe GroupsController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:group) { create(:group, :public) }
......@@ -658,4 +660,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
......
require('spec_helper')
describe ProjectsController do
include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
let(:project) { create(:project) }
......@@ -407,6 +408,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
......
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
......
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
# 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
# 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
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
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
# 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
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
......@@ -559,6 +559,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
......@@ -253,6 +253,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
......
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
......
-----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-----
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,79CCB506B0FD42A6F1BAE6D72E1CB20C
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
-----END RSA PRIVATE KEY-----
-----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-----
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
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
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
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
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
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
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) }
......
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
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
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
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
......
......@@ -114,6 +114,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 }
......
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
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 }
......@@ -2217,6 +2218,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)
......
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
......
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
......
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
require 'spec_helper'
shared_examples 'disabled when using an external authorization service' do
include ExternalAuthorizationServiceHelpers
it 'works when the feature is not enabled' do
subject
expect(response).to be_success
end
it 'renders a 404 with a message when the feature is enabled' do
enable_external_authorization_service_check
subject
expect(response).to have_gitlab_http_status(403)
end
end
shared_examples 'unauthorized when external service denies access' do
include ExternalAuthorizationServiceHelpers
it 'allows access when the authorization service allows it' do
external_service_allow_access(user, project)
subject
# Account for redirects after updates
expect(response.status).to be_between(200, 302)
end
it 'allows access when the authorization service denies it' do
external_service_deny_access(user, project)
subject
expect(response).to have_gitlab_http_status(403)
end
end
require 'spec_helper'
shared_examples 'a finder with external authorization service' do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
let(:project) { create(:project) }
before do
project.add_maintainer(user)
end
it 'finds the subject' do
expect(described_class.new(user).execute).to include(subject)
end
context 'with an external authorization service' do
before do
enable_external_authorization_service_check
end
it 'does not include the subject when no project was given' do
expect(described_class.new(user).execute).not_to include(subject)
end
it 'includes the subject when a project id was given' do
expect(described_class.new(user, project_params).execute).to include(subject)
end
end
end
require 'spec_helper'
describe X509CertificateCredentialsValidator do
let(:certificate_data) { File.read('spec/fixtures/x509_certificate.crt') }
let(:pkey_data) { File.read('spec/fixtures/x509_certificate_pk.key') }
let(:validatable) do
Class.new do
include ActiveModel::Validations
attr_accessor :certificate, :private_key, :passphrase
def initialize(certificate, private_key, passphrase = nil)
@certificate, @private_key, @passphrase = certificate, private_key, passphrase
end
end
end
subject(:validator) do
described_class.new(certificate: :certificate, pkey: :private_key)
end
it 'is not valid when the certificate is not valid' do
record = validatable.new('not a certificate', nil)
validator.validate(record)
expect(record.errors[:certificate]).to include('is not a valid X509 certificate.')
end
it 'is not valid without a certificate' do
record = validatable.new(nil, nil)
validator.validate(record)
expect(record.errors[:certificate]).not_to be_empty
end
context 'when a valid certificate is passed' do
let(:record) { validatable.new(certificate_data, nil) }
it 'does not track an error for the certificate' do
validator.validate(record)
expect(record.errors[:certificate]).to be_empty
end
it 'adds an error when not passing a correct private key' do
validator.validate(record)
expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?')
end
it 'has no error when the private key is correct' do
record.private_key = pkey_data
validator.validate(record)
expect(record.errors).to be_empty
end
end
context 'when using a passphrase' do
let(:passphrase_certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') }
let(:passphrase_pkey_data) { File.read('spec/fixtures/passphrase_x509_certificate_pk.key') }
let(:record) { validatable.new(passphrase_certificate_data, passphrase_pkey_data, '5iveL!fe') }
subject(:validator) do
described_class.new(certificate: :certificate, pkey: :private_key, pass: :passphrase)
end
it 'is valid with the correct data' do
validator.validate(record)
expect(record.errors).to be_empty
end
it 'adds an error when the passphrase is wrong' do
record.passphrase = 'wrong'
validator.validate(record)
expect(record.errors[:private_key]).not_to be_empty
end
end
end
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