Coverage Report

Created: 2025-11-24 06:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mosquitto/plugins/dynamic-security/acl.c
Line
Count
Source
1
/*
2
Copyright (c) 2020-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 "mosquitto.h"
22
23
#include "dynamic_security.h"
24
25
typedef int (*MOSQ_FUNC_acl_check)(struct dynsec__data *data, struct mosquitto_evt_acl_check *, struct dynsec__rolelist *);
26
27
28
/* FIXME - CACHE! */
29
30
31
/* ################################################################
32
 * #
33
 * # ACL check - publish broker to client
34
 * #
35
 * ################################################################ */
36
37
38
static int acl_check_publish_c_recv(struct dynsec__data *data, struct mosquitto_evt_acl_check *ed, struct dynsec__rolelist *base_rolelist)
39
0
{
40
0
  struct dynsec__rolelist *rolelist, *rolelist_tmp = NULL;
41
0
  struct dynsec__acl *acl, *acl_tmp = NULL;
42
0
  bool result;
43
0
  const char *clientid, *username;
44
45
0
  UNUSED(data);
46
47
0
  clientid = mosquitto_client_id(ed->client);
48
0
  username = mosquitto_client_username(ed->client);
49
50
0
  HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){
51
0
    HASH_ITER(hh, rolelist->role->acls.publish_c_recv, acl, acl_tmp){
52
0
      if(mosquitto_topic_matches_sub_with_pattern(acl->topic, ed->topic, clientid, username, &result)){
53
0
        return MOSQ_ERR_ACL_DENIED;
54
0
      }
55
0
      if(result){
56
0
        if(acl->allow){
57
0
          return MOSQ_ERR_SUCCESS;
58
0
        }else{
59
0
          return MOSQ_ERR_ACL_DENIED;
60
0
        }
61
0
      }
62
0
    }
63
0
  }
64
0
  return MOSQ_ERR_NOT_FOUND;
65
0
}
66
67
68
/* ################################################################
69
 * #
70
 * # ACL check - publish client to broker
71
 * #
72
 * ################################################################ */
73
74
75
static int acl_check_publish_c_send(struct dynsec__data *data, struct mosquitto_evt_acl_check *ed, struct dynsec__rolelist *base_rolelist)
76
0
{
77
0
  struct dynsec__rolelist *rolelist, *rolelist_tmp = NULL;
78
0
  struct dynsec__acl *acl, *acl_tmp = NULL;
79
0
  bool result;
80
0
  const char *clientid, *username;
81
82
0
  UNUSED(data);
83
84
0
  clientid = mosquitto_client_id(ed->client);
85
0
  username = mosquitto_client_username(ed->client);
86
87
0
  HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){
88
0
    HASH_ITER(hh, rolelist->role->acls.publish_c_send, acl, acl_tmp){
89
0
      if(mosquitto_topic_matches_sub_with_pattern(acl->topic, ed->topic, clientid, username, &result)){
90
0
        return MOSQ_ERR_ACL_DENIED;
91
0
      }
92
0
      if(result){
93
0
        if(acl->allow){
94
0
          return MOSQ_ERR_SUCCESS;
95
0
        }else{
96
0
          return MOSQ_ERR_ACL_DENIED;
97
0
        }
98
0
      }
99
0
    }
100
0
  }
101
0
  return MOSQ_ERR_NOT_FOUND;
102
0
}
103
104
105
/* ################################################################
106
 * #
107
 * # ACL check - subscribe
108
 * #
109
 * ################################################################ */
110
111
112
static int acl_check_subscribe(struct dynsec__data *data, struct mosquitto_evt_acl_check *ed, struct dynsec__rolelist *base_rolelist)
113
0
{
114
0
  struct dynsec__rolelist *rolelist, *rolelist_tmp = NULL;
115
0
  struct dynsec__acl *acl, *acl_tmp = NULL;
116
0
  size_t len;
117
0
  bool result;
118
0
  const char *clientid, *username;
119
0
  bool has_wildcard;
120
121
0
  UNUSED(data);
122
123
0
  len = strlen(ed->topic);
124
0
  has_wildcard = (strpbrk(ed->topic, "+#") != NULL);
125
126
0
  clientid = mosquitto_client_id(ed->client);
127
0
  username = mosquitto_client_username(ed->client);
128
129
0
  HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){
130
0
    if(rolelist->role->allow_wildcard_subs == false && has_wildcard == true){
131
0
      return MOSQ_ERR_ACL_DENIED;
132
0
    }
133
0
    HASH_FIND(hh, rolelist->role->acls.subscribe_literal, ed->topic, len, acl);
134
0
    if(acl){
135
0
      if(acl->allow){
136
0
        return MOSQ_ERR_SUCCESS;
137
0
      }else{
138
0
        return MOSQ_ERR_ACL_DENIED;
139
0
      }
140
0
    }
141
0
    HASH_ITER(hh, rolelist->role->acls.subscribe_pattern, acl, acl_tmp){
142
0
      if(mosquitto_sub_matches_acl_with_pattern(acl->topic, ed->topic, clientid, username, &result)){
143
        /* Invalid input, so deny */
144
0
        return MOSQ_ERR_ACL_DENIED;
145
0
      }
146
0
      if(result){
147
0
        if(acl->allow){
148
0
          return MOSQ_ERR_SUCCESS;
149
0
        }else{
150
0
          return MOSQ_ERR_ACL_DENIED;
151
0
        }
152
0
      }
153
0
    }
154
0
  }
155
0
  return MOSQ_ERR_NOT_FOUND;
156
0
}
157
158
159
/* ################################################################
160
 * #
161
 * # ACL check - unsubscribe
162
 * #
163
 * ################################################################ */
164
165
166
static int acl_check_unsubscribe(struct dynsec__data *data, struct mosquitto_evt_acl_check *ed, struct dynsec__rolelist *base_rolelist)
167
0
{
168
0
  struct dynsec__rolelist *rolelist, *rolelist_tmp = NULL;
169
0
  struct dynsec__acl *acl, *acl_tmp = NULL;
170
0
  size_t len;
171
0
  bool result;
172
0
  const char *clientid, *username;
173
174
0
  UNUSED(data);
175
176
0
  len = strlen(ed->topic);
177
178
0
  clientid = mosquitto_client_id(ed->client);
179
0
  username = mosquitto_client_username(ed->client);
180
181
0
  HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){
182
0
    HASH_FIND(hh, rolelist->role->acls.unsubscribe_literal, ed->topic, len, acl);
183
0
    if(acl){
184
0
      if(acl->allow){
185
0
        return MOSQ_ERR_SUCCESS;
186
0
      }else{
187
0
        return MOSQ_ERR_ACL_DENIED;
188
0
      }
189
0
    }
190
0
    HASH_ITER(hh, rolelist->role->acls.unsubscribe_pattern, acl, acl_tmp){
191
0
      if(mosquitto_sub_matches_acl_with_pattern(acl->topic, ed->topic, clientid, username, &result)){
192
        /* Invalid input, so deny */
193
0
        return MOSQ_ERR_ACL_DENIED;
194
0
      }
195
0
      if(result){
196
0
        if(acl->allow){
197
0
          return MOSQ_ERR_SUCCESS;
198
0
        }else{
199
0
          return MOSQ_ERR_ACL_DENIED;
200
0
        }
201
0
      }
202
0
    }
203
0
  }
204
0
  return MOSQ_ERR_NOT_FOUND;
205
0
}
206
207
208
/* ################################################################
209
 * #
210
 * # ACL check - generic check
211
 * #
212
 * ################################################################ */
213
214
215
static int acl_check(struct dynsec__data *data, struct mosquitto_evt_acl_check *ed, MOSQ_FUNC_acl_check check, bool acl_default_access)
216
0
{
217
0
  struct dynsec__client *client;
218
0
  struct dynsec__grouplist *grouplist, *grouplist_tmp = NULL;
219
0
  const char *username;
220
0
  int rc;
221
222
0
  username = mosquitto_client_username(ed->client);
223
224
0
  if(username){
225
0
    client = dynsec_clients__find(data, username);
226
0
    if(client == NULL){
227
0
      return MOSQ_ERR_PLUGIN_DEFER;
228
0
    }
229
230
    /* Client roles */
231
0
    rc = check(data, ed, client->rolelist);
232
0
    if(rc != MOSQ_ERR_NOT_FOUND){
233
0
      return rc;
234
0
    }
235
236
0
    HASH_ITER(hh, client->grouplist, grouplist, grouplist_tmp){
237
0
      rc = check(data, ed, grouplist->group->rolelist);
238
0
      if(rc != MOSQ_ERR_NOT_FOUND){
239
0
        return rc;
240
0
      }
241
0
    }
242
0
  }else if(data->anonymous_group){
243
    /* If we have a group for anonymous users, use that for checking. */
244
0
    rc = check(data, ed, data->anonymous_group->rolelist);
245
0
    if(rc != MOSQ_ERR_NOT_FOUND){
246
0
      return rc;
247
0
    }
248
0
  }
249
250
0
  if(acl_default_access == false){
251
0
    return MOSQ_ERR_PLUGIN_DEFER;
252
0
  }else{
253
0
    if(!strncmp(ed->topic, "$CONTROL", strlen("$CONTROL"))){
254
      /* We never give fall through access to $CONTROL topics, they must
255
       * be granted explicitly. */
256
0
      return MOSQ_ERR_PLUGIN_DEFER;
257
0
    }else{
258
0
      return MOSQ_ERR_SUCCESS;
259
0
    }
260
0
  }
261
0
}
262
263
264
/* ################################################################
265
 * #
266
 * # ACL check - plugin callback
267
 * #
268
 * ################################################################ */
269
270
271
int dynsec__acl_check_callback(int event, void *event_data, void *userdata)
272
0
{
273
0
  struct mosquitto_evt_acl_check *ed = event_data;
274
0
  struct dynsec__data *data = userdata;
275
276
0
  UNUSED(event);
277
0
  UNUSED(userdata);
278
279
  /* ACL checks are made in the order below until a match occurs, at which
280
   * point the decision is made.
281
   *
282
   * User roles in priority order highest to lowest.
283
   *    Roles have their ACLs checked in priority order, highest to lowest
284
   * Groups are processed in priority order highest to lowest
285
   *    Group roles are processed in priority order, highest to lowest
286
   *       Roles have their ACLs checked in priority order, highest to lowest
287
   */
288
289
0
  switch(ed->access){
290
0
    case MOSQ_ACL_SUBSCRIBE:
291
0
      return acl_check(data, event_data, acl_check_subscribe, data->default_access.subscribe);
292
0
      break;
293
0
    case MOSQ_ACL_UNSUBSCRIBE:
294
0
      return acl_check(data, event_data, acl_check_unsubscribe, data->default_access.unsubscribe);
295
0
      break;
296
0
    case MOSQ_ACL_WRITE: /* Client to broker */
297
0
      return acl_check(data, event_data, acl_check_publish_c_send, data->default_access.publish_c_send);
298
0
      break;
299
0
    case MOSQ_ACL_READ:
300
0
      return acl_check(data, event_data, acl_check_publish_c_recv, data->default_access.publish_c_recv);
301
0
      break;
302
0
    default:
303
0
      return MOSQ_ERR_PLUGIN_DEFER;
304
0
  }
305
0
  return MOSQ_ERR_PLUGIN_DEFER;
306
0
}