changeset 44:576f4b1ec79f

scid: implement API authentication
author David Demelier <markand@malikania.fr>
date Thu, 11 Aug 2022 21:24:07 +0200
parents 6854efe15210
children c03305b39b10
files Makefile scid/db.c scid/db.h scid/http.c scid/page-jobresults.o scid/page-projects.c scid/pageutil.c scid/scid.c scid/scid.h scid/theme.c sql/property-get.sql sql/property-set.sql
diffstat 12 files changed, 332 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Thu Aug 11 11:34:32 2022 +0200
+++ b/Makefile	Thu Aug 11 21:24:07 2022 +0200
@@ -54,12 +54,14 @@
                         sql/job-list.sql \
                         sql/job-todo.sql \
                         sql/jobresult-add.sql \
+                        sql/jobresult-list-by-job-group.sql \
                         sql/jobresult-list-by-job.sql \
-                        sql/jobresult-list-by-job-group.sql \
                         sql/jobresult-list-by-worker.sql \
                         sql/project-find.sql \
                         sql/project-list.sql \
                         sql/project-save.sql \
+                        sql/property-get.sql \
+                        sql/property-set.sql \
                         sql/worker-find.sql \
                         sql/worker-list.sql \
                         sql/worker-save.sql
--- a/scid/db.c	Thu Aug 11 11:34:32 2022 +0200
+++ b/scid/db.c	Thu Aug 11 21:24:07 2022 +0200
@@ -33,12 +33,14 @@
 #include "sql/job-list.h"
 #include "sql/job-todo.h"
 #include "sql/jobresult-add.h"
+#include "sql/jobresult-list-by-job-group.h"
 #include "sql/jobresult-list-by-job.h"
-#include "sql/jobresult-list-by-job-group.h"
 #include "sql/jobresult-list-by-worker.h"
 #include "sql/project-find.h"
 #include "sql/project-list.h"
 #include "sql/project-save.h"
+#include "sql/property-get.h"
+#include "sql/property-set.h"
 #include "sql/worker-find.h"
 #include "sql/worker-list.h"
 #include "sql/worker-save.h"
@@ -185,7 +187,7 @@
 }
 
 static void
-bindva(sqlite3_stmt *stmt, const char *fmt, va_list ap)
+bind_params_va(sqlite3_stmt *stmt, const char *fmt, va_list ap)
 {
 	for (int index = 1; *fmt; ++fmt) {
 		switch (*fmt) {
@@ -207,6 +209,16 @@
 	}
 }
 
+static void
+bind_params(sqlite3_stmt *stmt, const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	bind_params_va(stmt, fmt, ap);
+	va_end(ap);
+}
+
 static int
 insert(int (*binder)(json_t *, sqlite3_stmt *), json_t *obj, const char *sql)
 {
@@ -242,7 +254,7 @@
 	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
 		return log_warn("db: %s", sqlite3_errmsg(db)), NULL;
 
-	bindva(stmt, args, ap);
+	bind_params_va(stmt, args, ap);
 	array = json_array();
 
 	while (sqlite3_step(stmt) == SQLITE_ROW)
@@ -292,6 +304,61 @@
 	return obj;
 }
 
+static int
+get_property(const char *which, char *out, size_t outsz)
+{
+	sqlite3_stmt *stmt;
+	const char *val;
+	int ret;
+
+	if (sqlite3_prepare(db, CHAR(sql_property_get), -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	bind_params(stmt, "s", which);
+
+	switch (sqlite3_step(stmt)) {
+	case SQLITE_ROW:
+		val = CHAR(sqlite3_column_text(stmt, 0));
+		util_strlcpy(out, val, outsz);
+		ret = 1;
+		break;
+	case SQLITE_DONE:
+		/* No data. */
+		ret = 0;
+		break;
+	default:
+		/* Error. */
+		log_warn("db: %s", sqlite3_errmsg(db));
+		ret = -1;
+		break;
+	}
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+static int
+set_property(const char *which, const char *val)
+{
+	sqlite3_stmt *stmt;
+	int ret = -1;
+
+	if (sqlite3_prepare(db, CHAR(sql_property_set), -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	bind_params(stmt, "ss", which, val);
+
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		log_warn("db: %s", sqlite3_errmsg(db));
+	else
+		ret = 0;
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
 int
 db_open(const char *path)
 {
@@ -310,6 +377,27 @@
 }
 
 int
+db_key_init(char *out, size_t outsz)
+{
+	assert(out);
+
+	const char table[] = "abcdefghijklmnopqrstuvwxyz1234567890";
+
+	for (size_t i = 0; i < outsz - 1; ++i)
+		out[i] = table[rand() % (sizeof (table) - 1)];
+
+	return set_property("api-key", out);
+}
+
+int
+db_key_get(char *out, size_t outsz)
+{
+	assert(out);
+
+	return get_property("api-key", out, outsz);
+}
+
+int
 db_job_add(json_t *job)
 {
 	assert(job);
--- a/scid/db.h	Thu Aug 11 11:34:32 2022 +0200
+++ b/scid/db.h	Thu Aug 11 21:24:07 2022 +0200
@@ -39,6 +39,11 @@
 #include <jansson.h>
 
 /**
+ * Maximum property value.
+ */
+#define DB_PROP_VALUE_MAX 256
+
+/**
  * Open database specified by path.
  *
  * \pre path != NULL
@@ -48,6 +53,28 @@
 db_open(const char *path);
 
 /**
+ * Reinitialize the API key in database and return it.
+ *
+ * \pre out != NULL
+ * \param out the destination key
+ * \param outsz the maximum size to write
+ * \return 0 on success or -1 on error
+ */
+int
+db_key_init(char *out, size_t outsz);
+
+/**
+ * Fetch the key from the database.
+ *
+ * \pre out != NULL
+ * \param out the destination key
+ * \param outsz the maximum size to write
+ * \return 0 if not found, 1 if found or -1 on error
+ */
+int
+db_key_get(char *out, size_t outsz);
+
+/**
  * Add a new job.
  *
  * \pre job != NULL
--- a/scid/http.c	Thu Aug 11 11:34:32 2022 +0200
+++ b/scid/http.c	Thu Aug 11 21:24:07 2022 +0200
@@ -37,6 +37,7 @@
 #include "page-jobresults.h"
 #include "page-static.h"
 #include "pageutil.h"
+#include "scid.h"
 
 enum page {
 	PAGE_INDEX,             /* Job results at index. */
@@ -46,6 +47,17 @@
 	PAGE_LAST               /* Not used. */
 };
 
+static int
+allowed(const struct kreq *req)
+{
+	for (size_t i = 0; i < req->reqsz; ++i)
+		if (strcmp(req->reqs[i].key, "X-Api-Key") == 0 &&
+		    strcmp(req->reqs[i].val, scid.apikey) == 0)
+			return 1;
+
+	return 0;
+}
+
 static void
 dispatch_api(struct kreq *req)
 {
@@ -61,11 +73,17 @@
 		{ NULL,                 NULL                    }
 	};
 
-	for (size_t i = 0; apis[i].prefix; ++i)
-		if (strncmp(req->path, apis[i].prefix, strlen(apis[i].prefix)) == 0)
-			return apis[i].handler(req);
+	/* Any API page requires authentication key. */
+	if (req->method == KMETHOD_POST && !allowed(req)) {
+		log_warn("http: client not allowed");
+		pageutil_status(req, KHTTP_401);
+	} else {
+		for (size_t i = 0; apis[i].prefix; ++i)
+			if (strncmp(req->path, apis[i].prefix, strlen(apis[i].prefix)) == 0)
+				return apis[i].handler(req);
 
-	pageutil_status(req, KHTTP_404);
+		pageutil_status(req, KHTTP_404);
+	}
 }
 
 static const char *pages[] = {
@@ -87,7 +105,7 @@
 {
 	assert(req);
 
-	log_debug("http: accessing page '%s'", req->fullpath);
+	log_debug("http: accessing page '%s' method %d", req->fullpath, req->method);
 
 	if (req->page == PAGE_LAST)
 		pageutil_status(req, KHTTP_404);
Binary file scid/page-jobresults.o has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-projects.c	Thu Aug 11 21:24:07 2022 +0200
@@ -0,0 +1,138 @@
+/*
+ * page-projects.c -- page /projects route
+ *
+ * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+
+#include "db.h"
+#include "pageutil.h"
+#include "scid.h"
+#include "theme.h"
+#include "util.h"
+
+static void
+set_job_status(json_t *project, json_t *job, json_t *jobresults)
+{
+	json_t *iter, *status;
+	int exitcode, sigcode;
+	size_t i, ns = 0, nf = 0;
+
+	json_array_foreach(jobresults, i, iter) {
+		json_unpack(iter, "{si si}", "exitcode", &exitcode, "sigcode", &sigcode);
+
+		if (exitcode == 0 && sigcode == 0)
+			ns++;
+		else
+			nf++;
+	}
+
+	if (nf)
+		status = json_string("failed");
+	else
+		status = json_string("success");
+
+	json_object_set_new(job, "status", status);
+	json_object_set_new(project, "n-failed", json_integer(nf));
+	json_object_set_new(project, "n-success", json_integer(ns));
+}
+
+static void
+set_project_jobs(json_t *project, json_t *jobs)
+{
+	json_t *iter, *jobresults;
+	json_int_t job_id;
+	size_t i;
+
+	json_array_foreach(jobs, i, iter) {
+		/* Don't populate too much. */
+		if (i + 1 >= 10)
+			break;
+
+		/*
+		 * For this job, find all jobresult to check how many have
+		 * failed or not.
+		 *
+		 * Also, since we have the project name, we can remove it.
+		 */
+		json_object_del(iter, "project_name");
+		json_unpack(iter, "{sI}", "id", &job_id);
+
+		if (!(jobresults = db_jobresult_list_by_job_group(job_id)))
+			continue;
+
+		set_job_status(project, iter, jobresults);
+		json_decref(jobresults);
+	}
+
+	json_object_set_new(project, "jobs", jobs);
+}
+
+/*
+ * For every projects, find their jobs and add them as 'jobs' property.
+ */
+static void
+update_projects(json_t *projects)
+{
+	json_t *jobs, *iter;
+	const char *name;
+	size_t i;
+
+	json_array_foreach(projects, i, iter) {
+		/* Script is not necessary at this point. */
+		json_object_del(iter, "script");
+		json_unpack(iter, "{ss}", "name", &name);
+
+		/* Find jobs for this project. */
+		if (!(jobs = db_job_list(name)))
+			continue;
+
+		set_project_jobs(iter, jobs);
+	}
+}
+
+static void
+get(struct kreq *req)
+{
+	json_t *projects, *root;
+	char *data;
+
+	/* First, fetch all projects. */
+	if ((projects = db_project_list())) {
+		root = json_pack("{so}", "projects", projects);
+		data = theme_page_index(root);
+		pageutil_render(req, KHTTP_200, KMIME_TEXT_HTML, data);
+		free(data);
+		json_decref(root);
+	} else
+		pageutil_status(req, KHTTP_500);
+}
+
+void
+page_projects(struct kreq *r)
+{
+	(void)r;
+
+	switch (r->method) {
+	case KMETHOD_GET:
+		get(r);
+		break;
+	default:
+		pageutil_status(r, KHTTP_400);
+		break;
+	}
+}
--- a/scid/pageutil.c	Thu Aug 11 11:34:32 2022 +0200
+++ b/scid/pageutil.c	Thu Aug 11 21:24:07 2022 +0200
@@ -44,6 +44,7 @@
 static const int statustab[] = {
 	[KHTTP_200] = 200,
 	[KHTTP_400] = 400,
+	[KHTTP_401] = 401,
 	[KHTTP_404] = 404,
 	[KHTTP_500] = 500
 };
@@ -51,6 +52,7 @@
 static const char * const statusmsg[] = {
 	[KHTTP_200] = "OK",
 	[KHTTP_400] = "Bad Request",
+	[KHTTP_401] = "Unauthorized",
 	[KHTTP_404] = "Not Found",
 	[KHTTP_500] = "Internal Server Error"
 };
--- a/scid/scid.c	Thu Aug 11 11:34:32 2022 +0200
+++ b/scid/scid.c	Thu Aug 11 21:24:07 2022 +0200
@@ -28,16 +28,43 @@
 	.dbpath = VARDIR "/db/sci/sci.db"
 };
 
-void
-scid_init(void)
+static void
+init_misc(void)
 {
 	log_open("scid: version " VERSION);
 	log_info("scid: opening database %s", scid.dbpath);
 
+	theme_open(scid.themedir);
+}
+
+static void
+init_database(void)
+{
 	if (db_open(scid.dbpath) < 0)
-		log_die("scid: abort: unable to open database");
+		log_die("scid: unable to open database");
+
+	/* Retrieve or create the API key if not existing. */
+	switch (db_key_get(scid.apikey, sizeof (scid.apikey))) {
+	case 0:
+		/* Not found, create immediately. */
+		if (db_key_init(scid.apikey, sizeof (scid.apikey)) < 0)
+			log_die("scid: unable to insert key");
 
-	theme_open(scid.themedir);
+		break;
+	case -1:
+		log_die("scid: unable to retrieve API key in database");
+		break;
+	default:
+		/* We already fetched it. */
+		break;
+	}
+}
+
+void
+scid_init(void)
+{
+	init_misc();
+	init_database();
 }
 
 void
--- a/scid/scid.h	Thu Aug 11 11:34:32 2022 +0200
+++ b/scid/scid.h	Thu Aug 11 21:24:07 2022 +0200
@@ -28,6 +28,8 @@
 
 #include <limits.h>
 
+#define SCID_API_KEY_MAX (40 + 1)
+
 /**
  * \struct scid
  * \brief Main scid structure for configuration.
@@ -35,6 +37,7 @@
 extern struct scid {
 	char themedir[PATH_MAX];        /*!< Path to the theme. */
 	char dbpath[PATH_MAX];          /*!< Path to the database file. */
+	char apikey[SCID_API_KEY_MAX];  /*!< Maximum API key with NUL terminator. */
 } scid /*! Global variable. */;
 
 /**
--- a/scid/theme.c	Thu Aug 11 11:34:32 2022 +0200
+++ b/scid/theme.c	Thu Aug 11 21:24:07 2022 +0200
@@ -437,7 +437,10 @@
 	json_t *doc;
 	char *ret;
 
-	doc = util_json_pack("{si ss}", "status", status, "message", message);
+	doc = util_json_pack("{si ss}",
+		"status",  status,
+		"message", message
+	);
 	ret = call(doc, "onPageStatus");
 
 	json_decref(doc);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/property-get.sql	Thu Aug 11 21:24:07 2022 +0200
@@ -0,0 +1,3 @@
+SELECT `value`
+  FROM `property`
+ WHERE `key` = ?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/property-set.sql	Thu Aug 11 21:24:07 2022 +0200
@@ -0,0 +1,7 @@
+REPLACE
+   INTO `property`
+      (
+        `key`,
+        `value`
+      )
+ VALUES (?, ?)