summaryrefslogtreecommitdiff
path: root/lib/gitlab/o_auth/user.rb
blob: a3e1c66c19f2c914977c268ea1595c82acf29381 (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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# OAuth extension for User model
#
# * Find GitLab user based on omniauth uid and provider
# * Create new user from omniauth data
#
module Gitlab
  module OAuth
    class User
      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::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::LDAP::Config.providers.each do |provider|
          adapter = Gitlab::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::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
          Gitlab::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
          Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
      end

      def ldap_config
        Gitlab::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::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider)
      end

      def update_profile
        return unless sync_profile_from_provider? || creating_linked_ldap_user?

        metadata = gl_user.user_synced_attributes_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 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