[Asterisk-code-review] ARI event type filtering (asterisk[13])

Kevin Harwell asteriskteam at digium.com
Fri Feb 8 14:50:59 CST 2019


Kevin Harwell has uploaded this change for review. ( https://gerrit.asterisk.org/10978


Change subject: ARI event type filtering
......................................................................

ARI event type filtering

Event type filtering is now enabled, and configurable per application. An app is
now able to specify which events are sent to the application by configuring an
allowed and/or disallowed list(s). This can be done by issuing the following:

PUT /applications/{applicationName}/eventFilter

And then enumerating the allowed/disallowed event types as a body parameter.

ASTERISK-28106

Change-Id: I9671ba1fcdb3b6c830b553d4c5365aed5d588d5b
---
M CHANGES
M include/asterisk/stasis_app.h
M res/ari/ari_model_validators.c
M res/ari/ari_model_validators.h
M res/ari/resource_applications.c
M res/ari/resource_applications.h
M res/ari/resource_events.c
M res/res_ari_applications.c
M res/res_stasis.c
M res/stasis/app.c
M rest-api/api-docs/applications.json
11 files changed, 401 insertions(+), 5 deletions(-)



  git pull ssh://gerrit.asterisk.org:29418/asterisk refs/changes/78/10978/1

diff --git a/CHANGES b/CHANGES
index 26a3d49..6083b1a 100644
--- a/CHANGES
+++ b/CHANGES
@@ -9,6 +9,18 @@
 ==============================================================================
 
 ------------------------------------------------------------------------------
+--- Functionality changes from Asterisk 13.25.0 to Asterisk 13.26.0 ----------
+------------------------------------------------------------------------------
+
+ARI
+------------------
+ * Application event filtering is now supported. An application can now specify
+   an "allowed" and/or "disallowed" list(s) of event types. Only those types
+   indicated in the "allowed" list are sent to the application. Conversely, any
+   types defined in the "disallowed" list are not sent to the application. Note
+   that if a type is specified in both lists "disallowed" takes precedence.
+
+------------------------------------------------------------------------------
 --- Functionality changes from Asterisk 13.24.0 to Asterisk 13.25.0 ----------
 ------------------------------------------------------------------------------
 
diff --git a/include/asterisk/stasis_app.h b/include/asterisk/stasis_app.h
index 90a7ea3..37a4bf0 100644
--- a/include/asterisk/stasis_app.h
+++ b/include/asterisk/stasis_app.h
@@ -150,6 +150,16 @@
 /*!
  * \brief Return the JSON representation of a Stasis application.
  *
+ * \param app The application.
+ *
+ * \return JSON representation of app with given name.
+ * \return \c NULL on error.
+ */
+struct ast_json *stasis_app_object_to_json(struct stasis_app *app);
+
+/*!
+ * \brief Return the JSON representation of a Stasis application.
+ *
  * \param app_name Name of the application.
  *
  * \return JSON representation of app with given name.
@@ -939,6 +949,41 @@
  */
 void stasis_app_to_cli(const struct stasis_app *app, struct ast_cli_args *a);
 
+/*!
+ * \brief Convert and add the app's event type filter(s) to the given json object.
+ *
+ * \param app The application
+ * \param json The json object to add the filter data to
+ *
+ * \return The given json object
+ */
+struct ast_json *stasis_app_event_filter_to_json(struct stasis_app *app, struct ast_json *json);
+
+/*!
+ * \brief Set the application's event type filter
+ *
+ * \param app The application
+ * \param filter The allowed and/or disallowed event filter
+ *
+ * \return 0 if successfully set
+ */
+int stasis_app_event_filter_set(struct stasis_app *app, struct ast_json *filter);
+
+/*!
+ * \brief Check if the given event should be filtered.
+ *
+ * Attempts first to find the event in the application's disallowed events list.
+ * If found then the event won't be sent to the remote. If not found in the
+ * disallowed list then a search is done to see if it can be found in the allowed
+ * list. If found the event message is sent, otherwise it is not sent.
+ *
+ * \param app_name The application name
+ * \param event The event to check
+ *
+ * \return True if allowed, false otherwise
+ */
+int stasis_app_event_allowed(const char *app_name, struct ast_json *event);
+
 /*! @} */
 
 #endif /* _ASTERISK_STASIS_APP_H */
diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c
index 673864a..d22d705 100644
--- a/res/ari/ari_model_validators.c
+++ b/res/ari/ari_model_validators.c
@@ -6420,13 +6420,26 @@
 {
 	int res = 1;
 	struct ast_json_iter *iter;
+	int has_allowed_events = 0;
 	int has_bridge_ids = 0;
 	int has_channel_ids = 0;
 	int has_device_names = 0;
+	int has_disallowed_events = 0;
 	int has_endpoint_ids = 0;
 	int has_name = 0;
 
 	for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
+		if (strcmp("allowed_events", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_allowed_events = 1;
+			prop_is_valid = ast_ari_validate_list(
+				ast_json_object_iter_value(iter),
+				ast_ari_validate_object);
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI Application field allowed_events failed validation\n");
+				res = 0;
+			}
+		} else
 		if (strcmp("bridge_ids", ast_json_object_iter_key(iter)) == 0) {
 			int prop_is_valid;
 			has_bridge_ids = 1;
@@ -6460,6 +6473,17 @@
 				res = 0;
 			}
 		} else
+		if (strcmp("disallowed_events", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_disallowed_events = 1;
+			prop_is_valid = ast_ari_validate_list(
+				ast_json_object_iter_value(iter),
+				ast_ari_validate_object);
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI Application field disallowed_events failed validation\n");
+				res = 0;
+			}
+		} else
 		if (strcmp("endpoint_ids", ast_json_object_iter_key(iter)) == 0) {
 			int prop_is_valid;
 			has_endpoint_ids = 1;
@@ -6489,6 +6513,11 @@
 		}
 	}
 
+	if (!has_allowed_events) {
+		ast_log(LOG_ERROR, "ARI Application missing required field allowed_events\n");
+		res = 0;
+	}
+
 	if (!has_bridge_ids) {
 		ast_log(LOG_ERROR, "ARI Application missing required field bridge_ids\n");
 		res = 0;
@@ -6504,6 +6533,11 @@
 		res = 0;
 	}
 
+	if (!has_disallowed_events) {
+		ast_log(LOG_ERROR, "ARI Application missing required field disallowed_events\n");
+		res = 0;
+	}
+
 	if (!has_endpoint_ids) {
 		ast_log(LOG_ERROR, "ARI Application missing required field endpoint_ids\n");
 		res = 0;
diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h
index 64dd88a..53a6dbb 100644
--- a/res/ari/ari_model_validators.h
+++ b/res/ari/ari_model_validators.h
@@ -1801,9 +1801,11 @@
  * - endpoint: Endpoint
  * - message: TextMessage (required)
  * Application
+ * - allowed_events: List[object] (required)
  * - bridge_ids: List[string] (required)
  * - channel_ids: List[string] (required)
  * - device_names: List[string] (required)
+ * - disallowed_events: List[object] (required)
  * - endpoint_ids: List[string] (required)
  * - name: string (required)
  */
diff --git a/res/ari/resource_applications.c b/res/ari/resource_applications.c
index 85a631a..061d6b4 100644
--- a/res/ari/resource_applications.c
+++ b/res/ari/resource_applications.c
@@ -170,3 +170,23 @@
 			"Error processing request");
 	}
 }
+
+void ast_ari_applications_filter(struct ast_variable *headers,
+	struct ast_ari_applications_filter_args *args,
+	struct ast_ari_response *response)
+{
+	struct stasis_app *app = stasis_app_get_by_name(args->application_name);
+
+	if (!app) {
+		ast_ari_response_error(response, 404, "Not Found", "Application not found");
+		return;
+	}
+
+	if (stasis_app_event_filter_set(app, args->filter)) {
+		ast_ari_response_error(response, 400, "Bad Request", "Invalid format definition");
+	} else {
+		ast_ari_response_ok(response, stasis_app_object_to_json(app));
+	}
+
+	ao2_ref(app, -1);
+}
diff --git a/res/ari/resource_applications.h b/res/ari/resource_applications.h
index be62e9d..23a9b9b 100644
--- a/res/ari/resource_applications.h
+++ b/res/ari/resource_applications.h
@@ -127,5 +127,33 @@
  * \param[out] response HTTP response
  */
 void ast_ari_applications_unsubscribe(struct ast_variable *headers, struct ast_ari_applications_unsubscribe_args *args, struct ast_ari_response *response);
+/*! Argument struct for ast_ari_applications_filter() */
+struct ast_ari_applications_filter_args {
+	/*! Application's name */
+	const char *application_name;
+	/*! Specify which event types to allow/disallow */
+	struct ast_json *filter;
+};
+/*!
+ * \brief Body parsing function for /applications/{applicationName}/eventFilter.
+ * \param body The JSON body from which to parse parameters.
+ * \param[out] args The args structure to parse into.
+ * \retval zero on success
+ * \retval non-zero on failure
+ */
+int ast_ari_applications_filter_parse_body(
+	struct ast_json *body,
+	struct ast_ari_applications_filter_args *args);
+
+/*!
+ * \brief Filter application events types.
+ *
+ * Allowed and/or disallowed event type filtering can be done. The body (parameter) should specify a JSON key/value object that describes the type of event filtering needed. One, or both of the following keys can be designated:<br /><br />"allowed" - Specifies an allowed list of event types<br />"disallowed" - Specifies a disallowed list of event types<br /><br />Further, each of those key's value should be a JSON array that holds zero, or more JSON key/value objects. Each of these objects must contain the following key with an associated value:<br /><br />"type" - The type name of the event to filter<br /><br />The value must be the string name (case sensitive) of the event type that needs filtering. For example:<br /><br />{ "allowed": [ { "type": "StasisStart" }, { "type": "StasisEnd" } ] }<br /><br />As this specifies only an allowed list, then only those two event type messages are sent to the application. No other event messages are sent.<br /><br />The following rules apply:<br /><br />* If the body is empty, both the allowed and disallowed filters are set empty.<br />* If both list types are given then both are set to their respective values (note, specifying an empty array for a given type sets that type to empty).<br />* If only one list type is given then only that type is set. The other type is not updated.<br />* An empty "allowed" list means all events are allowed.<br />* An empty "disallowed" list means no events are disallowed.<br />* Disallowed events take precedence over allowed events if the event type is specified in both lists.
+ *
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void ast_ari_applications_filter(struct ast_variable *headers, struct ast_ari_applications_filter_args *args, struct ast_ari_response *response);
 
 #endif /* _ASTERISK_RESOURCE_APPLICATIONS_H */
diff --git a/res/ari/resource_events.c b/res/ari/resource_events.c
index fb996f7..20d9eaa 100644
--- a/res/ari/resource_events.c
+++ b/res/ari/resource_events.c
@@ -143,7 +143,7 @@
 	}
 
 	ao2_lock(session);
-	if (session->ws_session) {
+	if (session->ws_session && stasis_app_event_allowed(app_name, message)) {
 		if (stasis_app_get_debug_by_name(app_name)) {
 			char *str = ast_json_dump_string_format(message, ast_ari_json_format());
 
diff --git a/res/res_ari_applications.c b/res/res_ari_applications.c
index c70b926..4d5be3f 100644
--- a/res/res_ari_applications.c
+++ b/res/res_ari_applications.c
@@ -461,6 +461,74 @@
 	ast_free(args.event_source);
 	return;
 }
+int ast_ari_applications_filter_parse_body(
+	struct ast_json *body,
+	struct ast_ari_applications_filter_args *args)
+{
+	/* Parse query parameters out of it */
+	return 0;
+}
+
+/*!
+ * \brief Parameter parsing callback for /applications/{applicationName}/eventFilter.
+ * \param get_params GET parameters in the HTTP request.
+ * \param path_vars Path variables extracted from the request.
+ * \param headers HTTP headers.
+ * \param[out] response Response to the HTTP request.
+ */
+static void ast_ari_applications_filter_cb(
+	struct ast_tcptls_session_instance *ser,
+	struct ast_variable *get_params, struct ast_variable *path_vars,
+	struct ast_variable *headers, struct ast_json *body, struct ast_ari_response *response)
+{
+	struct ast_ari_applications_filter_args args = {};
+	struct ast_variable *i;
+#if defined(AST_DEVMODE)
+	int is_valid;
+	int code;
+#endif /* AST_DEVMODE */
+
+	for (i = path_vars; i; i = i->next) {
+		if (strcmp(i->name, "applicationName") == 0) {
+			args.application_name = (i->value);
+		} else
+		{}
+	}
+	args.filter = body;
+	ast_ari_applications_filter(headers, &args, response);
+#if defined(AST_DEVMODE)
+	code = response->response_code;
+
+	switch (code) {
+	case 0: /* Implementation is still a stub, or the code wasn't set */
+		is_valid = response->message == NULL;
+		break;
+	case 500: /* Internal Server Error */
+	case 501: /* Not Implemented */
+	case 400: /* Bad request. */
+	case 404: /* Application does not exist. */
+		is_valid = 1;
+		break;
+	default:
+		if (200 <= code && code <= 299) {
+			is_valid = ast_ari_validate_application(
+				response->message);
+		} else {
+			ast_log(LOG_ERROR, "Invalid error response %d for /applications/{applicationName}/eventFilter\n", code);
+			is_valid = 0;
+		}
+	}
+
+	if (!is_valid) {
+		ast_log(LOG_ERROR, "Response validation failed for /applications/{applicationName}/eventFilter\n");
+		ast_ari_response_error(response, 500,
+			"Internal Server Error", "Response validation failed");
+	}
+#endif /* AST_DEVMODE */
+
+fin: __attribute__((unused))
+	return;
+}
 
 /*! \brief REST handler for /api-docs/applications.json */
 static struct stasis_rest_handlers applications_applicationName_subscription = {
@@ -473,14 +541,23 @@
 	.children = {  }
 };
 /*! \brief REST handler for /api-docs/applications.json */
+static struct stasis_rest_handlers applications_applicationName_eventFilter = {
+	.path_segment = "eventFilter",
+	.callbacks = {
+		[AST_HTTP_PUT] = ast_ari_applications_filter_cb,
+	},
+	.num_children = 0,
+	.children = {  }
+};
+/*! \brief REST handler for /api-docs/applications.json */
 static struct stasis_rest_handlers applications_applicationName = {
 	.path_segment = "applicationName",
 	.is_wildcard = 1,
 	.callbacks = {
 		[AST_HTTP_GET] = ast_ari_applications_get_cb,
 	},
-	.num_children = 1,
-	.children = { &applications_applicationName_subscription, }
+	.num_children = 2,
+	.children = { &applications_applicationName_subscription,&applications_applicationName_eventFilter, }
 };
 /*! \brief REST handler for /api-docs/applications.json */
 static struct stasis_rest_handlers applications = {
diff --git a/res/res_stasis.c b/res/res_stasis.c
index 127d0e8..9cb56c6 100644
--- a/res/res_stasis.c
+++ b/res/res_stasis.c
@@ -1670,13 +1670,14 @@
 	return json;
 }
 
-static struct ast_json *stasis_app_object_to_json(struct stasis_app *app)
+struct ast_json *stasis_app_object_to_json(struct stasis_app *app)
 {
 	if (!app) {
 		return NULL;
 	}
 
-	return app_event_sources_to_json(app, app_to_json(app));
+	return stasis_app_event_filter_to_json(
+		app, app_event_sources_to_json(app, app_to_json(app)));
 }
 
 struct ast_json *stasis_app_to_json(const char *app_name)
diff --git a/res/stasis/app.c b/res/stasis/app.c
index e48d8cc..6642bc4 100644
--- a/res/stasis/app.c
+++ b/res/stasis/app.c
@@ -67,6 +67,10 @@
 	enum stasis_app_subscription_model subscription_model;
 	/*! Whether or not someone wants to see debug messages about this app */
 	int debug;
+	/*! An array of allowed events types for this application */
+	struct ast_json *allowed;
+	/*! An array of disallowed events types for this application */
+	struct ast_json *disallowed;
 	/*! Name of the Stasis application */
 	char name[];
 };
@@ -319,6 +323,12 @@
 	app->forwards = NULL;
 	ao2_cleanup(app->data);
 	app->data = NULL;
+
+	ast_json_unref(app->allowed);
+	app->allowed = NULL;
+	ast_json_unref(app->disallowed);
+	app->disallowed = NULL;
+
 }
 
 static void call_forwarded_handler(struct stasis_app *app, struct stasis_message *message)
@@ -1653,3 +1663,119 @@
 	stasis_app_unregister_event_source(&bridge_event_source);
 	stasis_app_unregister_event_source(&channel_event_source);
 }
+
+struct ast_json *stasis_app_event_filter_to_json(struct stasis_app *app, struct ast_json *json)
+{
+	if (!app || !json) {
+		return json;
+	}
+
+	ast_json_object_set(json, "allowed_events", app->allowed ?
+		ast_json_ref(app->allowed) : ast_json_array_create());
+	ast_json_object_set(json, "disallowed_events", app->disallowed ?
+		ast_json_ref(app->disallowed) : ast_json_array_create());
+
+	return json;
+}
+
+static int app_event_filter_set(struct stasis_app *app,	struct ast_json **member,
+	struct ast_json *filter, const char *filter_type)
+{
+	if (filter && ast_json_typeof(filter) == AST_JSON_OBJECT) {
+		if (!ast_json_object_size(filter)) {
+			/* If no filters are specified then reset this filter type */
+			filter = NULL;
+		} else {
+			/* Otherwise try to get the filter array for this type */
+			filter = ast_json_object_get(filter, filter_type);
+			if (!filter) {
+				/* A filter type exists, but not this one, so don't update */
+				return 0;
+			}
+		}
+	}
+
+	/* At this point the filter object should be an array */
+	if (filter && ast_json_typeof(filter) != AST_JSON_ARRAY) {
+		ast_log(LOG_ERROR, "Invalid json type event filter - app: %s, filter: %s\n",
+				app->name, filter_type);
+		return -1;
+	}
+
+	if (filter) {
+		/* Confirm that at least the type names are specified */
+		struct ast_json *obj;
+		int i;
+
+		for (i = 0; i < ast_json_array_size(filter) &&
+				 (obj = ast_json_array_get(filter, i)); ++i) {
+
+			if (ast_strlen_zero(ast_json_object_string_get(obj, "type"))) {
+				ast_log(LOG_ERROR, "Filter event must have a type - app: %s, "
+						"filter: %s\n",	app->name, filter_type);
+				return -1;
+			}
+		}
+	}
+
+	ao2_lock(app);
+	ast_json_unref(*member);
+	*member = filter ? ast_json_ref(filter) : NULL;
+	ao2_unlock(app);
+
+	return 0;
+}
+
+static int app_allowed_set(struct stasis_app *app, struct ast_json *filter)
+{
+	return app_event_filter_set(app, &app->allowed, filter, "allowed");
+}
+
+static int app_disallowed_set(struct stasis_app *app, struct ast_json *filter)
+{
+	return app_event_filter_set(app, &app->disallowed, filter, "disallowed");
+}
+
+int stasis_app_event_filter_set(struct stasis_app *app, struct ast_json *filter)
+{
+	return app_disallowed_set(app, filter) || app_allowed_set(app, filter);
+}
+
+static int app_event_filter_matched(struct ast_json *array, struct ast_json *event, int empty)
+{
+	struct ast_json *obj;
+	int i;
+
+	if (!array || !ast_json_array_size(array)) {
+		return empty;
+	}
+
+	for (i = 0; i < ast_json_array_size(array) &&
+			(obj = ast_json_array_get(array, i)); ++i) {
+
+		if (!ast_strcmp_s(ast_json_object_string_get(obj, "type"),
+						  ast_json_object_string_get(event, "type"))) {
+			return 1;
+		}
+	}
+
+	return 0;
+}
+
+int stasis_app_event_allowed(const char *app_name, struct ast_json *event)
+{
+	struct stasis_app *app = stasis_app_get_by_name(app_name);
+	int res;
+
+	if (!app) {
+		return 0;
+	}
+
+	ao2_lock(app);
+	res = !app_event_filter_matched(app->disallowed, event, 0) &&
+		app_event_filter_matched(app->allowed, event, 1);
+	ao2_unlock(app);
+	ao2_ref(app, -1);
+
+	return res;
+}
diff --git a/rest-api/api-docs/applications.json b/rest-api/api-docs/applications.json
index 5ed720d..d0eec1a 100644
--- a/rest-api/api-docs/applications.json
+++ b/rest-api/api-docs/applications.json
@@ -134,6 +134,47 @@
 					]
 				}
 			]
+		},
+		{
+			"path": "/applications/{applicationName}/eventFilter",
+			"description": "Stasis application",
+			"operations": [
+				{
+					"httpMethod": "PUT",
+					"summary": "Filter application events types.",
+					"notes": "Allowed and/or disallowed event type filtering can be done. The body (parameter) should specify a JSON key/value object that describes the type of event filtering needed. One, or both of the following keys can be designated:<br /><br />\"allowed\" - Specifies an allowed list of event types<br />\"disallowed\" - Specifies a disallowed list of event types<br /><br />Further, each of those key's value should be a JSON array that holds zero, or more JSON key/value objects. Each of these objects must contain the following key with an associated value:<br /><br />\"type\" - The type name of the event to filter<br /><br />The value must be the string name (case sensitive) of the event type that needs filtering. For example:<br /><br />{ \"allowed\": [ { \"type\": \"StasisStart\" }, { \"type\": \"StasisEnd\" } ] }<br /><br />As this specifies only an allowed list, then only those two event type messages are sent to the application. No other event messages are sent.<br /><br />The following rules apply:<br /><br />* If the body is empty, both the allowed and disallowed filters are set empty.<br />* If both list types are given then both are set to their respective values (note, specifying an empty array for a given type sets that type to empty).<br />* If only one list type is given then only that type is set. The other type is not updated.<br />* An empty \"allowed\" list means all events are allowed.<br />* An empty \"disallowed\" list means no events are disallowed.<br />* Disallowed events take precedence over allowed events if the event type is specified in both lists.",
+					"nickname": "filter",
+					"responseClass": "Application",
+					"parameters": [
+						{
+							"name": "applicationName",
+							"description": "Application's name",
+							"paramType": "path",
+							"required": true,
+							"allowMultiple": false,
+							"dataType": "string"
+						},
+						{
+							"name": "filter",
+							"description": "Specify which event types to allow/disallow",
+							"paramType": "body",
+							"required": false,
+							"dataType": "object",
+							"allowMultiple": false
+						}
+					],
+					"errorResponses": [
+						{
+							"code": 400,
+							"reason": "Bad request."
+						},
+						{
+							"code": 404,
+							"reason": "Application does not exist."
+						}
+					]
+				}
+			]
 		}
 	],
 	"models": {
@@ -165,6 +206,16 @@
 					"type": "List[string]",
 					"description": "Names of the devices subscribed to.",
 					"required": true
+				},
+				"allowed_events": {
+					"type": "List[object]",
+					"description": "Event types sent to the application.",
+					"required": true
+				},
+				"disallowed_events": {
+					"type": "List[object]",
+					"description": "Event types not sent to the application.",
+					"required": true
 				}
 			}
 		}

-- 
To view, visit https://gerrit.asterisk.org/10978
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: asterisk
Gerrit-Branch: 13
Gerrit-MessageType: newchange
Gerrit-Change-Id: I9671ba1fcdb3b6c830b553d4c5365aed5d588d5b
Gerrit-Change-Number: 10978
Gerrit-PatchSet: 1
Gerrit-Owner: Kevin Harwell <kharwell at digium.com>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20190208/fc3c3f1a/attachment-0001.html>


More information about the asterisk-code-review mailing list