Restructured signon database and added unit tests
[accounts-sso:signon.git] / src / signond / credentialsdb.cpp
1 /* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of signon
4  *
5  * Copyright (C) 2009-2010 Nokia Corporation.
6  *
7  * Contact: Aurel Popirtac <ext-aurel.popirtac@nokia.com>
8  * Contact: Alberto Mardegan <alberto.mardegan@nokia.com>
9  *
10  * This library is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public License
12  * version 2.1 as published by the Free Software Foundation.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17  * Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with this library; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22  * 02110-1301 USA
23  */
24
25 #include "credentialsdb.h"
26 #include "signond-common.h"
27
28 namespace SignonDaemonNS {
29
30     static const QString driver = QLatin1String("QSQLITE");
31     static const QString connectionName = QLatin1String("SSO-Connection");
32
33     SqlDatabase::SqlDatabase(const QString &databaseName)
34             : m_lastError(QSqlError()),
35               m_database(QSqlDatabase::addDatabase(driver, connectionName))
36
37     {
38         TRACE() << "Supported Drivers:" << this->supportedDrivers();
39         TRACE() << "DATABASE NAME [" << databaseName << "]";
40
41         m_database.setDatabaseName(databaseName);
42     }
43
44     SqlDatabase::~SqlDatabase()
45     {
46         //TODO - sync with driver commit
47         m_database.close();
48     }
49
50     bool SqlDatabase::connect()
51     {
52         if (!m_database.open()) {
53             TRACE() << "Could not open database connection.\n";
54             return false;
55         }
56         return true;
57     }
58
59     void SqlDatabase::disconnect()
60     {
61         m_database.close();
62     }
63
64     QSqlQuery SqlDatabase::exec(const QString &queryStr)
65     {
66         QSqlQuery query(QString(), m_database);
67
68         if (!query.prepare(queryStr))
69             TRACE() << "Query prepare warning: " << query.lastQuery();
70
71         if (!query.exec()) {
72             TRACE() << "Query exec error: " << query.lastQuery();
73             m_lastError = query.lastError();
74             TRACE() << errorInfo(m_lastError);
75         } else
76             m_lastError.setType(QSqlError::NoError);
77
78         return query;
79     }
80
81     bool SqlDatabase::transactionalExec(const QStringList &queryList)
82     {
83         if (!m_database.transaction()) {
84             TRACE() << "Could not start transaction";
85             return false;
86         }
87
88         bool allOk = true;
89         foreach (QString queryStr, queryList) {
90             TRACE() << QString::fromLatin1("TRANSACT Query [%1]").arg(queryStr);
91             QSqlQuery query = exec(queryStr);
92
93             if (lastError().type() != QSqlError::NoError) {
94                 TRACE() << "Error occurred while executing query in transaction." << queryStr;
95                 allOk = false;
96                 break;
97             }
98         }
99
100         if (allOk && m_database.commit()) {
101             TRACE() << "Commit SUCCEEDED.";
102             return true;
103         } else if (!m_database.rollback())
104             TRACE() << "Rollback failed";
105
106         TRACE() << "Transactional exec FAILED!";
107         return false;
108     }
109
110     QSqlError SqlDatabase::lastError(bool queryExecuted, bool clearError)
111     {
112         if (queryExecuted) {
113             QSqlError error = m_lastError;
114             if (clearError)
115                 m_lastError.setType(QSqlError::NoError);
116             return error;
117         } else
118             return m_database.lastError();
119     }
120
121     QMap<QString, QString> SqlDatabase::configuration()
122     {
123         QMap<QString, QString> map;
124
125         map.insert(QLatin1String("Database Name"), m_database.databaseName());
126         map.insert(QLatin1String("Host Name"), m_database.hostName());
127         map.insert(QLatin1String("Username"), m_database.databaseName());
128         map.insert(QLatin1String("Password"), m_database.password());
129         map.insert(QLatin1String("Tables"), m_database.tables().join(QLatin1String(" ")));
130         return map;
131     }
132
133     QString SqlDatabase::errorInfo(const QSqlError &error)
134     {
135         if (!error.isValid())
136             return QLatin1String("SQL Error invalid.");
137
138         QString text;
139         QTextStream stream(&text);
140         stream << "SQL error description:";
141         stream << "\n\tType: ";
142
143         const char *errType;
144         switch (error.type()) {
145             case QSqlError::NoError: errType = "NoError"; break;
146             case QSqlError::ConnectionError: errType = "ConnectionError"; break;
147             case QSqlError::StatementError: errType = "StatementError"; break;
148             case QSqlError::TransactionError: errType = "TransactionError"; break;
149             case QSqlError::UnknownError:
150                 /* fall trough */
151             default: errType = "UnknownError";
152         }
153         stream << errType;
154         stream << "\n\tDatabase text: " << error.databaseText();
155         stream << "\n\tDriver text: " << error.driverText();
156         stream << "\n\tNumber: " << error.number();
157
158         return text;
159     }
160
161     void SqlDatabase::removeDatabase()
162     {
163          QSqlDatabase::removeDatabase(connectionName);
164     }
165
166     /*    -------   CredentialsDB  implementation   -------    */
167
168     CredentialsDB::CredentialsDB(const QString &dbName)
169         : m_pSqlDatabase(new SqlDatabase(dbName))
170     {
171     }
172
173     CredentialsDB::~CredentialsDB()
174     {
175         if (m_pSqlDatabase)
176             delete m_pSqlDatabase;
177
178         SqlDatabase::removeDatabase();
179     }
180
181     QSqlQuery CredentialsDB::exec(const QString &query)
182     {
183         if (!m_pSqlDatabase->connected()) {
184             if (!m_pSqlDatabase->connect()) {
185                 TRACE() << "Could not establish database connection.";
186                 return QSqlQuery();
187             }
188         }
189         return m_pSqlDatabase->exec(query);
190     }
191
192     bool CredentialsDB::transactionalExec(const QStringList &queryList)
193     {
194         if (!m_pSqlDatabase->connected()) {
195             if (!m_pSqlDatabase->connect()) {
196                 TRACE() << "Could not establish database connection.";
197                 return false;
198             }
199         }
200         return m_pSqlDatabase->transactionalExec(queryList);
201     }
202
203     CredentialsDBError CredentialsDB::error(bool queryError, bool clearError) const
204     {
205         return m_pSqlDatabase->lastError(queryError, clearError);
206     }
207
208     QMap<QString, QString> CredentialsDB::sqlDBConfiguration() const
209     {
210         return m_pSqlDatabase->configuration();
211     }
212
213     bool CredentialsDB::hasTableStructure() const
214     {
215         return m_pSqlDatabase->hasTables();
216     }
217
218     bool CredentialsDB::createTableStructure()
219     {
220         /* !!! Foreign keys support seems to be disabled, for the moment... */
221         QStringList createTableQuery = QStringList()
222             <<  QString::fromLatin1(
223                     "CREATE TABLE CREDENTIALS"
224                     "(id INTEGER PRIMARY KEY AUTOINCREMENT,"
225                     "username TEXT,"
226                     "password TEXT,"
227                     "caption TEXT,"
228                     "type INTEGER)")
229             <<  QString::fromLatin1(
230                     "CREATE TABLE METHODS"
231                     "(id INTEGER PRIMARY KEY AUTOINCREMENT,"
232                     "identity_id INTEGER,"
233                     "method TEXT)")
234             <<  QString::fromLatin1(
235                     "CREATE TABLE MECHANISMS"
236                     "(id INTEGER PRIMARY KEY AUTOINCREMENT,"
237                     "method_id INTEGER,"
238                     "mechanism TEXT)")
239             <<  QString::fromLatin1(
240                     "CREATE TABLE ACL"
241                     "(identity_id INTEGER,"
242                     "method_id INTEGER,"
243                     "mechanism_id INTEGER,"
244                     "token TEXT)")
245             <<  QString::fromLatin1(
246                     "CREATE TABLE REALMS"
247                     "(identity_id INTEGER,"
248                     "realm TEXT)");
249
250        foreach (QString createTable, createTableQuery) {
251             QSqlQuery query = exec(createTable);
252             if (error().type() != QSqlError::NoError) {
253                 TRACE() << "Error occurred while creating the database.";
254                 return false;
255             }
256         }
257         return true;
258     }
259
260     bool CredentialsDB::connect()
261     {
262         return m_pSqlDatabase->connect();
263     }
264
265     void CredentialsDB::disconnect()
266     {
267         m_pSqlDatabase->disconnect();
268     }
269
270     QStringList CredentialsDB::methods(const quint32 id)
271     {
272         QStringList list = queryList(
273                 QString::fromLatin1("SELECT method FROM METHODS WHERE identity_id = %1").arg(id));
274         list.removeDuplicates();
275         return list;
276     }
277
278     bool CredentialsDB::checkPassword(const quint32 id, const QString &username, const QString &password)
279     {
280         QSqlQuery query = exec(
281                 QString::fromLatin1("SELECT id FROM CREDENTIALS "
282                                     "WHERE id = '%1' AND username = '%2' AND password = '%3'")
283                     .arg(id).arg(username).arg(password));
284
285         if (errorOccurred()) {
286             TRACE() << "Error occurred while checking password";
287             return false;
288         }
289         if (query.first())
290             return true;
291
292         return false;
293     }
294
295     QList<SignonIdentityInfo> CredentialsDB::credentials(const QMap<QString, QString> &filter)
296     {
297         TRACE();
298         Q_UNUSED(filter)
299         QList<SignonIdentityInfo> result;
300
301         QString queryStr(QString::fromLatin1("SELECT id FROM credentials"));
302
303         // TODO - process filtering step here !!!
304
305         queryStr += QString::fromLatin1(" ORDER BY id");
306
307         QSqlQuery query = exec(queryStr);
308         if (errorOccurred()) {
309             TRACE() << "Error occurred while fetching credentials from database.";
310             return result;
311         }
312
313         while (query.next()) {
314             SignonIdentityInfo info = credentials(query.value(0).toUInt(), false);
315             if (errorOccurred())
316                 break;
317             result << info;
318         }
319
320         return result;
321     }
322
323     bool CredentialsDB::insertList(const QStringList &list, const QString &query_str, const quint32 id)
324     {
325         if (list.isEmpty()) return false;
326         bool allOk = true;
327         QStringListIterator it(list);
328         while (it.hasNext()) {
329             QString queryStr2 = query_str + QString::fromLatin1("VALUES('%1', '%2')").
330                                    arg(id).arg(it.next());
331             exec(queryStr2);
332             if (errorOccurred()) {
333                 allOk = false;
334                 break;
335             }
336         }
337     return allOk;
338     }
339
340     bool CredentialsDB::removeList(const QString &query_str)
341     {
342         TRACE() << query_str;
343         bool allOk = true;
344         exec(query_str);
345         if (errorOccurred()) {
346             allOk = false;
347         }
348     return allOk;
349     }
350
351     QStringList CredentialsDB::queryList(const QString &query_str)
352     {
353         TRACE();
354         QStringList list;
355         QSqlQuery query = exec(query_str);
356         if (errorOccurred())
357             return list;
358         while (query.next()) {
359             list.append(query.value(0).toString());
360         }
361         return list;
362     }
363
364     SignonIdentityInfo CredentialsDB::credentials(const quint32 id, bool queryPassword)
365     {
366         QString query_str;
367
368         if (queryPassword)
369             query_str = QString::fromLatin1(
370                                     "SELECT username, caption, type, password "
371                                     "FROM credentials WHERE id = %1").arg(id);
372         else
373             query_str = QString::fromLatin1(
374                                     "SELECT username, caption, type "
375                                     "FROM credentials WHERE id = %1").arg(id);
376
377         QSqlQuery query = exec(query_str);
378
379         if (!query.first()) {
380             TRACE() << "No result or invalid credentials query.";
381             return SignonIdentityInfo();
382         }
383
384         QString username = query.value(0).toString();
385         QString caption = query.value(1).toString();
386         int type = query.value(2).toInt();
387         QString password;
388         if (queryPassword)
389             password = query.value(3).toString();
390
391         QStringList realms = queryList(QString::fromLatin1("SELECT realm FROM REALMS "
392                                         "WHERE identity_id = %1").arg(id));
393
394         QStringList security_tokens = queryList(QString::fromLatin1("SELECT token FROM ACL "
395                                         "WHERE identity_id = %1").arg(id));
396
397         QMap<QString, QVariant> methods;
398         query_str = QString::fromLatin1("SELECT id, method FROM METHODS "
399                                         "WHERE identity_id = %1").arg(id);
400
401         query = exec(query_str);
402         if (!query.first()) {
403             TRACE() << "Credentials have no authentication method stored.";
404         } else {
405             do {
406                 QStringList mechanisms = queryList(
407                             QString::fromLatin1("SELECT mechanism FROM MECHANISMS "
408                                         "WHERE method_id = %1").arg(query.value(0).toInt()));
409                 methods.insert(query.value(1).toString(), mechanisms);
410             } while (query.next());
411         }
412
413         return SignonIdentityInfo(id, username, password, methods,
414                                   caption, realms, security_tokens, type);
415     }
416
417     bool CredentialsDB::insertMethods(const quint32 id, QMap<QString, QStringList> methods)
418     {
419         QString queryStr;
420         bool allOk = true;
421
422         /* Methods inserts */
423         if (!(methods.keys().empty())) {
424             QMapIterator<QString, QStringList> it(methods);
425             while (it.hasNext()) {
426                 it.next();
427                 /* Correspondences insert */
428                 queryStr = QString::fromLatin1(
429                                     "INSERT INTO METHODS(identity_id, method) "
430                                     "VALUES(%1, '%2')").
431                                     arg(id).arg(it.key());
432
433                 exec(queryStr);
434                 if (errorOccurred()) {
435                     allOk = false;
436                     break;
437                 }
438                         /* Fetch id of the inserted method */
439                 QSqlQuery insertQuery = exec(queryStr);
440                 QVariant idVariant = insertQuery.lastInsertId();
441                 if (!idVariant.isValid()) {
442                     rollback();
443                     TRACE() << "Error occurred while inserting crendentials";
444                     return 0;
445                 }
446                 quint32 id2 = idVariant.toUInt();
447
448                 /* Mechanisms insert */
449
450                 QStringList mechs = it.value();
451                 if (!mechs.isEmpty()) {
452                     QStringListIterator it(mechs);
453                     while (it.hasNext()) {
454                         /* Mechanisms insert */
455                         queryStr = QString::fromLatin1(
456                                     "INSERT INTO MECHANISMS(method_id, mechanism) "
457                                     "VALUES(%1, '%2')").
458                                     arg(id2).arg(it.next());
459                         exec(queryStr);
460                         if (errorOccurred()) {
461                             allOk = false;
462                             break;
463                         }
464                         /* Fetch id of the inserted method */
465                         QVariant idVariant = insertQuery.lastInsertId();
466                         if (!idVariant.isValid()) {
467                             rollback();
468                             TRACE() << "Error occurred while inserting crendentials";
469                             return 0;
470                         }
471
472 /* TODO uncomment this to set mechanism level acl
473                         quint32 id3 = idVariant.toUInt();
474                         QStringList tokens = info.m_accessControlList;
475                         if (!tokens.isEmpty()) {
476                             QStringListIterator it2(tokens);
477                             while (it2.hasNext()) {
478                                 queryStr = QString::fromLatin1(
479                                        "INSERT INTO ACL(identity_id, method_id, mechanism_id, token) "
480                                        "VALUES('%1', '%2', '%3', '%4')").
481                                        arg(id).arg(id2).arg(id3).arg(it2.next());
482                                 exec(queryStr);
483                                 if (errorOccurred()) {
484                                     allOk = false;
485                                     break;
486                                 }
487                             }
488                         }
489 */
490                     }
491                 }
492             }
493         }
494
495         return allOk;
496     }
497
498     bool CredentialsDB::removeMethods(const quint32 id)
499     {
500         QString queryStr;
501         bool allOk = true;
502
503         /* query methods */
504         queryStr = QString::fromLatin1(
505                 "SELECT id FROM METHODS WHERE identity_id = %1").arg(id);
506         QSqlQuery query = exec(queryStr);
507         if (errorOccurred()) {
508             rollback();
509             return false;
510         }
511
512         /* remove mechanisms */
513         while (query.next()) {
514             exec(QString::fromLatin1(
515                     "DELETE FROM MECHANISMS WHERE method_id = %1")
516                     .arg(query.value(0).toInt()));
517         }
518
519         /* remove methods */
520         exec(QString::fromLatin1(
521                 "DELETE FROM METHODS WHERE identity_id = %1").arg(id));
522         return allOk;
523     }
524
525     quint32 CredentialsDB::insertCredentials(const SignonIdentityInfo &info, bool storeSecret)
526     {
527         if (!startTransaction()) {
528             TRACE() << "Could not start transaction. Error inserting credentials.";
529             return 0;
530         }
531
532         /* Credentials insert */
533         QString password;
534         if (storeSecret)
535             password = info.m_password;
536
537         QString queryStr;
538         queryStr = QString::fromLatin1(
539             "INSERT INTO CREDENTIALS (username, password, caption, type) "
540             "VALUES('%1', '%2', '%3', '%4')")
541             .arg(info.m_userName).arg(password).arg(info.m_caption)
542             .arg(info.m_type);
543
544         QSqlQuery insertQuery = exec(queryStr);
545         if (errorOccurred()) {
546             rollback();
547             TRACE() << "Error occurred while inserting crendentials";
548             return 0;
549         }
550
551         /* Fetch id of the inserted credentials */
552         QVariant idVariant = insertQuery.lastInsertId();
553         if (!idVariant.isValid()) {
554             rollback();
555             TRACE() << "Error occurred while inserting crendentials";
556             return 0;
557         }
558         quint32 id = idVariant.toUInt();
559
560         bool allOk = true;
561         /* Methods inserts */
562         insertMethods(id,  info.m_methods);
563
564         /* ACL insert, this will do identity level ACL */
565         insertList(info.m_accessControlList, QString::fromLatin1(
566                 "INSERT INTO ACL(identity_id, token) "), id);
567
568         /* Realms insert */
569         insertList(info.m_realms, QString::fromLatin1(
570                 "INSERT INTO REALMS(identity_id, realm) "), id);
571
572         if (allOk && commit()) {
573             return id;
574         } else {
575             rollback();
576             TRACE() << "Credentials insertion failed.";
577             return 0;
578         }
579     }
580
581     bool CredentialsDB::updateCredentials(const SignonIdentityInfo &info, bool storeSecret)
582     {
583         TRACE() << "UPDATING CREDENTIALS...";
584
585         if (!startTransaction()) {
586             TRACE() << "Could not start transaction. Error updating credentials.";
587             return false;
588         }
589
590         /* Credentials update */
591         QString password;
592         if (storeSecret)
593             password = info.m_password;
594
595         QString queryStr;
596         queryStr = QString::fromLatin1(
597             "UPDATE CREDENTIALS SET username = '%1', password = '%2', "
598             "caption = '%3', type = '%4' WHERE id = '%5'")
599             .arg(info.m_userName).arg(password).arg(info.m_caption)
600             .arg(info.m_type).arg(info.m_id);
601
602         QSqlQuery insertQuery = exec(queryStr);
603         if (errorOccurred()) {
604             rollback();
605             TRACE() << "Error occurred while updating crendentials";
606             return 0;
607         }
608
609         bool allOk = true;
610         /* update other tables by removing old values and inserting new ones */
611
612         /* Methods remove */
613         removeMethods(info.m_id);
614         /* Methods inserts */
615         insertMethods(info.m_id,  info.m_methods);
616
617         /* ACL remove */
618         removeList(QString::fromLatin1(
619                 "DELETE FROM ACL WHERE identity_id = %1")
620                    .arg(info.m_id));
621         /* ACL insert */
622         insertList(info.m_accessControlList, QString::fromLatin1(
623                 "INSERT INTO ACL(identity_id, token) "), info.m_id);
624
625         /* Realms remove */
626         removeList(QString::fromLatin1(
627                 "DELETE FROM REALMS WHERE identity_id = %1").arg( info.m_id));
628         /* Realms insert */
629         insertList(info.m_realms, QString::fromLatin1(
630                 "INSERT INTO REALMS(identity_id, realm) "), info.m_id);
631
632         if (allOk && commit()) {
633             return true;
634         } else {
635             rollback();
636             TRACE() << "Credentials update failed.";
637             return 0;
638         }
639
640         return 0;
641     }
642
643     bool CredentialsDB::removeCredentials(const quint32 id)
644     {
645         if (!startTransaction()) {
646             TRACE() << "Could not start database transaction.";
647             return false;
648         }
649
650         exec(QString::fromLatin1(
651                 "DELETE FROM CREDENTIALS WHERE id = %1").arg(id));
652         if (errorOccurred()) {
653             rollback();
654             return false;
655         }
656
657         exec(QString::fromLatin1(
658                 "DELETE FROM MECHANISMS WHERE method_id IN "
659                 "(SELECT id FROM METHODS WHERE identity_id = %1)").arg(id));
660         if (errorOccurred()) {
661             rollback();
662             return false;
663         }
664
665         exec(QString::fromLatin1(
666                 "DELETE FROM METHODS WHERE identity_id = %1").arg(id));
667         if (errorOccurred()) {
668             rollback();
669             return false;
670         }
671
672         exec(QString::fromLatin1(
673                 "DELETE FROM ACL WHERE identity_id = %1").arg(id));
674         if (errorOccurred()) {
675             rollback();
676             return false;
677         }
678
679         exec(QString::fromLatin1(
680                 "DELETE FROM REALMS WHERE identity_id = %1").arg(id));
681         if (errorOccurred()) {
682             rollback();
683             return false;
684         }
685
686         if (!commit())
687             return false;
688
689         return true;
690     }
691
692     bool CredentialsDB::clear()
693     {
694         exec(QLatin1String("DELETE FROM CREDENTIALS"));
695         if (errorOccurred())
696             return false;
697
698         exec(QLatin1String("DELETE FROM METHODS"));
699         if (errorOccurred())
700             return false;
701
702         exec(QLatin1String("DELETE FROM MECHANISMS"));
703         if (errorOccurred())
704             return false;
705
706         exec(QLatin1String("DELETE FROM ACL"));
707         if (errorOccurred())
708             return false;
709
710         exec(QLatin1String("DELETE FROM REALMS"));
711         if (errorOccurred())
712             return false;
713
714         return true;
715     }
716
717     QStringList CredentialsDB::accessControlList(const quint32 identityId)
718     {
719         return queryList(QString::fromLatin1(
720                 "SELECT token FROM ACL WHERE identity_id = %1")
721                          .arg(identityId));
722     }
723
724     QString CredentialsDB::credentialsOwnerSecurityToken(const quint32 identityId)
725     {
726         QStringList acl = accessControlList(identityId);
727         int index = -1;
728         QRegExp aegisIdTokenPrefixRegExp(QLatin1String("^AID::.*"));
729         if ((index = acl.indexOf(aegisIdTokenPrefixRegExp)) != -1)
730             return acl.at(index);
731         return QString();
732     }
733
734     bool CredentialsDB::startTransaction()
735     {
736         return m_pSqlDatabase->m_database.transaction();
737     }
738
739     bool CredentialsDB::commit()
740     {
741         return m_pSqlDatabase->m_database.commit();
742     }
743
744     void CredentialsDB::rollback()
745     {
746         if (!m_pSqlDatabase->m_database.rollback())
747             TRACE() << "Rollback failed, db data integrity could be compromised.";
748     }
749
750 } //namespace SignonDaemonNS