Commit b721780b authored by Douwe Maan's avatar Douwe Maan

Merge branch 'hvlad/gitlab-ce-feature/move_oauth_modules_to_auth_dir_structure-ee' into 'master'

[EE] Moved o_auth/saml/ldap modules under gitlab/auth

See merge request gitlab-org/gitlab-ee!4759
parents b05fb24b 426ca794
...@@ -124,8 +124,8 @@ Lint/DuplicateMethods: ...@@ -124,8 +124,8 @@ Lint/DuplicateMethods:
- 'lib/gitlab/git/repository.rb' - 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/tree.rb' - 'lib/gitlab/git/tree.rb'
- 'lib/gitlab/git/wiki_page.rb' - 'lib/gitlab/git/wiki_page.rb'
- 'lib/gitlab/ldap/person.rb' - 'lib/gitlab/auth/ldap/person.rb'
- 'lib/gitlab/o_auth/user.rb' - 'lib/gitlab/auth/o_auth/user.rb'
# Offense count: 4 # Offense count: 4
Lint/InterpolationCheck: Lint/InterpolationCheck:
...@@ -812,7 +812,7 @@ Style/TrivialAccessors: ...@@ -812,7 +812,7 @@ Style/TrivialAccessors:
Exclude: Exclude:
- 'app/models/external_issue.rb' - 'app/models/external_issue.rb'
- 'app/serializers/base_serializer.rb' - 'app/serializers/base_serializer.rb'
- 'lib/gitlab/ldap/person.rb' - 'lib/gitlab/auth/ldap/person.rb'
- 'lib/system_check/base_check.rb' - 'lib/system_check/base_check.rb'
# Offense count: 4 # Offense count: 4
......
...@@ -1064,7 +1064,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -1064,7 +1064,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Adds abitlity to render deploy boards in the frontend side. !1233 - Adds abitlity to render deploy boards in the frontend side. !1233
- Add filtered search to MR page. !1243 - Add filtered search to MR page. !1243
- Update project list API returns with approvals_before_merge attribute. !1245 (Geoff Webster) - Update project list API returns with approvals_before_merge attribute. !1245 (Geoff Webster)
- Catch Net::LDAP::DN exceptions in EE::Gitlab::LDAP::Group. !1260 - Catch Net::LDAP::DN exceptions in EE::Gitlab::Auth::LDAP::Group. !1260
- API: Use `post ":id/#{type}/:subscribable_id/subscribe"` to subscribe and `post ":id/#{type}/:subscribable_id/unsubscribe"` to unsubscribe from a resource. !1274 (Robert Schilling) - API: Use `post ":id/#{type}/:subscribable_id/subscribe"` to subscribe and `post ":id/#{type}/:subscribable_id/unsubscribe"` to unsubscribe from a resource. !1274 (Robert Schilling)
- API: Remove deprecated fields Notes#upvotes and Notes#downvotes. !1275 (Robert Schilling) - API: Remove deprecated fields Notes#upvotes and Notes#downvotes. !1275 (Robert Schilling)
- Deploy board backend. !1278 - Deploy board backend. !1278
......
...@@ -199,7 +199,7 @@ class ApplicationController < ActionController::Base ...@@ -199,7 +199,7 @@ class ApplicationController < ActionController::Base
return unless signed_in? && session[:service_tickets] return unless signed_in? && session[:service_tickets]
valid = session[:service_tickets].all? do |provider, ticket| valid = session[:service_tickets].all? do |provider, ticket|
Gitlab::OAuth::Session.valid?(provider, ticket) Gitlab::Auth::OAuth::Session.valid?(provider, ticket)
end end
unless valid unless valid
...@@ -223,7 +223,7 @@ class ApplicationController < ActionController::Base ...@@ -223,7 +223,7 @@ class ApplicationController < ActionController::Base
if current_user && current_user.requires_ldap_check? if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease return unless current_user.try_obtain_ldap_lease
unless Gitlab::LDAP::Access.allowed?(current_user) unless Gitlab::Auth::LDAP::Access.allowed?(current_user)
sign_out current_user sign_out current_user
flash[:alert] = "Access denied for your LDAP account." flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path redirect_to new_user_session_path
...@@ -238,7 +238,7 @@ class ApplicationController < ActionController::Base ...@@ -238,7 +238,7 @@ class ApplicationController < ActionController::Base
end end
def gitlab_ldap_access(&block) def gitlab_ldap_access(&block)
Gitlab::LDAP::Access.open { |access| yield(access) } Gitlab::Auth::LDAP::Access.open { |access| yield(access) }
end end
# JSON for infinite scroll via Pager object # JSON for infinite scroll via Pager object
...@@ -292,7 +292,7 @@ class ApplicationController < ActionController::Base ...@@ -292,7 +292,7 @@ class ApplicationController < ActionController::Base
end end
def github_import_configured? def github_import_configured?
Gitlab::OAuth::Provider.enabled?(:github) Gitlab::Auth::OAuth::Provider.enabled?(:github)
end end
def gitlab_import_enabled? def gitlab_import_enabled?
...@@ -300,7 +300,7 @@ class ApplicationController < ActionController::Base ...@@ -300,7 +300,7 @@ class ApplicationController < ActionController::Base
end end
def gitlab_import_configured? def gitlab_import_configured?
Gitlab::OAuth::Provider.enabled?(:gitlab) Gitlab::Auth::OAuth::Provider.enabled?(:gitlab)
end end
def bitbucket_import_enabled? def bitbucket_import_enabled?
...@@ -308,7 +308,7 @@ class ApplicationController < ActionController::Base ...@@ -308,7 +308,7 @@ class ApplicationController < ActionController::Base
end end
def bitbucket_import_configured? def bitbucket_import_configured?
Gitlab::OAuth::Provider.enabled?(:bitbucket) Gitlab::Auth::OAuth::Provider.enabled?(:bitbucket)
end end
def google_code_import_enabled? def google_code_import_enabled?
......
...@@ -71,7 +71,7 @@ class Import::BitbucketController < Import::BaseController ...@@ -71,7 +71,7 @@ class Import::BitbucketController < Import::BaseController
end end
def provider def provider
Gitlab::OAuth::Provider.config_for('bitbucket') Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end end
def options def options
......
...@@ -11,8 +11,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -11,8 +11,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end end
end end
if Gitlab::LDAP::Config.enabled? if Gitlab::Auth::LDAP::Config.enabled?
Gitlab::LDAP::Config.available_servers.each do |server| Gitlab::Auth::LDAP::Config.available_servers.each do |server|
define_method server['provider_name'] do define_method server['provider_name'] do
ldap ldap
end end
...@@ -32,7 +32,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -32,7 +32,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# We only find ourselves here # We only find ourselves here
# if the authentication to LDAP was successful. # if the authentication to LDAP was successful.
def ldap def ldap
ldap_user = Gitlab::LDAP::User.new(oauth) ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
ldap_user.save if ldap_user.changed? # will also save new users ldap_user.save if ldap_user.changed? # will also save new users
@user = ldap_user.gl_user @user = ldap_user.gl_user
...@@ -65,13 +65,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -65,13 +65,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to after_sign_in_path_for(current_user) redirect_to after_sign_in_path_for(current_user)
end end
else else
saml_user = Gitlab::Saml::User.new(oauth) saml_user = Gitlab::Auth::Saml::User.new(oauth)
saml_user.save if saml_user.changed? saml_user.save if saml_user.changed?
@user = saml_user.gl_user @user = saml_user.gl_user
continue_login_process continue_login_process
end end
rescue Gitlab::OAuth::SignupDisabledError rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error handle_signup_error
end end
...@@ -119,20 +119,20 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -119,20 +119,20 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
log_audit_event(current_user, with: oauth['provider']) log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated' redirect_to profile_account_path, notice: 'Authentication method updated'
else else
oauth_user = Gitlab::OAuth::User.new(oauth) oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
oauth_user.save oauth_user.save
@user = oauth_user.gl_user @user = oauth_user.gl_user
continue_login_process continue_login_process
end end
rescue Gitlab::OAuth::SigninDisabledForProviderError rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
handle_disabled_provider handle_disabled_provider
rescue Gitlab::OAuth::SignupDisabledError rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error handle_signup_error
end end
def handle_service_ticket(provider, ticket) def handle_service_ticket(provider, ticket)
Gitlab::OAuth::Session.create provider, ticket Gitlab::Auth::OAuth::Session.create provider, ticket
session[:service_tickets] ||= {} session[:service_tickets] ||= {}
session[:service_tickets][provider] = ticket session[:service_tickets][provider] = ticket
end end
...@@ -155,7 +155,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -155,7 +155,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end end
def handle_signup_error def handle_signup_error
label = Gitlab::OAuth::Provider.label_for(oauth['provider']) label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if Gitlab::CurrentSettings.allow_signup? if Gitlab::CurrentSettings.allow_signup?
...@@ -184,7 +184,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -184,7 +184,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end end
def handle_disabled_provider def handle_disabled_provider
label = Gitlab::OAuth::Provider.label_for(oauth['provider']) label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
flash[:alert] = "Signing in using #{label} has been disabled" flash[:alert] = "Signing in using #{label} has been disabled"
redirect_to new_user_session_path redirect_to new_user_session_path
......
...@@ -17,7 +17,7 @@ class SessionsController < Devise::SessionsController ...@@ -17,7 +17,7 @@ class SessionsController < Devise::SessionsController
def new def new
set_minimum_password_length set_minimum_password_length
@ldap_servers = Gitlab::LDAP::Config.available_servers @ldap_servers = Gitlab::Auth::LDAP::Config.available_servers
super super
end end
......
...@@ -78,7 +78,7 @@ module ApplicationSettingsHelper ...@@ -78,7 +78,7 @@ module ApplicationSettingsHelper
label_tag(checkbox_name, class: css_class) do label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, source, !disabled, check_box_tag(checkbox_name, source, !disabled,
autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source) autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source)
end end
end end
end end
......
...@@ -5,7 +5,7 @@ module AuthHelper ...@@ -5,7 +5,7 @@ module AuthHelper
delegate :slack_app_id, to: :'Gitlab::CurrentSettings.current_application_settings' delegate :slack_app_id, to: :'Gitlab::CurrentSettings.current_application_settings'
def ldap_enabled? def ldap_enabled?
Gitlab::LDAP::Config.enabled? Gitlab::Auth::LDAP::Config.enabled?
end end
def kerberos_enabled? def kerberos_enabled?
...@@ -21,11 +21,11 @@ module AuthHelper ...@@ -21,11 +21,11 @@ module AuthHelper
end end
def auth_providers def auth_providers
Gitlab::OAuth::Provider.providers Gitlab::Auth::OAuth::Provider.providers
end end
def label_for_provider(name) def label_for_provider(name)
Gitlab::OAuth::Provider.label_for(name) Gitlab::Auth::OAuth::Provider.label_for(name)
end end
def form_based_provider?(name) def form_based_provider?(name)
......
...@@ -3,7 +3,7 @@ module ProfilesHelper ...@@ -3,7 +3,7 @@ module ProfilesHelper
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
if user_synced_attributes_metadata&.synced?(attribute) if user_synced_attributes_metadata&.synced?(attribute)
if user_synced_attributes_metadata.provider if user_synced_attributes_metadata.provider
Gitlab::OAuth::Provider.label_for(user_synced_attributes_metadata.provider) Gitlab::Auth::OAuth::Provider.label_for(user_synced_attributes_metadata.provider)
else else
'LDAP' 'LDAP'
end end
......
...@@ -25,7 +25,7 @@ module SelectsHelper ...@@ -25,7 +25,7 @@ module SelectsHelper
def ldap_server_select_options def ldap_server_select_options
options_from_collection_for_select( options_from_collection_for_select(
Gitlab::LDAP::Config.available_servers, Gitlab::Auth::LDAP::Config.available_servers,
'provider_name', 'provider_name',
'label' 'label'
) )
......
...@@ -19,12 +19,12 @@ class Identity < ActiveRecord::Base ...@@ -19,12 +19,12 @@ class Identity < ActiveRecord::Base
end end
def ldap? def ldap?
Gitlab::OAuth::Provider.ldap_provider?(provider) Gitlab::Auth::OAuth::Provider.ldap_provider?(provider)
end end
def self.normalize_uid(provider, uid) def self.normalize_uid(provider, uid)
if Gitlab::OAuth::Provider.ldap_provider?(provider) if Gitlab::Auth::OAuth::Provider.ldap_provider?(provider)
Gitlab::LDAP::Person.normalize_dn(uid) Gitlab::Auth::LDAP::Person.normalize_dn(uid)
else else
uid.to_s uid.to_s
end end
......
...@@ -746,7 +746,7 @@ class User < ActiveRecord::Base ...@@ -746,7 +746,7 @@ class User < ActiveRecord::Base
def ldap_user? def ldap_user?
if identities.loaded? if identities.loaded?
identities.find { |identity| Gitlab::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? } identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
else else
identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"]) identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
end end
......
...@@ -26,6 +26,6 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base ...@@ -26,6 +26,6 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base
private private
def sync_profile_from_provider? def sync_profile_from_provider?
Gitlab::OAuth::Provider.sync_profile_from_provider?(provider) Gitlab::Auth::OAuth::Provider.sync_profile_from_provider?(provider)
end end
end end
...@@ -203,7 +203,7 @@ ...@@ -203,7 +203,7 @@
Password authentication enabled for Git over HTTP(S) Password authentication enabled for Git over HTTP(S)
.help-block .help-block
When disabled, a Personal Access Token When disabled, a Personal Access Token
- if Gitlab::LDAP::Config.enabled? - if Gitlab::Auth::LDAP::Config.enabled?
or LDAP password or LDAP password
must be used to authenticate. must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any? - if omniauth_enabled? && button_based_providers.any?
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel" = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
- if @group.persisted? && Gitlab::LDAP::Config.group_sync_enabled? - if @group.persisted? && Gitlab::Auth::LDAP::Config.group_sync_enabled?
%h3.page-title LDAP synchronizations %h3.page-title LDAP synchronizations
= render 'ldap_group_links/form', group: @group = render 'ldap_group_links/form', group: @group
= render 'ldap_group_links/ldap_group_links', group: @group = render 'ldap_group_links/ldap_group_links', group: @group
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
= render partial: "namespaces/shared_runner_status", locals: { namespace: @group } = render partial: "namespaces/shared_runner_status", locals: { namespace: @group }
- if Gitlab::LDAP::Config.group_sync_enabled? && @group.ldap_synced? - if Gitlab::Auth::LDAP::Config.group_sync_enabled? && @group.ldap_synced?
.panel.panel-default .panel.panel-default
.panel-heading Active synchronizations .panel-heading Active synchronizations
%ul.well-list %ul.well-list
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.form-group .form-group
= f.label :provider, class: 'control-label' = f.label :provider, class: 'control-label'
.col-sm-10 .col-sm-10
- values = Gitlab::OAuth::Provider.providers.map { |name| ["#{Gitlab::OAuth::Provider.label_for(name)} (#{name})", name] } - values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] }
= f.select :provider, values, { allow_blank: false }, class: 'form-control' = f.select :provider, values, { allow_blank: false }, class: 'form-control'
.form-group .form-group
= f.label :extern_uid, "Identifier", class: 'control-label' = f.label :extern_uid, "Identifier", class: 'control-label'
......
%tr %tr
%td %td
#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider}) #{Gitlab::Auth::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
%td %td
= identity.extern_uid = identity.extern_uid
%td %td
......
---
title: Moved o_auth/saml/ldap modules under gitlab/auth
merge_request: 17359
author: Horatiu Eugen Vlad
...@@ -212,9 +212,9 @@ Devise.setup do |config| ...@@ -212,9 +212,9 @@ Devise.setup do |config|
# manager.default_strategies(scope: :user).unshift :some_external_strategy # manager.default_strategies(scope: :user).unshift :some_external_strategy
# end # end
if Gitlab::LDAP::Config.enabled? if Gitlab::Auth::LDAP::Config.enabled?
Gitlab::LDAP::Config.providers.each do |provider| Gitlab::Auth::LDAP::Config.providers.each do |provider|
ldap_config = Gitlab::LDAP::Config.new(provider) ldap_config = Gitlab::Auth::LDAP::Config.new(provider)
config.omniauth(provider, ldap_config.omniauth_options) config.omniauth(provider, ldap_config.omniauth_options)
end end
end end
...@@ -235,9 +235,9 @@ Devise.setup do |config| ...@@ -235,9 +235,9 @@ Devise.setup do |config|
if provider['name'] == 'cas3' if provider['name'] == 'cas3'
provider['args'][:on_single_sign_out] = lambda do |request| provider['args'][:on_single_sign_out] = lambda do |request|
ticket = request.params[:session_index] ticket = request.params[:session_index]
raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) raise "Service Ticket not found." unless Gitlab::Auth::OAuth::Session.valid?(:cas3, ticket)
Gitlab::OAuth::Session.destroy(:cas3, ticket) Gitlab::Auth::OAuth::Session.destroy(:cas3, ticket)
true true
end end
end end
...@@ -245,8 +245,8 @@ Devise.setup do |config| ...@@ -245,8 +245,8 @@ Devise.setup do |config|
if provider['name'] == 'authentiq' if provider['name'] == 'authentiq'
provider['args'][:remote_sign_out_handler] = lambda do |request| provider['args'][:remote_sign_out_handler] = lambda do |request|
authentiq_session = request.params['sid'] authentiq_session = request.params['sid']
if Gitlab::OAuth::Session.valid?(:authentiq, authentiq_session) if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session)
Gitlab::OAuth::Session.destroy(:authentiq, authentiq_session) Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session)
true true
else else
false false
......
if Gitlab::LDAP::Config.enabled? if Gitlab::Auth::LDAP::Config.enabled?
module OmniAuth::Strategies module OmniAuth::Strategies
Gitlab::LDAP::Config.available_servers.each do |server| Gitlab::Auth::LDAP::Config.available_servers.each do |server|
# do not redeclare LDAP # do not redeclare LDAP
next if server['provider_name'] == 'ldap' next if server['provider_name'] == 'ldap'
......
...@@ -327,7 +327,7 @@ step of the sync. ...@@ -327,7 +327,7 @@ step of the sync.
1. Run a group sync for this particular group. 1. Run a group sync for this particular group.
```ruby ```ruby
EE::Gitlab::LDAP::Sync::Group.execute_all_providers(group) EE::Gitlab::Auth::LDAP::Sync::Group.execute_all_providers(group)
``` ```
1. Look through the output of the sync. See [example log output](#example-log-output) 1. Look through the output of the sync. See [example log output](#example-log-output)
below for more information about the output. below for more information about the output.
...@@ -336,11 +336,11 @@ step of the sync. ...@@ -336,11 +336,11 @@ step of the sync.
run the following query: run the following query:
```ruby ```ruby
adapter = Gitlab::LDAP::Adapter.new('ldapmain') # If `main` is the LDAP provider adapter = Gitlab::Auth::LDAP::Adapter.new('ldapmain') # If `main` is the LDAP provider
ldap_group = EE::Gitlab::LDAP::Group.find_by_cn('group_cn_here', adapter) ldap_group = EE::Gitlab::Auth::LDAP::Group.find_by_cn('group_cn_here', adapter)
# Output # Output
=> #<EE::Gitlab::LDAP::Group:0x007fcbdd0bb6d8 => #<EE::Gitlab::Auth::LDAP::Group:0x007fcbdd0bb6d8
``` ```
1. Query the LDAP group's member DNs and see if the user's DN is in the list. 1. Query the LDAP group's member DNs and see if the user's DN is in the list.
One of the DNs here should match the 'Identifier' from the LDAP identity One of the DNs here should match the 'Identifier' from the LDAP identity
......
...@@ -17,6 +17,6 @@ class Groups::LdapsController < Groups::ApplicationController ...@@ -17,6 +17,6 @@ class Groups::LdapsController < Groups::ApplicationController
private private
def check_enabled_extras! def check_enabled_extras!
render_404 unless Gitlab::LDAP::Config.group_sync_enabled? render_404 unless Gitlab::Auth::LDAP::Config.group_sync_enabled?
end end
end end
...@@ -29,14 +29,14 @@ class LdapGroupLink < ActiveRecord::Base ...@@ -29,14 +29,14 @@ class LdapGroupLink < ActiveRecord::Base
end end
def config def config
Gitlab::LDAP::Config.new(provider) Gitlab::Auth::LDAP::Config.new(provider)
rescue Gitlab::LDAP::Config::InvalidProvider rescue Gitlab::Auth::LDAP::Config::InvalidProvider
nil nil
end end
# default to the first LDAP server # default to the first LDAP server
def provider def provider
read_attribute(:provider) || Gitlab::LDAP::Config.providers.first read_attribute(:provider) || Gitlab::Auth::LDAP::Config.providers.first
end end
def provider_label def provider_label
......
- if Gitlab::LDAP::Config.group_sync_enabled? && can?(current_user, :admin_ldap_group_links, @group) - if Gitlab::Auth::LDAP::Config.group_sync_enabled? && can?(current_user, :admin_ldap_group_links, @group)
= nav_link(path: 'ldap_group_links#index') do = nav_link(path: 'ldap_group_links#index') do
= link_to group_ldap_group_links_path(@group), title: 'LDAP Group' do = link_to group_ldap_group_links_path(@group), title: 'LDAP Group' do
%span %span
......
...@@ -3,10 +3,10 @@ class LdapAllGroupsSyncWorker ...@@ -3,10 +3,10 @@ class LdapAllGroupsSyncWorker
include CronjobQueue include CronjobQueue
def perform def perform
return unless Gitlab::LDAP::Config.group_sync_enabled? return unless Gitlab::Auth::LDAP::Config.group_sync_enabled?
logger.info 'Started LDAP group sync' logger.info 'Started LDAP group sync'
EE::Gitlab::LDAP::Sync::Groups.execute EE::Gitlab::Auth::LDAP::Sync::Groups.execute
logger.info 'Finished LDAP group sync' logger.info 'Finished LDAP group sync'
end end
end end
...@@ -2,12 +2,12 @@ class LdapGroupSyncWorker ...@@ -2,12 +2,12 @@ class LdapGroupSyncWorker
include ApplicationWorker include ApplicationWorker
def perform(group_ids, provider = nil) def perform(group_ids, provider = nil)
return unless Gitlab::LDAP::Config.group_sync_enabled? return unless Gitlab::Auth::LDAP::Config.group_sync_enabled?
groups = Group.where(id: Array(group_ids)) groups = Group.where(id: Array(group_ids))
if provider if provider
EE::Gitlab::LDAP::Sync::Proxy.open(provider) do |proxy| EE::Gitlab::Auth::LDAP::Sync::Proxy.open(provider) do |proxy|
sync_groups(groups, proxy: proxy) sync_groups(groups, proxy: proxy)
end end
else else
...@@ -23,9 +23,9 @@ class LdapGroupSyncWorker ...@@ -23,9 +23,9 @@ class LdapGroupSyncWorker
logger.info "Started LDAP group sync for group #{group.name} (#{group.id})" logger.info "Started LDAP group sync for group #{group.name} (#{group.id})"
if proxy if proxy
EE::Gitlab::LDAP::Sync::Group.execute(group, proxy) EE::Gitlab::Auth::LDAP::Sync::Group.execute(group, proxy)
else else
EE::Gitlab::LDAP::Sync::Group.execute_all_providers(group) EE::Gitlab::Auth::LDAP::Sync::Group.execute_all_providers(group)
end end
logger.info "Finished LDAP group sync for group #{group.name} (#{group.id})" logger.info "Finished LDAP group sync for group #{group.name} (#{group.id})"
......
...@@ -3,14 +3,14 @@ class LdapSyncWorker ...@@ -3,14 +3,14 @@ class LdapSyncWorker
include CronjobQueue include CronjobQueue
def perform def perform
return unless Gitlab::LDAP::Config.group_sync_enabled? return unless Gitlab::Auth::LDAP::Config.group_sync_enabled?
Rails.logger.info "Performing daily LDAP sync task." Rails.logger.info "Performing daily LDAP sync task."
User.ldap.find_each(batch_size: 100).each do |ldap_user| User.ldap.find_each(batch_size: 100).each do |ldap_user|
Rails.logger.debug "Syncing user #{ldap_user.username}, #{ldap_user.email}" Rails.logger.debug "Syncing user #{ldap_user.username}, #{ldap_user.email}"
# Use the 'update_ldap_group_links_synchronously' option to avoid creating a ton # Use the 'update_ldap_group_links_synchronously' option to avoid creating a ton
# of new Sidekiq jobs all at once. # of new Sidekiq jobs all at once.
Gitlab::LDAP::Access.allowed?(ldap_user, update_ldap_group_links_synchronously: true) Gitlab::Auth::LDAP::Access.allowed?(ldap_user, update_ldap_group_links_synchronously: true)
end end
end end
end end
# rubocop:disable Rails/ReversibleMigration # rubocop:disable Rails/ReversibleMigration
class UpdateGroupLinks < ActiveRecord::Migration class UpdateGroupLinks < ActiveRecord::Migration
def change def change
provider = quote_string(Gitlab::LDAP::Config.providers.first) provider = quote_string(Gitlab::Auth::LDAP::Config.providers.first)
execute("UPDATE ldap_group_links SET provider = '#{provider}' WHERE provider IS NULL") execute("UPDATE ldap_group_links SET provider = '#{provider}' WHERE provider IS NULL")
end end
end end
...@@ -6,7 +6,7 @@ module API ...@@ -6,7 +6,7 @@ module API
helpers do helpers do
def get_group_list(provider, search) def get_group_list(provider, search)
search = Net::LDAP::Filter.escape(search) search = Net::LDAP::Filter.escape(search)
Gitlab::LDAP::Adapter.new(provider).groups("#{search}*", 20) Gitlab::Auth::LDAP::Adapter.new(provider).groups("#{search}*", 20)
end end
params :search_params do params :search_params do
...@@ -21,7 +21,7 @@ module API ...@@ -21,7 +21,7 @@ module API
use :search_params use :search_params
end end
get 'groups' do get 'groups' do
provider = Gitlab::LDAP::Config.available_servers.first['provider_name'] provider = Gitlab::Auth::LDAP::Config.available_servers.first['provider_name']
groups = get_group_list(provider, params[:search]) groups = get_group_list(provider, params[:search])
present groups, with: Entities::LdapGroup present groups, with: Entities::LdapGroup
end end
......
...@@ -30,7 +30,7 @@ module Audit ...@@ -30,7 +30,7 @@ module Audit
when :remove when :remove
"Removed #{value}" "Removed #{value}"
when :failed_login when :failed_login
"Failed to login with #{Gitlab::OAuth::Provider.label_for(value).upcase} authentication" "Failed to login with #{Gitlab::Auth::OAuth::Provider.label_for(value).upcase} authentication"
when :custom_message when :custom_message
value value
else else
......
module EE
module Gitlab
module Auth
module LDAP
# Create a hash map of member DNs to access levels. The highest
# access level is retained in cases where `set` is called multiple times
# for the same DN.
class AccessLevels < Hash
def set(dns, to:)
dns.each do |dn|
current = self[dn]
# Keep the higher of the access values.
self[dn] = to if current.nil? || to > current
end
end
end
end
end
end
end
# LDAP connection adapter EE mixin
#
# This module is intended to encapsulate EE-specific adapter methods
# and be **prepended** in the `Gitlab::Auth::LDAP::Adapter` class.
module EE
module Gitlab
module Auth
module LDAP
module Adapter
# Get LDAP groups from ou=Groups
#
# cn - filter groups by name
#
# Ex.
# groups("dev*") # return all groups start with 'dev'
#
def groups(cn = "*", size = nil)
options = {
base: config.group_base,
filter: Net::LDAP::Filter.eq("cn", cn),
attributes: %w(dn cn memberuid member submember uniquemember memberof)
}
options[:size] = size if size
ldap_search(options).map do |entry|
LDAP::Group.new(entry, self)
end
end
def group(*args)
groups(*args).first
end
def group_members_in_range(dn, range_start)
ldap_search(
base: dn,
scope: Net::LDAP::SearchScope_BaseObject,
attributes: ["member;range=#{range_start}-*"]
).first
end
def nested_groups(parent_dn)
options = {
base: config.group_base,
filter: Net::LDAP::Filter.join(
Net::LDAP::Filter.eq('objectClass', 'group'),
Net::LDAP::Filter.eq('memberOf', parent_dn)
)
}
ldap_search(options).map do |entry|
LDAP::Group.new(entry, self)
end
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module LDAP
module Config
extend ActiveSupport::Concern
class_methods do
def group_sync_enabled?
enabled? && ::License.feature_available?(:ldap_group_sync)
end
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module LDAP
class Group
attr_accessor :adapter
attr_reader :entry
def self.find_by_cn(cn, adapter)
cn = Net::LDAP::Filter.escape(cn)
adapter.group(cn)
end
def initialize(entry, adapter = nil)
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@adapter = adapter
end
def active_directory?
adapter.config.active_directory
end
def cn
entry.cn.first
end
def name
cn
end
def path
name.parameterize
end
def memberuid?
entry.respond_to? :memberuid
end
def member_uids
@member_uids ||= entry.memberuid.map do |uid|
::Gitlab::Auth::LDAP::Person.normalize_uid(uid)
end
end
delegate :dn, to: :entry
def member_dns(nested_groups_to_skip = [])
dns = []
if active_directory? && adapter
dns.concat(active_directory_members(entry, nested_groups_to_skip))
end
dns.concat(entry_member_dns(entry))
dns.uniq
end
private
# Active Directory range member methods
def has_member_range?(entry)
member_range_attribute(entry).present?
end
def member_range_attribute(entry)
entry.attribute_names.find { |a| a.to_s.start_with?("member;range=")}.to_s
end
def active_directory_members(entry, nested_groups_to_skip)
require 'net/ldap/dn'
members = []
# Retrieve all member pages/ranges
members.concat(ranged_members(entry)) if has_member_range?(entry)
# Process nested group members
members.concat(nested_members(nested_groups_to_skip))
# Clean dns of groups and users outside the base
members.reject! { |dn| nested_groups_to_skip.include?(dn) }
return [] if members.empty?
# Only return members within our given base
members_within_base(members)
end
# AD requires use of range retrieval for groups with more than 1500 members
# cf. https://msdn.microsoft.com/en-us/library/aa367017(v=vs.85).aspx
def ranged_members(entry)
members = []
# Concatenate the members in the current range
dns = entry[member_range_attribute(entry)]
dns = normalize_dns(dns)
members.concat(dns)
# Recursively concatenate members until end of ranges
if has_more_member_ranges?(entry)
next_entry = adapter.group_members_in_range(dn, next_member_range_start(entry))
members.concat(ranged_members(next_entry))
end
members
end
# Process any AD nested groups. Use a manual process because
# the AD recursive member of filter is too slow and uses too
# much CPU on the AD server.
def nested_members(nested_groups_to_skip)
# Ignore this group if we see it again in a nested group.
# Prevents infinite loops.
nested_groups_to_skip << dn
members = []
nested_groups = adapter.nested_groups(dn)
nested_groups.each do |nested_group|
next if nested_groups_to_skip.include?(nested_group.dn)
members.concat(nested_group.member_dns(nested_groups_to_skip))
end
members
end
def has_more_member_ranges?(entry)
next_member_range_start(entry).present?
end
def next_member_range_start(entry)
match = member_range_attribute(entry).match /^member;range=\d+-(\d+|\*)$/
match[1].to_i + 1 if match.present? && match[1] != '*'
end
# The old AD recursive member filter would exclude any members that
# were outside the given search base. To maintain that behavior,
# we need to do the same.
#
# Split the base and each member DN into pairs. Compare the last
# base N pairs of the member DN. If they match, the user is within
# the base DN.
#
# Ex.
# - Member DN: 'uid=user,ou=users,dc=example,dc=com'
# - Base DN: 'dc=example,dc=com'
#
# Base has 2 pairs ([dc,example], [dc,com]). If the last 2 pairs of
# the user DN match, profit!
def members_within_base(members)
begin
base = ::Gitlab::Auth::LDAP::DN.new(adapter.config.base).to_a
rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
Rails.logger.error "Configured LDAP `base` is invalid: '#{adapter.config.base}'. Error: \"#{e.message}\""
return []
end
members.select do |dn|
begin
::Gitlab::Auth::LDAP::DN.new(dn).to_a.last(base.length) == base
rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
Rails.logger.warn "Received invalid member DN from LDAP group '#{cn}': '#{dn}'. Error: \"#{e.message}\". Skipping"
end
end
end
def normalize_dns(dns)
dns.map do |dn|
::Gitlab::Auth::LDAP::Person.normalize_dn(dn)
end
end
def entry_member_dns(entry)
dns = entry.try(:member) || entry.try(:uniquemember) || entry.try(:memberof)
dns&.concat(entry.try(:submember) || [])
if dns
normalize_dns(dns)
else
Rails.logger.warn("Could not find member DNs for LDAP group #{entry.inspect}")
[]
end
end
end
end
end
end
end
require 'net/ldap/dn'
module EE
module Gitlab
module Auth
module LDAP
module Person
extend ActiveSupport::Concern
class_methods do
def find_by_email(email, adapter)
email_attributes = Array(adapter.config.attributes['email'])
email_attributes.each do |possible_attribute|
found_user = adapter.user(possible_attribute, email)
return found_user if found_user
end
nil
end
def find_by_kerberos_principal(principal, adapter)
uid, domain = principal.split('@', 2)
return nil unless uid && domain
# In multi-forest setups, there may be several users with matching
# uids but differing DNs, so skip adapters configured to connect to
# non-matching domains
return unless domain.casecmp(domain_from_dn(adapter.config.base)) == 0
find_by_uid(uid, adapter)
end
# Extracts the rightmost unbroken set of domain components from an
# LDAP DN and constructs a domain name from them
def domain_from_dn(dn)
dn_components = []
::Gitlab::Auth::LDAP::DN.new(dn).each_pair { |name, value| dn_components << { name: name, value: value } }
dn_components
.reverse
.take_while { |rdn| rdn[:name].casecmp('DC').zero? } # Domain Component
.map { |rdn| rdn[:value] }
.reverse
.join('.')
end
def ldap_attributes(config)
attributes = super + [
'memberof',
(config.sync_ssh_keys if config.sync_ssh_keys.is_a?(String))
]
attributes.compact.uniq
end
end
def ssh_keys
if config.sync_ssh_keys? && entry.respond_to?(config.sync_ssh_keys)
entry[config.sync_ssh_keys.to_sym]
.map { |key| key[/(ssh|ecdsa)-[^ ]+ [^\s]+/] }
.compact
else
[]
end
end
# We assume that the Kerberos username matches the configured uid
# attribute in LDAP. For Active Directory, this is `sAMAccountName`
def kerberos_principal
return nil unless uid
uid + '@' + self.class.domain_from_dn(dn).upcase
end
def memberof
return [] unless entry.attribute_names.include?(:memberof)
entry.memberof
end
def group_cns
memberof.map { |memberof_value| cn_from_memberof(memberof_value) }
end
def cn_from_memberof(memberof)
# Only get the first CN value of the string, that's the one that contains
# the group name
memberof.match(/(?:cn=([\w\s]+))/i)&.captures&.first
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module LDAP
module Sync
class AdminUsers < Sync::Users
private
def attribute
:admin
end
def member_dns
return [] if admin_group.empty?
proxy.dns_for_group_cn(admin_group)
end
def admin_group
proxy.adapter.config.admin_group
end
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module LDAP
module Sync
class ExternalUsers < Sync::Users
private
def attribute
:external
end
def member_dns
external_groups.flat_map do |group|
proxy.dns_for_group_cn(group)
end.uniq
end
def external_groups
proxy.adapter.config.external_groups
end
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module LDAP
module Sync
class Group
attr_reader :provider, :group, :proxy
class << self
# Sync members across all providers for the given group.
def execute_all_providers(group)
return unless ldap_sync_ready?(group)
group.start_ldap_sync
Rails.logger.debug { "Started syncing all providers for '#{group.name}' group" }
# Shuffle providers to prevent a scenario where sync fails after a time
# and only the first provider or two get synced. This shuffles the order
# so subsequent syncs should eventually get to all providers. Obviously
# we should avoid failure, but this is an additional safeguard.
::Gitlab::Auth::LDAP::Config.providers.shuffle.each do |provider|
Sync::Proxy.open(provider) do |proxy|
new(group, proxy).update_permissions
end
end
group.finish_ldap_sync
Rails.logger.debug { "Finished syncing all providers for '#{group.name}' group" }
end
# Sync members across a single provider for the given group.
def execute(group, proxy)
return unless ldap_sync_ready?(group)
group.start_ldap_sync
Rails.logger.debug { "Started syncing '#{proxy.provider}' provider for '#{group.name}' group" }
sync_group = new(group, proxy)
sync_group.update_permissions
group.finish_ldap_sync
Rails.logger.debug { "Finished syncing '#{proxy.provider}' provider for '#{group.name}' group" }
end
def ldap_sync_ready?(group)
fail_stuck_group(group)
return true unless group.ldap_sync_started?
Rails.logger.warn "Group '#{group.name}' is not ready for LDAP sync. Skipping"
false
end
def fail_stuck_group(group)
return unless group.ldap_sync_started?
if group.ldap_sync_last_sync_at < 1.hour.ago
group.mark_ldap_sync_as_failed('The sync took too long to complete.')
end
end
end
def initialize(group, proxy)
@provider = proxy.provider
@group = group
@proxy = proxy
end
def update_permissions
unless group.ldap_sync_started?
logger.warn "Group '#{group.name}' LDAP sync status must be 'started' before updating permissions"
return
end
access_levels = AccessLevels.new
# Only iterate over group links for the current provider
group.ldap_group_links.with_provider(provider).each do |group_link|
next unless group_link.active?
update_access_levels(access_levels, group_link)
end
update_existing_group_membership(group, access_levels)
add_new_members(group, access_levels)
end
private
def update_access_levels(access_levels, group_link)
if member_dns = get_member_dns(group_link)
access_levels.set(member_dns, to: group_link.group_access)
logger.debug do
"Resolved '#{group.name}' group member access: #{access_levels.to_hash}"
end
end
end
def get_member_dns(group_link)
group_link.cn ? dns_for_group_cn(group_link.cn) : UserFilter.filter(@proxy, group_link.filter)
end
def dns_for_group_cn(group_cn)
if config.group_base.blank?
logger.debug { "No `group_base` configured for '#{provider}' provider and group link CN #{group_cn}. Skipping" }
return nil
end
proxy.dns_for_group_cn(group_cn)
end
def dn_for_uid(uid)
proxy.dn_for_uid(uid)
end
def update_existing_group_membership(group, access_levels)
logger.debug { "Updating existing membership for '#{group.name}' group" }
select_and_preload_group_members(group).each do |member|
user = member.user
identity = user.identities.select(:id, :extern_uid)
.with_provider(provider).first
member_dn = identity.extern_uid.downcase
# Skip if this is not an LDAP user with a valid `extern_uid`.
next unless member_dn.present?
# Prevent shifting group membership, in case where user is a member
# of two LDAP groups from different providers linked to the same
# GitLab group. This is not ideal, but preserves existing behavior.
if user.ldap_identity.id != identity.id
access_levels.delete(member_dn)
next
end
desired_access = access_levels[member_dn]
# Skip validations and callbacks. We have a limited set of attrs
# due to the `select` lookup, and we need to be efficient.
# Low risk, because the member should already be valid.
member.update_column(:ldap, true) unless member.ldap?
# Don't do anything if the user already has the desired access level
if member.access_level == desired_access
access_levels.delete(member_dn)
next
end
# Check and update the access level. If `desired_access` is `nil`
# we need to delete the user from the group.
if desired_access.present?
# Delete this entry from the hash now that we're acting on it
access_levels.delete(member_dn)
next if member.ldap? && member.override?
add_or_update_user_membership(
user,
group,
desired_access
)
elsif group.last_owner?(user)
warn_cannot_remove_last_owner(user, group)
else
group.users.destroy(user)
end
end
end
def add_new_members(group, access_levels)
logger.debug { "Adding new members to '#{group.name}' group" }
access_levels.each do |member_dn, access_level|
user = ::Gitlab::Auth::LDAP::User.find_by_uid_and_provider(member_dn, provider)
if user.present?
add_or_update_user_membership(
user,
group,
access_level
)
else
logger.debug do
<<-MSG.strip_heredoc.tr("\n", ' ')
#{self.class.name}: User with DN `#{member_dn}` should have access
to '#{group.name}' group but there is no user in GitLab with that
identity. Membership will be updated once the user signs in for
the first time.
MSG
end
end
end
end
def add_or_update_user_membership(user, group, access, current_user: nil)
# Prevent the last owner of a group from being demoted
if access < ::Gitlab::Access::OWNER && group.last_owner?(user)
warn_cannot_remove_last_owner(user, group)
else
# If you pass the user object, instead of just user ID,
# it saves an extra user database query.
group.add_user(
user,
access,
current_user: current_user,
ldap: true
)
end
end
def warn_cannot_remove_last_owner(user, group)
logger.warn do
<<-MSG.strip_heredoc.tr("\n", ' ')
#{self.class.name}: LDAP group sync cannot remove #{user.name}
(#{user.id}) from group #{group.name} (#{group.id}) as this is
the group's last owner
MSG
end
end
def select_and_preload_group_members(group)
group.members.select(:id, :access_level, :user_id, :ldap, :override)
.with_identity_provider(provider).preload(:user)
end
def logger
Rails.logger
end
def config
@proxy.adapter.config
end
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module LDAP
module Sync
class Groups
attr_reader :provider, :proxy
def self.execute
# Shuffle providers to prevent a scenario where sync fails after a time
# and only the first provider or two get synced. This shuffles the order
# so subsequent syncs should eventually get to all providers. Obviously
# we should avoid failure, but this is an additional safeguard.
::Gitlab::Auth::LDAP::Config.providers.shuffle.each do |provider|
Sync::Proxy.open(provider) do |proxy|
group_sync = self.new(proxy)
group_sync.update_permissions
end
end
true
end
def initialize(proxy)
@provider = proxy.provider
@proxy = proxy
end
def update_permissions
logger.debug { "Performing LDAP group sync for '#{provider}' provider" }
sync_groups
logger.debug { "Finished LDAP group sync for '#{provider}' provider" }
if config.admin_group.present?
logger.debug { "Syncing admin users for '#{provider}' provider" }
sync_admin_users
logger.debug { "Finished syncing admin users for '#{provider}' provider" }
else
logger.debug { "No `admin_group` configured for '#{provider}' provider. Skipping" }
end
if config.external_groups.empty?
logger.debug { "No `external_groups` configured for '#{provider}' provider. Skipping" }
else
logger.debug { "Syncing external users for '#{provider}' provider" }
sync_external_users
logger.debug { "Finished syncing external users for '#{provider}' provider" }
end
nil
end
private
def sync_groups
groups_where_group_links_with_provider_ordered.each do |group|
Sync::Group.execute(group, proxy)
end
end
def sync_admin_users
Sync::AdminUsers.execute(proxy)
end
def sync_external_users
Sync::ExternalUsers.execute(proxy)
end
def groups_where_group_links_with_provider_ordered
::Group.where_group_links_with_provider(provider)
.preload(:ldap_group_links)
.reorder('ldap_sync_last_successful_update_at ASC, namespaces.id ASC')
.distinct
end
def config
proxy.adapter.config
end
def logger
Rails.logger
end
end
end
end
end
end
end
require 'net/ldap/dn'
module EE
module Gitlab
module Auth
module LDAP
module Sync
class Proxy
attr_reader :provider, :adapter
# Open a connection and run all queries through it.
# It's more efficient than the default of opening/closing per LDAP query.
def self.open(provider, &block)
::Gitlab::Auth::LDAP::Adapter.open(provider) do |adapter|
block.call(self.new(provider, adapter))
end
end
def initialize(provider, adapter)
@adapter = adapter
@provider = provider
end
# Cache LDAP group member DNs so we don't query LDAP groups more than once.
def dns_for_group_cn(group_cn)
@dns_for_group_cn ||= Hash.new { |h, k| h[k] = ldap_group_member_dns(k) }
@dns_for_group_cn[group_cn]
end
# Cache user DN so we don't generate excess queries to map UID to DN
def dn_for_uid(uid)
@dn_for_uid ||= Hash.new { |h, k| h[k] = member_uid_to_dn(k) }
@dn_for_uid[uid]
end
private
def ldap_group_member_dns(ldap_group_cn)
ldap_group = LDAP::Group.find_by_cn(ldap_group_cn, adapter)
unless ldap_group.present?
logger.warn { "Cannot find LDAP group with CN '#{ldap_group_cn}'. Skipping" }
return []
end
member_dns = ldap_group.member_dns
if member_dns.empty?
# Group must be empty
return [] unless ldap_group.memberuid?
members = ldap_group.member_uids
member_dns = members.map { |uid| dn_for_uid(uid) }
end
# Various lookups in this method could return `nil` values.
# Compact the array to remove those entries
member_dns.compact!
ensure_full_dns!(member_dns)
logger.debug { "Members in '#{ldap_group.name}' LDAP group: #{member_dns}" }
# Various lookups in this method could return `nil` values.
# Compact the array to remove those entries
member_dns
end
# At least one customer reported that their LDAP `member` values contain
# only `uid=username` and not the full DN. This method allows us to
# account for that. See gitlab-ee#442
def ensure_full_dns!(dns)
dns.map! do |dn|
begin
parsed_dn = ::Gitlab::Auth::LDAP::DN.new(dn).to_a
rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
logger.error { "Found malformed DN: '#{dn}'. Skipping. Error: \"#{e.message}\"" }
next
end
final_dn =
# If there is more than one key/value set we must have a full DN,
# or at least the probability is higher.
if parsed_dn.count > 2
dn
elsif parsed_dn.count == 0
logger.warn { "Found null DN. Skipping." }
nil
elsif parsed_dn[0] == 'uid'
dn_for_uid(parsed_dn[1])
else
logger.warn { "Found potentially malformed/incomplete DN: '#{dn}'" }
dn
end
clean_encoding(final_dn)
end
# Remove `nil` values generated by the rescue above.
dns.compact!
end
# net-ldap only returns ASCII-8BIT and does not support UTF-8 out-of-the-box:
# https://github.com/ruby-ldap/ruby-net-ldap/issues/4
def clean_encoding(dn)
return dn unless dn.present?
dn.force_encoding('UTF-8')
rescue
dn
end
def member_uid_to_dn(uid)
identity = ::Identity.with_secondary_extern_uid(provider, uid).take
if identity.present?
# Use the DN on record in GitLab when it's available
identity.extern_uid
else
ldap_user = ::Gitlab::Auth::LDAP::Person.find_by_uid(uid, adapter)
# Can't find a matching user
return nil unless ldap_user.present?
# Update user identity so we don't have to go through this again
update_identity(ldap_user.dn, uid)
ldap_user.dn
end
end
def update_identity(dn, uid)
identity = ::Identity.with_extern_uid(provider, dn).take
# User may not exist in GitLab yet. Skip.
return unless identity.present?
identity.secondary_extern_uid = uid
identity.save
end
def logger
Rails.logger
end
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module LDAP
module Sync
class Users
attr_reader :provider, :proxy
def self.execute(proxy)
self.new(proxy).update_permissions
end
def initialize(proxy)
@provider = proxy.provider
@proxy = proxy
end
def update_permissions
dns = member_dns
return if dns.empty?
current_users_with_attribute = ::User.with_provider(provider).where(attribute => true)
verified_users_with_attribute = []
# Verify existing users and add new ones.
dns.each do |member_dn|
user = update_user_by_dn(member_dn)
verified_users_with_attribute << user if user
end
# Revoke the unverified users.
(current_users_with_attribute - verified_users_with_attribute).each do |user|
user[attribute] = false
user.save
end
end
private
def attribute
raise NotImplementedError
end
def member_dns
raise NotImplementedError
end
def update_user_by_dn(member_dn)
user = ::Gitlab::Auth::LDAP::User.find_by_uid_and_provider(member_dn, provider)
if user.present?
user[attribute] = true
user.save
user
else
Rails.logger.debug do
<<-MSG.strip_heredoc.tr("\n", ' ')
#{self.class.name}: User with DN `#{member_dn}` should be marked as
#{attribute} but there is no user in GitLab with that identity.
Membership will be updated once the user signs in for the first time.
MSG
end
nil
end
end
end
end
end
end
end
end
# LDAP User EE mixin
#
# This module is intended to encapsulate EE-specific User methods
# and be **prepended** in the `Gitlab::Auth::LDAP::User` class.
module EE
module Gitlab
module Auth
module LDAP
module User
def initialize(auth_hash)
super
set_external_with_external_groups
end
private
# Intended to be called during #initialize, and #save should be called
# after initialize.
def set_external_with_external_groups
return if ldap_config.external_groups.empty?
gl_user.external = in_any_external_group?
end
# Returns true if the User is found in an external group listed in the
# config.
def in_any_external_group?
with_proxy do |proxy|
external_groups = proxy.adapter.config.external_groups
external_groups.any? do |group_cn|
in_group?(group_cn, proxy)
end
end
end
# Returns true if the User is a member of the group.
def in_group?(group_cn, proxy)
member_dns = proxy.dns_for_group_cn(group_cn)
member_dns.include?(auth_hash.uid)
end
def with_proxy(&block)
::EE::Gitlab::Auth::LDAP::Sync::Proxy.open(auth_hash.provider, &block)
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module LDAP
class UserFilter
def self.filter(*args)
new(*args).filter
end
def initialize(proxy, filter)
@proxy = proxy
@filter = filter
end
def filter
logger.debug "Running filter #{@filter} against #{@proxy.provider}"
@proxy.adapter.ldap_search(options).map(&:dn).tap do |dns|
logger.debug "Found #{dns.count} mathing users for filter #{@filter}"
end
end
private
def options
{ base: config.base, filter: construct_filter }
end
def construct_filter
Net::LDAP::Filter.construct(@filter)
end
def config
@proxy.adapter.config
end
def logger
Rails.logger
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module OAuth
module AuthHash
include ::Gitlab::Utils::StrongMemoize
def kerberos_default_realm
::Gitlab::Kerberos::Authentication.kerberos_default_realm
end
def uid
strong_memoize(:ee_uid) do
ee_uid = super
# For Kerberos, usernames `principal` and `principal@DEFAULT.REALM`
# are equivalent and may be used indifferently, but omniauth_kerberos
# does not normalize them as of version 0.3.0, so add the default
# realm ourselves if appropriate
if provider == 'kerberos' && ee_uid.present?
ee_uid += "@#{kerberos_default_realm}" unless ee_uid.include?('@')
end
ee_uid
end
end
end
end
end
end
end
module EE
module Gitlab
module Auth
module OAuth
module User
protected
def find_ldap_person(auth_hash, adapter)
if auth_hash.provider == 'kerberos'
::Gitlab::Auth::LDAP::Person.find_by_kerberos_principal(auth_hash.uid, adapter)
else
super
end
end
end
end
end
end
end
module EE
module Gitlab
module LDAP
# Create a hash map of member DNs to access levels. The highest
# access level is retained in cases where `set` is called multiple times
# for the same DN.
class AccessLevels < Hash
def set(dns, to:)
dns.each do |dn|
current = self[dn]
# Keep the higher of the access values.
self[dn] = to if current.nil? || to > current
end
end
end
end
end
end
# LDAP connection adapter EE mixin
#
# This module is intended to encapsulate EE-specific adapter methods
# and be **prepended** in the `Gitlab::LDAP::Adapter` class.
module EE
module Gitlab
module LDAP
module Adapter
# Get LDAP groups from ou=Groups
#
# cn - filter groups by name
#
# Ex.
# groups("dev*") # return all groups start with 'dev'
#
def groups(cn = "*", size = nil)
options = {
base: config.group_base,
filter: Net::LDAP::Filter.eq("cn", cn),
attributes: %w(dn cn memberuid member submember uniquemember memberof)
}
options[:size] = size if size
ldap_search(options).map do |entry|
LDAP::Group.new(entry, self)
end
end
def group(*args)
groups(*args).first
end
def group_members_in_range(dn, range_start)
ldap_search(
base: dn,
scope: Net::LDAP::SearchScope_BaseObject,
attributes: ["member;range=#{range_start}-*"]
).first
end
def nested_groups(parent_dn)
options = {
base: config.group_base,
filter: Net::LDAP::Filter.join(
Net::LDAP::Filter.eq('objectClass', 'group'),
Net::LDAP::Filter.eq('memberOf', parent_dn)
)
}
ldap_search(options).map do |entry|
LDAP::Group.new(entry, self)
end
end
end
end
end
end
module EE
module Gitlab
module LDAP
module Config
extend ActiveSupport::Concern
class_methods do
def group_sync_enabled?
enabled? && ::License.feature_available?(:ldap_group_sync)
end
end
end
end
end
end
module EE
module Gitlab
module LDAP
class Group
attr_accessor :adapter
attr_reader :entry
def self.find_by_cn(cn, adapter)
cn = Net::LDAP::Filter.escape(cn)
adapter.group(cn)
end
def initialize(entry, adapter = nil)
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@adapter = adapter
end
def active_directory?
adapter.config.active_directory
end
def cn
entry.cn.first
end
def name
cn
end
def path
name.parameterize
end
def memberuid?
entry.respond_to? :memberuid
end
def member_uids
@member_uids ||= entry.memberuid.map do |uid|
::Gitlab::LDAP::Person.normalize_uid(uid)
end
end
delegate :dn, to: :entry
def member_dns(nested_groups_to_skip = [])
dns = []
if active_directory? && adapter
dns.concat(active_directory_members(entry, nested_groups_to_skip))
end
dns.concat(entry_member_dns(entry))
dns.uniq
end
private
# Active Directory range member methods
def has_member_range?(entry)
member_range_attribute(entry).present?
end
def member_range_attribute(entry)
entry.attribute_names.find { |a| a.to_s.start_with?("member;range=")}.to_s
end
def active_directory_members(entry, nested_groups_to_skip)
require 'net/ldap/dn'
members = []
# Retrieve all member pages/ranges
members.concat(ranged_members(entry)) if has_member_range?(entry)
# Process nested group members
members.concat(nested_members(nested_groups_to_skip))
# Clean dns of groups and users outside the base
members.reject! { |dn| nested_groups_to_skip.include?(dn) }
return [] if members.empty?
# Only return members within our given base
members_within_base(members)
end
# AD requires use of range retrieval for groups with more than 1500 members
# cf. https://msdn.microsoft.com/en-us/library/aa367017(v=vs.85).aspx
def ranged_members(entry)
members = []
# Concatenate the members in the current range
dns = entry[member_range_attribute(entry)]
dns = normalize_dns(dns)
members.concat(dns)
# Recursively concatenate members until end of ranges
if has_more_member_ranges?(entry)
next_entry = adapter.group_members_in_range(dn, next_member_range_start(entry))
members.concat(ranged_members(next_entry))
end
members
end
# Process any AD nested groups. Use a manual process because
# the AD recursive member of filter is too slow and uses too
# much CPU on the AD server.
def nested_members(nested_groups_to_skip)
# Ignore this group if we see it again in a nested group.
# Prevents infinite loops.
nested_groups_to_skip << dn
members = []
nested_groups = adapter.nested_groups(dn)
nested_groups.each do |nested_group|
next if nested_groups_to_skip.include?(nested_group.dn)
members.concat(nested_group.member_dns(nested_groups_to_skip))
end
members
end
def has_more_member_ranges?(entry)
next_member_range_start(entry).present?
end
def next_member_range_start(entry)
match = member_range_attribute(entry).match /^member;range=\d+-(\d+|\*)$/
match[1].to_i + 1 if match.present? && match[1] != '*'
end
# The old AD recursive member filter would exclude any members that
# were outside the given search base. To maintain that behavior,
# we need to do the same.
#
# Split the base and each member DN into pairs. Compare the last
# base N pairs of the member DN. If they match, the user is within
# the base DN.
#
# Ex.
# - Member DN: 'uid=user,ou=users,dc=example,dc=com'
# - Base DN: 'dc=example,dc=com'
#
# Base has 2 pairs ([dc,example], [dc,com]). If the last 2 pairs of
# the user DN match, profit!
def members_within_base(members)
begin
base = ::Gitlab::LDAP::DN.new(adapter.config.base).to_a
rescue ::Gitlab::LDAP::DN::FormatError => e
Rails.logger.error "Configured LDAP `base` is invalid: '#{adapter.config.base}'. Error: \"#{e.message}\""
return []
end
members.select do |dn|
begin
::Gitlab::LDAP::DN.new(dn).to_a.last(base.length) == base
rescue ::Gitlab::LDAP::DN::FormatError => e
Rails.logger.warn "Received invalid member DN from LDAP group '#{cn}': '#{dn}'. Error: \"#{e.message}\". Skipping"
end
end
end
def normalize_dns(dns)
dns.map do |dn|
::Gitlab::LDAP::Person.normalize_dn(dn)
end
end
def entry_member_dns(entry)
dns = entry.try(:member) || entry.try(:uniquemember) || entry.try(:memberof)
dns&.concat(entry.try(:submember) || [])
if dns
normalize_dns(dns)
else
Rails.logger.warn("Could not find member DNs for LDAP group #{entry.inspect}")
[]
end
end
end
end
end
end
require 'net/ldap/dn'
module EE
module Gitlab
module LDAP
module Person
extend ActiveSupport::Concern
class_methods do
def find_by_email(email, adapter)
email_attributes = Array(adapter.config.attributes['email'])
email_attributes.each do |possible_attribute|
found_user = adapter.user(possible_attribute, email)
return found_user if found_user
end
nil
end
def find_by_kerberos_principal(principal, adapter)
uid, domain = principal.split('@', 2)
return nil unless uid && domain
# In multi-forest setups, there may be several users with matching
# uids but differing DNs, so skip adapters configured to connect to
# non-matching domains
return unless domain.casecmp(domain_from_dn(adapter.config.base)) == 0
find_by_uid(uid, adapter)
end
# Extracts the rightmost unbroken set of domain components from an
# LDAP DN and constructs a domain name from them
def domain_from_dn(dn)
dn_components = []
::Gitlab::LDAP::DN.new(dn).each_pair { |name, value| dn_components << { name: name, value: value } }
dn_components
.reverse
.take_while { |rdn| rdn[:name].casecmp('DC').zero? } # Domain Component
.map { |rdn| rdn[:value] }
.reverse
.join('.')
end
def ldap_attributes(config)
attributes = super + [
'memberof',
(config.sync_ssh_keys if config.sync_ssh_keys.is_a?(String))
]
attributes.compact.uniq
end
end
def ssh_keys
if config.sync_ssh_keys? && entry.respond_to?(config.sync_ssh_keys)
entry[config.sync_ssh_keys.to_sym]
.map { |key| key[/(ssh|ecdsa)-[^ ]+ [^\s]+/] }
.compact
else
[]
end
end
# We assume that the Kerberos username matches the configured uid
# attribute in LDAP. For Active Directory, this is `sAMAccountName`
def kerberos_principal
return nil unless uid
uid + '@' + self.class.domain_from_dn(dn).upcase
end
def memberof
return [] unless entry.attribute_names.include?(:memberof)
entry.memberof
end
def group_cns
memberof.map { |memberof_value| cn_from_memberof(memberof_value) }
end
def cn_from_memberof(memberof)
# Only get the first CN value of the string, that's the one that contains
# the group name
memberof.match(/(?:cn=([\w\s]+))/i)&.captures&.first
end
end
end
end
end
module EE
module Gitlab
module LDAP
module Sync
class AdminUsers
attr_reader :provider, :proxy
def self.execute(proxy)
self.new(proxy).update_permissions
end
def initialize(proxy)
@provider = proxy.provider
@proxy = proxy
end
def update_permissions
return if admin_group.empty?
admin_group_member_dns = proxy.dns_for_group_cn(admin_group)
current_admin_users = ::User.admins.with_provider(provider)
verified_admin_users = []
# Verify existing admin users and add new ones.
admin_group_member_dns.each do |member_dn|
user = ::Gitlab::LDAP::User.find_by_uid_and_provider(member_dn, provider)
if user.present?
user.admin = true
user.save
verified_admin_users << user
else
Rails.logger.debug do
<<-MSG.strip_heredoc.tr("\n", ' ')
#{self.class.name}: User with DN `#{member_dn}` should have admin
access but there is no user in GitLab with that identity.
Membership will be updated once the user signs in for the first time.
MSG
end
end
end
# Revoke the unverified admins.
current_admin_users.each do |user|
unless verified_admin_users.include?(user)
user.admin = false
user.save
end
end
end
private
def admin_group
proxy.adapter.config.admin_group
end
end
end
end
end
end
module EE
module Gitlab
module LDAP
module Sync
class ExternalUsers
attr_reader :provider, :proxy
def self.execute(proxy)
self.new(proxy).update_permissions
end
def initialize(proxy)
@provider = proxy.provider
@proxy = proxy
end
def update_permissions
return unless external_groups.any?
current_external_users = ::User.external.with_provider(provider)
verified_external_users = []
external_groups.each do |group|
group_dns = proxy.dns_for_group_cn(group)
group_dns.each do |member_dn|
user = ::Gitlab::LDAP::User.find_by_uid_and_provider(member_dn, provider)
if user.present?
user.external = true
user.save
verified_external_users << user
else
Rails.logger.debug do
<<-MSG.strip_heredoc.tr("\n", ' ')
#{self.class.name}: User with DN `#{member_dn}` should be marked as
external but there is no user in GitLab with that identity.
Membership will be updated once the user signs in for the first time.
MSG
end
end
end
end
# Restore normal access to users no longer found in the external groups
current_external_users.each do |user|
unless verified_external_users.include?(user)
user.external = false
user.save
end
end
end
private
def external_groups
proxy.adapter.config.external_groups
end
end
end
end
end
end
module EE
module Gitlab
module LDAP
module Sync
class Group
attr_reader :provider, :group, :proxy
class << self
# Sync members across all providers for the given group.
def execute_all_providers(group)
return unless ldap_sync_ready?(group)
group.start_ldap_sync
Rails.logger.debug { "Started syncing all providers for '#{group.name}' group" }
# Shuffle providers to prevent a scenario where sync fails after a time
# and only the first provider or two get synced. This shuffles the order
# so subsequent syncs should eventually get to all providers. Obviously
# we should avoid failure, but this is an additional safeguard.
::Gitlab::LDAP::Config.providers.shuffle.each do |provider|
Sync::Proxy.open(provider) do |proxy|
new(group, proxy).update_permissions
end
end
group.finish_ldap_sync
Rails.logger.debug { "Finished syncing all providers for '#{group.name}' group" }
end
# Sync members across a single provider for the given group.
def execute(group, proxy)
return unless ldap_sync_ready?(group)
group.start_ldap_sync
Rails.logger.debug { "Started syncing '#{proxy.provider}' provider for '#{group.name}' group" }
sync_group = new(group, proxy)
sync_group.update_permissions
group.finish_ldap_sync
Rails.logger.debug { "Finished syncing '#{proxy.provider}' provider for '#{group.name}' group" }
end
def ldap_sync_ready?(group)
fail_stuck_group(group)
return true unless group.ldap_sync_started?
Rails.logger.warn "Group '#{group.name}' is not ready for LDAP sync. Skipping"
false
end
def fail_stuck_group(group)
return unless group.ldap_sync_started?
if group.ldap_sync_last_sync_at < 1.hour.ago
group.mark_ldap_sync_as_failed('The sync took too long to complete.')
end
end
end
def initialize(group, proxy)
@provider = proxy.provider
@group = group
@proxy = proxy
end
def update_permissions
unless group.ldap_sync_started?
logger.warn "Group '#{group.name}' LDAP sync status must be 'started' before updating permissions"
return
end
access_levels = AccessLevels.new
# Only iterate over group links for the current provider
group.ldap_group_links.with_provider(provider).each do |group_link|
next unless group_link.active?
update_access_levels(access_levels, group_link)
end
update_existing_group_membership(group, access_levels)
add_new_members(group, access_levels)
end
private
def update_access_levels(access_levels, group_link)
if member_dns = get_member_dns(group_link)
access_levels.set(member_dns, to: group_link.group_access)
logger.debug do
"Resolved '#{group.name}' group member access: #{access_levels.to_hash}"
end
end
end
def get_member_dns(group_link)
group_link.cn ? dns_for_group_cn(group_link.cn) : UserFilter.filter(@proxy, group_link.filter)
end
def dns_for_group_cn(group_cn)
if config.group_base.blank?
logger.debug { "No `group_base` configured for '#{provider}' provider and group link CN #{group_cn}. Skipping" }
return nil
end
proxy.dns_for_group_cn(group_cn)
end
def dn_for_uid(uid)
proxy.dn_for_uid(uid)
end
def update_existing_group_membership(group, access_levels)
logger.debug { "Updating existing membership for '#{group.name}' group" }
select_and_preload_group_members(group).each do |member|
user = member.user
identity = user.identities.select(:id, :extern_uid)
.with_provider(provider).first
member_dn = identity.extern_uid.downcase
# Skip if this is not an LDAP user with a valid `extern_uid`.
next unless member_dn.present?
# Prevent shifting group membership, in case where user is a member
# of two LDAP groups from different providers linked to the same
# GitLab group. This is not ideal, but preserves existing behavior.
if user.ldap_identity.id != identity.id
access_levels.delete(member_dn)
next
end
desired_access = access_levels[member_dn]
# Skip validations and callbacks. We have a limited set of attrs
# due to the `select` lookup, and we need to be efficient.
# Low risk, because the member should already be valid.
member.update_column(:ldap, true) unless member.ldap?
# Don't do anything if the user already has the desired access level
if member.access_level == desired_access
access_levels.delete(member_dn)
next
end
# Check and update the access level. If `desired_access` is `nil`
# we need to delete the user from the group.
if desired_access.present?
# Delete this entry from the hash now that we're acting on it
access_levels.delete(member_dn)
next if member.ldap? && member.override?
add_or_update_user_membership(
user,
group,
desired_access
)
elsif group.last_owner?(user)
warn_cannot_remove_last_owner(user, group)
else
group.users.destroy(user)
end
end
end
def add_new_members(group, access_levels)
logger.debug { "Adding new members to '#{group.name}' group" }
access_levels.each do |member_dn, access_level|
user = ::Gitlab::LDAP::User.find_by_uid_and_provider(member_dn, provider)
if user.present?
add_or_update_user_membership(
user,
group,
access_level
)
else
logger.debug do
<<-MSG.strip_heredoc.tr("\n", ' ')
#{self.class.name}: User with DN `#{member_dn}` should have access
to '#{group.name}' group but there is no user in GitLab with that
identity. Membership will be updated once the user signs in for
the first time.
MSG
end
end
end
end
def add_or_update_user_membership(user, group, access, current_user: nil)
# Prevent the last owner of a group from being demoted
if access < ::Gitlab::Access::OWNER && group.last_owner?(user)
warn_cannot_remove_last_owner(user, group)
else
# If you pass the user object, instead of just user ID,
# it saves an extra user database query.
group.add_user(
user,
access,
current_user: current_user,
ldap: true
)
end
end
def warn_cannot_remove_last_owner(user, group)
logger.warn do
<<-MSG.strip_heredoc.tr("\n", ' ')
#{self.class.name}: LDAP group sync cannot remove #{user.name}
(#{user.id}) from group #{group.name} (#{group.id}) as this is
the group's last owner
MSG
end
end
def select_and_preload_group_members(group)
group.members.select(:id, :access_level, :user_id, :ldap, :override)
.with_identity_provider(provider).preload(:user)
end
def logger
Rails.logger
end
def config
@proxy.adapter.config
end
end
end
end
end
end
module EE
module Gitlab
module LDAP
module Sync
class Groups
attr_reader :provider, :proxy
def self.execute
# Shuffle providers to prevent a scenario where sync fails after a time
# and only the first provider or two get synced. This shuffles the order
# so subsequent syncs should eventually get to all providers. Obviously
# we should avoid failure, but this is an additional safeguard.
::Gitlab::LDAP::Config.providers.shuffle.each do |provider|
Sync::Proxy.open(provider) do |proxy|
group_sync = self.new(proxy)
group_sync.update_permissions
end
end
true
end
def initialize(proxy)
@provider = proxy.provider
@proxy = proxy
end
def update_permissions
logger.debug { "Performing LDAP group sync for '#{provider}' provider" }
sync_groups
logger.debug { "Finished LDAP group sync for '#{provider}' provider" }
if config.admin_group.present?
logger.debug { "Syncing admin users for '#{provider}' provider" }
sync_admin_users
logger.debug { "Finished syncing admin users for '#{provider}' provider" }
else
logger.debug { "No `admin_group` configured for '#{provider}' provider. Skipping" }
end
if config.external_groups.empty?
logger.debug { "No `external_groups` configured for '#{provider}' provider. Skipping" }
else
logger.debug { "Syncing external users for '#{provider}' provider" }
sync_external_users
logger.debug { "Finished syncing external users for '#{provider}' provider" }
end
nil
end
private
def sync_groups
groups_where_group_links_with_provider_ordered.each do |group|
Sync::Group.execute(group, proxy)
end
end
def sync_admin_users
Sync::AdminUsers.execute(proxy)
end
def sync_external_users
Sync::ExternalUsers.execute(proxy)
end
def groups_where_group_links_with_provider_ordered
::Group.where_group_links_with_provider(provider)
.preload(:ldap_group_links)
.reorder('ldap_sync_last_successful_update_at ASC, namespaces.id ASC')
.distinct
end
def config
proxy.adapter.config
end
def logger
Rails.logger
end
end
end
end
end
end
require 'net/ldap/dn'
module EE
module Gitlab
module LDAP
module Sync
class Proxy
attr_reader :provider, :adapter
# Open a connection and run all queries through it.
# It's more efficient than the default of opening/closing per LDAP query.
def self.open(provider, &block)
::Gitlab::LDAP::Adapter.open(provider) do |adapter|
block.call(self.new(provider, adapter))
end
end
def initialize(provider, adapter)
@adapter = adapter
@provider = provider
end
# Cache LDAP group member DNs so we don't query LDAP groups more than once.
def dns_for_group_cn(group_cn)
@dns_for_group_cn ||= Hash.new { |h, k| h[k] = ldap_group_member_dns(k) }
@dns_for_group_cn[group_cn]
end
# Cache user DN so we don't generate excess queries to map UID to DN
def dn_for_uid(uid)
@dn_for_uid ||= Hash.new { |h, k| h[k] = member_uid_to_dn(k) }
@dn_for_uid[uid]
end
private
def ldap_group_member_dns(ldap_group_cn)
ldap_group = LDAP::Group.find_by_cn(ldap_group_cn, adapter)
unless ldap_group.present?
logger.warn { "Cannot find LDAP group with CN '#{ldap_group_cn}'. Skipping" }
return []
end
member_dns = ldap_group.member_dns
if member_dns.empty?
# Group must be empty
return [] unless ldap_group.memberuid?
members = ldap_group.member_uids
member_dns = members.map { |uid| dn_for_uid(uid) }
end
# Various lookups in this method could return `nil` values.
# Compact the array to remove those entries
member_dns.compact!
ensure_full_dns!(member_dns)
logger.debug { "Members in '#{ldap_group.name}' LDAP group: #{member_dns}" }
# Various lookups in this method could return `nil` values.
# Compact the array to remove those entries
member_dns
end
# At least one customer reported that their LDAP `member` values contain
# only `uid=username` and not the full DN. This method allows us to
# account for that. See gitlab-ee#442
def ensure_full_dns!(dns)
dns.map! do |dn|
begin
parsed_dn = ::Gitlab::LDAP::DN.new(dn).to_a
rescue ::Gitlab::LDAP::DN::FormatError => e
logger.error { "Found malformed DN: '#{dn}'. Skipping. Error: \"#{e.message}\"" }
next
end
final_dn =
# If there is more than one key/value set we must have a full DN,
# or at least the probability is higher.
if parsed_dn.count > 2
dn
elsif parsed_dn.count == 0
logger.warn { "Found null DN. Skipping." }
nil
elsif parsed_dn[0] == 'uid'
dn_for_uid(parsed_dn[1])
else
logger.warn { "Found potentially malformed/incomplete DN: '#{dn}'" }
dn
end
clean_encoding(final_dn)
end
# Remove `nil` values generated by the rescue above.
dns.compact!
end
# net-ldap only returns ASCII-8BIT and does not support UTF-8 out-of-the-box:
# https://github.com/ruby-ldap/ruby-net-ldap/issues/4
def clean_encoding(dn)
return dn unless dn.present?
dn.force_encoding('UTF-8')
rescue
dn
end
def member_uid_to_dn(uid)
identity = ::Identity.with_secondary_extern_uid(provider, uid).take
if identity.present?
# Use the DN on record in GitLab when it's available
identity.extern_uid
else
ldap_user = ::Gitlab::LDAP::Person.find_by_uid(uid, adapter)
# Can't find a matching user
return nil unless ldap_user.present?
# Update user identity so we don't have to go through this again
update_identity(ldap_user.dn, uid)
ldap_user.dn
end
end
def update_identity(dn, uid)
identity = ::Identity.with_extern_uid(provider, dn).take
# User may not exist in GitLab yet. Skip.
return unless identity.present?
identity.secondary_extern_uid = uid
identity.save
end
def logger
Rails.logger
end
end
end
end
end
end
# LDAP User EE mixin
#
# This module is intended to encapsulate EE-specific User methods
# and be **prepended** in the `Gitlab::LDAP::User` class.
module EE
module Gitlab
module LDAP
module User
def initialize(auth_hash)
super
set_external_with_external_groups
end
private
# Intended to be called during #initialize, and #save should be called
# after initialize.
def set_external_with_external_groups
return if ldap_config.external_groups.empty?
gl_user.external = in_any_external_group?
end
# Returns true if the User is found in an external group listed in the
# config.
def in_any_external_group?
with_proxy do |proxy|
external_groups = proxy.adapter.config.external_groups
external_groups.any? do |group_cn|
in_group?(group_cn, proxy)
end
end
end
# Returns true if the User is a member of the group.
def in_group?(group_cn, proxy)
member_dns = proxy.dns_for_group_cn(group_cn)
member_dns.include?(auth_hash.uid)
end
def with_proxy(&block)
::EE::Gitlab::LDAP::Sync::Proxy.open(auth_hash.provider, &block)
end
end
end
end
end
module EE
module Gitlab
module LDAP
class UserFilter
def self.filter(*args)
new(*args).filter
end
def initialize(proxy, filter)
@proxy = proxy
@filter = filter
end
def filter
logger.debug "Running filter #{@filter} against #{@proxy.provider}"
@proxy.adapter.ldap_search(options).map(&:dn).tap do |dns|
logger.debug "Found #{dns.count} mathing users for filter #{@filter}"
end
end
private
def options
{ base: config.base, filter: construct_filter }
end
def construct_filter
Net::LDAP::Filter.construct(@filter)
end
def config
@proxy.adapter.config
end
def logger
Rails.logger
end
end
end
end
end
module EE
module Gitlab
module OAuth
module AuthHash
include ::Gitlab::Utils::StrongMemoize
def kerberos_default_realm
::Gitlab::Kerberos::Authentication.kerberos_default_realm
end
def uid
strong_memoize(:ee_uid) do
ee_uid = super
# For Kerberos, usernames `principal` and `principal@DEFAULT.REALM`
# are equivalent and may be used indifferently, but omniauth_kerberos
# does not normalize them as of version 0.3.0, so add the default
# realm ourselves if appropriate
if provider == 'kerberos' && ee_uid.present?
ee_uid += "@#{kerberos_default_realm}" unless ee_uid.include?('@')
end
ee_uid
end
end
end
end
end
end
module EE
module Gitlab
module OAuth
module User
protected
def find_ldap_person(auth_hash, adapter)
if auth_hash.provider == 'kerberos'
::Gitlab::LDAP::Person.find_by_kerberos_principal(auth_hash.uid, adapter)
else
super
end
end
end
end
end
end
desc "GITLAB | migrate provider names to multiple ldap setup" desc "GITLAB | migrate provider names to multiple ldap setup"
namespace :gitlab do namespace :gitlab do
task migrate_ldap_providers: :environment do task migrate_ldap_providers: :environment do
config = Gitlab::LDAP::Config config = Gitlab::Auth::LDAP::Config
raise 'No LDAP server hash defined. See config/gitlab.yml.example for an example' unless config.servers.any? raise 'No LDAP server hash defined. See config/gitlab.yml.example for an example' unless config.servers.any?
provider = config.servers.first['provider_name'] provider = config.servers.first['provider_name']
......
require 'spec_helper' require 'spec_helper'
describe EE::Gitlab::LDAP::AccessLevels do describe EE::Gitlab::Auth::LDAP::AccessLevels do
describe '#set' do describe '#set' do
let(:access_levels) { described_class.new } let(:access_levels) { described_class.new }
let(:dns) do let(:dns) do
......
require 'spec_helper' require 'spec_helper'
describe EE::Gitlab::LDAP::Group do describe EE::Gitlab::Auth::LDAP::Group do
include LdapHelpers include LdapHelpers
before do before do
......
require 'spec_helper' require 'spec_helper'
describe EE::Gitlab::LDAP::Sync::AdminUsers do describe EE::Gitlab::Auth::LDAP::Sync::AdminUsers do
include LdapHelpers include LdapHelpers
let(:adapter) { ldap_adapter } let(:adapter) { ldap_adapter }
......
require 'spec_helper' require 'spec_helper'
describe EE::Gitlab::LDAP::Sync::ExternalUsers do describe EE::Gitlab::Auth::LDAP::Sync::ExternalUsers do
include LdapHelpers include LdapHelpers
describe '#update_permissions' do describe '#update_permissions' do
......
require 'spec_helper' require 'spec_helper'
describe EE::Gitlab::LDAP::Sync::Group do describe EE::Gitlab::Auth::LDAP::Sync::Group do
include LdapHelpers include LdapHelpers
let(:adapter) { ldap_adapter } let(:adapter) { ldap_adapter }
...@@ -69,7 +69,7 @@ describe EE::Gitlab::LDAP::Sync::Group do ...@@ -69,7 +69,7 @@ describe EE::Gitlab::LDAP::Sync::Group do
adapter = ldap_adapter('main') adapter = ldap_adapter('main')
proxy = proxy(adapter, 'main') proxy = proxy(adapter, 'main')
allow(EE::Gitlab::LDAP::Sync::Proxy).to receive(:open).and_yield(proxy) allow(EE::Gitlab::Auth::LDAP::Sync::Proxy).to receive(:open).and_yield(proxy)
end end
let(:group) do let(:group) do
...@@ -303,7 +303,7 @@ describe EE::Gitlab::LDAP::Sync::Group do ...@@ -303,7 +303,7 @@ describe EE::Gitlab::LDAP::Sync::Group do
it 'does not update permissions when group base is missing' do it 'does not update permissions when group base is missing' do
stub_ldap_config(group_base: nil) stub_ldap_config(group_base: nil)
expect_any_instance_of(EE::Gitlab::LDAP::Sync::Proxy).not_to receive(:dns_for_group_cn) expect_any_instance_of(EE::Gitlab::Auth::LDAP::Sync::Proxy).not_to receive(:dns_for_group_cn)
sync_group.update_permissions sync_group.update_permissions
end end
...@@ -390,7 +390,7 @@ describe EE::Gitlab::LDAP::Sync::Group do ...@@ -390,7 +390,7 @@ describe EE::Gitlab::LDAP::Sync::Group do
# Safe-check because some permissions are removed when `Group#ldap_synced?` # Safe-check because some permissions are removed when `Group#ldap_synced?`
# is true (e.g. in `GroupPolicy`). # is true (e.g. in `GroupPolicy`).
expect(group).to be_ldap_synced expect(group).to be_ldap_synced
allow(EE::Gitlab::LDAP::UserFilter).to receive(:filter).and_return([user_dn(user.username)]) allow(EE::Gitlab::Auth::LDAP::UserFilter).to receive(:filter).and_return([user_dn(user.username)])
group.start_ldap_sync group.start_ldap_sync
end end
......
require 'spec_helper' require 'spec_helper'
describe EE::Gitlab::LDAP::Sync::Groups do describe EE::Gitlab::Auth::LDAP::Sync::Groups do
include LdapHelpers include LdapHelpers
let(:adapter) { ldap_adapter } let(:adapter) { ldap_adapter }
...@@ -8,9 +8,9 @@ describe EE::Gitlab::LDAP::Sync::Groups do ...@@ -8,9 +8,9 @@ describe EE::Gitlab::LDAP::Sync::Groups do
describe '#update_permissions' do describe '#update_permissions' do
before do before do
allow(EE::Gitlab::LDAP::Sync::Group).to receive(:execute) allow(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute)
allow(EE::Gitlab::LDAP::Sync::AdminUsers).to receive(:execute) allow(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).to receive(:execute)
allow(EE::Gitlab::LDAP::Sync::ExternalUsers).to receive(:execute) allow(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).to receive(:execute)
2.times { create(:group_with_ldap_group_link) } 2.times { create(:group_with_ldap_group_link) }
end end
...@@ -24,12 +24,12 @@ describe EE::Gitlab::LDAP::Sync::Groups do ...@@ -24,12 +24,12 @@ describe EE::Gitlab::LDAP::Sync::Groups do
stub_ldap_config(group_base: nil) stub_ldap_config(group_base: nil)
end end
it 'does not call EE::Gitlab::LDAP::Sync::AdminUsers#execute' do it 'does not call EE::Gitlab::Auth::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::AdminUsers).not_to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).not_to receive(:execute)
end end
it 'does not call EE::Gitlab::LDAP::Sync::ExternalUsers#execute' do it 'does not call EE::Gitlab::Auth::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::ExternalUsers).not_to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).not_to receive(:execute)
end end
end end
...@@ -39,16 +39,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do ...@@ -39,16 +39,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do
stub_ldap_config(group_base: 'dc=example,dc=com') stub_ldap_config(group_base: 'dc=example,dc=com')
end end
it 'calls EE::Gitlab::LDAP::Sync::Group#execute' do it 'calls EE::Gitlab::Auth::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute).twice expect(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute).twice
end end
it 'does not call EE::Gitlab::LDAP::Sync::AdminUsers#execute' do it 'does not call EE::Gitlab::Auth::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::AdminUsers).not_to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).not_to receive(:execute)
end end
it 'does not call EE::Gitlab::LDAP::Sync::ExternalUsers#execute' do it 'does not call EE::Gitlab::Auth::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::ExternalUsers).not_to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).not_to receive(:execute)
end end
end end
...@@ -60,16 +60,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do ...@@ -60,16 +60,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do
) )
end end
it 'calls EE::Gitlab::LDAP::Sync::Group#execute' do it 'calls EE::Gitlab::Auth::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute).twice expect(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute).twice
end end
it 'does not call EE::Gitlab::LDAP::Sync::AdminUsers#execute' do it 'does not call EE::Gitlab::Auth::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::AdminUsers).to receive(:execute).once expect(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).to receive(:execute).once
end end
it 'does not call EE::Gitlab::LDAP::Sync::ExternalUsers#execute' do it 'does not call EE::Gitlab::Auth::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::ExternalUsers).not_to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).not_to receive(:execute)
end end
end end
...@@ -81,16 +81,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do ...@@ -81,16 +81,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do
) )
end end
it 'calls EE::Gitlab::LDAP::Sync::Group#execute' do it 'calls EE::Gitlab::Auth::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute).twice expect(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute).twice
end end
it 'does not call EE::Gitlab::LDAP::Sync::AdminUsers#execute' do it 'does not call EE::Gitlab::Auth::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::AdminUsers).not_to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).not_to receive(:execute)
end end
it 'does not call EE::Gitlab::LDAP::Sync::ExternalUsers#execute' do it 'does not call EE::Gitlab::Auth::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::ExternalUsers).to receive(:execute).once expect(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).to receive(:execute).once
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
require 'net/ldap/dn' require 'net/ldap/dn'
describe EE::Gitlab::LDAP::Sync::Proxy do describe EE::Gitlab::Auth::LDAP::Sync::Proxy do
include LdapHelpers include LdapHelpers
let(:adapter) { ldap_adapter } let(:adapter) { ldap_adapter }
...@@ -56,7 +56,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do ...@@ -56,7 +56,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do
sync_proxy.dns_for_group_cn('ldap_group1') sync_proxy.dns_for_group_cn('ldap_group1')
expect(sync_proxy).not_to receive(:ldap_group_member_dns) expect(sync_proxy).not_to receive(:ldap_group_member_dns)
expect(EE::Gitlab::LDAP::Group).not_to receive(:find_by_cn) expect(EE::Gitlab::Auth::LDAP::Group).not_to receive(:find_by_cn)
sync_proxy.dns_for_group_cn('ldap_group1') sync_proxy.dns_for_group_cn('ldap_group1')
end end
...@@ -123,7 +123,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do ...@@ -123,7 +123,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do
end end
it 'retrieves the user from LDAP' do it 'retrieves the user from LDAP' do
expect(::Gitlab::LDAP::Person).to receive(:find_by_uid) expect(::Gitlab::Auth::LDAP::Person).to receive(:find_by_uid)
sync_proxy.dn_for_uid('jane_doe') sync_proxy.dn_for_uid('jane_doe')
end end
...@@ -133,7 +133,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do ...@@ -133,7 +133,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do
expect(sync_proxy).not_to receive(:member_uid_to_dn) expect(sync_proxy).not_to receive(:member_uid_to_dn)
expect(Identity).not_to receive(:find_by) expect(Identity).not_to receive(:find_by)
expect(::Gitlab::LDAP::Person).not_to receive(:find_by_uid) expect(::Gitlab::Auth::LDAP::Person).not_to receive(:find_by_uid)
sync_proxy.dn_for_uid('jane_doe') sync_proxy.dn_for_uid('jane_doe')
end end
...@@ -177,7 +177,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do ...@@ -177,7 +177,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do
end end
it 'does not query LDAP' do it 'does not query LDAP' do
expect(::Gitlab::LDAP::Person).not_to receive(:find_by_uid) expect(::Gitlab::Auth::LDAP::Person).not_to receive(:find_by_uid)
end end
it 'retrieves the DN from the identity' do it 'retrieves the DN from the identity' do
......
require 'spec_helper' require 'spec_helper'
describe EE::Gitlab::LDAP::UserFilter do describe EE::Gitlab::Auth::LDAP::UserFilter do
include LdapHelpers include LdapHelpers
let(:auth_hash) do let(:auth_hash) do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::LDAP::Adapter do describe Gitlab::Auth::LDAP::Adapter do
include LdapHelpers include LdapHelpers
let(:adapter) { ldap_adapter('ldapmain') } let(:adapter) { ldap_adapter('ldapmain') }
it 'includes the EE module' do it 'includes the EE module' do
expect(described_class).to include_module(EE::Gitlab::LDAP::Adapter) expect(described_class).to include_module(EE::Gitlab::Auth::LDAP::Adapter)
end end
describe '#groups' do describe '#groups' do
...@@ -34,7 +34,7 @@ describe Gitlab::LDAP::Adapter do ...@@ -34,7 +34,7 @@ describe Gitlab::LDAP::Adapter do
results = adapter.groups('group1') results = adapter.groups('group1')
expect(results.first).to be_a(EE::Gitlab::LDAP::Group) expect(results.first).to be_a(EE::Gitlab::Auth::LDAP::Group)
expect(results.first.cn).to eq('group1') expect(results.first.cn).to eq('group1')
expect(results.first.member_dns).to match_array(%w(uid=john uid=mary)) expect(results.first.member_dns).to match_array(%w(uid=john uid=mary))
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::LDAP::Person do describe Gitlab::Auth::LDAP::Person do
include LdapHelpers include LdapHelpers
let(:entry) { ldap_user_entry('john.doe') } let(:entry) { ldap_user_entry('john.doe') }
it 'includes the EE module' do it 'includes the EE module' do
expect(described_class).to include(EE::Gitlab::LDAP::Person) expect(described_class).to include(EE::Gitlab::Auth::LDAP::Person)
end end
describe '.ldap_attributes' do describe '.ldap_attributes' do
...@@ -84,7 +84,7 @@ describe Gitlab::LDAP::Person do ...@@ -84,7 +84,7 @@ describe Gitlab::LDAP::Person do
) )
end end
let(:config) { Gitlab::LDAP::Config.new('ldapmain') } let(:config) { Gitlab::Auth::LDAP::Config.new('ldapmain') }
let(:ldap_attributes) { described_class.ldap_attributes(config) } let(:ldap_attributes) { described_class.ldap_attributes(config) }
let(:expected_attributes) { %w(dn cn uid mail memberof) } let(:expected_attributes) { %w(dn cn uid mail memberof) }
...@@ -140,7 +140,7 @@ describe Gitlab::LDAP::Person do ...@@ -140,7 +140,7 @@ describe Gitlab::LDAP::Person do
subject { described_class.new(entry, 'ldapmain') } subject { described_class.new(entry, 'ldapmain') }
before do before do
allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(sync_ssh_keys: ssh_key_attribute_name) allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(sync_ssh_keys: ssh_key_attribute_name)
end end
context 'when the SSH key is literal' do context 'when the SSH key is literal' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::LDAP::User do describe Gitlab::Auth::LDAP::User do
include LdapHelpers include LdapHelpers
let(:ldap_user) { described_class.new(auth_hash) } let(:ldap_user) { described_class.new(auth_hash) }
...@@ -26,7 +26,7 @@ describe Gitlab::LDAP::User do ...@@ -26,7 +26,7 @@ describe Gitlab::LDAP::User do
end end
it 'includes the EE module' do it 'includes the EE module' do
expect(described_class).to include_module(EE::Gitlab::LDAP::User) expect(described_class).to include_module(EE::Gitlab::Auth::LDAP::User)
end end
describe '#initialize' do describe '#initialize' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::OAuth::AuthHash do describe Gitlab::Auth::OAuth::AuthHash do
let(:auth_hash) do let(:auth_hash) do
described_class.new( described_class.new(
OmniAuth::AuthHash.new( OmniAuth::AuthHash.new(
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::OAuth::User do describe Gitlab::Auth::OAuth::User do
include LdapHelpers include LdapHelpers
describe 'login through kerberos with linkable LDAP user' do describe 'login through kerberos with linkable LDAP user' do
......
...@@ -58,7 +58,7 @@ describe LdapGroupLink do ...@@ -58,7 +58,7 @@ describe LdapGroupLink do
end end
it 'defaults to the first ldap server if empty' do it 'defaults to the first ldap server if empty' do
expect( klass.new.provider ).to eql Gitlab::LDAP::Config.providers.first expect( klass.new.provider ).to eql Gitlab::Auth::LDAP::Config.providers.first
end end
end end
end end
......
...@@ -14,8 +14,8 @@ describe API::Ldap do ...@@ -14,8 +14,8 @@ describe API::Ldap do
OpenStruct.new(cn: 'students') OpenStruct.new(cn: 'students')
] ]
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter) allow(Gitlab::Auth::LDAP::Adapter).to receive(:new).and_return(adapter)
allow(adapter).to receive_messages(groups: groups) allow(adapter).to receive_messages(groups: groups)
end end
......
module EE module EE
module LdapHelpers module LdapHelpers
def proxy(adapter, provider = 'ldapmain') def proxy(adapter, provider = 'ldapmain')
EE::Gitlab::LDAP::Sync::Proxy.new(provider, adapter) EE::Gitlab::Auth::LDAP::Sync::Proxy.new(provider, adapter)
end end
# Stub an LDAP group search and provide the return entry. Specify `nil` for # Stub an LDAP group search and provide the return entry. Specify `nil` for
# `entry` to simulate when an LDAP group is not found # `entry` to simulate when an LDAP group is not found
# #
# Example: # Example:
# adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap)) # adapter = ::Gitlab::Auth::LDAP::Adapter.new('ldapmain', double(:ldap))
# ldap_group1 = ldap_group_entry('uid=user,ou=users,dc=example,dc=com') # ldap_group1 = ldap_group_entry('uid=user,ou=users,dc=example,dc=com')
# #
# stub_ldap_group_find_by_cn('ldap_group1', ldap_group1, adapter) # stub_ldap_group_find_by_cn('ldap_group1', ldap_group1, adapter)
def stub_ldap_group_find_by_cn(cn, entry, adapter = nil) def stub_ldap_group_find_by_cn(cn, entry, adapter = nil)
if entry.present? if entry.present?
return_value = EE::Gitlab::LDAP::Group.new(entry, adapter) return_value = EE::Gitlab::Auth::LDAP::Group.new(entry, adapter)
end end
allow(EE::Gitlab::LDAP::Group) allow(EE::Gitlab::Auth::LDAP::Group)
.to receive(:find_by_cn) .to receive(:find_by_cn)
.with(cn, kind_of(::Gitlab::LDAP::Adapter)).and_return(return_value) .with(cn, kind_of(::Gitlab::Auth::LDAP::Adapter)).and_return(return_value)
end end
# Create an LDAP group entry with any number of members. By default, creates # Create an LDAP group entry with any number of members. By default, creates
...@@ -94,7 +94,7 @@ module EE ...@@ -94,7 +94,7 @@ module EE
# Stub Active Directory range member retrieval. # Stub Active Directory range member retrieval.
# #
# Example: # Example:
# adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap)) # adapter = ::Gitlab::Auth::LDAP::Adapter.new('ldapmain', double(:ldap))
# group_entry_page1 = ldap_group_entry_with_member_range( # group_entry_page1 = ldap_group_entry_with_member_range(
# [user_dn('user1'), user_dn('user2'), user_dn('user3')], # [user_dn('user1'), user_dn('user2'), user_dn('user3')],
# range_start: '0', # range_start: '0',
...@@ -105,7 +105,7 @@ module EE ...@@ -105,7 +105,7 @@ module EE
# range_start: '3', # range_start: '3',
# range_end: '*' # range_end: '*'
# ) # )
# group = EE::Gitlab::LDAP::Group.new(group_entry_page1, adapter) # group = EE::Gitlab::Auth::LDAP::Group.new(group_entry_page1, adapter)
# #
# stub_ldap_adapter_group_members_in_range(group_entry_page2, adapter, range_start: '3') # stub_ldap_adapter_group_members_in_range(group_entry_page2, adapter, range_start: '3')
def stub_ldap_adapter_group_members_in_range( def stub_ldap_adapter_group_members_in_range(
...@@ -118,7 +118,7 @@ module EE ...@@ -118,7 +118,7 @@ module EE
end end
def stub_ldap_adapter_nested_groups(parent_dn, entries = [], adapter = ldap_adapter) def stub_ldap_adapter_nested_groups(parent_dn, entries = [], adapter = ldap_adapter)
groups = entries.map { |entry| EE::Gitlab::LDAP::Group.new(entry, adapter) } groups = entries.map { |entry| EE::Gitlab::Auth::LDAP::Group.new(entry, adapter) }
allow(adapter).to receive(:nested_groups).with(parent_dn).and_return(groups) allow(adapter).to receive(:nested_groups).with(parent_dn).and_return(groups)
end end
......
...@@ -5,13 +5,13 @@ describe LdapAllGroupsSyncWorker do ...@@ -5,13 +5,13 @@ describe LdapAllGroupsSyncWorker do
before do before do
allow(Sidekiq.logger).to receive(:info) allow(Sidekiq.logger).to receive(:info)
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
end end
describe '#perform' do describe '#perform' do
context 'with the default license key' do context 'with the default license key' do
it 'syncs all groups when group_id is nil' do it 'syncs all groups when group_id is nil' do
expect(EE::Gitlab::LDAP::Sync::Groups).to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::Groups).to receive(:execute)
subject.perform subject.perform
end end
...@@ -23,7 +23,7 @@ describe LdapAllGroupsSyncWorker do ...@@ -23,7 +23,7 @@ describe LdapAllGroupsSyncWorker do
end end
it 'does not sync all groups' do it 'does not sync all groups' do
expect(EE::Gitlab::LDAP::Sync::Groups).not_to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::Groups).not_to receive(:execute)
subject.perform subject.perform
end end
......
...@@ -7,14 +7,14 @@ describe LdapGroupSyncWorker do ...@@ -7,14 +7,14 @@ describe LdapGroupSyncWorker do
def expect_fake_proxy(provider) def expect_fake_proxy(provider)
fake = double fake = double
expect(EE::Gitlab::LDAP::Sync::Proxy) expect(EE::Gitlab::Auth::LDAP::Sync::Proxy)
.to receive(:open).with(provider).and_yield(fake) .to receive(:open).with(provider).and_yield(fake)
fake fake
end end
before do before do
allow(Sidekiq.logger).to receive(:info) allow(Sidekiq.logger).to receive(:info)
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
end end
describe '#perform' do describe '#perform' do
...@@ -63,16 +63,16 @@ describe LdapGroupSyncWorker do ...@@ -63,16 +63,16 @@ describe LdapGroupSyncWorker do
describe '#sync_group' do describe '#sync_group' do
it 'syncs a single provider when a provider was given' do it 'syncs a single provider when a provider was given' do
proxy = EE::Gitlab::LDAP::Sync::Proxy.new('ldapmain', ldap_adapter) proxy = EE::Gitlab::Auth::LDAP::Sync::Proxy.new('ldapmain', ldap_adapter)
expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute) expect(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute)
.with(group, proxy) .with(group, proxy)
subject.sync_group(group, proxy: proxy) subject.sync_group(group, proxy: proxy)
end end
it 'syncs all providers when no proxy was given' do it 'syncs all providers when no proxy was given' do
expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute_all_providers) expect(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute_all_providers)
.with(group) .with(group)
subject.sync_group(group) subject.sync_group(group)
......
...@@ -5,7 +5,7 @@ describe LdapSyncWorker do ...@@ -5,7 +5,7 @@ describe LdapSyncWorker do
before do before do
allow(Sidekiq.logger).to receive(:info) allow(Sidekiq.logger).to receive(:info)
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
create(:omniauth_user, provider: 'ldapmain') create(:omniauth_user, provider: 'ldapmain')
end end
...@@ -13,7 +13,7 @@ describe LdapSyncWorker do ...@@ -13,7 +13,7 @@ describe LdapSyncWorker do
describe '#perform' do describe '#perform' do
context 'with the default license key' do context 'with the default license key' do
it 'syncs all LDAP users' do it 'syncs all LDAP users' do
expect(Gitlab::LDAP::Access).to receive(:allowed?) expect(Gitlab::Auth::LDAP::Access).to receive(:allowed?)
subject.perform subject.perform
end end
...@@ -25,7 +25,7 @@ describe LdapSyncWorker do ...@@ -25,7 +25,7 @@ describe LdapSyncWorker do
end end
it 'does not sync LDAP users' do it 'does not sync LDAP users' do
expect(Gitlab::LDAP::Access).not_to receive(:allowed?) expect(Gitlab::Auth::LDAP::Access).not_to receive(:allowed?)
subject.perform subject.perform
end end
......
...@@ -275,7 +275,7 @@ module API ...@@ -275,7 +275,7 @@ module API
desc 'Sync a group with LDAP.' desc 'Sync a group with LDAP.'
post ":id/ldap_sync" do post ":id/ldap_sync" do
not_found! unless Gitlab::LDAP::Config.group_sync_enabled? not_found! unless Gitlab::Auth::LDAP::Config.group_sync_enabled?
group = find_group!(params[:id]) group = find_group!(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
......
...@@ -57,7 +57,7 @@ module Bitbucket ...@@ -57,7 +57,7 @@ module Bitbucket
end end
def provider def provider
Gitlab::OAuth::Provider.config_for('bitbucket') Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end end
def options def options
......
...@@ -56,7 +56,7 @@ module Gitlab ...@@ -56,7 +56,7 @@ module Gitlab
# LDAP users are only authenticated via LDAP # LDAP users are only authenticated via LDAP
if user.nil? || user.ldap_user? if user.nil? || user.ldap_user?
# Second chance - try LDAP authentication # Second chance - try LDAP authentication
Gitlab::LDAP::Authentication.login(login, password) Gitlab::Auth::LDAP::Authentication.login(login, password)
elsif Gitlab::CurrentSettings.password_authentication_enabled_for_git? elsif Gitlab::CurrentSettings.password_authentication_enabled_for_git?
user if user.active? && user.valid_password?(password) user if user.active? && user.valid_password?(password)
end end
...@@ -87,7 +87,7 @@ module Gitlab ...@@ -87,7 +87,7 @@ module Gitlab
private private
def authenticate_using_internal_or_ldap_password? def authenticate_using_internal_or_ldap_password?
Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::LDAP::Config.enabled? Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::Auth::LDAP::Config.enabled?
end end
def service_request_check(login, password, project) def service_request_check(login, password, project)
......
# LDAP authorization model
#
# * Check if we are allowed access (not blocked)
#
module Gitlab
module Auth
module LDAP
class Access
attr_reader :provider, :user, :ldap_identity
def self.open(user, &block)
Gitlab::Auth::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
block.call(self.new(user, adapter))
end
end
def self.allowed?(user, options = {})
self.open(user) do |access|
# Whether user is allowed, or not, we should update
# permissions to keep things clean
if access.allowed?
access.update_user
Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
true
else
false
end
end
end
def initialize(user, adapter = nil)
@adapter = adapter
@user = user
@ldap_identity = user.ldap_identity
@provider = adapter&.provider || @ldap_identity&.provider
end
def allowed?
if ldap_user
unless ldap_config.active_directory
unblock_user(user, 'is available again') if user.ldap_blocked?
return true
end
# Block user in GitLab if he/she was blocked in AD
if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(ldap_identity.extern_uid, adapter)
block_user(user, 'is disabled in Active Directory')
false
else
unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
block_user(user, 'does not exist anymore')
false
end
end
def adapter
@adapter ||= Gitlab::Auth::LDAP::Adapter.new(provider)
end
def ldap_config
Gitlab::Auth::LDAP::Config.new(provider)
end
def find_ldap_user
return unless provider
found_user = Gitlab::Auth::LDAP::Person.find_by_dn(ldap_identity.extern_uid, adapter)
return found_user if found_user
if ldap_identity
Gitlab::Auth::LDAP::Person.find_by_email(user.email, adapter)
end
end
def ldap_user
@ldap_user ||= find_ldap_user
end
def block_user(user, reason)
user.ldap_block
if provider
Gitlab::AppLogger.info(
"LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \
"blocking Gitlab user \"#{user.name}\" (#{user.email})"
)
else
Gitlab::AppLogger.info(
"Account is not provided by LDAP, " \
"blocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
end
def unblock_user(user, reason)
user.activate
Gitlab::AppLogger.info(
"LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \
"unblocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
def update_user
update_email
update_memberships
update_identity
update_ssh_keys if sync_ssh_keys?
update_kerberos_identity if import_kerberos_identities?
end
# Update user ssh keys if they changed in LDAP
def update_ssh_keys
remove_old_ssh_keys
add_new_ssh_keys
end
# Add ssh keys that are in LDAP but not in GitLab
def add_new_ssh_keys
keys = ldap_user.ssh_keys - user.keys.ldap.pluck(:key)
keys.each do |key|
logger.info "#{self.class.name}: adding LDAP SSH key #{key.inspect} to #{user.name} (#{user.id})"
new_key = LDAPKey.new(title: "LDAP - #{ldap_config.sync_ssh_keys}", key: key)
new_key.user = user
unless new_key.save
logger.error "#{self.class.name}: failed to add LDAP SSH key #{key.inspect} to #{user.name} (#{user.id})\n"\
"error messages: #{new_key.errors.messages}"
end
end
end
# Remove ssh keys that do not exist in LDAP any more
def remove_old_ssh_keys
keys = user.keys.ldap.where.not(key: ldap_user.ssh_keys)
keys.each do |deleted_key|
logger.info "#{self.class.name}: removing LDAP SSH key #{deleted_key.key} from #{user.name} (#{user.id})"
unless deleted_key.destroy
logger.error "#{self.class.name}: failed to remove LDAP SSH key #{key.inspect} from #{user.name} (#{user.id})"
end
end
end
# Update user Kerberos identity with Kerberos principal name from Active Directory
def update_kerberos_identity
# there can be only one Kerberos identity in GitLab; if the user has a Kerberos identity in AD,
# replace any existing Kerberos identity for the user
return unless ldap_user.kerberos_principal.present?
kerberos_identity = user.identities.where(provider: :kerberos).first
return if kerberos_identity && kerberos_identity.extern_uid == ldap_user.kerberos_principal
kerberos_identity ||= Identity.new(provider: :kerberos, user: user)
kerberos_identity.extern_uid = ldap_user.kerberos_principal
unless kerberos_identity.save
Rails.logger.error "#{self.class.name}: failed to add Kerberos principal #{principal} to #{user.name} (#{user.id})\n"\
"error messages: #{new_identity.errors.messages}"
end
end
# Update user email if it changed in LDAP
def update_email
return false unless ldap_user.try(:email)
ldap_email = ldap_user.email.last.to_s.downcase
return false if user.email == ldap_email
Users::UpdateService.new(user, user: user, email: ldap_email).execute do |user|
user.skip_reconfirmation!
end
end
def update_identity
return if ldap_user.dn.empty? || ldap_user.dn == ldap_identity.extern_uid
unless ldap_identity.update(extern_uid: ldap_user.dn)
Rails.logger.error "Could not update DN for #{user.name} (#{user.id})\n"\
"error messages: #{user.ldap_identity.errors.messages}"
end
end
delegate :sync_ssh_keys?, to: :ldap_config
def import_kerberos_identities?
# Kerberos may be enabled for Git HTTP access and/or as an Omniauth provider
ldap_config.active_directory && (Gitlab.config.kerberos.enabled || AuthHelper.kerberos_enabled? )
end
def update_memberships
return if ldap_user.nil? || ldap_user.group_cns.empty?
group_ids = LdapGroupLink.where(cn: ldap_user.group_cns, provider: provider)
.distinct(:group_id)
.pluck(:group_id)
LdapGroupSyncWorker.perform_async(group_ids, provider) if group_ids.any?
end
private
def logger
Rails.logger
end
end
end
end
end
module Gitlab
module Auth
module LDAP
class Adapter
prepend ::EE::Gitlab::Auth::LDAP::Adapter
attr_reader :provider, :ldap
def self.open(provider, &block)
Net::LDAP.open(config(provider).adapter_options) do |ldap|
block.call(self.new(provider, ldap))
end
end
def self.config(provider)
Gitlab::Auth::LDAP::Config.new(provider)
end
def initialize(provider, ldap = nil)
@provider = provider
@ldap = ldap || Net::LDAP.new(config.adapter_options)
end
def config
Gitlab::Auth::LDAP::Config.new(provider)
end
def users(fields, value, limit = nil)
options = user_options(Array(fields), value, limit)
entries = ldap_search(options).select do |entry|
entry.respond_to? config.uid
end
entries.map do |entry|
Gitlab::Auth::LDAP::Person.new(entry, provider)
end
end
def user(*args)
users(*args).first
end
def dn_matches_filter?(dn, filter)
ldap_search(base: dn,
filter: filter,
scope: Net::LDAP::SearchScope_BaseObject,
attributes: %w{dn}).any?
end
def ldap_search(*args)
# Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
Timeout.timeout(config.timeout) do
results = ldap.search(*args)
if results.nil?
response = ldap.get_operation_result
unless response.code.zero?
Rails.logger.warn("LDAP search error: #{response.message}")
end
[]
else
results
end
end
rescue Net::LDAP::Error => error
Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
[]
rescue Timeout::Error
Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
[]
end
private
def user_options(fields, value, limit)
options = {
attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config),
base: config.base
}
options[:size] = limit if limit
if fields.include?('dn')
raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1
options[:base] = value
options[:scope] = Net::LDAP::SearchScope_BaseObject
else
filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|)
end
options.merge(filter: user_filter(filter))
end
def user_filter(filter = nil)
user_filter = config.constructed_user_filter if config.user_filter.present?
if user_filter && filter
Net::LDAP::Filter.join(filter, user_filter)
elsif user_filter
user_filter
else
filter
end
end
end
end
end
end
# Class to parse and transform the info provided by omniauth
#
module Gitlab
module Auth
module LDAP
class AuthHash < Gitlab::Auth::OAuth::AuthHash
def uid
@uid ||= Gitlab::Auth::LDAP::Person.normalize_dn(super)
end
def username
super.tap do |username|
username.downcase! if ldap_config.lowercase_usernames
end
end
private
def get_info(key)
attributes = ldap_config.attributes[key.to_s]
return super unless attributes
attributes = Array(attributes)
value = nil
attributes.each do |attribute|
value = get_raw(attribute)
value = value.first if value
break if value.present?
end
return super unless value
Gitlab::Utils.force_utf8(value)
value
end
def get_raw(key)
auth_hash.extra[:raw_info][key] if auth_hash.extra
end
def ldap_config
@ldap_config ||= Gitlab::Auth::LDAP::Config.new(self.provider)
end
end
end
end
end
# These calls help to authenticate to LDAP by providing username and password
#
# Since multiple LDAP servers are supported, it will loop through all of them
# until a valid bind is found
#
module Gitlab
module Auth
module LDAP
class Authentication
def self.login(login, password)
return unless Gitlab::Auth::LDAP::Config.enabled?
return unless login.present? && password.present?
auth = nil
# loop through providers until valid bind
providers.find do |provider|
auth = new(provider)
auth.login(login, password) # true will exit the loop
end
# If (login, password) was invalid for all providers, the value of auth is now the last
# Gitlab::Auth::LDAP::Authentication instance we tried.
auth.user
end
def self.providers
Gitlab::Auth::LDAP::Config.providers
end
attr_accessor :provider, :ldap_user
def initialize(provider)
@provider = provider
end
def login(login, password)
@ldap_user = adapter.bind_as(
filter: user_filter(login),
size: 1,
password: password
)
end
def adapter
OmniAuth::LDAP::Adaptor.new(config.omniauth_options)
end
def config
Gitlab::Auth::LDAP::Config.new(provider)
end
def user_filter(login)
filter = Net::LDAP::Filter.equals(config.uid, login)
# Apply LDAP user filter if present
if config.user_filter.present?
filter = Net::LDAP::Filter.join(filter, config.constructed_user_filter)
end
filter
end
def user
return nil unless ldap_user
Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
end
end
end
end
end
# Load a specific server configuration
module Gitlab
module Auth
module LDAP
class Config
include ::EE::Gitlab::Auth::LDAP::Config
NET_LDAP_ENCRYPTION_METHOD = {
simple_tls: :simple_tls,
start_tls: :start_tls,
plain: nil
}.freeze
attr_accessor :provider, :options
InvalidProvider = Class.new(StandardError)
def self.enabled?
Gitlab.config.ldap.enabled
end
def self.servers
Gitlab.config.ldap['servers']&.values || []
end
def self.available_servers
return [] unless enabled?
::License.feature_available?(:multiple_ldap_servers) ? servers : Array.wrap(servers.first)
end
def self.providers
servers.map { |server| server['provider_name'] }
end
def self.valid_provider?(provider)
providers.include?(provider)
end
def self.invalid_provider(provider)
raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}")
end
def initialize(provider)
if self.class.valid_provider?(provider)
@provider = provider
else
self.class.invalid_provider(provider)
end
@options = config_for(@provider) # Use @provider, not provider
end
def enabled?
base_config.enabled
end
def adapter_options
opts = base_options.merge(
encryption: encryption_options
)
opts.merge!(auth_options) if has_auth?
opts
end
def omniauth_options
opts = base_options.merge(
base: base,
encryption: options['encryption'],
filter: omniauth_user_filter,
name_proc: name_proc,
disable_verify_certificates: !options['verify_certificates']
)
if has_auth?
opts.merge!(
bind_dn: options['bind_dn'],
password: options['password']
)
end
opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
opts
end
def base
@base ||= Person.normalize_dn(options['base'])
end
def uid
options['uid']
end
def label
options['label']
end
def sync_ssh_keys?
sync_ssh_keys.present?
end
# The LDAP attribute in which the ssh keys are stored
def sync_ssh_keys
options['sync_ssh_keys']
end
def user_filter
options['user_filter']
end
def constructed_user_filter
@constructed_user_filter ||= Net::LDAP::Filter.construct(user_filter)
end
def group_base
options['group_base']
end
def admin_group
options['admin_group']
end
def active_directory
options['active_directory']
end
def block_auto_created_users
options['block_auto_created_users']
end
def attributes
default_attributes.merge(options['attributes'])
end
def timeout
options['timeout'].to_i
end
def external_groups
options['external_groups']
end
def has_auth?
options['password'] || options['bind_dn']
end
def allow_username_or_email_login
options['allow_username_or_email_login']
end
def lowercase_usernames
options['lowercase_usernames']
end
def name_proc
if allow_username_or_email_login
proc { |name| name.gsub(/@.*\z/, '') }
else
proc { |name| name }
end
end
def default_attributes
{
'username' => %w(uid sAMAccountName userid),
'email' => %w(mail email userPrincipalName),
'name' => 'cn',
'first_name' => 'givenName',
'last_name' => 'sn'
}
end
protected
def base_options
{
host: options['host'],
port: options['port']
}
end
def base_config
Gitlab.config.ldap
end
def config_for(provider)
base_config.servers.values.find { |server| server['provider_name'] == provider }
end
def encryption_options
method = translate_method(options['encryption'])
return nil unless method
{
method: method,
tls_options: tls_options(method)
}
end
def translate_method(method_from_config)
NET_LDAP_ENCRYPTION_METHOD[method_from_config.to_sym]
end
def tls_options(method)
return { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless method
opts = if options['verify_certificates']
OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
else
# It is important to explicitly set verify_mode for two reasons:
# 1. The behavior of OpenSSL is undefined when verify_mode is not set.
# 2. The net-ldap gem implementation verifies the certificate hostname
# unless verify_mode is set to VERIFY_NONE.
{ verify_mode: OpenSSL::SSL::VERIFY_NONE }
end
opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
opts
end
def auth_options
{
auth: {
method: :simple,
username: options['bind_dn'],
password: options['password']
}
}
end
def omniauth_user_filter
uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')
if user_filter.present?
Net::LDAP::Filter.join(uid_filter, constructed_user_filter).to_s
else
uid_filter.to_s
end
end
end
end
end
end
This diff is collapsed.
# Contains methods common to both GitLab CE and EE.
# All EE methods should be in `EE::Gitlab::Auth::LDAP::Person` only.
module Gitlab
module Auth
module LDAP
class Person
prepend ::EE::Gitlab::Auth::LDAP::Person
# Active Directory-specific LDAP filter that checks if bit 2 of the
# userAccountControl attribute is set.
# Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2")
InvalidEntryError = Class.new(StandardError)
attr_accessor :entry, :provider
def self.find_by_uid(uid, adapter)
uid = Net::LDAP::Filter.escape(uid)
adapter.user(adapter.config.uid, uid)
end
def self.find_by_dn(dn, adapter)
adapter.user('dn', dn)
end
def self.find_by_email(email, adapter)
email_fields = adapter.config.attributes['email']
adapter.user(email_fields, email)
end
def self.disabled_via_active_directory?(dn, adapter)
adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
end
def self.ldap_attributes(config)
[
'dn',
config.uid,
*config.attributes['name'],
*config.attributes['email'],
*config.attributes['username']
].compact.uniq
end
def self.normalize_dn(dn)
::Gitlab::Auth::LDAP::DN.new(dn).to_normalized_s
rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
dn
end
# Returns the UID in a normalized form.
#
# 1. Excess spaces are stripped
# 2. The string is downcased (for case-insensitivity)
def self.normalize_uid(uid)
::Gitlab::Auth::LDAP::DN.normalize_value(uid)
rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
uid
end
def initialize(entry, provider)
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@provider = provider
end
def name
attribute_value(:name).first
end
def uid
entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend
end
def username
username = attribute_value(:username)
# Depending on the attribute, multiple values may
# be returned. We need only one for username.
# Ex. `uid` returns only one value but `mail` may
# return an array of multiple email addresses.
[username].flatten.first.tap do |username|
username.downcase! if config.lowercase_usernames
end
end
def email
attribute_value(:email)
end
def dn
self.class.normalize_dn(entry.dn)
end
private
def entry
@entry
end
def config
@config ||= Gitlab::Auth::LDAP::Config.new(provider)
end
# Using the LDAP attributes configuration, find and return the first
# attribute with a value. For example, by default, when given 'email',
# this method looks for 'mail', 'email' and 'userPrincipalName' and
# returns the first with a value.
def attribute_value(attribute)
attributes = Array(config.attributes[attribute.to_s])
selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
return nil unless selected_attr
entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
end
# LDAP extension for User model
#
# * Find or create user from omniauth.auth data
# * Links LDAP account with existing user
# * Auth LDAP user with login and password
#
module Gitlab
module Auth
module LDAP
class User < Gitlab::Auth::OAuth::User
prepend ::EE::Gitlab::Auth::LDAP::User
class << self
def find_by_uid_and_provider(uid, provider)
identity = ::Identity.with_extern_uid(provider, uid).take
identity && identity.user
end
end
def save
super('LDAP')
end
# instance methods
def find_user
find_by_uid_and_provider || find_by_email || build_new_user
end
def find_by_uid_and_provider
self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end
def changed?
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
def block_after_signup?
ldap_config.block_auto_created_users
end
def allowed?
Gitlab::Auth::LDAP::Access.allowed?(gl_user)
end
def ldap_config
Gitlab::Auth::LDAP::Config.new(auth_hash.provider)
end
def auth_hash=(auth_hash)
@auth_hash = Gitlab::Auth::LDAP::AuthHash.new(auth_hash)
end
end
end
end
end
# Class to parse and transform the info provided by omniauth
#
module Gitlab
module Auth
module OAuth
class AuthHash
prepend ::EE::Gitlab::Auth::OAuth::AuthHash
attr_reader :auth_hash
def initialize(auth_hash)
@auth_hash = auth_hash
end
def uid
@uid ||= Gitlab::Utils.force_utf8(auth_hash.uid.to_s)
end
def provider
@provider ||= auth_hash.provider.to_s
end
def name
@name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}"
end
def username
@username ||= username_and_email[:username].to_s
end
def email
@email ||= username_and_email[:email].to_s
end
def password
@password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase)
end
def location
location = get_info(:address)
if location.is_a?(Hash)
[location.locality.presence, location.country.presence].compact.join(', ')
else
location
end
end
def has_attribute?(attribute)
if attribute == :location
get_info(:address).present?
else
get_info(attribute).present?
end
end
private
def info
auth_hash.info
end
def get_info(key)
value = info[key]
Gitlab::Utils.force_utf8(value) if value
value
end
def username_and_email
@username_and_email ||= begin
username = get_info(:username).presence || get_info(:nickname).presence
email = get_info(:email).presence
username ||= generate_username(email) if email
email ||= generate_temporarily_email(username) if username
{
username: username,
email: email
}
end
end
# Get the first part of the email address (before @)
# In addtion in removes illegal characters
def generate_username(email)
email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s
end
def generate_temporarily_email(username)
"temp-email-for-oauth-#{username}@gitlab.localhost"
end
end
end
end
end
module Gitlab
module Auth
module OAuth
class Provider
LABELS = {
"github" => "GitHub",
"gitlab" => "GitLab.com",
"google_oauth2" => "Google"
}.freeze
def self.providers
Devise.omniauth_providers
end
def self.enabled?(name)
providers.include?(name.to_sym)
end
def self.ldap_provider?(name)
name.to_s.start_with?('ldap')
end
def self.sync_profile_from_provider?(provider)
return true if ldap_provider?(provider)
providers = Gitlab.config.omniauth.sync_profile_from_provider
if providers.is_a?(Array)
providers.include?(provider)
else
providers
end
end
def self.config_for(name)
name = name.to_s
if ldap_provider?(name)
if Gitlab::Auth::LDAP::Config.valid_provider?(name)
Gitlab::Auth::LDAP::Config.new(name).options
else
nil
end
else
Gitlab.config.omniauth.providers.find { |provider| provider.name == name }
end
end
def self.label_for(name)
name = name.to_s
config = config_for(name)
(config && config['label']) || LABELS[name] || name.titleize
end
end
end
end
end
# :nocov:
module Gitlab
module Auth
module OAuth
module Session
def self.create(provider, ticket)
Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration)
end
def self.destroy(provider, ticket)
Rails.cache.delete("gitlab:#{provider}:#{ticket}")
end
def self.valid?(provider, ticket)
Rails.cache.read("gitlab:#{provider}:#{ticket}").present?
end
end
end
end
end
# :nocov:
# OAuth extension for User model
#
# * Find GitLab user based on omniauth uid and provider
# * Create new user from omniauth data
#
module Gitlab
module Auth
module OAuth
class User
prepend ::EE::Gitlab::Auth::OAuth::User
SignupDisabledError = Class.new(StandardError)
SigninDisabledForProviderError = Class.new(StandardError)
attr_accessor :auth_hash, :gl_user
def initialize(auth_hash)
self.auth_hash = auth_hash
update_profile
add_or_update_user_identities
end
def persisted?
gl_user.try(:persisted?)
end
def new?
!persisted?
end
def valid?
gl_user.try(:valid?)
end
def save(provider = 'OAuth')
raise SigninDisabledForProviderError if oauth_provider_disabled?
raise SignupDisabledError unless gl_user
block_after_save = needs_blocking?
Users::UpdateService.new(gl_user, user: gl_user).execute!
gl_user.block if block_after_save
log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
gl_user
rescue ActiveRecord::RecordInvalid => e
log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}"
return self, e.record.errors
end
def gl_user
return @gl_user if defined?(@gl_user)
@gl_user = find_user
end
def find_user
user = find_by_uid_and_provider
user ||= find_or_build_ldap_user if auto_link_ldap_user?
user ||= build_new_user if signup_enabled?
user.external = true if external_provider? && user&.new_record?
user
end
protected
def add_or_update_user_identities
return unless gl_user
# find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
identity ||= gl_user.identities.build(provider: auth_hash.provider)
identity.extern_uid = auth_hash.uid
if auto_link_ldap_user? && !gl_user.ldap_user? && ldap_person
log.info "Correct LDAP account has been found. identity to user: #{gl_user.username}."
gl_user.identities.build(provider: ldap_person.provider, extern_uid: ldap_person.dn)
end
end
def find_or_build_ldap_user
return unless ldap_person
user = Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
if user
log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity."
return user
end
log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
build_new_user
end
def find_by_email
return unless auth_hash.has_attribute?(:email)
::User.find_by(email: auth_hash.email.downcase)
end
def auto_link_ldap_user?
Gitlab.config.omniauth.auto_link_ldap_user
end
def creating_linked_ldap_user?
auto_link_ldap_user? && ldap_person
end
def ldap_person
return @ldap_person if defined?(@ldap_person)
# Look for a corresponding person with same uid in any of the configured LDAP providers
Gitlab::Auth::LDAP::Config.providers.each do |provider|
adapter = Gitlab::Auth::LDAP::Adapter.new(provider)
@ldap_person = find_ldap_person(auth_hash, adapter)
break if @ldap_person
end
@ldap_person
end
def find_ldap_person(auth_hash, adapter)
Gitlab::Auth::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
Gitlab::Auth::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
Gitlab::Auth::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
end
def ldap_config
Gitlab::Auth::LDAP::Config.new(ldap_person.provider) if ldap_person
end
def needs_blocking?
new? && block_after_signup?
end
def signup_enabled?
providers = Gitlab.config.omniauth.allow_single_sign_on
if providers.is_a?(Array)
providers.include?(auth_hash.provider)
else
providers
end
end
def external_provider?
Gitlab.config.omniauth.external_providers.include?(auth_hash.provider)
end
def block_after_signup?
if creating_linked_ldap_user?
ldap_config.block_auto_created_users
else
Gitlab.config.omniauth.block_auto_created_users
end
end
def auth_hash=(auth_hash)
@auth_hash = AuthHash.new(auth_hash)
end
def find_by_uid_and_provider
identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
identity && identity.user
end
def build_new_user
user_params = user_attributes.merge(skip_confirmation: true)
Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
end
def user_attributes
# Give preference to LDAP for sensitive information when creating a linked account
if creating_linked_ldap_user?
username = ldap_person.username.presence
email = ldap_person.email.first.presence
end
username ||= auth_hash.username
email ||= auth_hash.email
valid_username = ::Namespace.clean_path(username)
uniquify = Uniquify.new
valid_username = uniquify.string(valid_username) { |s| !NamespacePathValidator.valid_path?(s) }
name = auth_hash.name
name = valid_username if name.strip.empty?
{
name: name,
username: valid_username,
email: email,
password: auth_hash.password,
password_confirmation: auth_hash.password,
password_automatically_set: true
}
end
def sync_profile_from_provider?
Gitlab::Auth::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider)
end
def update_profile
clear_user_synced_attributes_metadata
return unless sync_profile_from_provider? || creating_linked_ldap_user?
metadata = gl_user.build_user_synced_attributes_metadata
if sync_profile_from_provider?
UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend
metadata.set_attribute_synced(key, true)
else
metadata.set_attribute_synced(key, false)
end
end
metadata.provider = auth_hash.provider
end
if creating_linked_ldap_user? && gl_user.email == ldap_person.email.first
metadata.set_attribute_synced(:email, true)
metadata.provider = ldap_person.provider
end
end
def clear_user_synced_attributes_metadata
gl_user&.user_synced_attributes_metadata&.destroy
end
def log
Gitlab::AppLogger
end
def oauth_provider_disabled?
Gitlab::CurrentSettings.current_application_settings
.disabled_oauth_sign_in_sources
.include?(auth_hash.provider)
end
end
end
end
end
module Gitlab
module Auth
module Saml
class AuthHash < Gitlab::Auth::OAuth::AuthHash
def groups
Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups))
end
private
def get_raw(key)
# Needs to call `all` because of https://git.io/vVo4u
# otherwise just the first value is returned
auth_hash.extra[:raw_info].all[key]
end
end
end
end
end
module Gitlab
module Auth
module Saml
class Config
class << self
def options
Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
end
def groups
options[:groups_attribute]
end
def external_groups
options[:external_groups]
end
def required_groups
Array(options[:required_groups])
end
def admin_groups
options[:admin_groups]
end
end
end
end
end
end
# SAML extension for User model
#
# * Find GitLab user based on SAML uid and provider
# * Create new user from SAML data
#
module Gitlab
module Auth
module Saml
class User < Gitlab::Auth::OAuth::User
def save
super('SAML')
end
def find_user
user = find_by_uid_and_provider
user ||= find_by_email if auto_link_saml_user?
user ||= find_or_build_ldap_user if auto_link_ldap_user?
user ||= build_new_user if signup_enabled?
if user_in_required_group?
unblock_user(user, "in required group") if user.persisted? && user.blocked?
elsif user.persisted?
block_user(user, "not in required group") unless user.blocked?
else
user = nil
end
if user
user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty? if external_users_enabled?
user.admin = !(auth_hash.groups & Gitlab::Auth::Saml::Config.admin_groups).empty? if admin_groups_enabled?
end
user
end
def changed?
return true unless gl_user
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
protected
def block_user(user, reason)
user.ldap_block
log_user_changes(user, "#{reason}, blocking")
end
def unblock_user(user, reason)
user.activate
log_user_changes(user, "#{reason}, unblocking")
end
def log_user_changes(user, message)
Gitlab::AppLogger.info(
"SAML(#{auth_hash.provider}) account \"#{auth_hash.uid}\" #{message} " \
"Gitlab user \"#{user.name}\" (#{user.email})"
)
end
def user_in_required_group?
required_groups = Gitlab::Auth::Saml::Config.required_groups
required_groups.empty? || !(auth_hash.groups & required_groups).empty?
end
def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user
end
def external_users_enabled?
!Gitlab::Auth::Saml::Config.external_groups.nil?
end
def auth_hash=(auth_hash)
@auth_hash = Gitlab::Auth::Saml::AuthHash.new(auth_hash)
end
def admin_groups_enabled?
!Gitlab::Auth::Saml::Config.admin_groups.nil?
end
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment