summaryrefslogtreecommitdiffstats
path: root/src/web/api/http_auth.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/web/api/http_auth.c')
-rw-r--r--src/web/api/http_auth.c338
1 files changed, 298 insertions, 40 deletions
diff --git a/src/web/api/http_auth.c b/src/web/api/http_auth.c
index ec0520304..5c4fffcaf 100644
--- a/src/web/api/http_auth.c
+++ b/src/web/api/http_auth.c
@@ -2,83 +2,341 @@
#include "http_auth.h"
-#define BEARER_TOKEN_EXPIRATION 86400
+#define BEARER_TOKEN_EXPIRATION (86400 * 1)
-bool netdata_is_protected_by_bearer = false; // this is controlled by cloud, at the point the agent logs in - this should also be saved to /var/lib/netdata
+bool netdata_is_protected_by_bearer = false;
static DICTIONARY *netdata_authorized_bearers = NULL;
struct bearer_token {
nd_uuid_t cloud_account_id;
- char cloud_user_name[CLOUD_USER_NAME_LENGTH];
+ char client_name[CLOUD_CLIENT_NAME_LENGTH];
HTTP_ACCESS access;
HTTP_USER_ROLE user_role;
time_t created_s;
time_t expires_s;
};
-bool web_client_bearer_token_auth(struct web_client *w, const char *v) {
- if(!uuid_parse_flexi(v, w->auth.bearer_token)) {
- char uuid_str[UUID_COMPACT_STR_LEN];
- uuid_unparse_lower_compact(w->auth.bearer_token, uuid_str);
+static void bearer_tokens_path(char out[FILENAME_MAX]) {
+ filename_from_path_entry(out, netdata_configured_varlib_dir, "bearer_tokens", NULL);
+}
- struct bearer_token *z = dictionary_get(netdata_authorized_bearers, uuid_str);
- if (z && z->expires_s > now_monotonic_sec()) {
- strncpyz(w->auth.client_name, z->cloud_user_name, sizeof(w->auth.client_name) - 1);
- uuid_copy(w->auth.cloud_account_id, z->cloud_account_id);
- web_client_set_permissions(w, z->access, z->user_role, WEB_CLIENT_FLAG_AUTH_BEARER);
- return true;
- }
- }
- else
- nd_log(NDLS_DAEMON, NDLP_NOTICE, "Invalid bearer token '%s' received.", v);
+static void bearer_token_filename(char out[FILENAME_MAX], nd_uuid_t uuid) {
+ char uuid_str[UUID_STR_LEN];
+ uuid_unparse_lower(uuid, uuid_str);
+
+ char path[FILENAME_MAX];
+ bearer_tokens_path(path);
+ filename_from_path_entry(out, path, uuid_str, NULL);
+}
- return false;
+static inline bool bearer_tokens_ensure_path_exists(void) {
+ char path[FILENAME_MAX];
+ bearer_tokens_path(path);
+ return filename_is_dir(path, true);
}
-static void bearer_token_cleanup(void) {
+static void bearer_token_delete_from_disk(nd_uuid_t *token) {
+ char filename[FILENAME_MAX];
+ bearer_token_filename(filename, *token);
+ if(unlink(filename) != 0)
+ nd_log(NDLS_DAEMON, NDLP_ERR, "Failed to unlink() file '%s'", filename);
+}
+
+static void bearer_token_cleanup(bool force) {
static time_t attempts = 0;
- if(++attempts % 1000 != 0)
+ if(++attempts % 1000 != 0 && !force)
return;
- time_t now_s = now_monotonic_sec();
+ time_t now_s = now_realtime_sec();
struct bearer_token *z;
dfe_start_read(netdata_authorized_bearers, z) {
- if(z->expires_s < now_s)
+ if(z->expires_s < now_s) {
+ nd_uuid_t uuid;
+ if(uuid_parse_flexi(z_dfe.name, uuid) == 0)
+ bearer_token_delete_from_disk(&uuid);
+
dictionary_del(netdata_authorized_bearers, z_dfe.name);
+ }
}
dfe_done(z);
dictionary_garbage_collect(netdata_authorized_bearers);
}
-void bearer_tokens_init(void) {
- netdata_authorized_bearers = dictionary_create_advanced(
- DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
- NULL, sizeof(struct bearer_token));
+static uint64_t bearer_token_signature(nd_uuid_t token, struct bearer_token *bt) {
+ // we use a custom structure to make sure that changes in the other code will not affect the signature
+
+ struct {
+ nd_uuid_t host_uuid;
+ nd_uuid_t token;
+ nd_uuid_t cloud_account_id;
+ char client_name[CLOUD_CLIENT_NAME_LENGTH];
+ HTTP_ACCESS access;
+ HTTP_USER_ROLE user_role;
+ time_t created_s;
+ time_t expires_s;
+ } signature_payload = {
+ .access = bt->access,
+ .user_role = bt->user_role,
+ .created_s = bt->created_s,
+ .expires_s = bt->expires_s,
+ };
+ uuid_copy(signature_payload.host_uuid, localhost->host_id.uuid);
+ uuid_copy(signature_payload.token, token);
+ uuid_copy(signature_payload.cloud_account_id, bt->cloud_account_id);
+ memset(signature_payload.client_name, 0, sizeof(signature_payload.client_name));
+ strncpyz(signature_payload.client_name, bt->client_name, sizeof(signature_payload.client_name) - 1);
+
+ return XXH3_64bits(&signature_payload, sizeof(signature_payload));
}
-time_t bearer_create_token(nd_uuid_t *uuid, struct web_client *w) {
+static bool bearer_token_save_to_file(nd_uuid_t token, struct bearer_token *bt) {
+ CLEAN_BUFFER *wb = buffer_create(0, NULL);
+ buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
+ buffer_json_member_add_uint64(wb, "version", 1);
+ buffer_json_member_add_uuid(wb, "host_uuid", localhost->host_id.uuid);
+ buffer_json_member_add_uuid(wb, "token", token);
+ buffer_json_member_add_uuid(wb, "cloud_account_id", bt->cloud_account_id);
+ buffer_json_member_add_string(wb, "client_name", bt->client_name);
+ http_access2buffer_json_array(wb, "access", bt->access);
+ buffer_json_member_add_string(wb, "user_role", http_id2user_role(bt->user_role));
+ buffer_json_member_add_uint64(wb, "created_s", bt->created_s);
+ buffer_json_member_add_uint64(wb, "expires_s", bt->expires_s);
+ buffer_json_member_add_uint64(wb, "signature", bearer_token_signature(token, bt));
+ buffer_json_finalize(wb);
+
+ char filename[FILENAME_MAX];
+ bearer_token_filename(filename, token);
+
+ FILE *fp = fopen(filename, "w");
+ if(!fp) {
+ nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot create file '%s'", filename);
+ return false;
+ }
+
+ if(fwrite(buffer_tostring(wb), 1, buffer_strlen(wb), fp) != buffer_strlen(wb)) {
+ fclose(fp);
+ unlink(filename);
+ nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot save file '%s'", filename);
+ return false;
+ }
+
+ fclose(fp);
+ return true;
+}
+
+static time_t bearer_create_token_internal(nd_uuid_t token, HTTP_USER_ROLE user_role, HTTP_ACCESS access, nd_uuid_t cloud_account_id, const char *client_name, time_t created_s, time_t expires_s, bool save) {
char uuid_str[UUID_COMPACT_STR_LEN];
+ uuid_unparse_lower_compact(token, uuid_str);
+
+ struct bearer_token t = { 0 }, *bt;
+ const DICTIONARY_ITEM *item = dictionary_set_and_acquire_item(netdata_authorized_bearers, uuid_str, &t, sizeof(t));
+ bt = dictionary_acquired_item_value(item);
+
+ if(!bt->created_s) {
+ bt->created_s = created_s;
+ bt->expires_s = expires_s;
+ bt->user_role = user_role;
+ bt->access = access;
+
+ uuid_copy(bt->cloud_account_id, cloud_account_id);
+ strncpyz(bt->client_name, client_name, sizeof(bt->cloud_account_id) - 1);
+
+ if(save)
+ bearer_token_save_to_file(token, bt);
+ }
+
+ time_t expiration = bt->expires_s;
+
+ dictionary_acquired_item_release(netdata_authorized_bearers, item);
+
+ return expiration;
+}
+
+time_t bearer_create_token(nd_uuid_t *uuid, HTTP_USER_ROLE user_role, HTTP_ACCESS access, nd_uuid_t cloud_account_id, const char *client_name) {
+ time_t now_s = now_realtime_sec();
+ time_t expires_s = 0;
+
+ struct bearer_token *bt;
+ dfe_start_read(netdata_authorized_bearers, bt) {
+ if(bt->expires_s > now_s + 3600 * 2 && // expires in more than 2 hours
+ user_role == bt->user_role && // the user_role matches
+ access == bt->access && // the access matches
+ uuid_eq(cloud_account_id, bt->cloud_account_id) && // the cloud_account_id matches
+ strncmp(client_name, bt->client_name, sizeof(bt->client_name) - 1) == 0 && // the client_name matches
+ uuid_parse_flexi(bt_dfe.name, *uuid) == 0) // the token can be parsed
+ return expires_s; /* dfe will cleanup automatically */
+ }
+ dfe_done(bt);
uuid_generate_random(*uuid);
- uuid_unparse_lower_compact(*uuid, uuid_str);
-
- struct bearer_token t = { 0 }, *z;
- z = dictionary_set(netdata_authorized_bearers, uuid_str, &t, sizeof(t));
- if(!z->created_s) {
- z->created_s = now_monotonic_sec();
- z->expires_s = z->created_s + BEARER_TOKEN_EXPIRATION;
- z->user_role = w->user_role;
- z->access = w->access;
- uuid_copy(z->cloud_account_id, w->auth.cloud_account_id);
- strncpyz(z->cloud_user_name, w->auth.client_name, sizeof(z->cloud_account_id) - 1);
+ expires_s = bearer_create_token_internal(
+ *uuid, user_role, access, cloud_account_id, client_name,
+ now_s, now_s + BEARER_TOKEN_EXPIRATION, true);
+
+ bearer_token_cleanup(false);
+
+ return expires_s;
+}
+
+static bool bearer_token_parse_json(nd_uuid_t token, struct json_object *jobj, BUFFER *error) {
+ int64_t version;
+ nd_uuid_t token_in_file, cloud_account_id, host_uuid;
+ CLEAN_STRING *client_name = NULL;
+ HTTP_USER_ROLE user_role = HTTP_USER_ROLE_NONE;
+ HTTP_ACCESS access = HTTP_ACCESS_NONE;
+ time_t created_s = 0, expires_s = 0;
+ uint64_t signature = 0;
+
+ JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, ".", "version", version, error, true);
+ JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "host_uuid", host_uuid, error, true);
+ JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "token", token_in_file, error, true);
+ JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "cloud_account_id", cloud_account_id, error, true);
+ JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, ".", "client_name", client_name, error, true);
+ JSONC_PARSE_ARRAY_OF_TXT2BITMAP_OR_ERROR_AND_RETURN(jobj, ".", "access", http_access2id_one, access, error, true);
+ JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, ".", "user_role", http_user_role2id, user_role, error, true);
+ JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "created_s", created_s, error, true);
+ JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "expires_s", expires_s, error, true);
+ JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "signature", signature, error, true);
+
+ if(uuid_compare(token, token_in_file) != 0) {
+ buffer_flush(error);
+ buffer_strcat(error, "token in JSON file does not match the filename");
+ return false;
+ }
+
+ if(uuid_compare(host_uuid, localhost->host_id.uuid) != 0) {
+ buffer_flush(error);
+ buffer_strcat(error, "Host UUID in JSON file does not match our host UUID");
+ return false;
+ }
+
+ if(!created_s || !expires_s || created_s >= expires_s) {
+ buffer_flush(error);
+ buffer_strcat(error, "bearer token has invalid dates");
+ return false;
+ }
+
+ struct bearer_token bt = {
+ .access = access,
+ .user_role = user_role,
+ .created_s = created_s,
+ .expires_s = expires_s,
+ };
+ uuid_copy(bt.cloud_account_id, cloud_account_id);
+ strncpyz(bt.client_name, string2str(client_name), sizeof(bt.client_name) - 1);
+
+ if(signature != bearer_token_signature(token_in_file, &bt)) {
+ buffer_flush(error);
+ buffer_strcat(error, "bearer token has invalid signature");
+ return false;
+ }
+
+ bearer_create_token_internal(token, user_role, access,
+ cloud_account_id, string2str(client_name),
+ created_s, expires_s, false);
+
+ return true;
+}
+
+static bool bearer_token_load_token(nd_uuid_t token) {
+ char filename[FILENAME_MAX];
+ bearer_token_filename(filename, token);
+
+ CLEAN_BUFFER *wb = buffer_create(0, NULL);
+ if(!read_txt_file_to_buffer(filename, wb, 1 * 1024 * 1024))
+ return false;
+
+ CLEAN_JSON_OBJECT *jobj = json_tokener_parse(buffer_tostring(wb));
+ if (jobj == NULL) {
+ nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot parse bearer token file '%s'", filename);
+ return false;
+ }
+
+ CLEAN_BUFFER *error = buffer_create(0, NULL);
+ bool rc = bearer_token_parse_json(token, jobj, error);
+ if(!rc) {
+ nd_log(NDLS_DAEMON, NDLP_ERR, "Failed to parse bearer token file '%s': %s", filename, buffer_tostring(error));
+ unlink(filename);
+ return false;
+ }
+
+ bearer_token_cleanup(true);
+
+ return true;
+}
+
+static void bearer_tokens_load_from_disk(void) {
+ bearer_tokens_ensure_path_exists();
+
+ char path[FILENAME_MAX];
+ bearer_tokens_path(path);
+
+ DIR *dir = opendir(path);
+ if(!dir) {
+ nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot open directory '%s' to read saved bearer tokens", path);
+ return;
}
- bearer_token_cleanup();
+ struct dirent *de;
+ while((de = readdir(dir))) {
+ if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
+ continue;
+
+ ND_UUID uuid = UUID_ZERO;
+ if(uuid_parse_flexi(de->d_name, uuid.uuid) != 0 || UUIDiszero(uuid))
+ continue;
+
+ char filename[FILENAME_MAX];
+ filename_from_path_entry(filename, path, de->d_name, NULL);
+
+ if(de->d_type == DT_REG || (de->d_type == DT_LNK && filename_is_file(filename)))
+ bearer_token_load_token(uuid.uuid);
+ }
+
+ closedir(dir);
+}
+
+bool web_client_bearer_token_auth(struct web_client *w, const char *v) {
+ bool rc = false;
+
+ if(!uuid_parse_flexi(v, w->auth.bearer_token)) {
+ char uuid_str[UUID_COMPACT_STR_LEN];
+ uuid_unparse_lower_compact(w->auth.bearer_token, uuid_str);
+
+ const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(netdata_authorized_bearers, uuid_str);
+ if(!item && bearer_token_load_token(w->auth.bearer_token))
+ item = dictionary_get_and_acquire_item(netdata_authorized_bearers, uuid_str);
+
+ if(item) {
+ struct bearer_token *bt = dictionary_acquired_item_value(item);
+ if (bt->expires_s > now_realtime_sec()) {
+ strncpyz(w->auth.client_name, bt->client_name, sizeof(w->auth.client_name) - 1);
+ uuid_copy(w->auth.cloud_account_id, bt->cloud_account_id);
+ web_client_set_permissions(w, bt->access, bt->user_role, WEB_CLIENT_FLAG_AUTH_BEARER);
+ rc = true;
+ }
+
+ dictionary_acquired_item_release(netdata_authorized_bearers, item);
+ }
+ }
+ else
+ nd_log(NDLS_DAEMON, NDLP_NOTICE, "Invalid bearer token '%s' received.", v);
+
+ return rc;
+}
+
+void bearer_tokens_init(void) {
+ netdata_is_protected_by_bearer =
+ config_get_boolean(CONFIG_SECTION_WEB, "bearer token protection", netdata_is_protected_by_bearer);
+
+ netdata_authorized_bearers = dictionary_create_advanced(
+ DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
+ NULL, sizeof(struct bearer_token));
- return now_realtime_sec() + BEARER_TOKEN_EXPIRATION;
+ bearer_tokens_load_from_disk();
}
bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len) {