master
c 365 lines 13.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "http_auth.h"
4 #include "web/api/mcp_auth.h"
5
6 #define BEARER_TOKEN_EXPIRATION (86400 * 1)
7
8 bool netdata_is_protected_by_bearer = false;
9 static DICTIONARY *netdata_authorized_bearers = NULL;
10
11 struct bearer_token {
12 nd_uuid_t cloud_account_id;
13 char client_name[CLOUD_CLIENT_NAME_LENGTH];
14 HTTP_ACCESS access;
15 HTTP_USER_ROLE user_role;
16 time_t created_s;
17 time_t expires_s;
18 };
19
20 static void bearer_tokens_path(char out[FILENAME_MAX]) {
21 filename_from_path_entry(out, netdata_configured_varlib_dir, "bearer_tokens", NULL);
22 }
23
24 static void bearer_token_filename(char out[FILENAME_MAX], nd_uuid_t uuid) {
25 char uuid_str[UUID_STR_LEN];
26 uuid_unparse_lower(uuid, uuid_str);
27
28 char path[FILENAME_MAX];
29 bearer_tokens_path(path);
30 filename_from_path_entry(out, path, uuid_str, NULL);
31 }
32
33 static inline bool bearer_tokens_ensure_path_exists(void) {
34 char path[FILENAME_MAX];
35 bearer_tokens_path(path);
36 return filename_is_dir(path, true);
37 }
38
39 static void bearer_token_delete_from_disk(nd_uuid_t *token) {
40 char filename[FILENAME_MAX];
41 bearer_token_filename(filename, *token);
42 if(unlink(filename) != 0)
43 nd_log(NDLS_DAEMON, NDLP_ERR, "Failed to unlink() file '%s'", filename);
44 }
45
46 static void bearer_token_cleanup(bool force) {
47 static time_t attempts = 0;
48
49 if(++attempts % 1000 != 0 && !force)
50 return;
51
52 time_t now_s = now_realtime_sec();
53
54 struct bearer_token *z;
55 dfe_start_read(netdata_authorized_bearers, z) {
56 if(z->expires_s < now_s) {
57 nd_uuid_t uuid;
58 if(uuid_parse_flexi(z_dfe.name, uuid) == 0)
59 bearer_token_delete_from_disk(&uuid);
60
61 dictionary_del(netdata_authorized_bearers, z_dfe.name);
62 }
63 }
64 dfe_done(z);
65
66 dictionary_garbage_collect(netdata_authorized_bearers);
67 }
68
69 static uint64_t bearer_token_signature(nd_uuid_t token, struct bearer_token *bt) {
70 // we use a custom structure to make sure that changes in the other code will not affect the signature
71
72 struct {
73 nd_uuid_t host_uuid;
74 nd_uuid_t token;
75 nd_uuid_t cloud_account_id;
76 char client_name[CLOUD_CLIENT_NAME_LENGTH];
77 HTTP_ACCESS access;
78 HTTP_USER_ROLE user_role;
79 time_t created_s;
80 time_t expires_s;
81 } signature_payload = {
82 .access = bt->access,
83 .user_role = bt->user_role,
84 .created_s = bt->created_s,
85 .expires_s = bt->expires_s,
86 };
87 uuid_copy(signature_payload.host_uuid, localhost->host_id.uuid);
88 uuid_copy(signature_payload.token, token);
89 uuid_copy(signature_payload.cloud_account_id, bt->cloud_account_id);
90 memset(signature_payload.client_name, 0, sizeof(signature_payload.client_name));
91 strncpyz(signature_payload.client_name, bt->client_name, sizeof(signature_payload.client_name) - 1);
92
93 return XXH3_64bits(&signature_payload, sizeof(signature_payload));
94 }
95
96 static bool bearer_token_save_to_file(nd_uuid_t token, struct bearer_token *bt) {
97 CLEAN_BUFFER *wb = buffer_create(0, NULL);
98 buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
99 buffer_json_member_add_uint64(wb, "version", 1);
100 buffer_json_member_add_uuid(wb, "host_uuid", localhost->host_id.uuid);
101 buffer_json_member_add_uuid(wb, "token", token);
102 buffer_json_member_add_uuid(wb, "cloud_account_id", bt->cloud_account_id);
103 buffer_json_member_add_string(wb, "client_name", bt->client_name);
104 http_access2buffer_json_array(wb, "access", bt->access);
105 buffer_json_member_add_string(wb, "user_role", http_id2user_role(bt->user_role));
106 buffer_json_member_add_uint64(wb, "created_s", bt->created_s);
107 buffer_json_member_add_uint64(wb, "expires_s", bt->expires_s);
108 buffer_json_member_add_uint64(wb, "signature", bearer_token_signature(token, bt));
109 buffer_json_finalize(wb);
110
111 char filename[FILENAME_MAX];
112 bearer_token_filename(filename, token);
113
114 FILE *fp = fopen(filename, "w");
115 if(!fp) {
116 nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot create file '%s'", filename);
117 return false;
118 }
119
120 if(fwrite(buffer_tostring(wb), 1, buffer_strlen(wb), fp) != buffer_strlen(wb)) {
121 fclose(fp);
122 unlink(filename);
123 nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot save file '%s'", filename);
124 return false;
125 }
126
127 fclose(fp);
128 return true;
129 }
130
131 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) {
132 char uuid_str[UUID_COMPACT_STR_LEN];
133 uuid_unparse_lower_compact(token, uuid_str);
134
135 struct bearer_token t = { 0 }, *bt;
136 const DICTIONARY_ITEM *item = dictionary_set_and_acquire_item(netdata_authorized_bearers, uuid_str, &t, sizeof(t));
137 bt = dictionary_acquired_item_value(item);
138
139 if(!bt->created_s) {
140 bt->created_s = created_s;
141 bt->expires_s = expires_s;
142 bt->user_role = user_role;
143 bt->access = access;
144
145 uuid_copy(bt->cloud_account_id, cloud_account_id);
146 strncpyz(bt->client_name, client_name, sizeof(bt->client_name) - 1);
147
148 if(save)
149 bearer_token_save_to_file(token, bt);
150 }
151
152 time_t expiration = bt->expires_s;
153
154 dictionary_acquired_item_release(netdata_authorized_bearers, item);
155
156 return expiration;
157 }
158
159 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) {
160 time_t now_s = now_realtime_sec();
161 time_t expires_s = 0;
162
163 struct bearer_token *bt;
164 dfe_start_read(netdata_authorized_bearers, bt) {
165 if(bt->expires_s > now_s + 3600 * 2 && // expires in more than 2 hours
166 user_role == bt->user_role && // the user_role matches
167 access == bt->access && // the access matches
168 uuid_eq(cloud_account_id, bt->cloud_account_id) && // the cloud_account_id matches
169 strncmp(client_name, bt->client_name, sizeof(bt->client_name) - 1) == 0 && // the client_name matches
170 uuid_parse_flexi(bt_dfe.name, *uuid) == 0) // the token can be parsed
171 return bt->expires_s; /* dfe will cleanup automatically */
172 }
173 dfe_done(bt);
174
175 uuid_generate_random(*uuid);
176 expires_s = bearer_create_token_internal(
177 *uuid, user_role, access, cloud_account_id, client_name,
178 now_s, now_s + BEARER_TOKEN_EXPIRATION, true);
179
180 bearer_token_cleanup(false);
181
182 return expires_s;
183 }
184
185 static bool bearer_token_parse_json(nd_uuid_t token, struct json_object *jobj, BUFFER *error) {
186 int64_t version;
187 nd_uuid_t token_in_file, cloud_account_id, host_uuid;
188 CLEAN_STRING *client_name = NULL;
189 HTTP_USER_ROLE user_role = HTTP_USER_ROLE_NONE;
190 HTTP_ACCESS access = HTTP_ACCESS_NONE;
191 time_t created_s = 0, expires_s = 0;
192 uint64_t signature = 0;
193
194 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, ".", "version", version, error, JSONC_REQUIRED);
195 JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "host_uuid", host_uuid, error, JSONC_REQUIRED);
196 JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "token", token_in_file, error, JSONC_REQUIRED);
197 JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "cloud_account_id", cloud_account_id, error, JSONC_REQUIRED);
198 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, ".", "client_name", client_name, error, JSONC_REQUIRED);
199 JSONC_PARSE_ARRAY_OF_TXT2BITMAP_OR_ERROR_AND_RETURN(jobj, ".", "access", http_access2id_one, access, error, JSONC_REQUIRED);
200 JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, ".", "user_role", http_user_role2id, user_role, error, JSONC_REQUIRED);
201 JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "created_s", created_s, error, JSONC_REQUIRED);
202 JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "expires_s", expires_s, error, JSONC_REQUIRED);
203 JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "signature", signature, error, JSONC_REQUIRED);
204
205 if(uuid_compare(token, token_in_file) != 0) {
206 buffer_flush(error);
207 buffer_strcat(error, "token in JSON file does not match the filename");
208 return false;
209 }
210
211 if(uuid_compare(host_uuid, localhost->host_id.uuid) != 0) {
212 buffer_flush(error);
213 buffer_strcat(error, "Host UUID in JSON file does not match our host UUID");
214 return false;
215 }
216
217 if(!created_s || !expires_s || created_s >= expires_s) {
218 buffer_flush(error);
219 buffer_strcat(error, "bearer token has invalid dates");
220 return false;
221 }
222
223 struct bearer_token bt = {
224 .access = access,
225 .user_role = user_role,
226 .created_s = created_s,
227 .expires_s = expires_s,
228 };
229 uuid_copy(bt.cloud_account_id, cloud_account_id);
230 strncpyz(bt.client_name, string2str(client_name), sizeof(bt.client_name) - 1);
231
232 if(signature != bearer_token_signature(token_in_file, &bt)) {
233 buffer_flush(error);
234 buffer_strcat(error, "bearer token has invalid signature");
235 return false;
236 }
237
238 bearer_create_token_internal(token, user_role, access,
239 cloud_account_id, string2str(client_name),
240 created_s, expires_s, false);
241
242 return true;
243 }
244
245 static bool bearer_token_load_token(nd_uuid_t token) {
246 char filename[FILENAME_MAX];
247 bearer_token_filename(filename, token);
248
249 CLEAN_BUFFER *wb = buffer_create(0, NULL);
250 if(!read_txt_file_to_buffer(filename, wb, 1 * 1024 * 1024))
251 return false;
252
253 CLEAN_JSON_OBJECT *jobj = json_tokener_parse(buffer_tostring(wb));
254 if (jobj == NULL) {
255 nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot parse bearer token file '%s'", filename);
256 return false;
257 }
258
259 CLEAN_BUFFER *error = buffer_create(0, NULL);
260 bool rc = bearer_token_parse_json(token, jobj, error);
261 if(!rc) {
262 nd_log(NDLS_DAEMON, NDLP_ERR, "Failed to parse bearer token file '%s': %s", filename, buffer_tostring(error));
263 unlink(filename);
264 return false;
265 }
266
267 bearer_token_cleanup(true);
268
269 return true;
270 }
271
272 static void bearer_tokens_load_from_disk(void) {
273 bearer_tokens_ensure_path_exists();
274
275 char path[FILENAME_MAX];
276 bearer_tokens_path(path);
277
278 DIR *dir = opendir(path);
279 if(!dir) {
280 nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot open directory '%s' to read saved bearer tokens", path);
281 return;
282 }
283
284 struct dirent *de;
285 while((de = readdir(dir))) {
286 if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
287 continue;
288
289 ND_UUID uuid = UUID_ZERO;
290 if(uuid_parse_flexi(de->d_name, uuid.uuid) != 0 || UUIDiszero(uuid))
291 continue;
292
293 char filename[FILENAME_MAX];
294 filename_from_path_entry(filename, path, de->d_name, NULL);
295
296 if(de->d_type == DT_REG || (de->d_type == DT_LNK && filename_is_file(filename)))
297 bearer_token_load_token(uuid.uuid);
298 }
299
300 closedir(dir);
301 }
302
303 bool web_client_bearer_token_auth(struct web_client *w, const char *v) {
304 bool rc = false;
305
306 // javascript may send "null" or "undefined"
307 if(!v || !*v || strcmp(v, "null") == 0 || strcmp(v, "undefined") == 0)
308 return rc;
309
310 #ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
311 if (mcp_api_key_verify(v, true)) { // silent=true for speculative check
312 web_client_set_mcp_preview_key(w);
313 return true;
314 }
315 #endif
316
317 if(!uuid_parse_flexi(v, w->auth.bearer_token)) {
318 char uuid_str[UUID_COMPACT_STR_LEN];
319 uuid_unparse_lower_compact(w->auth.bearer_token, uuid_str);
320
321 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(netdata_authorized_bearers, uuid_str);
322 if(!item && bearer_token_load_token(w->auth.bearer_token))
323 item = dictionary_get_and_acquire_item(netdata_authorized_bearers, uuid_str);
324
325 if(item) {
326 struct bearer_token *bt = dictionary_acquired_item_value(item);
327 if (bt->expires_s > now_realtime_sec()) {
328 strncpyz(w->user_auth.client_name, bt->client_name, sizeof(w->user_auth.client_name) - 1);
329 uuid_copy(w->user_auth.cloud_account_id.uuid, bt->cloud_account_id);
330 web_client_set_permissions(w, bt->access, bt->user_role, USER_AUTH_METHOD_BEARER);
331 rc = true;
332 }
333
334 dictionary_acquired_item_release(netdata_authorized_bearers, item);
335 }
336 }
337 else
338 nd_log(NDLS_DAEMON, NDLP_NOTICE, "Invalid bearer token '%s' received.", v);
339
340 return rc;
341 }
342
343 void bearer_tokens_init(void) {
344 netdata_is_protected_by_bearer =
345 inicfg_get_boolean(&netdata_config, CONFIG_SECTION_WEB, "bearer token protection", netdata_is_protected_by_bearer);
346
347 netdata_authorized_bearers = dictionary_create_advanced(
348 DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
349 NULL, sizeof(struct bearer_token));
350
351 bearer_tokens_load_from_disk();
352 }
353
354 void bearer_tokens_destroy(void) {
355 dictionary_destroy(netdata_authorized_bearers);
356 netdata_authorized_bearers = NULL;
357 }
358
359 bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len) {
360 if(w->user_auth.method != USER_AUTH_METHOD_BEARER || dst_len != UUID_STR_LEN)
361 return false;
362
363 uuid_unparse_lower(w->auth.bearer_token, dst);
364 return true;
365 }