class MU::Master::LDAP
Routines for manipulating users and groups in 389 Directory Services or Active Directory.
Constants
- AD_PW_ATTRS
Public Class Methods
Find a group ID not currently in use from the local system's perspective XXX this is vulnerable to a race condition, and may not account for things in the directory
# File modules/mu/master/ldap.rb, line 175 def self.allocateGID(group: nil) MU::MommaCat.lock("gid_generator", false, true) used_gids = [] Etc.group{ |g| if !group.nil? and g.name == group raise MuLDAPError, "Group #{group} already exists as a local system group, cannot allocate in directory" end used_gids << g.gid } conn = getLDAPConnection conn.search( :filter => Net::LDAP::Filter.eq("objectClass", @group_class), :base => $MU_CFG['ldap']['base_dn'], :attributes => [@gidnum_attr] ) { |item| used_gids = used_gids + item[@gidnum_attr].map { |x| x.to_i } } for x in @gid_range_start..65535 do if !used_gids.include?(x) MU::MommaCat.unlock("gid_generator", true) return x.to_s end end MU::MommaCat.unlock("gid_generator", true) return nil end
Find a user ID not currently in use from the local system's perspective
# File modules/mu/master/ldap.rb, line 158 def self.allocateUID MU::MommaCat.lock("uid_generator", false, true) used_uids = getUsedUids for x in @uid_range_start..65535 do if !used_uids.include?(x) MU::MommaCat.unlock("uid_generator", true) return x.to_s end end MU::MommaCat.unlock("uid_generator", true) return nil end
Test whether our LDAP
binding user has permissions to create other users, manipulate groups, and set passwords. Note that it's not fatal if we can't, simply a design where most account management happens on the directory side. @return [Boolean]
# File modules/mu/master/ldap.rb, line 374 def self.canWriteLDAP? return @can_write if !@can_write.nil? conn = getLDAPConnection dn = "CN=Mu Testuser #{Process.pid},#{$MU_CFG["ldap"]["user_ou"]}" uid = "mu.testuser.#{Process.pid}" attr = { :cn => "Mu Testuser #{Process.pid}", @uid_attr.to_sym => uid } if $MU_CFG["ldap"]["type"] == "Active Directory" attr[:objectclass] = ["user"] attr[:userPrincipalName] = "#{uid}@#{$MU_CFG["ldap"]["domain_name"]}" attr[:pwdLastSet] = "-1" uid = dn elsif $MU_CFG["ldap"]["type"] == "389 Directory Services" attr[:objectclass] = ["top", "person", "organizationalPerson", "inetorgperson"] attr[:userPassword] = Password.pronounceable(12..14) attr[:displayName] = "Mu Test User #{Process.pid}" attr[:mail] = $MU_CFG['mu_admin_email'] attr[:givenName] = "Mu" attr[:sn] = "TestUser" end @can_write = true if !conn.add(:dn => dn, :attributes => attr) MU.log "Couldn't create write-test user #{dn}, operating in read-only LDAP mode (#{getLDAPErr})", MU::NOTICE, details: attr return false end # Make sure we can write various fields that we might need to touch [:displayName, :mail, :givenName, :sn].each { |field| if !conn.replace_attribute(dn, field, "foo@bar.com") MU.log "Couldn't modify write-test user #{dn} field #{field.to_s}, operating in read-only LDAP mode (#{getLDAPErr})", MU::NOTICE @can_write = false end } # Can we add them to the Mu membership group(s) [$MU_CFG["ldap"]["user_group_dn"], $MU_CFG["ldap"]["admin_group_dn"]].each { |group| if !conn.modify(:dn => group, :operations => [[:add, @member_attr, uid]]) MU.log "Couldn't add write-test user #{dn} to #{@member_attr} in group #{group}, operating in read-only LDAP mode (#{getLDAPErr})", MU::NOTICE @can_write = false end } if !conn.delete(:dn => dn) MU.log "Couldn't delete write-test user #{dn}, operating in read-only LDAP mode", MU::NOTICE @can_write = false end @can_write end
Convert a Microsoft timestamp to a Ruby Time object. See also getMicrosoftTime. @param stamp [Integer]: The MS-style timestamp, e.g. 130838184558490696 @return [Time]
# File modules/mu/master/ldap.rb, line 362 def self.convertMicrosoftTime(stamp) # ms_epoch = DateTime.new(1601,1,1).strftime("%Q").to_i unixtime = (stamp.to_i/10000) + DateTime.new(1601,1,1).strftime("%Q").to_i Time.at(unixtime/1000) end
Create a directory group. Valid for 389 DS only, will fail on AD.
# File modules/mu/master/ldap.rb, line 203 def self.createGroup(group, full_dn: nil) dn = "CN=#{group},"+$MU_CFG["ldap"]["group_ou"] dn = full_dn if !full_dn.nil? gid = allocateGID attr = { :cn => group, :description => "#{group} Group", :gidNumber => gid, :objectclass => ["top", "posixGroup"] } if !@ldap_conn.add( :dn => dn, :attributes => attr ) and @ldap_conn.get_operation_result.code != 68 MU.log "Error creating #{dn}: "+getLDAPErr, MU::ERR, details: attr return false elsif @ldap_conn.get_operation_result.code != 68 MU.log "Created group #{dn} with gid #{gid}", MU::NOTICE end return gid end
Delete a user from our directory @param user [String]: The username to remove. @return [Boolean]: Success/Failure
# File modules/mu/master/ldap.rb, line 707 def self.deleteUser(user) if canWriteLDAP? conn = getLDAPConnection dn = nil conn.search( :filter => Net::LDAP::Filter.eq(@uid_attr, user), :base => $MU_CFG["ldap"]["base_dn"], :attributes => [@uid_attr] ) do |acct| dn = acct.dn break end # Our default LDAP server doesn't cascade user deletes through groups, # so help it out. if $MU_CFG["ldap"]["type"] == "389 Directory Services" conn.search( :filter => Net::LDAP::Filter.eq("objectclass", @group_class), :base => $MU_CFG["ldap"]["base_dn"], :attributes => ["cn", @member_attr] ) do |group| group[@member_attr].each { |member| next if member.nil? if member.downcase == user or (!dn.nil? and member.downcase == dn.downcase) manageGroup(group.cn.first, remove_users: [user]) end } if group.cn.first.downcase == "#{user}.mu-user" and !conn.delete(:dn => group.dn) MU.log "Couldn't delete user's default group #{group.dn}", MU::WARN, details: getLDAPErr else MU.log "Removed user's default group #{user}.mu-user", MU::NOTICE end end end if !dn.nil? and !conn.delete(:dn => dn) MU.log "Failed to delete #{user} from LDAP: #{getLDAPErr}", MU::WARN, details: dn return false end MU.log "Removed LDAP user #{user}", MU::NOTICE return true else MU.log "We are in read-only LDAP mode. You must manually delete #{user} from your directory.", MU::WARN end false end
If there is an active LDAP
connection loaded, close it. Well, nil it out. There's no close method, that's theoretically handled in garbage collection.
# File modules/mu/master/ldap.rb, line 127 def self.dropLDAPConnection @ldap_conn = nil end
Search for groups whose names contain any of the given search terms and return their full DNs. @param search [Array<String>]: Strings to search for. @param exact [Boolean]: Return only exact matches for whole fields. @param searchbase [String]: The DN under which to search. @return [Array<String>]
# File modules/mu/master/ldap.rb, line 435 def self.findGroups(search = [], exact: false, searchbase: $MU_CFG['ldap']['base_dn']) if search.nil? or search.size == 0 raise MuLDAPError, "Need something to search for in MU::Master::LDAP.findGroups" end conn = getLDAPConnection filter = nil search.each { |term| curfilter = Net::LDAP::Filter.contains(@gid_attr, "#{term}") if exact curfilter = Net::LDAP::Filter.eq(@gid_attr, "#{term}") end if !filter filter = curfilter else filter = filter | curfilter end } filter = Net::LDAP::Filter.ne("objectclass", "computer") & (filter) groups = [] conn.search( :filter => filter, :base => searchbase, :attributes => ["objectclass"] ) do |group| groups << group.dn end groups end
Find a directory user with fuzzy string matching on sAMAccountName/uid, displayName, group memberships, or email @param search [Array<String>]: Strings to search for. @param exact [Boolean]: Return only exact matches for whole fields. @param searchbase [String]: The DN under which to search. @param extra_attrs [Array<String>]: Other LDAP
attributes to search @param matchgroups [Array<String>]: An array of groups. If supplied, a user must be a member of one of these in order to match. @return [Array<Hash>]
# File modules/mu/master/ldap.rb, line 489 def self.findUsers(search = [], exact: false, searchbase: $MU_CFG['ldap']['base_dn'], extra_attrs: [], matchgroups: []) # We want to search groups, but can't search on memberOf with wildcards. # So search groups independently, build a list of full CNs, and use # those. if search.size > 0 groups = findGroups(search, exact: exact, searchbase: searchbase) end searchattrs = [@uid_attr] getattrs = [] if $MU_CFG["ldap"]["type"] == "389 Directory Services" getattrs = ["uid", "displayName", "mail"] + extra_attrs elsif $MU_CFG["ldap"]["type"] == "Active Directory" getattrs = ["sAMAccountName", "displayName", "mail", "lastLogon", "lockoutTime", "pwdLastSet", "memberOf", "userAccountControl"] + extra_attrs end if !exact searchattrs = searchattrs + ["displayName", "mail"] + extra_attrs end conn = getLDAPConnection users = {} filter = nil rejected = 0 if search.size > 0 search.each { |term| if term.nil? or (term.length < 4 and !exact) MU.log "Search term '#{term}' is too short, ignoring.", MU::WARN rejected = rejected + 1 next end searchattrs.each { |attr| if !filter if exact filter = Net::LDAP::Filter.eq(attr, "#{term}") else filter = Net::LDAP::Filter.contains(attr, "#{term}") end else if exact filter = filter |Net::LDAP::Filter.eq(attr, "#{term}") else filter = filter |Net::LDAP::Filter.contains(attr, "#{term}") end end } } if rejected == search.size MU.log "No valid search strings provided.", MU::ERR return nil end end if groups groups.each { |group| filter = filter |Net::LDAP::Filter.eq("memberOf", group) } end if filter filter = Net::LDAP::Filter.ne("objectclass", "computer") & Net::LDAP::Filter.ne("objectclass", "group") & (filter) else filter = Net::LDAP::Filter.ne("objectclass", "computer") & Net::LDAP::Filter.ne("objectclass", "group") end conn.search( :filter => filter, :base => searchbase, :attributes => getattrs ) do |acct| begin next if users.has_key?(acct[@uid_attr].first) rescue NoMethodError next end if matchgroups and matchgroups.size > 0 next if (acct[:memberOf] & matchgroups).size < 1 end users[acct[@uid_attr].first] = {} users[acct[@uid_attr].first]['dn'] = acct.dn getattrs.each { |attr| begin if acct[attr].size == 1 users[acct[@uid_attr].first][attr] = acct[attr].first else users[acct[@uid_attr].first][attr] = acct[attr] end if attr == "userAccountControl" AD_PW_ATTRS.each_pair { |pw_attr, bitmask| if (bitmask | acct[attr].first.to_i) == acct[attr].first.to_i users[acct[@uid_attr].first][pw_attr] = true end } users[acct[@uid_attr].first][attr] = acct[attr].first.to_i.to_s(2) end end rescue NoMethodError } end # Make all of the Net::BER::BerIdentifiedString leaves in a Hash into # normal strings. # @param tree def self.hashStringify(tree) newtree = nil if tree.is_a?(Hash) newtree = {} tree.each_pair { |key, leaf| newtree[key.to_s] = hashStringify(leaf) } elsif tree.is_a?(Array) newtree = [] tree.each { |leaf| newtree << hashStringify(leaf) } elsif tree.is_a?(Net::BER::BerIdentifiedString) newtree = tree.to_s else newtree = tree end newtree end scrubbed_users = hashStringify(users) scrubbed_users end
Create and return a connection to our directory service. If we've already opened one, return that. @param username [String]: Optional alternative bind user, usually just used to see if someone knows their password @param password [String]: Optional alternative bind password @return [Net::LDAP]
# File modules/mu/master/ldap.rb, line 89 def self.getLDAPConnection(username: nil, password: nil) return @ldap_conn if @ldap_conn validateConfig(skipvaults: (username and password)) if $MU_CFG["ldap"]["type"] == "Active Directory" @gid_attr = "sAMAccountName" @member_attr = "member" @uid_attr = "sAMAccountName" @group_class = "group" @user_class = "user" end if (username and !password) or (password and !username) raise MuLDAPError, "When supply credentials to getLDAPConnection, both username and password must be specified" end if !username and !password bind_creds = MU::Groomer::Chef.getSecret(vault: $MU_CFG["ldap"]["bind_creds"]["vault"], item: $MU_CFG["ldap"]["bind_creds"]["item"]) username = bind_creds[$MU_CFG["ldap"]["bind_creds"]["username_field"]] password = bind_creds[$MU_CFG["ldap"]["bind_creds"]["password_field"]] end @ldap_conn = Net::LDAP.new( :host => $MU_CFG["ldap"]["dcs"].first, :encryption => { :method => :simple_tls, :tls_options => {} }, :port => 636, :base => $MU_CFG["ldap"]["base_dn"], :auth => { :method => :simple, :username => username, :password => password } ) @ldap_conn end
Shorthand for fetching the most recent error on the active LDAP
connection
# File modules/mu/master/ldap.rb, line 344 def self.getLDAPErr return nil if !@ldap_conn return @ldap_conn.get_operation_result.code.to_s+" "+@ldap_conn.get_operation_result.message.to_s end
Approximate a current Microsoft timestamp. They count the number of 100-nanoseconds intervals (1 nanosecond = one billionth of a second) since Jan 1, 1601 UTC.
# File modules/mu/master/ldap.rb, line 352 def self.getMicrosoftTime ms_epoch = DateTime.new(1601,1,1) # this is in milliseconds, so multiply it for the right number of zeroes elapsed = DateTime.now.strftime("%Q").to_i - ms_epoch.strftime("%Q").to_i return elapsed*10000 end
Fetch a list of numeric uids that are already allocated
# File modules/mu/master/ldap.rb, line 132 def self.getUsedUids used_uids = [] if $MU_CFG["ldap"]["type"] == "389 Directory Services" user_filter = Net::LDAP::Filter.ne("objectclass", "computer") & Net::LDAP::Filter.ne("objectclass", "group") conn = getLDAPConnection conn.search( :filter => user_filter, :base => $MU_CFG["ldap"]["base_dn"], :attributes => ["employeeNumber"] ) do |acct| if acct[:employeenumber] and acct[:employeenumber].size > 0 used_uids << acct[:employeenumber].first.to_i end end else Etc.passwd{ |u| if !user.nil? and u.name == user and mu_acct raise MuLDAPError, "Username #{user} already exists as a system user, cannot allocate in directory" end used_uids << u.uid } end used_uids end
Make all of the Net::BER::BerIdentifiedString leaves in a Hash
into normal strings. @param tree
# File modules/mu/master/ldap.rb, line 586 def self.hashStringify(tree) newtree = nil if tree.is_a?(Hash) newtree = {} tree.each_pair { |key, leaf| newtree[key.to_s] = hashStringify(leaf) } elsif tree.is_a?(Array) newtree = [] tree.each { |leaf| newtree << hashStringify(leaf) } elsif tree.is_a?(Net::BER::BerIdentifiedString) newtree = tree.to_s else newtree = tree end newtree end
Intended to run when Mu's local LDAP
server has been created. Use the root credentials to populate our OU structure, create other users, etc. This only needs to understand a 389 Directory style schema, since obviously we're not running Active Directory locally on Linux.
# File modules/mu/master/ldap.rb, line 229 def self.initLocalLDAP validateConfig if $MU_CFG["ldap"]["type"] != "389 Directory Services" or # XXX this should check all of the IPs and hostnames we're known by (!$MU_CFG["ldap"]["dcs"].include?("localhost") and !$MU_CFG["ldap"]["dcs"].include?("127.0.0.1")) MU.log "Custom directory service configured, not initializing bundled schema", MU::NOTICE return end root_creds = MU::Groomer::Chef.getSecret(vault: "mu_ldap", item: "root_dn_user") @ldap_conn = Net::LDAP.new( :host => "127.0.0.1", :encryption => { :method => :simple_tls, :tls_options => {} }, :port => 636, :base => "", :auth => { :method => :simple, :username => root_creds["username"], :password => root_creds["password"] } ) # Manufacture our OU tree and groups [$MU_CFG["ldap"]["base_dn"], "OU=Mu-System,#{$MU_CFG["ldap"]["base_dn"]}", $MU_CFG["ldap"]["user_ou"], $MU_CFG["ldap"]["group_ou"], $MU_CFG["ldap"]["user_group_dn"], $MU_CFG["ldap"]["admin_group_dn"] ].each { |full_dn| dn = "" full_dn.split(/,/).reverse.each { |chunk| if dn.empty? dn = chunk else dn = "#{chunk},#{dn}" end next if chunk.match(/^DC=/i) if chunk.match(/^OU=(.*)/i) ou = $1 if !@ldap_conn.add( :dn => dn, :attributes => { :ou => ou, :objectclass =>"organizationalUnit" } ) and @ldap_conn.get_operation_result.code != 68 # "already exists" MU.log "Error creating #{dn}: "+getLDAPErr, MU::ERR return false elsif @ldap_conn.get_operation_result.code != 68 MU.log "Created OU #{dn}", MU::NOTICE end elsif chunk.match(/^CN=(.*)/i) createGroup($1, full_dn: dn) end } } ["bind_creds", "join_creds"].each { |creds| data = MU::Groomer::Chef.getSecret(vault: $MU_CFG["ldap"][creds]["vault"], item: $MU_CFG["ldap"][creds]["item"]) user_dn = data[$MU_CFG["ldap"][creds]["username_field"]] user_dn.match(/^CN=(.*?),/i) username = $1 pw = data[$MU_CFG["ldap"][creds]["password_field"]] attr = { :cn => username, :displayName => "Mu Service Account", :objectclass => ["top", "person", "organizationalPerson", "inetorgperson"], :uid => username, :mail => $MU_CFG['mu_admin_email'], :givenName => "Mu", :sn => "Service", :userPassword => pw } if !@ldap_conn.add( :dn => data[$MU_CFG["ldap"][creds]["username_field"]], :attributes => attr ) and @ldap_conn.get_operation_result.code != 68 raise MuLDAPError, "Failed to create user #{user_dn} (#{getLDAPErr})" elsif @ldap_conn.get_operation_result.code != 68 MU.log "Created #{username} (#{user_dn})", MU::NOTICE end # Set the password if !@ldap_conn.replace_attribute(user_dn, :userPassword, [pw]) MU.log "Couldn't update password for user #{username}.", MU::ERR, details: getLDAPErr end # Grant this user appropriate privileges targets = [] if creds == "bind_creds" targets << $MU_CFG["ldap"]["user_ou"] targets << $MU_CFG["ldap"]["group_ou"] targets << $MU_CFG["ldap"]["user_group_dn"] targets << $MU_CFG["ldap"]["admin_group_dn"] elsif creds == "join_creds" # XXX Some machine-related OU? end targets.each { | target| aci = "(targetattr=\"*\")(target=\"ldap:///#{target}\")(version 3.0; acl \"#{username} admin privileges for #{target}\"; allow (all) userdn=\"ldap:///#{user_dn}\";)" if !@ldap_conn.modify(:dn => $MU_CFG["ldap"]["base_dn"], :operations => [[:add, :aci, aci]]) and @ldap_conn.get_operation_result.code != 20 MU.log "Couldn't modify permissions for user #{username}.", MU::ERR, details: getLDAPErr elsif @ldap_conn.get_operation_result.code != 20 MU.log "Granted #{username} user admin privileges over #{target}", MU::NOTICE end } } end
@return [Array<String>]
# File modules/mu/master/ldap.rb, line 646 def self.listUsers conn = getLDAPConnection users = {} # XXX why doesn't this work? # group_membership_filter = Net::LDAP::Filter.eq("memberOf", $MU_CFG["ldap"]["admin_group_name"]) | Net::LDAP::Filter.eq("memberOf", $MU_CFG["ldap"]["user_group_name"]) ["admin_group_name", "user_group_name"].each { |group| groupname_filter = Net::LDAP::Filter.eq(@gid_attr, $MU_CFG["ldap"][group]) group_filter = Net::LDAP::Filter.eq("objectClass", @group_class) member_uids = [] conn.search( :filter => Net::LDAP::Filter.join(groupname_filter, group_filter), :attributes => [@member_attr] ) do |item| member_uids = item[@member_attr].map { |u| u.to_s } end member_uids.each { |uid| username_filter = Net::LDAP::Filter.eq(@uid_attr, uid) if $MU_CFG["ldap"]["type"] == "Active Directory" # XXX this is a workaround, as we can't seem to look up the full # DN now for some reason. cn = uid.sub(/^CN=([^,]+?),.*/, "\\1") username_filter = Net::LDAP::Filter.eq("cn", cn) end user_filter = Net::LDAP::Filter.ne("objectclass", "computer") & Net::LDAP::Filter.ne("objectclass", "group") fetchattrs = ["cn", @uid_attr, "displayName", "mail"] fetchattrs << "employeeNumber" if $MU_CFG["ldap"]["type"] == "389 Directory Services" conn.search( :filter => username_filter & user_filter, :base => $MU_CFG["ldap"]["base_dn"], :attributes => fetchattrs ) do |acct| next if users.has_key?(acct[@uid_attr].first) users[acct[@uid_attr].first] = {} users[acct[@uid_attr].first]['dn'] = acct.dn if group == "admin_group_name" users[acct[@uid_attr].first]['admin'] = true else users[acct[@uid_attr].first]['admin'] = false end begin users[acct[@uid_attr].first]['realname'] = acct.displayname.first end rescue NoMethodError begin users[acct[@uid_attr].first]['email'] = acct.mail.first end rescue NoMethodError begin users[acct[@uid_attr].first]['uid'] = acct.employeenumber.first end rescue NoMethodError end } } users end
Add/remove users to/from a group. @param group [String]: The short name of the group @param add_users [Array<String>]: The short names of users to add to the group @param remove_users [Array<String>]: The short names of users to remove from the group
# File modules/mu/master/ldap.rb, line 758 def self.manageGroup(group, add_users: [], remove_users: []) group_dn = findGroups([group], exact: true).first if !group_dn or group_dn.empty? raise MuLDAPError, "Failed to find a Distinguished Name for group #{group}" end if (add_users & remove_users).size > 0 raise MuError, "Can't both add and remove the same user (#{(add_users & remove_users).join(", ")}) from a group" end add_users = findUsers(add_users, exact: true) if add_users.size > 0 remove_users = findUsers(remove_users, exact: true) if remove_users.size > 0 conn = getLDAPConnection if add_users.size > 0 add_users.each_pair { |user, data| uid = user uid = data["dn"] if $MU_CFG["ldap"]["type"] == "Active Directory" if !conn.modify(:dn => group_dn, :operations => [[:add, @member_attr, uid]]) and @ldap_conn.get_operation_result.code != 20 MU.log "Couldn't add user #{user} (#{data['dn']}) to #{@member_attr} of group #{group} (#{group_dn}).", MU::WARN, details: getLDAPErr else MU.log "Added #{user} to group #{group}", MU::NOTICE end } end if remove_users.size > 0 remove_users.each_pair { |user, data| uid = user uid = data["dn"] if $MU_CFG["ldap"]["type"] == "Active Directory" if !conn.modify(:dn => group_dn, :operations => [[:delete, @member_attr, uid]]) MU.log "Couldn't remove user #{user} from group #{group} (#{group_dn}) via #{@member_attr}.", MU::WARN, details: getLDAPErr else MU.log "Removed #{user} from group #{group}", MU::NOTICE end } end end
Call when creating or modifying a user. @param user [String]: The username on which to operate @param password [String]: Set the user's password @param name [String]: Full name of the user @param email [String]: Set the user's email address @param admin [Boolean]: Whether to flag this user as an admin @param unlock [Boolean]: Unlock a locked account (Active Directory) @param mu_acct [Boolean]: Whether to operate on users outside of Mu (generic directory users) @param ou [String]: The OU into which to deposit new users. @param disable [Boolean]: Disabled the user's account @param enable [Boolean]: Re-enable the user's account if it's disabled
# File modules/mu/master/ldap.rb, line 805 def self.manageUser(user, name: nil, password: nil, email: nil, admin: false, mu_acct: true, unlock: false, ou: $MU_CFG["ldap"]["user_ou"], enable: false, disable: false, change_uid: -1) cur_users = listUsers first = last = nil if !name.nil? last = name.split(/\s+/).pop first = name.split(/\s+/).shift end conn = getLDAPConnection # If we're operating on users that aren't specifically Mu users, # fetch generic directory information about them instead of the Mu # user descriptor. if !mu_acct cur_users = findUsers([user], exact: true) end # Oh, Microsoft. Slap quotes around it, convert it to Unicode, and call # it Sally. *Then* it's a password. password_attr = :userPassword if !password.nil? and $MU_CFG["ldap"]["type"] == "Active Directory" password = ('"'+password+'"').encode("utf-16le").force_encoding("utf-8") password_attr = :unicodePwd end ok = true if !cur_users.has_key?(user) # Creating a new user if canWriteLDAP? if password.nil? or email.nil? or name.nil? raise MuLDAPError, "Missing one or more required fields (name, password, email) creating new user #{user}" end user_dn = "CN=#{name},#{ou}" conn = getLDAPConnection attr = { :cn => name, :displayName => name, :givenName => first, :sn => last, :mail => email } attr[password_attr] = password gid = nil groups = [] if $MU_CFG["ldap"]["type"] == "389 Directory Services" attr[:objectclass] = ["top", "person", "organizationalPerson", "inetorgperson"] attr[:uid] = user if change_uid > 0 used_uids = getUsedUids if used_uids.include?(change_uid) raise MuLDAPError, "Uid #{change_uid} is unavailable, cannot allocate to user #{user}" end MU.log "Forcing uid #{change_uid} to user #{user}", MU::NOTICE, details: used_uids attr[:employeeNumber] = change_uid.to_s else attr[:employeeNumber] = allocateUID end if mu_acct gid = createGroup("#{user}.mu-user") groups << "#{user}.mu-user" else gid = createGroup(user) groups << user end attr[:departmentNumber] = gid elsif $MU_CFG["ldap"]["type"] == "Active Directory" attr[:objectclass] = ["user"] attr[:samaccountname] = user attr[:userAccountControl] = AD_PW_ATTRS['normal'].to_s attr[:userPrincipalName] = "#{user}@#{$MU_CFG["ldap"]["domain_name"]}" attr[:pwdLastSet] = "-1" attr.delete(:userPassword) if mu_acct attr[:userAccountControl] = (attr[:userAccountControl].to_i & AD_PW_ATTRS['pwdNeverExpires']).to_s end if disable attr[:userAccountControl] = (attr[:userAccountControl].to_i & AD_PW_ATTRS['disable']).to_s end end if !conn.add(:dn => user_dn, :attributes => attr) if getLDAPErr.match(/53 Unwilling to perform/) raise MuLDAPError, "Failed to create user #{user} (#{getLDAPErr}). Most likely the LDAP password policy objected to the password '#{password}'" else raise MuLDAPError, "Failed to create user #{user} (#{getLDAPErr}) from add(:dn => #{user_dn}, :attributes => #{attr.to_s})" end end attr[password_attr] = "********" MU.log "Created new LDAP user #{user}", details: attr if mu_acct groups << $MU_CFG["ldap"]["user_group_name"] groups << $MU_CFG["ldap"]["admin_group_name"] if admin end groups.each { |group| manageGroup(group, add_users: [user]) } wait = 10 begin %x{/usr/bin/getent passwd ; /usr/bin/getent group} # winbind is slow sometimes Etc.getpwnam(user) rescue ArgumentError if wait >= 30 MU.log "User #{user} has been created in LDAP, but local system can't see it. Are PAM/LDAP configured correctly?", MU::ERR return false end MU.log "User #{user} has been created in LDAP, but not yet visible to local system, waiting #{wait}s and checking again.", MU::WARN sleep wait wait = wait + 5 retry end if user != "mu" %x{/sbin/restorecon -r /home} # SELinux stupidity that oddjob misses MU::Master.setLocalDataPerms(user) if Etc.getpwuid(Process.uid).name == "root" and mu_acct else MU.log "We are in read-only LDAP mode. You must first create #{user} in your directory and add it to #{$MU_CFG["ldap"]["user_group_dn"]}. If the user is intended to be an admin, also add it to #{$MU_CFG["ldap"]["admin_group_dn"]}.", MU::WARN return true end else gid = MU::Master.setLocalDataPerms(user) if Etc.getpwuid(Process.uid).name == "root" and mu_acct # Modifying an existing user if canWriteLDAP? conn = getLDAPConnection user_dn = cur_users[user]['dn'] if $MU_CFG["ldap"]["type"] == "389 Directory Services" # Make sure we have a sensible default gid conn.replace_attribute(user_dn, :departmentNumber, gid.to_s) if change_uid > 0 used_uids = getUsedUids if used_uids.include?(change_uid) raise MuLDAPError, "Uid #{change_uid} is unavailable, cannot allocate to user #{user}" end MU.log "Forcing uid #{change_uid} to user #{user}", MU::NOTICE, details: used_uids conn.replace_attribute(user_dn, :employeeNumber, change_uid.to_s) end end if !name.nil? and cur_users[user]['realname'] != name MU.log "Updating display name for #{user} to #{name}", MU::NOTICE conn.replace_attribute(user_dn, :displayName, name) conn.replace_attribute(user_dn, :givenName, first) conn.replace_attribute(user_dn, :sn, last) cur_users[user]['realname'] = name end if disable findUsers([user], exact: true) MU.log "Disabling #{user}", MU::WARN conn.replace_attribute(user_dn, :userAccountControl, AD_PW_ATTRS['disable'].to_i.to_s(2)) elsif enable user_props = findUsers([user], exact: true) MU.log "Re-enabling #{user}", MU::NOTICE uac = (("0b"+user_props[user]["userAccountControl"]).to_i & AD_PW_ATTRS['disable']) conn.replace_attribute(user_dn, :userAccountControl, uac.to_s(2)) end if unlock conn.replace_attribute(user_dn, :lockoutTime, "0") end if !email.nil? and cur_users[user]['email'] != email MU.log "Updating email for #{user} to #{email}", MU::NOTICE conn.replace_attribute(user_dn, :mail, email) cur_users[user]['email'] = email end if !password.nil? MU.log "Updating password for #{user}", MU::NOTICE if !conn.replace_attribute(user_dn, password_attr, [password]) MU.log "Couldn't update password for user #{user}.", MU::WARN, details: getLDAPErr ok = false end end if admin and !cur_users[user]['admin'] MU.log "Granting Mu admin privileges to #{user}", MU::NOTICE manageGroup($MU_CFG["ldap"]["admin_group_name"], add_users: [user]) elsif !admin and cur_users[user]['admin'] MU.log "Revoking Mu admin privileges from #{user}", MU::NOTICE manageGroup($MU_CFG["ldap"]["admin_group_name"], remove_users: [user]) end else MU.log "We are in read-only LDAP mode. You must manage #{user} in your directory.", MU::WARN ok = false end end return ok if !mu_acct # everything below is Mu-specific cur_users = listUsers if cur_users.has_key?(user) ["realname", "email", "monitoring_email"].each { |field| next if !cur_users[user].has_key?(field) File.open($MU_CFG['datadir']+"/users/#{user}/#{field}", File::CREAT|File::RDWR, 0640) { |f| f.puts cur_users[user][field] } } else MU.log "Load of current user list didn't include #{user}, even though we just created them!", MU::WARN end MU::Master.setLocalDataPerms(user) if Etc.getpwuid(Process.uid).name == "root" and mu_acct ok end
Make sure the LDAP
section of $MU_CFG makes sense.
# File modules/mu/master/ldap.rb, line 27 def self.validateConfig(skipvaults: false) ok = true supported = ["Active Directory", "389 Directory Services"] if !$MU_CFG raise MuLDAPError, "Configuration not loaded yet, but MU::Master::LDAP.validateConfig was called!" end if !$MU_CFG.has_key?("ldap") raise MuLDAPError "Missing 'ldap' section of config (files: #{$MU_CFG['config_files']})" end ldap = $MU_CFG["ldap"] # shorthand if !ldap.has_key?("type") or !supported.include?(ldap["type"]) ok = false MU.log "Bad or missing 'type' of LDAP server (should be one of #{supported})", MU::ERR end ["base_dn", "user_ou", "domain_name", "domain_netbios_name", "user_group_dn", "user_group_name", "admin_group_dn", "admin_group_name"].each { |var| if !ldap.has_key?(var) or !ldap[var].is_a?(String) ok = false MU.log "LDAP config section parameter '#{var}' is missing or is not a String", MU::ERR end } if !ldap.has_key?("dcs") or !ldap["dcs"].is_a?(Array) or ldap["dcs"].size < 1 ok = false MU.log "Missing or empty 'dcs' section of LDAP config" end ["bind_creds", "join_creds"].each { |creds| if !ldap.has_key?(creds) or !ldap[creds].is_a?(Hash) or !ldap[creds].has_key?("vault") or !ldap[creds].has_key?("item") or !ldap[creds].has_key?("username_field") or !ldap[creds].has_key?("password_field") MU.log "LDAP config subsection '#{creds}' misconfigured, should be hash containing: vault, item, username_field, password_field", MU::ERR ok = false next end if !skipvaults loaded = MU::Groomer::Chef.getSecret(vault: ldap[creds]["vault"], item: ldap[creds]["item"]) if !loaded or !loaded.has_key?(ldap[creds]["username_field"]) or loaded[ldap[creds]["username_field"]].empty? or !loaded.has_key?(ldap[creds]["password_field"]) or loaded[ldap[creds]["password_field"]].empty? MU.log "LDAP config subsection '#{creds}' refers to a bogus vault or incorrect/missing item fields", MU::ERR, details: ldap[creds] ok = false end end } if !ok raise MuLDAPError, "One or more LDAP configuration errors from files #{$MU_CFG['config_files']}" end end