Skip to content

Instantly share code, notes, and snippets.

@mgrobelin
Created October 25, 2012 15:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgrobelin/3953472 to your computer and use it in GitHub Desktop.
Save mgrobelin/3953472 to your computer and use it in GitHub Desktop.
force ldap auth for gitlab
#this code borrowed pieces from activeldap and net-ldap
require 'ldap'
require 'rack'
#require 'net/ldap'
require 'net/ntlm'
require 'uri'
require 'sasl'
require 'kconv'
def bind_as_2(args = {})
result = {}
result[:uid] = args[:username]
result[:email] = "#{args[:username]}@example.com"
ldap_host = 'ldap.example.com' #LDAP server IP or fqdn
ldap_port = 636
login = args[:username]
password = args[:password]
ldap_conn = LDAP::SSLConn.new(ldap_host, ldap_port) # start_tls=true
ldap_conn.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 )
ad_login = "uid=#{login},ou=People,dc=example,dc=com"
false
result if ldap_conn.bind( ad_login, password )
end
module OmniAuth
module LDAP2
class Adaptor
class LdapError < StandardError; end
class ConfigurationError < StandardError; end
class AuthenticationError < StandardError; end
class ConnectionError < StandardError; end
VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :bind_dn, :password, :try_sasl, :sasl_mechanisms, :uid, :base, :allow_anonymous]
MUST_HAVE_KEYS = [:host, :port, :method, :uid, :base]
METHOD = {
:ssl => :simple_tls,
:tls => :start_tls,
:plain => nil,
}
attr_accessor :bind_dn, :password
attr_reader :connection, :uid, :base, :auth
def self.validate(configuration={})
message = []
MUST_HAVE_KEYS.each do |name|
message << name if configuration[name].nil?
end
raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty?
end
def initialize(configuration={})
Adaptor.validate(configuration)
@configuration = configuration.dup
@configuration[:allow_anonymous] ||= false
@logger = @configuration.delete(:logger)
VALID_ADAPTER_CONFIGURATION_KEYS.each do |name|
instance_variable_set("@#{name}", @configuration[name])
end
method = ensure_method(@method)
config = {
:host => @host,
:port => @port,
:encryption => method,
:base => @base
}
@uri = construct_uri(@host, @port, @method != :plain)
@bind_method = @try_sasl ? :sasl : (@allow_anonymous||!@bind_dn||!@password ? :anonymous : :simple)
@auth = sasl_auths({:username => @bind_dn, :password => @password}).first if @bind_method == :sasl
@auth ||= { :method => @bind_method,
:username => @bind_dn,
:password => @password
}
config[:auth] = @auth
#@connection = Net::LDAP.new(config)
end
#:base => "dc=yourcompany, dc=com",
# :filter => "(mail=#{user})",
# :password => psw
def bind_as(args = {})
result = bind_as_2(args)
#result = false
#@connection.open do |me|
# rs = me.search args
# if rs and rs.first and dn = rs.first.dn
# password = args[:password]
# method = args[:method] || @method
# password = password.call if password.respond_to?(:call)
# if method == 'sasl'
# result = rs.first if me.bind(sasl_auths({:username => dn, :password => password}).first)
# else
# result = rs.first if me.bind(:method => :simple, :username => dn,
# :password => password)
# end
# end
#end
result
end
private
def ensure_method(method)
method ||= "plain"
normalized_method = method.to_s.downcase.to_sym
return METHOD[normalized_method] if METHOD.has_key?(normalized_method)
available_methods = METHOD.keys.collect {|m| m.inspect}.join(", ")
format = "%s is not one of the available connect methods: %s"
raise ConfigurationError, format % [method.inspect, available_methods]
end
def sasl_auths(options={})
auths = []
sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
sasl_mechanisms.each do |mechanism|
normalized_mechanism = mechanism.downcase.gsub(/-/, '_')
sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}"
next unless respond_to?(sasl_bind_setup, true)
initial_credential, challenge_response = send(sasl_bind_setup, options)
auths << {
:method => :sasl,
:initial_credential => initial_credential,
:mechanism => mechanism,
:challenge_response => challenge_response
}
end
auths
end
def sasl_bind_setup_digest_md5(options)
bind_dn = options[:username]
initial_credential = ""
challenge_response = Proc.new do |cred|
pref = SASL::Preferences.new :digest_uri => "ldap/#{@host}", :username => bind_dn, :has_password? => true, :password => options[:password]
sasl = SASL.new("DIGEST-MD5", pref)
response = sasl.receive("challenge", cred)
response[1]
end
[initial_credential, challenge_response]
end
def sasl_bind_setup_gss_spnego(options)
bind_dn = options[:username]
psw = options[:password]
raise LdapError.new( "invalid binding information" ) unless (bind_dn && psw)
nego = proc {|challenge|
t2_msg = Net::NTLM::Message.parse( challenge )
bind_dn, domain = bind_dn.split('\\').reverse
t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain
t3_msg = t2_msg.response( {:user => bind_dn, :password => psw}, {:ntlmv2 => true} )
t3_msg.serialize
}
[Net::NTLM::Message::Type1.new.serialize, nego]
end
def construct_uri(host, port, ssl)
protocol = ssl ? "ldaps" : "ldap"
URI.parse("#{protocol}://#{host}:#{port}").to_s
end
end
end
end
module Gitlab
class Auth
def find_for_ldap_auth(auth, signed_in_resource = nil)
auth.info = auth.extra.raw_info
uid = auth.info.uid
provider = auth.provider
email = auth.info.email.downcase unless auth.info.email.nil?
raise OmniAuth::Error, "LDAP accounts must provide an uid and email address" if uid.nil? or email.nil?
if @user = User.find_by_extern_uid_and_provider(uid, provider)
@user
elsif @user = User.find_by_email(email)
log.info "Updating legacy LDAP user #{email} with extern_uid => #{uid}"
@user.update_attributes(:extern_uid => uid, :provider => provider)
@user
else
create_from_omniauth(auth, true)
end
end
def create_from_omniauth(auth, ldap = false)
provider = auth.provider
uid = auth.info.uid || auth.uid
name = auth.info.name.force_encoding("utf-8")
email = auth.info.email.downcase unless auth.info.email.nil?
ldap_prefix = ldap ? '(LDAP) ' : ''
raise OmniAuth::Error, "#{ldap_prefix}#{provider} does not provide an email"\
" address" if auth.info.email.blank?
log.info "#{ldap_prefix}Creating user from #{provider} login"\
" {uid => #{uid}, name => #{name}, email => #{email}}"
password = Devise.friendly_token[0, 8].downcase
@user = User.new({
extern_uid: uid,
provider: provider,
name: name,
email: email,
password: password,
password_confirmation: password,
projects_limit: Gitlab.config.default_projects_limit,
}, as: :admin)
if Gitlab.config.omniauth['block_auto_created_users'] && !ldap
@user.blocked = true
end
@user.save!
@user
end
def find_or_new_for_omniauth(auth)
provider, uid = auth.provider, auth.uid
if @user = User.find_by_provider_and_extern_uid(provider, uid)
@user
else
if Gitlab.config.omniauth['allow_single_sign_on']
@user = create_from_omniauth(auth)
@user
end
end
end
def log
Gitlab::AppLogger
end
end
end
# this provides 'ldap' - 'net/ldap' does not work with 389-ds
gem "ruby-ldap"
module Grack
class Auth < Rack::Auth::Basic
def valid?
# Authentication with ldap
email, password = @auth.credentials
#user = User.find_by_email(email)
authhash=OmniAuth::AuthHash.new({
:info => {},
:provider => 'ldap',
:extra => {
:raw_info => {
:uid => email,
:email => "#{email}@example.com"
}
}
})
user = User.find_for_ldap_auth(authhash)
return false if user.nil?
# Set GL_USER env variable
ENV['GL_USER'] = email
# Pass Gitolite update hook
ENV['GL_BYPASS_UPDATE_HOOK'] = "true"
# Need this patch due to the rails mount
@env['PATH_INFO'] = @request.path
@env['SCRIPT_NAME'] = ""
# Find project by PATH_INFO from env
if m = /^\/([\w-]+).git/.match(@request.path_info).to_a
return false unless project = Project.find_by_path(m.last)
end
# Git upload and receive
if @request.get?
true
elsif @request.post?
if @request.path_info.end_with?('git-upload-pack')
return project.dev_access_for?(user)
elsif @request.path_info.end_with?('git-receive-pack')
if project.protected_branches.map(&:name).include?(current_ref)
project.master_access_for?(user)
else
project.dev_access_for?(user)
end
else
false
end
else
false
end
end# valid?
def current_ref
if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
input = Zlib::GzipReader.new(@request.body).read
else
input = @request.body.read
end
# Need to reset seek point
@request.body.rewind
/refs\/heads\/([\w-]+)/.match(input).to_a.first
end
end# Auth
end# Grack
require 'omniauth'
module OmniAuth
module Strategies
class LDAP
class MissingCredentialsError < StandardError; end
include OmniAuth::Strategy
@@config = {
'name' => 'cn',
'first_name' => 'givenName',
'last_name' => 'sn',
'email' => ['mail', "email", 'userPrincipalName'],
'phone' => ['telephoneNumber', 'homePhone', 'facsimileTelephoneNumber'],
'mobile' => ['mobile', 'mobileTelephoneNumber'],
'nickname' => ['uid', 'userid', 'sAMAccountName'],
'title' => 'title',
'location' => {"%0, %1, %2, %3 %4" => [['address', 'postalAddress', 'homePostalAddress', 'street', 'streetAddress'], ['l'], ['st'],['co'],['postOfficeBox']]},
'uid' => 'dn',
'url' => ['wwwhomepage'],
'image' => 'jpegPhoto',
'description' => 'description'
}
option :title, "LDAP Authentication" #default title for authentication form
option :port, 389
option :method, :plain
option :uid, 'sAMAccountName'
option :name_proc, lambda {|n| n}
def request_phase
OmniAuth::LDAP2::Adaptor.validate @options
f = OmniAuth::Form.new(:title => (options[:title] || "LDAP Authentication"), :url => callback_path)
f.text_field 'Login', 'username'
f.password_field 'Password', 'password'
f.button "Sign In"
f.to_response
end
def callback_phase
@adaptor = OmniAuth::LDAP2::Adaptor.new @options
begin
# GITLAB security patch
# Dont allow blank password for ldap auth
if request['username'].nil? || request['username'].empty? || request['password'].nil? || request['password'].empty?
raise MissingCredentialsError.new("Missing login credentials")
end
# playa
#@ldap_user_info = @adaptor.bind_as(:filter => Net::LDAP::Filter.eq(@adaptor.uid, @options[:name_proc].call(request['username'])),:size => 1, :password => request['password'])
@ldap_user_info = @adaptor.bind_as(:size => 1, :password => request['password'], :username => request['username'])
return fail!(:invalid_credentials) if !@ldap_user_info
@user_info = self.class.map_user(@@config, @ldap_user_info)
super
rescue Exception => e
return fail!(:ldap_error, e)
end
end
uid {
@user_info["uid"]
}
info {
@user_info
}
extra {
{ :raw_info => @ldap_user_info }
}
def self.map_user(mapper, object)
user = {}
mapper.each do |key, value|
case value
when String
user[key] = object[value.downcase.to_sym].first if object[value.downcase.to_sym]
when Array
value.each {|v| (user[key] = object[v.downcase.to_sym].first; break;) if object[v.downcase.to_sym]}
when Hash
value.map do |key1, value1|
pattern = key1.dup
value1.each_with_index do |v,i|
part = ''; v.collect(&:downcase).collect(&:to_sym).each {|v1| (part = object[v1].first; break;) if object[v1]}
pattern.gsub!("%#{i}",part||'')
end
user[key] = pattern
end
end
end
user
end
end
end
end
OmniAuth.config.add_camelization 'ldap', 'LDAP'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment