The PWDB Library Guide Cristian Gafton and Andrew G. Morgan DRAFT v0.54 1996/12/4 This manual documents the [4mPassword[24m [4mdatabase[24m library (libpwdb). This is a library implementing a generic user information database. It was specifically designed to satisfy the needs of [1mLinux-PAM[22m. ______________________________________________________________________ Table of Contents 1. Introduction 1.1 Motivation 2. Configuration 2.1 The configuration file 3. Implementation 3.1 The core (public) functions 3.1.1 Initialization and termination 3.1.2 Structure management 3.1.3 Database query functions 3.1.4 Diagnostics 3.1.5 Types of database 3.2 Functions supplied by database modules 3.3 Standard entries in pwdb structures 3.4 Helper functions 3.4.1 Public functions 3.4.2 Private functions 3.5 Typical usage 4. Supported modules 4.1 UNIX module 4.2 Shadow module 4.3 NIS module 4.4 DECNIS module 4.5 RADIUS module 5. Proposals/unresolved issues 6. References 7. Acknowledgments ______________________________________________________________________ [1m1. Introduction[0m This is a document describing the libpwdb functions (or how they will function). Cristian and Andrew will be modifying this file as we go along. Hopefully it will describe the library when it is finished. [1m1.1. Motivation[0m Linux, at the level of the kernel, identifies a user by a 16-bit integer--their [4mUID[24m. At the level of a functioning system, the user has other characteristics; a name, group memberships, a home, a number of aliases (email address etc.) and a password. The mapping of a UID to each of these pieces of personal data is with respect to a database file. Unfortunately, depending on circumstances there is no single location for this database. The most basic Linux system might have a single /etc/passwd file, with a single entry for each user, and an /etc/group file containing a list of the groups defined on the local system. In shadow aware systems a user might additionally have an /etc/shadow entry. In a networked environment, all of the users might be identified by entries in a file located on a remote computer. While it is possible for a local system to adopt one scheme to satisfy the demands of the POSIX getpw.. commands, it is problematic for an individual system to simultaneously support a variety of databases to define its users and groups. It is desirable for applications to be able to obtain user-information in a transparent manner. At the same time certain applications might like to obtain information from a specific database for a given user - these applications might perform the task of maintaining and updating databases for example. The functions of libc, do not currently offer this flexibility. It is for this reason that [4mlibpwdb[24m has been created. It is intended to provide a flexible and yet simple database management primarily for user and group information under Linux systems. An API that all applications can use. Should this library become popular, it is anticipated that the database will be extended to include other types of database files. [1m2. Configuration[0m [1m2.1. The configuration file[0m Use of libpwdb requires the presence of a correctly formatted configuration file: /etc/pwdb.conf. It has the following syntax: # # first the list of user databases # user: list1 list2 list3 ... # # next, the list of group databases # group: list1 list2 ... # # end of file # Here, listN has the following form name1+name2+name3. It indicates a collection of databases that are merged to form the record for the user/group. When selecting the default database (PWDB_DEFAULT) the library chooses the list whose first named database contains an entry for the requested user (id). Currently, the nameN items are from the following selection: nis, unix, radius and shadow. Newlines are ignored except where they terminate comments; comments are preceded with `#' characters. A simple example /etc/pwdb.conf file would be: # This is an example /etc/pwdb.conf file. It defines the # database information sources for users in this system # First, we define where user information is stored # (here, users are listed in /etc/passwd and may have # supplementary information in the /etc/shadow file) # Should the user have a shadow entry the combination is # used user: shadow + unix unix # Second, we define where the users' groups are listed # (here the groups are listed in the /etc/group file # in addition, there is group information to be found # in the /etc/sgroup file) group: unix + shadow # # end of file [1m3. Implementation[0m [4mThis[24m [4mwill[24m [4mget[24m [4mcompleted[24m [4mas[24m [4mwe[24m [4mdevelop[24m [4mthe[24m [4mlibrary[0m [1m3.1. The core (public) functions[0m libpwdb offers the following generic interface: [1m3.1.1. Initialization and termination[0m +o int pwdb_start(void) Initialize the library for use by the current application Will read the configuration file and publicize the database policies (as they are listed in /etc/pwdb.conf) in const int *pwdb_policy and const int *pwdb_group_policy. The library maintains a count of the number of times it has been pwdb_start()ed. +o int pwdb_end(void) Once the pwdb_start()ed count returns to zero, this function closes down the library and free()'s all memory allocated by it. Any attempt to pwdb_end() the library more times than it has been pwdb_start()ed will cause PWDB_ABORT to be returned. [4mThis[24m [4mfeature[0m [4mcan[24m [4mbe[24m [4mused[24m [4mby[24m [4man[24m [4mapplication[24m [4mto[24m [4mguarantee[24m [4mthe[24m [4mlibrary[24m [4mis[0m [4mshutdown..[0m while (pwdb_end() == PWDB_SUCCESS); [1m3.1.2. Structure management[0m +o int pwdb_entry_delete(const struct pwdb_entry **e) free() the memory associated with the pointer *e. Its value will be overwritten with '\\0's before the memory is free()d. This is reassuring from the point of view of minimizing security problems. This function should be used to liberate a pwdb_entry returned by pwdb_get_entry(). +o int pwdb_get_entry(const struct pwdb *p, const char *entry, const struct pwdb_entry **e); Read (and duplicate) an entry from the argument pwdb structure. Should the requested entry not prove to be present, PWDB_NOT_FOUND is returned. +o int pwdb_set_entry(const struct pwdb *p, const char *entry, const void *datum, const int length, int (*compare)(const void *, const void *, int), int (*strvalue)(const void *, char *, int), int max_strval_size)) Set an entry in the argument pwdb structure. One can delete an entry from a struct pwdb by calling this function with NULL for datum and -1 for length. The two functions passed as arguments in this call are as follows: int compare(const void *value1, const void *value2, int length) compare the two entry values. They are both of given length in chars. int strvalue(const void *value, char *buffer, int length) Produce a text version of the given entry value. The buffer is the destination of the output text and the length is that of the *value in bytes. Note, the buffer is guaranteed by the calling process to be long enough for the output text. +o int pwdb_delete(const struct pwdb **old) Applications use this to liberate the memory associated with a pwdb structure. Following a successful free(), *old is set to NULL. +o int pwdb_new(const struct pwdb **new, int life_sec) Applications can request a new (blank) pwdb structure with this call. Note that it is returned as a const structure. This is to prevent the application from altering the structure directly. An application should use the library functions to modify this structure. The life_sec time is used to limit the length of time this pwdb structure will be valid for. It is some number of seconds from the present. If life_sec is non-zero, the pwdb structure will expire in that many seconds. Zero indicates the pwdb structure will non expire. int pwdb_merge(const struct pwdb *target, const struct pwdb *source, int overwrite) this function copies the elements of source to target. If overwrite is PWDB_TRUE then all elements of source that are also in target will be overwritten by this call. +o int pwdb_expire(const struct pwdb *p, int life_sec) This function can shorten the lifetime of a the referenced struct pwdb. It computes the expiry time from the present with respect to life_sec and if this is [4mbefore[24m the expiration time currently associated with the *p structure, it shortens the life of the structure accordingly. Note, it is not possible to extend the life of a pwdb structure, only to [4mshorten[24m it. An argument of 0 or less will result in the immediate expiry of the pwdb structure. +o int pwdb_source(const struct pwdb *old, const pwdb_type *src, const char *class, const char *name, const int id) This function is used to set the source of the indicate pwdb structure. The argument src is a pointer to a list of pwdb_type entries. This list is terminated with a _PWDB_MAX_TYPES item. Valid types are listed in the <security/pwdb_public.h> file. The remaining arguments are used to initialize the caching facilities. [1m3.1.3. Database query functions[0m +o int pwdb_locate(const char *class, const pwdb_type *src, const char *name, const int id, struct pwdb **p) Obtain the entry for a given name and/or id in the indicated database. If *p is not NULL the database-module may choose to use the information it contains. It is intended that information obtained with this function is merged with that of the original *p structure. If *p is NULL, then a struct pwdb is allocated by the pwdb_locate() function in the first module used (and eventually merged with the subsequent modules, depending on the local setup). The class is the kind of database we are searching, examples include user and group. +o int pwdb_request(const char *class, const pwdb_type *src, const char *entry, struct pwdb **p) This function will further query the database, for an entry that may depend on the entries already present in the *p structure. The entrys that can be appended to the existing *p structure are defined for the class of database. For example, one may request the [4m"groupids"[24m entry from "group" class, which will search the group database for the list of groups to which a given user belongs. The name of the user is passed as an entry in the preexisting pwdb structure. +o int pwdb_replace(const char *class, const pwdb_type *src, const char *name, const int id, struct pwdb **p) Add/replace the entry for a name and/or id in the indicated list of databases. The fields for the new database entries are taken from the p argument. The src argument is the list of entries that will be updated. +o int pwdb_remove(const char *class, const pwdb_type *src, const char *name, const int id, struct pwdb **p) Remove the entry for the indicated name and/or id in the indicated database. If not NULL the remove function may obtain access information from the p argument. Note, this function only acts on the specified class of database. If reference to a name or id is present in another class of database, then it is the responsibility of the application to purge these databases too. The pwdb_remove() function is not sufficiently powerful to follow up on cross-references. As an example of the above concern, consider the removal of name ``joe'' from the "user" database. The "group" database entries that refer to ``joe'' as a group-member are unaffected by this pwdb_remove() request. Instead, it is the responsibility of the calling application to search for such entries and systematically pwdb_remove() them. +o int pwdb_support(const char *class, const pwdb_type *src, const char *entry_name) Indicate whether the given entry name is supported by the given database(s). PWDB_SUCCESS indicates yes. Anything else is NO (or something more specific, could be ``yes, if you supply a pass_phrase'' for example). +o int pwdb_flags(const char *class, const pwdb_type *db, pwdb_flag *flag_p) In order to know in advance whether a process is able to read/modify a specified database, this command is provided by each module. The input arguments are the class of database (user, group etc.), db (the database(s) we are going to use) and some storage space for the returned flags. Valid return flags which can be logically OR'd together are: PWDB_F_NOACCESS insufficient privilege to access database PWDB_F_NOUPDATE insufficient privilege to alter an entry PWDB_F_PASS_PHRASE to access the database, the process must supply a "pass_phrase" entry with a preallocated pwdb structure (use pwdb_new() call) [4m..and..[0m [4mmore[24m [4mflags[24m [4mare[24m [4mlikely[24m [4mto[24m [4mbe[24m [4madded[0m To establish if a given flag is set the following macro is supplied: +o pwdb_on(flag, PWDB_F_XXX) it returns TRUE(1) or FALSE(0). [1m3.1.4. Diagnostics[0m +o const char *pwdb_strerror(int pwdb_error) return a textual description of the indicated return value. Valid return codes are: PWDB_SUCCESS task completed successfully PWDB_BAD_REQUEST request not recognized PWDB_TOO_WEAK insufficient privilege for operation PWDB_ABORT internal failure - seek help PWDB_BLOCKED another process has locked resource PWDB_MALLOC insufficient memory for operation PWDB_NOT_FOUND requested item was not found PWDB_PASS_PHRASE_REQD pass_phrase needed to satisfy request [4mhere[24m [4mthe[24m [4mapplication[0m [4mshould[24m [4msupply[24m [4ma[24m [4mpwdb[24m [4mstructure[24m [4mwith[24m [4ma[24m [4m"pass_phrase"[24m [4mentry[24m [4mand[0m [4mcall[24m [4mthe[24m [4mfunction[24m [4magain[0m PWDB_CONF_ERR there is a problem with the PWDB_CONF file. PWDB_EXPIRED the referenced pwdb structure has expired it is no longer valid and should be deleted. PWDB_UNSUPPORTED this function is not supported by some module (not supported means also unimplemented, for a while...) PWDB_TIMEOUT a timeout occured while performing the function. Presently could show up only when using RADIUS interface. [1m3.1.5. Types of database[0m The pwdb_type of database a request is associated with is given by one of the following values: PWDB_DEFAULT no database indicated, use configured list PWDB_UNIX generic /etc/passwd and /etc/group files PWDB_SHADOW /etc/shadow and /etc/gshadow [4mIntended[24m [4mto[24m [4msupplement[24m [4mother[0m [4mdatabases[0m PWDB_NIS Use NIS server PWDB_RADIUS Use RADIUS server PWDB_DECNIS Use a NIS server configured for Digital Equipment Corp Enhanced Security, or a Solaris server with an adjunct NIS password file. +o const char *pwdb_db_name(pwdb_type src) return a character representation of the database functions [1m3.2. Functions supplied by database modules[0m Each module must provide 7 functions to the generic pwdb interface. They are registered with the generic interface via a structure of the following form: struct _pwdb_module { pwdb_type type; /* type of database (code) */ const char *name; /* type of database (text) */ const char *class; /* class of database (user/group) */ /* FUNCTIONS: used to access the relevant database */ int (*locate)(const char *name, const int id, const struct pwdb **p); int (*request)(const char *entry_name, const struct pwdb **p); int (*replace)(const char *name, const int id, const struct pwdb **p); int (*delete)(const char *name, const int id, const struct pwdb **p); int (*support)(const char *entry_name); int (*flags)(pwdb_flag *flags); int (*cleanup)(int code); }; For the functions above taking a name and an id entry, the application may choose to leave one unspecified with the following defaults: PWDB_NAME_UNKNOWN just look at the id field PWDB_ID_UNKNOWN just look at the name field In the case that the application supplies neither the name or the id, the module functions will try to obtain the relevant information from the argument pwdb structure. It is legal for both the name and id to be specified. In this case they must both match an entry in the database to satisfy one of the above function calls. If both values are supplied and there is no entry in the database which matches them, PWDB_BAD_REQUEST is returned. The structure is registered via an entry in the modules list (see pwdb_module.c). [1m3.3. Standard entries in pwdb structures[0m The following are standard entries in the pwdb structure. They can be read/written with calls to pwdb_g/set_entry. First, we consider the "user" class of databases. For these, two entries are mandatory. They correspond to the name of the user and the user's uid. user character string; the user's login id. uid uid_t; the user's [4muser-id[24m. The next entries are named by convention. Where possible new database functions should map these entries into their corresponding fields. These entries correspond to the entries in the /etc/passwd file. passwd character string; the [4mencrypted[24m password for the user. defer_pass This entry is intended to take care of situations that the normal passwd field is not used for the password. The defer_pass entry contains a character string that has typically two functions: +o it indicates that the password is to be stored in the database that sets this entry. +o it serves as a substitution string for databases that would normally contain the password entry. For example, for a unix+shadow setup, defer_pass would have the value ``x''. The unix (no shadow) value for this entry is ``U'' which implies that the passwd field came from the user's entry in the /etc/passwd file. gid gid_t; the user's principal [4mgroup-id[24m. group character string; naming the user's principal group. gecos character string; giving a more complete name for the user. It is conventional for this field to contain office and other information concerning the real-world identity of the user. dir character string; the home directory of the user. shell character string; the shell that the user prefers to use. These entries correspond to the entries in the /etc/group file [4min[0m [4maddition[24m [4mto[24m [4mthe[24m [4muser[24m [4mand[24m [4mgid[24m [4mentries[24m [4mabove[24m. They can be pwdb_request()d from the "group" class of databases. groups character string; listing the group memberships of the user. The field separators are commas -- no spaces. groupids array of gid_t; an array containing the group id's of the user in numerical form. The following are intended to correspond to /etc/shadow entries. last_change long integer; day of last change of password min_change long integer; minimum number of days between password changes max_change integer; maximum number of days between password changes warn_change long integer; number of days to warn user to change their password before it expires defer_change long integer; number of days after a user's password has expired before the user is denied access expire long integer; day the user's account expires shadow_flags long integer; reserved for use by underlying shadow implementation The following is the entry used to supply a [4mclear-text[24m password for access to the database. pass_phrase character string; this is the password required to access the user's record in a database When integrating another database format the implementor is strongly encouraged to try to reuse the entries above to the extent they are appropriate. Should there be an absent entry in any database, the database management functions should be able to supply a reasonable default but only when updating its database. [1m3.4. Helper functions[0m [1m3.4.1. Public functions[0m +o char *_pwdb_delete_string(char *s) overwrite the string 's' and return NULL. usage: old_ptr = _pwdb_delete(old_ptr); +o char *_pwdb_dup_string(const char *s) malloc() a copy of the string 's'. Return its address or NULL if s == NULL or on error. this memory will [4mnot[24m be free()'d by a call to pwdb_end(). +o void pwdb_print_pwdb_struct(const struct pwdb *p) Dump the contents of *p to the stderr. Useful for debugging. [1m3.4.2. Private functions[0m +o static struct pwdb *_pwd_check(const struct pwdb *p) Establish if the pwdb structure was allocated by the library This library should not honor requests from elsewhere. return the local version (non-const) of this structure or NULL on error. this function is strictly designed for the use of the generic code. Both modules and applications should never need to call it. [1m3.5. Typical usage[0m Here is a skeleton usage for a login type program. pwdb_start(); pwdb_locate("user", PWDB_DEFAULT, username, PWDB_ID_UNKNOWN, &pw); pwdb_request_group("group", PWDB_DEFAULT, "groupids", &pw); pwdb_get_entry(pw, "uid", &e1); pwdb_get_entry(pw, "gid", &e2); pwdb_get_entry(pw, "groupids", &e3); pwdb_end(); [1m4. Supported modules[0m PLEASE NOTE. Currently few group functions have been implemented. [1m4.1. UNIX module[0m This section documents the current state of the UNIX module. From the point of view of the application, it is named "unix" and has the pwdb_type PWDB_UNIX. Entries supported by the "user" class of the UNIX module are as follows: user - username uid - user-id gid - group-id passwd - encrypted password defer_pass - "U" unless set by other database gecos - user information dir - home directory shell - shell executable Entries supported by the "group" class of the UNIX module are as follows: group - username gid - group-id passwd - encrypted password users - text list of user names separated by commas The pwdb_request() function call is only supported for the "group" class. The two entries that may be requested are: groups - text list of group names separated by commas groupids - array of gid_t values that contain the numerical form of the "groups" entry. Note, for such requests to be honored the name of the user should be contained in the pwdb ** argument prior to the pwdb_request() call. [1m4.2. Shadow module[0m This section documents the current implementation of the [4mshadow[0m database module. The [4mshadow[24m module does not make any use of the id parameter. Since the shadow database does not contain such an entry, the only way of identifying a user is with a name argument. However, an id based lookup is possible, if the shadow database is searched after a database that contains a user-uid mapping. In this case the ``user'' entry in the partially built pwdb structure is used to locate the appropriate entry in the shadow file. The shadow module is designed to work in conjunction with a database that provides the standard user-uid mapping. It should be noted that it does not provide sufficient information to support a user login session. Entries supported by the "user" class of the shadow module are: user - username passwd - encrypted password last_change - date password was last changed min_change - minimum period before password can be changed max_change - lifetime of current password warn_change - number of days prior to expiry that the user should be warned defer_change - grace period before password is finally invalid expire - date account expires Entries supported by the "group" class of the shadow module are: group - groupname passwd - encrypted group password users - text list of user names separated by commas (members of the group) admins - text list of user names separated by commas (administrators of the group) [1m4.3. NIS module[0m +o user removal is not possible. (lack of documentation) +o user creation is not possible (lack of documentation) [1m4.4. DECNIS module[0m The decnis module allows use of some extensions to the basic NIS maps. The two current extensions supported are the Solaris passwd.adjunct and the Digital Equipment Corp prpasswd maps. Both of these are shadow password schemes for basic (non-secure) NIS. The solaris support is untested. This option encompasses the NIS option, and the decnis option should replace the standard nis option in the pwdb.conf file. In the event of a user not having a shadow NIS password entry, the result should be identical to that returned by the NIS module above. [4mWarning1[24m Support for many of the extensions (password expiry, life time, account locking etc..) is not present in this version. Just raw authentication. [4mWarning2[24m Clustering your Linux boxes with this software will reduce the security level of your NIS server/cluster. Users may have access to encrypted passwords of other users. +o user removal is not possible. (lack of documentation) +o user creation is not possible (lack of documentation) [1m4.5. RADIUS module[0m The RADIUS module is acting just as a user validation mechanism. The official Livingston radiusd 2.0 is supported, but in order to take advantage of the all information and auth tokens the radius server can provide, a session PAM module should be written, and be stacked over pam_unix. The following should be taken into consideration when writing applications to authenticate to a radius server: +o the pwdb group functions are not supported (RADIUS does not have this concept) +o Other than checking for username/password pairs, the radius module can not be used alone with the stock radiusd server to handle the user login without a suitable RADIUS client (for example there is no way to get an UID for a user). However, with few hacks to the radius server and proper modification of the /etc/raddb/dictionary file this module uses, a NIS-like environment could be achieved. Full documentation on how to achieve this will be added later. +o All radius module functions that access the remote server require the presence of the "pass_phrase" pwdb entry which contains the user password in [4mclear[24m [4mtext[24m. The radius module will destroy this information as soon as it can dispose of it, so an application can assume that after a call to a function in the radius module which returned with PWDB_SUCCESS, the pass_phrase entry is wiped out. +o When updating a RADIUS user password, one should supply [1mboth[0m pass_phrase and passwd entries in [4mclear[24m [4mtext[24m. By convention, the pass_phrase contains the old password (which is required by the RADIUS server to authenticate the user) and the passwd entry contains the new password. One should be very careful about this issue, as some other modules used before RADIUS authentication may set the value of passwd entry, and the application should make sure that the clear text password is passed to the RADIUS module for changing password. The RADIUS module will wipe out both pass_phrase and password entries - thus the application can not rely on validity of any of those entries in the pwdb structure after a call to the update function of the RADIUS module. Note that the RADIUS server must permit changing of the passwords user passwords. IF the RADIUS does not accept changing the user passwords, a PWDB_TIMEOUT will occur. +o The password update is the [1monly [22mfunction supported by the RADIUS update function. Entries supported by this database are set according to the definitions from /etc/raddb/dictionary file. Three entries have a special meaning when calling the RADIUS functions: user, passwd and pass_phrase. The passwd and pass_phrase will be wiped out by the RADIUS functions as soon as the module can dispose them. The reponse from the RADIUS server is processed and entries are set in the pwdb structure according to the names from the dictionary file. A sample RADIUS dictionary entry list is provided here - valid for Livingston RADIUSD 2.0: #--------------------------------------------------------------------------- # # @(#)dictionary 1.3 10/1/96 Copyright 1991 Livingston Enterprises Inc # #--------------------------------------------------------------------------- # # This file contains dictionary translations for parsing # requests and generating responses. All transactions are # composed of Attribute/Value Pairs. The value of each attribute # is specified as one of 4 data types. Valid data types are: # # string - 0-253 octets # ipaddr - 4 octets in network byte order # integer - 32 bit value in big endian order (high byte first) # date - 32 bit value in big endian order - seconds since # 00:00:00 GMT, Jan. 1, 1970 # ATTRIBUTE User-Name 1 string ATTRIBUTE Password 2 string ATTRIBUTE CHAP-Password 3 string ATTRIBUTE NAS-IP-Address 4 ipaddr ATTRIBUTE NAS-Port 5 integer ATTRIBUTE Service-Type 6 integer ATTRIBUTE Framed-Protocol 7 integer ATTRIBUTE Framed-IP-Address 8 ipaddr ATTRIBUTE Framed-IP-Netmask 9 ipaddr ATTRIBUTE Framed-Routing 10 integer ATTRIBUTE Filter-Id 11 string ATTRIBUTE Framed-MTU 12 integer ATTRIBUTE Framed-Compression 13 integer ATTRIBUTE Login-IP-Host 14 ipaddr ATTRIBUTE Login-Service 15 integer ATTRIBUTE Login-TCP-Port 16 integer ATTRIBUTE Reply-Message 18 string ATTRIBUTE Callback-Number 19 string ATTRIBUTE Callback-Id 20 string ATTRIBUTE Framed-Route 22 string ATTRIBUTE Framed-IPX-Network 23 ipaddr ATTRIBUTE State 24 string ATTRIBUTE Session-Timeout 27 integer ATTRIBUTE Idle-Timeout 28 integer ATTRIBUTE Termination-Action 29 integer ATTRIBUTE Called-Station-Id 30 string ATTRIBUTE Calling-Station-Id 31 string ATTRIBUTE Acct-Status-Type 40 integer ATTRIBUTE Acct-Delay-Time 41 integer ATTRIBUTE Acct-Input-Octets 42 integer ATTRIBUTE Acct-Output-Octets 43 integer ATTRIBUTE Acct-Session-Id 44 string ATTRIBUTE Acct-Authentic 45 integer ATTRIBUTE Acct-Session-Time 46 integer ATTRIBUTE Acct-Terminate-Cause 49 integer ATTRIBUTE NAS-Port-Type 61 integer ATTRIBUTE Port-Limit 62 integer [1m5. Proposals/unresolved issues[0m There have been proposals to offer other flavors of database access through this library. It is hoped that the library will prove flexible enough to support this need. But such support is not currently the primary goal. In other words Cristian and Andrew will not be doing it in the short term.. so unless you want to volunteer, please don't hold your breath! :) [1m6. References[0m [1m7. Acknowledgments[0m This document was written by Cristian Gafton and Andrew G. Morgan. Thanks are also due to the following for their many helpful comments and suggestions: Nalin Dahyabhai, Torsten Duwe, Elliot Lee, Marek Michalkiewicz, Aleph One and Roland Schemers. [4mMore[24m [4mnames[24m [4mto[24m [4mbe[24m [4madded..[0m