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