ARI event type filtering
authorKevin Harwell <kharwell@digium.com>
Fri, 8 Feb 2019 20:48:27 +0000 (14:48 -0600)
committerGeorge Joseph <gjoseph@digium.com>
Wed, 20 Feb 2019 15:56:22 +0000 (09:56 -0600)
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

CHANGES
include/asterisk/stasis_app.h
res/ari/ari_model_validators.c
res/ari/ari_model_validators.h
res/ari/resource_applications.c
res/ari/resource_applications.h
res/ari/resource_events.c
res/res_ari_applications.c
res/res_stasis.c
res/stasis/app.c
rest-api/api-docs/applications.json

diff --git a/CHANGES b/CHANGES
index 4f41c39..78e019e 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -72,6 +72,18 @@ Bridging
    the last snapshot and the new one.
 
 ------------------------------------------------------------------------------
+--- Functionality changes from Asterisk 16.2.0 to Asterisk 16.3.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 16.1.0 to Asterisk 16.2.0 ------------
 ------------------------------------------------------------------------------
 
index b0829ab..c40e901 100644 (file)
@@ -149,6 +149,17 @@ const char *stasis_app_name(const struct stasis_app *app);
 
 /*!
  * \brief Return the JSON representation of a Stasis application.
+ * \since 16.3.0
+ *
+ * \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.
  *
@@ -958,6 +969,41 @@ struct ast_cli_args;
  */
 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 */
index a06a1f5..44d9d77 100644 (file)
@@ -6534,6 +6534,8 @@ int ast_ari_validate_application(struct ast_json *json)
        int has_channel_ids = 0;
        int has_device_names = 0;
        int has_endpoint_ids = 0;
+       int has_events_allowed = 0;
+       int has_events_disallowed = 0;
        int has_name = 0;
 
        for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
@@ -6581,6 +6583,28 @@ int ast_ari_validate_application(struct ast_json *json)
                                res = 0;
                        }
                } else
+               if (strcmp("events_allowed", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       has_events_allowed = 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 events_allowed failed validation\n");
+                               res = 0;
+                       }
+               } else
+               if (strcmp("events_disallowed", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       has_events_disallowed = 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 events_disallowed failed validation\n");
+                               res = 0;
+                       }
+               } else
                if (strcmp("name", ast_json_object_iter_key(iter)) == 0) {
                        int prop_is_valid;
                        has_name = 1;
@@ -6619,6 +6643,16 @@ int ast_ari_validate_application(struct ast_json *json)
                res = 0;
        }
 
+       if (!has_events_allowed) {
+               ast_log(LOG_ERROR, "ARI Application missing required field events_allowed\n");
+               res = 0;
+       }
+
+       if (!has_events_disallowed) {
+               ast_log(LOG_ERROR, "ARI Application missing required field events_disallowed\n");
+               res = 0;
+       }
+
        if (!has_name) {
                ast_log(LOG_ERROR, "ARI Application missing required field name\n");
                res = 0;
index ab0b2f6..1ee74f4 100644 (file)
@@ -1830,6 +1830,8 @@ ari_validator ast_ari_validate_application_fn(void);
  * - channel_ids: List[string] (required)
  * - device_names: List[string] (required)
  * - endpoint_ids: List[string] (required)
+ * - events_allowed: List[object] (required)
+ * - events_disallowed: List[object] (required)
  * - name: string (required)
  */
 
index fc93b28..4a97401 100644 (file)
@@ -168,3 +168,23 @@ void ast_ari_applications_unsubscribe(struct ast_variable *headers,
                        "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);
+}
index be62e9d..23a9b9b 100644 (file)
@@ -127,5 +127,33 @@ int ast_ari_applications_unsubscribe_parse_body(
  * \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 */
index 5983f7b..d935da1 100644 (file)
@@ -114,7 +114,7 @@ static void stasis_app_message_handler(
                                "Queued '%s' message for Stasis app '%s'; websocket is not ready\n",
                                msg_type,
                                msg_application);
-       } else {
+       } else if (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());
 
index fd8a448..4d1443e 100644 (file)
@@ -459,6 +459,74 @@ fin: __attribute__((unused))
        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 = {
@@ -471,14 +539,23 @@ static struct stasis_rest_handlers applications_applicationName_subscription = {
        .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 = {
index 86723a1..4da78a6 100644 (file)
@@ -1689,13 +1689,14 @@ static struct ast_json *app_event_sources_to_json(
        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)
index 9317e08..0f0923a 100644 (file)
@@ -65,6 +65,10 @@ struct stasis_app {
        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 *events_allowed;
+       /*! An array of disallowed events types for this application */
+       struct ast_json *events_disallowed;
        /*! Name of the Stasis application */
        char name[];
 };
@@ -303,6 +307,12 @@ static void app_dtor(void *obj)
        app->forwards = NULL;
        ao2_cleanup(app->data);
        app->data = NULL;
+
+       ast_json_unref(app->events_allowed);
+       app->events_allowed = NULL;
+       ast_json_unref(app->events_disallowed);
+       app->events_disallowed = NULL;
+
 }
 
 static void call_forwarded_handler(struct stasis_app *app, struct stasis_message *message)
@@ -1603,3 +1613,119 @@ void stasis_app_unregister_event_sources(void)
        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, "events_allowed", app->events_allowed ?
+               ast_json_ref(app->events_allowed) : ast_json_array_create());
+       ast_json_object_set(json, "events_disallowed", app->events_disallowed ?
+               ast_json_ref(app->events_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_events_allowed_set(struct stasis_app *app, struct ast_json *filter)
+{
+       return app_event_filter_set(app, &app->events_allowed, filter, "allowed");
+}
+
+static int app_events_disallowed_set(struct stasis_app *app, struct ast_json *filter)
+{
+       return app_event_filter_set(app, &app->events_disallowed, filter, "disallowed");
+}
+
+int stasis_app_event_filter_set(struct stasis_app *app, struct ast_json *filter)
+{
+       return app_events_disallowed_set(app, filter) || app_events_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_strings_equal(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->events_disallowed, event, 0) &&
+               app_event_filter_matched(app->events_allowed, event, 1);
+       ao2_unlock(app);
+       ao2_ref(app, -1);
+
+       return res;
+}
index cdd69c4..09c5cd5 100644 (file)
                                        ]
                                }
                        ]
+               },
+               {
+                       "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": {
                                        "type": "List[string]",
                                        "description": "Names of the devices subscribed to.",
                                        "required": true
+                               },
+                               "events_allowed": {
+                                       "type": "List[object]",
+                                       "description": "Event types sent to the application.",
+                                       "required": true
+                               },
+                               "events_disallowed": {
+                                       "type": "List[object]",
+                                       "description": "Event types not sent to the application.",
+                                       "required": true
                                }
                        }
                }