Created
October 25, 2012 15:41
-
-
Save mgrobelin/3953472 to your computer and use it in GitHub Desktop.
force ldap auth for gitlab
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# this provides 'ldap' - 'net/ldap' does not work with 389-ds | |
gem "ruby-ldap" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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