Commit 767f7d9ae7eb29d5feaa65863a8948ca60d3825f
Exists in
spb-stable
and in
3 other branches
Merge branch 'ldap-code' into 'master'
LDAP code from EE
Showing
11 changed files
with
243 additions
and
56 deletions
Show diff stats
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: &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: &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
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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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