Skip to content

Commit

Permalink
Add rpc.acl to specify ACL directly.
Browse files Browse the repository at this point in the history
`rpc.acl_file` is preserved for compatibility but equivalent functionality
to setting `rpc.acl_file="XXX"` is provided by setting `rpc.acl="@xxx"`.
  • Loading branch information
rojer committed Aug 5, 2021
1 parent 32dd0fa commit 79aea0d
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 44 deletions.
15 changes: 15 additions & 0 deletions include/mgos_rpc.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ enum mgos_rpc_event {
MGOS_RPC_EV_CHANNEL_CLOSED, /* struct mg_str *dst */
};

/* Result returned by mgos_rpc_check_authz. */
enum mgos_rpc_authz_result {
MGOS_RPC_AUTHZ_DENY = 0,
MGOS_RPC_AUTHZ_ALLOW = 1,
MGOS_RPC_AUTHZ_AUTHN_REQD = 2,
MGOS_RPC_AUTHZ_ERROR = 3,
};

/*
* Check the specified RPC request against ACL.
* ACL can be JSON or @file.
*/
enum mgos_rpc_authz_result mgos_rpc_check_authz(
const struct mg_rpc_request_info *ri, const char *acl);

#ifdef __cplusplus
}
#endif
3 changes: 2 additions & 1 deletion mos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ config_schema:
- ["rpc.max_frame_size", "i", 4096, {title: "Max Frame Size"}]
- ["rpc.max_queue_length", "i", 25, {title: "Max Queue Length"}]
- ["rpc.default_out_channel_idle_close_timeout", "i", 10, {title: "Default idle close timeout for outbound channels"}]
- ["rpc.acl_file", "s", "", {title: "File with RPC ACL JSON"}]
- ["rpc.acl", "s", "", {title: "RPC ACL spec - JSON or @file"}]
- ["rpc.acl_file", "s", "", {title: "File with RPC ACL JSON; deprecated, used if rpc.acl is not set"}]
- ["rpc.auth_domain", "s", "RPC", {title: "Realm to use for digest authentication"}]
- ["rpc.auth_file", "s", "", {title: "File with user credentials in the htdigest format"}]
- ["rpc.auth_algo", "i", 0, {title: "Password file hashing algorithm: 0 - MD5, 1 - SHA256"}]
Expand Down
150 changes: 107 additions & 43 deletions src/mgos_rpc.c
Original file line number Diff line number Diff line change
Expand Up @@ -388,55 +388,123 @@ static void acl_parse_cb(void *callback_data, const char *name, size_t name_len,
(void) path;
}

/*
* Mgos-specific middleware which is called for every incoming RPC request
*/
static bool mgos_rpc_req_prehandler(struct mg_rpc_request_info *ri,
void *cb_arg, struct mg_rpc_frame_info *fi,
struct mg_str args) {
bool ret = true;
struct mg_str acl_entry = mg_mk_str("*");
static enum mgos_rpc_authz_result mgos_rpc_check_authz_internal(
const struct mg_rpc_request_info *ri, const char *acl, const char *acl_file,
struct mg_str *acl_entry_out) {
size_t acl_len = 0;
char *acl_data = NULL;
const char *auth_domain = NULL;
const char *auth_file = NULL;
struct mg_str acl_entry = MG_NULL_STR;
enum mgos_rpc_authz_result res = MGOS_RPC_AUTHZ_ERROR;

/* No ACL set - everything is allowed. */
if (acl == NULL && acl_file == NULL) {
res = MGOS_RPC_AUTHZ_ALLOW;
goto out;
}

const char *acl_file = mgos_sys_config_get_rpc_acl_file();
if (acl_file != NULL) {
/* acl_file is set: then, by default, deny everything */
acl_entry = mg_mk_str("-*");
/* Setting both acl and acl_file is an error. */
if (acl != NULL && acl_file != NULL) {
LOG(LL_ERROR, ("Both acl and acl_file are set!"));
goto out;
}

/* Does ACL specify a file? */
if ((acl != NULL && acl[0] == '@') || acl_file != NULL) {
if (acl_file == NULL) acl_file = acl + 1;
acl_data = cs_read_file(acl_file, &acl_len);
if (acl_data == NULL) {
LOG(LL_ERROR, ("Error reading %s", acl_file));
goto out;
}
acl = acl_data;
} else {
acl_len = strlen(acl);
}

/* Find the corresponding entry. */
{
struct acl_ctx ctx = {
.ri = ri,
};

size_t size;
acl_data = cs_read_file(acl_file, &size);
int walk_res = json_walk(acl_data, size, acl_parse_cb, &ctx);

int walk_res = json_walk(acl, acl_len, acl_parse_cb, &ctx);
if (walk_res < 0) {
LOG(LL_ERROR, ("error parsing ACL JSON: %d", walk_res));
goto out;
} else if (ctx.acl_entry.len > 0) {
acl_entry = ctx.acl_entry;
} else {
/* No match = deny */
res = MGOS_RPC_AUTHZ_DENY;
goto out;
}
}

LOG(LL_DEBUG, ("Called '%.*s' via '%s', ACL: '%.*s'", (int) ri->method.len,
ri->method.p, ri->ch->get_type(ri->ch), (int) acl_entry.len,
acl_entry.p));
/* Is it "allow all" or "deny all" type entry? */
if (mg_vcmp(&acl_entry, "*") == 0 || mg_vcmp(&acl_entry, "+*") == 0) {
res = MGOS_RPC_AUTHZ_ALLOW;
goto out;
}
if (mg_vcmp(&acl_entry, "-*") == 0) {
res = MGOS_RPC_AUTHZ_DENY;
goto out;
}

if (mg_vcmp(&acl_entry, "*") == 0) {
/*
* The method is allowed to be called by anyone, so, don't bother checking
* (even unauthenticated users will be able to call it).
*/
goto clean;
if (acl_entry_out != NULL) {
*acl_entry_out = mg_strdup(acl_entry);
}

if (mg_vcmp(&acl_entry, "-*") == 0) {
/* Not allowed to be called by anyone, don't bother checking. */
mg_rpc_send_errorf(ri, 403, "unauthorized");
ret = false;
goto clean;
/* If not, we need authn info. Do we have it? */
if (ri->authn_info.username.len == 0) {
res = MGOS_RPC_AUTHZ_AUTHN_REQD;
goto out;
}

/* We have the username, match it against the ACL entry. */
res = (mgos_conf_check_access_n(ri->authn_info.username, acl_entry)
? MGOS_RPC_AUTHZ_ALLOW
: MGOS_RPC_AUTHZ_DENY);

out:
free(acl_data);
return res;
}

enum mgos_rpc_authz_result mgos_rpc_check_authz(
const struct mg_rpc_request_info *ri, const char *acl) {
return mgos_rpc_check_authz_internal(ri, acl, NULL, NULL);
}

/*
* Mgos-specific middleware which is called for every incoming RPC request
*/
static bool mgos_rpc_req_prehandler(struct mg_rpc_request_info *ri,
void *cb_arg, struct mg_rpc_frame_info *fi,
struct mg_str args) {
bool ret = false;
const char *auth_domain = NULL;
const char *auth_file = NULL;
struct mg_str acl_entry = MG_NULL_STR;

enum mgos_rpc_authz_result authz_res = mgos_rpc_check_authz_internal(
ri, mgos_sys_config_get_rpc_acl(), mgos_sys_config_get_rpc_acl_file(),
&acl_entry);

switch (authz_res) {
case MGOS_RPC_AUTHZ_DENY: {
mg_rpc_send_errorf(ri, 403, "unauthorized");
ri = NULL;
goto out;
}
case MGOS_RPC_AUTHZ_ALLOW: {
ret = true;
goto out;
}
case MGOS_RPC_AUTHZ_ERROR: {
goto out;
}
case MGOS_RPC_AUTHZ_AUTHN_REQD: {
break;
}
}

/*
Expand All @@ -460,8 +528,7 @@ static bool mgos_rpc_req_prehandler(struct mg_rpc_request_info *ri,
if (!mg_rpc_check_digest_auth(ri)) {
mg_rpc_send_errorf(ri, 400, "bad request");
ri = NULL;
ret = false;
goto clean;
goto out;
}
}

Expand All @@ -475,7 +542,6 @@ static bool mgos_rpc_req_prehandler(struct mg_rpc_request_info *ri,
* No valid auth; send 401. If a channel has its channel-specific method to
* send 401, call it; otherwise send generic RPC response.
*/

if (ri->ch->send_not_authorized != NULL) {
ri->ch->send_not_authorized(ri->ch, auth_domain);
mg_rpc_free_request_info(ri);
Expand All @@ -494,28 +560,26 @@ static bool mgos_rpc_req_prehandler(struct mg_rpc_request_info *ri,
: "SHA-256"));
ri = NULL;
}
ret = false;
goto clean;
goto out;
}

/*
* Now we're guaranteed to have non-empty ri->authn_info.username. Let's
* check ACL finally.
*/

if (!mgos_conf_check_access_n(ri->authn_info.username, acl_entry)) {
ret = mgos_conf_check_access_n(ri->authn_info.username, acl_entry);
if (!ret) {
mg_rpc_send_errorf(ri, 403, "unauthorized");
ri = NULL;
ret = false;
goto clean;
}

(void) cb_arg;
(void) fi;
(void) args;

clean:
free(acl_data);
out:
mg_strfree(&acl_entry);
return ret;
}

Expand Down

0 comments on commit 79aea0d

Please sign in to comment.