Add uri prefix based acl support to the built in http server.
This allows an acl to be added per uri prefix (ie '/metrics'
or '/ws') to restrict access.
Add user based acl support for ARI. This adds new acl options
to the user section of ari.conf to restrict access on a per
user basis.
resolves: #1799
UserNote: A new section, type=restriction has been added to http.conf
to allow an uri prefix based acl to be configured. See
http.conf.sample for examples and more information.
The user section of ari.conf can now contain an acl configuration
to restrict users access. See ari.conf.sample for examples and more
information
; 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
;
; 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
#include <fcntl.h>
#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"
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;
}
}
+ /* 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)) {
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
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) {
}
}
+ 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)) {
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;
}
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);
}
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;
}
</since>
<synopsis>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</synopsis>
</configOption>
+ <configOption name="acl">
+ <since>
+ <version>20.19.0</version>
+ <version>22.9.0</version>
+ <version>23.3.0</version>
+ </since>
+ <synopsis>List of IP ACL section names in acl.conf</synopsis>
+ <description><para>
+ This matches sections configured in <literal>acl.conf</literal>.
+ </para></description>
+ </configOption>
+ <configOption name="deny">
+ <since>
+ <version>20.19.0</version>
+ <version>22.9.0</version>
+ <version>23.3.0</version>
+ </since>
+ <synopsis>List of IP addresses to deny access from</synopsis>
+ <description><para>
+ 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 ('/')
+ </para></description>
+ </configOption>
+ <configOption name="permit">
+ <since>
+ <version>20.19.0</version>
+ <version>22.9.0</version>
+ <version>23.3.0</version>
+ </since>
+ <synopsis>List of IP addresses to permit access from</synopsis>
+ <description><para>
+ 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 ('/')
+ </para></description>
+ </configOption>
</configObject>
<configObject name="outbound_websocket">
<since>
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;
}
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);
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;
}
{
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));
}
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;
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, "");
* \author David M. Lee, II <dlee@digium.com>
*/
+#include "asterisk/acl.h"
#include "asterisk/http.h"
#include "asterisk/json.h"
#include "asterisk/md5.h"
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 {
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",