Commit 77dcfee2 authored by Douwe Maan's avatar Douwe Maan

Move EE specific LDAP, OAuth and SAML classes under Gitlab::Auth

parent 07900bac
......@@ -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
......
......@@ -71,7 +71,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
continue_login_process
end
rescue Gitlab::Auth::OAuth::SignupDisabledError
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
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'
)
......
......@@ -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
......
......@@ -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
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::Auth::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
end
module EE
module Gitlab
module Auth
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::Auth::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
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
# 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
......
......@@ -32,7 +32,7 @@ feature 'Admin updates settings' do
describe 'LDAP settings' do
context 'with LDAP enabled' do
scenario 'Change allow group owners to manage ldap' do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
visit admin_application_settings_path
find('#application_setting_allow_group_owners_to_manage_ldap').set(false)
......
......@@ -13,7 +13,7 @@ feature 'Edit group settings' do
context 'with LDAP enabled' do
before do
allow_any_instance_of(Group).to receive(:ldap_synced?).and_return(true)
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
end
scenario 'is able to navigate to LDAP group section' do
......
......@@ -19,14 +19,14 @@ describe Gitlab::Auth::LDAP::Access do
describe '#find_ldap_user' do
it 'finds a user by dn first' do
expect(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
expect(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
access.find_ldap_user
end
it 'finds a user by email if the email came from LDAP' do
expect(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil)
expect(Gitlab::LDAP::Person).to receive(:find_by_email)
expect(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
expect(Gitlab::Auth::LDAP::Person).to receive(:find_by_email)
access.find_ldap_user
end
......@@ -196,7 +196,7 @@ describe Gitlab::Auth::LDAP::Access do
before do
allow(access).to(
receive_messages(
ldap_user: Gitlab::LDAP::Person.new(entry, user.ldap_identity.provider)
ldap_user: Gitlab::Auth::LDAP::Person.new(entry, user.ldap_identity.provider)
)
)
end
......@@ -240,18 +240,18 @@ describe Gitlab::Auth::LDAP::Access do
end
before do
allow(access).to receive_messages(ldap_user: Gitlab::LDAP::Person.new(entry, user.ldap_identity.provider))
allow(access).to receive_messages(ldap_user: Gitlab::Auth::LDAP::Person.new(entry, user.ldap_identity.provider))
end
it "adds a Kerberos identity if it is in Active Directory but not in GitLab" do
allow_any_instance_of(EE::Gitlab::LDAP::Person).to receive_messages(kerberos_principal: "mylogin@FOO.COM")
allow_any_instance_of(EE::Gitlab::Auth::LDAP::Person).to receive_messages(kerberos_principal: "mylogin@FOO.COM")
expect { access.update_kerberos_identity }.to change(user.identities.where(provider: :kerberos), :count).from(0).to(1)
expect(user.identities.where(provider: "kerberos").last.extern_uid).to eq("mylogin@FOO.COM")
end
it "updates existing Kerberos identity in GitLab if Active Directory has a different one" do
allow_any_instance_of(EE::Gitlab::LDAP::Person).to receive_messages(kerberos_principal: "otherlogin@BAR.COM")
allow_any_instance_of(EE::Gitlab::Auth::LDAP::Person).to receive_messages(kerberos_principal: "otherlogin@BAR.COM")
user.identities.build(provider: "kerberos", extern_uid: "mylogin@FOO.COM").save
expect { access.update_kerberos_identity }.not_to change(user.identities.where(provider: "kerberos"), :count)
......@@ -259,7 +259,7 @@ describe Gitlab::Auth::LDAP::Access do
end
it "does not remove Kerberos identities from GitLab if they are none in the LDAP provider" do
allow_any_instance_of(EE::Gitlab::LDAP::Person).to receive_messages(kerberos_principal: nil)
allow_any_instance_of(EE::Gitlab::Auth::LDAP::Person).to receive_messages(kerberos_principal: nil)
user.identities.build(provider: "kerberos", extern_uid: "otherlogin@BAR.COM").save
expect { access.update_kerberos_identity }.not_to change(user.identities.where(provider: "kerberos"), :count)
......@@ -267,7 +267,7 @@ describe Gitlab::Auth::LDAP::Access do
end
it "does not modify identities in GitLab if they are no kerberos principal in the LDAP provider" do
allow_any_instance_of(EE::Gitlab::LDAP::Person).to receive_messages(kerberos_principal: nil)
allow_any_instance_of(EE::Gitlab::Auth::LDAP::Person).to receive_messages(kerberos_principal: nil)
expect { access.update_kerberos_identity }.not_to change(user.identities, :count)
end
......@@ -281,18 +281,18 @@ describe Gitlab::Auth::LDAP::Access do
end
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)
allow(access).to receive_messages(sync_ssh_keys?: true)
end
it "adds a SSH key if it is in LDAP but not in gitlab" do
allow_any_instance_of(Gitlab::LDAP::Adapter).to receive(:user) { Gitlab::LDAP::Person.new(entry, 'ldapmain') }
allow_any_instance_of(Gitlab::Auth::LDAP::Adapter).to receive(:user) { Gitlab::Auth::LDAP::Person.new(entry, 'ldapmain') }
expect { access.update_ssh_keys }.to change(user.keys, :count).from(0).to(1)
end
it "adds a SSH key and give it a proper name" do
allow_any_instance_of(Gitlab::LDAP::Adapter).to receive(:user) { Gitlab::LDAP::Person.new(entry, 'ldapmain') }
allow_any_instance_of(Gitlab::Auth::LDAP::Adapter).to receive(:user) { Gitlab::Auth::LDAP::Person.new(entry, 'ldapmain') }
access.update_ssh_keys
expect(user.keys.last.title).to match(/LDAP/)
......@@ -301,7 +301,7 @@ describe Gitlab::Auth::LDAP::Access do
it "does not add a SSH key if it is invalid" do
entry = Net::LDAP::Entry.from_single_ldif_string("dn: cn=foo, dc=bar, dc=com\n#{ssh_key_attribute_name}: I am not a valid key")
allow_any_instance_of(Gitlab::LDAP::Adapter).to receive(:user) { Gitlab::LDAP::Person.new(entry, 'ldapmain') }
allow_any_instance_of(Gitlab::Auth::LDAP::Adapter).to receive(:user) { Gitlab::Auth::LDAP::Person.new(entry, 'ldapmain') }
expect { access.update_ssh_keys }.not_to change(user.keys, :count)
end
......@@ -313,14 +313,14 @@ describe Gitlab::Auth::LDAP::Access do
it "removes a SSH key if it is no longer in LDAP" do
entry = Net::LDAP::Entry.from_single_ldif_string("dn: cn=foo, dc=bar, dc=com\n#{ssh_key_attribute_name}:\n")
allow_any_instance_of(Gitlab::LDAP::Adapter).to receive(:user) { Gitlab::LDAP::Person.new(entry, 'ldapmain') }
allow_any_instance_of(Gitlab::Auth::LDAP::Adapter).to receive(:user) { Gitlab::Auth::LDAP::Person.new(entry, 'ldapmain') }
expect { access.update_ssh_keys }.to change(user.keys, :count).from(1).to(0)
end
it "removes a SSH key if the ldap attribute was removed" do
entry = Net::LDAP::Entry.from_single_ldif_string("dn: cn=foo, dc=bar, dc=com")
allow_any_instance_of(Gitlab::LDAP::Adapter).to receive(:user) { Gitlab::LDAP::Person.new(entry, 'ldapmain') }
allow_any_instance_of(Gitlab::Auth::LDAP::Adapter).to receive(:user) { Gitlab::Auth::LDAP::Person.new(entry, 'ldapmain') }
expect { access.update_ssh_keys }.to change(user.keys, :count).from(1).to(0)
end
......@@ -331,7 +331,7 @@ describe Gitlab::Auth::LDAP::Access do
let(:entry) { Net::LDAP::Entry.new }
before do
allow(access).to receive_messages(ldap_user: Gitlab::LDAP::Person.new(entry, user.ldap_identity.provider))
allow(access).to receive_messages(ldap_user: Gitlab::Auth::LDAP::Person.new(entry, user.ldap_identity.provider))
end
it "does not update email if email attribute is not set" do
......@@ -361,7 +361,7 @@ describe Gitlab::Auth::LDAP::Access do
let(:person_with_memberof) do
entry['memberof'] = ['CN=Group1,CN=Users,DC=The dc,DC=com',
'CN=Group2,CN=Builtin,DC=The dc,DC=com']
Gitlab::LDAP::Person.new(entry, provider)
Gitlab::Auth::LDAP::Person.new(entry, provider)
end
it 'triggers a sync for all groups found in `memberof`' do
......@@ -379,7 +379,7 @@ describe Gitlab::Auth::LDAP::Access do
it "doesn't continue when there is no `memberOf` param" do
allow(access).to receive(:ldap_user)
.and_return(Gitlab::LDAP::Person.new(entry, provider))
.and_return(Gitlab::Auth::LDAP::Person.new(entry, provider))
expect(LdapGroupLink).not_to receive(:where)
expect(LdapGroupSyncWorker).not_to receive(:perform_async)
......@@ -404,7 +404,7 @@ describe Gitlab::Auth::LDAP::Access do
it 'updates the external UID if it changed in the entry' do
entry = ldap_user_entry('another uid')
provider = user.ldap_identity.provider
person = Gitlab::LDAP::Person.new(entry, provider)
person = Gitlab::Auth::LDAP::Person.new(entry, provider)
allow(access).to receive(:ldap_user).and_return(person)
......
......@@ -23,7 +23,7 @@ describe Gitlab::Auth::LDAP::Config do
end
it 'raises an error if a unknown provider is used' do
expect { described_class.new 'unknown' }.to raise_error(Gitlab::LDAP::Config::InvalidProvider)
expect { described_class.new 'unknown' }.to raise_error(Gitlab::Auth::LDAP::Config::InvalidProvider)
end
end
......
......@@ -180,15 +180,7 @@ describe Gitlab::Auth::LDAP::User do
describe 'blocking' do
def configure_block(value)
<<<<<<< HEAD:spec/lib/gitlab/ldap/user_spec.rb
stub_ldap_config(block_auto_created_users: value)
||||||| parent of 50a70efd118... Moved o_auth/saml/ldap modules under gitlab/auth
allow_any_instance_of(Gitlab::LDAP::Config)
.to receive(:block_auto_created_users).and_return(value)
=======
allow_any_instance_of(Gitlab::Auth::LDAP::Config)
.to receive(:block_auto_created_users).and_return(value)
>>>>>>> 50a70efd118... Moved o_auth/saml/ldap modules under gitlab/auth:spec/lib/gitlab/auth/ldap/user_spec.rb
end
context 'signup' do
......
......@@ -25,23 +25,23 @@ describe Gitlab::Auth::Saml::User do
end
def stub_ldap_config(messages)
allow(Gitlab::LDAP::Config).to receive_messages(messages)
allow(Gitlab::Auth::LDAP::Config).to receive_messages(messages)
end
def stub_basic_saml_config
allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
end
def stub_saml_group_config(groups)
allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
end
def stub_saml_required_group_config(groups)
allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', required_groups: groups, args: {} } })
allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', required_groups: groups, args: {} } })
end
def stub_saml_admin_group_config(groups)
allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', admin_groups: groups, args: {} } })
allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', admin_groups: groups, args: {} } })
end
before do
......@@ -216,7 +216,7 @@ describe Gitlab::Auth::Saml::User do
it 'does not allow non-members' do
stub_saml_required_group_config(%w(ArchitectureAstronauts))
expect { saml_user.save }.to raise_error Gitlab::OAuth::SignupDisabledError
expect { saml_user.save }.to raise_error Gitlab::Auth::OAuth::User::SignupDisabledError
end
it 'blocks non-members' do
......
......@@ -938,7 +938,7 @@ describe API::Groups do
describe 'POST /groups/:id/ldap_sync' do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
end
context 'when the ldap_group_sync feature is available' do
......
......@@ -7,7 +7,7 @@ module LdapHelpers
def fake_ldap_sync_proxy(provider)
fake_proxy = double(:proxy, adapter: ldap_adapter)
allow(::EE::Gitlab::LDAP::Sync::Proxy).to receive(:open).with(provider).and_yield(fake_proxy)
allow(::EE::Gitlab::Auth::LDAP::Sync::Proxy).to receive(:open).with(provider).and_yield(fake_proxy)
fake_proxy
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment