diff options
-rw-r--r-- | app/controllers/application_controller.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/expiring_lock.rb | 52 | ||||
-rw-r--r-- | lib/gitlab/ldap/access.rb | 6 | ||||
-rw-r--r-- | lib/gitlab/user_access.rb | 5 |
4 files changed, 63 insertions, 2 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fb74919ea23..bc8019193ee 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -246,6 +246,8 @@ class ApplicationController < ActionController::Base def ldap_security_check if current_user && current_user.requires_ldap_check? + return unless Gitlab::LDAP::Access.try_lock_user(user) + unless Gitlab::LDAP::Access.allowed?(current_user) sign_out current_user flash[:alert] = "Access denied for your LDAP account." diff --git a/lib/gitlab/expiring_lock.rb b/lib/gitlab/expiring_lock.rb new file mode 100644 index 00000000000..ef77a24cf7d --- /dev/null +++ b/lib/gitlab/expiring_lock.rb @@ -0,0 +1,52 @@ +module Gitlab + # This class implements a distributed self-expiring lock. + # + # [2] pry(main)> l = Gitlab::ExpiringLock.new('foobar', 5) + # => #<Gitlab::ExpiringLock:0x007ffb9d7cb7f8 @key="foobar", @timeout=5> + # [3] pry(main)> l.try_lock + # => true + # [4] pry(main)> l.try_lock # Only the first try_lock succeeds + # => false + # [5] pry(main)> l.locked? + # => true + # [6] pry(main)> sleep 5 + # => 5 + # [7] pry(main)> l.locked? # After the timeout the lock is released + # => false + # + class ExpiringLock + def initialize(key, timeout) + @key, @timeout = key, timeout + end + + # Try to obtain the lock. Return true on succes, + # false if the lock is already taken. + def try_lock + # INCR does not change the key TTL + if redis.incr(redis_key) == 1 + # We won the race to insert the key into Redis + redis.expire(redis_key, @timeout) + true + else + # Somebody else won the race + false + end + end + + # Check if somebody somewhere locked this key + def locked? + !!redis.get(redis_key) + end + + private + + def redis + # Maybe someday we want to use a connection pool... + @redis ||= Redis.new(url: Gitlab::RedisConfig.url) + end + + def redis_key + "gitlab:expiring_lock:#{@key}" + end + end +end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index da4435c7308..29347c05b7d 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -7,6 +7,12 @@ module Gitlab class Access attr_reader :provider, :user + LOCK_TIMEOUT = 600 + + def self.try_lock_user(user) + Gitlab::ExpiringLock.new("user_ldap_check:#{user.id}", LOCK_TIMEOUT).try_lock + end + def self.open(user, &block) Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| block.call(self.new(user, adapter)) diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 4885baf9526..46ac5825fd1 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -3,8 +3,9 @@ module Gitlab def self.allowed?(user) return false if user.blocked? - if user.requires_ldap_check? - return false unless Gitlab::LDAP::Access.allowed?(user) + if user.requires_ldap_check? && Gitlab::LDAP::Access.try_lock_user(user) + return Gitlab::LDAP::Access.allowed?(user) + end end true |