Created
December 9, 2011 06:48
-
-
Save joshbeitelspacher/1450528 to your computer and use it in GitHub Desktop.
A script to copy Redmine users and groups from a Postgres DB to LDAP. Requires a minor modification to Redmine to use SSH passwords.
This file contains hidden or 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
#!/usr/bin/ruby1.8 -rubygems | |
require "base64" | |
require "net/ldap" | |
require "pg" | |
require "set" | |
class User | |
attr_reader :login | |
attr_reader :firstname | |
attr_reader :lastname | |
attr_reader :mail | |
attr_reader :password | |
def initialize(login, firstname, lastname, mail, password) | |
@login = login | |
@firstname = firstname | |
@lastname = lastname | |
@mail = mail | |
@password = password | |
end | |
end | |
class Group | |
attr_reader :name | |
attr_reader :members | |
def initialize(name, members) | |
@name = name | |
@members = members | |
end | |
end | |
conn = PGconn.open(:dbname => 'redmine') | |
user_results = conn.exec('SELECT login, firstname, lastname, mail, admin, hashed_password, salt FROM users WHERE status = 1 AND type = \'User\'') | |
users = {} | |
admins = Set.new | |
user_results.values.each do |user| | |
password = "{SSHA}#{Base64.encode64(user[5].to_a.pack('H*') + user[6]).gsub("\n", '')}" | |
users[user[0]] = User.new(user[0], user[1], user[2], user[3], password) | |
if user[4] == 't' | |
admins << user[0] | |
end | |
end | |
user_results.clear() | |
group_results = conn.exec('SELECT id, lastname FROM users WHERE status = 1 AND type = \'Group\'') | |
groups = {"Administrators" => Group.new("Administrators", admins)} | |
group_results.values.each do |group| | |
member_results = conn.exec("SELECT login FROM groups_users, users WHERE user_id = id AND group_id = #{group[0]}") | |
members = Set.new | |
member_results.values.each do |member| | |
members << member[0] | |
end | |
member_results.clear() | |
groups[group[1]] = Group.new(group[1], members) | |
end | |
group_results.clear() | |
conn.close() | |
conn = Net::LDAP.new({ | |
:host => 'localhost', | |
:port => 389, | |
:auth => { | |
:method => :simple, | |
:username => 'cn=admin,dc=netbeetle,dc=com', | |
:password => 'secret' | |
} | |
}) | |
base = "ou=people,dc=netbeetle,dc=com" | |
filter = Net::LDAP::Filter.eq("objectClass", "person") | |
conn.search(:base => base, :filter => filter, :attributes=> ['uid']) do |entry| | |
user = users[entry.uid.to_s] | |
if user | |
puts "Updating #{entry.dn}" | |
ops = [ | |
[:replace, :uid, user.login], | |
[:replace, :sn, user.lastname], | |
[:replace, :cn, "#{user.firstname} #{user.lastname}"], | |
[:replace, :displayName, "#{user.firstname} #{user.lastname}"], | |
[:replace, :mail, user.mail], | |
[:replace, :userPassword, user.password] | |
] | |
conn.modify(:dn => entry.dn, :operations => ops) | |
users.delete(entry.uid.to_s) | |
else | |
puts "Removing #{entry.dn}" | |
conn.delete(:dn => entry.dn) | |
end | |
end | |
users.values.each do |user| | |
dn = "uid=#{user.login},ou=people,dc=netbeetle,dc=com" | |
puts "Adding #{dn}" | |
attr = { | |
:objectclass => ["inetOrgPerson", "organizationalPerson", "person", "top"], | |
:uid => user.login, | |
:sn => user.lastname, | |
:cn => "#{user.firstname} #{user.lastname}", | |
:displayName => "#{user.firstname} #{user.lastname}", | |
:mail => user.mail, | |
:userPassword => user.password | |
} | |
conn.add(:dn => dn, :attributes => attr) | |
end | |
base = "ou=groups,dc=netbeetle,dc=com" | |
filter = Net::LDAP::Filter.eq("objectClass", "groupOfNames") | |
conn.search(:base => base, :filter => filter, :attributes=> ['cn']) do |entry| | |
group = groups[entry.cn.to_s] | |
if group | |
puts "Updating #{entry.dn}" | |
member_dns = group.members.map do |member| | |
"uid=#{member},ou=people,dc=netbeetle,dc=com" | |
end | |
ops = [ | |
[:replace, :cn, group.name], | |
[:replace, :member, member_dns] | |
] | |
conn.modify(:dn => entry.dn, :operations => ops) | |
groups.delete(entry.cn.to_s) | |
else | |
puts "Removing #{entry.dn}" | |
conn.delete(:dn => entry.dn) | |
end | |
end | |
groups.values.each do |group| | |
dn = "cn=#{group.name},ou=groups,dc=netbeetle,dc=com" | |
puts "Adding #{dn}" | |
member_dns = group.members.map do |member| | |
"uid=#{member},ou=people,dc=netbeetle,dc=com" | |
end | |
attr = { | |
:objectclass => ["groupOfNames", "top"], | |
:member => member_dns | |
} | |
conn.add(:dn => dn, :attributes => attr) | |
end |
This file contains hidden or 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
diff --git a/app/models/user.rb b/app/models/user.rb | |
index c06a907..83672e5 100644 | |
--- a/app/models/user.rb | |
+++ b/app/models/user.rb | |
@@ -217,15 +217,15 @@ class User < Principal | |
if auth_source_id.present? | |
auth_source.authenticate(self.login, clear_password) | |
else | |
- User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password | |
+ User.hash_password("#{clear_password}#{salt}") == hashed_password || User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password | |
end | |
end | |
# Generates a random salt and computes hashed_password for +clear_password+ | |
- # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) | |
+ # The hashed password is stored in the following form: SHA1(password + salt) | |
def salt_password(clear_password) | |
self.salt = User.generate_salt | |
- self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") | |
+ self.hashed_password = User.hash_password("#{clear_password}#{salt}") | |
end | |
# Does the backend storage allow this user to change their password? | |
@@ -573,9 +573,9 @@ class User < Principal | |
Digest::SHA1.hexdigest(clear_password || "") | |
end | |
- # Returns a 128bits random salt as a hex string (32 chars long) | |
+ # Returns a 192bits random salt as a base64 string (32 chars long) | |
def self.generate_salt | |
- ActiveSupport::SecureRandom.hex(16) | |
+ ActiveSupport::SecureRandom.base64(24) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment