1 /*
2   Copyright 2021 Northern.tech AS
3 
4   This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the
8   Free Software Foundation; version 3.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
18 
19   To the extent this program is licensed as part of the Enterprise
20   versions of CFEngine, the applicable Commercial Open Source License
21   (COSL) may apply to this file if you as a licensee so wish it. See
22   included file COSL.txt.
23 */
24 
25 #include <verify_databases.h>
26 
27 #include <actuator.h>
28 #include <promises.h>
29 #include <files_names.h>
30 #include <conversion.h>
31 #include <attributes.h>
32 #include <string_lib.h>
33 #include <locks.h>
34 #include <cf_sql.h>
35 #include <rlist.h>
36 #include <policy.h>
37 #include <cf-agent-enterprise-stubs.h>
38 #include <eval_context.h>
39 #include <ornaments.h>
40 #include <misc_lib.h>
41 
42 static int CheckDatabaseSanity(const Attributes *a, const Promise *pp);
43 static PromiseResult VerifySQLPromise(EvalContext *ctx, const Attributes *a, const Promise *pp);
44 static bool VerifyDatabasePromise(CfdbConn *cfdb, char *database, const Attributes *a);
45 
46 static bool ValidateSQLTableName(const char *path, char *db, size_t db_size, char *table, size_t table_size);
47 static bool VerifyTablePromise(EvalContext *ctx, CfdbConn *cfdb, char *table_path, Rlist *columns, const Attributes *a, const Promise *pp, PromiseResult *result);
48 static void QueryTableColumns(char *s, char *db, char *table);
49 static bool NewSQLColumns(char *table, Rlist *columns, char ***name_table, char ***type_table, int **size_table,
50                          int **done);
51 static void DeleteSQLColumns(char **name_table, char **type_table, int *size_table, int *done, int len);
52 static void CreateDBQuery(DatabaseType type, char *query);
53 static bool CreateTableColumns(CfdbConn *cfdb, char *table, Rlist *columns);
54 static bool CheckSQLDataType(char *type, char *ref_type, const Promise *pp);
55 static int TableExists(CfdbConn *cfdb, char *name);
56 static Rlist *GetSQLTables(CfdbConn *cfdb);
57 static void ListTables(int type, char *query);
58 static bool ValidateRegistryPromiser(char *s, const Promise *pp);
59 static bool CheckRegistrySanity(const Attributes *a, const Promise *pp);
60 
61 /*****************************************************************************/
62 
VerifyDatabasePromises(EvalContext * ctx,const Promise * pp)63 PromiseResult VerifyDatabasePromises(EvalContext *ctx, const Promise *pp)
64 {
65     PromiseBanner(ctx, pp);
66 
67     Attributes a = GetDatabaseAttributes(ctx, pp);
68 
69     if (!CheckDatabaseSanity(&a, pp))
70     {
71         return PROMISE_RESULT_FAIL;
72     }
73 
74     if (strcmp(a.database.type, "sql") == 0)
75     {
76         return VerifySQLPromise(ctx, &a, pp);
77     }
78     else if (strcmp(a.database.type, "ms_registry") == 0)
79     {
80 #if defined(__MINGW32__)
81         return VerifyRegistryPromise(ctx, &a, pp);
82 #endif
83         return PROMISE_RESULT_NOOP;
84     }
85     else
86     {
87         ProgrammingError("Unknown database type '%s'", a.database.type);
88     }
89 }
90 
91 /*****************************************************************************/
92 /* Level                                                                     */
93 /*****************************************************************************/
94 
VerifySQLPromise(EvalContext * ctx,const Attributes * a,const Promise * pp)95 static PromiseResult VerifySQLPromise(EvalContext *ctx, const Attributes *a, const Promise *pp)
96 {
97     assert(a != NULL);
98     char database[CF_MAXDBNAMESIZE];
99     char table[CF_MAXTABLENAMESIZE];
100     char db_path[CF_MAXVARSIZE];
101     // Should have room for both buffers + 1 dot minus 1 NUL byte:
102     nt_static_assert(sizeof(db_path) >= (sizeof(database) + sizeof(table)));
103     char *sp;
104     int count = 0;
105     CfdbConn cfdb;
106     CfLock thislock;
107     char lockname[CF_BUFSIZE];
108 
109     snprintf(lockname, CF_BUFSIZE - 1, "db-%s", pp->promiser);
110 
111     thislock = AcquireLock(ctx, lockname, VUQNAME, CFSTARTTIME, a->transaction.ifelapsed, a->transaction.expireafter, pp, false);
112     if (thislock.lock == NULL)
113     {
114         return PROMISE_RESULT_SKIPPED;
115     }
116 
117     database[0] = '\0';
118     table[0] = '\0';
119 
120     for (sp = pp->promiser; *sp != '\0'; sp++)
121     {
122         if (strchr("./\\", *sp))
123         {
124             count++;
125             strlcpy(table, sp + 1, sizeof(table));
126             sscanf(pp->promiser, "%[^.\\/]", database);
127 
128             if (strlen(database) == 0)
129             {
130                 cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
131                      "SQL database promiser syntax should be of the form \"database.table\"");
132                 PromiseRef(LOG_LEVEL_ERR, pp);
133                 YieldCurrentLock(thislock);
134                 return PROMISE_RESULT_FAIL;
135             }
136         }
137     }
138 
139     PromiseResult result = PROMISE_RESULT_NOOP;
140     if (count > 1)
141     {
142         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "SQL database promiser syntax should be of the form \"database.table\"");
143         PromiseRef(LOG_LEVEL_ERR, pp);
144         result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
145     }
146 
147     if (strlen(database) == 0)
148     {
149         strlcpy(database, pp->promiser, sizeof(database));
150     }
151 
152     if (a->database.operation == NULL)
153     {
154         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
155              "Missing database_operation in database promise");
156         PromiseRef(LOG_LEVEL_ERR, pp);
157         YieldCurrentLock(thislock);
158         return PROMISE_RESULT_FAIL;
159     }
160 
161     if (strcmp(a->database.operation, "delete") == 0)
162     {
163         /* Just deal with one */
164         strcpy(a->database.operation, "drop");
165     }
166 
167 /* Connect to the server */
168 
169     CfConnectDB(&cfdb, a->database.db_server_type, a->database.db_server_host, a->database.db_server_owner,
170                 a->database.db_server_password, database);
171 
172     if (!cfdb.connected)
173     {
174         /* If we haven't said create then db should already exist */
175 
176         if ((a->database.operation) && (strcmp(a->database.operation, "create") != 0))
177         {
178             Log(LOG_LEVEL_ERR, "Could not connect an existing database '%s' - check server configuration?", database);
179             PromiseRef(LOG_LEVEL_ERR, pp);
180             CfCloseDB(&cfdb);
181             YieldCurrentLock(thislock);
182             return PROMISE_RESULT_FAIL;
183         }
184     }
185 
186 /* Check change of existential constraints */
187 
188     if ((a->database.operation) && (strcmp(a->database.operation, "create") == 0))
189     {
190         CfConnectDB(&cfdb, a->database.db_server_type, a->database.db_server_host, a->database.db_server_owner,
191                     a->database.db_server_password, a->database.db_connect_db);
192 
193         if (!cfdb.connected)
194         {
195             Log(LOG_LEVEL_ERR, "Could not connect to the sql_db server for '%s'", database);
196             YieldCurrentLock(thislock);
197             return PROMISE_RESULT_FAIL;
198         }
199 
200         /* Don't drop the db if we really want to drop a table */
201 
202         if ((strlen(table) == 0) || ((strlen(table) > 0) && (strcmp(a->database.operation, "drop") != 0)))
203         {
204             VerifyDatabasePromise(&cfdb, database, a);
205         }
206 
207         /* Close the database here to commit the change - might have to reopen */
208 
209         CfCloseDB(&cfdb);
210     }
211 
212 /* Now check the structure of the named table, if any */
213 
214     if (strlen(table) == 0)
215     {
216         YieldCurrentLock(thislock);
217         return result;
218     }
219 
220     CfConnectDB(&cfdb, a->database.db_server_type, a->database.db_server_host, a->database.db_server_owner,
221                 a->database.db_server_password, database);
222 
223     if (!cfdb.connected)
224     {
225         Log(LOG_LEVEL_INFO, "Database '%s' is not connected", database);
226     }
227     else
228     {
229         snprintf(db_path, sizeof(db_path), "%s.%s", database, table);
230 
231         if (VerifyTablePromise(ctx, &cfdb, db_path, a->database.columns, a, pp, &result))
232         {
233             cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_NOOP, pp, a, "Table '%s' is as promised", db_path);
234         }
235         else
236         {
237             cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "Table '%s' is not as promised", db_path);
238             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
239         }
240 
241 /* Finally check any row constraints on this table */
242 
243         if (a->database.rows)
244         {
245             Log(LOG_LEVEL_INFO,
246                   "Database row operations are not currently supported. Please contact cfengine with suggestions.");
247         }
248 
249         CfCloseDB(&cfdb);
250     }
251 
252     YieldCurrentLock(thislock);
253 
254     return result;
255 }
256 
VerifyDatabasePromise(CfdbConn * cfdb,char * database,const Attributes * a)257 static bool VerifyDatabasePromise(CfdbConn *cfdb, char *database, const Attributes *a)
258 {
259     assert(a != NULL);
260     char query[CF_BUFSIZE], name[CF_MAXVARSIZE];
261     int found = false;
262 
263     Log(LOG_LEVEL_VERBOSE, "Verifying promised database");
264 
265     if (!cfdb->connected)
266     {
267         Log(LOG_LEVEL_INFO, "Database '%s' is not connected", database);
268         return false;
269     }
270 
271     CreateDBQuery(cfdb->type, query);
272 
273     CfNewQueryDB(cfdb, query);
274 
275     if (cfdb->maxcolumns < 1)
276     {
277         Log(LOG_LEVEL_ERR, "The schema did not promise the expected number of fields - got %d expected >= %d",
278               cfdb->maxcolumns, 1);
279         return false;
280     }
281 
282     while (CfFetchRow(cfdb))
283     {
284         strlcpy(name, CfFetchColumn(cfdb, 0), CF_MAXVARSIZE);
285 
286         Log(LOG_LEVEL_VERBOSE, "Discovered a database called '%s'", name);
287 
288         if (strcmp(name, database) == 0)
289         {
290             found = true;
291         }
292     }
293 
294     if (found)
295     {
296         Log(LOG_LEVEL_VERBOSE, "Database '%s' exists on this connection", database);
297         return true;
298     }
299     else
300     {
301         Log(LOG_LEVEL_VERBOSE, "Database '%s' does not seem to exist on this connection", database);
302     }
303 
304     if ((a->database.operation) && (strcmp(a->database.operation, "drop") == 0))
305     {
306         if (((a->transaction.action) != cfa_warn) && (!DONTDO))
307         {
308             Log(LOG_LEVEL_VERBOSE, "Attempting to delete the database '%s'", database);
309             snprintf(query, CF_MAXVARSIZE - 1, "drop database %s", database);
310             CfVoidQueryDB(cfdb, query);
311             return cfdb->result;
312         }
313         else
314         {
315             Log(LOG_LEVEL_WARNING, "Need to delete the database '%s' but only a warning was promised", database);
316             return false;
317         }
318     }
319 
320     if ((a->database.operation) && (strcmp(a->database.operation, "create") == 0))
321     {
322         if (((a->transaction.action) != cfa_warn) && (!DONTDO))
323         {
324             Log(LOG_LEVEL_VERBOSE, "Attempting to create the database '%s'", database);
325             snprintf(query, CF_MAXVARSIZE - 1, "create database %s", database);
326             CfVoidQueryDB(cfdb, query);
327             return cfdb->result;
328         }
329         else
330         {
331             Log(LOG_LEVEL_WARNING, "Need to create the database '%s' but only a warning was promised", database);
332             return false;
333         }
334     }
335 
336     return false;
337 }
338 
339 /*****************************************************************************/
340 
CheckDatabaseSanity(const Attributes * a,const Promise * pp)341 static int CheckDatabaseSanity(const Attributes *a, const Promise *pp)
342 {
343     assert(a != NULL);
344     Rlist *rp;
345     int retval = true, commas = 0;
346 
347     if ((a->database.type) && (strcmp(a->database.type, "ms_registry") == 0))
348     {
349         retval = CheckRegistrySanity(a, pp);
350     }
351     else if ((a->database.type) && (strcmp(a->database.type, "sql") == 0))
352     {
353         if ((strchr(pp->promiser, '.') == NULL) && (strchr(pp->promiser, '/') == NULL)
354             && (strchr(pp->promiser, '\\') == NULL))
355         {
356             if (a->database.columns)
357             {
358                 Log(LOG_LEVEL_ERR, "Row values promised for an SQL table, but only the root database was promised");
359                 retval = false;
360             }
361 
362             if (a->database.rows)
363             {
364                 Log(LOG_LEVEL_ERR, "Columns promised for an SQL table, but only the root database was promised");
365                 retval = false;
366             }
367         }
368 
369         if (a->database.db_server_host == NULL)
370         {
371             Log(LOG_LEVEL_ERR, "No server host is promised for connecting to the SQL server");
372             retval = false;
373         }
374 
375         if (a->database.db_server_owner == NULL)
376         {
377             Log(LOG_LEVEL_ERR, "No database login user is promised for connecting to the SQL server");
378             retval = false;
379         }
380 
381         if (a->database.db_server_password == NULL)
382         {
383             Log(LOG_LEVEL_ERR, "No database authentication password is promised for connecting to the SQL server");
384             retval = false;
385         }
386 
387         for (rp = a->database.columns; rp != NULL; rp = rp->next)
388         {
389             commas = CountChar(RlistScalarValue(rp), ',');
390 
391             if ((commas > 2) || (commas < 1))
392             {
393                 Log(LOG_LEVEL_ERR, "SQL Column format should be NAME,TYPE[,SIZE]");
394                 retval = false;
395             }
396         }
397 
398     }
399 
400     if ((a->database.operation)
401         && ((strcmp(a->database.operation, "delete") == 0) || (strcmp(a->database.operation, "drop") == 0)))
402     {
403         if (pp->comment == NULL)
404         {
405             Log(LOG_LEVEL_ERR,
406                   "When specifying a delete/drop from an SQL database you must add a comment. Take a backup of the database before making this change. This is a highly destructive operation.");
407             retval = false;
408         }
409     }
410 
411     return retval;
412 }
413 
CheckRegistrySanity(const Attributes * a,const Promise * pp)414 static bool CheckRegistrySanity(const Attributes *a, const Promise *pp)
415 {
416     assert(a != NULL);
417     bool retval = true;
418 
419     ValidateRegistryPromiser(pp->promiser, pp);
420 
421     if ((a->database.operation) && (strcmp(a->database.operation, "create") == 0))
422     {
423         if (a->database.rows == NULL)
424         {
425             Log(LOG_LEVEL_INFO, "No row values promised for the MS registry database");
426         }
427 
428         if (a->database.columns != NULL)
429         {
430             Log(LOG_LEVEL_ERR, "Columns are only used to delete promised values for the MS registry database");
431             retval = false;
432         }
433     }
434 
435     if ((a->database.operation)
436         && ((strcmp(a->database.operation, "delete") == 0) || (strcmp(a->database.operation, "drop") == 0)))
437     {
438         if (a->database.columns == NULL)
439         {
440             Log(LOG_LEVEL_INFO, "No columns were promised deleted in the MS registry database");
441         }
442 
443         if (a->database.rows != NULL)
444         {
445             Log(LOG_LEVEL_ERR, "Rows cannot be deleted in the MS registry database, only entire columns");
446             retval = false;
447         }
448     }
449 
450     for (Rlist *rp = a->database.rows; rp != NULL; rp = rp->next)
451     {
452         if (CountChar(RlistScalarValue(rp), ',') != 2)
453         {
454             Log(LOG_LEVEL_ERR, "Registry row format should be NAME,REG_SZ,VALUE, not '%s'", RlistScalarValue(rp));
455             retval = false;
456         }
457     }
458 
459     for (Rlist *rp = a->database.columns; rp != NULL; rp = rp->next)
460     {
461         if (CountChar(RlistScalarValue(rp), ',') > 0)
462         {
463             Log(LOG_LEVEL_ERR, "MS registry column format should be NAME only in deletion");
464             retval = false;
465         }
466     }
467 
468     return retval;
469 }
470 
ValidateRegistryPromiser(char * key,const Promise * pp)471 static bool ValidateRegistryPromiser(char *key, const Promise *pp)
472 {
473     static char *const valid[] = { "HKEY_CLASSES_ROOT", "HKEY_CURRENT_CONFIG",
474         "HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE", "HKEY_USERS", NULL
475     };
476     char root_key[CF_MAXVARSIZE];
477     char *sp;
478     int i;
479 
480     /* First remove the root key */
481 
482     strlcpy(root_key, key, CF_MAXVARSIZE );
483     sp = strchr(root_key, '\\');
484     if (sp == NULL)
485     {
486         Log(LOG_LEVEL_ERR, "Cannot locate '\\' in '%s'", root_key);
487         Log(LOG_LEVEL_ERR, "Failed validating registry promiser");
488         PromiseRef(LOG_LEVEL_ERR, pp);
489         return false;
490     }
491     *sp = '\0';
492 
493     for (i = 0; valid[i] != NULL; i++)
494     {
495         if (strcmp(root_key, valid[i]) == 0)
496         {
497             return true;
498         }
499     }
500 
501     Log(LOG_LEVEL_ERR, "Non-editable registry prefix '%s'", root_key);
502     PromiseRef(LOG_LEVEL_ERR, pp);
503     return false;
504 }
505 
506 /*****************************************************************************/
507 /* Linker troubles require this code to be here in the main body             */
508 /*****************************************************************************/
509 
VerifyTablePromise(EvalContext * ctx,CfdbConn * cfdb,char * table_path,Rlist * columns,const Attributes * a,const Promise * pp,PromiseResult * result)510 static bool VerifyTablePromise(EvalContext *ctx, CfdbConn *cfdb, char *table_path, Rlist *columns, const Attributes *a,
511                               const Promise *pp, PromiseResult *result)
512 {
513     assert(a != NULL);
514     char name[CF_MAXVARSIZE], type[CF_MAXVARSIZE], query[CF_MAXVARSIZE];
515     char db[CF_MAXDBNAMESIZE];
516     char table[CF_MAXTABLENAMESIZE];
517     int i, count, size, no_of_cols, *size_table, *done, identified;
518     bool retval = true;
519     char **name_table, **type_table;
520 
521     Log(LOG_LEVEL_VERBOSE, "Verifying promised table structure for '%s'", table_path);
522 
523     if (!ValidateSQLTableName(table_path, db, sizeof(db), table, sizeof(table)))
524     {
525         Log(LOG_LEVEL_ERR,
526             "The structure of the promiser did not match that for an SQL table, i.e. 'database.table'");
527         return false;
528     }
529     else
530     {
531         Log(LOG_LEVEL_VERBOSE, "Assuming database '%s' with table '%s'", db, table);
532     }
533 
534 /* Verify the existence of the tables within the database */
535 
536     if (!TableExists(cfdb, table))
537     {
538         Log(LOG_LEVEL_ERR, "The database did not contain the promised table '%s'", table_path);
539 
540         if ((a->database.operation) && (strcmp(a->database.operation, "create") == 0))
541         {
542             if ((!DONTDO) && ((a->transaction.action) != cfa_warn))
543             {
544                 cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_CHANGE, pp, a, "Database.table '%s' doesn't seem to exist, creating",
545                      table_path);
546                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
547                 return CreateTableColumns(cfdb, table, columns);
548             }
549             else
550             {
551                 Log(LOG_LEVEL_WARNING, "Database.table '%s' doesn't seem to exist, but only a warning was promised",
552                       table_path);
553             }
554         }
555 
556         return false;
557     }
558 
559 /* Get a list of the columns in the table */
560 
561     QueryTableColumns(query, db, table);
562     CfNewQueryDB(cfdb, query);
563 
564     if (cfdb->maxcolumns != 3)
565     {
566         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "Could not make sense of the columns");
567         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
568         CfDeleteQuery(cfdb);
569         return false;
570     }
571 
572 /* Assume that the Rlist has been validated and consists of a,b,c */
573 
574     count = 0;
575     no_of_cols = RlistLen(columns);
576 
577     if (!NewSQLColumns(table, columns, &name_table, &type_table, &size_table, &done))
578     {
579         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "Could not make sense of the columns");
580         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
581         return false;
582     }
583 
584 /* Obtain columns from the named table - if any */
585 
586     while (CfFetchRow(cfdb))
587     {
588         char *sizestr;
589 
590         name[0] = '\0';
591         type[0] = '\0';
592         size = CF_NOINT;
593 
594         strlcpy(name, CfFetchColumn(cfdb, 0), CF_MAXVARSIZE);
595         strlcpy(type, CfFetchColumn(cfdb, 1), CF_MAXVARSIZE);
596         ToLowerStrInplace(type);
597         sizestr = CfFetchColumn(cfdb, 2);
598 
599         if (sizestr)
600         {
601             size = IntFromString(sizestr);
602         }
603 
604         Log(LOG_LEVEL_VERBOSE, "Discovered database column (%s,%s,%d)", name, type, size);
605 
606         if (sizestr && (size == CF_NOINT))
607         {
608             cfPS(ctx, LOG_LEVEL_VERBOSE, PROMISE_RESULT_NOOP, pp, a,
609                  "Integer size of SQL datatype could not be determined or was not specified - invalid promise.");
610             DeleteSQLColumns(name_table, type_table, size_table, done, no_of_cols);
611             CfDeleteQuery(cfdb);
612             return false;
613         }
614 
615         identified = false;
616 
617         for (i = 0; i < no_of_cols; i++)
618         {
619             if (done[i])
620             {
621                 continue;
622             }
623 
624             if (strcmp(name, name_table[i]) == 0)
625             {
626                 CheckSQLDataType(type, type_table[i], pp);
627 
628                 if (size != size_table[i])
629                 {
630                     cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
631                          "Promised column '%s' in database.table '%s' has a non-matching array size (%d != %d)",
632                          name, table_path, size, size_table[i]);
633                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
634                 }
635                 else
636                 {
637                     Log(LOG_LEVEL_VERBOSE, "Promised column '%s' in database.table '%s' is as promised", name,
638                           table_path);
639                 }
640 
641                 count++;
642                 done[i] = true;
643                 identified = true;
644                 break;
645             }
646         }
647 
648         if (!identified)
649         {
650             cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
651                  "Column '%s' found in database.table '%s' is not part of its promise.", name, table_path);
652             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
653 
654             if ((a->database.operation) && (strcmp(a->database.operation, "drop") == 0))
655             {
656                 cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
657                      "CFEngine will not promise to repair this, as the operation is potentially too destructive.");
658                 // Future allow deletion?
659                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
660             }
661 
662             retval = false;
663         }
664     }
665 
666     CfDeleteQuery(cfdb);
667 
668 /* Now look for deviations - only if we have promised to create missing */
669 
670     if ((a->database.operation) && (strcmp(a->database.operation, "drop") == 0))
671     {
672         return retval;
673     }
674 
675     if (count != no_of_cols)
676     {
677         for (i = 0; i < no_of_cols; i++)
678         {
679             if (!done[i])
680             {
681                 Log(LOG_LEVEL_ERR, "Promised column '%s' missing from database table '%s'", name_table[i],
682                       pp->promiser);
683 
684                 if ((!DONTDO) && ((a->transaction.action) != cfa_warn))
685                 {
686                     if (size_table[i] > 0)
687                     {
688                         snprintf(query, CF_MAXVARSIZE - 1, "ALTER TABLE %s ADD %s %s(%d)", table, name_table[i],
689                                  type_table[i], size_table[i]);
690                     }
691                     else
692                     {
693                         snprintf(query, CF_MAXVARSIZE - 1, "ALTER TABLE %s ADD %s %s", table, name_table[i],
694                                  type_table[i]);
695                     }
696 
697                     CfVoidQueryDB(cfdb, query);
698                     cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_CHANGE, pp, a, "Adding promised column '%s' to database table '%s'",
699                          name_table[i], table);
700                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
701                     retval = true;
702                 }
703                 else
704                 {
705                     cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a,
706                          "Promised column '%s' missing from database table '%s' but only a warning was promised",
707                          name_table[i], table);
708                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
709                     retval = false;
710                 }
711             }
712         }
713     }
714 
715     DeleteSQLColumns(name_table, type_table, size_table, done, no_of_cols);
716 
717     return retval;
718 }
719 
720 /*****************************************************************************/
721 
TableExists(CfdbConn * cfdb,char * name)722 static int TableExists(CfdbConn *cfdb, char *name)
723 {
724     Rlist *rp, *list = NULL;
725     int match = false;
726 
727     list = GetSQLTables(cfdb);
728 
729     for (rp = list; rp != NULL; rp = rp->next)
730     {
731         if (strcmp(name, RlistScalarValue(rp)) == 0)
732         {
733             match = true;
734         }
735     }
736 
737     RlistDestroy(list);
738 
739     return match;
740 }
741 
742 /*****************************************************************************/
743 
CreateTableColumns(CfdbConn * cfdb,char * table,Rlist * columns)744 static bool CreateTableColumns(CfdbConn *cfdb, char *table, Rlist *columns)
745 {
746     char entry[CF_MAXVARSIZE], query[CF_BUFSIZE];
747     int i, *size_table, *done;
748     char **name_table, **type_table;
749     int no_of_cols = RlistLen(columns);
750 
751     Log(LOG_LEVEL_ERR, "Trying to create table '%s'", table);
752 
753     if (!NewSQLColumns(table, columns, &name_table, &type_table, &size_table, &done))
754     {
755         return false;
756     }
757 
758     if (no_of_cols > 0)
759     {
760         snprintf(query, CF_BUFSIZE - 1, "create table %s(", table);
761 
762         for (i = 0; i < no_of_cols; i++)
763         {
764             Log(LOG_LEVEL_VERBOSE, "Forming column template %s %s %d", name_table[i], type_table[i],
765                   size_table[i]);;
766 
767             if (size_table[i] > 0)
768             {
769                 snprintf(entry, CF_MAXVARSIZE - 1, "%s %s(%d)", name_table[i], type_table[i], size_table[i]);
770             }
771             else
772             {
773                 snprintf(entry, CF_MAXVARSIZE - 1, "%s %s", name_table[i], type_table[i]);
774             }
775 
776             strcat(query, entry);
777 
778             if (i < no_of_cols - 1)
779             {
780                 strcat(query, ",");
781             }
782         }
783 
784         strcat(query, ")");
785     }
786 
787     CfVoidQueryDB(cfdb, query);
788     DeleteSQLColumns(name_table, type_table, size_table, done, no_of_cols);
789     return true;
790 }
791 
792 /*****************************************************************************/
793 /* Level                                                                     */
794 /*****************************************************************************/
795 
GetSQLTables(CfdbConn * cfdb)796 static Rlist *GetSQLTables(CfdbConn *cfdb)
797 {
798     Rlist *list = NULL;
799     char query[CF_MAXVARSIZE];
800 
801     ListTables(cfdb->type, query);
802 
803     CfNewQueryDB(cfdb, query);
804 
805     if (cfdb->maxcolumns != 1)
806     {
807         Log(LOG_LEVEL_ERR, "Could not make sense of the columns");
808         CfDeleteQuery(cfdb);
809         return NULL;
810     }
811 
812     while (CfFetchRow(cfdb))
813     {
814         RlistPrepend(&list, CfFetchColumn(cfdb, 0), RVAL_TYPE_SCALAR);
815     }
816 
817     CfDeleteQuery(cfdb);
818 
819     return list;
820 }
821 
822 /*****************************************************************************/
823 
CreateDBQuery(DatabaseType type,char * query)824 static void CreateDBQuery(DatabaseType type, char *query)
825 {
826     switch (type)
827     {
828     case DATABASE_TYPE_MYSQL:
829         snprintf(query, CF_MAXVARSIZE - 1, "show databases");
830         break;
831 
832     case DATABASE_TYPE_POSTGRES:
833         /* This gibberish is the simplest thing I can find in postgres */
834 
835         snprintf(query, CF_MAXVARSIZE - 1, "SELECT pg_database.datname FROM pg_database");
836         break;
837 
838     default:
839         snprintf(query, CF_MAXVARSIZE, "NULL QUERY");
840         break;
841     }
842 }
843 
844 /*****************************************************************************/
845 
ValidateSQLTableName(const char * const path,char * const db,const size_t db_size,char * const table,const size_t table_size)846 static bool ValidateSQLTableName(
847     const char *const path,
848     char *const db,
849     const size_t db_size,
850     char *const table,
851     const size_t table_size)
852 {
853     assert(path != NULL);
854     assert(db != NULL);
855     assert(table != NULL);
856 
857     // path is db + table, for example: cfsettings.table
858     const char *separator = strchr(path, '.');
859 
860     if (separator == NULL)
861     {
862         Log(LOG_LEVEL_ERR, "No separator found in database path '%s'", path);
863         return false;
864     }
865 
866     // Backwards compatibility with bugs - this would cause false before:
867     if ((strchr(path, '/') != NULL) || (strchr(path, '\\') != NULL))
868     {
869         Log(LOG_LEVEL_ERR,
870             "Unexpected slash in database path '%s'",
871             path);
872         return false;
873     }
874 
875     assert(separator >= path);
876     const size_t db_length = separator - path;
877     if (db_length >= db_size)
878     {
879         Log(LOG_LEVEL_ERR,
880             "Database name too long (%zu bytes) in '%s'",
881             db_length,
882             path);
883         return false;
884     }
885 
886     const char *table_start = separator + 1;
887     const size_t table_length = strlen(table_start);
888 
889     if (table_length >= table_size)
890     {
891         Log(LOG_LEVEL_ERR,
892             "Database table name too long (%zu bytes) in '%s'",
893             table_length,
894             path);
895         return false;
896     }
897 
898     memcpy(db, path, db_length);
899     memcpy(table, table_start, table_length);
900     db[db_length] = table[table_length] = '\0';
901 
902     assert(strlen(db) < db_size);
903     assert(strlen(table) < table_size);
904     assert(strlen(db) == db_length);
905     assert(strlen(path) == (strlen(db) + 1 + strlen(table)));
906 
907     return true;
908 }
909 
910 /*****************************************************************************/
911 
QueryTableColumns(char * s,char * db,char * table)912 static void QueryTableColumns(char *s, char *db, char *table)
913 {
914     snprintf(s, CF_MAXVARSIZE - 1,
915              "SELECT column_name,data_type,character_maximum_length FROM information_schema->columns WHERE table_name ='%s' AND table_schema = '%s'",
916              table, db);
917 }
918 
919 /*****************************************************************************/
920 
NewSQLColumns(char * table,Rlist * columns,char *** name_table,char *** type_table,int ** size_table,int ** done)921 static bool NewSQLColumns(char *table, Rlist *columns, char ***name_table, char ***type_table, int **size_table,
922                          int **done)
923 {
924     int i, no_of_cols = RlistLen(columns);
925     Rlist *cols, *rp;
926 
927     *name_table = (char **) xmalloc(sizeof(char *) * (no_of_cols + 1));
928     *type_table = (char **) xmalloc(sizeof(char *) * (no_of_cols + 1));
929     *size_table = (int *) xmalloc(sizeof(int) * (no_of_cols + 1));
930     *done = (int *) xmalloc(sizeof(int) * (no_of_cols + 1));
931 
932     for (i = 0, rp = columns; rp != NULL; rp = rp->next, i++)
933     {
934         (*done)[i] = 0;
935 
936         cols = RlistFromSplitString(RlistScalarValue(rp), ',');
937 
938         if (!cols)
939         {
940             Log(LOG_LEVEL_ERR, "No columns promised for table '%s' - makes no sense", table);
941             return false;
942         }
943 
944         if (cols->val.item == NULL)
945         {
946             Log(LOG_LEVEL_ERR, "Malformed column promise for table '%s' - found not even a name", table);
947             free(*name_table);
948             free(*type_table);
949             free(*size_table);
950             free(*done);
951             return false;
952         }
953 
954         (*name_table)[i] = xstrdup(RlistScalarValue(cols));
955 
956         if (cols->next == NULL)
957         {
958             Log(LOG_LEVEL_ERR, "Malformed column '%s' promised for table '%s' - missing a type", (*name_table)[i],
959                   table);
960             free(*name_table);
961             free(*type_table);
962             free(*size_table);
963             free(*done);
964             return false;
965         }
966 
967         (*type_table)[i] = xstrdup(RlistScalarValue(cols->next));
968 
969         if (cols->next->next == NULL)
970         {
971             (*size_table)[i] = 0;
972         }
973         else
974         {
975             if (cols->next->next->val.item)
976             {
977                 (*size_table)[i] = IntFromString(RlistScalarValue(cols->next->next));
978             }
979             else
980             {
981                 (*size_table)[i] = 0;
982             }
983         }
984 
985         RlistDestroy(cols);
986     }
987 
988     return true;
989 }
990 
991 /*****************************************************************************/
992 
DeleteSQLColumns(char ** name_table,char ** type_table,int * size_table,int * done,int len)993 static void DeleteSQLColumns(char **name_table, char **type_table, int *size_table, int *done, int len)
994 {
995     int i;
996 
997     if ((name_table == NULL) || (type_table == NULL) || (size_table == NULL))
998     {
999         return;
1000     }
1001 
1002     for (i = 0; i < len; i++)
1003     {
1004         if (name_table[i] != NULL)
1005         {
1006             free(name_table[i]);
1007         }
1008 
1009         if (type_table[i] != NULL)
1010         {
1011             free(type_table[i]);
1012         }
1013     }
1014 
1015     free(name_table);
1016     free(type_table);
1017     free(size_table);
1018     free(done);
1019 }
1020 
1021 /*****************************************************************************/
1022 
CheckSQLDataType(char * type,char * ref_type,const Promise * pp)1023 static bool CheckSQLDataType(char *type, char *ref_type, const Promise *pp)
1024 {
1025     static char *const aliases[3][2] =
1026         {
1027             {"varchar", "character@varying"},
1028             {"varchar", "character varying"},
1029             {NULL, NULL}
1030         };
1031 
1032     int i;
1033     bool ret = true;
1034 
1035     for (i = 0; aliases[i][0] != NULL; i++)
1036     {
1037         if ((strcmp(ref_type, aliases[i][0]) == 0) || (strcmp(ref_type, aliases[i][1]) == 0)
1038             || (strcmp(type, aliases[i][0]) == 0) || (strcmp(type, aliases[i][1]) == 0))
1039         {
1040             if ((strcmp(type, ref_type) != 0) && (strcmp(aliases[i][0], ref_type) != 0))
1041             {
1042                 Log(LOG_LEVEL_VERBOSE, "Promised column in database '%s' has a non-matching type (%s != %s)",
1043                       pp->promiser, ref_type, type);
1044                 ret = false;
1045             }
1046         }
1047         else
1048         {
1049             if (strcmp(type, ref_type) != 0)
1050             {
1051                 Log(LOG_LEVEL_VERBOSE, "Promised column in database '%s' has a non-matching type (%s != %s)",
1052                       pp->promiser, ref_type, type);
1053                 ret = false;
1054             }
1055         }
1056     }
1057 
1058     return ret;
1059 }
1060 
1061 /*****************************************************************************/
1062 
ListTables(int type,char * query)1063 static void ListTables(int type, char *query)
1064 {
1065     switch (type)
1066     {
1067     case DATABASE_TYPE_MYSQL:
1068         snprintf(query, CF_MAXVARSIZE - 1, "show tables");
1069         break;
1070 
1071     case DATABASE_TYPE_POSTGRES:
1072         /* This gibberish is the simplest thing I can find in postgres */
1073 
1074         snprintf(query, CF_MAXVARSIZE - 1,
1075                  "SELECT c.relname as \"Name\" FROM pg_catalog.pg_class c JOIN pg_catalog.pg_roles r ON r.oid = c.relowner LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public'");
1076         break;
1077 
1078     default:
1079         snprintf(query, CF_MAXVARSIZE, "NULL QUERY");
1080         break;
1081     }
1082 }
1083