class MU::Master::LDAP

Routines for manipulating users and groups in 389 Directory Services or Active Directory.

Constants

AD_PW_ATTRS

See technet.microsoft.com/en-us/library/ee198831.aspx

Public Class Methods

allocateGID(group: nil) click to toggle source

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
allocateUID() click to toggle source

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
authorize(username, password, require_group: nil) click to toggle source

Authenticate a user against our directory, optionally requiring them to be a member of a particular group in order to return true. @param username [String]: The bare username of the user to authorize @param password [String]: The user's password @return [Boolean]

# File modules/mu/master/ldap.rb, line 614
def self.authorize(username, password, require_group: nil)
  auth = nil

  begin
    # see if this user/pw combo works
    conn = getLDAPConnection(username: username, password: password)
    auth = conn.auth(username, password) if username and password
  rescue Net::LDAP::LdapError
    return false
  end
  if !conn.bind(auth)
    MU.log conn.get_operation_result.message, MU::ERR
    return false
  end
  
  return true if !require_group

  shortuser = username.sub(/\@.*/, "")
  user = findUsers([shortuser], exact: true)
  if user[shortuser]["memberOf"].is_a?(Array)
    user[shortuser]["memberOf"].each { |group|
      shortname = group.sub(/^CN=(.*?),.*/, '\1')
      return true if shortname == require_group
    }
  elsif user[shortuser]["memberOf"].is_a?(String)
    shortname = user[shortuser]["memberOf"].sub(/^CN=(.*?),.*/, '\1')
    return true if shortname == require_group
  end
  return false
end
canWriteLDAP?() click to toggle source

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
convertMicrosoftTime(stamp) click to toggle source

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
createGroup(group, full_dn: nil) click to toggle source

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
deleteUser(user) click to toggle source

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
dropLDAPConnection() click to toggle source

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
findGroups(search = [], exact: false, searchbase: $MU_CFG['ldap']['base_dn']) click to toggle source

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
findUsers(search = [], exact: false, searchbase: $MU_CFG['ldap']['base_dn'], extra_attrs: [], matchgroups: []) click to toggle source

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
getLDAPConnection(username: nil, password: nil) click to toggle source

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
getLDAPErr() click to toggle source

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
getMicrosoftTime() click to toggle source

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
getUsedUids() click to toggle source

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
hashStringify(tree) click to toggle source

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
initLocalLDAP() click to toggle source

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
listUsers() click to toggle source

@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
manageGroup(group, add_users: [], remove_users: []) click to toggle source

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
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) click to toggle source

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
validateConfig(skipvaults: false) click to toggle source

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