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:
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/tree.rb'
- 'lib/gitlab/git/wiki_page.rb'
- 'lib/gitlab/ldap/person.rb'
- 'lib/gitlab/o_auth/user.rb'
- 'lib/gitlab/auth/ldap/person.rb'
- 'lib/gitlab/auth/o_auth/user.rb'
# Offense count: 4
Lint/InterpolationCheck:
......@@ -812,7 +812,7 @@ Style/TrivialAccessors:
Exclude:
- 'app/models/external_issue.rb'
- 'app/serializers/base_serializer.rb'
- 'lib/gitlab/ldap/person.rb'
- 'lib/gitlab/auth/ldap/person.rb'
- 'lib/system_check/base_check.rb'
# Offense count: 4
......
......@@ -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
- Add filtered search to MR page. !1243
- 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: Remove deprecated fields Notes#upvotes and Notes#downvotes. !1275 (Robert Schilling)
- Deploy board backend. !1278
......
......@@ -199,7 +199,7 @@ class ApplicationController < ActionController::Base
return unless signed_in? && session[:service_tickets]
valid = session[:service_tickets].all? do |provider, ticket|
Gitlab::OAuth::Session.valid?(provider, ticket)
Gitlab::Auth::OAuth::Session.valid?(provider, ticket)
end
unless valid
......@@ -223,7 +223,7 @@ class ApplicationController < ActionController::Base
if current_user && current_user.requires_ldap_check?
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
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
......@@ -238,7 +238,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_ldap_access(&block)
Gitlab::LDAP::Access.open { |access| yield(access) }
Gitlab::Auth::LDAP::Access.open { |access| yield(access) }
end
# JSON for infinite scroll via Pager object
......@@ -292,7 +292,7 @@ class ApplicationController < ActionController::Base
end
def github_import_configured?
Gitlab::OAuth::Provider.enabled?(:github)
Gitlab::Auth::OAuth::Provider.enabled?(:github)
end
def gitlab_import_enabled?
......@@ -300,7 +300,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_import_configured?
Gitlab::OAuth::Provider.enabled?(:gitlab)
Gitlab::Auth::OAuth::Provider.enabled?(:gitlab)
end
def bitbucket_import_enabled?
......@@ -308,7 +308,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_configured?
Gitlab::OAuth::Provider.enabled?(:bitbucket)
Gitlab::Auth::OAuth::Provider.enabled?(:bitbucket)
end
def google_code_import_enabled?
......
......@@ -71,7 +71,7 @@ class Import::BitbucketController < Import::BaseController
end
def provider
Gitlab::OAuth::Provider.config_for('bitbucket')
Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end
def options
......
......@@ -11,8 +11,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
if Gitlab::LDAP::Config.enabled?
Gitlab::LDAP::Config.available_servers.each do |server|
if Gitlab::Auth::LDAP::Config.enabled?
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
define_method server['provider_name'] do
ldap
end
......@@ -32,7 +32,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# We only find ourselves here
# if the authentication to LDAP was successful.
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
@user = ldap_user.gl_user
......@@ -65,13 +65,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to after_sign_in_path_for(current_user)
end
else
saml_user = Gitlab::Saml::User.new(oauth)
saml_user = Gitlab::Auth::Saml::User.new(oauth)
saml_user.save if saml_user.changed?
@user = saml_user.gl_user
continue_login_process
end
rescue Gitlab::OAuth::SignupDisabledError
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end
......@@ -119,20 +119,20 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
oauth_user = Gitlab::OAuth::User.new(oauth)
oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
oauth_user.save
@user = oauth_user.gl_user
continue_login_process
end
rescue Gitlab::OAuth::SigninDisabledForProviderError
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
handle_disabled_provider
rescue Gitlab::OAuth::SignupDisabledError
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end
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][provider] = ticket
end
......@@ -155,7 +155,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
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."
if Gitlab::CurrentSettings.allow_signup?
......@@ -184,7 +184,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
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"
redirect_to new_user_session_path
......
......@@ -17,7 +17,7 @@ class SessionsController < Devise::SessionsController
def new
set_minimum_password_length
@ldap_servers = Gitlab::LDAP::Config.available_servers
@ldap_servers = Gitlab::Auth::LDAP::Config.available_servers
super
end
......
......@@ -78,7 +78,7 @@ module ApplicationSettingsHelper
label_tag(checkbox_name, class: css_class) do
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
......
......@@ -5,7 +5,7 @@ module AuthHelper
delegate :slack_app_id, to: :'Gitlab::CurrentSettings.current_application_settings'
def ldap_enabled?
Gitlab::LDAP::Config.enabled?
Gitlab::Auth::LDAP::Config.enabled?
end
def kerberos_enabled?
......@@ -21,11 +21,11 @@ module AuthHelper
end
def auth_providers
Gitlab::OAuth::Provider.providers
Gitlab::Auth::OAuth::Provider.providers
end
def label_for_provider(name)
Gitlab::OAuth::Provider.label_for(name)
Gitlab::Auth::OAuth::Provider.label_for(name)
end
def form_based_provider?(name)
......
......@@ -3,7 +3,7 @@ module ProfilesHelper
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
if user_synced_attributes_metadata&.synced?(attribute)
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
'LDAP'
end
......
......@@ -25,7 +25,7 @@ module SelectsHelper
def ldap_server_select_options
options_from_collection_for_select(
Gitlab::LDAP::Config.available_servers,
Gitlab::Auth::LDAP::Config.available_servers,
'provider_name',
'label'
)
......
......@@ -19,12 +19,12 @@ class Identity < ActiveRecord::Base
end
def ldap?
Gitlab::OAuth::Provider.ldap_provider?(provider)
Gitlab::Auth::OAuth::Provider.ldap_provider?(provider)
end
def self.normalize_uid(provider, uid)
if Gitlab::OAuth::Provider.ldap_provider?(provider)
Gitlab::LDAP::Person.normalize_dn(uid)
if Gitlab::Auth::OAuth::Provider.ldap_provider?(provider)
Gitlab::Auth::LDAP::Person.normalize_dn(uid)
else
uid.to_s
end
......
......@@ -746,7 +746,7 @@ class User < ActiveRecord::Base
def ldap_user?
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
identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
end
......
......@@ -26,6 +26,6 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base
private
def sync_profile_from_provider?
Gitlab::OAuth::Provider.sync_profile_from_provider?(provider)
Gitlab::Auth::OAuth::Provider.sync_profile_from_provider?(provider)
end
end
......@@ -203,7 +203,7 @@
Password authentication enabled for Git over HTTP(S)
.help-block
When disabled, a Personal Access Token
- if Gitlab::LDAP::Config.enabled?
- if Gitlab::Auth::LDAP::Config.enabled?
or LDAP password
must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
......
......@@ -41,7 +41,7 @@
= f.submit 'Save changes', class: "btn btn-save"
= 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
= render 'ldap_group_links/form', group: @group
= render 'ldap_group_links/ldap_group_links', group: @group
......@@ -62,7 +62,7 @@
= 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-heading Active synchronizations
%ul.well-list
......
......@@ -4,7 +4,7 @@
.form-group
= f.label :provider, class: 'control-label'
.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'
.form-group
= f.label :extern_uid, "Identifier", class: 'control-label'
......
%tr
%td
#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
#{Gitlab::Auth::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
%td
= identity.extern_uid
%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|
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
if Gitlab::LDAP::Config.enabled?
Gitlab::LDAP::Config.providers.each do |provider|
ldap_config = Gitlab::LDAP::Config.new(provider)
if Gitlab::Auth::LDAP::Config.enabled?
Gitlab::Auth::LDAP::Config.providers.each do |provider|
ldap_config = Gitlab::Auth::LDAP::Config.new(provider)
config.omniauth(provider, ldap_config.omniauth_options)
end
end
......@@ -235,9 +235,9 @@ Devise.setup do |config|
if provider['name'] == 'cas3'
provider['args'][:on_single_sign_out] = lambda do |request|
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
end
end
......@@ -245,8 +245,8 @@ Devise.setup do |config|
if provider['name'] == 'authentiq'
provider['args'][:remote_sign_out_handler] = lambda do |request|
authentiq_session = request.params['sid']
if Gitlab::OAuth::Session.valid?(:authentiq, authentiq_session)
Gitlab::OAuth::Session.destroy(:authentiq, authentiq_session)
if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session)
Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session)
true
else
false
......
if Gitlab::LDAP::Config.enabled?
if Gitlab::Auth::LDAP::Config.enabled?
module OmniAuth::Strategies
Gitlab::LDAP::Config.available_servers.each do |server|
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
# do not redeclare LDAP
next if server['provider_name'] == 'ldap'
......
......@@ -327,7 +327,7 @@ step of the sync.
1. Run a group sync for this particular group.
```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)
below for more information about the output.
......@@ -336,11 +336,11 @@ step of the sync.
run the following query:
```ruby
adapter = Gitlab::LDAP::Adapter.new('ldapmain') # If `main` is the LDAP provider
ldap_group = EE::Gitlab::LDAP::Group.find_by_cn('group_cn_here', adapter)
adapter = Gitlab::Auth::LDAP::Adapter.new('ldapmain') # If `main` is the LDAP provider
ldap_group = EE::Gitlab::Auth::LDAP::Group.find_by_cn('group_cn_here', adapter)
# 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.
One of the DNs here should match the 'Identifier' from the LDAP identity
......
......@@ -17,6 +17,6 @@ class Groups::LdapsController < Groups::ApplicationController
private
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
......@@ -29,14 +29,14 @@ class LdapGroupLink < ActiveRecord::Base
end
def config
Gitlab::LDAP::Config.new(provider)
rescue Gitlab::LDAP::Config::InvalidProvider
Gitlab::Auth::LDAP::Config.new(provider)
rescue Gitlab::Auth::LDAP::Config::InvalidProvider
nil
end
# default to the first LDAP server
def provider
read_attribute(:provider) || Gitlab::LDAP::Config.providers.first
read_attribute(:provider) || Gitlab::Auth::LDAP::Config.providers.first
end
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
= link_to group_ldap_group_links_path(@group), title: 'LDAP Group' do
%span
......
......@@ -3,10 +3,10 @@ class LdapAllGroupsSyncWorker
include CronjobQueue
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'
EE::Gitlab::LDAP::Sync::Groups.execute
EE::Gitlab::Auth::LDAP::Sync::Groups.execute
logger.info 'Finished LDAP group sync'
end
end
......@@ -2,12 +2,12 @@ class LdapGroupSyncWorker
include ApplicationWorker
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))
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)
end
else
......@@ -23,9 +23,9 @@ class LdapGroupSyncWorker
logger.info "Started LDAP group sync for group #{group.name} (#{group.id})"
if proxy
EE::Gitlab::LDAP::Sync::Group.execute(group, proxy)
EE::Gitlab::Auth::LDAP::Sync::Group.execute(group, proxy)
else
EE::Gitlab::LDAP::Sync::Group.execute_all_providers(group)
EE::Gitlab::Auth::LDAP::Sync::Group.execute_all_providers(group)
end
logger.info "Finished LDAP group sync for group #{group.name} (#{group.id})"
......
......@@ -3,14 +3,14 @@ class LdapSyncWorker
include CronjobQueue
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."
User.ldap.find_each(batch_size: 100).each do |ldap_user|
Rails.logger.debug "Syncing user #{ldap_user.username}, #{ldap_user.email}"
# Use the 'update_ldap_group_links_synchronously' option to avoid creating a ton
# 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
# rubocop:disable Rails/ReversibleMigration
class UpdateGroupLinks < ActiveRecord::Migration
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")
end
end
......@@ -6,7 +6,7 @@ module API
helpers do
def get_group_list(provider, 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
params :search_params do
......@@ -21,7 +21,7 @@ module API
use :search_params
end
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])
present groups, with: Entities::LdapGroup
end
......
......@@ -30,7 +30,7 @@ module Audit
when :remove
"Removed #{value}"
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
value
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"
namespace :gitlab 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?
provider = config.servers.first['provider_name']
......
require 'spec_helper'
describe EE::Gitlab::LDAP::AccessLevels do
describe EE::Gitlab::Auth::LDAP::AccessLevels do
describe '#set' do
let(:access_levels) { described_class.new }
let(:dns) do
......
require 'spec_helper'
describe EE::Gitlab::LDAP::Group do
describe EE::Gitlab::Auth::LDAP::Group do
include LdapHelpers
before do
......
require 'spec_helper'
describe EE::Gitlab::LDAP::Sync::AdminUsers do
describe EE::Gitlab::Auth::LDAP::Sync::AdminUsers do
include LdapHelpers
let(:adapter) { ldap_adapter }
......
require 'spec_helper'
describe EE::Gitlab::LDAP::Sync::ExternalUsers do
describe EE::Gitlab::Auth::LDAP::Sync::ExternalUsers do
include LdapHelpers
describe '#update_permissions' do
......
require 'spec_helper'
describe EE::Gitlab::LDAP::Sync::Group do
describe EE::Gitlab::Auth::LDAP::Sync::Group do
include LdapHelpers
let(:adapter) { ldap_adapter }
......@@ -69,7 +69,7 @@ describe EE::Gitlab::LDAP::Sync::Group do
adapter = ldap_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
let(:group) do
......@@ -303,7 +303,7 @@ describe EE::Gitlab::LDAP::Sync::Group do
it 'does not update permissions when group base is missing' do
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
end
......@@ -390,7 +390,7 @@ describe EE::Gitlab::LDAP::Sync::Group do
# Safe-check because some permissions are removed when `Group#ldap_synced?`
# is true (e.g. in `GroupPolicy`).
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
end
......
require 'spec_helper'
describe EE::Gitlab::LDAP::Sync::Groups do
describe EE::Gitlab::Auth::LDAP::Sync::Groups do
include LdapHelpers
let(:adapter) { ldap_adapter }
......@@ -8,9 +8,9 @@ describe EE::Gitlab::LDAP::Sync::Groups do
describe '#update_permissions' do
before do
allow(EE::Gitlab::LDAP::Sync::Group).to receive(:execute)
allow(EE::Gitlab::LDAP::Sync::AdminUsers).to receive(:execute)
allow(EE::Gitlab::LDAP::Sync::ExternalUsers).to receive(:execute)
allow(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute)
allow(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).to receive(:execute)
allow(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).to receive(:execute)
2.times { create(:group_with_ldap_group_link) }
end
......@@ -24,12 +24,12 @@ describe EE::Gitlab::LDAP::Sync::Groups do
stub_ldap_config(group_base: nil)
end
it 'does not call EE::Gitlab::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::AdminUsers).not_to receive(:execute)
it 'does not call EE::Gitlab::Auth::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).not_to receive(:execute)
end
it 'does not call EE::Gitlab::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::ExternalUsers).not_to receive(:execute)
it 'does not call EE::Gitlab::Auth::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).not_to receive(:execute)
end
end
......@@ -39,16 +39,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do
stub_ldap_config(group_base: 'dc=example,dc=com')
end
it 'calls EE::Gitlab::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute).twice
it 'calls EE::Gitlab::Auth::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute).twice
end
it 'does not call EE::Gitlab::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::AdminUsers).not_to receive(:execute)
it 'does not call EE::Gitlab::Auth::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).not_to receive(:execute)
end
it 'does not call EE::Gitlab::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::ExternalUsers).not_to receive(:execute)
it 'does not call EE::Gitlab::Auth::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).not_to receive(:execute)
end
end
......@@ -60,16 +60,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do
)
end
it 'calls EE::Gitlab::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute).twice
it 'calls EE::Gitlab::Auth::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute).twice
end
it 'does not call EE::Gitlab::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::AdminUsers).to receive(:execute).once
it 'does not call EE::Gitlab::Auth::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).to receive(:execute).once
end
it 'does not call EE::Gitlab::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::ExternalUsers).not_to receive(:execute)
it 'does not call EE::Gitlab::Auth::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).not_to receive(:execute)
end
end
......@@ -81,16 +81,16 @@ describe EE::Gitlab::LDAP::Sync::Groups do
)
end
it 'calls EE::Gitlab::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute).twice
it 'calls EE::Gitlab::Auth::LDAP::Sync::Group#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::Group).to receive(:execute).twice
end
it 'does not call EE::Gitlab::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::AdminUsers).not_to receive(:execute)
it 'does not call EE::Gitlab::Auth::LDAP::Sync::AdminUsers#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::AdminUsers).not_to receive(:execute)
end
it 'does not call EE::Gitlab::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::LDAP::Sync::ExternalUsers).to receive(:execute).once
it 'does not call EE::Gitlab::Auth::LDAP::Sync::ExternalUsers#execute' do
expect(EE::Gitlab::Auth::LDAP::Sync::ExternalUsers).to receive(:execute).once
end
end
end
......
require 'spec_helper'
require 'net/ldap/dn'
describe EE::Gitlab::LDAP::Sync::Proxy do
describe EE::Gitlab::Auth::LDAP::Sync::Proxy do
include LdapHelpers
let(:adapter) { ldap_adapter }
......@@ -56,7 +56,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do
sync_proxy.dns_for_group_cn('ldap_group1')
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')
end
......@@ -123,7 +123,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do
end
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')
end
......@@ -133,7 +133,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do
expect(sync_proxy).not_to receive(:member_uid_to_dn)
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')
end
......@@ -177,7 +177,7 @@ describe EE::Gitlab::LDAP::Sync::Proxy do
end
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
it 'retrieves the DN from the identity' do
......
require 'spec_helper'
describe EE::Gitlab::LDAP::UserFilter do
describe EE::Gitlab::Auth::LDAP::UserFilter do
include LdapHelpers
let(:auth_hash) do
......
require 'spec_helper'
describe Gitlab::LDAP::Adapter do
describe Gitlab::Auth::LDAP::Adapter do
include LdapHelpers
let(:adapter) { ldap_adapter('ldapmain') }
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
describe '#groups' do
......@@ -34,7 +34,7 @@ describe Gitlab::LDAP::Adapter do
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.member_dns).to match_array(%w(uid=john uid=mary))
end
......
require 'spec_helper'
describe Gitlab::LDAP::Person do
describe Gitlab::Auth::LDAP::Person do
include LdapHelpers
let(:entry) { ldap_user_entry('john.doe') }
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
describe '.ldap_attributes' do
......@@ -84,7 +84,7 @@ describe Gitlab::LDAP::Person do
)
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(:expected_attributes) { %w(dn cn uid mail memberof) }
......@@ -140,7 +140,7 @@ describe Gitlab::LDAP::Person do
subject { described_class.new(entry, 'ldapmain') }
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
context 'when the SSH key is literal' do
......
require 'spec_helper'
describe Gitlab::LDAP::User do
describe Gitlab::Auth::LDAP::User do
include LdapHelpers
let(:ldap_user) { described_class.new(auth_hash) }
......@@ -26,7 +26,7 @@ describe Gitlab::LDAP::User do
end
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
describe '#initialize' do
......
require 'spec_helper'
describe Gitlab::OAuth::AuthHash do
describe Gitlab::Auth::OAuth::AuthHash do
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
......
require 'spec_helper'
describe Gitlab::OAuth::User do
describe Gitlab::Auth::OAuth::User do
include LdapHelpers
describe 'login through kerberos with linkable LDAP user' do
......
......@@ -58,7 +58,7 @@ describe LdapGroupLink do
end
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
......
......@@ -14,8 +14,8 @@ describe API::Ldap do
OpenStruct.new(cn: 'students')
]
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter)
allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
allow(Gitlab::Auth::LDAP::Adapter).to receive(:new).and_return(adapter)
allow(adapter).to receive_messages(groups: groups)
end
......
module EE
module LdapHelpers
def proxy(adapter, provider = 'ldapmain')
EE::Gitlab::LDAP::Sync::Proxy.new(provider, adapter)
EE::Gitlab::Auth::LDAP::Sync::Proxy.new(provider, adapter)
end
# Stub an LDAP group search and provide the return entry. Specify `nil` for
# `entry` to simulate when an LDAP group is not found
#
# 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')
#
# stub_ldap_group_find_by_cn('ldap_group1', ldap_group1, adapter)
def stub_ldap_group_find_by_cn(cn, entry, adapter = nil)
if entry.present?
return_value = EE::Gitlab::LDAP::Group.new(entry, adapter)
return_value = EE::Gitlab::Auth::LDAP::Group.new(entry, adapter)
end
allow(EE::Gitlab::LDAP::Group)
allow(EE::Gitlab::Auth::LDAP::Group)
.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
# Create an LDAP group entry with any number of members. By default, creates
......@@ -94,7 +94,7 @@ module EE
# Stub Active Directory range member retrieval.
#
# 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(
# [user_dn('user1'), user_dn('user2'), user_dn('user3')],
# range_start: '0',
......@@ -105,7 +105,7 @@ module EE
# range_start: '3',
# 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')
def stub_ldap_adapter_group_members_in_range(
......@@ -118,7 +118,7 @@ module EE
end
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)
end
......
......@@ -5,13 +5,13 @@ describe LdapAllGroupsSyncWorker do
before do
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
describe '#perform' do
context 'with the default license key' 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
end
......@@ -23,7 +23,7 @@ describe LdapAllGroupsSyncWorker do
end
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
end
......
......@@ -7,14 +7,14 @@ describe LdapGroupSyncWorker do
def expect_fake_proxy(provider)
fake = double
expect(EE::Gitlab::LDAP::Sync::Proxy)
expect(EE::Gitlab::Auth::LDAP::Sync::Proxy)
.to receive(:open).with(provider).and_yield(fake)
fake
end
before do
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
describe '#perform' do
......@@ -63,16 +63,16 @@ describe LdapGroupSyncWorker do
describe '#sync_group' 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)
subject.sync_group(group, proxy: proxy)
end
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)
subject.sync_group(group)
......
......@@ -5,7 +5,7 @@ describe LdapSyncWorker do
before do
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')
end
......@@ -13,7 +13,7 @@ describe LdapSyncWorker do
describe '#perform' do
context 'with the default license key' do
it 'syncs all LDAP users' do
expect(Gitlab::LDAP::Access).to receive(:allowed?)
expect(Gitlab::Auth::LDAP::Access).to receive(:allowed?)
subject.perform
end
......@@ -25,7 +25,7 @@ describe LdapSyncWorker do
end
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
end
......
......@@ -275,7 +275,7 @@ module API
desc 'Sync a group with LDAP.'
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])
authorize! :admin_group, group
......
......@@ -57,7 +57,7 @@ module Bitbucket
end
def provider
Gitlab::OAuth::Provider.config_for('bitbucket')
Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end
def options
......
......@@ -56,7 +56,7 @@ module Gitlab
# LDAP users are only authenticated via LDAP
if user.nil? || user.ldap_user?
# 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?
user if user.active? && user.valid_password?(password)
end
......@@ -87,7 +87,7 @@ module Gitlab
private
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
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