document closed Debian bug
[davical.git] / inc / auth-functions.php
blobe9803bda7a49035078b20d669854bf047a2c7d39
1 <?php
2 /**
3 * The authentication handling plugins can be used by the Session class to
4 * provide authentication.
6 * Each authenticate hook needs to:
7 * - Accept a username / password
8 * - Confirm the username / password are correct
9 * - Create (or update) a 'usr' record in our database
10 * - Return the 'usr' record as an object
11 * - Return === false when authentication fails
13 * It can expect that:
14 * - Configuration data will be in $c->authenticate_hook['config'], which might be an array, or whatever is needed.
16 * In order to be called:
17 * - This file should be included
18 * - $c->authenticate_hook['call'] should be set to the name of the plugin
19 * - $c->authenticate_hook['config'] should be set up with any configuration data for the plugin
21 * @package davical
22 * @subpackage authentication
23 * @author Andrew McMillan <andrew@mcmillan.net.nz>
24 * @copyright Catalyst IT Ltd, Morphoss Ltd
25 * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
28 require_once("DataUpdate.php");
30 if ( !function_exists('auth_functions_deprecated') ) {
31 function auth_functions_deprecated( $method, $message = null ) {
32 global $c;
33 if ( isset($c->dbg['ALL']) || isset($c->dbg['deprecated']) ) {
34 $stack = debug_backtrace();
35 array_shift($stack);
36 if ( preg_match( '{/inc/auth-functions.php$}', $stack[0]['file'] ) && $stack[0]['line'] > __LINE__ ) return;
37 dbg_error_log("LOG", " auth-functions: Call to deprecated routine '%s'%s", $method, (isset($message)?': '.$message:'') );
38 foreach( $stack AS $k => $v ) {
39 dbg_error_log( 'LOG', ' auth-functions: Deprecated call from line %4d of %s', $v['line'], $v['file']);
46 function getUserByName( $username, $use_cache=true ) {
47 auth_functions_deprecated('getUserByName','replaced by Principal class');
48 return new Principal('username', $username, $use_cache);
51 function getUserByEMail( $email, $use_cache = true ) {
52 auth_functions_deprecated('getUserByEMail','replaced by Principal class');
53 return new Principal('email', $email, $use_cache);
56 function getUserByID( $user_no, $use_cache = true ) {
57 auth_functions_deprecated('getUserByID','replaced by Principal class');
58 return new Principal('user_no', $user_no, $use_cache);
61 function getPrincipalByID( $principal_id, $use_cache = true ) {
62 auth_functions_deprecated('getPrincipalByID','replaced by Principal class');
63 return new Principal('principal_id', $principal_id, $use_cache);
67 /**
68 * Creates some default home collections for the user.
69 * @param string $username The username of the user we are creating relationships for.
71 function CreateHomeCollections( $username, $defult_timezone = null ) {
72 global $session, $c;
74 if ( !isset($c->default_collections) )
76 $c->default_collections = array();
78 if( !empty($c->home_calendar_name) )
79 $c->default_collections[] = array( 'type' => 'calendar', 'name' => $c->home_calendar_name );
80 if( !empty($c->home_addressbook_name) )
81 $c->default_collections[] = array( 'type' => 'addressbook', 'name' => $c->home_addressbook_name );
84 if ( !is_array($c->default_collections) || !count($c->default_collections) ) return true;
86 $principal = new Principal('username',$username);
88 $user_fullname = $principal->fullname; // user fullname
89 $user_rfullname = implode(' ', array_reverse(explode(' ', $principal->fullname))); // user fullname in reverse order
91 $sql = 'INSERT INTO collection (user_no, parent_container, dav_name, dav_etag, dav_displayname, is_calendar, is_addressbook, default_privileges, created, modified, resourcetypes) ';
92 $sql .= 'VALUES( :user_no, :parent_container, :collection_path, :dav_etag, :displayname, :is_calendar, :is_addressbook, :privileges::BIT(24), current_timestamp, current_timestamp, :resourcetypes );';
94 foreach( $c->default_collections as $v ) {
95 if ( $v['type'] == 'calendar' || $v['type']=='addressbook' ) {
96 if ( !empty($v['name']) ) {
97 $qry = new AwlQuery( 'SELECT 1 FROM collection WHERE dav_name = :dav_name', array( ':dav_name' => $principal->dav_name().$v['name'].'/') );
98 if ( !$qry->Exec() ) {
99 $c->messages[] = i18n('There was an error reading from the database.');
100 return false;
102 if ( $qry->rows() > 0 ) {
103 $c->messages[] = i18n('Home '.( $v['type']=='calendar' ? 'calendar' : 'addressbook' ).' already exists.');
104 return true;
106 else {
107 $params[':user_no'] = $principal->user_no();
108 $params[':parent_container'] = $principal->dav_name();
109 $params[':dav_etag'] = '-1';
110 $params[':collection_path'] = $principal->dav_name().$v['name'].'/';
111 $params[':displayname'] = ( !isset($v['displayname']) || empty($v['displayname']) ? $user_fullname.( $v['type']=='calendar' ? ' calendar' : ' addressbook' ) : str_replace(array('%fn', '%rfn'), array($user_fullname, $user_rfullname), $v['displayname']) );
112 $params[':resourcetypes'] = ( $v['type']=='calendar' ? '<DAV::collection/><urn:ietf:params:xml:ns:caldav:calendar/>' : '<DAV::collection/><urn:ietf:params:xml:ns:carddav:addressbook/>' );
113 $params[':is_calendar'] = ( $v['type']=='calendar' ? true : false );
114 $params[':is_addressbook'] = ( $v['type']=='addressbook' ? true : false );
115 $params[':privileges'] = ( !isset($v['privileges']) || $v['privileges']===null ? null : privilege_to_bits($v['privileges']) );
117 $qry = new AwlQuery( $sql, $params );
118 if ( $qry->Exec() ) {
119 $c->messages[] = i18n('Home '.( $v['type']=='calendar' ? 'calendar' : 'addressbook' ).' added.');
120 dbg_error_log("User",":Write: Created user's home ".( $v['type']=='calendar' ? 'calendar' : 'addressbook' )." at '%s'", $params[':collection_path'] );
122 else {
123 $c->messages[] = i18n("There was an error writing to the database.");
124 return false;
130 return true;
134 * Backward compatibility
135 * @param unknown_type $username
137 function CreateHomeCalendar($username) {
138 auth_functions_deprecated('CreateHomeCalendar','renamed to CreateHomeCollections');
139 return CreateHomeCollections($username);
143 * Defunct function for creating default relationships.
144 * @param string $username The username of the user we are creating relationships for.
146 function CreateDefaultRelationships( $username ) {
147 global $c;
148 if(! isset($c->default_relationships) || count($c->default_relationships) == 0) return true;
150 $changes = false;
151 foreach($c->default_relationships as $group => $relationships)
153 $sql = 'INSERT INTO grants (by_principal, to_principal, privileges) VALUES(:by_principal, :to_principal, :privileges::INT::BIT(24))';
154 $params = array(
155 ':by_principal' => getUserByName($username)->principal_id,
156 ':to_principal' => $group,
157 ':privileges' => privilege_to_bits($relationships)
159 $qry = new AwlQuery($sql, $params);
161 if ( $qry->Exec() ) {
162 $changes = true;
163 dbg_error_log("User",":Write: Created user's default relationship by:'%s', to:'%s', privileges:'%s'",$params[':by_principal'],$params[':to_principal'],$params[':privileges']);
165 else {
166 $c->messages[] = i18n("There was an error writing to the database.");
167 return false;
171 if($changes)
172 $c->messages[] = i18n("Default relationships added.");
174 return true;
178 function UpdateCollectionTimezones( $username, $new_timezone=null ) {
179 if ( empty($new_timezone) ) return;
180 $qry = new AwlQuery('UPDATE collection SET timezone=? WHERE dav_name LIKE ? AND is_calendar', '/'.$username.'/%', $new_timezone);
181 $qry->Exec();
185 * Update the local cache of the remote user details
186 * @param object $usr The user details we read from the remote.
188 function UpdateUserFromExternal( &$usr ) {
189 global $c;
191 auth_functions_deprecated('UpdateUserFromExternal','refactor to use the "Principal" class');
193 * When we're doing the create we will usually need to generate a user number
195 if ( !isset($usr->user_no) || intval($usr->user_no) == 0 ) {
196 $qry = new AwlQuery( "SELECT nextval('usr_user_no_seq');" );
197 $qry->Exec('Login',__LINE__,__FILE__);
198 $sequence_value = $qry->Fetch(true); // Fetch as an array
199 $usr->user_no = $sequence_value[0];
202 $qry = new AwlQuery('SELECT * FROM usr WHERE user_no = :user_no', array(':user_no' => $usr->user_no) );
203 if ( $qry->Exec('Login',__LINE__,__FILE__) && $qry->rows() == 1 ) {
204 $type = "UPDATE";
205 if ( $old = $qry->Fetch() ) {
206 $changes = false;
207 foreach( $usr AS $k => $v ) {
208 if ( $old->{$k} != $v ) {
209 $changes = true;
210 dbg_error_log("Login","User '%s' field '%s' changed from '%s' to '%s'", $usr->username, $k, $old->{$k}, $v );
211 break;
214 if ( !$changes ) {
215 dbg_error_log("Login","No changes to user record for '%s' - leaving as-is.", $usr->username );
216 if ( isset($usr->active) && $usr->active == 'f' ) return false;
217 return; // Normal case, if there are no changes
219 else {
220 dbg_error_log("Login","Changes to user record for '%s' - updating.", $usr->username );
224 else
225 $type = "INSERT";
227 $params = array();
228 if ( $type != 'INSERT' ) $params[':user_no'] = $usr->user_no;
229 $qry = new AwlQuery( sql_from_object( $usr, $type, 'usr', 'WHERE user_no= :user_no' ), $params );
230 $qry->Exec('Login',__LINE__,__FILE__);
233 * We disallow login by inactive users _after_ we have updated the local copy
235 if ( isset($usr->active) && ($usr->active === 'f' || $usr->active === false) ) return false;
237 if ( $type == 'INSERT' ) {
238 $qry = new AwlQuery( 'INSERT INTO principal( type_id, user_no, displayname, default_privileges) SELECT 1, user_no, fullname, :privs::INT::BIT(24) FROM usr WHERE username=(text(:username))',
239 array( ':privs' => privilege_to_bits($c->default_privileges), ':username' => $usr->username) );
240 $qry->Exec('Login',__LINE__,__FILE__);
241 CreateHomeCalendar($usr->username);
242 CreateDefaultRelationships($usr->username);
244 else if ( $usr->fullname != $old->{'fullname'} ) {
245 // Also update the displayname if the fullname has been updated.
246 $qry->QDo( 'UPDATE principal SET displayname=:new_display WHERE user_no=:user_no',
247 array(':new_display' => $usr->fullname, ':user_no' => $usr->user_no)
254 * Authenticate against a different PostgreSQL database which contains a usr table in
255 * the AWL format.
257 * Use this as in the following example config snippet:
259 * require_once('auth-functions.php');
260 * $c->authenticate_hook = array(
261 * 'call' => 'AuthExternalAwl',
262 * 'config' => array(
263 * // A PgSQL database connection string for the database containing user records
264 * 'connection[]' => 'dbname=wrms host=otherhost port=5433 user=general',
265 * // Which columns should be fetched from the database
266 * 'columns' => "user_no, active, email_ok, joined, last_update AS updated, last_used, username, password, fullname, email",
267 * // a WHERE clause to limit the records returned.
268 * 'where' => "active AND org_code=7"
270 * );
273 function AuthExternalAWL( $username, $password ) {
274 global $c;
276 $persistent = isset($c->authenticate_hook['config']['use_persistent']) && $c->authenticate_hook['config']['use_persistent'];
278 if ( isset($c->authenticate_hook['config']['columns']) )
279 $cols = $c->authenticate_hook['config']['columns'];
280 else
281 $cols = '*';
283 if ( isset($c->authenticate_hook['config']['where']) )
284 $andwhere = ' AND '.$c->authenticate_hook['config']['where'];
285 else
286 $andwhere = '';
288 $qry = new AwlQuery('SELECT '.$cols.' FROM usr WHERE lower(username) = :username '. $andwhere, array( ':username' => strtolower($username) ));
289 $authconn = $qry->SetConnection($c->authenticate_hook['config']['connection'], ($persistent ? array(PDO::ATTR_PERSISTENT => true) : null));
290 if ( ! $authconn ) {
291 echo <<<EOERRMSG
292 <html><head><title>Database Connection Failure</title></head><body>
293 <h1>Database Error</h1>
294 <h3>Could not connect to PostgreSQL database</h3>
295 </body>
296 </html>
297 EOERRMSG;
298 @ob_flush(); exit(1);
301 if ( $qry->Exec('Login',__LINE__,__FILE__) && $qry->rows() == 1 ) {
302 $usr = $qry->Fetch();
303 if ( session_validate_password( $password, $usr->password ) ) {
304 $principal = new Principal('username',$username);
305 if ( $principal->Exists() ) {
306 if ( $principal->modified <= $usr->updated )
307 $principal->Update($usr);
309 else {
310 $principal->Create($usr);
311 CreateHomeCollections($username);
315 * We disallow login by inactive users _after_ we have updated the local copy
317 if ( isset($usr->active) && $usr->active == 'f' ) return false;
319 return $principal;
323 return false;