Skip to content

Instantly share code, notes, and snippets.

@spullen
Created August 20, 2013 13:44
Show Gist options
  • Save spullen/6281635 to your computer and use it in GitHub Desktop.
Save spullen/6281635 to your computer and use it in GitHub Desktop.
Database and LDAP Authentication
require 'net/ldap'
module LdapAdapter
# Add valid users to database
mattr_accessor :ldap_create_user
@@ldap_create_user = false
mattr_accessor :ldap_config
# @@ldap_config = "#{Rails.root}/config/ldap.yml"
mattr_accessor :ldap_update_password
@@ldap_update_password = true
mattr_accessor :ldap_check_group_membership
@@ldap_check_group_membership = false
mattr_accessor :ldap_check_attributes
@@ldap_check_role_attribute = false
mattr_accessor :ldap_use_admin_to_bind
@@ldap_use_admin_to_bind = false
mattr_accessor :ldap_auth_username_builder
@@ldap_auth_username_builder = Proc.new() {|attribute, login, ldap| "#{attribute}=#{login},#{ldap.base}" }
mattr_accessor :ldap_ad_group_check
@@ldap_ad_group_check = false
def self.valid_credentials?(login, password_plaintext)
options = {:login => login,
:password => password_plaintext,
:ldap_auth_username_builder => LdapAdapter.ldap_auth_username_builder,
:admin => LdapAdapter.ldap_use_admin_to_bind}
resource = LdapConnect.new(options)
resource.authorized?
end
def self.update_password(login, new_password)
options = {:login => login,
:new_password => new_password,
:ldap_auth_username_builder => LdapAdapter.ldap_auth_username_builder,
:admin => LdapAdapter.ldap_use_admin_to_bind}
resource = LdapConnect.new(options)
resource.change_password! if new_password.present?
end
def self.update_own_password(login, new_password, current_password)
set_ldap_param(login, :userpassword, new_password, current_password)
end
def self.ldap_connect(login)
options = {:login => login,
:ldap_auth_username_builder => LdapAdapter.ldap_auth_username_builder,
:admin => LdapAdapter.ldap_use_admin_to_bind}
resource = LdapConnect.new(options)
end
def self.valid_login?(login)
self.ldap_connect(login).valid_login?
end
def self.get_groups(login)
self.ldap_connect(login).user_groups
end
def self.get_dn(login)
self.ldap_connect(login).dn
end
def self.set_ldap_param(login, param, new_value, password = nil)
options = { :login => login,
:ldap_auth_username_builder => LdapAdapter.ldap_auth_username_builder,
:password => password }
resource = LdapConnect.new(options)
resource.set_param(param, new_value)
end
def self.delete_ldap_param(login, param, password = nil)
options = { :login => login,
:ldap_auth_username_builder => LdapAdapter.ldap_auth_username_builder,
:password => password }
resource = LdapConnect.new(options)
resource.delete_param(param)
end
def self.get_ldap_param(login,param)
resource = self.ldap_connect(login)
resource.ldap_param_value(param)
end
def self.get_ldap_entry(login)
self.ldap_connect(login).search_for_login
end
class LdapConnect
attr_reader :ldap, :login
def initialize(params = {})
ldap_config = YAML.load(ERB.new(File.read(LdapAdapter.ldap_config || "#{Rails.root}/config/ldap.yml")).result)[Rails.env]
ldap_options = params
ldap_config["ssl"] = :simple_tls if ldap_config["ssl"] === true
ldap_options[:encryption] = ldap_config["ssl"].to_sym if ldap_config["ssl"]
@ldap = ::Net::LDAP.new(ldap_options)
@ldap.host = ldap_config["host"]
@ldap.port = ldap_config["port"]
@ldap.base = ldap_config["base"]
@attribute = ldap_config["attribute"]
@ldap_auth_username_builder = params[:ldap_auth_username_builder]
@group_base = ldap_config["group_base"]
@check_group_membership = ldap_config.has_key?("check_group_membership") ? ldap_config["check_group_membership"] : LdapAdapter.ldap_check_group_membership
@required_groups = ldap_config["required_groups"]
@required_attributes = ldap_config["require_attribute"]
@ldap.auth ldap_config["admin_user"], ldap_config["admin_password"] if params[:admin]
@login = params[:login]
@password = params[:password]
@new_password = params[:new_password]
end
def delete_param(param)
update_ldap [[:delete, param.to_sym, nil]]
end
def set_param(param, new_value)
update_ldap( { param.to_sym => new_value } )
end
def dn
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("LDAP dn lookup: #{@attribute}=#{@login}")
ldap_entry = search_for_login
if ldap_entry.nil?
@ldap_auth_username_builder.call(@attribute,@login,@ldap)
else
ldap_entry.dn
end
end
def ldap_param_value(param)
filter = ::Net::LDAP::Filter.eq(@attribute.to_s, @login.to_s)
ldap_entry = nil
@ldap.search(:filter => filter) {|entry| ldap_entry = entry}
if ldap_entry
if ldap_entry[param]
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("Requested param #{param} has value #{ldap_entry.send(param)}")
value = ldap_entry.send(param)
value = value.first if value.is_a?(Array) and value.count == 1
value
else
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("Requested param #{param} does not exist")
value = nil
end
else
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("Requested ldap entry does not exist")
value = nil
end
end
def authenticate!
@ldap.auth(dn, @password)
@ldap.bind
end
def authenticated?
authenticate!
end
def authorized?
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("Authorizing user #{dn}")
authenticated? && in_required_groups? && has_required_attribute?
end
def change_password!
update_ldap(:userpassword => ::Net::LDAP::Password.generate(:sha, @new_password))
end
def in_required_groups?
return true unless @check_group_membership
## FIXME set errors here, the ldap.yml isn't set properly.
return false if @required_groups.nil?
admin_ldap = LdapConnect.admin
for group in @required_groups
if group.is_a?(Array)
group_attribute, group_name = group
else
group_attribute = "uniqueMember"
group_name = group
end
unless LdapAdapter.ldap_ad_group_check
admin_ldap.search(:base => group_name, :scope => ::Net::LDAP::SearchScope_BaseObject) do |entry|
unless entry[group_attribute].include? dn
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("User #{dn} is not in group: #{group_name }")
return false
end
end
else
# AD optimization - extension will recursively check sub-groups with one query
# "(memberof:1.2.840.113556.1.4.1941:=group_name)"
search_result = admin_ldap.search(:base => dn,
:filter => ::Net::LDAP::Filter.ex("memberof:1.2.840.113556.1.4.1941", group_name),
:scope => ::Net::LDAP::SearchScope_BaseObject)
# Will return the user entry if belongs to group otherwise nothing
unless search_result.length == 1 && search_result[0].dn.eql?(dn)
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("User #{dn} is not in group: #{group_name }")
return false
end
end
end
return true
end
def has_required_attribute?
return true unless LdapAdapter.ldap_check_attributes
admin_ldap = LdapConnect.admin
user = find_ldap_user(admin_ldap)
@required_attributes.each do |key,val|
unless user[key].include? val
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("User #{dn} did not match attribute #{key}:#{val}")
return false
end
end
return true
end
def user_groups
admin_ldap = LdapConnect.admin
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("Getting groups for #{dn}")
filter = ::Net::LDAP::Filter.eq("uniqueMember", dn)
admin_ldap.search(:filter => filter, :base => @group_base).collect(&:dn)
end
def valid_login?
!search_for_login.nil?
end
# Searches the LDAP for the login
#
# @return [Object] the LDAP entry found; nil if not found
def search_for_login
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("LDAP search for login: #{@attribute}=#{@login}")
filter = ::Net::LDAP::Filter.eq(@attribute.to_s, @login.to_s)
ldap_entry = nil
@ldap.search(:filter => filter) {|entry| ldap_entry = entry}
ldap_entry
end
private
def self.admin
ldap = LdapConnect.new(:admin => true).ldap
unless ldap.bind
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("Cannot bind to admin LDAP user")
raise LdapException, "Cannot connect to admin LDAP user"
end
return ldap
end
def find_ldap_user(ldap)
#DeviseLdapAuthenticatable::Logger.send("Finding user: #{dn}")
ldap.search(:base => dn, :scope => ::Net::LDAP::SearchScope_BaseObject).try(:first)
end
def update_ldap(ops)
operations = []
if ops.is_a? Hash
ops.each do |key,value|
operations << [:replace,key,value]
end
elsif ops.is_a? Array
operations = ops
end
if LdapAdapter.ldap_use_admin_to_bind
privileged_ldap = LdapConnect.admin
else
authenticate!
privileged_ldap = self.ldap
end
#TODO Replace: DeviseLdapAuthenticatable::Logger.send("Modifying user #{dn}")
privileged_ldap.modify(:dn => dn, :operations => operations)
end
end
class LdapException < Exception
end
end
class User < ActiveRecord::Base
include LdapAdapter
attr_accessible :username, :email, :password, :ldap_authenticatable
attr_accessor :password, :password_confirmation
validates :username, :presence => true, :uniqueness => true
validates :email, :presence => true, :uniqueness => true
validates :password, :presence => true, :confirmation => true, :if => :password_required?
before_create :generate_remember_token
before_save :encrypt_password, :if => :password_encryptable?
def self.authenticate(username, password)
user = User.find_by_username(username)
if user && user.valid_authentication?(password)
user
else
nil
end
end
def valid_authentication?(password)
if ldap_authenticatable
valid_authentication_by_ldap?(password)
else
valid_authentication_by_database?(password)
end
end
private
def self.generate_salt
BCrypt::Engine.generate_salt
end
def self.hash_secret(password, password_salt)
BCrypt::Engine.hash_secret(password, password_salt)
end
def valid_authentication_by_database?(password)
return self.password_encrypted == User.hash_secret(password, self.password_salt)
end
def valid_authentication_by_ldap?(password)
return LdapAdapter.valid_credentials?(self.username, password)
end
def generate_remember_token
self.remember_token = SecureRandom.urlsafe_base64
end
def encrypt_password
self.password_salt = User.generate_salt
self.password_encrypted = User.hash_secret(password, password_salt)
end
def password_required?
!ldap_authenticatable
end
def password_encryptable?
password_required? && password.present?
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment