class ASF::Person

An instance of this class represents a person. Data comes from a variety of sources: LDAP, asf--authorization-template, iclas.txt, members.txt, nominated-members.txt, and potential-member-watch-list.txt.

Constants

SUFFIXES

generational suffixes

Public Class Methods

[](id) click to toggle source

return person only if it actually exits

Calls superclass method ASF::Base.[]
# File lib/whimsy/asf/ldap.rb, line 596
def self.[] id
  person = super
  person.attrs['dn'] ? person : nil
end
add(attrs) click to toggle source

add a new person to LDAP. Attrs must include uid, cn, and mail

# File lib/whimsy/asf/ldap.rb, line 744
def self.add(attrs)
  # convert keys to strings
  attrs = attrs.map {|key, value| [key.to_s, value]}.to_h

  # verify required arguments are present
  %w(uid cn mail).each do |required|
    unless attrs.include? required
      raise ArgumentError.new("missing attribute #{required}")
    end
  end

  availid = attrs['uid']

  # determine next uid and group
  nextuid = ASF::search_one(ASF::Person.base, 'uid=*', 'uidNumber').
    flatten.map(&:to_i).max + 1

  nextgid = ASF::search_one(group_base, 'cn=*', 'gidNumber').
    flatten.map(&:to_i).max + 1
 
  # fixed attributes
  attrs.merge!({
    'uidNumber' => nextuid.to_s,
    'gidNumber' => nextuid.to_s,
    'asf-committer-email' => "#{availid}@apache.org",
    'objectClass' => %w(person top posixAccount organizationalPerson
       inetOrgPerson asf-committer hostObject ldapPublicKey)
  })

  # defaults
  attrs['loginShell'] ||= '/usr/local/bin/bash'
  attrs['homeDirectory'] ||= "/home/#{availid}"
  attrs['host'] ||= "home.apache.org"
  attrs['asf-sascore'] ||= "10"

  # parse name
  attrs = ASF::Person.ldap_name(attrs['cn']).merge(attrs)

  # generate a password that is between 8 and 16 alphanumeric characters
  if not attrs['userPassword']
    while attrs['userPassword'].to_s.length < 8
      attrs['userPassword'] = SecureRandom.base64(12).gsub(/\W+/, '')
    end
  end

  # create new LDAP group
  entry = [
    mod_add('objectClass', ['posixGroup', 'top']),
    mod_add('cn', availid),
    mod_add('userPassword', '{crypt}*'),
    mod_add('gidNumber', nextgid.to_s),
  ]

  ASF::LDAP.add("cn=#{availid},#{group_base}", entry)

  # create new LDAP person
  begin
    entry = attrs.map {|key, value| mod_add(key, value)}
    ASF::LDAP.add("uid=#{availid},#{base}", entry)
  rescue
    # don't leave an orphan group behind
    ASF::LDAP.delete("cn=#{availid},#{group_base}") rescue nil
    raise
  end

  # return person object with password filled in
  person = ASF::Person.find(availid)
  person.attrs['userPassword'] = [attrs['userPassword']]
  person
end
asciize(name) click to toggle source

sort support

# File lib/whimsy/asf/person.rb, line 14
def self.asciize(name)
  if name.match /[^\x00-\x7F]/
    # digraphs.  May be culturally sensitive
    name.gsub! /\u00df/, 'ss'
    name.gsub! /\u00e4|a\u0308/, 'ae'
    name.gsub! /\u00e5|a\u030a/, 'aa'
    name.gsub! /\u00e6/, 'ae'
    name.gsub! /\u00f1|n\u0303/, 'ny'
    name.gsub! /\u00f6|o\u0308/, 'oe'
    name.gsub! /\u00fc|u\u0308/, 'ue'

    # latin 1
    name.gsub! /\u00c9/, 'e'
    name.gsub! /\u00d3/, 'o'
    name.gsub! /[\u00e0-\u00e5]/, 'a'
    name.gsub! /\u00e7/, 'c'
    name.gsub! /[\u00e8-\u00eb]/, 'e'
    name.gsub! /[\u00ec-\u00ef]/, 'i'
    name.gsub! /[\u00f2-\u00f6]|\u00f8/, 'o'
    name.gsub! /[\u00f9-\u00fc]/, 'u'
    name.gsub! /[\u00fd\u00ff]/, 'y'

    # Latin Extended-A
    name.gsub! /[\u0100-\u0105]/, 'a'
    name.gsub! /[\u0106-\u010d]/, 'c'
    name.gsub! /[\u010e-\u0111]/, 'd'
    name.gsub! /[\u0112-\u011b]/, 'e'
    name.gsub! /[\u011c-\u0123]/, 'g'
    name.gsub! /[\u0124-\u0127]/, 'h'
    name.gsub! /[\u0128-\u0131]/, 'i'
    name.gsub! /[\u0132-\u0133]/, 'ij'
    name.gsub! /[\u0134-\u0135]/, 'j'
    name.gsub! /[\u0136-\u0138]/, 'k'
    name.gsub! /[\u0139-\u0142]/, 'l'
    name.gsub! /[\u0143-\u014b]/, 'n'
    name.gsub! /[\u014C-\u0151]/, 'o'
    name.gsub! /[\u0152-\u0153]/, 'oe'
    name.gsub! /[\u0154-\u0159]/, 'r'
    name.gsub! /[\u015a-\u0162]/, 's'
    name.gsub! /[\u0162-\u0167]/, 't'
    name.gsub! /[\u0168-\u0173]/, 'u'
    name.gsub! /[\u0174-\u0175]/, 'w'
    name.gsub! /[\u0176-\u0178]/, 'y'
    name.gsub! /[\u0179-\u017e]/, 'z'

    # denormalized diacritics
    name.gsub! /[\u0300-\u036f]/, ''
  end

  name.strip.gsub /[^\w]+/, '-'
end
find_by_email(value) click to toggle source

find a Person by email address

# File lib/whimsy/asf/mail.rb, line 133
def self.find_by_email(value)
  value.downcase!

  person = Mail.list[value]
  return person if person
end
group_base() click to toggle source
# File lib/whimsy/asf/ldap.rb, line 558
def self.group_base
  'ou=people,' + ASF::Group.base
end
ldap_name(name) click to toggle source

parse a name into LDAP fields

# File lib/whimsy/asf/person.rb, line 83
def self.ldap_name(name)
  words = name.gsub(',', '').split(' ')
  result = {'cn' => name}
  result['title'] = words.shift if words.first == 'Dr.'
  result['title'] ||= words.pop if words.last =~ /^Ph\.D\.?/
  result['generationQualifier'] = words.pop if words.last =~ SUFFIXES
  result['givenName'] = words.first
  result['sn'] = words.last
  result
end
list(filter='uid=*') click to toggle source

Obtain a list of people known to LDAP. LDAP filters may be used to retrieve only a subset.

# File lib/whimsy/asf/ldap.rb, line 564
def self.list(filter='uid=*')
  ASF.search_one(base, filter, 'uid').flatten.map {|uid| find(uid)}
end
member_nominees() click to toggle source

Return a hash of nominated members. Keys are ASF::Person objects, values are the nomination text.

# File lib/whimsy/asf/nominees.rb, line 9
def self.member_nominees
  begin
    return Hash[@member_nominees.to_a] if @member_nominees
  rescue
  end

  meetings = ASF::SVN['private/foundation/Meetings']
  nominations = Dir["#{meetings}/*/nominated-members.txt"].sort.last.untaint

  nominations = File.read(nominations).split(/^\s*---+--\s*/)
  nominations.shift(2)

  nominees = {}
  nominations.each do |nomination|
    id = nomination[/^\s?\w+.*<(\S+)@apache.org>/,1]
    id ||= nomination[/^\s?\w+.*\((\S+)@apache.org\)/,1]
    id ||= nomination[/^\s?\w+.*\(([a-z]+)\)/,1]

    next unless id

    nominees[find(id)] = nomination
  end

  @member_nominees = WeakRef.new(nominees)
  nominees
end
member_watch_list() click to toggle source

Return a hash of individuals in the member watch list. Keys are ASF::Person objects, values are the text from potential-member-watch-list.txt..

# File lib/whimsy/asf/watch.rb, line 8
def self.member_watch_list
  return @member_watch_list if @member_watch_list

  foundation = ASF::SVN['private/foundation']
  text = File.read "#{foundation}/potential-member-watch-list.txt"

  nominations = text.scan(/^\s+\*\)\s+\w.*?\n\s*(?:---|\Z)/m)

  i = 0
  member_watch_list = {}
  nominations.each do |nomination|
    id = nil
    name = nomination[/\*\)\s+(.+?)\s+(\(|\<|$)/,1]
    id ||= nomination[/\*\)\s.+?\s\((.*?)\)/,1]
    id ||= nomination[/\*\)\s.+?\s<(.*?)@apache.org>/,1]

    unless id
      id = "notinavail_#{i+=1}"
      find(id).attrs['cn'] = name
    end

    member_watch_list[find(id)] = nomination
  end

  @member_watch_list = member_watch_list
end
preload(attributes, people=[]) click to toggle source

pre-fetch a given set of attributes, for a given list of people

# File lib/whimsy/asf/ldap.rb, line 569
def self.preload(attributes, people=[])
  list = Hash.new {|hash, name| hash[name] = find(name)}

  attributes = [attributes].flatten

  if people.empty? or people.length > 1000
    filter = "(|#{attributes.map {|attribute| "(#{attribute}=*)"}.join})"
  else
    filter = "(|#{people.map {|person| "(uid=#{person.name})"}.join})"
  end
  
  zero = Hash[attributes.map {|attribute| [attribute,nil]}]

  data = ASF.search_one(base, filter, attributes + ['uid'])
  data = Hash[data.map! {|hash| [list[hash['uid'].first], hash]}]
  data.each {|person, hash| person.attrs.merge!(zero.merge(hash))}

  if people.empty?
    (list.values - data.keys).each do |person|
      person.attrs.merge! zero
    end
  end

  list.values
end
remove(availid) click to toggle source

remove a person from LDAP

# File lib/whimsy/asf/ldap.rb, line 816
def self.remove(availid)
  ASF::LDAP.delete("cn=#{availid},#{group_base}")
  ASF::LDAP.delete("uid=#{availid},#{base}")
end
sortable_name(name) click to toggle source

rearrange line in an order suitable for sorting

# File lib/whimsy/asf/person.rb, line 70
def self.sortable_name(name)
  name = name.split.reverse
  suffix = (name.shift if name.first =~ SUFFIXES)
  suffix += ' ' + name.shift if name.first =~ SUFFIXES
  name << name.shift
  # name << name.shift if name.first=='van'
  name.last.sub! /^IJ/, 'Ij'
  name.unshift(suffix) if suffix
  name.map! {|word| asciize(word)}
  name.reverse.join(' ').downcase
end

Public Instance Methods

active_emails() click to toggle source

Active emails: primary email address, alt email addresses, and member email addresses.

# File lib/whimsy/asf/mail.rb, line 156
def active_emails
  (mail + alt_email + member_emails).uniq
end
all_mail() click to toggle source

All known email addresses: includes active, obsolete, and apache.org email addresses.

# File lib/whimsy/asf/mail.rb, line 162
def all_mail
  (active_emails + obsolete_emails + ["#{id}@apache.org"]).uniq
end
alt_email() click to toggle source

list all of the alternative emails for this person

# File lib/whimsy/asf/ldap.rb, line 645
def alt_email
  attrs['asf-altEmail'] || []
end
asf_banned?() click to toggle source

determine if the person has asf-banned: yes. If scanning a large list, consider preloading the asf-banned attributes for these people.

# File lib/whimsy/asf/ldap.rb, line 634
def asf_banned?
  # No idea what this means (yet)
  attrs['asf-banned'] == 'yes'
end
asf_committer?() click to toggle source

Is this person listed in the committers LDAP group?

# File lib/whimsy/asf/ldap.rb, line 614
def asf_committer?
   ASF::Group.new('committers').include? self
end
asf_member?() click to toggle source

Returns true if this person is listed as an ASF member in either LDAP or members.txt.

# File lib/whimsy/asf/person.rb, line 123
def asf_member?
  ASF::Member.status[name] or ASF.members.include? self
end
asf_officer_or_member?() click to toggle source

Returns true if this person is listed as an ASF member in either LDAP or members.txt or this person is listed as an PMC chair in LDAP.

# File lib/whimsy/asf/person.rb, line 130
def asf_officer_or_member?
  asf_member? or ASF.pmc_chairs.include? self
end
attrs() click to toggle source

list of LDAP attributes for this person, populated lazily upon first reference.

# File lib/whimsy/asf/ldap.rb, line 603
def attrs
  @attrs ||= LazyHash.new {ASF.search_one(base, "uid=#{name}").first}
end
auth() click to toggle source

return a list of ASF authorizations that contain this individual

# File lib/whimsy/asf/auth.rb, line 76
def auth
  @auths ||= ASF::Authorization.find_by_id(name)
end
banned?() click to toggle source

determine if the person is banned. If scanning a large list, consider preloading the loginShell attributes for these people.

# File lib/whimsy/asf/ldap.rb, line 620
def banned?
  # FreeBSD uses /usr/bin/false; Ubuntu uses /bin/false
  not attrs['loginShell'] or %w(/bin/false bin/nologin bin/no-cla).any? {|a| attrs['loginShell'].first.include? a}
end
committees() click to toggle source

list of LDAP committees that this individual is a member of

# File lib/whimsy/asf/ldap.rb, line 665
def committees
  # legacy LDAP entries
  committees = weakref(:committees) do
    Committee.list("member=uid=#{name},#{base}")
  end

  # add in projects (currently only includes GUINEAPIGS)
  # Get list of project names where the person is an owner
  projects = self.projects.select{|prj| prj.owners.include? self}.map(&:name)
  committees += ASF::Committee.pmcs.select do |pmc| 
    projects.include? pmc.name
  end

  # dedup
  committees.uniq
end
createTimestamp() click to toggle source

determine account creation date. Notes:

  • LDAP info is not accurate for dates prior to 2009. See person/override-dates.rb

  • createTimestamp isn't loaded by default (but can either be preloaded or fetched explicitly)

# File lib/whimsy/asf/person.rb, line 104
def createTimestamp
  result = @@create_date[name] 
  result ||= attrs['createTimestamp'][0] rescue nil # in case not loaded
  result ||= ASF.search_one(base, "uid=#{name}", 'createTimestamp')[0][0]
  result
end
dn() click to toggle source

Designated Name from LDAP

# File lib/whimsy/asf/ldap.rb, line 704
def dn
  "uid=#{name},#{ASF::Person.base}"
end
groups() click to toggle source

list of LDAP groups that this individual is a member of

# File lib/whimsy/asf/ldap.rb, line 690
def groups
  weakref(:groups) do
    Group.list("memberUid=#{name}")
  end
end
icla() click to toggle source

ASF::ICLA information for this person.

# File lib/whimsy/asf/icla.rb, line 202
def icla
  @icla ||= ASF::ICLA.find_by_id(name)
end
icla=(icla) click to toggle source

setter for icla, should only be used by ASF::ICLA.preload

# File lib/whimsy/asf/icla.rb, line 207
def icla=(icla)
  @icla = icla
end
icla?() click to toggle source

does this individual have an ICLA on file?

# File lib/whimsy/asf/icla.rb, line 212
def icla?
  @icla || ICLA.availids.include?(name)
end
mail() click to toggle source

primary mail addresses

# File lib/whimsy/asf/ldap.rb, line 640
def mail
  attrs['mail'] || []
end
member_emails() click to toggle source

email addresses from members.txt

# File lib/whimsy/asf/member.rb, line 176
def member_emails
  ASF::Member.emails(members_txt)
end
member_name() click to toggle source

Person's name as found in members.txt

# File lib/whimsy/asf/member.rb, line 181
def member_name
  members_txt[/(\w.*?)\s*(\/|$)/, 1] if members_txt
end
member_nomination() click to toggle source

Return the member nomination text for this individual

# File lib/whimsy/asf/nominees.rb, line 37
def member_nomination
  @member_nomination ||= Person.member_nominees[self]
end
member_watch() click to toggle source

This person's entry in potential-member-watch-list.txt.

# File lib/whimsy/asf/watch.rb, line 36
def member_watch
  text = Person.member_watch_list[self]
  if text
    text.sub!(/\A\s*\n/,'')
    text.sub!(/\n---\Z/,'')
  end
  text
end
members_txt(full = false) click to toggle source

text entry from members.txt. If full is true, this will also include the text delimiters.

# File lib/whimsy/asf/member.rb, line 169
def members_txt(full = false)
  prefix, suffix = " *) ", "\n\n" if full
  @members_txt ||= ASF::Member.find_text_by_id(id)
  "#{prefix}#{@members_txt}#{suffix}" if @members_txt
end
method_missing(name, *args) click to toggle source

Allow artibtrary LDAP attibutes to be referenced as object properties. Example: ASF::Person.find('rubys').cn. Can also be used to modify an LDAP attribute.

Calls superclass method
# File lib/whimsy/asf/ldap.rb, line 711
def method_missing(name, *args)
  if name.to_s.end_with? '=' and args.length == 1
    return modify(name.to_s[0..-2], args)
  end

  return super unless args.empty?
  result = self.attrs[name.to_s]
  return super unless result

  if result.empty?
    return nil
  else
    result.map! do |value|
      value = value.dup.force_encoding('utf-8') if String === value
      value
    end

    if result.length == 1
      result.first
    else
      result
    end
  end
end
modify(attr, value) click to toggle source

update an LDAP attribute for this person. This needs to be run either inside or after ASF::LDAP.bind.

# File lib/whimsy/asf/ldap.rb, line 738
def modify(attr, value)
  ASF::LDAP.modify(self.dn, [ASF::Base.mod_replace(attr.to_s, value)])
  attrs[attr.to_s] = value
end
nologin?() click to toggle source

determine if the person has no login. If scanning a large list, consider preloading the loginShell attributes for these people.

# File lib/whimsy/asf/ldap.rb, line 627
def nologin?
  # FreeBSD uses /usr/bin/false; Ubuntu uses /bin/false
  not attrs['loginShell'] or %w(/bin/false bin/nologin bin/no-cla).any? {|a| attrs['loginShell'].first.include? a}
end
obsolete_emails() click to toggle source

List of inactive email addresses: currently only contains the address in iclas.txt if it is not contained in the list of active email addresses.

# File lib/whimsy/asf/mail.rb, line 143
def obsolete_emails
  return @obsolete_emails if @obsolete_emails
  result = []
  if icla
    unless active_emails.any? {|mail| mail.downcase == icla.email.downcase}
      result << icla.email
    end
  end
  @obsolete_emails = result
end
pgp_key_fingerprints() click to toggle source

list all of the PGP key fingerprints

# File lib/whimsy/asf/ldap.rb, line 650
def pgp_key_fingerprints
  attrs['asf-pgpKeyFingerprint'] || []
end
projects() click to toggle source

list of LDAP projects that this individual is a member of

# File lib/whimsy/asf/ldap.rb, line 683
def projects
  weakref(:projects) do
    Project.list("member=uid=#{name},#{base}")
  end
end
public_name() click to toggle source

return person's public name, searching a variety of sources, starting with iclas.txt, then LDAP, and finally the archives.

# File lib/whimsy/asf/person.rb, line 113
def public_name
  return icla.name if icla
  cn = [attrs['cn']].flatten.first
  cn.force_encoding('utf-8') if cn.respond_to? :force_encoding
  return cn if cn
  ASF.search_archive_by_id(name)
end
reload!() click to toggle source

reload all attributes from LDAP

# File lib/whimsy/asf/ldap.rb, line 608
def reload!
  @attrs = nil
  attrs
end
services() click to toggle source

list of LDAP services that this individual is a member of

# File lib/whimsy/asf/ldap.rb, line 697
def services
  weakref(:services) do
    Service.list("member=#{dn}")
  end
end
sortable_name() click to toggle source

return name in a sortable order (last name first)

# File lib/whimsy/asf/person.rb, line 95
def sortable_name
  Person.sortable_name(self.public_name)
end
ssh_public_keys() click to toggle source

list all of the ssh public keys

# File lib/whimsy/asf/ldap.rb, line 655
def ssh_public_keys
  attrs['sshPublicKey'] || []
end
urls() click to toggle source

list all of the personal URLs

# File lib/whimsy/asf/ldap.rb, line 660
def urls
  attrs['asf-personalURL'] || []
end