1: <?php
2: /**
3: * The Horde_Auth_Cyrsql class provides a SQL implementation of the Horde
4: * authentication system for the Cyrus IMAP server. Most of the functionality
5: * is the same as for the SQL class; only what is different overrides the
6: * parent class implementations.
7: *
8: * The table structure for the auth system is as follows:
9: * <pre>
10: * CREATE TABLE accountuser (
11: * username VARCHAR(255) BINARY NOT NULL DEFAULT '',
12: * password VARCHAR(32) BINARY NOT NULL DEFAULT '',
13: * prefix VARCHAR(50) NOT NULL DEFAULT '',
14: * domain_name VARCHAR(255) NOT NULL DEFAULT '',
15: * UNIQUE KEY username (username)
16: * );
17: *
18: * CREATE TABLE adminuser (
19: * username VARCHAR(50) BINARY NOT NULL DEFAULT '',
20: * password VARCHAR(50) BINARY NOT NULL DEFAULT '',
21: * type INT(11) NOT NULL DEFAULT '0',
22: * SID VARCHAR(255) NOT NULL DEFAULT '',
23: * home VARCHAR(255) NOT NULL DEFAULT '',
24: * PRIMARY KEY (username)
25: * );
26: *
27: * CREATE TABLE alias (
28: * alias VARCHAR(255) NOT NULL DEFAULT '',
29: * dest LONGTEXT,
30: * username VARCHAR(50) NOT NULL DEFAULT '',
31: * status INT(11) NOT NULL DEFAULT '1',
32: * PRIMARY KEY (alias)
33: * );
34: *
35: * CREATE TABLE domain (
36: * domain_name VARCHAR(255) NOT NULL DEFAULT '',
37: * prefix VARCHAR(50) NOT NULL DEFAULT '',
38: * maxaccounts INT(11) NOT NULL DEFAULT '20',
39: * quota INT(10) NOT NULL DEFAULT '20000',
40: * transport VARCHAR(255) NOT NULL DEFAULT 'cyrus',
41: * freenames ENUM('YES','NO') NOT NULL DEFAULT 'NO',
42: * freeaddress ENUM('YES','NO') NOT NULL DEFAULT 'NO',
43: * PRIMARY KEY (domain_name),
44: * UNIQUE KEY prefix (prefix)
45: * );
46: *
47: * CREATE TABLE domainadmin (
48: * domain_name VARCHAR(255) NOT NULL DEFAULT '',
49: * adminuser VARCHAR(255) NOT NULL DEFAULT ''
50: * );
51: *
52: * CREATE TABLE search (
53: * search_id VARCHAR(255) NOT NULL DEFAULT '',
54: * search_sql TEXT NOT NULL,
55: * perpage INT(11) NOT NULL DEFAULT '0',
56: * timestamp TIMESTAMP(14) NOT NULL,
57: * PRIMARY KEY (search_id),
58: * KEY search_id (search_id)
59: * );
60: *
61: * CREATE TABLE virtual (
62: * alias VARCHAR(255) NOT NULL DEFAULT '',
63: * dest LONGTEXT,
64: * username VARCHAR(50) NOT NULL DEFAULT '',
65: * status INT(11) NOT NULL DEFAULT '1',
66: * KEY alias (alias)
67: * );
68: *
69: * CREATE TABLE log (
70: * id INT(11) NOT NULL AUTO_INCREMENT,
71: * msg TEXT NOT NULL,
72: * user VARCHAR(255) NOT NULL DEFAULT '',
73: * host VARCHAR(255) NOT NULL DEFAULT '',
74: * time DATETIME NOT NULL DEFAULT '2000-00-00 00:00:00',
75: * pid VARCHAR(255) NOT NULL DEFAULT '',
76: * PRIMARY KEY (id)
77: * );
78: * </pre>
79: *
80: * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
81: *
82: * See the enclosed file COPYING for license information (LGPL). If you did
83: * not receive this file, http://www.horde.org/licenses/lgpl21
84: *
85: * @author Ilya Krel <mail@krel.org>
86: * @author Jan Schneider <jan@horde.org>
87: * @category Horde
88: * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
89: * @package Auth
90: */
91: class Horde_Auth_Cyrsql extends Horde_Auth_Sql
92: {
93: /**
94: * An array of capabilities, so that the driver can report which
95: * operations it supports and which it doesn't.
96: *
97: * @var array
98: */
99: protected $_capabilities = array(
100: 'add' => true,
101: 'list' => true,
102: 'remove' => true,
103: 'resetpassword' => false,
104: 'update' => true,
105: 'authenticate' => true,
106: );
107:
108: /**
109: * Horde_Imap_Client object.
110: *
111: * @var Horde_Imap_Client_Base
112: */
113: protected $_imap;
114:
115: /**
116: * Constructor.
117: *
118: * @param array $params Parameters:
119: * <pre>
120: * 'charset' - (string) Default charset.
121: * DEFAULT: NONE
122: * 'domain_field' - (string) If set to anything other than 'none' this is
123: * used as field name where domain is stored.
124: * DEFAULT: 'domain_name'
125: * 'folders' - (array) An array of folders to create under username.
126: * DEFAULT: NONE
127: * 'hidden_accounts' - (array) An array of system accounts to hide from
128: * the user interface.
129: * DEFAULT: None.
130: * 'imap' - (Horde_Imap_Client_Base) [REQUIRED] An IMAP client object.
131: * 'quota' - (integer) The quota (in kilobytes) to grant on the mailbox.
132: * DEFAULT: NONE
133: * 'userhierarchy' - (string) The user hierarchy prefix.
134: * DEFAULT: 'user.'
135: * </pre>
136: *
137: * @throws InvalidArgumentException
138: */
139: public function __construct(array $params = array())
140: {
141: if (!isset($params['imap']) ||
142: !($params['imap'] instanceof Horde_Imap_Client_Base)) {
143: throw new InvalidArgumentException('Missing imap parameter.');
144: }
145: $this->_imap = $params['imap'];
146: unset($params['imap']);
147:
148: $params = array_merge(array(
149: 'charset' => null,
150: 'domain_field' => 'domain_name',
151: 'folders' => array(),
152: 'hidden_accounts' => array('cyrus'),
153: 'quota' => null,
154: 'userhierarchy' => 'user.'
155: ), $params);
156:
157: parent::__construct($params);
158: }
159:
160: /**
161: * Find out if a set of login credentials are valid.
162: *
163: * @param string $userId The userId to check.
164: * @param array $credentials The credentials to use.
165: *
166: * @throws Horde_Auth_Exception
167: */
168: protected function _authenticate($userId, $credentials)
169: {
170: if (!empty($this->_params['domain_field']) &&
171: ($this->_params['domain_field'] != 'none')) {
172: /* Build the SQL query with domain. */
173: $query = sprintf('SELECT * FROM %s WHERE %s = ? AND %s = ?',
174: $this->_params['table'],
175: $this->_params['username_field'],
176: $this->_params['domain_field']);
177: $values = explode('@', $userId);
178: } else {
179: /* Build the SQL query without domain. */
180: $query = sprintf('SELECT * FROM %s WHERE %s = ?',
181: $this->_params['table'],
182: $this->_params['username_field']);
183: $values = array($userId);
184: }
185:
186: try {
187: $row = $this->_db->selectOne($query, $values);
188: } catch (Horde_Db_Exception $e) {
189: throw new Horde_Auth_Exception('', Horde_Auth::REASON_FAILED);
190: }
191:
192: if (!$row ||
193: !$this->_comparePasswords($row[$this->_params['password_field']], $credentials['password'])) {
194: throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN);
195: }
196:
197: $now = time();
198: if (!empty($this->_params['hard_expiration_field']) &&
199: !empty($row[$this->_params['hard_expiration_field']]) &&
200: ($now > $row[$this->_params['hard_expiration_field']])) {
201: throw new Horde_Auth_Exception('', Horde_Auth::REASON_EXPIRED);
202: }
203:
204: if (!empty($this->_params['soft_expiration_field']) &&
205: !empty($row[$this->_params['soft_expiration_field']]) &&
206: ($now > $row[$this->_params['soft_expiration_field']])) {
207: $this->setCredential('change', true);
208: }
209: }
210:
211: /**
212: * Add a set of authentication credentials.
213: *
214: * @param string $userId The userId to add.
215: * @param array $credentials The credentials to add.
216: *
217: * @throw Horde_Auth_Exception
218: */
219: public function addUser($userId, $credentials)
220: {
221: if (!empty($this->_params['domain_field']) &&
222: ($this->_params['domain_field'] != 'none')) {
223: list($name, $domain) = explode('@', $userId);
224:
225: $query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)',
226: $this->_params['table'],
227: $this->_params['username_field'],
228: $this->_params['domain_field'],
229: $this->_params['password_field']);
230: $values = array(
231: $name,
232: $domain,
233: Horde_Auth::getCryptedPassword($credentials['password'],
234: '',
235: $this->_params['encryption'],
236: $this->_params['show_encryption'])
237: );
238:
239: $query2 = 'INSERT INTO virtual (alias, dest, username, status) VALUES (?, ?, ?, 1)';
240: $values2 = array($userId, $userId, $name);
241:
242: try {
243: $this->_db->insert($query, $values);
244: $this->_db->insert($query2, $values2);
245: } catch (Horde_Db_Exception $e) {
246: throw new Horde_Auth_Exception($e);
247: }
248: } else {
249: parent::addUser($userId, $credentials);
250: }
251:
252: $mailbox = Horde_String::convertCharset($this->_params['userhierarchy'] . $userId, $this->_params['charset'], 'utf7-imap');
253:
254: try {
255: $this->_imap->createMailbox($mailbox);
256: $this->_imap->setACL($mailbox, $this->_params['cyradmin'], 'lrswipcda');
257: if (isset($this->_params['quota']) &&
258: ($this->_params['quota'] >= 0)) {
259: $this->_imap->setQuota($mailbox, array('storage' => $this->_params['quota']));
260: }
261: } catch (Horde_Imap_Client_Exception $e) {
262: throw new Horde_Auth_Exception($e);
263: }
264:
265: foreach ($this->_params['folders'] as $val) {
266: try {
267: $this->_imap->createMailbox($val);
268: $this->_imap->setACL($val, $this->_params['cyradmin'], 'lrswipcda');
269: } catch (Horde_Imap_Client_Exception $e) {}
270: }
271: }
272:
273: /**
274: * Delete a set of authentication credentials.
275: *
276: * @param string $userId The userId to delete.
277: *
278: * @throws Horde_Auth_Exception
279: */
280: public function removeUser($userId)
281: {
282: if (!empty($this->_params['domain_field']) &&
283: ($this->_params['domain_field'] != 'none')) {
284: list($name, $domain) = explode('@', $userId);
285:
286: /* Build the SQL query. */
287: $query = sprintf('DELETE FROM %s WHERE %s = ? and %s = ?',
288: $this->_params['table'],
289: $this->_params['username_field'],
290: $this->_params['domain_field']);
291: $values = array($name, $domain);
292:
293: $query2 = 'DELETE FROM virtual WHERE dest = ?';
294: $values2 = array($userId);
295:
296: try {
297: $this->_db->delete($query, $values);
298: $this->_db->delete($query2, $values2);
299: } catch (Horde_Db_Exception $e) {
300: throw new Horde_Auth_Exception($e);
301: }
302: } else {
303: parent::removeUser($userId);
304: }
305:
306: /* Set ACL for mailbox deletion. */
307: list($admin) = explode('@', $this->_params['cyradmin']);
308:
309: $mailbox = Horde_String::convertCharset($this->_params['userhierarchy'] . $userId, $this->_params['charset'], 'utf7-imap');
310:
311: try {
312: $this->_imap->setACL($mailbox, $admin, array('rights' => 'lrswipcda'));
313: $this->_imap->deleteMailbox($mailbox);
314: } catch (Horde_Imap_Client_Exception $e) {
315: throw new Horde_Auth_Exception($e);
316: }
317: }
318:
319: /**
320: * List all users in the system.
321: *
322: * @return mixed The array of userIds.
323: * @throws Horde_Auth_Exception
324: */
325: public function listUsers($sort = false)
326: {
327: if (!empty($this->_params['domain_field']) &&
328: ($this->_params['domain_field'] != 'none')) {
329: /* Build the SQL query with domain. */
330: $query = sprintf('SELECT %s, %s FROM %s',
331: $this->_params['username_field'],
332: $this->_params['domain_field'],
333: $this->_params['table']);
334: } else {
335: /* Build the SQL query without domain. */
336: $query = sprintf('SELECT %s FROM %s',
337: $this->_params['username_field'],
338: $this->_params['table']);
339: }
340: if ($sort) {
341: $query .= sprintf(" ORDER BY %s", $this->_params['username_field']);
342: }
343:
344: try {
345: $result = $this->_db->selectAll($query);
346: } catch (Horde_Db_Exception $e) {
347: throw new Horde_Auth_Exception($e);
348: }
349:
350: /* Loop through and build return array. */
351: $users = array();
352: if (!empty($this->_params['domain_field']) &&
353: ($this->_params['domain_field'] != 'none')) {
354: foreach ($result as $ar) {
355: if (!in_array($ar[$this->_params['username_field']], $this->_params['hidden_accounts'])) {
356: $users[] = $ar[$this->_params['username_field']] . '@' . $ar[$this->_params['domain_field']];
357: }
358: }
359: } else {
360: foreach ($result as $ar) {
361: if (!in_array($ar[$this->_params['username_field']], $this->_params['hidden_accounts'])) {
362: $users[] = $ar[$this->_params['username_field']];
363: }
364: }
365: }
366: return $users;
367: }
368:
369: /**
370: * Update a set of authentication credentials.
371: *
372: * @param string $oldID The old userId.
373: * @param string $newID The new userId. [NOT SUPPORTED]
374: * @param array $credentials The new credentials
375: *
376: * @throws Horde_Auth_Exception
377: */
378: public function updateUser($oldID, $newID, $credentials)
379: {
380: if (!empty($this->_params['domain_field']) &&
381: ($this->_params['domain_field'] != 'none')) {
382: list($name, $domain) = explode('@', $oldID);
383: /* Build the SQL query with domain. */
384: $query = sprintf(
385: 'UPDATE %s SET %s = ? WHERE %s = ? and %s = ?',
386: $this->_params['table'],
387: $this->_params['password_field'],
388: $this->_params['username_field'],
389: $this->_params['domain_field']
390: );
391: $values = array(
392: Horde_Auth::getCryptedPassword($credentials['password'], '', $this->_params['encryption'], $this->_params['show_encryption']),
393: $name,
394: $domain
395: );
396: } else {
397: /* Build the SQL query. */
398: $query = sprintf(
399: 'UPDATE %s SET %s = ? WHERE %s = ?',
400: $this->_params['table'],
401: $this->_params['password_field'],
402: $this->_params['username_field']
403: );
404: $values = array(
405: Horde_Auth::getCryptedPassword($credentials['password'], '', $this->_params['encryption'], $this->_params['show_encryption']),
406: $oldID
407: );
408: }
409:
410: try {
411: $this->_db->update($query, $values);
412: } catch (Horde_Db_Exception $e) {
413: throw new Horde_Auth_Exception($e);
414: }
415: }
416:
417: }
418: