Mercurial > sci
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);
--- /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);