diff -ur courier-imap-1.4.2.orig/authlib/README.authmysql.myownquery courier-imap-1.4.2/authlib/README.authmysql.myownquery --- courier-imap-1.4.2.orig/authlib/README.authmysql.myownquery Tue Jan 8 06:01:22 2002 +++ courier-imap-1.4.2/authlib/README.authmysql.myownquery Thu Feb 14 00:14:33 2002 @@ -2,12 +2,13 @@ - Developer Notes for courier-imap-myownquery.patch + Developer Notes and Usage Instructions + of courier-imap-authmysql-myownquery - document version: 1.03 + document version: 1.20 author: Pawel Wilk @@ -24,57 +25,739 @@ +PART I - Usage Instructions -0 What's that? + 1 What's that? + + 2 When I'll need it? + + 3 How does it work? + 3.1 configuration variables + 3.2 queries + 3.3 substitutions + 3.4 triggers + + 4 Examples of usage + 4.1 corporate mail system + 4.1.1 database structure + 4.1.2 authdaemon configuration + 4.2 virtual mail domains provider + 4.2.1 database structure + 4.2.2 authdaemon configuration + +PART II - Developer Notes + + 1 Modifications overview + + 2 Definitions + + 3 New data types + 3.1 struct var_data + 3.2 typedef size_t (*parsefunc) + + 4 New functions + 4.1 get_variable + 4.2 parse_core + 4.3 ParsePlugin_counter + 4.4 ParsePlugin_builder + 4.5 parse_string + 4.6 validate_password + 4.7 get_localpart + 4.8 get_domain + 4.9 get_username + 4.10 parse_select_clause + 4.11 parse_chpass_clause + 4.12 auth_mysql_on_trigger -1 Modifications overview + 5 Ideas and TODO -2 Definitions + 6 Thanks -3 New data types - 3.1 struct var_data - 3.2 typedef size_t (*parsefunc) - -4 New functions - 4.1 get_variable - 4.2 parse_core - 4.3 ParsePlugin_counter - 4.4 ParsePlugin_builder - 4.5 parse_string - 4.6 validate_password - 4.7 get_localpart - 4.8 get_domain - 4.9 parse_select_clause - 4.10 parse_chpass_clause - -5 Ideas and TODO - -6 Thanks +//////////////////////////////// PART I - Usage /////////////////////////////// *----------------------- - 0 What's that? + 1 What's that? *----------------------- -Courier-imap-myownquery.patch allows administrator to set own MySQL queries -used by authdaemon to authenticate user (including fetchig credentials) and to -change user's password. It allows to construct SELECT or UPDATE clause in the -configuration file (authmysqlrc) by adding two new configuration variables: -MYSQL_SELECT_CLAUSE and MYSQL_CHPASS_CLAUSE. It may be useful in the mail -environments where there is such a need to have different database structure -and/or tables scheme than expected by authmysql module. +Courier-imap-myownquery feature allows administrator to set his own MySQL +queries used by authdaemon to authenticate a user (including fetchig his +credentials) and to change the user's password. It allows to construct +SELECT and UPDATE clause in the configuration file (authmysqlrc) using +new configuration variables. It may be useful in the mail environments where +there is such a need to have different database structure and/or tables +scheme than expected by authmysql module. It also implements a small parsing engine for substitution variables which -may appear in the clauses and are used to put informations like username -or domain into the right place of a query. +may appear in the clauses and are used to put information like a username +or a domain into the right place within the query. -This patch was created using `diff -Nur` on courier-imap-1.3.12 source. + *----------------------- + 2 When I'll need it? + *----------------------- + + o When you already have some MySQL database filled up with the data + and there is no chance to change the whole structure to make it + working with standard authmysql table. Typical situation is when all the + data required to authenticate a user is arranged in more than one table. + + o When you have some great idea how to make the database structure + more efficient due to your needs and your requirements. + + o When doing something 'by-myself' is in your style and you just want + to create your own database, just to feel the pleasure of doing + something original. :) + + + + + + *----------------------- + 3 How does it work? + *----------------------- + +There are three things which the feature concerns: + +- fetching clauses from the configuration file +- doing substitution replacements inside of clauses +- passing prepared query on to the mysql interface funtions + +3.1 configuration options + +You can apply your own MySQL queries using a set of the configuration options. +The options you'll need to make the authmysql your slave are: + +MYSQL_SERVER (required) +MYSQL_USERNAME (required) +MYSQL_PASSWORD (required) + + The server name, userid, and password used to log in. + +MYSQL_DATABASE (required) + + The name of the MySQL database we will open. + +DEFAULT_DOMAIN (optional) + + If DEFAULT_DOMAIN is defined, and someone tries to log in as + 'user', we will look up 'user@DEFAULT_DOMAIN' instead. + +USER_DOMAIN_CONCAT (optional) + + The USER_DOMAIN_CONCAT defines a character(s) used to + concatenate a local part and a domain while parsing + the $(username) substitution variable (see section 3.3 + for more info). If it's not defined the @ sign is assumed. + +USER_DOMAIN_SEPARATORS (optional) + + This may contain the set of characters used by parsing + routines to split local part of the virtual mailbox name + from the part which describes the domain name. If it's not + defined the set containing @% is assumed, so the user can + enter either: user@domain or user%domain when he wants to be + authenticated. + +MYSQL_SELECT_CLAUSE (required) +MYSQL_CHPASS_CLAUSE (required under some circumstances) + + These are the major options you should use. See 3.2 section + for more info. + +MYSQL_ONSUCCESS_CLAUSE (optional) +MYSQL_ONFAIL_CLAUSE (optional) + + These are used to do a MySQL query whether user has passed + the authentication verification (MYSQL_ONSUCCESS_CLAUSE) + or there was the authentication failure (MYSQL_ONFAIL_CLAUSE). + Query results have no meaning. You can use the same + substitution variables in your query as with + MYSQL_SELECT_CLAUSE. See 3.4 section for more info. + +The options which have no effect, and may be safetly left blank are: + +MYSQL_USER_TABLE +MYSQL_CRYPT_PWFIELD +MYSQL_CLEAR_PWFIELD +MYSQL_UID_FIELD +MYSQL_GID_FIELD +MYSQL_LOGIN_FIELD +MYSQL_HOME_FIELD +MYSQL_NAME_FIELD +MYSQL_MAILDIR_FIELD +MYSQL_QUOTA_FIELD +MYSQL_WHERE_CLAUSE + +3.2 queries + +The feature adds two configuration options (clauses), which are parsed first, +and then applied as MySQL queries to MySQL interface routines. These options +are: MYSQL_SELECT_CLAUSE and MYSQL_CHPASS_CLAUSE. After each option a number of +spaces and/or tabs is allowed, and then MySQL query is expected. For better +look, your queries can have line breaks. Each line break should be preceded by +the backslash sign. Look into examples chapter (4) to see how it should look +like. First clause is used to authenticate a user, and the second to change his +password. + +You should note that a query identified by MYSQL_SELECT_CLAUSE should return +fixed number (9) of fields and each field should match the variable expected +by authentication routines. These fields are: + +* username - which is the currently logged user's username (or the + username with domain if you want it) + +* cryptpw - which is the user's crypted password + +* clearpw - which is the user's plaintext password + +* uid - which is a numerical UID value used as a process's UID when + accessing the mailbox directory + +* gid - as above, but refers to GID + +* home - which contains full path to the user's home directory + +maildir - which contains the directory name inside the user's home + which is treated as INBOX folder when accessing mailbox + - if it's empty then the 'Maildir' string is used + +quota - which describes a quota size for the mailbox + +fullname - which may contain the user's fullname + +(The fields marked by the asterix sign are required and cannot have an + empty results) + +So, the typical query clause may start with: + +MYSQL_SELECT_CLAUSE SELECT \ + users.username, \ + users.cryptpw, \ + users.clearpw, \ + domains.uid, \ + domains.gid, \ + users.mailbox_path) \ + '' \ + domains.quota, \ + '' \ +... + +Note that in this short example we're assuming that we have two tables +(users and domains) and INBOX path is always called 'Maildir' and +we're not using the fullname field (the query will always return an empty +string in its place). + +Also note that you may discard one of the password fields if you don't want +to use an authentication mechanism, which needs it. For example, if you don't +want to use MD5-CRAM you may put '' into the place of clearpw (because, for +example you're in paranoid mode and you don't even want to keep plain passwords +in the database:). + +3.3 substitutions + +Substitutions are strings, which may appear in your query, and which have a +special meaning. You can also call them substitution variables. If substitution +variable is known for a clause context then it is parsed. If it isn't known the +error is generated. In the default compilation of authmysql module any +substitution variable is declared inside of two substrings - the first is a +dollar sign concatenated with opening parenthesis, and the second is a closing +parenthesis sign. First symbol identifies beginning of a substitution variable, +and the second closes it. The string between the beginning and the closing +symbol is called substitution variable's name. + +When, as I said before, the name is known to the parsing routine the +substitution is made and the proper value appears in place of the substitution +variable, while passing on the query for later processing. + +Allowed substitution variables: + +context: MYSQL_SELECT_CLAUSE, MYSQL_ONFAIL_CLAUSE, MYSQL_ONSUCCESS_CLAUSE + +$(local_part) will be replaced by currently verified user's username + (without the domain part) + +$(domain) will be replaced by currently verified user's domain + name (if present, or if not present but the + DEFAULT_DOMAIN was used) or by the empty, zero-length + string if the domain cannot be obtained + +$(username) will be replaced by currently verified user's username + concatenated with the given domain name using symbol + defined by USER_DOMAIN_CONCAT - if the domiain name + cannot be obtained (even by looking up DEFAULT_DOMAIN) + the separation sign will not appear and only the given + username will be presented + +context: MYSQL_CHPASS_CLAUSE + +$(local_part) will be replaced by currently verified user's username + (without the domain part) + +$(domain) will be replaced by currently verified user's domain + name (if present, or if not present but the + DEFAULT_DOMAIN was used) or by the empty, zero-length + string if the domain cannot be obtained + +$(username) will be replaced by currently verified user's username + concatenated with the given domain name using symbol + defined by USER_DOMAIN_CONCAT - if the domiain name + cannot be obtained (even by looking up DEFAULT_DOMAIN) + the separation sign will not appear and only the given + username will be presented + +$(newpass) will be replaced by currently authenticated user's + new password to set up (plaintext password) + +$(newpass_crypt) will be replaced by currently authenticated user's + new password to set up (MD5 form created from entered + plain form) + +3.4 triggers + +Triggers are MySQL queries, which are performed depending on authentication +state. Currently, there are two triggers which you may use. First is called +MYSQL_ONSUCCESS_CLAUSE and it is performed when the authentication succeedes. +The second is called MYSQL_ONFAIL_CLAUSE and has the reverse meaning. You can +declare triggers in the authmysqlrc configuration file. They can be used to +arrange some logging facility in the database or just to keep last times +of the successful/failed login tries. The typical trigger, which puts last +login date into the users' table can look like this: + +MYSQL_ONSUCCESS_CLAUSE UPDATE users SET last_login=CURRENT_TIMESTAMP \ + WHERE username='$(username)'; + +or, if you would like to know about last login failure for users you can try: + +MYSQL_ONFAIL_CLAUSE UPDATE users SET last_bad_login=CURRENT_TIMESTAMP \ + WHERE username='$(username)'; + +Note, that YOU CAN use the triggers even if you aren't using +MYSQL_SELECT_CLAUSE. Also note, that there is such a possibility that ONFAIL +trigger may be performed without a proper username. Take it into consideration +when creating queries to avoid messy data on INSERT operations. + + + + + + + *----------------------- + 4 Examples of usage + *----------------------- + +The "ownquery" feature gives you possibility to adapt an authentication query +to the database. So the first thing you have to do is to design the database +structure you need, whithout being grieved at what structure authentication +routines like. You have to take care about four essential things: + + o The database + + o The users' data in the database + + o The proper directories for keeping virtual mailboxes and a system user + which can read and write them + + o The proper MySQL queries in your authmysqlrc configuration file + +4.1 corporate mail system + +This example is concerned about a corporate mail system with a small +count of served virtual domains. The database scheme was derived from tpop3d +documentation and modified a bit. + +4.1.1 database structure + +Our goal here is to separate the data responsible for keeping mailbox +credentials from the data, which describes a domain. + +Let's create some tables for our example, filled up with an example data: + +table: domains + +purpose: associates virtual domain with domain name and informations + necessary to access mailboxes withing the domain + +fields: domain_name - fully qualified domain name + path_prefix - absolute pathname which points to + a directory where domain's mailboxes + are located + quota - default quota for each mailbox + uid - UID used to work on mailboxes + gid - GID used to work on mailboxes + + +----------------+-------------+-----+-----+----------+ + | domain_name | path_prefix | uid | gid | quota | + +----------------+-------------+-----+-----+----------+ + | exampledom.com | /var/mail/x | 555 | 555 | 10000000 | + | pld.org.pl | /var/mail/p | 556 | 556 | 20000000 | + | pld.net.pl | /var/mail/p | 556 | 556 | 20000000 | + +----------------+-------------+-----+-----+----------+ + +table: users + +purpose: associates virtual mailbox with user and domain name, + and with informations necessary to access mailbox + +fields: username - user login name (mailbox name) + domain_name - fully qualified domain name + mailbox_path - relative pathname for mailbox + (will be appended to the path_prefix + from domain_auth table to specify + user's mailbox location) + cryptpw - crypted password + plainpw - plaintext password + + +----------+----------------+--------------+------------+--------+ + | username | domain_name | mailbox_path | cryptpw | plainpw | + +----------+----------------+--------------+-----------+---------+ + | siefca | pld.org.pl | s/siefca | $1$fs45.. | dupa.8 | + | siefca | pld.net.pl | s/siefca | $1$fs45.. | dupa.8 | + | f00bar | exampledom.com | foobar | $1$g44w.. | secret | + +----------+----------------+--------------+-----------+---------+ + +Using MySQL monitor you can create these tables entering CREATE sequences. +Be sure to connect to the database using administrative MySQL account +(usualy: mysql -u mysql -p). + +--------------------- cut here + +# Create the database called vmail. + +CREATE database vmail; + +# Create an example MySQL user, which can read, write and delete data from +# vmail database. Username: vuser Password: secret_password + +GRANT SELECT,INSERT,UPDATE,DELETE ON vmail.* + TO vuser@localhost + IDENTIFIED BY 'secret_password'; + +FLUSH PRIVILEGES; + +# Create the tables. + +use vmail; + +CREATE TABLE domains ( + domain_name char(255) DEFAULT '', + path_prefix char(255) DEFAULT '' NOT NULL, + uid int(10) unsigned DEFAULT '15000' NOT NULL, + gid int(10) unsigned DEFAULT '15000' NOT NULL, + quota char(255) DEFAULT '2000000' NOT NULL, + KEY domain_name (domain_name(255)) + ); + +CREATE TABLE users ( + username char(128) DEFAULT '' NOT NULL, + domain_name char(255) DEFAULT '', + mailbox_path char(255) DEFAULT '' NOT NULL, + cryptpw char(128) DEFAULT '' NOT NULL, + clearpw char(128) DEFAULT '' NOT NULL, + KEY username (username(128)) + ); + +# Create an example virtual domain entry +# name : exampledom.com +# uid : 555 +# gid : 555 +# path : /var/mail/x +# quota : 10 Megs per mailbox + +INSERT INTO domains VALUES ('exampledom.com', '/var/mail/x', 555, 555, + '10000000'); + +# Create an example virtual user entry +# username : siefca +# domain name : exampledom.com +# cryptpw : $1$wIfVZ8uK$qhagYAcIoZpQM83Et7c1e/ +# clearpw : dupa.8 +# mailbox path : s/siefca + +INSERT INTO users VALUES ('siefca', 'exampledom.com', 's/siefca', + '$1$wIfVZ8uK$qhagYAcIoZpQM83Et7c1e/', + 'dupa.8'); + +--------------------- cut here + +Note: If you would like to have your passwords more safe then just omit the + clearpw column and put '' into the config-query in its place while + doing SELECT on a database. But be ware - you'll be unable to use + authentication methods which needs it, like MD5_CRAM. + +4.1.2 authdaemon configuration + +When our database is ready we can set up the configuration. :-) Go to +authmysqlrc file and edit it. + +At the beginning we should take care about general informations, which +are identifying our database: + +MYSQL_SERVER localhost +MYSQL_USERNAME vuser +MYSQL_PASSWORD secret_password +MYSQL_DATABASE vmail + +Then we should add a clause responsible for authenticating user and +fetching credentials: + +DEFAULT_DOMAIN exampledom.com + +MYSQL_SELECT_CLAUSE SELECT \ + users.username, users.cryptpw, users.clearpw, \ + domains.uid, domains.gid, \ + CONCAT_WS('/',domains.path_prefix,users.mailbox_path), \ + '', domains.quota, '' \ + FROM users, domains \ + WHERE domains.domain_name='$(domain)' \ + AND users.username='$(local_part)' \ + AND domains.domain_name=users.domain_name + + +Note the '' in the place of field which tells where user's INBOX resides +and in place of realname field. You should use '' if you want to put an empty +value as a query result for some field. + +We also should add some configuration for changing user's password: + +MYSQL_CHPASS_CLAUSE UPDATE \ + users \ + SET clearpw='$(newpass)', \ + cryptpw='$(newpass_crypt)' \ + WHERE username='$(local_part)' \ + AND domain_name='$(domain)' + +Last things to have sure... +Create a system user/group and a proper directory structure. In our example: + +groupadd -g 555 xdomain +useradd -u 555 -g 555 xdomain +mkdir -p /var/mail/x/s/siefca +chmod -R 0770 /var/mail/x +maildirmake /var/mail/x/s/siefca/Maildir +chown -R xdomain.xdomain /var/mail/x + +Now, restart the authdaemon and see if it works. Try: telnet 0 pop3 + +and type: + +USER siefca [ENTER] +PASS dupa.8 [ENTER] + +You should get Ok response. ;) + +4.2 virtual mail domains provider + +Let's consider more complicated database scheme, where is a need to +associate a lot of information with the domain name, including registrant +information, owner, etc. That implies data separation between domain name, +user and domain additional informations (which are unwanted when +authentication process takes place). By the proper data separation I mean +avoiding unwanted redundancy in the database. + +Currently applied example doesn't care about the update password problem. +This is due to current abilities of MySQL and authdaemon (authmysql). +MySQL doesn't support subsequent SELECTs on UPDATE operation, and authmysql +doesn't supports batched queries at the moment. + +4.2.1 database structure + +table: domain_names + +purpose: associates domain_id with domain name + +fields: domain_name - fully qualified domain name + domain_id - domain identifier + + +----------------+-----------+ + | domain_name | domain_id | + +----------------+-----------+ + | exampledom.com | 1 | + | pld.org.pl | 2 | + | pld.net.pl | 2 | + | foobare.net.uk | 3 | + +----------------+-----------+ + +Note, that for pld.org.pl and pld.net.pl the domain identifiers are the same. +We can create a domain aliases in such a way. :) + +table: domain_auth + +purpose: associates domain_id with authentication credentials + which are common for all users in the virtual domain + +fields: domain_id - domain identifier + path_prefix - absolute pathname which points to + a directory where domain's mailboxes + are located + quota - default quota for each mailbox + uid - UID used to work on mailboxes + gid - GID used to work on mailboxes + + +------------+---------------+--------+-------+-------+ + | domain_id | path_prefix | quota | uid | gid | + +------------+---------------+--------+-------+-------+ + | 1 | /var/mail/ex | 100000 | 15000 | 15000 | + | 2 | /var/mail/pld | 555500 | 15001 | 15000 | + | 3 | /home/f0/mail | 8000 | 15002 | 15000 | + +------------+---------------+--------+-------+-------+ + +table: domain_info + +purpose: associates domain_id with additional informations + +fields: domain_id - domain identifier + registrant_id - registrant identifier + nic_handle - NIC handle + owner_id - domain's owner identifier + expires - domain's expiration date + + +------------+---------------+------------+----------+---------+ + | domain_id | registrant_id | nic_handle | owner_id | expires | + +------------+---------------+------------+----------+---------+ + + (we don't need to say anything more about this table indeed) + +table: users + +purpose: associates users' identifiers with domains' identifiers + and infers the credentials for various virtual mailboxes + +fields: username - user's login name + domain_id - domain identifier + cryptpw - crypted password + plainpw - plaintext password + quota - user's mailbox quota + (will override quota value set for + the whole virtual domain) + path - relative pathname for mailbox + (will be appended to the path_prefix + from domain_auth table to specify + user's mailbox location) + + +------------+-----------+----------+-----------+-------+------------+ + | username | domain_id | cryptpw | plainpw | quota | path | + +------------+-----------+----------+-----------+-------+------------+ + | foobar | 1 | $1$hlIeE | dupa.8 | NULL | f/o/foobar | + | breeder | 2 | $1$TWsdf | ziarno128 | 77777 | brd | + +------------+-----------+----------+-----------+-------+------------+ + + (you can add a realname column here, it doesn't fit to my terminal window:) + +--------------------- cut here + +# Create the database called vmail. + +CREATE database vmail; + +# Create an example MySQL user, which can read, write and delete data from +# vmail database. Username: vuser Password: secret_password + +GRANT SELECT,INSERT,UPDATE,DELETE ON vmail.* + TO vuser@localhost + IDENTIFIED BY 'secret_password'; + +FLUSH PRIVILEGES; + +# Create the tables. + +use vmail; + +CREATE TABLE domain_names ( + domain_id int(10) unsigned NOT NULL, + domain_name char(255) DEFAULT '' NOT NULL, + KEY domain_name (domain_name(255)) + ); + +CREATE TABLE domain_auth ( + domain_id int(10) unsigned DEFAULT 1 NOT NULL, + uid int(10) unsigned DEFAULT '15000' NOT NULL, + gid int(10) unsigned DEFAULT '15000' NOT NULL, + path_prefix char(255) DEFAULT '' NOT NULL, + quota char(255) DEFAULT '20000000' NOT NULL, + KEY domain_id (domain_id) + ); + +CREATE TABLE users ( + username char(128) DEFAULT '' NOT NULL, + domain_id int(10) unsigned DEFAULT 1 NOT NULL, + cryptpw char(128) DEFAULT '' NOT NULL, + plainpw char(128) DEFAULT '' NOT NULL, + name char(128) DEFAULT '' NOT NULL, + quota char(255), + path char(255) DEFAULT '' NOT NULL, + KEY username (username(128)) + ); + +# Create an example virtual domain entry +# id : 1 +# name : exampledom.com +# uid : 15000 +# gid : 15000 +# path : /var/mail/example +# quota : 20 Megs per mailbox + +INSERT INTO domain_names VALUES (1, 'exampledom.com'); +INSERT INTO domain_auth VALUES (1, '15000', '15000', '/var/mail/example', + '20000000'); + +# Create an example virtual user entry +# username : siefca +# domain id : 1 (points to exampledom.com) +# cryptpw : $1$wIfVZ8uK$qhagYAcIoZpQM83Et7c1e/ +# clearpw : dupa.8 +# name : Pawel Wilk +# quota : NULL (we want it to be fetched from domain_auth table) +# mailbox path : s/i/siefca + +INSERT INTO users VALUES ('siefca', 1, '$1$wIfVZ8uK$qhagYAcIoZpQM83Et7c1e/', + 'dupa.8', 'Pawel Wilk', NULL, 's/i/siefca'); + +--------------------- cut here + +Ok, we've done what we need. Don't forget to create system user with UID and +GID set to 15000, and a directory containing mailboxes (in this case: +/var/mail/example) owned by system user I've mentioned above. +There is also necessary to create Maildir folder structure for our user +inside the virtual domain directory - you can configure your MTA agent to do +such thing when first message arrive or use maildirmake tool, which comes +with Courier-IMAP. + + +4.2.2 authdaemon configuration + +DEFAULT_DOMAIN exampledom.com + +MYSQL_SELECT_CLAUSE SELECT \ + users.username, \ + users.cryptpw, \ + users.plainpw, \ + domain_auth.uid, \ + domain_auth.gid, \ + CONCAT_WS('/',domain_auth.path_prefix,users.path), \ + '', \ + IFNULL(users.quota, domain_auth.quota), \ + users.name \ + FROM users, domain_names, domain_auth \ + WHERE domain_names.domain_name='$(domain)' \ + AND users.username='$(local_part)' \ + AND domain_names.domain_id=users.domain_id \ + AND domain_names.domain_id=domain_auth.domain_id + + +. +. +. +. +. +. + +/////////////////////////// PART II - Developer Notes ///////////////////////// *----------------------- 1 Modifications overview @@ -85,7 +768,7 @@ Each modified set of instructions is marked by my e-mail address: siefca@pld.org.pl -Changes in the current source code are related to: +Changes in the source code are related to: - sections where the queries are constructed (including memory allocation for the buffers) @@ -102,6 +785,10 @@ newline as the second is replaced by two whitespaces while putting into the buffer + i've also added USER_DOMAIN_CONCAT and USER_DOMAIN_SEPARATORS + configuration options - they're used by get_localpart(), get_domain() + and get_username() functions, which are described below + - sections where the query is constructed selection is made, depending on configuration variables which @@ -130,7 +817,16 @@ MAX_SUBSTITUTION_LEN defines maximal length of a substitution variable's identifier (name). -The last two definitions are just for code simplification. +The last two definitions (SV_BEGIN_LEN and SV_END_LEN) are just for code +simplification. + +#define DEF_CONCAT_STRING "@" +#define DEF_SEPARATORS_SET "@%" + +The first (DEF_CONCAT_STRING) is used to set the defaults for a +concatenation string, used when parsing $(username) substitution variable. +The second (DEF_SEPARATORS_SET) is the set of characters, which are treated as +separators when splitting local part from the domain. @@ -179,7 +875,7 @@ In this example we've declared that $(some) in the query should be replaced by 'replacement' text, and replacement for $(anotha) will be defined in the code before passing on the array pointer to -the paring function. +the general parsing function. 3.2 typedef size_t (*parsefunc) @@ -414,14 +1110,17 @@ SYNOPSIS - static const char *get_localpart (const char *username); + static const char *get_localpart (const char *username, + const char *separators); DESCRIPTION This function detaches local part of an e-mail address from string pointed with username and puts it to the buffer of the fixed length. All necessary cleaning is - made on the result string. + made on the result string. String pointed with separators + refers to a set of characters, which are treated as + separation signs between local part and a domain. RETURN VALUE @@ -438,16 +1137,22 @@ SYNOPSIS static const char *get_domain (const char *username, - const char *defdomain); + const char *defdomain, + const char *separators); DESCRIPTION This function detaches domain part of an e-mail address from string pointed with username and puts it to the buffer of the fixed length. All necessary cleaning is - made on the result string. If function cannot find domain - part in the string the string pointed by defdomain is - used instead. + made on the result string. If the function cannot find a domain + part in the string then the string pointed to by defdomain is + used instead. If this function cannot find a domain part + as well as it cannot obtain the default domain (it's empty string + or the defdomain pointer is NULL) the returned result string is an + empty string. The string pointed with separators refers to a set + of characters, which are treated as separation signs between local + part and a domain. RETURN VALUE @@ -455,7 +1160,36 @@ NULL if there was some error. -4.9 parse_select_clause +4.9 get_username + +NAME + + get_username + +SYNOPSIS + + static const char *get_username (const char *username, + const char *domainname, + const char *concat_str); + +DESCRIPTION + + This function concatenates the localpart with a domain name + using the string pointed with concat_str. If the domain is + empty or NULL the result comes without binding string. + +RETURN VALUE + + Pointer to the static buffer containing output string or + NULL if there was some error. + +WARNINGS + + This function does not any string cleaning, nor default domain + checking. It is designed to work on results of get_localpart() and + get_domain(). + +4.10 parse_select_clause NAME @@ -465,7 +1199,9 @@ static char *parse_select_clause (const char *clause, const char *username, - const char *defdomain); + const char *defdomain + const char *concat_str, + const char *separators_set); DESCRIPTION @@ -473,15 +1209,17 @@ function. It parses a query pointed by caluse. username and defdomain strings are used to replace corresponding substitution strings if present in the query: $(local_part) - and $(domain). + and $(domain). The separators_set is passed to get_username() + and get_domain() invocations, and the concat_str is passed + to get_username() function, which is responsible for replacing + $(username) substitution variable. - RETURN VALUE Same as parse_string(). -4.10 parse_chpass_clause +4.11 parse_chpass_clause NAME @@ -492,6 +1230,8 @@ static char *parse_chpass_clause (const char *clause, const char *username, const char *defdomain, + const char *separators_set, + const char *concat_str, const char *newpass, const char *newpass_crypt); @@ -502,12 +1242,47 @@ defdomain, newpass and newpass_crypt strings are used to replace corresponding substitution strings if present in the query: $(local_part), $(domain), $(newpass), - $(newpass_crypt). + $(newpass_crypt). The separators_set and the concat_str + are passed to get_localpart(), get_domain(), and get_username() + functions as described in the entry for parse_select_clause(). RETURN VALUE Same as parse_string(). +4.12 auth_mysql_on_trigger + +NAME + + auth_mysql_on_trigger + +SYNOPSIS + + int auth_mysql_on_trigger (const char *clause_name, + const char *username); + +DESCRIPTION + + This function is responsible for calling out the MySQL queries in + depend which authentication state was reached. + + The clause_name should contain the name of a clause, which can be found + in the configuration file, and the username is simply the string used + as username (including the domain if entered). + + This function reads DEFAULT_DOMAIN, USER_DOMAIN_CONCAT and + USER_DOMAIN_SEPARATORS from the configuration file using read_env(), + then it uses parse_select_clause() to parse the query obtained using + read_env(clause_name), and then it calls querying subroutines to + perform the action. + +RETURN VALUE + + This function returns 1 on success and 0 on failure. The query results + are simply discarded. If a trigger's clause is not defined in the + configuration file the 1 is returned and function silently ends its + work. + @@ -520,11 +1295,9 @@ strings after split (problem?) - allow admin to set a group name instead of numerical group id - allow admin to set a username instead of numerical user id - -- add clauses: - - - MYSQL_PRESELECT_CLAUSE (query which comes before MYSQL_SELECT_CLAUSE) - - MYSQL_POSTSELECT_CLAUSE (query which comes after MYSQL_SELECT_CLAUSE) +- allow batched queries and register variables for keeping results +- put the parsing routines into separate files to make possible of sharing it + by more authentication modules @@ -534,10 +1307,12 @@ 6 Thanks *------------------------ -At the beginning this patch was messy indeed. :> I would like to thank +At the beginning the patch was messy indeed. :> I would like to thank Sam Varshavchik for pointing me a lot how to make it more fast and solid. I would also thank Philip Hazel, Chris Lightfoot and Mike Bremford which -by their software capabilities inspired me to write it. +by their software capabilities inspired me to write it. Oliver Oblasnik +remainded me to make the documentation more friendly for those who are +not programmers and just want to use it. :> --------------------------------------------------------------------------- diff -ur courier-imap-1.4.2.orig/authlib/authmysql.c courier-imap-1.4.2/authlib/authmysql.c --- courier-imap-1.4.2.orig/authlib/authmysql.c Sun Jun 24 01:42:05 2001 +++ courier-imap-1.4.2/authlib/authmysql.c Sun Feb 10 04:54:37 2002 @@ -31,7 +31,11 @@ if ((user=strtok(authdata, "\n")) == 0 || (pass=strtok(0, "\n")) == 0) { - errno=EPERM; + if (!auth_mysql_on_trigger("MYSQL_ONFAIL_CLAUSE", user)) + errno=EACCES; + else + errno=EPERM; + return (0); } @@ -50,7 +54,11 @@ { if (authcheckpassword(pass,authinfo->cryptpw)) { - errno=EPERM; + if (!auth_mysql_on_trigger("MYSQL_ONFAIL_CLAUSE", user)) + errno=EACCES; + else + errno=EPERM; + return (0); /* User/Password not found. */ } } @@ -58,13 +66,21 @@ { if (strcmp(pass, authinfo->clearpw)) { - errno=EPERM; + if (!auth_mysql_on_trigger("MYSQL_ONFAIL_CLAUSE", user)) + errno=EACCES; + else + errno=EPERM; + return (0); } } else { - errno=EPERM; + if (!auth_mysql_on_trigger("MYSQL_ONFAIL_CLAUSE", user)) + errno=EACCES; + else + errno=EPERM; + return (0); /* Username not found */ } @@ -132,6 +148,12 @@ (*callback_func)(&aa, callback_arg); } + if (!auth_mysql_on_trigger("MYSQL_ONSUCCESS_CLAUSE", user)) + { + errno=EACCES; + return (0); + } + return (strdup(authinfo->username)); } @@ -153,7 +175,11 @@ { if (authcheckpassword(pass,authinfo->cryptpw)) { - errno=EPERM; + if (!auth_mysql_on_trigger("MYSQL_ONFAIL_CLAUSE", user)) + errno=EACCES; + else + errno=EPERM; + return (-1); /* User/Password not found. */ } } @@ -161,13 +187,21 @@ { if (strcmp(pass, authinfo->clearpw)) { - errno=EPERM; + if (!auth_mysql_on_trigger("MYSQL_ONFAIL_CLAUSE", user)) + errno=EACCES; + else + errno=EPERM; + return (-1); } } else { - errno=EPERM; + if (!auth_mysql_on_trigger("MYSQL_ONFAIL_CLAUSE", user)) + errno=EACCES; + else + errno=EPERM; + return (-1); } @@ -176,6 +210,13 @@ errno=EPERM; return (-1); } + + if (!auth_mysql_on_trigger("MYSQL_ONSUCCESS_CLAUSE", user)) + { + errno=EACCES; + return (-1); + } + return (0); } diff -ur courier-imap-1.4.2.orig/authlib/authmysql.h courier-imap-1.4.2/authlib/authmysql.h --- courier-imap-1.4.2.orig/authlib/authmysql.h Mon Aug 6 05:12:39 2001 +++ courier-imap-1.4.2/authlib/authmysql.h Sun Feb 10 04:56:30 2002 @@ -21,6 +21,7 @@ } ; extern struct authmysqluserinfo *auth_mysql_getuserinfo(const char *); +extern int auth_mysql_on_trigger (const char *clause_name, const char *username); extern void auth_mysql_cleanup(); extern int auth_mysql_setpass(const char *, const char *); diff -ur courier-imap-1.4.2.orig/authlib/authmysqllib.c courier-imap-1.4.2/authlib/authmysqllib.c --- courier-imap-1.4.2.orig/authlib/authmysqllib.c Thu Jan 10 05:44:06 2002 +++ courier-imap-1.4.2/authlib/authmysqllib.c Sun Feb 10 14:35:10 2002 @@ -24,6 +24,9 @@ #define SV_BEGIN_LEN ((sizeof(SV_BEGIN_MARK))-1) #define SV_END_LEN ((sizeof(SV_END_MARK))-1) +#define DEF_CONCAT_STRING "@" +#define DEF_SEPARATORS_SET "@%" + static const char rcsid[]="$Id$"; /* siefca@pld.org.pl */ @@ -426,21 +429,43 @@ return NULL; } *pass_buf = '\0'; - + return output_buf; } /* siefca@pld.org.pl */ -static const char *get_localpart (const char *username) +static const char *get_username (const char *username, const char *domainname, + const char *concat_str) +{ +static char username_buf[400]; + + if (!username || !domainname || !concat_str || + *username == '\0' || *concat_str == '\0') return NULL; + if (( strlen(username) + + strlen(concat_str) + + strlen(domainname)) > 397) return NULL; + + if (*domainname == '\0') + strcpy (username_buf, username); + else + sprintf (username_buf, "%s%s%s", username, concat_str, + domainname); + + return (username_buf); +} + +/* siefca@pld.org.pl */ +static const char *get_localpart (const char *username, const char *separators) { size_t lbuf = 0; const char *l_end, *p; char *q; static char localpart_buf[130]; - if (!username || *username == '\0') return NULL; + if (!username || *username == '\0' || + !separators || *separators == '\0') return NULL; - p = strchr(username,'@'); + p = strpbrk (username, separators); if (p) { if ((p-username) > 128) @@ -469,21 +494,27 @@ } /* siefca@pld.org.pl */ -static const char *get_domain (const char *username, const char *defdomain) +static const char *get_domain (const char *username, const char *defdomain, + const char *separators) { static char domain_buf[260]; const char *p; char *q; - if (!username || *username == '\0') return NULL; - p = strchr(username,'@'); + if (!username || *username == '\0' || + !separators || *separators == '\0') return NULL; + + p = strpbrk (username, separators); if (!p || *(p+1) == '\0') { - if (defdomain && *defdomain) + if (defdomain && *defdomain != '\0') return defdomain; else - return NULL; + { + *domain_buf = '\0'; + return domain_buf; + } } p++; @@ -528,20 +559,27 @@ /* siefca@pld.org.pl */ static char *parse_select_clause (const char *clause, const char *username, - const char *defdomain) + const char *defdomain, + const char *concat_str, + const char *separators_set) { static struct var_data vd[]={ {"local_part", NULL, sizeof("local_part"), 0}, {"domain", NULL, sizeof("domain"), 0}, + {"username", NULL, sizeof("username"), 0}, {NULL, NULL, 0, 0}}; if (clause == NULL || *clause == '\0' || - !username || *username == '\0') + !username || *username == '\0' || + !concat_str || *concat_str == '\0' || + !separators_set || *separators_set == '\0') return NULL; - vd[0].value = get_localpart (username); - vd[1].value = get_domain (username, defdomain); - if (!vd[0].value || !vd[1].value) + vd[0].value = get_localpart (username, separators_set); + vd[1].value = get_domain (username, defdomain, separators_set); + vd[2].value = get_username (vd[0].value, vd[1].value, concat_str); + + if (!vd[0].value || !vd[1].value || !vd[2].value) return NULL; return (parse_string (clause, vd)); @@ -549,12 +587,16 @@ /* siefca@pld.org.pl */ static char *parse_chpass_clause (const char *clause, const char *username, - const char *defdomain, const char *newpass, + const char *defdomain, + const char *separators_set, + const char *concat_str, + const char *newpass, const char *newpass_crypt) { static struct var_data vd[]={ {"local_part", NULL, sizeof("local_part"), 0}, {"domain", NULL, sizeof("domain"), 0}, + {"username", NULL, sizeof("username"), 0}, {"newpass", NULL, sizeof("newpass"), 0}, {"newpass_crypt", NULL, sizeof("newpass_crypt"), 0}, {NULL, NULL, 0, 0}}; @@ -562,19 +604,83 @@ if (clause == NULL || *clause == '\0' || !username || *username == '\0' || !newpass || *newpass == '\0' || + !separators_set || *separators_set == '\0' || !newpass_crypt || *newpass_crypt == '\0') return NULL; - vd[0].value = get_localpart (username); - vd[1].value = get_domain (username, defdomain); - vd[2].value = validate_password (newpass); - vd[3].value = validate_password (newpass_crypt); + vd[0].value = get_localpart (username, separators_set); + vd[1].value = get_domain (username, defdomain, separators_set); + vd[3].value = get_username (vd[0].value, vd[1].value, concat_str); + vd[4].value = validate_password (newpass); + vd[5].value = validate_password (newpass_crypt); if (!vd[0].value || !vd[1].value || - !vd[2].value || !vd[3].value) return NULL; + !vd[2].value || !vd[3].value || + !vd[4].value || !vd[5].value) return NULL; return (parse_string (clause, vd)); } +/* siefca@pld.org.pl */ +int auth_mysql_on_trigger (const char *clause_name, const char *username) +{ +char *querybuf =NULL; +const char *concat_str =NULL, + *separators_set =NULL, + *defdomain =NULL, + *on_clause =NULL; +MYSQL_RES *result; + + if (!clause_name || *clause_name == '\0') return (0); + on_clause = read_env (clause_name); + if (!on_clause || *on_clause == '\0') return (1); + + defdomain = read_env ("DEFAULT_DOMAIN"); + concat_str = read_env ("USER_DOMAIN_CONCAT"); + separators_set = read_env ("USER_DOMAIN_SEPARATORS"); + if (!defdomain) defdomain = ""; + if (!concat_str || *concat_str == '\0') + concat_str = DEF_CONCAT_STRING; + if (!separators_set || *separators_set == '\0') + separators_set = DEF_SEPARATORS_SET; + + querybuf = parse_select_clause (on_clause, + username, + defdomain, + concat_str, + separators_set); + + if (!querybuf) return (0); + + if (mysql_query (mysql, querybuf)) + { + /* */ + + auth_mysql_cleanup(); + + if (do_connect()) + { + free(querybuf); + return (1); + } + + if (mysql_query (mysql, querybuf)) + { + free(querybuf); + auth_mysql_cleanup(); + /* Server went down, that's OK, + ** try again next time. + */ + return (1); + } + } + free(querybuf); + result = mysql_store_result(mysql); + if (result) mysql_free_result(result); + + return (1); +} + + struct authmysqluserinfo *auth_mysql_getuserinfo(const char *username) { const char *user_table =NULL; @@ -593,6 +699,8 @@ *gid_field =NULL, *quota_field =NULL, *where_clause =NULL, + *concat_str =NULL, + *separators_set =NULL, *select_clause =NULL; /* siefca@pld.org.pl */ static const char query[]= @@ -701,7 +809,19 @@ else { /* siefca@pld.org.pl */ - querybuf=parse_select_clause (select_clause, username, defdomain); + concat_str = read_env ("USER_DOMAIN_CONCAT"); + separators_set = read_env ("USER_DOMAIN_SEPARATORS"); + + if (!concat_str || *concat_str == '\0') + concat_str = DEF_CONCAT_STRING; + if (!separators_set || *separators_set == '\0') + separators_set = DEF_SEPARATORS_SET; + + querybuf = parse_select_clause (select_clause, + username, + defdomain, + concat_str, + separators_set); if (!querybuf) return 0; } @@ -785,6 +905,8 @@ *where_clause =NULL, *user_table =NULL, *login_field =NULL, + *concat_str =NULL, + *separators_set =NULL, *chpass_clause =NULL; /* siefca@pld.org.pl */ if (!mysql) @@ -834,14 +956,23 @@ } else { + concat_str = read_env ("USER_DOMAIN_CONCAT"); + separators_set = read_env ("USER_DOMAIN_SEPARATORS"); + + if (!concat_str || *concat_str == '\0') + concat_str = DEF_CONCAT_STRING; + if (!separators_set || *separators_set == '\0') + separators_set = DEF_SEPARATORS_SET; + sql_buf=parse_chpass_clause(chpass_clause, user, defdomain, + concat_str, + separators_set, pass, newpass_crypt); } - if (!sql_buf) { free(newpass_crypt); diff -ur courier-imap-1.4.2.orig/authlib/authmysqlrc courier-imap-1.4.2/authlib/authmysqlrc --- courier-imap-1.4.2.orig/authlib/authmysqlrc Tue Jan 8 06:20:46 2002 +++ courier-imap-1.4.2/authlib/authmysqlrc Thu Feb 14 00:19:43 2002 @@ -141,65 +141,91 @@ # # MYSQL_WHERE_CLAUSE server='mailhost.example.com' -##NAME: MYSQL_SELECT_CLAUSE:0 +##NAME: USER_DOMAIN_CONCAT:0 # -# (EXPERIMENTAL) -# This is optional, MYSQL_SELECT_CLAUSE can be set when you have a database, -# which is structuraly different from proposed. The fixed string will -# be used to do a SELECT operation on database, which should return fields -# in order specified bellow: -# -# username, cryptpw, uid, gid, clearpw, home, maildir, quota, fullname -# -# Enabling this option causes ignorance of any other field-related -# options, excluding default domain. -# -# There are two variables, which you can use. Substitution will be made -# for them, so you can put entered username (local part) and domain name -# in the right place of your query. These variables are: -# $(local_part) and $(domain) +# This is optional. Here you can set the string used to concatenate +# usename with domain part while expanding the $(username) substitution +# variable. If it's not set the '@' character is used. # -# If a $(domain) is empty (not given by the remote user) the default domain -# name is used in its place. +# USER_DOMAIN_CONCAT @ + +##NAME: USER_DOMAIN_SEPARATORS:0 # -# This example is a little bit modified adaptation of vmail-sql -# database scheme: +# This is optional. Using this option you can set the set of characters +# which are treated as separators when splitting entered username into the +# local part and the domain name. If it's not set the default set @% is used, +# do the user can log on using user@domain or user%domain. # -# MYSQL_SELECT_CLAUSE SELECT popbox.local_part, \ -# CONCAT('{MD5}', popbox.password_hash), \ -# domain.uid, \ -# domain.gid, \ -# popbox.clearpw, \ -# CONCAT(domain.path, '/', popbox.mbox_name), \ -# '', \ -# domain.quota, \ -# '', \ -# FROM popbox, domain \ -# WHERE popbox.local_part = '$(local_part)' \ -# AND popbox.domain_name = '$(domain)' \ -# AND popbox.domain_name = domain.domain_name +# USER_DOMAIN_SEPARATORS @%+ + +##NAME: MYSQL_SELECT_CLAUSE:0 +# +# This is optional, MYSQL_SELECT_CLAUSE can be set when you have a database, +# which is structuraly different from proposed. You can type here your MySQL +# query, which will be used to fetch user's credentials, and which should +# return fields in order specified bellow: +# +# username, cryptpw, clearpw, uid, gid, home, maildir, quota, fullname +# +# Enabling this option causes ignorance of any other field-related options. +# +# There also are variables, which you can use. Substitution will be made +# for them, so you can pass currently entered username and a domain name +# up to the right place within your query. These variables are: +# $(local_part) , $(domain) , $(username) # +# If a $(domain) is empty (not given by the remote user) the default domain +# name is used in its place. $(username) is a local part concatenated with +# domain name using symbol defined in USER_DOMAIN_CONCAT or '@' if this option +# is not set. +# +# MYSQL_SELECT_CLAUSE SELECT \ +# users.username, users.cryptpw, users.clearpw, \ +# domains.uid, domains.gid, \ +# CONCAT_WS('/',domains.path_prefix,users.mailbox_path), \ +# '', domains.quota, '' \ +# FROM users, domains \ +# WHERE domains.domain_name='$(domain)' \ +# AND users.username='$(local_part)' \ +# AND domains.domain_name=users.domain_name + ##NAME: MYSQL_CHPASS_CLAUSE:0 # -# (EXPERIMENTAL) # This is optional, MYSQL_CHPASS_CLAUSE can be set when you have a database, -# which is structuraly different from proposed. The fixed string will -# be used to do an UPDATE operation on database. In other words, it is -# used, when changing password. +# which is structuraly different from proposed. You can use it to set up +# a MySQL query used to change user's password. # # There are four variables, which you can use. Substitution will be made -# for them, so you can put entered username (local part) and domain name +# for them, so you can put the currently entered username and the domain name # in the right place of your query. There variables are: -# $(local_part) , $(domain) , $(newpass) , $(newpass_crypt) +# $(local_part) , $(domain) , $(username) , $(newpass) , $(newpass_crypt) # # If a $(domain) is empty (not given by the remote user) the default domain -# name is used in its place. -# $(newpass) contains plain password -# $(newpass_crypt) contains its crypted form -# -# MYSQL_CHPASS_CLAUSE UPDATE popbox \ -# SET clearpw='$(newpass)', \ -# password_hash='$(newpass_crypt)' \ -# WHERE local_part='$(local_part)' \ -# AND domain_name='$(domain)' +# name is used in its place. $(newpass) contains plain password and +# $(newpass_crypt) contains its crypted form. +# +# MYSQL_CHPASS_CLAUSE UPDATE users \ +# SET clearpw='$(newpass)', \ +# cryptpw='$(newpass_crypt)' \ +# WHERE username='$(local_part)' \ +# AND domain_name='$(domain)' + +##NAME: MYSQL_ONSUCCESS_CLAUSE:0 +# +# This is optional, MYSQL_ONSUCCESS_CLAUSE is a trigger - the query is performed +# each time user has successfuly logged in. [experimental] +# +# MYSQL_ONSUCCESS_CLAUSE UPDATE users \ +# SET last_ok=CURRENT_TIMESTAMP \ +# WHERE username='$(local_part)' \ +# AND domain_name='$(domain)' + +##NAME: MYSQL_ONFAIL_CLAUSE:0 +# +# This is optional, MYSQL_ONFAIL_CLAUSE is a trigger - the query is performed +# each time user has successfuly logged in. [experimental] # +# MYSQL_ONFAIL_CLAUSE UPDATE users \ +# SET last_fail=CURRENT_TIMESTAMP \ +# WHERE username='$(local_part)' \ +# AND domain_name='$(domain)'