--- /dev/null
+/*
+ * Sub-authentication Daemon: Authentication database
+ *
+ * (c) 2017 Martin Mares <mj@ucw.cz>
+ */
+
+#include <ucw/lib.h>
+#include <ucw/gary.h>
+#include <ucw/stkstring.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "subauthd.h"
+
+#define HASH_NODE struct auth_user
+#define HASH_PREFIX(x) user_hash_##x
+#define HASH_KEY_ENDSTRING login
+//#define HASH_WANT_FIND
+#define HASH_WANT_LOOKUP
+// #define HASH_WANT_REMOVE
+#define HASH_GIVE_INIT_DATA
+
+static void user_hash_init_data(struct auth_user *au)
+{
+ clist_init(&au->accounts);
+}
+
+static struct auth_zone *find_zone_by_name(const char *name)
+{
+ CLIST_FOR_EACH(struct auth_zone *, z, zone_list)
+ if (!strcmp(z->name, name))
+ return z;
+ return NULL;
+}
+
+#include <ucw/hashtable.h>
+
+static void NONRET db_corrupted(const char *reason)
+{
+ die("Database file corrupted: %s", reason);
+}
+
+static void db_parse_user(struct json_node *ju)
+{
+ if (ju->type != JSON_OBJECT)
+ db_corrupted("Expected object");
+
+ const char *login = get_string(ju, "l");
+ if (!login)
+ db_corrupted("User has no login");
+
+ struct json_node **jas = get_array(ju, "a");
+ if (!jas)
+ db_corrupted("User has no accounts");
+
+ struct auth_user *au = user_hash_lookup((char *) login);
+ if (clist_head(&au->accounts))
+ db_corrupted("Multiple users with the same login");
+
+ for (uint i=0; i < GARY_SIZE(jas); i++)
+ {
+ struct json_node *ja = jas[i];
+
+ const char *zone_name = get_string(ja, "z");
+ if (!zone_name)
+ db_corrupted("Account has no zone");
+
+ struct auth_acct *aa = xmalloc_zero(sizeof(*aa));
+ clist_add_tail(&au->accounts, &aa->n);
+ aa->zone = find_zone_by_name(zone_name);
+ if (!aa->zone)
+ die("Database defines accounts in zone %s, which is not configured", zone_name);
+ clist_init(&aa->tokens);
+
+ struct json_node **jts = get_array(ja, "t");
+ if (jts)
+ {
+ for (uint j=0; j < GARY_SIZE(jts); j++)
+ {
+ struct json_node *jt = jts[j];
+ if (jt->type != JSON_OBJECT)
+ db_corrupted("Token is not an object");
+
+ uint type;
+ if (!get_uint(jt, "t", &type) || type >= TOKEN_NUM_TYPES)
+ db_corrupted("Token has no valid type");
+
+ const char *salt = get_string(jt, "s");
+ const char *hash = get_string(jt, "h");
+ const char *cmt = get_string(jt, "c");
+ if (!salt || !hash)
+ db_corrupted("Token must have salt and hash");
+
+ uint lastmod;
+ if (!get_uint(jt, "m", &lastmod))
+ db_corrupted("Token has invalid last modified time");
+
+ struct auth_token *at = xmalloc(sizeof(*at));
+ clist_add_tail(&aa->tokens, &at->n);
+ at->type = type;
+ at->salt = xstrdup(salt);
+ at->hash = xstrdup(hash);
+ at->comment = xstrdup(cmt);
+ at->last_modified = lastmod;
+ }
+ }
+ }
+}
+
+static void db_read(void)
+{
+ struct fastbuf *fb = bopen_try(database_name, O_RDONLY, 65536);
+ if (!fb)
+ {
+ msg(L_WARN, "No database file found, starting with no users");
+ return;
+ }
+
+ struct json_context *js = json_new();
+ json_set_input(js, fb);
+
+ struct json_node *sep = json_next_token(js);
+ if (sep->type != JSON_BEGIN_ARRAY)
+ db_corrupted("Does not start with '['");
+
+ for (;;)
+ {
+ json_push(js);
+
+ sep = json_peek_token(js);
+ if (sep->type == JSON_END_ARRAY)
+ {
+ json_next_token(js);
+ json_pop(js);
+ break;
+ }
+ else if (sep->type == JSON_VALUE_SEP)
+ json_next_token(js);
+
+ struct json_node *ju = json_next_value(js);
+ db_parse_user(ju);
+ json_pop(js);
+ }
+
+ sep = json_next_token(js);
+ if (sep->type != JSON_EOF)
+ db_corrupted("Garbage at the end");
+
+ json_delete(js);
+ bclose(fb);
+}
+
+void db_write(void)
+{
+ char *temp_name = stk_strcat(database_name, ".new");
+ struct fastbuf *fb = bopen(temp_name, O_WRONLY | O_CREAT | O_TRUNC, 65536);
+ struct json_context *js = json_new();
+ json_set_output(js, fb);
+ bputs(fb, "[\n");
+ uint cnt = 0;
+
+ HASH_FOR_ALL(user_hash, au)
+ {
+ json_push(js);
+ if (cnt)
+ bputs(fb, ",\n");
+ struct json_node *ju = json_new_object(js);
+ json_object_set(ju, "l", json_new_string_ref(js, au->login));
+ struct json_node *jas = json_new_array(js);
+ json_object_set(ju, "a", jas);
+ CLIST_FOR_EACH(struct auth_acct *, aa, au->accounts)
+ {
+ struct json_node *ja = json_new_object(js);
+ json_array_append(jas, ja);
+ json_object_set(ja, "z", json_new_string_ref(js, aa->zone->name));
+ struct json_node *jts = json_new_array(js);
+ CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
+ {
+ struct json_node *jt = json_new_object(js);
+ json_array_append(jts, jt);
+ json_object_set(jt, "t", json_new_number(js, at->type));
+ if (at->salt)
+ json_object_set(jt, "s", json_new_string_ref(js, at->salt));
+ if (at->hash)
+ json_object_set(jt, "h", json_new_string_ref(js, at->hash));
+ if (at->comment)
+ json_object_set(jt, "c", json_new_string_ref(js, at->comment));
+ json_object_set(jt, "m", json_new_number(js, at->last_modified));
+ }
+ if (GARY_SIZE(jts->elements))
+ json_object_set(ja, "t", jts);
+ }
+ json_write_value(js, ju);
+ json_pop(js);
+ cnt++;
+ }
+ HASH_END_FOR;
+
+ bputs(fb, "\n]\n");
+ json_delete(js);
+ bclose(fb);
+
+ char *old_name = stk_strcat(database_name, ".old");
+ if (rename(database_name, old_name) < 0 && errno != ENOENT)
+ die("Cannot rename %s to %s: %m", database_name, old_name);
+
+ if (rename(temp_name, database_name) < 0)
+ die("Cannot rename %s to %s: %m", temp_name, database_name);
+}
+
+void auth_init(void)
+{
+ user_hash_init();
+ db_read();
+}
#include "subauthd.h"
+// Configuration
static char *socket_path = "subauthd.socket";
static uint max_connections = ~0U;
+clist zone_list;
+char *database_name = "subauthd.db";
static struct main_file listen_socket;
static uint num_connections;
msg(L_INFO, "Listening on %s", socket_path);
}
+static char *zone_commit(void *z_)
+{
+ struct auth_zone *z = z_;
+ if (!z->name)
+ return "A zone must have a name";
+ return NULL;
+}
+
+static struct cf_section zone_config = {
+ CF_TYPE(struct auth_zone),
+ CF_COMMIT(zone_commit),
+ CF_ITEMS {
+ CF_STRING("Name", PTR_TO(struct auth_zone, name)),
+ CF_UINT("AutoCreateAcct", PTR_TO(struct auth_zone, auto_create_acct)),
+ CF_UINT("AllowPasswd", PTR_TO(struct auth_zone, allow_passwd)),
+ CF_UINT("AllowTokens", PTR_TO(struct auth_zone, allow_tokens)),
+ CF_END
+ }
+};
+
static struct cf_section daemon_config = {
CF_ITEMS {
CF_STRING("SocketPath", &socket_path),
CF_UINT("MaxConnections", &max_connections),
+ CF_LIST("Zone", &zone_list, &zone_config),
+ CF_STRING("Database", &database_name),
CF_END
}
};
cf_declare_section("SubauthD", &daemon_config, 0);
opt_parse(&options, argv+1);
+ auth_init();
main_init();
init_socket();