diff options
author | Imre Farkas <ifarkas@gitlab.com> | 2019-04-05 11:45:47 +0000 |
---|---|---|
committer | Andreas Brandl <abrandl@gitlab.com> | 2019-04-05 11:45:47 +0000 |
commit | d9d7237d2ebf101ca35ed8ba2740e7c7093437ea (patch) | |
tree | 419b0af4bc8de6de5888feec4f502bcc468df400 /lib | |
parent | 30fa3cbdb74df2dfeebb2929a10dd301a0dde55e (diff) | |
download | gitlab-ce-d9d7237d2ebf101ca35ed8ba2740e7c7093437ea.tar.gz |
Move Contribution Analytics related spec in spec/features/groups/group_page_with_external_authorization_service_spec to EE
Diffstat (limited to 'lib')
-rw-r--r-- | lib/api/entities.rb | 3 | ||||
-rw-r--r-- | lib/api/helpers/projects_helpers.rb | 5 | ||||
-rw-r--r-- | lib/api/settings.rb | 4 | ||||
-rw-r--r-- | lib/gitlab/external_authorization.rb | 40 | ||||
-rw-r--r-- | lib/gitlab/external_authorization/access.rb | 55 | ||||
-rw-r--r-- | lib/gitlab/external_authorization/cache.rb | 62 | ||||
-rw-r--r-- | lib/gitlab/external_authorization/client.rb | 63 | ||||
-rw-r--r-- | lib/gitlab/external_authorization/config.rb | 47 | ||||
-rw-r--r-- | lib/gitlab/external_authorization/logger.rb | 21 | ||||
-rw-r--r-- | lib/gitlab/external_authorization/response.rb | 38 |
10 files changed, 335 insertions, 3 deletions
diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 2dd3120d3fc..079ee7f5ccc 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -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 diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 7b858dc2e72..aaf32dafca4 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -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 diff --git a/lib/api/settings.rb b/lib/api/settings.rb index d742c6c97c1..120c5f4ccfc 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -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 diff --git a/lib/gitlab/external_authorization.rb b/lib/gitlab/external_authorization.rb new file mode 100644 index 00000000000..25f8b7b3628 --- /dev/null +++ b/lib/gitlab/external_authorization.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + extend ExternalAuthorization::Config + + RequestFailed = Class.new(StandardError) + + def self.access_allowed?(user, label, project_path = nil) + return true unless perform_check? + return false unless user + + access_for_user_to_label(user, label, project_path).has_access? + end + + def self.rejection_reason(user, label) + return unless enabled? + return unless user + + access_for_user_to_label(user, label, nil).reason + end + + def self.access_for_user_to_label(user, label, project_path) + if RequestStore.active? + RequestStore.fetch("external_authorisation:user-#{user.id}:label-#{label}") do + load_access(user, label, project_path) + end + else + load_access(user, label, project_path) + end + end + + def self.load_access(user, label, project_path) + access = ::Gitlab::ExternalAuthorization::Access.new(user, label).load! + ::Gitlab::ExternalAuthorization::Logger.log_access(access, project_path) + + access + end + end +end diff --git a/lib/gitlab/external_authorization/access.rb b/lib/gitlab/external_authorization/access.rb new file mode 100644 index 00000000000..e111c41fcc2 --- /dev/null +++ b/lib/gitlab/external_authorization/access.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + class Access + attr_reader :user, + :reason, + :loaded_at, + :label, + :load_type + + def initialize(user, label) + @user, @label = user, label + end + + def loaded? + loaded_at && (loaded_at > ExternalAuthorization::Cache::VALIDITY_TIME.ago) + end + + def has_access? + @access + end + + def load! + load_from_cache + load_from_service unless loaded? + self + end + + private + + def load_from_cache + @load_type = :cache + @access, @reason, @loaded_at = cache.load + end + + def load_from_service + @load_type = :request + response = Client.new(@user, @label).request_access + @access = response.successful? + @reason = response.reason + @loaded_at = Time.now + cache.store(@access, @reason, @loaded_at) if response.valid? + rescue ::Gitlab::ExternalAuthorization::RequestFailed => e + @access = false + @reason = e.message + @loaded_at = Time.now + end + + def cache + @cache ||= ExternalAuthorization::Cache.new(@user, @label) + end + end + end +end diff --git a/lib/gitlab/external_authorization/cache.rb b/lib/gitlab/external_authorization/cache.rb new file mode 100644 index 00000000000..acdc028b4dc --- /dev/null +++ b/lib/gitlab/external_authorization/cache.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + class Cache + VALIDITY_TIME = 6.hours + + def initialize(user, label) + @user, @label = user, label + end + + def load + @access, @reason, @refreshed_at = ::Gitlab::Redis::Cache.with do |redis| + redis.hmget(cache_key, :access, :reason, :refreshed_at) + end + + [access, reason, refreshed_at] + end + + def store(new_access, new_reason, new_refreshed_at) + ::Gitlab::Redis::Cache.with do |redis| + redis.pipelined do + redis.mapped_hmset( + cache_key, + { + access: new_access.to_s, + reason: new_reason.to_s, + refreshed_at: new_refreshed_at.to_s + } + ) + + redis.expire(cache_key, VALIDITY_TIME) + end + end + end + + private + + def access + ::Gitlab::Utils.to_boolean(@access) + end + + def reason + # `nil` if the cached value was an empty string + return unless @reason.present? + + @reason + end + + def refreshed_at + # Don't try to parse a time if there was no cache + return unless @refreshed_at.present? + + Time.parse(@refreshed_at) + end + + def cache_key + "external_authorization:user-#{@user.id}:label-#{@label}" + end + end + end +end diff --git a/lib/gitlab/external_authorization/client.rb b/lib/gitlab/external_authorization/client.rb new file mode 100644 index 00000000000..60aab2e7044 --- /dev/null +++ b/lib/gitlab/external_authorization/client.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +Excon.defaults[:ssl_verify_peer] = false + +module Gitlab + module ExternalAuthorization + class Client + include ExternalAuthorization::Config + + REQUEST_HEADERS = { + 'Content-Type' => 'application/json', + 'Accept' => 'application/json' + }.freeze + + def initialize(user, label) + @user, @label = user, label + end + + def request_access + response = Excon.post( + service_url, + post_params + ) + ::Gitlab::ExternalAuthorization::Response.new(response) + rescue Excon::Error => e + raise ::Gitlab::ExternalAuthorization::RequestFailed.new(e) + end + + private + + def post_params + params = { headers: REQUEST_HEADERS, + body: body.to_json, + connect_timeout: timeout, + read_timeout: timeout, + write_timeout: timeout } + + if has_tls? + params[:client_cert_data] = client_cert + params[:client_key_data] = client_key + params[:client_key_pass] = client_key_pass + end + + params + end + + def body + @body ||= begin + body = { + user_identifier: @user.email, + project_classification_label: @label + } + + if @user.ldap_identity + body[:user_ldap_dn] = @user.ldap_identity.extern_uid + end + + body + end + end + end + end +end diff --git a/lib/gitlab/external_authorization/config.rb b/lib/gitlab/external_authorization/config.rb new file mode 100644 index 00000000000..8654a8c1e2e --- /dev/null +++ b/lib/gitlab/external_authorization/config.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + module Config + extend self + + def timeout + application_settings.external_authorization_service_timeout + end + + def service_url + application_settings.external_authorization_service_url + end + + def enabled? + application_settings.external_authorization_service_enabled + end + + def perform_check? + enabled? && service_url.present? + end + + def client_cert + application_settings.external_auth_client_cert + end + + def client_key + application_settings.external_auth_client_key + end + + def client_key_pass + application_settings.external_auth_client_key_pass + end + + def has_tls? + client_cert.present? && client_key.present? + end + + private + + def application_settings + ::Gitlab::CurrentSettings.current_application_settings + end + end + end +end diff --git a/lib/gitlab/external_authorization/logger.rb b/lib/gitlab/external_authorization/logger.rb new file mode 100644 index 00000000000..61246cd870e --- /dev/null +++ b/lib/gitlab/external_authorization/logger.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + class Logger < ::Gitlab::Logger + def self.log_access(access, project_path) + status = access.has_access? ? "GRANTED" : "DENIED" + message = ["#{status} #{access.user.email} access to '#{access.label}'"] + + message << "(#{project_path})" if project_path.present? + message << "- #{access.load_type} #{access.loaded_at}" if access.load_type == :cache + + info(message.join(' ')) + end + + def self.file_name_noext + 'external-policy-access-control' + end + end + end +end diff --git a/lib/gitlab/external_authorization/response.rb b/lib/gitlab/external_authorization/response.rb new file mode 100644 index 00000000000..4f3fe5882db --- /dev/null +++ b/lib/gitlab/external_authorization/response.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + class Response + include ::Gitlab::Utils::StrongMemoize + + def initialize(excon_response) + @excon_response = excon_response + end + + def valid? + @excon_response && [200, 401, 403].include?(@excon_response.status) + end + + def successful? + valid? && @excon_response.status == 200 + end + + def reason + parsed_response['reason'] if parsed_response + end + + private + + def parsed_response + strong_memoize(:parsed_response) { parse_response! } + end + + def parse_response! + JSON.parse(@excon_response.body) + rescue JSON::JSONError + # The JSON response is optional, so don't fail when it's missing + nil + end + end + end +end |