Coverage Report

Created: 2025-06-20 06:45

/src/mosquitto/plugins/dynamic-security/config_init.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
Copyright (c) 2021 Roger Light <roger@atchoo.org>
3
4
All rights reserved. This program and the accompanying materials
5
are made available under the terms of the Eclipse Public License 2.0
6
and Eclipse Distribution License v1.0 which accompany this distribution.
7
8
The Eclipse Public License is available at
9
   https://www.eclipse.org/legal/epl-2.0/
10
and the Eclipse Distribution License is available at
11
  http://www.eclipse.org/org/documents/edl-v10.php.
12
13
SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
14
15
Contributors:
16
   Roger Light - initial implementation and documentation.
17
*/
18
19
#include "config.h"
20
21
#include <cjson/cJSON.h>
22
#include <ctype.h>
23
#include <errno.h>
24
#include <stdio.h>
25
#include <stdlib.h>
26
#include <string.h>
27
#include <sys/stat.h>
28
#include <openssl/rand.h>
29
30
#include "dynamic_security.h"
31
#include "json_help.h"
32
33
const char pw_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-=_+[]{}@#~,./<>?";
34
35
static int add_default_access(cJSON *j_tree)
36
0
{
37
0
  cJSON *j_default_access;
38
39
0
  j_default_access = cJSON_AddObjectToObject(j_tree, "defaultACLAccess");
40
0
  if(j_default_access == NULL){
41
0
    return MOSQ_ERR_NOMEM;
42
0
  }
43
  /* Set default behaviour:
44
   * * Client can not publish to the broker by default.
45
   * * Broker *CAN* publish to the client by default.
46
   * * Client con not subscribe to topics by default.
47
   * * Client *CAN* unsubscribe from topics by default.
48
   */
49
0
  if(cJSON_AddBoolToObject(j_default_access, "publishClientSend", false) == NULL
50
0
      || cJSON_AddBoolToObject(j_default_access, "publishClientReceive", true) == NULL
51
0
      || cJSON_AddBoolToObject(j_default_access, "subscribe", false) == NULL
52
0
      || cJSON_AddBoolToObject(j_default_access, "unsubscribe", true) == NULL
53
0
      ){
54
55
0
    return MOSQ_ERR_NOMEM;
56
0
  }
57
0
  return MOSQ_ERR_SUCCESS;
58
0
}
59
60
61
static int get_password_from_init_file(struct dynsec__data *data, char **pw)
62
0
{
63
0
  FILE *fptr;
64
0
  char buf[1024];
65
0
  int pos;
66
67
0
  if(data->password_init_file == NULL){
68
0
    *pw = NULL;
69
0
    return MOSQ_ERR_SUCCESS;
70
0
  }
71
0
  fptr = mosquitto_fopen(data->password_init_file, "rt", true);
72
0
  if(!fptr){
73
0
    mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Unable to get initial password from '%s', file not accessible.", data->password_init_file);
74
0
    return MOSQ_ERR_INVAL;
75
0
  }
76
0
  if(!fgets(buf, sizeof(buf), fptr)){
77
0
    fclose(fptr);
78
0
    mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Unable to get initial password from '%s', file empty.", data->password_init_file);
79
0
    return MOSQ_ERR_INVAL;
80
0
  }
81
0
  fclose(fptr);
82
83
0
  pos = (int)strlen(buf)-1;
84
0
  while(pos >= 0 && isspace(buf[pos])){
85
0
    buf[pos] = '\0';
86
0
    pos--;
87
0
  }
88
0
  if(strlen(buf) == 0){
89
0
    mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Unable to get initial password from '%s', password is empty.", data->password_init_file);
90
0
    return MOSQ_ERR_INVAL;
91
0
  }
92
0
  *pw = strdup(buf);
93
0
  if(!*pw){
94
0
    mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Unable to get initial password from '%s', out of memory.", data->password_init_file);
95
0
    return MOSQ_ERR_NOMEM;
96
0
  }else{
97
0
    return MOSQ_ERR_SUCCESS;
98
0
  }
99
0
}
100
101
102
/* Generate a password for the admin user
103
 *
104
 * Uses passwords from, in order:
105
 *
106
 * * The password defined in the plugin_opt_password_init_file file
107
 * * The contents of the MOSQUITTO_DYNSEC_PASSWORD environment variable
108
 * * Randomly generated passwords for "admin", "user", stored in plain text at '<plugin_opt_config_file>.pw'
109
 */
110
static int generate_password(struct dynsec__data *data, cJSON *j_client, char **password)
111
0
{
112
0
  struct mosquitto_pw *pw;
113
0
  int i;
114
0
  unsigned char vb;
115
0
  unsigned long v;
116
0
  size_t len;
117
0
  char *pwenv;
118
119
0
  if(data->init_mode == dpwim_file){
120
0
    if(get_password_from_init_file(data, password)){
121
0
      return MOSQ_ERR_INVAL;
122
0
    }
123
0
  }else if(data->init_mode == dpwim_env){
124
0
    pwenv = getenv("MOSQUITTO_DYNSEC_PASSWORD");
125
0
    if(pwenv == NULL || strlen(pwenv) < 12){
126
0
      mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Not generating dynsec config, MOSQUITTO_DYNSEC_PASSWORD must be at least 12 characters.");
127
0
      return MOSQ_ERR_INVAL;
128
0
    }
129
0
    *password = strdup(pwenv);
130
0
    if(*password == NULL){
131
0
      return MOSQ_ERR_NOMEM;
132
0
    }
133
0
  }else{
134
0
    *password = malloc(21);
135
0
    if(*password == NULL){
136
0
      return MOSQ_ERR_NOMEM;
137
0
    }
138
0
    len = sizeof(pw_chars)-1;
139
0
    for(i=0; i<20; i++){
140
0
      do{
141
0
        if(RAND_bytes(&vb, 1) != 1){
142
0
          free(*password);
143
0
          return MOSQ_ERR_UNKNOWN;
144
0
        }
145
0
        v = vb;
146
0
      }while(v >= (RAND_MAX - (RAND_MAX % len)));
147
0
      (*password)[i] = pw_chars[v%len];
148
0
    }
149
0
    (*password)[20] = '\0';
150
0
  }
151
152
0
  if(mosquitto_pw_new(&pw, MOSQ_PW_DEFAULT) != MOSQ_ERR_SUCCESS
153
0
      || mosquitto_pw_hash_encoded(pw, *password) != MOSQ_ERR_SUCCESS
154
0
      || cJSON_AddStringToObject(j_client, "encoded_password", mosquitto_pw_get_encoded(pw)) == NULL){
155
156
0
    mosquitto_pw_cleanup(pw);
157
0
    free(*password);
158
0
    *password = NULL;
159
0
    return MOSQ_ERR_UNKNOWN;
160
0
  }
161
162
0
  mosquitto_pw_cleanup(pw);
163
164
0
  return MOSQ_ERR_SUCCESS;
165
0
}
166
167
168
static int client_role_add(cJSON *j_roles, const char *rolename)
169
0
{
170
0
  cJSON *j_role;
171
172
0
  j_role = cJSON_CreateObject();
173
0
  if(j_role == NULL){
174
0
    return MOSQ_ERR_NOMEM;
175
0
  }
176
0
  cJSON_AddItemToArray(j_roles, j_role);
177
0
  if(cJSON_AddStringToObject(j_role, "rolename", rolename) == NULL){
178
0
    return MOSQ_ERR_NOMEM;
179
0
  }else{
180
0
    return MOSQ_ERR_SUCCESS;
181
0
  }
182
0
}
183
184
185
static int client_add_admin(struct dynsec__data *data, FILE *pwfile, cJSON *j_clients)
186
0
{
187
0
  cJSON *j_client, *j_roles;
188
0
  char *password = NULL;
189
190
0
  j_client = cJSON_CreateObject();
191
0
  if(j_client == NULL){
192
0
    return MOSQ_ERR_NOMEM;
193
0
  }
194
0
  if(generate_password(data, j_client, &password)){
195
0
    cJSON_Delete(j_client);
196
0
    return MOSQ_ERR_UNKNOWN;
197
0
  }
198
199
0
  cJSON_AddItemToArray(j_clients, j_client);
200
0
  if(cJSON_AddStringToObject(j_client, "username", "admin") == NULL
201
0
      || cJSON_AddStringToObject(j_client, "textname", "Admin user") == NULL
202
0
      || (j_roles = cJSON_AddArrayToObject(j_client, "roles")) == NULL
203
0
      ){
204
205
0
    cJSON_Delete(j_client);
206
0
    free(password);
207
0
    return MOSQ_ERR_NOMEM;
208
0
  }
209
210
0
  if(client_role_add(j_roles, "super-admin")
211
0
      || client_role_add(j_roles, "sys-observe")
212
0
      || client_role_add(j_roles, "topic-observe")){
213
214
0
    free(password);
215
0
    return MOSQ_ERR_NOMEM;
216
0
  }
217
218
0
  if(data->init_mode == dpwim_random){
219
0
    fprintf(pwfile, "admin %s\n", password);
220
0
  }
221
0
  free(password);
222
223
0
  return MOSQ_ERR_SUCCESS;
224
0
}
225
226
static int client_add_user(struct dynsec__data *data, FILE *pwfile, cJSON *j_clients)
227
0
{
228
0
  cJSON *j_client, *j_roles;
229
0
  char *password = NULL;
230
231
0
  if(data->init_mode != dpwim_random){
232
0
    return MOSQ_ERR_SUCCESS;
233
0
  }
234
0
  j_client = cJSON_CreateObject();
235
0
  if(j_client == NULL){
236
0
    return MOSQ_ERR_NOMEM;
237
0
  }
238
239
0
  if(generate_password(data, j_client, &password)){
240
0
    cJSON_Delete(j_client);
241
0
    return MOSQ_ERR_UNKNOWN;
242
0
  }
243
244
0
  if(cJSON_AddStringToObject(j_client, "username", "democlient") == NULL
245
0
      || cJSON_AddStringToObject(j_client, "textname", "Demonstration client with full read/write access to the '#' topic hierarchy.") == NULL
246
0
      || (j_roles = cJSON_AddArrayToObject(j_client, "roles")) == NULL
247
0
      ){
248
249
0
    free(password);
250
0
    cJSON_Delete(j_client);
251
0
    return MOSQ_ERR_NOMEM;
252
0
  }
253
0
  cJSON_AddItemToArray(j_clients, j_client);
254
255
0
  if(client_role_add(j_roles, "client")){
256
0
    free(password);
257
0
    return MOSQ_ERR_NOMEM;
258
0
  }
259
260
0
  fprintf(pwfile, "democlient %s\n", password);
261
0
  free(password);
262
263
0
  return MOSQ_ERR_SUCCESS;
264
0
}
265
266
static int add_clients(struct dynsec__data *data, cJSON *j_tree)
267
0
{
268
0
  cJSON *j_clients;
269
0
  char *pwfile;
270
0
  size_t len;
271
0
  FILE *fptr = NULL;
272
273
0
  if(data->init_mode == dpwim_random){
274
0
    len = strlen(data->config_file) + 5;
275
0
    pwfile = malloc(len);
276
0
    if(pwfile == NULL){
277
0
      return MOSQ_ERR_NOMEM;
278
0
    }
279
0
    snprintf(pwfile, len, "%s.pw", data->config_file);
280
0
    fptr = mosquitto_fopen(pwfile, "wb", true);
281
0
    free(pwfile);
282
0
    if(fptr == NULL){
283
0
      return MOSQ_ERR_UNKNOWN;
284
0
    }
285
0
  }
286
287
0
  j_clients = cJSON_AddArrayToObject(j_tree, "clients");
288
0
  if(j_clients == NULL){
289
0
    if(fptr) fclose(fptr);
290
0
    return MOSQ_ERR_NOMEM;
291
0
  }
292
293
0
  if(client_add_admin(data, fptr, j_clients)
294
0
      || client_add_user(data, fptr, j_clients)
295
0
      ){
296
297
0
    if(fptr) fclose(fptr);
298
0
    return MOSQ_ERR_NOMEM;
299
0
  }
300
301
0
  if(fptr) fclose(fptr);
302
0
  return MOSQ_ERR_SUCCESS;
303
0
}
304
305
306
static int group_add_anon(cJSON *j_groups)
307
0
{
308
0
  cJSON *j_group;
309
310
0
  j_group = cJSON_CreateObject();
311
0
  if(j_group == NULL){
312
0
    return MOSQ_ERR_NOMEM;
313
0
  }
314
315
0
  cJSON_AddItemToArray(j_groups, j_group);
316
0
  if(cJSON_AddStringToObject(j_group, "groupname", "unauthenticated") == NULL
317
0
      || cJSON_AddStringToObject(j_group, "textname", "Unauthenticated group") == NULL
318
0
      || cJSON_AddStringToObject(j_group, "textdescription", "If unauthenticated access is allowed, this group can be used to define roles for clients that connect without a password.") == NULL
319
0
      || cJSON_AddArrayToObject(j_group, "roles") == NULL
320
0
      ){
321
322
0
    return MOSQ_ERR_NOMEM;
323
0
  }
324
325
0
  return MOSQ_ERR_SUCCESS;
326
0
}
327
328
static int add_groups(cJSON *j_tree)
329
0
{
330
0
  cJSON *j_groups;
331
332
0
  j_groups = cJSON_AddArrayToObject(j_tree, "groups");
333
0
  if(j_groups == NULL){
334
0
    return MOSQ_ERR_NOMEM;
335
0
  }
336
337
0
  return group_add_anon(j_groups);
338
0
}
339
340
341
static int acl_add(cJSON *j_acls, const char *acltype, const char *topic, int priority, bool allow)
342
0
{
343
0
  cJSON *j_acl;
344
345
0
  j_acl = cJSON_CreateObject();
346
0
  cJSON_AddItemToArray(j_acls, j_acl);
347
0
  if(cJSON_AddStringToObject(j_acl, "acltype", acltype) == NULL
348
0
      || cJSON_AddStringToObject(j_acl, "topic", topic) == NULL
349
0
      || cJSON_AddNumberToObject(j_acl, "priority", priority) == NULL
350
0
      || cJSON_AddBoolToObject(j_acl, "allow", allow) == NULL
351
0
      ){
352
0
    return MOSQ_ERR_NOMEM;
353
0
  }else{
354
0
    return MOSQ_ERR_SUCCESS;
355
0
  }
356
0
}
357
358
static int add_role_with_full_permission(cJSON *j_roles, const char *role_name, const char *text_description, const char *topic_pattern)
359
0
{
360
0
  cJSON *j_role, *j_acls;
361
362
0
  j_role = cJSON_CreateObject();
363
0
  if(j_role == NULL){
364
0
    return MOSQ_ERR_NOMEM;
365
0
  }
366
0
  cJSON_AddItemToArray(j_roles, j_role);
367
368
0
  if(cJSON_AddStringToObject(j_role, "rolename", role_name) == NULL
369
0
       || cJSON_AddStringToObject(j_role, "textdescription", text_description) == NULL
370
0
       || (j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL){
371
0
    return MOSQ_ERR_NOMEM;
372
0
  }
373
374
0
  if(acl_add(j_acls, "publishClientSend", topic_pattern, 0, true)
375
0
    || acl_add(j_acls, "publishClientReceive", topic_pattern, 0, true)
376
0
    || acl_add(j_acls, "subscribePattern", topic_pattern, 0, true)
377
0
    || acl_add(j_acls, "unsubscribePattern", topic_pattern, 0, true)){
378
0
    return MOSQ_ERR_NOMEM;
379
0
  }
380
0
  return MOSQ_ERR_SUCCESS;
381
0
}
382
383
static int role_add_sys_notify(cJSON *j_roles)
384
0
{
385
0
  cJSON *j_role, *j_acls;
386
387
0
  j_role = cJSON_CreateObject();
388
0
  if(j_role == NULL){
389
0
    return MOSQ_ERR_NOMEM;
390
0
  }
391
0
  cJSON_AddItemToArray(j_roles, j_role);
392
393
0
  if(cJSON_AddStringToObject(j_role, "rolename", "sys-notify") == NULL
394
0
      || cJSON_AddStringToObject(j_role, "textdescription",
395
0
        "Allow bridges to publish connection state messages.") == NULL
396
0
      || (j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL
397
0
      ){
398
399
0
    return MOSQ_ERR_NOMEM;
400
0
  }
401
402
0
  if(acl_add(j_acls, "publishClientSend", "$SYS/broker/connection/%c/state", 0, true)
403
0
      ){
404
405
0
    return MOSQ_ERR_NOMEM;
406
0
  }
407
0
  return MOSQ_ERR_SUCCESS;
408
0
}
409
410
static int role_add_sys_observe(cJSON *j_roles)
411
0
{
412
0
  cJSON *j_role, *j_acls;
413
414
0
  j_role = cJSON_CreateObject();
415
0
  if(j_role == NULL){
416
0
    return MOSQ_ERR_NOMEM;
417
0
  }
418
0
  cJSON_AddItemToArray(j_roles, j_role);
419
420
0
  if(cJSON_AddStringToObject(j_role, "rolename", "sys-observe") == NULL
421
0
      || cJSON_AddStringToObject(j_role, "textdescription",
422
0
        "Observe the $SYS topic hierarchy.") == NULL
423
0
      || (j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL
424
0
      ){
425
426
0
    return MOSQ_ERR_NOMEM;
427
0
  }
428
429
0
  if(acl_add(j_acls, "publishClientReceive", "$SYS/#", 0, true)
430
0
      || acl_add(j_acls, "subscribePattern", "$SYS/#", 0, true)
431
0
      ){
432
433
0
    return MOSQ_ERR_NOMEM;
434
0
  }
435
0
  return MOSQ_ERR_SUCCESS;
436
0
}
437
438
static int role_add_topic_observe(cJSON *j_roles)
439
0
{
440
0
  cJSON *j_role, *j_acls;
441
442
0
  j_role = cJSON_CreateObject();
443
0
  if(j_role == NULL){
444
0
    return MOSQ_ERR_NOMEM;
445
0
  }
446
0
  cJSON_AddItemToArray(j_roles, j_role);
447
448
0
  if(cJSON_AddStringToObject(j_role, "rolename", "topic-observe") == NULL
449
0
      || cJSON_AddStringToObject(j_role, "textdescription",
450
0
        "Read only access to the full application topic hierarchy.") == NULL
451
0
      || (j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL
452
0
      ){
453
454
0
    return MOSQ_ERR_NOMEM;
455
0
  }
456
457
0
  if(acl_add(j_acls, "publishClientReceive", "#", 0, true)
458
0
      || acl_add(j_acls, "subscribePattern", "#", 0, true)
459
0
      || acl_add(j_acls, "unsubscribePattern", "#", 0, true)
460
0
      ){
461
462
0
    return MOSQ_ERR_NOMEM;
463
0
  }
464
0
  return MOSQ_ERR_SUCCESS;
465
0
}
466
467
468
static int add_roles(cJSON *j_tree)
469
0
{
470
0
  cJSON *j_roles;
471
472
0
  j_roles = cJSON_AddArrayToObject(j_tree, "roles");
473
0
  if(j_roles == NULL){
474
0
    return MOSQ_ERR_NOMEM;
475
0
  }
476
477
0
  if(add_role_with_full_permission(j_roles, "client", "Read/write access to the full application topic hierarchy.", "#")
478
0
    || add_role_with_full_permission(j_roles, "broker-admin", "Grants access to administer general broker configuration.", "$CONTROL/broker/#")
479
0
    || add_role_with_full_permission(j_roles, "dynsec-admin", "Grants access to administer clients/groups/roles.", "$CONTROL/dynamic-security/#")
480
0
    || add_role_with_full_permission(j_roles, "super-admin", "Grants access to administer all kind of broker controls", "$CONTROL/#")
481
0
    || role_add_sys_notify(j_roles) || role_add_sys_observe(j_roles) || role_add_topic_observe(j_roles)){
482
0
    return MOSQ_ERR_NOMEM;
483
0
  }
484
485
0
  return MOSQ_ERR_SUCCESS;
486
0
}
487
488
489
int dynsec__config_init(struct dynsec__data *data)
490
0
{
491
0
  FILE *fptr;
492
0
  cJSON *j_tree;
493
0
  char *json_str;
494
495
0
  mosquitto_log_printf(MOSQ_LOG_INFO, "Dynamic security plugin config not found, generating a default config.");
496
497
0
  if(data->password_init_file){
498
0
    mosquitto_log_printf(MOSQ_LOG_INFO, "  Using admin password from file '%s'", data->password_init_file);
499
0
    data->init_mode = dpwim_file;
500
0
  }else if(getenv("MOSQUITTO_DYNSEC_PASSWORD")){
501
0
    mosquitto_log_printf(MOSQ_LOG_INFO, "  Using admin password from MOSQUITTO_DYNSEC_PASSWORD environment variable");
502
0
    data->init_mode = dpwim_env;
503
0
  }else{
504
0
    mosquitto_log_printf(MOSQ_LOG_INFO, "  Generated passwords are at %s.pw", data->config_file);
505
0
    data->init_mode = dpwim_random;
506
0
  }
507
508
0
  j_tree = cJSON_CreateObject();
509
0
  if(j_tree == NULL){
510
0
    return MOSQ_ERR_NOMEM;
511
0
  }
512
513
0
  if(add_default_access(j_tree) != MOSQ_ERR_SUCCESS
514
0
      || add_clients(data, j_tree) != MOSQ_ERR_SUCCESS
515
0
      || add_groups(j_tree) != MOSQ_ERR_SUCCESS
516
0
      || add_roles(j_tree) != MOSQ_ERR_SUCCESS
517
0
      || cJSON_AddStringToObject(j_tree, "anonymousGroup", "unauthenticated") == NULL
518
0
      ){
519
520
0
    cJSON_Delete(j_tree);
521
0
    return MOSQ_ERR_NOMEM;
522
0
  }
523
524
0
  json_str = cJSON_Print(j_tree);
525
0
  cJSON_Delete(j_tree);
526
0
  if(json_str == NULL){
527
0
    return MOSQ_ERR_NOMEM;
528
0
  }
529
530
0
  fptr = mosquitto_fopen(data->config_file, "wb", true);
531
0
  if(fptr == NULL){
532
0
    return MOSQ_ERR_UNKNOWN;
533
0
  }
534
0
  fprintf(fptr, "%s", json_str);
535
0
  free(json_str);
536
0
  fclose(fptr);
537
538
0
  return MOSQ_ERR_SUCCESS;
539
0
}