diff --git a/configs/samples/ari.conf.sample b/configs/samples/ari.conf.sample index e50eb39fe5..04973e10b4 100644 --- a/configs/samples/ari.conf.sample +++ b/configs/samples/ari.conf.sample @@ -35,6 +35,22 @@ enabled = yes ; When set to no, ARI support is disabled. ; When set to plain, the password is in plaintext. ; ;password_format = plain +; +; The following three options (permit, deny, acl) allow for a per-user acl to be +; configured. The format follows the rules as documented in acl.conf.sample +; +; If no restriction is defined for a given user, no IPs will be blocked by +; Asterisk (legacy behavior). +; +;deny = ; Deny acces from the subnet for the given user +;permit = ; Permit access from the subnet(s) for the given user +;acl = ; Optional name for the acl. +; +;Example: +;deny = 0.0.0.0/0 +;permit = 127.0.0.1,10.0.0.0/24 +;acl = localasteriskuser +; ; Outbound Websocket Connections ; diff --git a/configs/samples/http.conf.sample b/configs/samples/http.conf.sample index bd9794c5a9..c1c38b21ca 100644 --- a/configs/samples/http.conf.sample +++ b/configs/samples/http.conf.sample @@ -130,3 +130,41 @@ bindaddr=127.0.0.1 ; POST URL: /asterisk/uploads will put files in /var/lib/asterisk/uploads/. ;uploads = /var/lib/asterisk/uploads/ ; + +;[uripath] +; +;type = restriction ; Specifies acl configuration +; +; The following options (permit, deny, acl) allow for an acl to be configured +; on a per uri prefix basis. The first character should be an '/' +; +; The format follows the rules as documented in acl.conf.sample +; +; If no restriction is defined for a given prefix, legacy behavior will apply. +; +; If multiple restrictions apply, any restriction that denies will supersede +; any another restrictions that permit. For example if an /ari restriction +; results in a deny, but an /ari/channels restriction would permit, the +; attempt would still be denied. +; +;deny = ; Deny the subnet access for the given user +;permit = ; Permit the subnet(s) access for the given user +;acl = ; Optional name for the acl. +; +;Examples: +; +; Only allow ari connections from localhost: +; +;[/ari] +;type = restriction +;deny = 0.0.0.0/0 +;permit = 127.0.0.1 +;acl = localarionly +; +; Only allow metrics to be gathered by 10.0.0.23 +; +;[/metrics] +;type = restriction +;deny = 0.0.0.0/0 +;permit = 10.0.0.23 +; \ No newline at end of file diff --git a/main/http.c b/main/http.c index 9d7ae3d6aa..37d4d08a7e 100644 --- a/main/http.c +++ b/main/http.c @@ -51,6 +51,7 @@ #include #include "asterisk/paths.h" /* use ast_config_AST_DATA_DIR */ +#include "asterisk/acl.h" #include "asterisk/cli.h" #include "asterisk/tcptls.h" #include "asterisk/http.h" @@ -177,6 +178,19 @@ struct http_uri_redirect { static AST_RWLIST_HEAD_STATIC(uri_redirects, http_uri_redirect); +/*! \brief Per-path ACL restriction */ +struct http_restriction { + AST_LIST_ENTRY(http_restriction) entry; + struct ast_acl_list *acl; + char path[]; +}; + +AST_LIST_HEAD_NOLOCK(http_restriction_list, http_restriction); + +static AST_RWLIST_HEAD_STATIC(restrictions, http_restriction); + +static int check_restriction_acl(struct ast_tcptls_session_instance *ser, const char *uri); + static const struct ast_cfhttp_methods_text { enum ast_http_method method; const char *text; @@ -1503,6 +1517,13 @@ static int handle_uri(struct ast_tcptls_session_instance *ser, char *uri, } } + /* Check path-based ACL restrictions */ + if (check_restriction_acl(ser, uri) != 0) { + ast_http_request_close_on_completion(ser); + ast_http_error(ser, 403, "Forbidden", "Access denied by ACL"); + goto cleanup; + } + AST_RWLIST_RDLOCK(&uri_redirects); AST_RWLIST_TRAVERSE(&uri_redirects, redirect, entry) { if (!strcasecmp(uri, redirect->target)) { @@ -2127,6 +2148,36 @@ done: return NULL; } +/*! + * \brief Check if a URI path is allowed or denied by acl + * \param ser TCP/TLS session instance + * \param uri The URI path to check + * \return 0 if allowed, -1 if denied + */ +static int check_restriction_acl(struct ast_tcptls_session_instance *ser, const char *uri) +{ + struct http_restriction *restriction; + int denied = 0; + + AST_RWLIST_RDLOCK(&restrictions); + AST_RWLIST_TRAVERSE(&restrictions, restriction, entry) { + if (ast_begins_with(uri, restriction->path)) { + if (restriction->acl && !ast_acl_list_is_empty(restriction->acl)) { + if (ast_apply_acl(restriction->acl, &ser->remote_address, + "HTTP Path ACL") == AST_SENSE_DENY) { + ast_debug(2, "HTTP request for uri '%s' from %s denied by acl by restriction on '%s'\n", + uri, ast_sockaddr_stringify(&ser->remote_address), restriction->path); + denied = -1; + break; + } + } + } + } + AST_RWLIST_UNLOCK(&restrictions); + + return denied; +} + /*! * \brief Add a new URI redirect * The entries in the redirect list are sorted by length, just like the list @@ -2484,10 +2535,14 @@ static int __ast_http_load(int reload) char newprefix[MAX_PREFIX] = ""; char server_name[MAX_SERVER_NAME_LENGTH]; struct http_uri_redirect *redirect; + struct http_restriction *restriction; + struct http_restriction_list new_restrictions = AST_LIST_HEAD_NOLOCK_INIT_VALUE; + struct http_restriction_list old_restrictions = AST_LIST_HEAD_NOLOCK_INIT_VALUE; struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; uint32_t bindport = DEFAULT_PORT; int http_tls_was_enabled = 0; - char *bindaddr = NULL; + const char *bindaddr = NULL; + const char *cat = NULL; cfg = ast_config_load2("http.conf", "http", config_flags); if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) { @@ -2602,6 +2657,61 @@ static int __ast_http_load(int reload) } } + while ((cat = ast_category_browse(cfg, cat))) { + const char *type; + struct http_restriction *new_restriction; + struct ast_acl_list *acl = NULL; + int acl_error = 0; + int acl_subscription_flag = 0; + + if (strcasecmp(cat, "general") == 0) { + continue; + } + + type = ast_variable_retrieve(cfg, cat, "type"); + if (!type || strcasecmp(type, "restriction") != 0) { + continue; + } + + new_restriction = ast_calloc(1, sizeof(*new_restriction) + strlen(cat) + 1); + if (!new_restriction) { + continue; + } + + /* Safe */ + strcpy(new_restriction->path, cat); + + /* Parse ACL options for this restriction */ + for (v = ast_variable_browse(cfg, cat); v; v = v->next) { + if (!strcasecmp(v->name, "permit") || + !strcasecmp(v->name, "deny") || + !strcasecmp(v->name, "acl")) { + ast_append_acl(v->name, v->value, &acl, &acl_error, &acl_subscription_flag); + if (acl_error) { + ast_log(LOG_ERROR, "Bad ACL '%s' at line '%d' of http.conf for restriction '%s'\n", + v->value, v->lineno, cat); + } + } + } + + new_restriction->acl = acl; + + AST_LIST_INSERT_TAIL(&new_restrictions, new_restriction, entry); + ast_debug(2, "HTTP: Added restriction for path '%s'\n", cat); + } + + AST_RWLIST_WRLOCK(&restrictions); + AST_RWLIST_APPEND_LIST(&old_restrictions, &restrictions, entry); + AST_RWLIST_APPEND_LIST(&restrictions, &new_restrictions, entry); + AST_RWLIST_UNLOCK(&restrictions); + + while ((restriction = AST_LIST_REMOVE_HEAD(&old_restrictions, entry))) { + if (restriction->acl) { + ast_free_acl_list(restriction->acl); + } + ast_free(restriction); + } + ast_config_destroy(cfg); if (strcmp(prefix, newprefix)) { @@ -2711,13 +2821,31 @@ static char *handle_show_http(struct ast_cli_entry *e, int cmd, struct ast_cli_a ast_cli(a->fd, "\nEnabled Redirects:\n"); AST_RWLIST_RDLOCK(&uri_redirects); - AST_RWLIST_TRAVERSE(&uri_redirects, redirect, entry) - ast_cli(a->fd, " %s => %s\n", redirect->target, redirect->dest); if (AST_RWLIST_EMPTY(&uri_redirects)) { ast_cli(a->fd, " None.\n"); + } else { + AST_RWLIST_TRAVERSE(&uri_redirects, redirect, entry) + ast_cli(a->fd, " %s => %s\n", redirect->target, redirect->dest); } AST_RWLIST_UNLOCK(&uri_redirects); + ast_cli(a->fd, "\nPath Restrictions:\n"); + AST_RWLIST_RDLOCK(&restrictions); + if (AST_RWLIST_EMPTY(&restrictions)) { + ast_cli(a->fd, " None.\n"); + } else { + struct http_restriction *restriction; + AST_RWLIST_TRAVERSE(&restrictions, restriction, entry) { + ast_cli(a->fd, " Path: %s\n", restriction->path); + if (restriction->acl && !ast_acl_list_is_empty(restriction->acl)) { + ast_acl_output(a->fd, restriction->acl, " "); + } else { + ast_cli(a->fd, " No ACL configured\n"); + } + } + } + AST_RWLIST_UNLOCK(&restrictions); + return CLI_SUCCESS; } @@ -2733,6 +2861,7 @@ static struct ast_cli_entry cli_http[] = { static int unload_module(void) { struct http_uri_redirect *redirect; + struct http_restriction *restriction; ast_cli_unregister_multiple(cli_http, ARRAY_LEN(cli_http)); ao2_cleanup(global_http_server); @@ -2760,6 +2889,15 @@ static int unload_module(void) } AST_RWLIST_UNLOCK(&uri_redirects); + AST_RWLIST_WRLOCK(&restrictions); + while ((restriction = AST_RWLIST_REMOVE_HEAD(&restrictions, entry))) { + if (restriction->acl) { + ast_free_acl_list(restriction->acl); + } + ast_free(restriction); + } + AST_RWLIST_UNLOCK(&restrictions); + return 0; } diff --git a/res/ari/ari_doc.xml b/res/ari/ari_doc.xml index f897ecb0f3..dd7e54cdfa 100644 --- a/res/ari/ari_doc.xml +++ b/res/ari/ari_doc.xml @@ -103,6 +103,45 @@ password_format may be set to plain (the default) or crypt. When set to crypt, crypt(3) is used to validate the password. A crypted password can be generated using mkpasswd -m sha-512. When set to plain, the password is in plaintext + + + 20.19.0 + 22.9.0 + 23.3.0 + + List of IP ACL section names in acl.conf + + This matches sections configured in acl.conf. + + + + + 20.19.0 + 22.9.0 + 23.3.0 + + List of IP addresses to deny access from + + The value is a comma-delimited list of IP addresses. IP addresses may + have a subnet mask appended. The subnet mask may be written in either + CIDR or dotted-decimal notation. Separate the IP address and subnet + mask with a slash ('/') + + + + + 20.19.0 + 22.9.0 + 23.3.0 + + List of IP addresses to permit access from + + The value is a comma-delimited list of IP addresses. IP addresses may + have a subnet mask appended. The subnet mask may be written in either + CIDR or dotted-decimal notation. Separate the IP address and subnet + mask with a slash ('/') + + diff --git a/res/ari/cli.c b/res/ari/cli.c index 30c5f45c4a..c548e7a52e 100644 --- a/res/ari/cli.c +++ b/res/ari/cli.c @@ -78,8 +78,9 @@ static int show_users_cb(void *obj, void *arg, int flags) struct ari_conf_user *user = obj; struct ast_cli_args *a = arg; - ast_cli(a->fd, "%-4s %s\n", + ast_cli(a->fd, "%-4s %-4s %s\n", AST_CLI_YESNO(user->read_only), + AST_CLI_YESNO(user->acl && !ast_acl_list_is_empty(user->acl)), ast_sorcery_object_get_id(user)); return 0; } @@ -112,8 +113,8 @@ static char *ari_show_users(struct ast_cli_entry *e, int cmd, return CLI_FAILURE; } - ast_cli(a->fd, "r/o? Username\n"); - ast_cli(a->fd, "---- --------\n"); + ast_cli(a->fd, "r/o? ACL? Username\n"); + ast_cli(a->fd, "---- ---- --------\n"); ao2_callback(users, OBJ_NODATA, show_users_cb, a); @@ -173,6 +174,10 @@ static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args ast_cli(a->fd, "Username: %s\n", ast_sorcery_object_get_id(user)); ast_cli(a->fd, "Read only?: %s\n", AST_CLI_YESNO(user->read_only)); + ast_cli(a->fd, "ACL?: %s\n", AST_CLI_YESNO(user->acl && !ast_acl_list_is_empty(user->acl))); + if (!ast_acl_list_is_empty(user->acl)) { + ast_acl_output(a->fd, user->acl, NULL); + } return CLI_SUCCESS; } diff --git a/res/ari/config.c b/res/ari/config.c index 56fe4fc411..6575ef7a66 100644 --- a/res/ari/config.c +++ b/res/ari/config.c @@ -505,6 +505,7 @@ static void user_dtor(void *obj) { struct ari_conf_user *user = obj; ast_string_field_free_memory(user); + user->acl = ast_free_acl_list(user->acl); ast_debug(3, "%s: Disposing of user\n", ast_sorcery_object_get_id(user)); } @@ -558,6 +559,23 @@ static int user_password_format_from_str(const struct aco_option *opt, return 0; } +/*! \brief Handler for user ACL options */ +static int user_acl_handler(const struct aco_option *opt, + struct ast_variable *var, void *obj) +{ + struct ari_conf_user *user = obj; + int error = 0; + int ignore; + + ast_append_acl(var->name, var->value, &user->acl, &error, &ignore); + if (error) { + ast_log(LOG_ERROR, "Bad ACL '%s' at line '%d' of ari.conf\n", + var->value, var->lineno); + } + + return error; +} + static int user_password_format_to_str(const void *obj, const intptr_t *args, char **buf) { const struct ari_conf_user *user = obj; @@ -729,6 +747,9 @@ static int ari_conf_init(void) ast_sorcery_register_sf(user, ari_conf_user, password, password, ""); ast_sorcery_register_bool(user, ari_conf_user, read_only, read_only, "no"); ast_sorcery_register_cust(user, password_format, "plain"); + ast_sorcery_object_field_register_custom(sorcery, "user", "permit", "", user_acl_handler, NULL, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "user", "deny", "", user_acl_handler, NULL, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "user", "acl", "", user_acl_handler, NULL, NULL, 0, 0); ast_sorcery_object_field_register(sorcery, "outbound_websocket", "type", "", OPT_NOOP_T, 0, 0); ast_sorcery_register_cust(outbound_websocket, websocket_client_id, ""); diff --git a/res/ari/internal.h b/res/ari/internal.h index 2a5850f468..abc3381cd0 100644 --- a/res/ari/internal.h +++ b/res/ari/internal.h @@ -25,6 +25,7 @@ * \author David M. Lee, II */ +#include "asterisk/acl.h" #include "asterisk/http.h" #include "asterisk/json.h" #include "asterisk/md5.h" @@ -91,6 +92,8 @@ struct ari_conf_user { enum ari_user_password_format password_format; /*! If true, user cannot execute change operations */ int read_only; + /*! ACL setting */ + struct ast_acl_list *acl; }; enum ari_conf_owc_fields { diff --git a/res/res_ari.c b/res/res_ari.c index cb0a7248df..c38451be6f 100644 --- a/res/res_ari.c +++ b/res/res_ari.c @@ -575,6 +575,11 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se general->auth_realm); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); + } else if (user && user->acl && !ast_acl_list_is_empty(user->acl) && + ast_apply_acl(user->acl, &ser->remote_address, "ARI User ACL") == AST_SENSE_DENY) { + ast_ari_response_error(response, 403, "Forbidden", "Access denied by ACL"); + SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", + response->response_code, response->response_text); } else if (!ast_fully_booted) { ast_ari_response_error(response, 503, "Service Unavailable", "Asterisk not booted"); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CLOSE, "Response: %d : %s\n",