From c50d88536a4feb3087d1aa802e110250cb2861fc Mon Sep 17 00:00:00 2001
From: jgg <>
Date: Wed, 22 Sep 1999 03:19:03 +0000
Subject: [PATCH 1/1] Initial import
---
doc/apache-config.txt | 15 ++
doc/makefile | 11 +
doc/samples/ud-generate | 2 +
doc/samples/ud-replicate | 3 +
doc/slapd-config.txt | 45 ++++
doc/ud-generate.8.yo | 48 ++++
doc/ud-gpgimport.8.yo | 67 +++++
doc/ud-info.1.yo | 151 +++++++++++
doc/ud-useradd.8.yo | 107 ++++++++
doc/ud-userimport.8.yo | 76 ++++++
doc/ud-xearth.1.yo | 34 +++
templates/list-subscribe | 4 +
templates/passwd-changed | 14 ++
templates/ping-reply | 11 +
templates/welcome-message-60000 | 37 +++
templates/welcome-message-800 | 92 +++++++
ud-arbimport | 46 ++++
ud-emailmatcher | 172 +++++++++++++
ud-forwardlist | 68 +++++
ud-generate | 253 +++++++++++++++++++
ud-gpgimport | 234 +++++++++++++++++
ud-homecheck | 16 ++
ud-info | 359 ++++++++++++++++++++++++++
ud-ldapshow | 96 +++++++
ud-mailgate | 170 +++++++++++++
ud-passchk | 47 ++++
ud-replicate | 12 +
ud-useradd | 248 ++++++++++++++++++
ud-userimport | 238 ++++++++++++++++++
ud-xearth | 80 ++++++
userdir-ldap.conf | 51 ++++
userdir_gpg.py | 432 ++++++++++++++++++++++++++++++++
userdir_ldap.py | 170 +++++++++++++
web/Util.pm | 271 ++++++++++++++++++++
web/domains.tab | 255 +++++++++++++++++++
web/fetchkey.cgi | 27 ++
web/login.cgi | 52 ++++
web/login.html | 16 ++
web/logout.cgi | 23 ++
web/search.cgi | 255 +++++++++++++++++++
web/searchform.html | 277 ++++++++++++++++++++
web/searchhelp.html | 18 ++
web/searchresults.html | 37 +++
web/settings.cfg | 26 ++
web/update.cgi | 152 +++++++++++
web/update.html | 362 ++++++++++++++++++++++++++
46 files changed, 5180 insertions(+)
create mode 100644 doc/apache-config.txt
create mode 100644 doc/makefile
create mode 100644 doc/samples/ud-generate
create mode 100644 doc/samples/ud-replicate
create mode 100644 doc/slapd-config.txt
create mode 100644 doc/ud-generate.8.yo
create mode 100644 doc/ud-gpgimport.8.yo
create mode 100644 doc/ud-info.1.yo
create mode 100644 doc/ud-useradd.8.yo
create mode 100644 doc/ud-userimport.8.yo
create mode 100644 doc/ud-xearth.1.yo
create mode 100644 templates/list-subscribe
create mode 100644 templates/passwd-changed
create mode 100644 templates/ping-reply
create mode 100644 templates/welcome-message-60000
create mode 100644 templates/welcome-message-800
create mode 100755 ud-arbimport
create mode 100755 ud-emailmatcher
create mode 100755 ud-forwardlist
create mode 100755 ud-generate
create mode 100755 ud-gpgimport
create mode 100755 ud-homecheck
create mode 100755 ud-info
create mode 100755 ud-ldapshow
create mode 100755 ud-mailgate
create mode 100755 ud-passchk
create mode 100755 ud-replicate
create mode 100755 ud-useradd
create mode 100755 ud-userimport
create mode 100755 ud-xearth
create mode 100644 userdir-ldap.conf
create mode 100644 userdir_gpg.py
create mode 100644 userdir_ldap.py
create mode 100644 web/Util.pm
create mode 100644 web/domains.tab
create mode 100755 web/fetchkey.cgi
create mode 100755 web/login.cgi
create mode 100644 web/login.html
create mode 100755 web/logout.cgi
create mode 100755 web/search.cgi
create mode 100644 web/searchform.html
create mode 100644 web/searchhelp.html
create mode 100644 web/searchresults.html
create mode 100644 web/settings.cfg
create mode 100755 web/update.cgi
create mode 100644 web/update.html
diff --git a/doc/apache-config.txt b/doc/apache-config.txt
new file mode 100644
index 0000000..461eb5b
--- /dev/null
+++ b/doc/apache-config.txt
@@ -0,0 +1,15 @@
+To setup apache for use with the web database access scripts use:
+
+
+ ServerAdmin webmaster@mydomain.com
+ DocumentRoot /var/www/userdir-ldap
+ ServerName db.mydomain.com
+ DirectoryIndex /search.cgi
+
+
+
+ Options +ExecCGI
+ AllowOverride All
+ AddHandler cgi-script .cgi
+
+
diff --git a/doc/makefile b/doc/makefile
new file mode 100644
index 0000000..4e314b3
--- /dev/null
+++ b/doc/makefile
@@ -0,0 +1,11 @@
+.SILENT:
+
+MANPAGES = ud-generate.8 ud-gpgimport.8 ud-info.1 ud-xearth.1 ud-useradd.8 \
+ ud-userimport.8
+
+all: $(MANPAGES)
+
+$(MANPAGES) :: % : %.yo
+ echo Creating man page $@
+ yodl2man -o $@ $<
+
diff --git a/doc/samples/ud-generate b/doc/samples/ud-generate
new file mode 100644
index 0000000..24c003c
--- /dev/null
+++ b/doc/samples/ud-generate
@@ -0,0 +1,2 @@
+# Cron tab for the generate operation, 15 min cycle. Put it in /etc/cron.d/
+08,23,38,53 * * * * sshdist if [ -x /usr/bin/ud-generate ]; then /usr/bin/ud-generate; fi
diff --git a/doc/samples/ud-replicate b/doc/samples/ud-replicate
new file mode 100644
index 0000000..7f41347
--- /dev/null
+++ b/doc/samples/ud-replicate
@@ -0,0 +1,3 @@
+# Cron tab for the replicate operation, 15 min cycle, offset from generate.
+# Put it in /etc/cron.d/
+10,25,40,55 * * * * root if [ -x /usr/bin/ud-replicate ]; then /usr/bin/ud-replicate; fi
diff --git a/doc/slapd-config.txt b/doc/slapd-config.txt
new file mode 100644
index 0000000..0cc7546
--- /dev/null
+++ b/doc/slapd-config.txt
@@ -0,0 +1,45 @@
+Most of the configuration of the ldap server has to do with getting correct
+access controls to keep the data safe. Here is a sample:
+
+# Turn on automatic last modification time
+lastmod on
+
+# Index some things
+index uid eq
+index keyfingerprint eq
+index cn,sn approx,sub,eq
+
+# Administrate
+#rootdn "uid=admin,ou=users,dc=debian,dc=org"
+#rootpw
+
+# Restrict reading/modification of the password to administration and self
+access to attrs=userpassword
+ by self write
+ by dn="uid=admin,ou=users,dc=debian,dc=org" write
+ by * compare
+
+# Reading of eamil forward is restricted by machine
+access to attrs=emailforward
+ by dn="uid=admin,ou=users,dc=debian,dc=org" write
+ by self write
+ by addr=127.0.0.1 read
+ by domain=.*\.debian\.org read
+ by * none
+
+# Public self modifyable attributes
+access to attrs=c,l,loginShell,ircNick,labeledURL
+ by dn="uid=admin,ou=users,dc=debian,dc=org" write
+ by self write
+
+# Private self modifyable fields that are still viewable by other users
+# in the directory.
+access to attrs=facsimileTelephoneNumber,telephoneNumber,postalAddress,postalCode,loginShell,onvacation
+ by dn="uid=admin,ou=users,dc=debian,dc=org" write
+ by self write
+ by dn="uid=.*,ou=users,dc=debian,dc=org" read
+ by * none
+
+# Remainder
+access to *
+ by dn="uid=admin,ou=users,dc=debian,dc=org" write
diff --git a/doc/ud-generate.8.yo b/doc/ud-generate.8.yo
new file mode 100644
index 0000000..f3f9e38
--- /dev/null
+++ b/doc/ud-generate.8.yo
@@ -0,0 +1,48 @@
+mailto(admin@db.debian.org)
+manpage(ud-generate)(1)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-generate)(Produce machine specific formatted version of the
+directory)
+
+manpagesynopsis()
+ ud-generate
+
+manpagedescription()
+
+ud-generate prouces machine specific versions of the directory in the
+following formats:
+
+itemize(
+ it() passwd file [in normal and DB form]
+ it() shadow file [in normal and DB form]
+ it() group file [in normal and DB form]
+ it() Exim forwarding file
+)
+
+Generation of the files is controlled by the configuration file
+bf(/etc/userdir-ldap/ud-generate.conf). The output is placed in
+bf(/var/cache/userdir-ldap/hosts//). Each host listed in the
+configuration file has its own home dir path and its own list of groups that
+are allowed to login to the machine.
+
+The format of the configuration file is a one line per host with these fields:
+verb(host homedirpath group1 group2 ...)
+Only users who are a member of the named groups are emitted to the output
+files.
+
+Authorization to read protected entries from the directory is achieved by
+reading a username and password from the pass- file in the userdir-ldap
+directory.
+
+manpagefiles()
+itemize(
+ it() /etc/userdir-ldap/userdir-ldap.conf
+ Configuration variables to select what server and what base DN to use.
+ it() /etc/userdir-ldap/ud-generate.conf
+ Configuration variables to determine how hosts are generated.
+ it() /etc/userdir-ldap/pass-
+ Directory authentication credentials
+)
+
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe .
+
diff --git a/doc/ud-gpgimport.8.yo b/doc/ud-gpgimport.8.yo
new file mode 100644
index 0000000..c9f4976
--- /dev/null
+++ b/doc/ud-gpgimport.8.yo
@@ -0,0 +1,67 @@
+mailto(admin@db.debian.org)
+manpage(ud-gpgimport)(8)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-gpgimport)(Key Ring Syncronization utility)
+
+manpagesynopsis()
+ ud-gpgimport [options] [keyrings]
+
+manpagedescription()
+ud-gpgimport maintains the key fingerprint to user ID mapping in the
+directory. It takes as input a set of keyrings that represent all keys
+belonging to all users in the directory. It then reads each key and attempts
+to match it up to a user already in the directory. This matching process has
+several steps:
+
+1) If the key fingerprint already exists in the directory then the key is
+assumed to be already assigned so it is ignored
+
+2) If the key email address is in the override table then the key is
+assigned to the user in the override table
+
+3) An exact match of first name + last name from the key's primary UID is
+performed against the directory. If a single hit is found then the key is
+assigned to that user
+
+4) If the email address in the key is within the debian.org domain then the
+key is assigned to the to the mentioned user if the last name from the
+directory appears some place in the key UID. This is called an bf(EmailAppend)
+hit.
+
+5) Nothing is done, but a soundex matcher is invoked to give some suggestions
+on who the key may belong to.
+
+An override table is used to deal with keys that do not exactly match any
+user in the directory. The override table takes the email address that
+appears on a key and maps it to a uid in the directory.
+
+By default the matcher only generates a report on what it would do but makes
+no changes. The -a option must be given and an password entered to allow
+modification.
+
+GnuPG must be properly installed in the system to extract the key
+information from the key rings.
+
+manpageoptions()
+startdit()
+dit(bf(-a))
+Enable modification of the directory.
+
+dit(bf(-u))
+Set the authentication user. This is the user who's authority is used when
+accessing the LDAP directory. The default is to use the current system user
+name.
+
+dit(bf(-m))
+Set the override file to use. The format of the override file is a map of key
+email address to uid, eg verb(foo@bar.com: baz)
+enddit()
+
+manpagefiles()
+itemize(
+ it() /etc/userdir-ldap/userdir-ldap.conf
+ Configuration variables to select what server and what base DN to use.
+)
+
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe .
+
diff --git a/doc/ud-info.1.yo b/doc/ud-info.1.yo
new file mode 100644
index 0000000..119d95d
--- /dev/null
+++ b/doc/ud-info.1.yo
@@ -0,0 +1,151 @@
+mailto(admin@db.debian.org)
+manpage(ud-info)(1)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-info)(Command line LDAP user record manipulator)
+
+manpagesynopsis()
+ ud-info [options]
+
+manpagedescription()
+
+ud-info is the command-line tool for end users to manipulate their own
+database information and to view other users information. It also provides
+root functions which when combined with sufficient LDAP privilages allow
+an administrator to completely manipulate a users record.
+
+The defined fields are:
+itemize(
+ it() cn - Common (first) name. [root]
+ it() mn - Middle name or initial. [root]
+ it() sn - Surname (last name). [root]
+ it() cn - ISO 3166 country code, see file(/usr/share/zoneinfo/iso3166.tab)
+ Should be upper case.
+ it() ircnick - IRC nickname.
+ it() l - City name, state/province. The part of a mailing address that is
+ not the street address. e.g.: Dallas, Texas
+ it() postalcode - Postal Code or ZIP Code
+ it() postaladdress - Complete mailing address including postal codes and
+ country designations. Newlines are seperated by a $ character. The
+ address should be formed exactly as it would appear on a parcel.
+ it() latitude/longitude - The physical latitude and longitude. This
+ information is typically used to generate an xearth marker file.
+ See the discussion below on position formats.
+ it() facsimiletelephonenumber - FAX phone number, do not forget to specify a
+ country code [North Armerica is +1].
+ it() telephonenumber - Voice phone number.
+ it() loginshell - Full path to the prefered Unix login shell. e.g. file(/bin/bash)
+ it() emailforward - Destination email address.
+ it() userpassword - Encrypted version of the password. [root]
+ it() supplementarygid - A list of group names that the user belongs.
+ This field emulates the functionality of the traditional Unix group
+ file. [root]
+ it() onvacation - A message indicating that the user is on vacation. The
+ time of departure and expected return date should be included as
+ well as any special instructions.
+ it() comment - Administrative comment about the account. [root]
+ it() labeledurl - User's web site.
+)
+
+When prompted for a password it is possible to enter a blank password and
+access the database anonymously. This is useful to check PGP key
+fingerprints, for instance.
+
+manpagesection(SECURITY AND PRIVACY)
+Three levels of information security are provided by the database. The first
+is completely public information that anyone can see either by issuing an
+LDAP query or by visiting the web site. The next level is "maintainer-only"
+information that requires authentication to the directory before it can be
+accessed. The final level is admin-only or user-only information; this
+information can only be viewed by the user or an administrator.
+
+Maintainer-only information includes precise location information
+[postalcode, postal address, lat/long] telephone numbers, and the vacation
+message.
+
+Admin-only/maintainer-only information includes email forwarding and the
+encrypted password. Note that email forwarding is necessarily publicly viewable
+from accounts on the actual machines.
+
+manpagesection(LAT/LONG POSITION)
+There are three possible formats for giving position information and several
+online sites that can give an accurate position fix based on mailing address.
+
+startdit()
+dit(Decimal Degrees)
+The format is +-DDD.DDDDDDDDDDDDDDD. This is the format programs like
+bf(xearth)
+use and the format that many positioning web sites use. However typically
+the precision is limited to 4 or 5 decimals.
+
+dit(Degrees Minutes (DGM))
+The format is +-DDDMM.MMMMMMMMMMMMM. It is not an arithmetic type, but a
+packed representation of two seperate units, degrees and minutes. This
+output is common from some types of hand held GPS units and from NMEA format
+GPS messages.
+
+dit(Degrees Minutes Seconds (DGMS))
+The format is +-DDDMMSS.SSSSSSSSSSS. Like DGM, it is not an arithmetic type but
+a packed representation of three seperate units, degrees minutes and
+seconds. This output is typically derived from web sites that give 3 values
+for each position. For instance 34:50:12.24523 North might be the position
+given, in DGMS it would be +0345012.24523.
+enddit()
+
+For Latitude + is North, for Longitude + is East. It is important to specify
+enough leading zeros to dis-ambiguate the format that is being used if your
+position is less than 2 degrees from a zero point.
+
+So locations to find positioning information are:
+
+itemize(
+ it() Good starting point - http://www.ckdhr.com/dns-loc/finding.html
+ it() AirNav - GPS locations for airports around the world http://www.airnav.com/
+ it() GeoCode - US index by ZIP Code http://www.geocode.com/eagle.html-ssi
+ it() Map Blast! Canadian, US and some European maps - http://www.mapblast.com/
+ it() Australian Database http://www.environment.gov.au/database/MAN200R.html
+ it() Canadian Database http://GeoNames.NRCan.gc.ca/
+ it() GNU Timezone database, organized partially by country /usr/share/zoneinfo/zone.tab
+)
+
+Remember that we are after reasonable coordinates for drawing an xearth
+graph and looking for people to sign keys, not for coordinates accurate
+enough to land an ICBM on your doorstop!
+
+manpagesection(Editing Supplemental GIDs)
+When the root function is activated then the supplemental GIDs can be
+manipulated as a list of items. It is possible to add and remove items from
+the list by name. Proper prompts are given.
+
+manpageoptions()
+startdit()
+dit(bf(-a))
+Set the authentication user. This is the user whose authority is used when
+accessing the LDAP directory. The default is to use the current system user
+name.
+
+dit(bf(-u))
+Select the user whose fields will be displayed/edited. The default is to use
+the current system user name.
+
+dit(bf(-c))
+Set both the authentication user and the target user. This option is useful
+if the login name does not match the user who is operating the program.
+
+dit(bf(-r))
+Enable root functions. This enables more options to allow changing
+any entry in the directory. This function only has meaning if the
+authentication user has the necessary permissions at the LDAP server.
+
+dit(bf(-n))
+No actions. Anonymously bind and show the information for the user and then
+exit.
+enddit()
+
+manpagefiles()
+itemize(
+ it() /etc/userdir-ldap/userdir-ldap.conf
+ Configuration variables to select what server and what base DN to use.
+)
+
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe .
+
diff --git a/doc/ud-useradd.8.yo b/doc/ud-useradd.8.yo
new file mode 100644
index 0000000..96df739
--- /dev/null
+++ b/doc/ud-useradd.8.yo
@@ -0,0 +1,107 @@
+mailto(admin@db.debian.org)
+manpage(ud-useradd)(8)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-useradd)(Interactive user addition program)
+
+manpagesynopsis()
+ ud-useradd [options]
+
+manpagedescription()
+ud-uaseradd is an interactive program for adding new users to the directory.
+It takes care of all steps of user addition including generating a random
+new password and sending a greeting form letter.
+
+The operator is taken through a set of prompts to determine the data to be
+loaded into the directory:
+
+startdit()
+dit(PGP Key Fingerprint)
+The first prompt is to determine the user's PGP key. For this to be
+successfull the key must have already been loaded into a keyring referenced
+by the GPG configuration file. The search specification is passed directly
+to GPG and then the results are presented, when a single match is found then
+it is taken as the correct key.
+
+dit(Account Name)
+This is the UID of the user, their login name and email local part. If the
+name already exists then it is possible to update the account directly. This
+feature should probably be used very infrequently as ud-info can adjust
+all of the values.
+
+dit(First, Last and Middle Name)
+The proper name of the user, split into three components. The name
+name attached to the PGP key is provided as a default. In most cases this
+should be adaquate and correct.
+
+dit(Email Forwarding Address)
+The address that all general email should be forwarded to. This is analogous
+to a .forward file in the users home directory except that it applies
+globally to all machines. The email address attached to the PGP key is
+provided as a default.
+
+dit(Debian-Private Subscription)
+The address the user should be subscribed to debian-private with. Currently
+this sets the field in the DB and emails a subscription form to the
+list server.
+
+dit(Group ID Number)
+Main group the user will be part of. The group the user is assigned to
+determines which welcome form they are sent. The default is taken from
+the global configuration file
+
+dit(UID)
+The uid is selected automatically based on the first found free UID.
+
+dit(Password)
+The password can be specified if the user is not legaly able to use
+encryption (they live in France for instance) otherwise pressing enter at
+this prompt will generate a random new password. The password to be entered
+is the plain text version, the script will crypt it automatically.
+enddit()
+
+After the information has been collected a summary is displayed and
+confirmation is required to proceed. Once confirmed the script will create a
+new entry and fill it with the given values. Then it will open the greeting
+form bf(/etc/userdir-ldap/templates/welcome-message-) and perform a
+variable substitution before sending it. Then the debian-private subscription
+form is sent.
+
+It is expected that the PGP key of the user has already been inserted into a
+local keyring known to GPG.
+
+manpagesection(Substitution Variables)
+A number of values are provided as substitution variables for the greeting
+and subscription message, they are:
+
+itemize(
+ it() __REALNAME__ The combined First/Middle/Last name
+ it() __WHOAMI__ The invoking user ID [unix ID]
+ it() __DATE__ The current date in RFC 822 form
+ it() __LOGIN__ The new users login ID
+ it() __PRIVATE__ The address to subscribe to debian-private
+ it() __EMAIL__ The normal email address of the user
+ it() __PASSWORD__ An ascii armored PGP packet containing the users
+ password.
+ it() __LISTPASS__ The contents of the file ~/.debian-lists_passwd
+)
+
+manpageoptions()
+startdit()
+dit(bf(-u))
+Set the authentication user. This is the user who's authority is used when
+accessing the LDAP directory. The default is to use the current system user
+name.
+enddit()
+
+manpagefiles()
+itemize(
+ it() /etc/userdir-ldap/userdir-ldap.conf
+ Configuration variables to select what server and what base DN to use.
+ it() /etc/userdir-ldap/templates/welcome-message-
+ The welcoming message to send to the user. Each primary group has its
+ own message
+ it() ~/.debian-lists_passwd
+ Authentication password for the list server
+)
+
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe .
diff --git a/doc/ud-userimport.8.yo b/doc/ud-userimport.8.yo
new file mode 100644
index 0000000..73dad01
--- /dev/null
+++ b/doc/ud-userimport.8.yo
@@ -0,0 +1,76 @@
+mailto(admin@db.debian.org)
+manpage(ud-userimport)(1)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-userimport)(Perform initial import of date)
+
+manpagesynopsis()
+ ud-userimport [options]
+
+manpagedescription()
+
+ud-userimport is the utility that is used to initially load data into the
+directory. It takes as input a set of normal unix password, group and shadow
+files and loads their contents. Also it provide enough functionality to
+allow simple additions at a later date.
+
+Before attempting to import the data the passwd file should be sanitized
+of any system entries and the GECOs fields should be cleaned of any
+strangeness users may have inserted.
+
+Next the passwd file alone should be added using the command
+verb(ud-userimport -a -p passwd)
+The passwd file will be loaded into the
+empty directory and new entries created for all the users.
+
+The shadow file does not have to be santized, importing it without the -a
+option will automatically skip any records that are not needed.
+The command to use is verb(ud-userimport -s shadow)
+
+Like the passwd file the group file needs to be cleaned of system groups and
+groups that are no longer needed. It is not necessary to remove non-existant
+users from the group lists, they will be automatically ignored. Like for
+the shadow file the command is verb(ud-userimport -a -g group)
+
+After the initial import is completed the ud-info tool can be used to
+manipulate the user records, however new groups can most easially be created
+by giving a file containing only a single group (and its initial membership)
+to ud-userimport.
+
+The importer is optimized to get good speed on updates through the use
+of the async ldap mechanism. If errors are found in the import of the
+passwd file or shadow file it is possible to re-run the import command
+(without the -a option) to freshen the data set.
+
+Aside from the evident transformations, the splitter also processes the
+unix gecos field into split first/last/middle names and it also sanitizes
+the gecos field to follow normal Debian convetions.
+
+manpageoptions()
+startdit()
+dit(bf(-u))
+Set the authentication user. This is the user who's authority is used when
+accessing the LDAP directory. The default is to use the current system user
+name.
+
+dit(bf(-x))
+Do not write new passwords into the directory. This is usefull if other
+information is being freshened but users have changed their passwords.
+
+dit(bf(-p))
+Specify the passwd file to import.
+
+dit(bf(-g))
+Specify the group file to import.
+
+dit(bf(-s))
+Specify the shadow file to import.
+enddit()
+
+manpagefiles()
+itemize(
+ it() /etc/userdir-ldap/userdir-ldap.conf
+ Configuration variables to select what server and what base DN to use.
+)
+
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe .
+
diff --git a/doc/ud-xearth.1.yo b/doc/ud-xearth.1.yo
new file mode 100644
index 0000000..666c903
--- /dev/null
+++ b/doc/ud-xearth.1.yo
@@ -0,0 +1,34 @@
+mailto(admin@db.debian.org)
+manpage(ud-xearth)(1)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-xearth)(Extracts the XEarth marker database)
+
+manpagesynopsis()
+ ud-xearth [options]
+
+manpagedescription()
+ud-xearth simply extracts the lat/long information from the directory and
+formats it in a form suitable for use by XEarth or XPlanet. The program
+takes the lat/long coords stored in the directory and converts them to a
+decimal degrees format and then outputs a file containing the UID of the
+user and their coordinates as well as their full email address in a comment.
+The output is place in a file called ./markers.dat
+
+Since lat/long information is restricted to developers only a valid login is
+required to extract the information.
+
+manpageoptions()
+startdit()
+dit(bf(-u))
+Set the authentication user. This is the user who's authority is used when
+accessing the LDAP directory. The default is to use the current system user
+name.
+enddit()
+
+manpagefiles()
+itemize(
+ it() /etc/userdir-ldap/userdir-ldap.conf
+ Configuration variables to select what server and what base DN to use.
+)
+
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe .
diff --git a/templates/list-subscribe b/templates/list-subscribe
new file mode 100644
index 0000000..e59d65d
--- /dev/null
+++ b/templates/list-subscribe
@@ -0,0 +1,4 @@
+To: debian-private-REQUEST@lists.debian.org
+X-We-Want-Cabal: listmaster@lists.debian.org __LISTPASS__ subscribe __PRIVATE__
+
+foo
diff --git a/templates/passwd-changed b/templates/passwd-changed
new file mode 100644
index 0000000..74b497f
--- /dev/null
+++ b/templates/passwd-changed
@@ -0,0 +1,14 @@
+From: __FROM__
+Subject: Password Changed!
+
+Hello __EMAIL__!
+
+Your password has been updated. Enclosed below is the new password encrypted
+with your key. __CRYPTTYPE__
+
+Currently LDAP information is replicated to each machine every 15 mins,
+va, pandora and master are not presently in the LDAP system.
+
+Please email __ADMIN__ if you have any questions.
+
+__PASSWORD__
diff --git a/templates/ping-reply b/templates/ping-reply
new file mode 100644
index 0000000..4db4b87
--- /dev/null
+++ b/templates/ping-reply
@@ -0,0 +1,11 @@
+From: __FROM__
+Subject: Pring Reply
+
+Hello __EMAIL__!
+
+Here is a list of all the public fields associated with your LDAP entry:
+
+__LDAPFIELDS__
+
+Please email __ADMIN__ if you have any questions.
+
diff --git a/templates/welcome-message-60000 b/templates/welcome-message-60000
new file mode 100644
index 0000000..f89fa1c
--- /dev/null
+++ b/templates/welcome-message-60000
@@ -0,0 +1,37 @@
+To: "__REALNAME__" <__EMAIL__>
+Subject: Debian Guest Account for __REALNAME__
+Cc: debian-admin@lists.debian.org
+Reply-To: debian-admin@lists.@debian.org
+Date: __DATE__
+User-Agent: Script run by __WHOAMI__
+
+Dear __REALNAME__!
+
+An account has been created for you on the Debian machine cluster. You can
+use this account to help make software run properly on the Debian GNU/Linux
+distribution. The username for this account is '__LOGIN__'. The password can
+be found encrypted with your PGP key and appended to this message.
+
+The following machines are accesible:
+ faure.debian.org Alpha running unstable
+ albert.debian.org Alpha running stable [slow]
+ kubrick.dbian.org Sparc running unstable
+ debussy.debian.org ARM running unstable
+
+Requests for Debian software to be installed should be directed at
+debian-admin@lists.debian.org. Please note that not all software is available
+on all architectures.
+
+You should use ssh to log into the machines instead of regular telnet or
+rlogin. We have installed ~/.ssh directories and empty authorized_keys
+files with appropriate permissions on each machine.If you want to ssh to them
+without typing the password, run ssh-keygen on your machine and add the
+contents of ~/.ssh/identity.pub into the authorized_keys files in ~/.ssh. But
+please be aware of the security implications of doing this.
+
+After a short while of inactivity this account will be expired.
+
+--
+Debian Administration
+
+__PASSWORD__
diff --git a/templates/welcome-message-800 b/templates/welcome-message-800
new file mode 100644
index 0000000..ea87686
--- /dev/null
+++ b/templates/welcome-message-800
@@ -0,0 +1,92 @@
+To: "__REALNAME__" <__EMAIL__>
+From: __WHOAMI__
+Subject: New Debian maintainer __REALNAME__
+Cc: new-maintainer@debian.org
+Reply-To: new-maintainer@debian.org
+Date: __DATE__
+User-Agent: nm-create script run by __WHOAMI__
+
+[ This is a long (automatically-generated) mail, but it contains
+ important information, please read it all carefully. ]
+
+Dear __REALNAME__!
+
+An account has been created for you on master.debian.org and
+va.debian.org with username '__LOGIN__'. The password for this
+account can be found encrypted with your PGP key and appended to this
+message.
+
+You have been subscribed to the debian-private mailing list as
+<__PRIVATE__> (you should receive seperate confirmation of this from
+smartlist). Please respect the privacy of that list and don't forward
+mail from it elsewhere. E-mail to <__LOGIN__@debian.org> will be
+forwarded to <__EMAIL__>. To change this, edit the file '.qmail' in
+your home directory on master to point to the new address (don't
+forget the '&' character at the beginning of the line). Please
+subscribe to debian-devel-announce, if you haven't done so already.
+
+We strongly suggest that you use your __LOGIN__@debian.org address for
+the maintainer field in your packages, because that one will be valid
+as long as you are a Debian developer, even if you change jobs, leave
+university or change Internet Service providers. If you do so, please
+add that address to your PGP key (using `pgp -ke "YOUR USER ID"'), if
+you have one, and your GnuPG key (using `gpg --edit-key "YOUR USER ID"')
+and send it to .
+
+You can find more information useful to developers at
+ (in particular, see the subsection
+titled "Debian Developer's reference").
+
+We suggest that you subscribe to debian-mentors@lists.debian.org.
+This list is for new maintainers who seek help with initial packaging
+and other developer-related issues. Those who prefer one-on-one help
+can also post to the list, and an experienced developer may volunteer
+to help you. You can get online help on IRC, too, if you join the
+channel #debian-devel on irc.debian.org. Take a look at the support
+section on www.debian.org in order to find out more information.
+
+You should have read these documents before working on your packages.
+
+ o The Debian Social Contract
+
+
+ o The Debian Policy Manual
+
+
+ o The Debian Packaging Manual
+
+
+If you have some spare time and want to contribute it to Debian you
+may wish to take a look at the "Work-Needing and Prospective Packages
+for Debian GNU/Linux" also known as WNPP that can be found at
+
+
+If you plan to make a Debian package from a not yet packaged piece of
+software you *must* announce your intention on the debian-devel mailing
+list to make sure nobody else is working on them.
+
+The machine master.debian.org is our main archive server. Every
+uploaded package finds it's way there (except for Packages covered by
+US crypto laws which go to non-us.debian.org) eventually. That
+machine is also the home of our web pages and our bug tracking system.
+The second machine va.debian.org is supposed to take over some of the
+work master does.
+
+Both machines were sponsored by companies (Novare Inc. for master and
+VA Linux Systems for va). Please don't over-stress them and use your
+accounts carefully.
+
+You should use ssh to log into the machines instead of regular telnet
+or rlogin. We have installed ~/.ssh directories and authorized_keys
+files with appropriate permissions on both machines. If you want to
+ssh to them without typing the password, run ssh-keygen on your
+machine and add the contents of ~/.ssh/identity.pub into the
+authorized_keys files in ~/.ssh on master and va. But please be aware
+of the security implications of doing this.
+
+Welcome to the project!
+
+--
+The Debian New Maintainer Team
+
+__PASSWORD__
diff --git a/ud-arbimport b/ud-arbimport
new file mode 100755
index 0000000..cf57cbc
--- /dev/null
+++ b/ud-arbimport
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script imports arbitary lists of data. The input is a file with
+# the form of:
+# uid:
+
+import string, re, time, ldap, getopt, sys;
+from userdir_ldap import *;
+
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "u:m:n")
+for (switch, val) in options:
+ if (switch == '-u'):
+ AdminUser = val
+ elif (switch == '-m'):
+ LoadOverride(val);
+ elif (switch == '-n'):
+ NoAct = 1;
+if len(arguments) == 0:
+ print "Give the key to assignt to then the file to import";
+ os.exit(0);
+
+# Main program starts here
+print "Accessing LDAP directory as '" + AdminUser + "'";
+Password = getpass(AdminUser + "'s password: ");
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+UserDn = "uid=" + AdminUser + "," + BaseDn;
+l.simple_bind_s(UserDn,Password);
+
+# Read the override file into the unknown map. The override file is a list
+# of colon delimited entires mapping PGP email addresess to local users
+List = open(arguments[1],"r");
+while(1):
+ Line = List.readline();
+ if Line == "":
+ break;
+ Split = re.split("[:\n]",Line);
+
+ Rec = [(ldap.MOD_REPLACE,arguments[0],string.strip(Split[1]))];
+ Dn = "uid=" + Split[0] + "," + BaseDn;
+ try:
+ l.modify_s(Dn,Rec);
+ except:
+ print "Failed",Dn;
diff --git a/ud-emailmatcher b/ud-emailmatcher
new file mode 100755
index 0000000..daf76cd
--- /dev/null
+++ b/ud-emailmatcher
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script tries to match a list of email addresses to the ldap database
+# uids. It makes use of the PGP key ring to determine matches
+
+import string, re, time, ldap, getopt, sys;
+from userdir_ldap import *;
+from userdir_gpg import *;
+
+AddressSplit = re.compile("(.*).*<([^@]*)@([^>]*)>");
+
+# Import an an forward file
+def ImportForward(File,EmailMap):
+ F = open(File,"r");
+ while(1):
+ Line = string.strip(F.readline());
+ if Line == "":
+ break;
+ Split = string.split(Line,":");
+ if len(Split) != 2:
+ continue;
+
+ Addr = string.strip(Split[1]);
+ if EmailMap.has_key(Addr) and EmailMap[Addr] != Split[0]:
+ print "Dup Over Emap",Line,Split
+ else:
+ EmailMap[Addr] = Split[0];
+ F.close();
+
+# Import an override file
+def ImportOverride(File,OverMap):
+ F = open(File,"r");
+ while(1):
+ Line = F.readline();
+ if Line == "":
+ break;
+ Line = string.strip(Line);
+
+ Split = string.split(Line,":");
+ if len(Split) != 2:
+ continue;
+ OverMap[Split[0]] = string.strip(Split[1]);
+ F.close();
+
+(options, arguments) = getopt.getopt(sys.argv[1:], "o:f:")
+
+# Popen GPG with the correct magic special options
+Args = [GPGPath] + GPGBasicOptions + GPGKeyRings;
+for x in arguments:
+ Args.append("--keyring");
+ Args.append(x);
+Args = Args + GPGSearchOptions + [" 2> /dev/null"]
+Keys = os.popen(string.join(Args," "),"r");
+
+l = ldap.open(LDAPServer);
+l.simple_bind_s("","");
+
+# Fetch the key list and map to email address
+PasswdAttrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=*",\
+ ["uid","keyfingerprint"]);
+KFMap = {}
+for x in PasswdAttrs:
+ if x[1].has_key("keyfingerprint") == 0 or x[1].has_key("uid") == 0:
+ continue;
+ for I in x[1]["keyfingerprint"]:
+ KFMap[I] = x[1]["uid"][0];
+
+# Loop over the GPG key file mapping addresses to uids
+Outstanding = 0;
+Ignored = 0;
+Emails = [];
+EmailMap = {};
+UIDMap = {};
+UID = None;
+FingerPrint = None;
+print "Reading keyrings",
+sys.stdout.flush();
+while(1):
+ Line = Keys.readline();
+ if Line == "":
+ break;
+
+ Split = string.split(Line,":");
+ if len(Split) >= 8 and Split[0] == "pub":
+ if FingerPrint != None and UID != None:
+ for x in Emails:
+ Match = AddressSplit.match(x);
+ if Match == None:
+ continue;
+ Groups = Match.groups();
+ Email = Groups[1]+'@'+Groups[2];
+ if UIDMap.has_key(Groups[1]):
+ UIDMap[Groups[1]].append(Email);
+ else:
+ UIDMap[Groups[1]] = [Email];
+ if EmailMap.has_key(Email) and EmailMap[Email] != UID:
+ print "Dup Emap",Email
+ else:
+ EmailMap[Email] = UID;
+ Emails = [Split[9]];
+ continue;
+ if len(Split) >= 11 and Split[0] == "fpr":
+ FingerPrint = Split[9];
+ if KFMap.has_key(FingerPrint) == 0:
+ print "Failed",FingerPrint;
+ UID = None;
+ continue;
+ UID = KFMap[FingerPrint];
+ if len(Split) >= 9 and Split[0] == "uid":
+ Emails.append(Split[9]);
+print;
+
+# Process the override files
+for (switch, val) in options:
+ if (switch == '-f'):
+ ImportForward(val,EmailMap);
+ BindUser = val;
+ elif (switch == '-o'):
+ ImportOverride(val,EmailMap);
+
+# Map the input
+FinalMap = {};
+while(1):
+ Line = sys.stdin.readline();
+ if Line == "":
+ break;
+ Line = string.strip(Line);
+
+ Split = string.split(Line,"@");
+ if len(Split) != 2:
+ continue;
+
+ # The address is in our domain, go directly
+ if Split[1] == EmailAppend:
+ if FinalMap.has_key(Line):
+ print "Dup",Line
+ Split2 = string.split(Split[0],"-");
+ FinalMap[Line] = Split2[0];
+ continue;
+
+ # Exists in the email map..
+ if EmailMap.has_key(Line):
+ if FinalMap.has_key(Line):
+ print "Dup",Line
+ FinalMap[Line] = EmailMap[Line];
+ continue;
+
+ # Try again splitting off common address appendage modes
+ Split2 = string.split(Split[0],"-");
+ Addr = Split2[0]+'@'+Split[1];
+ if EmailMap.has_key(Addr):
+ if FinalMap.has_key(Addr):
+ print "Dup",Addr
+ FinalMap[Line] = EmailMap[Addr];
+ continue;
+
+ # Failed
+ if UIDMap.has_key(Split[0]):
+ print Line,UIDMap[Split[0]];
+ print Line;
+print "-----";
+
+# Generate a reverse map and check for duplicates
+Back = {};
+for x in FinalMap.keys():
+ if Back.has_key(FinalMap[x]):
+ print "Dup",x,FinalMap[x],Back[FinalMap[x]];
+ Back[FinalMap[x]] = x;
+
+# Print the forward map
+for x in Back.keys():
+ print "%s: %s" % (x,Back[x]);
diff --git a/ud-forwardlist b/ud-forwardlist
new file mode 100755
index 0000000..9e5e28a
--- /dev/null
+++ b/ud-forwardlist
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script takes a list of .forward files and generates a list of colon
+# delimited fields for import into a ldap directory. The fields represent
+# the user and their email forwarding.
+#
+# A sample invokation..
+# cd /home
+# find -name ".foward" -maxdepth 2 | mkforwardlist | sort | less
+# Then correct any invalid forward files if possible. After that stash the
+# output in a file, remove the invalid lines and import it.
+#
+# It also understand .qmail type files
+
+import string, re, time, getopt, os, sys, pwd, stat;
+
+AddressSplit = re.compile("<(.*)>");
+
+while (1):
+ File = string.strip(sys.stdin.readline());
+ if File == "":
+ break;
+
+ # Attempt to determine the UID
+ try:
+ User = pwd.getpwuid(os.stat(File)[stat.ST_UID])[0];
+ except KeyError:
+ print "Invalid0", File;
+ continue;
+
+ # Read the first two non comment non empty lines
+ Forward = open(File,"r");
+ Line = None;
+ while (1):
+ Line2 = string.strip(Forward.readline());
+ if Line2 == "":
+ break;
+ if Line2[0] == '#' or Line2[0] == '\n':
+ continue;
+ if Line == None:
+ Line = Line2;
+ else:
+ break;
+
+ # If we got more than one line or no lines at all it is invalid
+ if Line == None or Line == "" or Line2 != "":
+ print "Invalid1", File;
+ continue;
+
+ # Abort for funky things like pipes or directions to mailboxes
+ if Line[0] == '/' or Line[0] == '|' or Line[0] == '.' or Line[-1] == '/' or \
+ string.find(Line,'@') == -1:
+ print "Invalid2", File;
+ continue;
+
+ # Split off the address part
+ Address = AddressSplit.match(Line);
+ if Address == None:
+ # Or parse a qmail adddress..
+ Address = Line;
+ if Address[0] == '&':
+ Address = Address[1:];
+
+ if Address == "":
+ print "Invalid3", File;
+ continue;
+
+ print User + ":",Address;
diff --git a/ud-generate b/ud-generate
new file mode 100755
index 0000000..b627cae
--- /dev/null
+++ b/ud-generate
@@ -0,0 +1,253 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Generates passwd, shadow and group files from the ldap directory.
+
+import string, re, time, ldap, getopt, sys, os, posix, pwd;
+from userdir_ldap import *;
+
+PasswdAttrs = None;
+GroupIDMap = {};
+
+# See if this user is in the group list
+def IsInGroup(DnRecord,Allowed):
+ # See if the primary group is in the list
+ if Allowed.has_key(GetAttr(DnRecord,"gidnumber")) != 0:
+ return 1;
+
+ # See if there are supplementary groups
+ if DnRecord[1].has_key("supplementarygid") == 0:
+ return 0;
+
+ # Check the supplementary groups
+ for I in DnRecord[1]["supplementarygid"]:
+ if Allowed.has_key(I):
+ return 1;
+ return 0;
+
+def Die(F,Fdb):
+ if F != None:
+ F.close();
+ if Fdb != None:
+ Fdb.close();
+ try: os.remove(File + ".tmp");
+ except: pass;
+ try: os.remove(File + ".tdb.tmp");
+ except: pass;
+
+def Done(File,F,Fdb):
+ if F != None:
+ F.close();
+ os.rename(File + ".tmp",File);
+ if Fdb != None:
+ Fdb.close();
+ os.rename(File + ".tdb.tmp",File+".tdb");
+
+# Generate the password list
+def GenPasswd(l,File,HomePrefix,Allowed):
+ F = None;
+ Fdb = None;
+ try:
+ F = open(File + ".tmp","w");
+ Fdb = open(File + ".tdb.tmp","w");
+
+ # Fetch all the users
+ global PasswdAttrs;
+ if PasswdAttrs == None:
+ raise "No Users";
+
+ I = 0;
+ for x in PasswdAttrs:
+ if x[1].has_key("uidnumber") == 0 or IsInGroup(x,Allowed) == 0:
+ continue;
+
+ Line = "%s:x:%s:%s:%s:%s%s:%s\n" % (GetAttr(x,"uid"),\
+ GetAttr(x,"uidnumber"),GetAttr(x,"gidnumber"),\
+ GetAttr(x,"gecos"),HomePrefix,GetAttr(x,"uid"),\
+ GetAttr(x,"loginshell"));
+ F.write(Line);
+ Fdb.write("0%u %s" % (I,Line));
+ Fdb.write(".%s %s" % (GetAttr(x,"uid"),Line));
+ Fdb.write("=%s %s" % (GetAttr(x,"uidnumber"),Line));
+ I = I + 1;
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(F,Fdb);
+ raise;
+ Done(File,F,Fdb);
+
+# Generate the shadow list
+def GenShadow(l,File,Allowed):
+ F = None;
+ Fdb = None;
+ try:
+ OldMask = os.umask(0077);
+ F = open(File + ".tmp","w",0600);
+ Fdb = open(File + ".tdb.tmp","w",0600);
+ os.umask(OldMask);
+
+ # Fetch all the users
+ global PasswdAttrs;
+ if PasswdAttrs == None:
+ raise "No Users";
+
+ I = 0;
+ for x in PasswdAttrs:
+ if x[1].has_key("uidnumber") == 0 or IsInGroup(x,Allowed) == 0:
+ continue;
+
+ Pass = GetAttr(x,"userpassword");
+ if Pass[0:7] != "{crypt}":
+ Pass = '*';
+ else:
+ Pass = Pass[7:];
+ Line = "%s:%s:%s:%s:%s:%s:%s:%s:\n" % (GetAttr(x,"uid"),\
+ Pass,GetAttr(x,"shadowlastchange"),\
+ GetAttr(x,"shadowmin"),GetAttr(x,"shadowmax"),\
+ GetAttr(x,"shadowwarning"),GetAttr(x,"shadowinactive"),\
+ GetAttr(x,"shadowexpire"));
+ F.write(Line);
+ Fdb.write("0%u %s" % (I,Line));
+ Fdb.write(".%s %s" % (GetAttr(x,"uid"),Line));
+ I = I + 1;
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(F,Fdb);
+ raise;
+ Done(File,F,Fdb);
+
+# Generate the group list
+def GenGroup(l,File,Allowed):
+ F = None;
+ Fdb = None;
+ try:
+ F = open(File + ".tmp","w");
+ Fdb = open(File + ".tdb.tmp","w");
+
+ # Generate the GroupMap
+ GroupMap = {};
+ for x in GroupIDMap.keys():
+ GroupMap[x] = [];
+
+ # Fetch all the users
+ global PasswdAttrs;
+ if PasswdAttrs == None:
+ raise "No Users";
+
+ # Sort them into a list of groups having a set of users
+ for x in PasswdAttrs:
+ if x[1].has_key("uidnumber") == 0 or IsInGroup(x,Allowed) == 0:
+ continue;
+ if x[1].has_key("supplementarygid") == 0:
+ continue;
+
+ for I in x[1]["supplementarygid"]:
+ if GroupMap.has_key(I):
+ GroupMap[I].append(GetAttr(x,"uid"));
+ else:
+ GroupMap[I] = [GetAttr(x,"uid")];
+
+ # Output the group file.
+ Counter = 0;
+ for x in GroupMap.keys():
+ Line = "%s:x:%u:" % (x,GroupIDMap[x]);
+ Comma = '';
+ for I in GroupMap[x]:
+ Line = Line + ("%s%s" % (Comma,I));
+ Comma = ',';
+ Line = Line + '\n';
+ F.write(Line);
+ Fdb.write("0%u %s" % (Counter,Line));
+ Fdb.write(".%s %s" % (x,Line));
+ Fdb.write("=%u %s" % (GroupIDMap[x],Line));
+ Counter = Counter + 1;
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(F,Fdb);
+ raise;
+ Done(File,F,Fdb);
+
+# Generate the email forwarding list
+def GenForward(l,File,Allowed):
+ F = None;
+ Fdb = None;
+ try:
+ F = open(File + ".tmp","w");
+ Fdb = None;
+
+ # Fetch all the users
+ global PasswdAttrs;
+ if PasswdAttrs == None:
+ raise "No Users";
+
+ # Write out the email address for each user
+ for x in PasswdAttrs:
+ if x[1].has_key("emailforward") == 0 or IsInGroup(x,Allowed) == 0:
+ continue;
+ Line = "%s: %s\n" % (GetAttr(x,"uid"),GetAttr(x,"emailforward"));
+ F.write(Line);
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(F,Fdb);
+ raise;
+ Done(File,F,Fdb);
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+F = open(PassDir+"/pass-"+pwd.getpwuid(posix.getuid())[0],"r");
+Pass = string.split(string.strip(F.readline())," ");
+F.close();
+l.simple_bind_s("uid="+Pass[0]+","+BaseDn,Pass[1]);
+
+# Fetch all the groups
+GroupIDMap = {};
+Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"gid=*",\
+ ["gid","gidnumber"]);
+
+# Generate the GroupMap and GroupIDMap
+for x in Attrs:
+ if x[1].has_key("gidnumber") == 0:
+ continue;
+ GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidnumber"][0]);
+
+# Fetch all the users
+PasswdAttrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
+ ["uid","uidnumber","gidnumber","supplementarygid",\
+ "gecos","loginshell","userpassword","shadowlastchange",\
+ "shadowmin","shadowmax","shadowwarning","shadowinactive",
+ "shadowexpire","emailforward"]);
+
+# Open the control file
+if len(sys.argv) == 1:
+ F = open(GenerateConf,"r");
+else:
+ F = open(sys.argv[1],"r")
+while(1):
+ Line = F.readline();
+ if Line == "":
+ break;
+ Line = string.strip(Line);
+ if Line == "":
+ continue;
+ if Line[0] == '#':
+ continue;
+
+ Split = string.split(Line," ");
+ OutDir = GenerateDir + '/' + Split[0] + '/';
+ try: os.mkdir(OutDir);
+ except: pass;
+
+ # Get the group list and convert any named groups to numerics
+ GroupList = {};
+ for I in Split[2:]:
+ GroupList[I] = None;
+ if GroupIDMap.has_key(I):
+ GroupList[str(GroupIDMap[I])] = None;
+
+ GenPasswd(l,OutDir+"passwd",Split[1],GroupList);
+ GenGroup(l,OutDir+"group",GroupList);
+ GenShadow(l,OutDir+"shadow",GroupList);
+ GenForward(l,OutDir+"forward-alias",GroupList);
diff --git a/ud-gpgimport b/ud-gpgimport
new file mode 100755
index 0000000..73e2a03
--- /dev/null
+++ b/ud-gpgimport
@@ -0,0 +1,234 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script tries to match key fingerprints from a keyring with user
+# name in a directory. When an unassigned key is found a heuristic match
+# against the keys given cn/sn and the directory is performed to try to get
+# a matching. Generally this works about 90% of the time, matching is fairly
+# strict. In the event a non-match a fuzzy sounds-alike search is performed
+# and the results printed to aide the user.
+#
+# GPG is automatically invoked with the correct magic special options,
+# pass the names of all the valid key rings on the command line.
+#
+# The output report will list what actions were taken. Keys that are present
+# in the directory but not in the key ring will be removed from the
+# directory.
+
+import string, re, time, ldap, getopt, sys, pwd, posix;
+from userdir_ldap import *;
+from userdir_gpg import *;
+
+# This map deals with people who put the wrong sort of stuff in their pgp
+# key entries
+UnknownMap = {};
+NoAct = 1;
+
+AddressSplit = re.compile("(.*).*<([^@]*)@([^>]*)>");
+
+# Read the override file into the unknown map. The override file is a list
+# of colon delimited entires mapping PGP email addresess to local users
+def LoadOverride(File):
+ List = open(File,"r");
+ while(1):
+ Line = List.readline();
+ if Line == "":
+ break;
+ Split = re.split("[:\n]",Line);
+ UnknownMap[Split[0]] = string.strip(Split[1]);
+
+# Convert the PGP name string to a uid value
+def GetUID(l,Name):
+ # Crack up the email address into a best guess first/middle/last name
+ (cn,mn,sn) = NameSplit(re.sub('["]','',Name[0]))
+
+ # Brackets anger the ldap searcher
+ cn = re.sub('[(")]','?',cn);
+ sn = re.sub('[(")]','?',sn);
+
+ # First check the unknown map for the email address
+ if UnknownMap.has_key(Name[1] + '@' + Name[2]):
+ print "unknown map hit for",Name;
+ return UnknownMap[Name[1] + '@' + Name[2]];
+
+ # Then the cruft component (ie there was no email address to match)
+ if UnknownMap.has_key(Name[2]):
+ print "unknown map hit for",Name;
+ return UnknownMap[Name[2]];
+
+ # Search for a possible first/last name hit
+ try:
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(cn=%s)(sn=%s))"%(cn,sn),["uid"]);
+ except ldap.FILTER_ERROR:
+ print "Filter failure:","(&(cn=%s)(sn=%s))"%(cn,sn);
+ return None;
+
+ # Hmm, more than one/no return
+ if (len(Attrs) != 1):
+ # Key claims a local address
+ if Name[2] == EmailAppend:
+
+ # Pull out the record for the claimed user
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(uid=%s)"%(Name[1]),["uid","sn","cn"]);
+
+ # We require the UID surname to be someplace in the key name, this
+ # deals with special purpose keys like 'James Troup (Alternate Debian key)'
+ # Some people put their names backwards on their key too.. check that as well
+ if len(Attrs) == 1 and \
+ (string.find(string.lower(sn),string.lower(Attrs[0][1]["sn"][0])) != -1 or \
+ string.find(string.lower(cn),string.lower(Attrs[0][1]["sn"][0])) != -1):
+ print EmailAppend,"hit for",Name;
+ return Name[1];
+
+ # Attempt to give some best guess suggestions for use in editing the
+ # override file.
+ print "None for",Name;
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(sn~=%s)"%(sn),["uid","sn","cn"]);
+ for x in Attrs:
+ print " But might be:",x[1]["cn"][0],x[1]["sn"][0],"<" + x[1]["uid"][0] + "@debian.org>";
+ else:
+ return Attrs[0][1]["uid"][0];
+
+ return None;
+
+# Process options
+AdminUser = pwd.getpwuid(posix.getuid())[0];
+(options, arguments) = getopt.getopt(sys.argv[1:], "au:m:n")
+for (switch, val) in options:
+ if (switch == '-u'):
+ AdminUser = val
+ elif (switch == '-m'):
+ LoadOverride(val);
+ elif (switch == '-a'):
+ NoAct = 0;
+if len(arguments) == 0:
+ print "Give some keyrings to probe";
+ os.exit(0);
+
+# Main program starts here
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+if NoAct == 0:
+ print "Accessing LDAP directory as '" + AdminUser + "'";
+ Password = getpass(AdminUser + "'s password: ");
+ UserDn = "uid=" + AdminUser + "," + BaseDn;
+ l.simple_bind_s(UserDn,Password);
+else:
+ l.simple_bind_s("","");
+
+# Download the existing key list and put it into a map
+print "Fetching key list..",
+sys.stdout.flush();
+Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=*",["keyfingerprint","uid"]);
+KeyMap = {};
+KeyCount = {};
+for x in Attrs:
+ try:
+ # Sense a bad fingerprint.. Slapd has problems, it will store a null
+ # value that ldapsearch doesn't show up.. detect and remove
+ if len(x[1]["keyfingerprint"]) == 0 or x[1]["keyfingerprint"][0] == "":
+ print;
+ print "Fixing bad fingerprint for",x[1]["uid"][0],
+ sys.stdout.flush();
+ if NoAct == 0:
+ l.modify_s("uid="+x[1]["uid"][0]+","+BaseDn,\
+ [(ldap.MOD_DELETE,"keyfingerprint",None)]);
+ else:
+ for I in x[1]["keyfingerprint"]:
+ KeyMap[I] = [x[1]["uid"][0],0];
+ if KeyCount.has_key(x[1]["uid"][0]):
+ KeyCount[x[1]["uid"][0]] = KeyCount[x[1]["uid"][0]] + 1;
+ else:
+ KeyCount[x[1]["uid"][0]] = 1;
+ except:
+ continue;
+Attrs = None;
+print;
+
+# Popen GPG with the correct magic special options
+Args = [GPGPath] + GPGBasicOptions;
+for x in arguments:
+ Args.append("--keyring");
+ if string.find(x,"/") == -1:
+ Args.append("./"+x);
+ else:
+ Args.append(x);
+Args = Args + GPGSearchOptions + [" 2> /dev/null"]
+Keys = os.popen(string.join(Args," "),"r");
+
+# Loop over the GPG key file
+Outstanding = 0;
+Ignored = 0;
+while(1):
+ Line = Keys.readline();
+ if Line == "":
+ break;
+
+ Split = string.split(Line,":");
+ if len(Split) < 8 or Split[0] != "pub":
+ continue;
+
+ while (1):
+ Line2 = Keys.readline();
+ if Line2 == "":
+ break;
+ Split2 = string.split(Line2,":");
+ if len(Split2) < 11 or Split2[0] != "fpr":
+ continue;
+ break;
+ if Line2 == "":
+ break;
+
+ if KeyMap.has_key(Split2[9]):
+ Ignored = Ignored + 1;
+ # print "Ignoring keyID",Split2[9],"belonging to",KeyMap[Split2[9]][0];
+ KeyMap[Split2[9]][1] = 1;
+ continue;
+
+ Match = AddressSplit.match(Split[9]);
+ if Match == None:
+ UID = GetUID(l,("","",Split[9]));
+ else:
+ UID = GetUID(l,Match.groups());
+
+ if UID == None:
+ print "MISSING 0x" + Split2[9];
+ continue;
+
+ Rec = [(ldap.MOD_ADD,"keyfingerprint",Split2[9])];
+ Dn = "uid=" + UID + "," + BaseDn;
+ print "Adding keyID",Split2[9],"to",UID;
+ if KeyCount.has_key(UID):
+ KeyCount[UID] = KeyCount[UID] + 1;
+ else:
+ KeyCount[UID] = 1;
+
+ if NoAct == 1:
+ continue;
+
+ # Send the modify request
+ l.modify(Dn,Rec);
+ Outstanding = Outstanding + 1;
+ Outstanding = FlushOutstanding(l,Outstanding,1);
+ sys.stdout.flush();
+
+if NoAct == 0:
+ FlushOutstanding(l,Outstanding);
+
+if Keys.close() != None:
+ raise "Error","GPG failed"
+
+print Ignored,"keys already in the directory (ignored)";
+
+# Look for unmatched keys
+for x in KeyMap.keys():
+ if KeyMap[x][1] == 0:
+ print "keyID",x,"belonging to",KeyMap[x][0],"removed";
+ if KeyCount.has_key(KeyMap[x][0]) :
+ KeyCount[KeyMap[x][0]] = KeyCount[KeyMap[x][0]] - 1
+ if KeyCount[KeyMap[x][0]] <= 0:
+ print "**",KeyMap[x][0],"no longer has any keys";
+ if NoAct == 0:
+ l.modify_s("uid="+KeyMap[x][0]+","+BaseDn,\
+ [(ldap.MOD_DELETE,"keyfingerprint",x)]);
+
diff --git a/ud-homecheck b/ud-homecheck
new file mode 100755
index 0000000..dda3410
--- /dev/null
+++ b/ud-homecheck
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Checks a directory against the passwd file assuming it is the home
+# directory directory
+
+import string, ldap, getopt, sys, os, pwd;
+
+for x in os.listdir(sys.argv[1]):
+ try:
+ User = pwd.getpwnam(x);
+ st = os.stat(sys.argv[1]+x);
+ if User[2] != st[4] or User[3] != st[5]:
+ print "Bad ownership",x;
+ except:
+ print "Failed",x,"==> %s: %s" %(sys.exc_type,sys.exc_value);
+
diff --git a/ud-info b/ud-info
new file mode 100755
index 0000000..5e9603e
--- /dev/null
+++ b/ud-info
@@ -0,0 +1,359 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script is an interactive way to manipulate fields in the LDAP directory.
+# When run it connects to the directory using the current users ID and fetches
+# all the attributes for that user. It then formats them nicely and allows
+# the user to change them.
+# It is possible to authenticate as someone differnt than you are viewing/changing
+# this allows administrative functions and also allows users to view
+# restricted information about others, such as phone numbers and addresses.
+#
+# Usage: userinfo -a -u -c -r
+# -a Set the authentication user (the user whose password you are
+# going to enter)
+# -u Set the user to display
+# -c Set both -a and -u, use this if your login uid is not in the
+# database
+# -r Enable 'root' functions, do this if your uid has access to
+# restricted variables.
+#
+# http://www.geocode.com/eagle.html-ssi
+
+import string, time, posix, pwd, sys, getopt, ldap, crypt, whrandom, readline, copy;
+from userdir_ldap import *;
+
+RootMode = 0;
+AttrInfo = {"cn": ["First Name", 101],
+ "mn": ["Middle Name", 102],
+ "sn": ["Surname", 103],
+ "c": ["Country Code",1],
+ "l": ["Locality",2],
+ "ou": ["Membership",0],
+ "facsimiletelephonenumber": ["Fax Phone Number",3],
+ "telephonenumber": ["Phone Number",4],
+ "postaladdress": ["Mailing Address",5],
+ "postalcode": ["Postal Code",6],
+ "uid": ["Unix User ID",0],
+ "loginshell": ["Unix Shell",7],
+ "supplementarygid": ["Unix Groups",0],
+ "emailforward": ["Email Forwarding",8],
+ "ircnick": ["IRC Nickname",9],
+ "onvacation": ["Vacation Message",10],
+ "labeledurl": ["Home Page",11],
+ "latitude": ["Latitude",12],
+ "longitude": ["Longitude",13],
+ "comment": ["Comment",114],
+ "userpassword": ["Crypted Password",115]};
+
+AttrPrompt = {"cn": ["Common name or first name"],
+ "mn": ["Middle name (or initial if it ends in a dot)"],
+ "sn": ["Surname or last name"],
+ "c": ["ISO 2 letter country code, such as US, DE, etc"],
+ "l": ["City name, State/Provice (Locality)\n e.g. Dallas, Texas"],
+ "facsimiletelephonenumber": ["Fax phone number, with area code and country code"],
+ "telephonenumber": ["Voice phone number"],
+ "postaladdress": ["Complete mailing address including postal codes and country designations\nSeperate lines using a $ character"],
+ "postalcode": ["Postal Code or Zip Code"],
+ "loginshell": ["Login shell with full path (no check is done for validity)"],
+ "emailforward": ["EMail address to send all mail to or blank to disable"],
+ "ircnick": ["IRC nickname if you use IRC"],
+ "onvacation": ["A message if on vaction, indicating the time of departure and return"],
+ "userpassword": ["The users Crypt'd password"],
+ "comment": ["Admin Comment about the account"],
+ "supplementarygid": ["Groups the user is in"],
+ "latitude": ["XEarth latitude in ISO 6709 format - see /usr/share/zoneinfo/zone.tab or etak.com"],
+ "longitude": ["XEarth latitude in ISO 6709 format - see /usr/share/zoneinfo/zone.tab or etak.com"],
+ "labeledurl": ["Web home page"]};
+
+# Create a map of IDs to desc,value,attr
+OrderedIndex = {};
+for at in AttrInfo.keys():
+ if (AttrInfo[at][1] != 0):
+ OrderedIndex[AttrInfo[at][1]] = [AttrInfo[at][0], "", at];
+OrigOrderedIndex = copy.deepcopy(OrderedIndex);
+
+# Show shadow information
+def PrintShadow(Attrs):
+ Changed = int(GetAttr(Attrs,"shadowlastchange","0"));
+ MinDays = int(GetAttr(Attrs,"shadowmin","0"));
+ MaxDays = int(GetAttr(Attrs,"shadowmax","0"));
+ WarnDays = int(GetAttr(Attrs,"shadowwarning","0"));
+ InactDays = int(GetAttr(Attrs,"shadowinactive","0"));
+ Expire = int(GetAttr(Attrs,"shadowexpire","0"));
+
+ print "%-24s:" % ("Password last changed"),
+ print time.strftime("%a %d/%m/%Y %Z",time.localtime(Changed*24*60*60));
+ if (Expire > 0):
+ print "%-24s:" % ("Account expires on"),
+ print time.strftime("%a %d/%m/%Y %Z",time.localtime(Expire*24*60*60));
+ if (InactDays >= 0 and MaxDays < 99999):
+ print "Account aging is active, you must change your password every", MaxDays, "days."
+
+# Print out the automatic time stamp information
+def PrintModTime(Attrs):
+ Stamp = GetAttr(Attrs,"modifytimestamp","");
+ if len(Stamp) >= 13:
+ Time = (int(Stamp[0:4]),int(Stamp[4:6]),int(Stamp[6:8]),
+ int(Stamp[8:10]),int(Stamp[10:12]),int(Stamp[12:14]),0,0,-1);
+ print "%-24s:" % ("Record last modified on"), time.strftime("%a %d/%m/%Y %X UTC",Time),
+ print "by",ldap.explode_dn(GetAttr(Attrs,"modifiersname"),1)[0];
+
+ Stamp = GetAttr(Attrs,"createtimestamp","");
+ if len(Stamp) >= 13:
+ Time = (int(Stamp[0:4]),int(Stamp[4:6]),int(Stamp[6:8]),
+ int(Stamp[8:10]),int(Stamp[10:12]),int(Stamp[12:14]),0,0,-1);
+ print "%-24s:" % ("Record created on"), time.strftime("%a %d/%m/%Y %X UTC",Time);
+
+# Print the PGP key for a user
+def PrintKeys(Attrs):
+ if Attrs[1].has_key("keyfingerprint") == 0:
+ return;
+ First = 0;
+ for x in Attrs[1]["keyfingerprint"]:
+ if First == 0:
+ print "%-24s:" % ("PGP/GPG Key Fingerprints"),
+ First = 1;
+ else:
+ print "%-24s:" % (""),
+
+ # PGP Print
+ if (len(x) == 32):
+ I = 0;
+ while (I < len(x)):
+ print x[I]+x[I+1],
+ I = I + 2;
+ if I == 32/2:
+ print "",
+ elif (len(x) == 40):
+ # GPG Print
+ I = 0;
+ while (I < len(x)):
+ print x[I]+x[I+1]+x[I+2]+x[I+3],
+ I = I + 4;
+ if I == 40/2:
+ print "",
+ else:
+ print x,
+ print;
+
+# Display all of the attributes in a numbered list
+def ShowAttrs(Attrs):
+ print;
+ print EmailAddress(Attrs);
+ PrintModTime(Attrs);
+ PrintShadow(Attrs);
+ PrintKeys(Attrs);
+
+ for at in Attrs[1].keys():
+ if AttrInfo.has_key(at):
+ if AttrInfo[at][1] == 0:
+ print " %-18s:" % (AttrInfo[at][0]),
+ for x in Attrs[1][at]:
+ print "'%s'" % (x),
+ if at == "uid":
+ print "(id=%s, gid=%s)" % (GetAttr(Attrs,"uidnumber","-1"),GetAttr(Attrs,"gidnumber","-1")),
+ print;
+ else:
+ OrderedIndex[AttrInfo[at][1]][1] = Attrs[1][at];
+
+ Keys = OrderedIndex.keys();
+ Keys.sort();
+ for at in Keys:
+ if at < 100 or RootMode != 0:
+ print " %3u) %-18s: " % (at,OrderedIndex[at][0]),
+ for x in OrderedIndex[at][1]:
+ print "'%s'" % (re.sub('[\n\r]','?',x)),
+ print;
+
+# Change a single attribute
+def ChangeAttr(Attrs,Attr):
+ if (Attr == "supplementarygid"):
+ return MultiChangeAttr(Attrs,Attr);
+
+ print "Old value: '%s'" % (GetAttr(Attrs,Attr,""));
+ print "Press enter to leave unchanged and a single space to set to empty";
+ NewValue = raw_input("New? ");
+
+ # Empty string
+ if (NewValue == ""):
+ print "Leaving unchanged.";
+ return;
+
+ # Single space designates delete, trap the delete error
+ if (NewValue == " "):
+ print "Deleting.",;
+ try:
+ l.modify_s(UserDn,[(ldap.MOD_DELETE,Attr,None)]);
+ except ldap.NO_SUCH_ATTRIBUTE:
+ pass;
+
+ print;
+ Attrs[1][Attr] = [""];
+ return;
+
+ # Set a new value
+ print "Setting.",;
+ l.modify_s(UserDn,[(ldap.MOD_REPLACE,Attr,NewValue)]);
+ Attrs[1][Attr] = [NewValue];
+ print;
+
+def MultiChangeAttr(Attrs,Attr):
+ # Make sure that we have an entry
+ if not Attrs[1].has_key(Attr):
+ Attrs[1][Attr] = [];
+
+ Attrs[1][Attr].sort();
+ print "Old values: ",Attrs[1][Attr];
+
+ Mode = string.upper(raw_input("[D]elete or [A]dd? "));
+ if (Mode != 'D' and Mode != 'A'):
+ return;
+
+ NewValue = raw_input("Value? ");
+ # Empty string
+ if (NewValue == ""):
+ print "Leaving unchanged.";
+ return;
+
+ # Delete
+ if (Mode == "D"):
+ print "Deleting.",;
+ try:
+ l.modify_s(UserDn,[(ldap.MOD_DELETE,Attr,NewValue)]);
+ except ldap.NO_SUCH_ATTRIBUTE:
+ print "Failed";
+
+ print;
+ Attrs[1][Attr].remove(NewValue);
+ return;
+
+ # Set a new value
+ print "Setting.",;
+ l.modify_s(UserDn,[(ldap.MOD_ADD,Attr,NewValue)]);
+ Attrs[1][Attr].append(NewValue);
+ print;
+
+# Main program starts here
+User = pwd.getpwuid(posix.getuid())[0];
+BindUser = User;
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "nu:c:a:r")
+for (switch, val) in options:
+ if (switch == '-u'):
+ User = val;
+ elif (switch == '-a'):
+ BindUser = val;
+ elif (switch == '-c'):
+ BindUser = val;
+ User = val;
+ elif (switch == '-r'):
+ RootMode = 1;
+ elif (switch == '-n'):
+ BindUser = "";
+
+if (BindUser != ""):
+ print "Accessing LDAP entry for '" + User + "'",
+if (BindUser != User):
+ if (BindUser != ""):
+ print "as '" + BindUser + "'";
+else:
+ print;
+if (BindUser != ""):
+ Password = getpass(BindUser + "'s password: ");
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+UserDn = "uid=" + BindUser + "," + BaseDn;
+if (BindUser != ""):
+ l.simple_bind_s(UserDn,Password);
+else:
+ l.simple_bind_s("","");
+UserDn = "uid=" + User + "," + BaseDn;
+
+# Enable changing of supplementary gid's
+if (RootMode == 1):
+ AttrInfo["supplementarygid"][1] = 100;
+ OrderedIndex[AttrInfo["supplementarygid"][1]] = [AttrInfo["supplementarygid"][0], "","supplementarygid"];
+ OrigOrderedIndex[AttrInfo["supplementarygid"][1]] = [AttrInfo["supplementarygid"][0], "","supplementarygid"];
+
+# Query the server for all of the attributes
+Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=" + User);
+
+# repeatedly show the account configuration
+while(1):
+ ShowAttrs(Attrs[0]);
+ if (BindUser == ""):
+ sys.exit(0);
+
+ if RootMode == 1:
+ print " a) Arbitary Change";
+ print " p) Change Password";
+ print " u) Switch Users";
+ print " x) Exit";
+
+ # Prompt
+ Response = raw_input("Change? ");
+ if (Response == "x" or Response == "X" or Response == "q" or
+ Response == "quit" or Response == "exit"):
+ break;
+
+ # Change who we are looking at
+ if (Response == 'u' or Response == 'U'):
+ NewUser = raw_input("User? ");
+ if NewUser == "":
+ continue;
+ User = NewUser;
+ UserDn = "uid=" + User + "," + BaseDn;
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=" + User);
+ OrderedIndex = copy.deepcopy(OrigOrderedIndex);
+ continue;
+
+ # Handle changing the password
+ if (Response == "p"):
+ print "Please enter a new password. Your password can be of unlimited length,";
+ print "contain spaces and other special characters. No checking is done on the";
+ print "strength of the passwords so pick good ones please!";
+
+ Pass1 = getpass(User + "'s new password: ");
+ Pass2 = getpass(User + "'s new password again: ");
+ if Pass1 != Pass2:
+ print "Passwords did not match";
+ raw_input("Press a key");
+ continue;
+
+ # Hash it telling glibc to use the MD5 algorithm - if you dont have
+ # glibc then just change Salt = "$1$" to Salt = "";
+ SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/.";
+ Salt = "$1$";
+ for x in range(0,10):
+ Salt = Salt + SaltVals[whrandom.randint(0,len(SaltVals)-1)];
+ Pass = crypt.crypt(Pass1,Salt);
+ if len(Pass) < 14:
+ print "Caution! MD5 Password hashing failed, not changing password!";
+ raw_input("Press a key");
+ continue;
+
+ print "Setting password..";
+ Pass = "{crypt}" + Pass;
+ l.modify_s(UserDn,[(ldap.MOD_REPLACE,"userpassword",Pass)]);
+ continue;
+
+ # Handle changing an arbitary value
+ if (Response == "a"):
+ Attr = raw_input("Attr? ");
+ ChangeAttr(Attrs[0],Attr);
+ continue;
+
+ # Convert the integer response
+ try:
+ ID = int(Response);
+ if (not OrderedIndex.has_key(ID) or (ID > 100 and RootMode == 0)):
+ raise ValueError;
+ except ValueError:
+ print "Invalid";
+ continue;
+
+ # Print the what to do prompt
+ print "Changing LDAP entry '%s' (%s)" % (OrderedIndex[ID][0],OrderedIndex[ID][2]);
+ print AttrPrompt[OrderedIndex[ID][2]][0];
+ ChangeAttr(Attrs[0],OrderedIndex[ID][2]);
diff --git a/ud-ldapshow b/ud-ldapshow
new file mode 100755
index 0000000..5faef77
--- /dev/null
+++ b/ud-ldapshow
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Show some reports from the ldap database
+# Call with nokey to generate a missing key report
+# Call with noforward to generate a missing .forward report
+
+import string, re, time, ldap, getopt, sys;
+from userdir_ldap import *;
+
+def ShowDups(Attrs,Len):
+ for x in Attrs:
+ if x[1].has_key("keyfingerprint") == 0:
+ continue;
+
+ Count = 0;
+ for I in x[1]["keyfingerprint"]:
+ if len(I) == Len:
+ Count = Count + 1;
+ if Count > 1:
+ for I in x[1]["keyfingerprint"]:
+ if len(I) == Len:
+ print "%s: %s" % (EmailAddress(x),I);
+
+# Main program starts here
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "")
+for (switch, val) in options:
+ if (switch == '-a'):
+ DoAdd = 1;
+
+print "Connecting to LDAP directory";
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+l.simple_bind_s("","");
+
+if arguments[0] == "nokey":
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(!(keyfingerprint=*))",\
+ ["uid","cn","sn","emailforward","comment"]);
+ Attrs.sort();
+ for x in Attrs:
+ print "Key Missing:",EmailAddress(x);
+ if GetAttr(x,"emailforward") != "":
+ print " ->",GetAttr(x,"emailforward");
+ if GetAttr(x,"comment") != "":
+ print " :",GetAttr(x,"comment");
+
+if arguments[0] == "noforward":
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(!(emailforward=*))",\
+ ["uid","cn","sn","emailforward","comment"]);
+ Attrs.sort();
+ for x in Attrs:
+ print "No Forward:",EmailAddress(x);
+
+if arguments[0] == "badpriv":
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(!(keyfingerprint=*))(privatesub=*))",\
+ ["uid","cn","sn","privatesub"]);
+ Attrs.sort();
+ for x in Attrs:
+ print EmailAddress(x)+": "+GetAttr(x,"privatesub");
+
+if arguments[0] == "nopriv":
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(keyfingerprint=*)(!(privatesub=*)))",\
+ ["uid","cn","sn","privatesub"]);
+ Attrs.sort();
+ for x in Attrs:
+ print " ",EmailAddress(x)+": "+GetAttr(x,"privatesub");
+
+if arguments[0] == "keymap":
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
+ ["uid","cn","sn","keyfingerprint"]);
+ Attrs.sort();
+ for x in Attrs:
+ if x[1].has_key("keyfingerprint"):
+ for I in x[1]["keyfingerprint"]:
+ print "%s: %s" % (EmailAddress(x),I);
+
+if arguments[0] == "devcount":
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(keyfingerprint=*)(gidnumber=800))",\
+ ["uid"]);
+ Count = 0;
+ for x in Attrs:
+ Count = Count + 1;
+ print "There are",Count,"developers as of",time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
+
+if arguments[0] == "multikeys":
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
+ ["uid","cn","sn","keyfingerprint"]);
+ Attrs.sort();
+
+
+ print "--- PGP Keys ---"
+ ShowDups(Attrs,32);
+ print "--- GPG Keys ---"
+ ShowDups(Attrs,40);
+
diff --git a/ud-mailgate b/ud-mailgate
new file mode 100755
index 0000000..922a1bb
--- /dev/null
+++ b/ud-mailgate
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+import userdir_gpg, userdir_ldap, sys, traceback, time, ldap, posix;
+import string, pwd
+from userdir_gpg import *;
+from userdir_ldap import *;
+
+# Error codes from /usr/include/sysexits.h
+ReplyTo = ConfModule.replyto;
+PingFrom = ConfModule.pingfrom;
+ChPassFrom = ConfModule.chpassfrom;
+ReplayCacheFile = ConfModule.replaycachefile;
+
+EX_TEMPFAIL = 75;
+EX_PERMFAIL = 65; # EX_DATAERR
+Error = 'Message Error';
+
+# Handle ping handles an email sent to the 'ping' address (ie this program
+# called with a ping argument) It replies with a dump of the public records.
+def HandlePing(Reply,DnRecord,Key):
+ Subst = {};
+ Subst["__FROM__"] = PingFrom;
+ Subst["__EMAIL__"] = EmailAddress(DnRecord);
+ Subst["__LDAPFIELDS__"] = PrettyShow(Attrs[0]);
+ Subst["__ADMIN__"] = ReplyTo;
+
+ return Reply + TemplateSubst(Subst,open(TemplatesDir+"ping-reply","r").read());
+
+# Handle a change password email sent to the change password address
+# (this program called with the chpass argument)
+def HandleChPass(Reply,DnRecord,Key):
+ # Generate a random password
+ Password = GenPass();
+ Pass = HashPass(Password);
+
+ # Use GPG to encrypt it
+ Message = GPGEncrypt("Your new password is '" + Password + "'\n",\
+ "0x"+Key[1],Key[4]);
+ Password = None;
+
+ if Message == None:
+ raise Error, "Unable to generate the encrypted reply, gpg failed.";
+
+ if (Key[4] == 1):
+ Type = "Your message was encrypted using PGP 2.x\ncompatibility mode.";
+ else:
+ Type = "Your message was encrypted using GPG (OpenPGP)\ncompatibility "\
+ "mode, without IDEA. This message cannot be decoded using PGP 2.x";
+
+ Subst = {};
+ Subst["__FROM__"] = ChPassFrom;
+ Subst["__EMAIL__"] = EmailAddress(DnRecord);
+ Subst["__CRYPTTYPE__"] = Type;
+ Subst["__PASSWORD__"] = Message;
+ Subst["__ADMIN__"] = ReplyTo;
+ Reply = Reply + TemplateSubst(Subst,open(TemplatesDir+"passwd-changed","r").read());
+
+ # Connect to the ldap server
+ l = ldap.open(LDAPServer);
+ F = open(PassDir+"/pass-"+pwd.getpwuid(posix.getuid())[0],"r");
+ AccessPass = string.split(string.strip(F.readline())," ");
+ F.close();
+
+ # Modify the password
+ l.simple_bind_s("uid="+AccessPass[0]+","+BaseDn,AccessPass[1]);
+ Rec = [(ldap.MOD_REPLACE,"userPassword","{crypt}"+Pass)];
+ Dn = "uid=" + GetAttr(DnRecord,"uid") + "," + BaseDn;
+ l.modify_s(Dn,Rec);
+
+ return Reply;
+
+# Start of main program
+ErrMsg = "Indeterminate Error";
+ErrType = EX_TEMPFAIL;
+try:
+ # Startup the replay cache
+ ErrType = EX_TEMPFAIL;
+ ErrMsg = "Failed to initialize the replay cache:";
+ RC = ReplayCache(ReplayCacheFile);
+ RC.Clean();
+
+ # Get the email
+ ErrType = EX_PERMFAIL;
+ ErrMsg = "Failed to understand the email or find a signature:";
+ Email = mimetools.Message(sys.stdin,0);
+ Msg = GetClearSig(Email);
+
+ # Check the signature
+ ErrMsg = "Unable to check the signature or the signature was invalid:";
+ Res = GPGCheckSig(Msg[0]);
+
+ if Res[0] != None:
+ raise Error, Res[0];
+
+ if Res[3] == None:
+ raise Error, "Null signature text";
+
+ # Extract the plain message text in the event of mime encoding
+ ErrMsg = "Problem stripping MIME headers from the decoded message"
+ if Msg[1] == 1:
+ try:
+ Index = string.index(Res[3],"\n\n") + 2;
+ except ValueError:
+ Index = string.index(Res[3],"\n\r\n") + 3;
+ PlainText = Res[3][Index:];
+ else:
+ PlainText = Res[3];
+
+ # Check the signature against the replay cache
+ ErrMsg = "The replay cache rejected your message. Check your clock!";
+ Rply = RC.Check(Res[1]);
+ if Rply != None:
+ raise Error, Rply;
+ RC.Add(Res[1]);
+
+ # Connect to the ldap server
+ ErrType = EX_TEMPFAIL;
+ ErrMsg = "An error occured while performing the LDAP lookup";
+ l = ldap.open(LDAPServer);
+ l.simple_bind_s("","");
+
+ # Search for the matching key fingerprint
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=" + Res[2][1]);
+ if len(Attrs) == 0:
+ raise Error, "Key not found"
+ if len(Attrs) != 1:
+ raise Error, "Oddly your key fingerprint is assigned to more than one account.."
+
+ # Determine the sender address
+ ErrType = EX_PERMFAIL;
+ ErrMsg = "A problem occured while trying to formulate the reply";
+ Sender = Email.getheader("Reply-To");
+ if Sender == None:
+ Sender = Email.getheader("From");
+ if Sender == None:
+ raise Error, "Unable to determine the sender's address";
+
+ # Formulate a reply
+ Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
+ Reply = "To: %s\nReply-To: %s\nDate: %s\n" % (Sender,ReplyTo,Date);
+
+ # Dispatch
+ if sys.argv[1] == "ping":
+ Reply = HandlePing(Reply,Attrs[0],Res[2]);
+ elif sys.argv[1] == "chpass":
+ if string.find(string.strip(PlainText),"Please change my Debian password") != 0:
+ raise Error,"Please send a signed message where the first line of text is the string 'Please change my Debian password'";
+ Reply = HandleChPass(Reply,Attrs[0],Res[2]);
+ else:
+ print sys.argv;
+ raise Error, "Incorrect Invokation";
+
+ # Send the message through sendmail
+ ErrMsg = "A problem occured while trying to send the reply";
+ Child = posix.popen("/usr/sbin/sendmail -t","w");
+# Child = posix.popen("cat","w");
+ Child.write(Reply);
+ if Child.close() != None:
+ raise Error, "Sendmail gave a non-zero return code";
+
+except:
+ print ErrMsg;
+ print "==> %s: %s" %(sys.exc_type,sys.exc_value);
+ List = traceback.extract_tb(sys.exc_traceback);
+ if len(List) > 1:
+ print "Trace: ";
+ for x in List:
+ print " %s %s:%u: %s" %(x[2],x[0],x[1],x[3]);
+ sys.exit(ErrType);
+
diff --git a/ud-passchk b/ud-passchk
new file mode 100755
index 0000000..6929f4f
--- /dev/null
+++ b/ud-passchk
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Checks the passwd file to make sure all entries are in the directory
+
+import string, ldap, getopt, sys, os;
+from userdir_ldap import *;
+
+def PassCheck(l,File,HomePrefix):
+ F = open(File,"r");
+
+ # Fetch all the users and generate a map out of them
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
+ ["uid","uidnumber","gidnumber","loginshell"]);
+ UIDMap = {};
+ for x in Attrs:
+ if x[1].has_key("uid") == 0:
+ continue;
+ UIDMap[x[1]["uid"][0]] = x[1];
+
+ # Iterate over every user in the passwd file
+ while(1):
+ Line = F.readline();
+ if Line == "":
+ break;
+
+ Split = string.split(Line,":");
+ if UIDMap.has_key(Split[0]) == 0:
+ print Line,
+ continue;
+
+ Ats = UIDMap[Split[0]];
+ Miss = [];
+ if Ats.has_key("uidnumber") and Ats["uidnumber"][0] != Split[2]:
+ Miss.append("UID");
+ if Ats.has_key("uidnumber") and Ats["gidnumber"][0] != Split[3]:
+ Miss.append("GID");
+ if Ats.has_key("homedirectory") and \
+ split[5] != HomePrefix + Split[0]:
+ Miss.append("Home");
+ if len(Miss) != 0:
+ print "mismatch",Split[0],Miss;
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+l.simple_bind_s("","");
+
+PassCheck(l,sys.argv[1],sys.argv[2]);
diff --git a/ud-replicate b/ud-replicate
new file mode 100755
index 0000000..c69c6b8
--- /dev/null
+++ b/ud-replicate
@@ -0,0 +1,12 @@
+#! /bin/sh
+# The rsync source host needs to be customized..
+set -e
+
+HOST=`hostname -f`
+cd /tmp/
+cd /var/lib/misc > /dev/null 2>&1 || cd /var/state/glibc/ > /dev/null 2>&1 || cd /var/db/ > /dev/null 2>&1
+lockfile -r 1 -l 3600 lock
+rsync -e ssh -rp sshdist@samosa:/var/cache/userdir-ldap/hosts/$HOST . > /dev/null 2>&1
+makedb $HOST/passwd.tdb -o passwd.db > /dev/null 2>&1
+makedb $HOST/shadow.tdb -o shadow.db > /dev/null 2>&1
+makedb $HOST/group.tdb -o group.db > /dev/null 2>&1
diff --git a/ud-useradd b/ud-useradd
new file mode 100755
index 0000000..b875470
--- /dev/null
+++ b/ud-useradd
@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+
+import string, re, time, ldap, getopt, sys, posix, pwd;
+from userdir_ldap import *;
+from userdir_gpg import *;
+
+AddressSplit = re.compile("(.*).*<([^@]*)@([^>]*)>");
+
+# This tries to search for a free UID. There are two possible ways to do
+# this, one is to fetch all the entires and pick the highest, the other
+# is to randomly guess uids until one is free. This uses the formar.
+# Regrettably ldap doesn't have an integer attribute comparision function
+# so we can only cut the search down slightly
+def GetFreeID(l):
+ HighestUID = 1400;
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uidnumber>="+str(HighestUID),["uidnumber"]);
+ HighestUID = 0;
+ for I in Attrs:
+ ID = int(GetAttr(I,"uidnumber","0"));
+ if ID > HighestUID:
+ HighestUID = ID;
+ return HighestUID + 1;
+
+# Main starts here
+
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "u:")
+for (switch, val) in options:
+ if (switch == '-u'):
+ AdminUser = val
+
+print "Accessing LDAP directory as '" + AdminUser + "'";
+Password = getpass(AdminUser + "'s password: ");
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+UserDn = "uid=" + AdminUser + "," + BaseDn;
+l.simple_bind_s(UserDn,Password);
+
+# Locate the key of the user we are adding
+GPGBasicOptions[0] = "--batch" # Permit loading of the config file
+while (1):
+ Foo = raw_input("Who are you going to add (for a GPG search)? ");
+ if Foo == "":
+ continue;
+
+ Keys = GPGKeySearch(Foo);
+
+ if len(Keys) == 0:
+ print "Sorry, that search did not turn up any keys";
+ continue;
+ if len(Keys) > 1:
+ print "Sorry, more than one key was found, please specify the key to use by\nfingerprint:";
+ for i in Keys:
+ GPGPrintKeyInfo(i);
+ continue;
+
+ print
+ print "A matching key was found:"
+ GPGPrintKeyInfo(Keys[0]);
+ break;
+
+# Crack up the email address from the key into a best guess
+# first/middle/last name
+Match = AddressSplit.match(Keys[0][2]);
+if Match == None:
+ (cn,mn,sn,email,account) = ('','','','','');
+else:
+ (cn,mn,sn) = NameSplit(re.sub('["]','',Match.groups()[0]))
+ email = Match.groups()[1] + '@' + Match.groups()[2];
+ account = Match.groups()[1];
+
+privsub = email;
+gidnumber = str(DefaultGID);
+uidnumber = 0;
+
+# Decide if we should use IDEA encryption
+UsePGP2 = 0;
+while len(Keys[0][1]) < 40:
+ Res = raw_input("Use PGP2.x compatibility [no]? ");
+ if Res == "yes":
+ UsePGP2 = 1;
+ break;
+ if Res == "":
+ break;
+
+# Try to get a uniq account name
+Update=0
+while 1:
+ Res = raw_input("Login account [" + account + "]? ");
+ if Res != "":
+ account = Res;
+ Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=" + account);
+ if len(Attrs) == 0:
+ break;
+ Res = raw_input("That account already exists, update [no]? ");
+ if Res == "yes":
+ # Update mode, fetch the default values from the directory
+ Update = 1;
+ privsub = GetAttr(Attrs[0],"privatesub");
+ gidnumber = GetAttr(Attrs[0],"gidnumber");
+ uidnumber = GetAttr(Attrs[0],"uidnumber");
+ email = GetAttr(Attrs[0],"emailforward");
+ cn = GetAttr(Attrs[0],"cn");
+ sn = GetAttr(Attrs[0],"sn");
+ mn = GetAttr(Attrs[0],"mn");
+ if privsub == None or privsub == "":
+ privsub = " ";
+ break;
+
+# Prompt for the first/last name and email address
+Res = raw_input("First name [" + cn + "]? ");
+if Res != "":
+ cn = Res;
+Res = raw_input("Middle name [" + mn + "]? ");
+if Res != "":
+ mn = Res;
+Res = raw_input("Last name [" + sn + "]? ");
+if Res != "":
+ sn = Res;
+Res = raw_input("Email forwarding address [" + email + "]? ");
+if Res != "":
+ email = Res;
+
+# Debian-Private subscription
+Res = raw_input("Subscribe to debian-private (space is none) [" + privsub + "]? ");
+if Res != "":
+ privsub = Res;
+
+# GID
+Res = raw_input("Group ID Number [" + gidnumber + "]? ");
+if Res != "":
+ gidnumber = Res;
+
+# UID
+if uidnumber == 0:
+ uidnumber = GetFreeID(l);
+
+# Generate a random password
+if Update == 0:
+ Password = raw_input("User's Password (Enter for random)? ");
+
+ if Password == "":
+ print "Randomizing and encrypting password"
+ Password = GenPass();
+ Pass = HashPass(Password);
+ print "PASS: ", Password;
+
+ # Use GPG to encrypt it, pass the fingerprint to ID it
+ CryptedPass = GPGEncrypt("Your new password is '" + Password + "'\n",\
+ "0x"+Keys[0][1],UsePGP2);
+ Password = None;
+ if CryptedPass == None:
+ raise "Error","Password Encryption failed"
+ else:
+ Pass = HashPass(Password);
+ CryptedPass = "Your password has been set to the previously agreed value.";
+else:
+ CryptedPass = "";
+ Pass = None;
+
+# Now we have all the bits of information.
+if mn != "":
+ FullName = "%s %s %s" % (cn,mn,sn);
+else:
+ FullName = "%s %s" % (cn,sn);
+print "------------";
+print "Final information collected:"
+print " %s <%s@%s>:" % (FullName,account,EmailAppend);
+print " Assigned UID:",uidnumber," GID:", gidnumber;
+print " Email forwarded to:",email;
+print " Private Subscription:",privsub;
+print " GECOS Field: \"%s,,,,\"" % (FullName);
+print " Login Shell: /bin/bash";
+print " Key Fingerprint:",Keys[0][1];
+Res = raw_input("Continue [no]? ");
+if Res != "yes":
+ sys.exit(1);
+
+# Initialize the substitution Map
+Subst = {}
+Subst["__REALNAME__"] = FullName;
+Subst["__WHOAMI__"] = pwd.getpwuid(posix.getuid())[0];
+Subst["__DATE__"] = time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
+Subst["__LOGIN__"] = account;
+Subst["__PRIVATE__"] = privsub;
+Subst["__EMAIL__"] = email;
+Subst["__PASSWORD__"] = CryptedPass;
+Subst["__LISTPASS__"] = string.strip(open(pwd.getpwuid(posix.getuid())[5]+"/.debian-lists_passwd","r").read());
+
+# Generate the LDAP request
+Rec = [(ldap.MOD_REPLACE,"uid",account),
+ (ldap.MOD_REPLACE,"uidNumber",str(uidnumber)),
+ (ldap.MOD_REPLACE,"gidNumber",str(gidnumber)),
+ (ldap.MOD_REPLACE,"gecos",FullName+",,,,"),
+ (ldap.MOD_REPLACE,"loginShell","/bin/bash"),
+ (ldap.MOD_REPLACE,"keyfingerprint",Keys[0][1]),
+ (ldap.MOD_REPLACE,"cn",cn),
+ (ldap.MOD_REPLACE,"mn",mn),
+ (ldap.MOD_REPLACE,"sn",sn),
+ (ldap.MOD_REPLACE,"emailforward",email),
+ (ldap.MOD_REPLACE,"shadowLastChange",str(int(time.time()/24/60/60))),
+ (ldap.MOD_REPLACE,"shadowMin","0"),
+ (ldap.MOD_REPLACE,"shadowMax","99999"),
+ (ldap.MOD_REPLACE,"shadowWarning","7"),
+ (ldap.MOD_REPLACE,"shadowInactive",""),
+ (ldap.MOD_REPLACE,"shadowExpire","")];
+if privsub != " ":
+ Rec.append((ldap.MOD_REPLACE,"privatesub",privsub));
+if Pass != None:
+ Rec.append((ldap.MOD_REPLACE,"userPassword","{crypt}"+Pass));
+
+# Submit the modification request
+Dn = "uid=" + account + "," + BaseDn;
+print "Updating LDAP directory..",
+sys.stdout.flush();
+try:
+ l.add_s(Dn,[("uid",account),
+ ("objectclass","top"),
+ ("objectclass","account"),
+ ("objectclass","posixAccount"),
+ ("objectclass","shadowAccount"),
+ ("objectclass","debiandeveloper")]);
+except ldap.ALREADY_EXISTS:
+ pass;
+
+# Send the modify request
+l.modify_s(Dn,Rec);
+print;
+
+# Abort email sends for an update operation
+if Update == 1:
+ print "Account is not new, Not sending mails"
+ sys.exit(0);
+
+# Do the subscription/welcome message
+if privsub != " ":
+ print TemplateSubst(Subst,open("templates/list-subscribe","r").read());
+
+# Send the Welcome message
+print "Sending Welcome Email"
+Reply = TemplateSubst(Subst,open("templates/welcome-message-"+gidnumber,"r").read());
+Child = posix.popen("/usr/sbin/sendmail -t","w");
+#Child = posix.popen("cat","w");
+Child.write(Reply);
+if Child.close() != None:
+ raise Error, "Sendmail gave a non-zero return code";
diff --git a/ud-userimport b/ud-userimport
new file mode 100755
index 0000000..2e6f903
--- /dev/null
+++ b/ud-userimport
@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Imports passwd, shadow and group files into the directory.
+# You should cleanse the files of anything you do not want to add to the
+# directory.
+#
+# The first step is to call this script to import the passwd file and
+# create all the new entries. This should be done on an empty freshly
+# initialized directory with the rootdn/password set in the server.
+# The command to execute is
+# ldapimport -a -p ~/passwd
+# The -a tells the script to add all the entries it finds, it should be
+# used only once.
+#
+# The next step is to import the shadow file and group, no clensing need be
+# done for
+# this as any entries that do not exist will be ignored (silently)
+# ldapimport -s /etc/shadow -g /etc/group
+#
+
+import string, re, time, ldap, getopt, sys;
+from userdir_ldap import *;
+
+DoAdd = 0;
+WritePasses = 1;
+Passwd = "";
+Shadow = "";
+Group = "";
+
+# This parses a gecos field and returns a tuple containing the new normalized
+# field and the first, middle and last name of the user. Gecos is formed
+# in the standard debian manner with 5 feilds seperated by commas
+def ParseGecos(Field):
+ Gecos = re.split("[,:]",Field);
+ cn = "";
+ mn = "";
+ sn = "";
+ if (len(Gecos) >= 1):
+ (cn,mn,sn) = NameSplit(Gecos[0]);
+
+ # Normalize the gecos field
+ if (len(Gecos) > 5):
+ Gecos = Gecos[0:4];
+ else:
+ while (len(Gecos) < 5):
+ Gecos.append("");
+ else:
+ Gecos = ["","","","",""];
+
+ # Reconstruct the gecos after mauling it
+ Field = Gecos[0] + "," + Gecos[1] + "," + Gecos[2] + "," + \
+ Gecos[3] + "," + Gecos[4];
+ return (Field,cn,mn,sn);
+
+# Check if a number string is really a number
+def CheckNumber(Num):
+ for x in Num:
+ string.index(string.digits,x);
+
+# Read the passwd file into the database
+def DoPasswd(l,Passwd):
+ # Read the passwd file and import it
+ Passwd = open(Passwd,"r");
+ Outstanding = 0;
+ while(1):
+ Line = Passwd.readline();
+ if Line == "":
+ break;
+
+ Split = re.split("[:\n]",Line);
+ (Split[4],cn,mn,sn) = ParseGecos(Split[4]);
+ CheckNumber(Split[2]);
+ CheckNumber(Split[3]);
+ Rec = [(ldap.MOD_REPLACE,"uid",Split[0]),
+ (ldap.MOD_REPLACE,"uidNumber",Split[2]),
+ (ldap.MOD_REPLACE,"gidNumber",Split[3]),
+ (ldap.MOD_REPLACE,"gecos",Split[4]),
+ (ldap.MOD_REPLACE,"homeDirectory",Split[5]),
+ (ldap.MOD_REPLACE,"loginShell",Split[6]),
+ (ldap.MOD_REPLACE,"cn",cn),
+ (ldap.MOD_REPLACE,"mn",mn),
+ (ldap.MOD_REPLACE,"sn",sn)];
+
+ Dn = "uid=" + Split[0] + "," + BaseDn;
+ print "Importing",Dn,
+ sys.stdout.flush();
+
+ # Unfortunately add_s does not take the same args as modify :|
+ if (DoAdd == 1):
+ try:
+ l.add_s(Dn,[("uid",Split[0]),
+ ("objectclass","top"),
+ ("objectclass","account"),
+ ("objectclass","posixAccount"),
+ ("objectclass","shadowAccount"),
+ ("objectclass","debiandeveloper")]);
+ except ldap.ALREADY_EXISTS:
+ print "exists",;
+
+ # Send the modify request
+ l.modify(Dn,Rec);
+ Outstanding = Outstanding + 1;
+ Outstanding = FlushOutstanding(l,Outstanding,1);
+ print "done";
+ FlushOutstanding(l,Outstanding);
+
+# Read the shadow file into the database
+def DoShadow(l,Shadow):
+ # Read the passwd file and import it
+ Shadow = open(Shadow,"r");
+ Outstanding = 0;
+ while(1):
+ Line = Shadow.readline();
+ if Line == "":
+ break;
+
+ Split = re.split("[:\n]",Line);
+
+ # Ignore system accounts with no password, they do not belong in the
+ # directory.
+ if (Split[1] == 'x' or Split[1] == '*'):
+ print "Ignoring system account,",Split[0];
+ continue;
+
+ for x in range(2,8):
+ CheckNumber(Split[x]);
+
+ Rec = [(ldap.MOD_REPLACE,"shadowLastChange",Split[2]),
+ (ldap.MOD_REPLACE,"shadowMin",Split[3]),
+ (ldap.MOD_REPLACE,"shadowMax",Split[4]),
+ (ldap.MOD_REPLACE,"shadowWarning",Split[5]),
+ (ldap.MOD_REPLACE,"shadowInactive",Split[6]),
+ (ldap.MOD_REPLACE,"shadowExpire",Split[7])];
+ if (WritePasses == 1):
+ Rec.append((ldap.MOD_REPLACE,"userPassword","{crypt}"+Split[1]));
+
+ Dn = "uid=" + Split[0] + "," + BaseDn;
+ print "Importing",Dn,
+ sys.stdout.flush();
+
+ # Send the modify request
+ l.modify(Dn,Rec);
+ Outstanding = Outstanding + 1;
+ print "done";
+ Outstanding = FlushOutstanding(l,Outstanding,1);
+ FlushOutstanding(l,Outstanding);
+
+# Read the group file into the database
+def DoGroup(l,Group):
+ # Read the passwd file and import it
+ Group = open(Group,"r");
+ Outstanding = 0;
+ while(1):
+ Line = Group.readline();
+ if Line == "":
+ break;
+
+ # Split up the group information
+ Split = re.split("[:\n]",Line);
+ Members = re.split("[, ]*",Split[3]);
+ CheckNumber(Split[2]);
+
+ # Iterate over the membership list and add the membership information
+ # To the directory
+ Rec = [(ldap.MOD_ADD,"supplementarygid",Split[0])];
+ Counter = 0;
+ for x in Members:
+ if x == "":
+ continue;
+
+ Dn = "uid=" + x + "," + BaseDn;
+ print "Adding",Dn,"to group",Split[0];
+ Counter = Counter+1;
+
+ # Send the modify request
+ l.modify(Dn,Rec);
+ Outstanding = Outstanding + 1;
+ Outstanding = FlushOutstanding(l,Outstanding,1);
+
+ if Counter == 0:
+ continue;
+
+ Rec = [(ldap.MOD_REPLACE,"gid",Split[0]),
+ (ldap.MOD_REPLACE,"gidNumber",Split[2])];
+
+ Dn = "gid=" + Split[0] + "," + BaseDn;
+ print "Importing",Dn,
+ sys.stdout.flush();
+
+ # Unfortunately add_s does not take the same args as modify :|
+ if (DoAdd == 1):
+ try:
+ l.add_s(Dn,[("gid",Split[0]),
+ ("objectclass","top"),
+ ("objectclass","posixGroup")]);
+ except ldap.ALREADY_EXISTS:
+ print "exists",;
+
+ # Send the modify request
+ l.modify(Dn,Rec);
+ Outstanding = Outstanding + 1;
+ print ".";
+
+ FlushOutstanding(l,Outstanding);
+
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "ap:s:g:xu:")
+for (switch, val) in options:
+ if (switch == '-a'):
+ DoAdd = 1;
+ if (switch == '-x'):
+ WritePasses = 0;
+ elif (switch == '-p'):
+ Passwd = val
+ elif (switch == '-s'):
+ Shadow = val
+ elif (switch == '-g'):
+ Group = val
+ elif (switch == '-u'):
+ AdminUser = val
+
+# Main program starts here
+print "Accessing LDAP directory as '" + AdminUser + "'";
+Password = getpass(AdminUser + "'s password: ");
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+UserDn = "uid=" + AdminUser + "," + BaseDn;
+l.simple_bind_s(UserDn,Password);
+
+if (Passwd != ""):
+ DoPasswd(l,Passwd);
+
+if (Shadow != ""):
+ DoShadow(l,Shadow);
+
+if (Group != ""):
+ DoGroup(l,Group);
diff --git a/ud-xearth b/ud-xearth
new file mode 100755
index 0000000..b1c1fee
--- /dev/null
+++ b/ud-xearth
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Generate an xearth database from the LDAP entries
+# LDAP entires for lat/long can be in one of 3 different formats
+# 1) Decimal Degrees
+# +-DDD.DDDDDDDDDDDDDDD
+# 2) Degrees Minutes (DGM), common output from GPS units
+# +-DDDMM.MMMMMMMMMMMMM
+# 3) Degrees Minutes Seconds (DGMS)
+# +-DDDMMSS.SSSSSSSSSSS
+# Decimal Degrees is the most basic format, but to have good accuracy it
+# needs a large number of decimals. The other formats are all derived from it:
+# DGM -> DD DDD + (MM.MMMMMMMM)/60
+# DGMS -> DD DDD + (MM + (SS.SSSSSS)/60)/60
+# For Latitude + is North, for Longitude + is East
+
+import string, re, time, ldap, getopt, sys, pwd, posix;
+from userdir_ldap import *;
+
+# This needs to check for leading 0 to disambiguate some things
+def DecDegree(Attr,Type):
+ Parts = re.match('[+-]?(\d*)\\.?(\d*)?',GetAttr(Attr,Type)).groups();
+ Val = string.atof(GetAttr(Attr,Type));
+
+ if (abs(Val) >= 1806060.0):
+ raise ValueError,"Too Big";
+
+ # Val is in DGMS
+ if abs(Val) >= 18060.0 or len(Parts[0]) > 5:
+ Val = Val/100.0;
+ Secs = Val - long(Val);
+ Val = long(Val)/100.0;
+ Min = Val - long(Val);
+ Val = long(Val) + (Min*100.0 + Secs*100.0/60.0)/60.0;
+
+ # Val is in DGM
+ elif abs(Val) >= 180 or len(Parts[0]) > 3:
+ Val = Val/100.0;
+ Min = Val - long(Val);
+ Val = long(Val) + Min*100.0/60.0;
+
+ if Val >= 0:
+ return "+" + str(Val);
+ return str(Val);
+
+# Main program starts here
+User = pwd.getpwuid(posix.getuid())[0];
+BindUser = User;
+(options, arguments) = getopt.getopt(sys.argv[1:], "u:")
+for (switch, val) in options:
+ if (switch == '-u'):
+ User = val
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+print "Accessing LDAP directory as '" + User + "'";
+Password = getpass(User + "'s password: ");
+UserDn = "uid=" + User + "," + BaseDn;
+l.simple_bind_s(UserDn,Password);
+
+Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"latitude=*",\
+ ["uid","cn","mn","sn","latitude","longitude"]);
+
+#ttrs = [('uid=bma,ou=users,dc=debian,dc=org', {'longitude': ['-0771426.059'], 'sn': ['Almeida'], 'cn': ['Brian'], 'latitude': ['0384514.263'], 'uid': ['bma']}), ('uid=jgg,ou=users,dc=debian,dc=org', {'longitude': ['-11328'], 'sn': ['Gunthorpe'], 'cn': ['Jason'], 'latitude': ['+5333'], 'uid': ['jgg']})]
+Attrs.sort();
+
+print "Markers file will be written to markers.dat,",
+sys.stdout.flush();
+F = open("markers.dat","w");
+Count = 0;
+for x in Attrs:
+ if x[1].has_key("latitude") == 0 or x[1].has_key("longitude") == 0:
+ continue;
+ Count = Count + 1;
+ try:
+ F.write("%16s %16s \"%s\" \t# %s\n"%(DecDegree(x,"latitude"),DecDegree(x,"longitude"),GetAttr(x,"uid"),EmailAddress(x)));
+ except:
+ F.write("# Failed %s => %s: %s\n" %(x[0],sys.exc_type,sys.exc_value));
+F.close();
+print Count,"entries.";
diff --git a/userdir-ldap.conf b/userdir-ldap.conf
new file mode 100644
index 0000000..70344a4
--- /dev/null
+++ b/userdir-ldap.conf
@@ -0,0 +1,51 @@
+# Config file for ldap scripts
+
+# Basic LDAP configuration
+ldaphost = "db.debian.org";
+basedn = "ou=users,dc=debian,dc=org";
+adminuser = "admin";
+
+# Printable email addresses are shown as: 'cn mn sn '
+emailappend = "debian.org";
+
+# For the mail interface
+maildomain = "db.debian.org";
+replyto = "admin@" + maildomain;
+pingfrom = "ping@" + maildomain;
+chpassfrom = "chpasswd@" + maildomain;
+templatesdir = "/etc/userdir-ldap/templates/";
+replaycachefile = "/var/cache/userdir-ldap/replay";
+#replaycachefile = "/tmp/replay";
+
+# User properties
+defaultgid = 800;
+
+# For the output generator
+generateconf = "/etc/userdir-ldap/generate.conf"
+generatedir = "/var/cache/userdir-ldap/hosts/";
+#generatedir = "/tmp/hosts";
+passdir = "/etc/userdir-ldap/";
+
+# GPG Things
+gpg = "/usr/bin/gpg";
+keyrings = "/usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-keyring.pgp";
+
+# For the WEB interface
+webloginhtml = "login.html";
+websearchhtml = "searchform.html";
+websearchresulthtml = "searchresults.html";
+webupdatehtml = "update.html";
+
+webloginurl = "login.cgi";
+websearchurl = "search.cgi";
+webupdateurl = "update.cgi";
+
+# When should authentication tokens expire?
+authexpires = 600;
+
+# How many bytes to use for the blowfish key (max = 56 (448 bits))
+blowfishkeylen = 10;
+
+# Change this!
+authtokenpath = "/var/cache/userdir-ldap/web-cookies";
+countrylist = "/var/www/userdir-ldap/domains.tab";
diff --git a/userdir_gpg.py b/userdir_gpg.py
new file mode 100644
index 0000000..82e9ed9
--- /dev/null
+++ b/userdir_gpg.py
@@ -0,0 +1,432 @@
+ #!/usr/bin/env python
+# -*- mode: python -*-
+
+# GPG issues -
+# - gpgm with a status FD being fed keymaterial and other interesting
+# things does nothing.. If it could ID the keys and stuff over the
+# status-fd I could decide what to do with them. I would also like it
+# to report which key it selected for encryption (also if there
+# were multi-matches..) Being able to detect a key-revoke cert would be
+# good too.
+# - I would like to be able to fetch the comment and version fields from the
+# packets so I can tell if a signature is made by pgp2 to enable the
+# pgp2 encrypting mode.
+
+import string, mimetools, multifile, sys, StringIO, os, tempfile, re;
+import rfc822, time, fcntl, FCNTL, anydbm
+
+# General GPG options
+GPGPath = "gpg"
+GPGBasicOptions = ["--no-options","--batch","--load-extension","rsa",\
+ "--no-default-keyring","--always-trust"];
+GPGKeyRings = ["--keyring","/usr/share/keyrings/debian-keyring.pgp",\
+ "--keyring","/usr/share/keyrings/debian-keyring.gpg"];
+GPGSigOptions = ["--output","-"];
+GPGSearchOptions = ["--dry-run","--with-colons","--fingerprint"];
+GPGEncryptOptions = ["--output","-","--quiet","--always-trust",\
+ "--armor","--encrypt"];
+GPGEncryptPGP2Options = ["--set-filename","","--rfc1991",\
+ "--load-extension","idea",\
+ "--cipher-algo","idea"] + GPGEncryptOptions;
+
+# Replay cutoff times in seconds
+CleanCutOff = 7*24*60*60;
+AgeCutOff = 4*24*60*60;
+FutureCutOff = 3*24*60*60;
+
+# GetClearSig takes an un-seekable email message stream (mimetools.Message)
+# and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded
+# clear signed text.
+# If this is fed to gpg/pgp it will verify the signature and spit out the
+# signed text component. Email headers and PGP mime (RFC 2015) is understood
+# but no effort is made to cull any information outside the PGP boundaries
+# Please note that in the event of a mime decode the mime headers will be
+# present in the signature text! The return result is a tuple, the first
+# element is the text itself the second is a mime flag indicating if the
+# result should be mime processed after sig checking.
+def GetClearSig(Msg):
+ Error = 'MIME Error';
+ # See if this is a MIME encoded multipart signed message
+ if Msg.gettype() == "multipart/signed":
+ Boundary = Msg.getparam("boundary");
+ if not Boundary:
+ raise Error, "multipart/* without a boundary parameter";
+
+ # Create the multipart handler. Regrettably their implementation
+ # Needs seeking..
+ SkMessage = StringIO.StringIO();
+ SkMessage.write(Msg.fp.read());
+ SkMessage.seek(0);
+ mf = multifile.MultiFile(SkMessage)
+ mf.push(Msg.getparam("boundary"));
+
+ # Get the first part of the multipart message
+ if not mf.next():
+ raise Error, "Invalid pgp/mime encoding [no section]";
+
+ # Get the part as a safe seekable stream
+ Signed = StringIO.StringIO();
+ Signed.write(mf.read());
+ InnerMsg = mimetools.Message(Signed);
+
+ # Make sure it is the right type
+ if InnerMsg.gettype() != "text/plain":
+ raise Error, "Invalid pgp/mime encoding [wrong plaintext type]";
+
+ # Get the next part of the multipart message
+ if not mf.next():
+ raise Error, "Invalid pgp/mime encoding [no section]";
+ InnerMsg = mimetools.Message(mf);
+ if InnerMsg.gettype() != "application/pgp-signature":
+ raise Error, "Invalid pgp/mime encoding [wrong signature type]";
+ Signature = string.joinfields(mf.readlines(),'');
+
+ # Append the PGP boundary header and the signature text to re-form the
+ # original signed block [needs to convert to \r\n]
+ Output = "-----BEGIN PGP SIGNED MESSAGE-----\r\n\r\n" + Signed.getvalue() + Signature;
+ return (Output,1);
+ else:
+ # Just return the message body
+ return (string.joinfields(Msg.fp.readlines(),''),0);
+
+# This opens GPG in 'write filter' mode. It takes Message and sends it
+# to GPGs standard input, pipes the standard output to a temp file along
+# with the status FD. The two tempfiles are passed to GPG by fd and are
+# accessible from the filesystem for only a short period. Message may be
+# None in which case GPGs stdin is closed directly after forking. This
+# is best used for sig checking and encryption.
+# The return result is a tuple (Exit,StatusFD,OutputFD), both fds are
+# fully rewound and readable.
+def GPGWriteFilter(Program,Options,Message):
+ # Make sure the tmp files we open are unreadable, there is a short race
+ # between when the temp file is opened and unlinked that some one else
+ # could open it or hard link it. This is not important however as no
+ # Secure data is fed through the temp files.
+ OldMask = os.umask(0777);
+ try:
+ Output = tempfile.TemporaryFile("w+b");
+ GPGText = tempfile.TemporaryFile("w+b");
+ InPipe = os.pipe();
+ InPipe = [InPipe[0],InPipe[1]];
+ finally:
+ os.umask(OldMask);
+
+ try:
+ # Fork off GPG in a horrible way, we redirect most of its FDs
+ # Input comes from a pipe and its two outputs are spooled to unlinked
+ # temp files (ie private)
+ Child = os.fork();
+ if Child == 0:
+ try:
+ os.dup2(InPipe[0],0);
+ os.close(InPipe[1]);
+ os.dup2(Output.fileno(),1);
+ os.dup2(os.open("/dev/null",os.O_WRONLY),2);
+ os.dup2(GPGText.fileno(),3);
+
+ Args = [Program,"--status-fd","3"] + GPGBasicOptions + GPGKeyRings + Options
+ os.execvp(Program,Args);
+ finally:
+ os._exit(100);
+
+ # Get rid of the other end of the pipe
+ os.close(InPipe[0])
+ InPipe[0] = -1;
+
+ # Send the message
+ if Message != None:
+ try:
+ os.write(InPipe[1],Message);
+ except:
+ pass;
+ os.close(InPipe[1]);
+ InPipe[1] = -1;
+
+ # Wait for GPG to finish
+ Exit = os.waitpid(Child,0);
+
+ # Create the result including the new readable file descriptors
+ Result = (Exit,os.fdopen(os.dup(GPGText.fileno()),"r"), \
+ os.fdopen(os.dup(Output.fileno()),"r"));
+ Result[1].seek(0);
+ Result[2].seek(0);
+
+ Output.close();
+ GPGText.close();
+ return Result;
+ finally:
+ if InPipe[0] != -1:
+ os.close(InPipe[0]);
+ if InPipe[1] != -1:
+ os.close(InPipe[1]);
+ Output.close();
+ GPGText.close();
+
+# This takes a text passage, a destination and a flag indicating the
+# compatibility to use and returns an encrypted message to the recipient.
+# It is best if the recipient is specified using the hex key fingerprint
+# of the target, ie 0x64BE1319CCF6D393BF87FF9358A6D4EE
+def GPGEncrypt(Message,To,PGP2):
+ # Encrypt using the PGP5 block encoding and with the PGP5 option set.
+ # This will handle either RSA or DSA/DH asymetric keys.
+ # In PGP2 compatible mode IDEA and rfc1991 encoding are used so that
+ # PGP2 can read the result. RSA keys do not need PGP2 to be set, as GPG
+ # can read a message encrypted with blowfish and RSA.
+ if PGP2 == 0:
+ try:
+ Res = None;
+ Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptOptions,Message);
+ if Res[0][1] != 0:
+ return None;
+ Text = Res[2].read();
+ return Text;
+ finally:
+ if Res != None:
+ Res[1].close();
+ Res[2].close();
+ else:
+ # We have to call gpg with a filename or it will create a packet that
+ # PGP2 cannot understand.
+ TmpName = tempfile.mktemp();
+ try:
+ Res = None;
+ MsgFile = open(TmpName,"wc");
+ MsgFile.write(Message);
+ MsgFile.close();
+ Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptPGP2Options+[TmpName],None);
+ if Res[0][1] != 0:
+ return None;
+ Text = Res[2].read();
+ return Text;
+ finally:
+ try:
+ os.unlink(TmpName);
+ except:
+ pass;
+ if Res != None:
+ Res[1].close();
+ Res[2].close();
+
+# Checks the signature of a standard PGP message, like that returned by
+# GetClearSig. It returns a large tuple of the form:
+# (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,Length,PGP2),Text);
+# Where,
+# Why = None if checking was OK otherwise an error string.
+# SigID+Date represent something suitable for use in a replay cache. The
+# date is returned as the number of seconds since the UTC epoch.
+# The keyID is also in this tuple for easy use of the replay
+# cache
+# KeyID, KeyFinger and Owner represent the Key used to sign this message
+# PGP2 indicates if the message was created using PGP 2.x
+# Text is the full byte-for-byte signed text in a string
+def GPGCheckSig(Message):
+ Res = None;
+ try:
+ Res = GPGWriteFilter(GPGPath,GPGSigOptions,Message);
+ Exit = Res[0];
+
+ # Parse the GPG answer
+ Strm = Res[1];
+ GoodSig = 0;
+ SigId = None;
+ KeyFinger = None;
+ KeyID = None;
+ Owner = None;
+ Date = None;
+ Why = None;
+ TagMap = {};
+ while(1):
+ # Grab and split up line
+ Line = Strm.readline();
+ if Line == "":
+ break;
+ Split = re.split("[ \n]",Line);
+ if Split[0] != "[GNUPG:]":
+ continue;
+
+ # We only process the first occurance of any tag.
+ if TagMap.has_key(Split[1]):
+ continue;
+ TagMap[Split[1]] = None;
+
+ # Good signature response
+ if Split[1] == "GOODSIG":
+ # Just in case GPG returned a bad signal before this (bug?)
+ if Why == None:
+ GoodSig = 1;
+ KeyID = Split[2];
+ Owner = string.join(Split[3:],' ');
+
+ # Bad signature response
+ if Split[1] == "BADSIG":
+ GoodSig = 0;
+ KeyID = Split[2];
+ Why = "Verification of signature failed";
+
+ # Bad signature response
+ if Split[1] == "ERRSIG" or Split[1] == "NO_PUBKEY":
+ GoodSig = 0;
+ KeyID = Split[2];
+ if Split[7] == '9':
+ Why = "Unable to verify signature, signing key missing.";
+ elif Split[7] == '4':
+ Why = "Unable to verify signature, unknown packet format/key type";
+ else:
+ Why = "Unable to verify signature, unknown reason";
+
+ # Expired signature
+ if Split[1] == "SIGEXPIRED":
+ GoodSig = 0;
+ Why = "Signature has expired";
+
+ # Revoked key
+ if Split[1] == "KEYREVOKED":
+ GoodSig = 0;
+ Why = "Signing key has been revoked";
+
+ # Corrupted packet
+ if Split[1] == "NODATA" or Split[1] == "BADARMOR":
+ GoodSig = 0;
+ Why = "The packet was corrupted or contained no data";
+
+ # Signature ID
+ if Split[1] == "SIG_ID":
+ SigId = Split[2];
+ Date = long(Split[4]);
+
+ # ValidSig has the key finger print
+ if Split[1] == "VALIDSIG":
+ KeyFinger = Split[2];
+
+ # Reopen the stream as a readable stream
+ Text = Res[2].read();
+
+ # A gpg failure is an automatic bad signature
+ if Exit[1] != 0 and Why == None:
+ GoodSig = 0;
+ Why = "GPG execution failed " + str(Exit[0]);
+
+ if GoodSig == 0 and (Why == None or len(Why) == 0):
+ Why = "Checking Failed";
+
+ # Try to decide if this message was sent using PGP2
+ PGP2Message = 0;
+ if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) != None):
+ PGP2Message = 1;
+
+ return (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,0,PGP2Message),Text);
+ finally:
+ if Res != None:
+ Res[1].close();
+ Res[2].close();
+
+# Search for keys given a search pattern. The pattern is passed directly
+# to GPG for processing. The result is a list of tuples of the form:
+# (KeyID,KeyFinger,Owner,Length)
+# Which is similar to the key identification tuple output by GPGChecksig
+def GPGKeySearch(SearchCriteria):
+ Args = [GPGPath] + GPGBasicOptions + GPGKeyRings + GPGSearchOptions + \
+ [SearchCriteria," 2> /dev/null"]
+ Strm = None;
+ Result = [];
+ Owner = "";
+ KeyID = "";
+ try:
+ Strm = os.popen(string.join(Args," "),"r");
+
+ while(1):
+ # Grab and split up line
+ Line = Strm.readline();
+ if Line == "":
+ break;
+ Split = string.split(Line,":");
+
+ # Store some of the key fields
+ if Split[0] == 'pub':
+ KeyID = Split[4];
+ Owner = Split[9];
+ Length = int(Split[2]);
+
+ # Output the key
+ if Split[0] == 'fpr':
+ Result.append( (KeyID,Split[9],Owner,Length) );
+ finally:
+ if Strm != None:
+ Strm.close();
+ return Result;
+
+# Print the available key information in a format similar to GPG's output
+# We do not know the values of all the feilds so they are just replaced
+# with ?'s
+def GPGPrintKeyInfo(Ident):
+ print "pub %u?/%s ??-??-?? %s" % (Ident[3],Ident[0][-8:],Ident[2]);
+ print " key fingerprint = 0x%s" % (Ident[1]);
+
+# Perform a substition of template
+def TemplateSubst(Map,Template):
+ for x in Map.keys():
+ Template = string.replace(Template,x,Map[x]);
+ return Template;
+
+# The replay class uses a python DB (BSD db if avail) to implement
+# protection against replay. Replay is an attacker capturing the
+# plain text signed message and sending it back to the victim at some
+# later date. Each signature has a unique signature ID (and signing
+# Key Fingerprint) as well as a timestamp. The first stage of replay
+# protection is to ensure that the timestamp is reasonable, in particular
+# not to far ahead or too far behind the current system time. The next
+# step is to look up the signature + key fingerprint in the replay database
+# and determine if it has been recived. The database is cleaned out
+# periodically and old signatures are discarded. By using a timestamp the
+# database size is bounded to being within the range of the allowed times
+# plus a little fuzz. The cache is serialized with a flocked lock file
+class ReplayCache:
+ def __init__(self,Database):
+ self.Lock = open(Database + ".lock","w",0600);
+ fcntl.flock(self.Lock.fileno(),FCNTL.LOCK_EX);
+ self.DB = anydbm.open(Database,"c",0600);
+ self.CleanCutOff = CleanCutOff;
+ self.AgeCutOff = AgeCutOff;
+ self.FutureCutOff = FutureCutOff;
+
+ # Close the cache and lock
+ def __del__(self):
+ self.close();
+ def close(self):
+ self.DB.close();
+ self.Lock.close();
+
+ # Clean out any old signatures
+ def Clean(self):
+ CutOff = time.time() - self.CleanCutOff;
+ for x in self.DB.keys():
+ if int(self.DB[x]) <= CutOff:
+ del self.DB[x];
+
+ # Check a signature. 'sig' is a 3 tuple that has the sigId, date and
+ # key ID
+ def Check(self,Sig):
+ if Sig[0] == None or Sig[1] == None or Sig[2] == None:
+ return "Invalid signature";
+ if int(Sig[1]) > time.time() + self.FutureCutOff:
+ return "Signature has a time too far in the future";
+ if self.DB.has_key(Sig[0] + '-' + Sig[2]):
+ return "Signature has already been received";
+ if int(Sig[1]) < time.time() - self.AgeCutOff:
+ return "Signature has passed the age cut off ";
+ # + str(int(Sig[1])) + ',' + str(time.time()) + "," + str(Sig);
+ return None;
+
+ # Add a signature, the sig is the same as is given to Check
+ def Add(self,Sig):
+ if Sig[0] == None or Sig[1] == None:
+ raise RuntimeError,"Invalid signature";
+ if Sig[1] < time.time() - self.CleanCutOff:
+ return;
+ Key = Sig[0] + '-' + Sig[2]
+ if self.DB.has_key(Key):
+ if int(self.DB[Key]) < Sig[1]:
+ self.DB[Key] = str(int(Sig[1]));
+ else:
+ self.DB[Key] = str(int(Sig[1]));
+
diff --git a/userdir_ldap.py b/userdir_ldap.py
new file mode 100644
index 0000000..eab3cab
--- /dev/null
+++ b/userdir_ldap.py
@@ -0,0 +1,170 @@
+# Some routines and configuration that are used by the ldap progams
+import termios, TERMIOS, re, string, imp, ldap, sys, whrandom, crypt;
+
+try:
+ File = open("/etc/userdir-ldap/userdir-ldap.conf");
+except:
+ File = open("userdir-ldap.conf");
+ConfModule = imp.load_source("userdir_config","/etc/userdir-ldap.conf",File);
+File.close();
+
+BaseDn = ConfModule.basedn;
+BaseDn = ConfModule.basedn;
+LDAPServer = ConfModule.ldaphost;
+EmailAppend = ConfModule.emailappend;
+AdminUser = ConfModule.adminuser;
+GenerateDir = ConfModule.generatedir;
+GenerateConf = ConfModule.generateconf;
+DefaultGID = ConfModule.defaultgid;
+TemplatesDir = ConfModule.templatesdir;
+PassDir = ConfModule.passdir;
+
+# This is a list of common last-name prefixes
+LastNamesPre = {"van": None, "le": None, "de": None, "di": None};
+
+# Safely get an attribute from a tuple representing a dn and an attribute
+# list. It returns the first attribute if there are multi.
+def GetAttr(DnRecord,Attribute,Default = ""):
+ try:
+ return DnRecord[1][Attribute][0];
+ except IndexError:
+ return Default;
+ except KeyError:
+ return Default;
+ return Default;
+
+# Return a printable email address from the attributes.
+def EmailAddress(DnRecord):
+ cn = GetAttr(DnRecord,"cn");
+ sn = GetAttr(DnRecord,"sn");
+ uid = GetAttr(DnRecord,"uid");
+ if cn == "" and sn == "":
+ return "<" + uid + "@" + EmailAppend + ">";
+ return cn + " " + sn + " <" + uid + "@" + EmailAppend + ">"
+
+# Show a dump like ldapsearch
+def PrettyShow(DnRecord):
+ Result = "";
+ List = DnRecord[1].keys();
+ List.sort();
+ for x in List:
+ Rec = DnRecord[1][x];
+ for i in Rec:
+ Result = Result + "%s: %s\n" % (x,i);
+ return Result[:-1];
+
+# Function to prompt for a password
+def getpass(prompt = "Password: "):
+ import termios, TERMIOS, sys;
+ fd = sys.stdin.fileno();
+ old = termios.tcgetattr(fd);
+ new = termios.tcgetattr(fd);
+ new[3] = new[3] & ~TERMIOS.ECHO; # lflags
+ try:
+ termios.tcsetattr(fd, TERMIOS.TCSADRAIN, new);
+ passwd = raw_input(prompt);
+ finally:
+ termios.tcsetattr(fd, TERMIOS.TCSADRAIN, old);
+ print;
+ return passwd;
+
+# Split up a name into multiple components. This tries to best guess how
+# to split up a name
+def NameSplit(Name):
+ Words = re.split(" ",string.strip(Name));
+
+ # Insert an empty middle name
+ if (len(Words) == 2):
+ Words.insert(1,"");
+ if (len(Words) < 2):
+ Words.append("");
+
+ # Put a dot after any 1 letter words, must be an initial
+ for x in range(0,len(Words)):
+ if len(Words[x]) == 1:
+ Words[x] = Words[x] + '.';
+
+ # If a word starts with a -, ( or [ we assume it marks the start of some
+ # Non-name information and remove the remainder of the string
+ for x in range(0,len(Words)):
+ if len(Words[x]) != 0 and (Words[x][0] == '-' or \
+ Words[x][0] == '(' or Words[x][0] == '['):
+ Words = Words[0:x];
+ break;
+
+ # Merge any of the middle initials
+ if len(Words) > 2:
+ while len(Words[2]) == 2 and Words[2][1] == '.':
+ Words[1] = Words[1] + Words[2];
+ del Words[2];
+
+ while len(Words) < 2:
+ Words.append('');
+
+ # Merge any of the last name prefixes into one big last name
+ while LastNamesPre.has_key(string.lower(Words[-2])):
+ Words[-1] = Words[-2] + " " + Words[-1];
+ del Words[-2];
+
+ # Fix up a missing middle name after lastname globbing
+ if (len(Words) == 2):
+ Words.insert(1,"");
+
+ # If the name is multi-word then we glob them all into the last name and
+ # do not worry about a middle name
+ if (len(Words) > 3):
+ Words[2] = string.join(Words[1:]);
+ Words[1] = "";
+
+ return (string.strip(Words[0]),string.strip(Words[1]),string.strip(Words[2]));
+
+# Compute a random password using /dev/urandom
+def GenPass():
+ # Generate a 10 character random string
+ SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/.";
+ Rand = open("/dev/urandom");
+ Password = "";
+ for i in range(0,10):
+ Password = Password + SaltVals[ord(Rand.read(1)[0]) % len(SaltVals)];
+ return Password;
+
+# Compute the MD5 crypted version of the given password
+def HashPass(Password):
+ # Hash it telling glibc to use the MD5 algorithm - if you dont have
+ # glibc then just change Salt = "$1$" to Salt = "";
+ SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/.";
+ Salt = "$1$";
+ for x in range(0,10):
+ Salt = Salt + SaltVals[whrandom.randint(0,len(SaltVals)-1)];
+ Pass = crypt.crypt(Password,Salt);
+ if len(Pass) < 14:
+ raise "Password Error", "MD5 password hashing failed, not changing the password!";
+ return Pass;
+
+# Sync with the server, we count the number of async requests that are pending
+# and make sure result has been called that number of times
+def FlushOutstanding(l,Outstanding,Fast=0):
+ # Sync with the remote end
+ if Fast == 0:
+ print "Waiting for",Outstanding,"requests:",
+ while (Outstanding > 0):
+ try:
+ if Fast == 0 or Outstanding > 50:
+ sys.stdout.write(".",);
+ sys.stdout.flush();
+ if (l.result(ldap.RES_ANY,1) != (None,None)):
+ Outstanding = Outstanding - 1;
+ else:
+ if (l.result(ldap.RES_ANY,1,0) != (None,None)):
+ Outstanding = Outstanding - 1;
+ else:
+ break;
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ Outstanding = Outstanding - 1;
+ except ldap.NO_SUCH_ATTRIBUTE:
+ Outstanding = Outstanding - 1;
+ except ldap.NO_SUCH_OBJECT:
+ Outstanding = Outstanding - 1;
+ if Fast == 0:
+ print;
+ return Outstanding;
diff --git a/web/Util.pm b/web/Util.pm
new file mode 100644
index 0000000..4cbec32
--- /dev/null
+++ b/web/Util.pm
@@ -0,0 +1,271 @@
+# -*- perl -*-x
+package Util;
+
+use strict;
+use Crypt::Blowfish;
+
+my $blocksize = 8; # A blowfish block is 8 bytes
+my $configfile = "/etc/userdir-ldap/userdir-ldap.conf";
+
+my %config = &ReadConfigFile;
+
+sub CreateKey {
+ my $keysize = shift;
+ my $input;
+ open (F, "encrypt(substr($input, $pos, $blocksize)));
+ }
+ return $output;
+}
+
+sub Decrypt {
+ # like encrypt, needs to deal with big blocks. Note that we assume
+ # trailing spaces are unimportant.
+ my $cipher = shift;
+ my $input = shift;
+ my ($pos, $portion, $output);
+
+ ((length($input) % $blocksize) == 0) || &HTMLError("Password corrupted"); # should always be true...
+
+ for ($pos = 0; $pos < length($input); $pos += $blocksize*2) {
+ $portion = pack("H16", substr($input, $pos, $blocksize*2));
+ $output .= $cipher->decrypt($portion);
+ }
+
+ $output =~ s/ +$//;
+ return $output;
+}
+
+sub SavePasswordToFile {
+ my $userid = shift;
+ my $password = shift;
+ my $cipher = shift;
+
+ my $cryptuser = crypt($userid, &CreateCryptSalt);
+ my $secret = Encrypt($cipher, $password);
+ $cryptuser =~ y/\//_/; # translate slashes to underscores...
+
+ my $fn = "$config{authtokenpath}/$cryptuser";
+ open (F, ">$fn") || &HTMLError("$fn: $!");
+ print F "$secret\n";
+ print F time."\n";
+ close F;
+ chmod 0600, $fn;
+ return $cryptuser;
+}
+
+sub ReadPasswordFromFile {
+ my $userid = shift;
+ my $cipher = shift;
+ my $passwd;
+ my $time;
+
+ $userid =~ y/\//_/; # translate slashes to underscores...
+
+ # if we couldn't read the password file, assume user is unauthenticated. is this ok?
+ open (F, "<$config{authtokenpath}/$userid") || return undef;
+ chomp($passwd = );
+ chomp($time = );
+ close F;
+
+ # check to make sure we read something
+ return undef if (!$passwd || !$time);
+
+ # check to make sure the time is positive, and that the auth token
+ # has not expired
+ my $tdiff = (time - $time);
+ &HTMLError("Your authentication token has expired. Please relogin") if (($tdiff < 0) || ($tdiff > $config{authexpires}));
+
+ return Decrypt($cipher, $passwd);
+}
+
+sub CheckAuthToken {
+ my ($id, $hrkey) = split(/:/, shift, 2);
+ return undef if (!$id || !$hrkey);
+ my $key = pack("H".(length($hrkey)), $hrkey);
+ my $cipher = new Crypt::Blowfish $key;
+ my $r = ReadPasswordFromFile($id, $cipher);
+ if ($r) {
+ UpdateAuthToken("$id:$hrkey", $r);
+ } else {
+ ClearAuthToken("$id:$hrkey")
+ }
+ return $r;
+}
+
+sub ClearAuthToken {
+ my ($id, $hrkey) = split(/:/, shift, 2);
+ $id =~ y/\//_/; # switch / to _
+ unlink "$config{authtokenpath}/$id" || &HTMLError("Error removing authtoken: $!");
+}
+
+sub UpdateAuthToken {
+ my ($id, $hrkey) = split(/:/, shift, 2);
+ my $password = shift;
+ my $key = pack("H".(length($hrkey)), $hrkey);
+ $id =~ y/\//_/; # switch / to _
+ my $cipher = new Crypt::Blowfish $key;
+ my $secret = Encrypt($cipher, $password);
+
+ my $fn = "$config{authtokenpath}/$id";
+ open (F, ">$fn") || &HTMLError("$fn: $!");
+ print F "$secret\n";
+ print F time."\n";
+ close F;
+ chmod 0600, "$fn" || &HTMLError("$fn: $!");
+ return 1;
+}
+
+sub FormatFingerPrint {
+ my $in = shift;
+ my $out;
+
+ if (length($in) == 32) {
+ foreach (0..15) {
+ $out .= substr($in, $_*2, 2)." ";
+ $out .= " " if ($_ == 7);
+ }
+ } else {
+ foreach (0..int(length($in)/2)) {
+ $out .= substr($in, $_*4, 4)." ";
+ }
+ }
+ return $out;
+}
+
+sub FetchKey {
+ my $fingerprint = shift;
+ my ($out, $keyringparam);
+
+ foreach (split(/:/, $config{keyrings})) {
+ $keyringparam .= "--keyring $_ ";
+ }
+
+ $fingerprint =~ s/\s//g;
+ $fingerprint = "0x".$fingerprint;
+
+ $/ = undef; # just suck it up ....
+ open(FP, "$config{gpg} $keyringparam --list-sigs --fingerprint $fingerprint|");
+ $out = ;
+ close FP;
+ open(FP, "$config{gpg} $keyringparam --export -a $fingerprint|");
+ $out .= ;
+ close FP;
+ $/ = "\n";
+
+ return $out;
+}
+
+sub FormatTimestamp {
+ my $in = shift;
+ $in =~ /^(....)(..)(..)(..)(..)(..)/;
+
+ return sprintf("%04d/%02d/%02d %02d:%02d:%02d UTC", $1,$2,$3,$4,$5,$6);
+}
+
+sub LookupCountry {
+ my $in = shift;
+ my ($abbrev, $country);
+ open (F, $config{countrylist}) || return uc($in);
+ while () {
+ chomp;
+ ($abbrev, $country) = split(/\s+/, $_, 2);
+ if ($abbrev eq $in) {
+ close F;
+ return $country;
+ }
+ }
+ close F;
+ return uc($in);
+}
+
+####################
+# Some HTML Routines
+
+my $htmlhdrsent = 0;
+
+sub HTMLSendHeader {
+ print "Content-type: text/html\n\n" if (!$htmlhdrsent);
+ $htmlhdrsent = 1;
+}
+
+sub HTMLPrint {
+ &HTMLSendHeader if (!$htmlhdrsent);
+ print shift;
+}
+
+sub HTMLError {
+ HTMLPrint(shift);
+ die "\n";
+}
+
+sub CheckLatLong {
+ my ($lat, $long) = @_;
+
+ $lat =~ s/[^-+\.\d]//g; $long =~ s/[^-+\.\d]//g;
+
+ if (($lat =~ /^(\-|\+?)\d+(\.\d+)?/) && ($long =~ /^(\-|\+?)\d+(\.\d+)?/)) {
+ return ($lat, $long);
+ } else {
+ return ("", "");
+ }
+}
+
+###################
+# Config file stuff
+sub ReadConfigFile {
+ # reads a config file and results a hashref with the results
+ my (%config, $attr, $setting);
+ open (F, "<$configfile") || &HTMLError("Cannot open $configfile: $!");
+ while () {
+ chomp;
+ if ((!/^\s*#/) && ($_ ne "")) {
+ # Chop off any trailing comments
+ s/#.*//;
+ ($attr, $setting) = split(/=/, $_, 2);
+ $setting =~ s/"//g;
+ $setting =~ s/;$//;
+ $attr =~ s/^ +//; $attr =~ s/ +$//;
+ $setting =~ s/^ +//; $setting =~ s/ +$//;
+ $config{$attr} = $setting;
+ }
+ }
+ close F;
+ return %config;
+}
+
+1;
diff --git a/web/domains.tab b/web/domains.tab
new file mode 100644
index 0000000..fd7d1d7
--- /dev/null
+++ b/web/domains.tab
@@ -0,0 +1,255 @@
+ad Andorra
+ae United Arab Emirates
+af Afghanistan
+ag Antigua and Barbuda
+ai Anguilla
+al Albania
+am Armenia
+an Netherlands Antilles
+ao Angola
+aq Antarctica
+ar Argentina
+arpa Old style Arpanet
+as American Samoa
+at Austria
+au Australia
+aw Aruba
+az Azerbaidjan
+ba Bosnia-Herzegovina
+bb Barbados
+bd Bangladesh
+be Belgium
+bf Burkina Faso
+bg Bulgaria
+bh Bahrain
+bi Burundi
+bj Benin
+bm Bermuda
+bn Brunei Darussalam
+bo Bolivia
+br Brazil
+bs Bahamas
+bt Bhutan
+bv Bouvet Island
+bw Botswana
+by Belarus
+bz Belize
+ca Canada
+cc Cocos (Keeling) Islands
+cd Democratic Republic of Congo
+cf Central African Republic
+cg Congo
+ch Switzerland
+ci Ivory Coast (Cote D'Ivoire)
+ck Cook Islands
+cl Chile
+cm Cameroon
+cn China
+co Colombia
+com Commercial
+cr Costa Rica
+cs Czech Republic and Slovakia
+cu Cuba
+cv Cape Verde
+cx Christmas Island
+cy Cyprus
+cz Czech Republic
+de Germany
+dj Djibouti
+dk Denmark
+dm Dominica
+do Dominican Republic
+dz Algeria
+ec Ecuador
+edu USA Educational
+ee Estonia
+eg Egypt
+eh Western Sahara
+er Eritrea
+es Spain
+et Ethiopia
+fi Finland
+fj Fiji
+fk Falkland Islands
+fm Micronesia
+fo Faroe Islands
+fr France
+fx France (European Territory)
+ga Gabon
+gb Great Britain
+gd Grenada
+ge Georgia
+gf French Guyana
+gg Guernsey
+gh Ghana
+gi Gibraltar
+gl Greenland
+gm Gambia
+gn Guinea
+gov USA Government
+gp Guadeloupe (French)
+gq Equatorial Guinea
+gr Greece
+gs S. Georgia & S. Sandwich Isls.
+gt Guatemala
+gu Guam (USA)
+gw Guinea Bissau
+gy Guyana
+hk Hong Kong
+hm Heard and McDonald Islands
+hn Honduras
+hr Croatia
+ht Haiti
+hu Hungary
+id Indonesia
+ie Ireland
+il Israel
+im Isle of Man
+in India
+int International
+io British Indian Ocean Territory
+iq Iraq
+ir Iran
+is Iceland
+it Italy
+je Jersey
+jm Jamaica
+jo Jordan
+jp Japan
+ke Kenya
+kg Kyrgyzstan
+kh Cambodia
+ki Kiribati
+km Comoros
+kn Saint Kitts & Nevis Anguilla
+kp North Korea
+kr South Korea
+kw Kuwait
+ky Cayman Islands
+kz Kazakhstan
+la Laos
+lb Lebanon
+lc Saint Lucia
+li Liechtenstein
+lk Sri Lanka
+lr Liberia
+ls Lesotho
+lt Lithuania
+lu Luxembourg
+lv Latvia
+ly Libya
+ma Morocco
+mc Monaco
+md Moldavia
+mg Madagascar
+mh Marshall Islands
+mil USA Military
+mk Macedonia
+ml Mali
+mm Myanmar
+mn Mongolia
+mo Macau
+mp Northern Mariana Islands
+mq Martinique (French)
+mr Mauritania
+ms Montserrat
+mt Malta
+mu Mauritius
+mv Maldives
+mw Malawi
+mx Mexico
+my Malaysia
+mz Mozambique
+na Namibia
+nato NATO
+nc New Caledonia (French)
+ne Niger
+net Network
+nf Norfolk Island
+ng Nigeria
+ni Nicaragua
+nl Netherlands
+no Norway
+np Nepal
+nr Nauru
+nt Neutral Zone
+nu Niue
+nz New Zealand
+om Oman
+org Non-Profit Making Organisations
+pa Panama
+pe Peru
+pf Polynesia (French)
+pg Papua New Guinea
+ph Philippines
+pk Pakistan
+pl Poland
+pm Saint Pierre and Miquelon
+pn Pitcairn Island
+pr Puerto Rico
+pt Portugal
+pw Palau
+py Paraguay
+qa Qatar
+re Reunion (French)
+ro Romania
+ru Russia
+rw Rwanda
+sa Saudi Arabia
+sb Solomon Islands
+sc Seychelles
+sd Sudan
+se Sweden
+sg Singapore
+sh Saint Helena
+si Slovenia
+sj Svalbard and Jan Mayen Islands
+sk Slovak Republic
+sl Sierra Leone
+sm San Marino
+sn Senegal
+so Somalia
+sr Suriname
+st Saint Tome and Principe
+su Former USSR
+sv El Salvador
+sy Syria
+sz Swaziland
+tc Turks and Caicos Islands
+td Chad
+tf French Southern Territories
+tg Togo
+th Thailand
+tj Tadjikistan
+tk Tokelau
+tm Turkmenistan
+tn Tunisia
+to Tonga
+tp East Timor
+tr Turkey
+tt Trinidad and Tobago
+tv Tuvalu
+tw Taiwan
+tz Tanzania
+ua Ukraine
+ug Uganda
+uk United Kingdom
+um USA Minor Outlying Islands
+us United States
+uy Uruguay
+uz Uzbekistan
+va Vatican City State
+vc Saint Vincent & Grenadines
+ve Venezuela
+vg Virgin Islands (British)
+vi Virgin Islands (USA)
+vn Vietnam
+vu Vanuatu
+wf Wallis and Futuna Islands
+ws Samoa
+ye Yemen
+yt Mayotte
+yu Yugoslavia
+za South Africa
+zm Zambia
+zw Zimbabwe
diff --git a/web/fetchkey.cgi b/web/fetchkey.cgi
new file mode 100755
index 0000000..8e88502
--- /dev/null
+++ b/web/fetchkey.cgi
@@ -0,0 +1,27 @@
+#!/usr/bin/perl
+
+use strict;
+use CGI;
+use Util;
+
+# Global settings...
+my %config = &Util::ReadConfigFile;
+
+my $query = new CGI;
+print "Content-type: text/plain\n\n";
+
+my $fp = $query->param('fingerprint');
+
+if ($fp) {
+ my $key = &Util::FetchKey($fp);
+ if ($key) {
+ print $key;
+ } else {
+ print "Sorry, no key found matching fingerprint $fp\n";
+ }
+} else {
+ print "No fingerprint given\n";
+}
+
+exit 0;
+
diff --git a/web/login.cgi b/web/login.cgi
new file mode 100755
index 0000000..3514953
--- /dev/null
+++ b/web/login.cgi
@@ -0,0 +1,52 @@
+#!/usr/bin/perl
+
+# (c) 1999 Debian and Randolph Chung. Licensed under the GPL.
+
+use lib '.';
+use strict;
+#use Apache::Registry;
+use CGI;
+use Util;
+use URI::Escape;
+use Crypt::Blowfish;
+use Net::LDAP qw(:all);
+
+my %config = &Util::ReadConfigFile;
+
+my $query = new CGI;
+my $proto = ($ENV{HTTPS} ? "https" : "http");
+
+if (!($query->param('username')) || !($query->param('password'))) {
+ print "Location: $proto://$ENV{SERVER_NAME}/$config{webloginurl}\n\n";
+ exit;
+}
+
+my $key = &Util::CreateKey($config{blowfishkeylen}); # human-readable version of the key
+my $hrkey = unpack("H".($config{blowfishkeylen}*2), $key);
+my $cipher = new Crypt::Blowfish $key;
+
+my $ldap = Net::LDAP->new($config{ldaphost}) || &Util::HTMLError($!);
+
+my $username = $query->param('username');
+my $password = $query->param('password');
+my $binddn = "uid=$username,$config{basedn}";
+
+my $mesg = $ldap->bind($binddn, password => $password);
+$mesg->sync;
+
+if ($mesg->code == LDAP_SUCCESS) {
+ my $cryptid = &Util::SavePasswordToFile($username, $password, $cipher);
+
+ if ($query->param('update')) {
+ my $url = "$proto://$ENV{SERVER_NAME}/$config{webupdateurl}?id=$username&authtoken=$cryptid:$hrkey&editdn=";
+ $url .= uri_escape("uid=$username,$config{basedn}", "\x00-\x40\x7f-\xff");
+ print "Location: $url\n\n";
+ } else {
+ print "Location: $proto://$ENV{SERVER_NAME}/$config{websearchurl}?id=$username&authtoken=$cryptid:$hrkey\n\n";
+ }
+
+ $ldap->unbind;
+} else {
+ print "Content-type: text/html\n\n";
+ print "Not authenticated
\n";
+}
diff --git a/web/login.html b/web/login.html
new file mode 100644
index 0000000..300dcd5
--- /dev/null
+++ b/web/login.html
@@ -0,0 +1,16 @@
+debian.org Developers LDAP Search
+
+
+
+
+
diff --git a/web/logout.cgi b/web/logout.cgi
new file mode 100755
index 0000000..11167b4
--- /dev/null
+++ b/web/logout.cgi
@@ -0,0 +1,23 @@
+#!/usr/bin/perl
+
+# (c) 1999 Debian and Randolph Chung. Licensed under the GPL.
+
+use lib '.';
+use strict vars;
+#use Apache::Registry;
+use CGI;
+use Util;
+use Net::LDAP qw(:all);
+
+my %config = &Util::ReadConfigFile;
+my $proto = ($ENV{HTTPS} ? "https" : "http");
+
+my $query = new CGI;
+my $id = $query->param('id');
+my $authtoken = $query->param('authtoken');
+&Util::ClearAuthToken($authtoken);
+my $doneurl = $query->param('done') || "$config{websearchurl}";
+
+&Util::ClearAuthToken($authtoken);
+
+print "Location: $proto://$ENV{SERVER_NAME}/$doneurl\n\n";
diff --git a/web/search.cgi b/web/search.cgi
new file mode 100755
index 0000000..3c21fb7
--- /dev/null
+++ b/web/search.cgi
@@ -0,0 +1,255 @@
+#!/usr/bin/perl
+
+# (c) 1999 Debian and Randolph Chung. Licensed under the GPL.
+
+use lib '.';
+use strict vars;
+#use Apache::Registry;
+use CGI;
+use Util;
+use URI::Escape;
+use Net::LDAP qw(:all);
+
+# Global settings...
+my %config = &Util::ReadConfigFile;
+
+my $query = new CGI;
+my $id = $query->param('id');
+my $authtoken = $query->param('authtoken');
+my $password = &Util::CheckAuthToken($authtoken);
+my $dosearch = $query->param('dosearch');
+my $searchdn = $query->param('searchdn');
+my $ldap = undef;
+
+my $proto = ($ENV{HTTPS} ? "https" : "http");
+
+sub DieHandler {
+ $ldap->unbind if (defined($ldap));
+}
+
+$SIG{__DIE__} = \&DieHandler;
+
+if (!$dosearch) {
+ # No action yet, send back the search form...
+ print "Content-type: text/html\n\n";
+ open (F, "<$config{websearchhtml}") || &Util::HTMLError($!);
+ while () {
+ s/~id~/$id/g;
+ s/~authtoken~/$authtoken/g;
+ print;
+ }
+ close F;
+} else {
+ # Go ahead and construct the search terms
+ my %searchdata = (
+ cn => { fuzzy => 'cnfuzzy', formname => 'cn' }, # First name
+ mn => { fuzzy => 'mnfuzzy', formname => 'mn' }, # Middle name
+ sn => { fuzzy => 'snfuzzy', formname => 'sn' }, # Last name
+ email => { fuzzy => 'emailfuzzy', formname => 'email' }, # email
+ uid => { fuzzy => 'uidfuzzy', formname => 'uid' }, # Login name
+ ircnick => { fuzzy => 'ircfuzzy', formname => 'ircnick' }, # IRC nickname
+ keyfingerprint => { fuzzy => 'fpfuzzy', formname => 'fingerprint' }, # PGP/GPG fingerprint
+ c => { formname => 'country'}, # Country
+ );
+
+ # Do a little preprocessing - strip the spaces out of the fingerprint
+ my $temp = $query->param('fingerprint');
+ $temp =~ s/ //g; $query->param('fingerprint', $temp);
+
+ # go through %searchdata and pull out all the search criteria the user
+ # specified...
+ my $filter = undef;
+ foreach (keys(%searchdata)) {
+ if ($query->param($searchdata{$_}{formname})) {
+ if ($query->param($searchdata{$_}{fuzzy})) {
+ # fuzzy search
+ $filter .= "($_~=".$query->param($searchdata{$_}{formname}).")";
+ } else {
+ $filter .= "($_=".$query->param($searchdata{$_}{formname}).")";
+ }
+ }
+ }
+
+ # Vacation is a special case
+ $filter .= "(onvacation=*)" if ($query->param('vacation'));
+
+ # AND all the search terms together
+ $filter = "(&$filter)";
+
+ # Read in the result template...
+ my ($lineref, $dataspecref) = ParseResult($config{websearchresulthtml});
+
+ # Now, we are ready to connect to the LDAP server.
+ $ldap = Net::LDAP->new($config{ldaphost}) || &Util::HTMLError($!);
+ my $auth = 0;
+ my $mesg;
+
+ if ($id && $password) {
+ $mesg = $ldap->bind("uid=$id,$config{basedn}", password => $password);
+ $mesg->sync;
+ $auth = ($mesg->code == LDAP_SUCCESS);
+ }
+
+ if (!$auth) { # Not authenticated - either the above failed, or no password supplied
+ $ldap->bind;
+ }
+
+# &Util::HTMLPrint("Searching in $config{basedn} for $filter...\n");
+
+ $mesg = $ldap->search(base => ($searchdn ? $searchdn : $config{basedn}),
+ filter => ($searchdn ? "(uid=*)" : $filter));
+ $mesg->code && &Util::HTMLError($mesg->error);
+
+ my %outsub; # this hash will contain all the substitution tokens in the output
+ $outsub{count} = $mesg->count; # Count number of requests, also ensures we're done with the search
+ $outsub{auth} = $authtoken;
+ $outsub{authtoken} = $authtoken; # alias
+ $outsub{id} = $id;
+ $outsub{searchresults} = undef;
+
+ my $entries = $mesg->as_struct; # entries contain a hashref to all the search results
+ my ($dn, $attr, $data);
+
+ # Format the output....
+ foreach $dn (sort {$entries->{$a}->{sn}->[0] <=> $entries->{$b}->{sn}->[0]} keys(%$entries)) {
+ $data = $entries->{$dn};
+
+ # These are local variables.. i have enough global vars as it is...
+ my ($ufdn, $login, $name, $email, $fingerprint, $address, $latlong, $vacation, $created, $modified) = undef;
+
+ $ufdn = $dn; # Net::LDAP does not have a dn2ufn function, but this is close enough :)
+
+ # Assemble name, attach web page link if present.
+ $name = $data->{cn}->[0]." ".$data->{mn}->[0]." ".$data->{sn}->[0];
+ if (my $url = $data->{labeledurl}->[0]) {
+ $name = "$name";
+ }
+
+ # Add links to all email addresses
+ foreach (@{$data->{emailforward}}) {
+ $email .= "
" if ($email);
+ $email .= "$_";
+ }
+
+ # Format PGP/GPG key fingerprints
+ my $fi;
+ foreach (@{$data->{keyfingerprint}}) {
+ $fingerprint .= "
" if ($fingerprint);
+ $fingerprint .= sprintf("%d:- %s", ++$fi, $_, &Util::FormatFingerPrint($_));
+ }
+
+ # Assemble addresses
+ $address = $data->{postaladdress}->[0] || "- unlisted -";
+ $address =~ s/\$/
/g;
+ $address .= "
".$data->{l}->[0]."
".&Util::LookupCountry($data->{c}->[0])."
".$data->{postalcode}->[0];
+
+ # Assemble latitude/longitude
+ $latlong = $data->{latitude}->[0] || "none";
+ $latlong .= " / ";
+ $latlong .= $data->{longitude}->[0] || "none";
+
+ # Modified/created time. TODO: maybe add is the name of the creator/modifier
+ $modified = &Util::FormatTimestamp($data->{modifytimestamp}->[0]);
+ $created = &Util::FormatTimestamp($data->{createtimestamp}->[0]);
+
+ # Link in the debian login id
+ $login = $data->{uid}->[0]."\@debian.org";
+ $login = "$login";
+
+ # See if the user has a vacation message
+ $vacation = $data->{onvacation}->[0];
+
+ # OK, now generate output... (i.e. put the output into the buffer )
+ $outsub{searchresults} .= '';
+ $outsub{searchresults} .= ''."$name ";
+ $outsub{searchresults} .= "($ufdn) |
\n";
+
+ if ($vacation) {
+ $outsub{searchresults} .= "$vacation |
\n";
+ }
+
+ $outsub{searchresults} .= FormatEntry($dataspecref->{uid}, $login);
+ $outsub{searchresults} .= FormatEntry($dataspecref->{ircnick}, $data->{ircnick}->[0]);
+ $outsub{searchresults} .= FormatEntry($dataspecref->{loginshell}, $data->{loginshell}->[0]);
+ $outsub{searchresults} .= FormatEntry($dataspecref->{fingerprint}, $fingerprint);
+
+ if ($auth) {
+ # Some data should only be available to authorized users...
+ if ($id eq $data->{uid}->[0]) {
+ $outsub{searchresults} .= FormatEntry($dataspecref->{email}, $email);
+ }
+ $outsub{searchresults} .= FormatEntry($dataspecref->{address}, $address);
+ $outsub{searchresults} .= FormatEntry($dataspecref->{latlong}, $latlong);
+ $outsub{searchresults} .= FormatEntry($dataspecref->{phone}, $data->{telephonenumber}->[0] || "- unlisted -");
+ $outsub{searchresults} .= FormatEntry($dataspecref->{fax}, $data->{fascimiletelephonenumber}->[0] || "- unlisted -");
+ }
+ $outsub{searchresults} .= FormatEntry($dataspecref->{created}, $created);
+ $outsub{searchresults} .= FormatEntry($dataspecref->{modified}, $modified);
+
+ $outsub{searchresults} .= "
";
+
+ # If this is ourselves, present a link to do mods
+ if ($auth && ($id eq $data->{uid}->[0])) { #TODO: extract this string into a url for translation...
+ $outsub{searchresults} .= "Edit my settings\n";
+ }
+
+ $outsub{searchresults} .= "
\n";
+ }
+
+ # Finally, we can write the output... yuck...
+ &Util::HTMLSendHeader;
+ foreach (@$lineref) {
+ if (/<\?ifauth(.+?)\?>/) {
+ $_ = ($auth ? $1 : "");
+ } elsif (/<\?ifnoauth(.+?)\?>/) {
+ $_ = ($auth ? "" : $1);
+ }
+ s/~(.+?)~/$outsub{$1}/g;
+ print;
+ }
+
+ $ldap->unbind;
+}
+
+sub ParseResult {
+ # Reads the output html file and find out how the output should be named
+ # -- this gives us a way to do translations more easily
+ # Returns the contents of the template (w/o the searchresult portion) and
+ # the output specification
+ my $fn = shift;
+ my $insec = 0;
+ my @lines;
+ my %hash;
+
+ open (F, "<$fn") || &Util::HTMLError("$fn: $!");
+ while () {
+ if (!$insec) {
+ if (/<\?searchresults/i) {
+ $insec = 1;
+ push(@lines, "~searchresults~\n"); # Leave token so we know where to put the result
+ } else {
+ push(@lines, $_);
+ }
+ } else {
+ if (/searchresults\?>/i) {
+ $insec = 0;
+ } else {
+ if (!/^\s*#/) {
+ s/^ *\(//;
+ s/\) *$//; # remove leading/trailing () and spaces
+ chomp;
+ my ($desc, $attr) = split(/, /, $_, 2);
+ $hash{$attr} = $desc;
+ }
+ }
+ }
+ }
+ close F;
+ return (\@lines, \%hash);
+}
+
+sub FormatEntry {
+ my ($key, $val) = @_;
+
+ return "$key: | $val |
\n";
+}
diff --git a/web/searchform.html b/web/searchform.html
new file mode 100644
index 0000000..f9d57ec
--- /dev/null
+++ b/web/searchform.html
@@ -0,0 +1,277 @@
+debian.org Developers LDAP Search
+
+
+
+
diff --git a/web/searchhelp.html b/web/searchhelp.html
new file mode 100644
index 0000000..1167078
--- /dev/null
+++ b/web/searchhelp.html
@@ -0,0 +1,18 @@
+debian.org Developers Online Database
+
+
+
+
+To look up information about Debian developers, enter your search criteria
+in the form. Results are returned which match all of the search criteria.
+Wildcards may be used. For example, entering *de* in the last name
+field will return all developers whose surname contains the substring
+de . Matches are case-insensitive, and all searching criteria that
+are left empty will be ignored. Selecting the "fuzzy search" option will turn
+on approximate searching.
+
+The "On vacation" field will return all developers who have left a vacation
+message.
+
+ |
+
diff --git a/web/searchresults.html b/web/searchresults.html
new file mode 100644
index 0000000..daec13d
--- /dev/null
+++ b/web/searchresults.html
@@ -0,0 +1,37 @@
+
+
+Search results
+
+
+
+Logout | ?>
+Login (developers only) | ?>
+Search again
+Number of entries matched: ~count~
+
+
+
+
+
+
+
+
+
+
+Logout | ?>
+Login (developers only) | ?>
+Search again
+
+
diff --git a/web/settings.cfg b/web/settings.cfg
new file mode 100644
index 0000000..6738fae
--- /dev/null
+++ b/web/settings.cfg
@@ -0,0 +1,26 @@
+# Config file for ldap scripts
+
+ldaphost = "db.debian.org";
+basedn = "ou=users,dc=debian,dc=org";
+
+gpg = "/usr/bin/gpg";
+keyrings = "/usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-keyring.pgp";
+
+webloginhtml = "login.html";
+websearchhtml = "searchform.html";
+websearchresulthtml = "searchresults.html";
+webupdatehtml = "update.html";
+
+webloginurl = "debian/login.cgi";
+websearchurl = "debian/search.cgi";
+webupdateurl = "debian/update.cgi";
+
+# When should authentication tokens expire?
+authexpires = 600;
+
+# How many bytes to use for the blowfish key (max = 56 (448 bits))
+blowfishkeylen = 10;
+
+# Change this!
+authtokenpath = "/tmp/ldapsessions";
+countrylist = "/usr/lib/analog/domains.tab";
diff --git a/web/update.cgi b/web/update.cgi
new file mode 100755
index 0000000..5fdfb74
--- /dev/null
+++ b/web/update.cgi
@@ -0,0 +1,152 @@
+#!/usr/bin/perl
+
+# (c) 1999 Debian and Randolph Chung. Licensed under the GPL.
+
+use lib '.';
+use strict vars;
+#use Apache::Registry;
+use CGI;
+use Util;
+use URI::Escape;
+use Net::LDAP qw(:all);
+
+my %config = &Util::ReadConfigFile;
+
+my $query = new CGI;
+my $proto = ($ENV{HTTPS} ? "https" : "http");
+
+my $id = $query->param('id');
+my $authtoken = $query->param('authtoken');
+my $password = &Util::CheckAuthToken($authtoken);
+my $editdn = $query->param('editdn');
+
+if (!($id && $password)) {
+ print "Location: $proto://$ENV{SERVER_NAME}/$config{webloginurl}\n\n";
+ exit;
+}
+
+my $ldap;
+
+sub DieHandler {
+ $ldap->unbind if (defined($ldap));
+}
+
+$SIG{__DIE__} = \&DieHandler;
+
+$ldap = Net::LDAP->new($config{ldaphost});
+my $auth = 0;
+my $mesg;
+$mesg = $ldap->bind($editdn, password => $password);
+$mesg->sync;
+$auth = ($mesg->code == LDAP_SUCCESS);
+
+if (!$auth) {
+ $ldap->unbind;
+ &Util::HTMLError("You have not been authenticated. Please Login");
+}
+
+# Authenticated....
+# Get our entry...
+$mesg = $ldap->search(base => $editdn,
+ filter => "uid=*");
+$mesg->code && &Util::HTMLError($mesg->error);
+
+my $entries = $mesg->as_struct;
+if ($mesg->count != 1) {
+ # complain and quit
+}
+
+my @dns = keys(%$entries);
+my $entry = $entries->{$dns[0]};
+
+if (!($query->param('doupdate'))) {
+ # Not yet update, just fill in the form with the current values
+ my %data;
+
+ # Fill in %data
+ # First do the easy stuff - this catches most of the cases
+ foreach (keys(%$entry)) {
+ $data{$_} = $entry->{$_}->[0];
+ }
+
+ # Now we have to fill in the rest that needs some processing...
+ $data{id} = $id;
+ $data{authtoken} = $authtoken;
+ $data{editdn} = $editdn;
+ $data{staddress} = $entry->{postaladdress}->[0];
+ $data{staddress} =~ s/\$/\n/;
+ $data{countryname} = &Util::LookupCountry($data{c});
+
+ $data{email} = join(", ", @{$entry->{emailforward}});
+
+ # finally we can send output...
+ my ($sub, $substr);
+ &Util::HTMLSendHeader;
+ open (F, "<$config{webupdatehtml}") || &Util::HTMLError($!);
+ while () {
+ s/~(.+?)~/$data{$1}/g;
+ print;
+ }
+ close F;
+
+} else {
+ # Actually update stuff...
+ my ($newpassword, $newstaddress);
+
+ if ($query->param('newpass') && $query->param('newpassvrfy')) {
+ if ($query->param('newpass') ne $query->param('newpassvrfy')) {
+ # passwords don't match...
+ &Util::HTMLError("The passwords you specified do not match. Please go back and try again.");
+ }
+ # create a md5 crypted password
+ $newpassword = '{crypt}'.crypt($query->param('newpass'), &Util::CreateCryptSalt(1));
+
+ LDAPUpdate($ldap, $editdn, 'userPassword', $newpassword);
+ &Util::UpdateAuthToken($authtoken, $query->param('newpass'));
+ }
+
+ $newstaddress = $query->param('staddress');
+ $newstaddress =~ s/\n/\$/m;
+
+ my ($lat, $long);
+ ($lat, $long) = &Util::CheckLatLong($query->param('latitude'),
+ $query->param('longitude'));
+
+ LDAPUpdate($ldap, $editdn, 'postalAddress', $newstaddress);
+ LDAPUpdate($ldap, $editdn, 'l', $query->param('l'));
+ LDAPUpdate($ldap, $editdn, 'latitude', $lat);
+ LDAPUpdate($ldap, $editdn, 'longitude', $long);
+ LDAPUpdate($ldap, $editdn, 'c', $query->param('country'));
+ LDAPUpdate($ldap, $editdn, 'postalcode', $query->param('postalcode'));
+ LDAPUpdate($ldap, $editdn, 'telephoneNumber', $query->param('telephonenumber'));
+ LDAPUpdate($ldap, $editdn, 'facsimileTelephoneNumber', $query->param('facsimiletelephonenumber'));
+ LDAPUpdate($ldap, $editdn, 'loginShell', $query->param('loginshell'));
+ LDAPUpdate($ldap, $editdn, 'emailForward', $query->param('email'));
+ LDAPUpdate($ldap, $editdn, 'privatesub', $query->param('privatesub'));
+ LDAPUpdate($ldap, $editdn, 'ircNick', $query->param('ircnick'));
+ LDAPUpdate($ldap, $editdn, 'labeledUrl', $query->param('labeledurl'));
+ LDAPUpdate($ldap, $editdn, 'onvacation', $query->param('onvacation'));
+
+ # when we are done, reload the page with the updated details.
+ my $url = "$proto://$ENV{SERVER_NAME}/$config{webupdateurl}?id=$id&authtoken=$authtoken&editdn=";
+ $url .= uri_escape($editdn, "\x00-\x40\x7f-\xff");
+ print "Location: $url\n\n";
+}
+
+$ldap->unbind;
+
+sub LDAPUpdate {
+ my $ldap = shift;
+ my $dn = shift;
+ my $attr = shift;
+ my $val = shift;
+ my $mesg;
+
+ if (!$val) {
+ $mesg = $ldap->modify($dn, delete => { $attr => [] });
+ } else {
+ $val = [ $val ] if (!ref($val));
+ $mesg = $ldap->modify($dn, replace => { $attr => $val });
+ $mesg->code && &Util::HTMLError("error updating $attr: ".$mesg->error);
+ }
+}
diff --git a/web/update.html b/web/update.html
new file mode 100644
index 0000000..1abb9e3
--- /dev/null
+++ b/web/update.html
@@ -0,0 +1,362 @@
+ debian.org Developers LDAP Maintainence
+
+
+
+
--
2.20.1