summaryrefslogtreecommitdiff
path: root/lib/gitlab/ldap/person.rb
blob: b91757c2a4b9d4053939eb3b9907defb8de8158a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
module Gitlab
  module LDAP
    class 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::LDAP::DN.new(dn).to_normalized_s
      rescue ::Gitlab::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::LDAP::DN.normalize_value(uid)
      rescue ::Gitlab::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

        validate_entry
      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::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

      def validate_entry
        allowed_attrs = self.class.ldap_attributes(config).map(&:downcase)

        # Net::LDAP::Entry transforms keys to symbols. Change to strings to compare.
        entry_attrs = entry.attribute_names.map { |n| n.to_s.downcase }
        invalid_attrs = entry_attrs - allowed_attrs

        if invalid_attrs.any?
          raise InvalidEntryError,
                "#{self.class.name} initialized with Net::LDAP::Entry containing invalid attributes(s): #{invalid_attrs}"
        end
      end
    end
  end
end