Commit 767f7d9ae7eb29d5feaa65863a8948ca60d3825f

Authored by Dmitriy Zaporozhets
2 parents 92a92846 c6d39a14

Merge branch 'ldap-code' into 'master'

LDAP code from EE
app/controllers/application_controller.rb
... ... @@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
6 6 before_filter :check_password_expiration
7 7 around_filter :set_current_user_for_thread
8 8 before_filter :add_abilities
  9 + before_filter :ldap_security_check
9 10 before_filter :dev_tools if Rails.env == 'development'
10 11 before_filter :default_headers
11 12 before_filter :add_gon_variables
... ... @@ -179,11 +180,28 @@ class ApplicationController < ActionController::Base
179 180 end
180 181 end
181 182  
  183 + def ldap_security_check
  184 + if current_user && current_user.requires_ldap_check?
  185 + if gitlab_ldap_access.allowed?(current_user)
  186 + current_user.last_credential_check_at = Time.now
  187 + current_user.save
  188 + else
  189 + sign_out current_user
  190 + flash[:alert] = "Access denied for your LDAP account."
  191 + redirect_to new_user_session_path
  192 + end
  193 + end
  194 + end
  195 +
182 196 def event_filter
183 197 filters = cookies['event_filter'].split(',') if cookies['event_filter'].present?
184 198 @event_filter ||= EventFilter.new(filters)
185 199 end
186 200  
  201 + def gitlab_ldap_access
  202 + Gitlab::LDAP::Access.new
  203 + end
  204 +
187 205 # JSON for infinite scroll via Pager object
188 206 def pager_json(partial, count)
189 207 html = render_to_string(
... ...
app/models/user.rb
... ... @@ -185,7 +185,7 @@ class User < ActiveRecord::Base
185 185 where(conditions).first
186 186 end
187 187 end
188   -
  188 +
189 189 def find_for_commit(email, name)
190 190 # Prefer email match over name match
191 191 User.where(email: email).first ||
... ... @@ -275,7 +275,9 @@ class User < ActiveRecord::Base
275 275 # Projects user has access to
276 276 def authorized_projects
277 277 @authorized_projects ||= begin
278   - project_ids = (personal_projects.pluck(:id) + groups_projects.pluck(:id) + projects.pluck(:id)).uniq
  278 + project_ids = personal_projects.pluck(:id)
  279 + project_ids += groups_projects.pluck(:id)
  280 + project_ids += projects.pluck(:id).uniq
279 281 Project.where(id: project_ids).joins(:namespace).order('namespaces.name ASC')
280 282 end
281 283 end
... ... @@ -406,6 +408,14 @@ class User < ActiveRecord::Base
406 408 end
407 409 end
408 410  
  411 + def requires_ldap_check?
  412 + if ldap_user?
  413 + !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
  414 + else
  415 + false
  416 + end
  417 + end
  418 +
409 419 def solo_owned_groups
410 420 @solo_owned_groups ||= owned_groups.select do |group|
411 421 group.owners == [self]
... ...
config/gitlab.yml.example
... ... @@ -121,7 +121,6 @@ production: &amp;base
121 121 ldap:
122 122 enabled: false
123 123 host: '_your_ldap_server'
124   - base: '_the_base_where_you_search_for_users'
125 124 port: 636
126 125 uid: 'sAMAccountName'
127 126 method: 'ssl' # "tls" or "ssl" or "plain"
... ... @@ -138,6 +137,20 @@ production: &amp;base
138 137 # disable this setting, because the userPrincipalName contains an '@'.
139 138 allow_username_or_email_login: true
140 139  
  140 + # Base where we can search for users
  141 + #
  142 + # Ex. ou=People,dc=gitlab,dc=example
  143 + #
  144 + base: ''
  145 +
  146 + # Filter LDAP users
  147 + #
  148 + # Format: RFC 4515
  149 + # Ex. (employeeType=developer)
  150 + #
  151 + user_filter: ''
  152 +
  153 +
141 154 ## OmniAuth settings
142 155 omniauth:
143 156 # Allow login via Twitter, Google, etc. using OmniAuth providers
... ...
db/migrate/20130809124851_add_permission_check_to_user.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class AddPermissionCheckToUser < ActiveRecord::Migration
  2 + def change
  3 + add_column :users, :last_credential_check_at, :datetime
  4 + end
  5 +end
... ...
db/schema.rb
... ... @@ -76,8 +76,8 @@ ActiveRecord::Schema.define(version: 20140304005354) do
76 76 t.integer "assignee_id"
77 77 t.integer "author_id"
78 78 t.integer "project_id"
79   - t.datetime "created_at", null: false
80   - t.datetime "updated_at", null: false
  79 + t.datetime "created_at"
  80 + t.datetime "updated_at"
81 81 t.integer "position", default: 0
82 82 t.string "branch_name"
83 83 t.text "description"
... ... @@ -95,8 +95,8 @@ ActiveRecord::Schema.define(version: 20140304005354) do
95 95  
96 96 create_table "keys", force: true do |t|
97 97 t.integer "user_id"
98   - t.datetime "created_at", null: false
99   - t.datetime "updated_at", null: false
  98 + t.datetime "created_at"
  99 + t.datetime "updated_at"
100 100 t.text "key"
101 101 t.string "title"
102 102 t.string "type"
... ... @@ -123,8 +123,8 @@ ActiveRecord::Schema.define(version: 20140304005354) do
123 123 t.integer "author_id"
124 124 t.integer "assignee_id"
125 125 t.string "title"
126   - t.datetime "created_at", null: false
127   - t.datetime "updated_at", null: false
  126 + t.datetime "created_at"
  127 + t.datetime "updated_at"
128 128 t.integer "milestone_id"
129 129 t.string "state"
130 130 t.string "merge_status"
... ... @@ -176,8 +176,8 @@ ActiveRecord::Schema.define(version: 20140304005354) do
176 176 t.text "note"
177 177 t.string "noteable_type"
178 178 t.integer "author_id"
179   - t.datetime "created_at", null: false
180   - t.datetime "updated_at", null: false
  179 + t.datetime "created_at"
  180 + t.datetime "updated_at"
181 181 t.integer "project_id"
182 182 t.string "attachment"
183 183 t.string "line_code"
... ... @@ -199,8 +199,8 @@ ActiveRecord::Schema.define(version: 20140304005354) do
199 199 t.string "name"
200 200 t.string "path"
201 201 t.text "description"
202   - t.datetime "created_at", null: false
203   - t.datetime "updated_at", null: false
  202 + t.datetime "created_at"
  203 + t.datetime "updated_at"
204 204 t.integer "creator_id"
205 205 t.boolean "issues_enabled", default: true, null: false
206 206 t.boolean "wall_enabled", default: true, null: false
... ... @@ -252,8 +252,8 @@ ActiveRecord::Schema.define(version: 20140304005354) do
252 252 t.text "content", limit: 2147483647
253 253 t.integer "author_id", null: false
254 254 t.integer "project_id"
255   - t.datetime "created_at", null: false
256   - t.datetime "updated_at", null: false
  255 + t.datetime "created_at"
  256 + t.datetime "updated_at"
257 257 t.string "file_name"
258 258 t.datetime "expires_at"
259 259 t.boolean "private", default: true, null: false
... ... @@ -275,45 +275,42 @@ ActiveRecord::Schema.define(version: 20140304005354) do
275 275 t.datetime "created_at"
276 276 end
277 277  
278   - add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
279   - add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
280   -
281 278 create_table "tags", force: true do |t|
282 279 t.string "name"
283 280 end
284 281  
285 282 create_table "users", force: true do |t|
286   - t.string "email", default: "", null: false
287   - t.string "encrypted_password", default: "", null: false
  283 + t.string "email", default: "", null: false
  284 + t.string "encrypted_password", limit: 128, default: "", null: false
288 285 t.string "reset_password_token"
289 286 t.datetime "reset_password_sent_at"
290 287 t.datetime "remember_created_at"
291   - t.integer "sign_in_count", default: 0
  288 + t.integer "sign_in_count", default: 0
292 289 t.datetime "current_sign_in_at"
293 290 t.datetime "last_sign_in_at"
294 291 t.string "current_sign_in_ip"
295 292 t.string "last_sign_in_ip"
296   - t.datetime "created_at", null: false
297   - t.datetime "updated_at", null: false
  293 + t.datetime "created_at"
  294 + t.datetime "updated_at"
298 295 t.string "name"
299   - t.boolean "admin", default: false, null: false
300   - t.integer "projects_limit", default: 10
301   - t.string "skype", default: "", null: false
302   - t.string "linkedin", default: "", null: false
303   - t.string "twitter", default: "", null: false
  296 + t.boolean "admin", default: false, null: false
  297 + t.integer "projects_limit", default: 10
  298 + t.string "skype", default: "", null: false
  299 + t.string "linkedin", default: "", null: false
  300 + t.string "twitter", default: "", null: false
304 301 t.string "authentication_token"
305   - t.integer "theme_id", default: 1, null: false
  302 + t.integer "theme_id", default: 1, null: false
306 303 t.string "bio"
307   - t.integer "failed_attempts", default: 0
  304 + t.integer "failed_attempts", default: 0
308 305 t.datetime "locked_at"
309 306 t.string "extern_uid"
310 307 t.string "provider"
311 308 t.string "username"
312   - t.boolean "can_create_group", default: true, null: false
313   - t.boolean "can_create_team", default: true, null: false
  309 + t.boolean "can_create_group", default: true, null: false
  310 + t.boolean "can_create_team", default: true, null: false
314 311 t.string "state"
315   - t.integer "color_scheme_id", default: 1, null: false
316   - t.integer "notification_level", default: 1, null: false
  312 + t.integer "color_scheme_id", default: 1, null: false
  313 + t.integer "notification_level", default: 1, null: false
317 314 t.datetime "password_expires_at"
318 315 t.integer "created_by_id"
319 316 t.string "avatar"
... ... @@ -321,15 +318,15 @@ ActiveRecord::Schema.define(version: 20140304005354) do
321 318 t.datetime "confirmed_at"
322 319 t.datetime "confirmation_sent_at"
323 320 t.string "unconfirmed_email"
324   - t.boolean "hide_no_ssh_key", default: false
325   - t.string "website_url", default: "", null: false
  321 + t.boolean "hide_no_ssh_key", default: false
  322 + t.string "website_url", default: "", null: false
  323 + t.datetime "last_credential_check_at"
326 324 end
327 325  
328 326 add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
329 327 add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
330 328 add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
331 329 add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
332   - add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree
333 330 add_index "users", ["name"], name: "index_users_on_name", using: :btree
334 331 add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
335 332 add_index "users", ["username"], name: "index_users_on_username", using: :btree
... ... @@ -348,8 +345,8 @@ ActiveRecord::Schema.define(version: 20140304005354) do
348 345 create_table "users_projects", force: true do |t|
349 346 t.integer "user_id", null: false
350 347 t.integer "project_id", null: false
351   - t.datetime "created_at", null: false
352   - t.datetime "updated_at", null: false
  348 + t.datetime "created_at"
  349 + t.datetime "updated_at"
353 350 t.integer "project_access", default: 0, null: false
354 351 t.integer "notification_level", default: 3, null: false
355 352 end
... ... @@ -361,8 +358,8 @@ ActiveRecord::Schema.define(version: 20140304005354) do
361 358 create_table "web_hooks", force: true do |t|
362 359 t.string "url"
363 360 t.integer "project_id"
364   - t.datetime "created_at", null: false
365   - t.datetime "updated_at", null: false
  361 + t.datetime "created_at"
  362 + t.datetime "updated_at"
366 363 t.string "type", default: "ProjectHook"
367 364 t.integer "service_id"
368 365 t.boolean "push_events", default: true, null: false
... ...
lib/api/internal.rb
... ... @@ -35,8 +35,14 @@ module API
35 35 user = key.user
36 36  
37 37 return false if user.blocked?
  38 +
38 39 if Gitlab.config.ldap.enabled
39   - return false if user.ldap_user? && Gitlab::LDAP::User.blocked?(user.extern_uid)
  40 + if user.ldap_user?
  41 + # Check if LDAP user exists and match LDAP user_filter
  42 + unless Gitlab::LDAP::Access.new.allowed?(user)
  43 + return false
  44 + end
  45 + end
40 46 end
41 47  
42 48 action = case git_cmd
... ...
lib/gitlab/ldap/access.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +module Gitlab
  2 + module LDAP
  3 + class Access
  4 + def allowed?(user)
  5 + !!Gitlab::LDAP::Person.find_by_dn(user.extern_uid)
  6 + rescue
  7 + false
  8 + end
  9 + end
  10 + end
  11 +end
... ...
lib/gitlab/ldap/adapter.rb 0 → 100644
... ... @@ -0,0 +1,72 @@
  1 +module Gitlab
  2 + module LDAP
  3 + class Adapter
  4 + attr_reader :ldap
  5 +
  6 + def initialize
  7 + encryption = config['method'].to_s == 'ssl' ? :simple_tls : nil
  8 +
  9 + options = {
  10 + host: config['host'],
  11 + port: config['port'],
  12 + encryption: encryption
  13 + }
  14 +
  15 + auth_options = {
  16 + auth: {
  17 + method: :simple,
  18 + username: config['bind_dn'],
  19 + password: config['password']
  20 + }
  21 + }
  22 +
  23 + if config['password'] || config['bind_dn']
  24 + options.merge!(auth_options)
  25 + end
  26 +
  27 + @ldap = Net::LDAP.new(options)
  28 + end
  29 +
  30 + def users(field, value)
  31 + if field.to_sym == :dn
  32 + options = {
  33 + base: value
  34 + }
  35 + else
  36 + options = {
  37 + base: config['base'],
  38 + filter: Net::LDAP::Filter.eq(field, value)
  39 + }
  40 + end
  41 +
  42 + if config['user_filter'].present?
  43 + user_filter = Net::LDAP::Filter.construct(config['user_filter'])
  44 +
  45 + options[:filter] = if options[:filter]
  46 + Net::LDAP::Filter.join(options[:filter], user_filter)
  47 + else
  48 + user_filter
  49 + end
  50 + end
  51 +
  52 + entries = ldap.search(options).select do |entry|
  53 + entry.respond_to? config.uid
  54 + end
  55 +
  56 + entries.map do |entry|
  57 + Gitlab::LDAP::Person.new(entry)
  58 + end
  59 + end
  60 +
  61 + def user(*args)
  62 + users(*args).first
  63 + end
  64 +
  65 + private
  66 +
  67 + def config
  68 + @config ||= Gitlab.config.ldap
  69 + end
  70 + end
  71 + end
  72 +end
... ...
lib/gitlab/ldap/person.rb 0 → 100644
... ... @@ -0,0 +1,48 @@
  1 +module Gitlab
  2 + module LDAP
  3 + class Person
  4 + def self.find_by_uid(uid)
  5 + Gitlab::LDAP::Adapter.new.user(config.uid, uid)
  6 + end
  7 +
  8 + def self.find_by_dn(dn)
  9 + Gitlab::LDAP::Adapter.new.user('dn', dn)
  10 + end
  11 +
  12 + def initialize(entry)
  13 + Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
  14 + @entry = entry
  15 + end
  16 +
  17 + def name
  18 + entry.cn.first
  19 + end
  20 +
  21 + def uid
  22 + entry.send(config.uid).first
  23 + end
  24 +
  25 + def username
  26 + uid
  27 + end
  28 +
  29 + def dn
  30 + entry.dn
  31 + end
  32 +
  33 + private
  34 +
  35 + def entry
  36 + @entry
  37 + end
  38 +
  39 + def adapter
  40 + @adapter ||= Gitlab::LDAP::Adapter.new
  41 + end
  42 +
  43 + def config
  44 + @config ||= Gitlab.config.ldap
  45 + end
  46 + end
  47 + end
  48 +end
... ...
lib/gitlab/ldap/user.rb
... ... @@ -13,8 +13,8 @@ module Gitlab
13 13 def find_or_create(auth)
14 14 @auth = auth
15 15  
16   - if uid.blank? || email.blank?
17   - raise_error("Account must provide an uid and email address")
  16 + if uid.blank? || email.blank? || username.blank?
  17 + raise_error("Account must provide a dn, uid and email address")
18 18 end
19 19  
20 20 user = find(auth)
... ... @@ -62,8 +62,16 @@ module Gitlab
62 62 return nil unless ldap_conf.enabled && login.present? && password.present?
63 63  
64 64 ldap = OmniAuth::LDAP::Adaptor.new(ldap_conf)
  65 + filter = Net::LDAP::Filter.eq(ldap.uid, login)
  66 +
  67 + # Apply LDAP user filter if present
  68 + if ldap_conf['user_filter'].present?
  69 + user_filter = Net::LDAP::Filter.construct(ldap_conf['user_filter'])
  70 + filter = Net::LDAP::Filter.join(filter, user_filter)
  71 + end
  72 +
65 73 ldap_user = ldap.bind_as(
66   - filter: Net::LDAP::Filter.eq(ldap.uid, login),
  74 + filter: filter,
67 75 size: 1,
68 76 password: password
69 77 )
... ... @@ -71,22 +79,20 @@ module Gitlab
71 79 find_by_uid(ldap_user.dn) if ldap_user
72 80 end
73 81  
74   - # Check LDAP user existance by dn. User in git over ssh check
75   - #
76   - # It covers 2 cases:
77   - # * when ldap account was removed
78   - # * when ldap account was deactivated by change of OU membership in 'dn'
79   - def blocked?(dn)
80   - ldap = OmniAuth::LDAP::Adaptor.new(ldap_conf)
81   - ldap.connection.search(base: dn, scope: Net::LDAP::SearchScope_BaseObject, size: 1).blank?
82   - end
83   -
84 82 private
85 83  
86 84 def find_by_uid(uid)
87 85 model.where(provider: provider, extern_uid: uid).last
88 86 end
89 87  
  88 + def username
  89 + (auth.info.nickname || samaccountname).to_s.force_encoding("utf-8")
  90 + end
  91 +
  92 + def samaccountname
  93 + (auth.extra[:raw_info][:samaccountname] || []).first
  94 + end
  95 +
90 96 def provider
91 97 'ldap'
92 98 end
... ...
spec/lib/gitlab/ldap/ldap_user_auth_spec.rb
... ... @@ -9,7 +9,8 @@ describe Gitlab::LDAP do
9 9 @info = double(
10 10 uid: '12djsak321',
11 11 name: 'John',
12   - email: 'john@mail.com'
  12 + email: 'john@mail.com',
  13 + nickname: 'john'
13 14 )
14 15 end
15 16  
... ...