From c50d88536a4feb3087d1aa802e110250cb2861fc Mon Sep 17 00:00:00 2001 From: jgg <> Date: Wed, 22 Sep 1999 03:19:03 +0000 Subject: [PATCH] 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 + +

+
+ + + + + +
Authentication Required


If you are not a developer, please return to the main search page, otherwise, enter your Debian user ID and password below

login:@debian.org
Password:
+    + +
+
+ + 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} .= '\n"; + + if ($vacation) { + $outsub{searchresults} .= "\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} .= "
'."$name "; + $outsub{searchresults} .= "($ufdn)
$vacation
"; + + # 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 + +

+
+ + + + + + + + + + + +
Debian Developers Database Search +
(any field can be left blank....)
+Help on searching
First name: +Fuzzy search
Last name: +Fuzzy search
login: +Fuzzy search
IRC nickname: +Fuzzy search
PGP/GPG fingerprint: +Fuzzy search
country: + +
On vacation
+ +
+Login (registered developers only) +
+ 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 + +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Debian Developers Database Maintanence
+ login:~uid~@debian.org +
+ Name:~cn~ ~mn~ ~sn~ +
+ Change password:
(re-enter to verify) +
+
+ +
+ Street address: + + +
+ City/State: + + +
+ Country: + + +
+ Postal code: + + +
+ Latitude / Longitude:
+ (format: +-DDDMMSS; + is north/east) +
+ / + +
+ Phone: + + +
+ FAX:
+
+ +
+ Preferred shell: + + +
+ email forwarded to: + + +
+ debian-private subscript addr: + + +
+ IRC nickname: + + +
+ Web page: + + +
+ Vacation message:
+ (Note: if this is set, you will be shown to
+ be on vacation)
+
+ +
+ +
+ Return to search page
+ Logout! +
+ -- 2.20.1