diff options
Diffstat (limited to 'src/web/api/http_auth.c')
-rw-r--r-- | src/web/api/http_auth.c | 338 |
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) { |