summaryrefslogtreecommitdiffstats
path: root/src/web/api/http_auth.c
blob: 5c4fffcaf7c61ebe714b0bfe7f41e261c5d113ef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
// SPDX-License-Identifier: GPL-3.0-or-later

#include "http_auth.h"

#define BEARER_TOKEN_EXPIRATION (86400 * 1)

bool netdata_is_protected_by_bearer = false;
static DICTIONARY *netdata_authorized_bearers = NULL;

struct bearer_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;
};

static void bearer_tokens_path(char out[FILENAME_MAX]) {
    filename_from_path_entry(out, netdata_configured_varlib_dir, "bearer_tokens", NULL);
}

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);
}

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_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 && !force)
        return;

    time_t now_s = now_realtime_sec();

    struct bearer_token *z;
    dfe_start_read(netdata_authorized_bearers, z) {
        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);
}

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));
}

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);
    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;
    }

    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));

    bearer_tokens_load_from_disk();
}

bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len) {
    if(!web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_BEARER) || dst_len != UUID_STR_LEN)
        return false;

    uuid_unparse_lower(w->auth.bearer_token, dst);
    return true;
}