}
}
-/*!
- * \brief Authenticate a <code>?api_key=userid:password</code>
- *
- * \param api_key API key query parameter
- * \return User object for the authenticated user.
- * \retval NULL if authentication failed.
+/*
+ * This function is actually copied from http.c. Didn't see a need to make
+ * that function public just for this one use case.
*/
-static struct ari_conf_user *authenticate_api_key(const char *api_key)
+static struct ast_http_auth *create_http_auth(const char *userid, const char *password)
{
- RAII_VAR(char *, copy, NULL, ast_free);
- char *username;
- char *password;
+ struct ast_http_auth *auth;
+ size_t userid_len;
+ size_t password_len;
- password = copy = ast_strdup(api_key);
- if (!copy) {
+ if (!userid || !password) {
+ ast_log(LOG_ERROR, "Invalid userid/password\n");
return NULL;
}
- username = strsep(&password, ":");
- if (!password) {
- ast_log(LOG_WARNING, "Invalid api_key\n");
+ userid_len = strlen(userid) + 1;
+ password_len = strlen(password) + 1;
+
+ /* Allocate enough room to store everything in one memory block */
+ auth = ao2_alloc_options(sizeof(*auth) + userid_len + password_len, NULL, AO2_ALLOC_OPT_LOCK_NOLOCK);
+ if (!auth) {
return NULL;
}
- return ari_conf_validate_user(username, password);
+ /* Put the userid right after the struct */
+ auth->userid = (char *)(auth + 1);
+ strcpy(auth->userid, userid);
+
+ /* Put the password right after the userid */
+ auth->password = auth->userid + userid_len;
+ strcpy(auth->password, password);
+
+ return auth;
}
+/*!
+ * \brief Parse an api_key parameter from the query parameters into an ast_http_auth structure.
+ *
+ * \param get_params query string parameters
+ * \return ast_http_user object for the api_key.
+ * \retval NULL if api_key wasn't found or is had no password.
+ */
+static struct ast_http_auth *parse_api_keys(struct ast_variable *get_params)
+{
+ struct ast_variable *v;
+
+ for (v = get_params; v; v = v->next) {
+ if (ast_strings_equal(v->name, "api_key")) {
+ if (!ast_strlen_zero(v->value)) {
+ char *password = ast_strdupa(v->value);
+ char *username = strsep(&password, ":");
+ if (!ast_strlen_zero(password)) {
+ return create_http_auth(username, password);
+ }
+ }
+ break;
+ }
+ }
+ return NULL;
+}
+
+#if AST_DEVMODE
+static void test_ari_user_destructor(void *obj)
+{
+ struct ari_conf_user *ari_user = obj;
+ ast_string_field_free_memory(ari_user);
+}
+#endif
+
/*!
* \brief Authenticate an HTTP request.
*
* \retval NULL if authentication failed.
*/
static struct ari_conf_user *authenticate_user(struct ast_variable *get_params,
- struct ast_variable *headers)
+ struct ast_variable *headers, enum ast_ari_invoke_source source)
{
RAII_VAR(struct ast_http_auth *, http_auth, NULL, ao2_cleanup);
- struct ast_variable *v;
+ struct ari_conf_user *ari_user = NULL;
+ SCOPE_ENTER(3, "\n");
/* HTTP Basic authentication */
http_auth = ast_http_get_auth(headers);
- if (http_auth) {
- return ari_conf_validate_user(http_auth->userid,
- http_auth->password);
+ if (!http_auth) {
+ /* ?api_key authentication */
+ http_auth = parse_api_keys(get_params);
+ }
+ if (!http_auth) {
+ SCOPE_EXIT_RTN_VALUE(NULL, "No credentials found in HTTP headers or api_key\n");
}
- /* ?api_key authentication */
- for (v = get_params; v; v = v->next) {
- if (strcasecmp("api_key", v->name) == 0) {
- return authenticate_api_key(v->value);
+ if (source != ARI_INVOKE_SOURCE_TEST) {
+ ast_trace(-1, "Validating user %s\n", http_auth->userid);
+ ari_user = ari_conf_validate_user(http_auth->userid, http_auth->password);
+ if (!ari_user) {
+ SCOPE_EXIT_RTN_VALUE(NULL, "User %s failed to validate\n", http_auth->userid);
}
+ SCOPE_EXIT_RTN_VALUE(ari_user, "User %s validated. Read only: %s\n",
+ http_auth->userid, AST_YESNO(ari_user->read_only));
}
+#if AST_DEVMODE
+ /*
+ * ARI_INVOKE_SOURCE_TEST should only be set by the tests in test_ari.c but
+ * as a safety precaution, only check against the test usernames and passwords
+ * if we're in DEVMODE.
+ */
+ else {
+ int readonly = 0;
+ if (ast_strings_equal(http_auth->userid, "aritest")) {
+ if (!ast_strings_equal(http_auth->password, "aritestpw")) {
+ SCOPE_EXIT_RTN_VALUE(NULL, "User %s failed to validate\n", http_auth->userid);
+ }
+ } else if (ast_strings_equal(http_auth->userid, "aritestro")) {
+ if (!ast_strings_equal(http_auth->password, "aritestropw")) {
+ SCOPE_EXIT_RTN_VALUE(NULL, "User %s failed to validate\n", http_auth->userid);
+ }
+ readonly = 1;
+ }
+
+ ari_user = ao2_alloc(sizeof(*ari_user), test_ari_user_destructor);
+ if (!ari_user) {
+ SCOPE_EXIT_RTN_VALUE(NULL, "alloc failed\n");
+ }
+ if (ast_string_field_init(ari_user, 256) != 0) {
+ SCOPE_EXIT_RTN_VALUE(NULL, "alloc failed\n");
+ }
+ ast_string_field_set(ari_user, password, http_auth->password);
+ ari_user->read_only = readonly;
+
+ SCOPE_EXIT_RTN_VALUE(ari_user, "User %s validated. Read only: %s\n",
+ http_auth->userid, AST_YESNO(ari_user->read_only));
+ }
+
+#else
return NULL;
+#endif
}
static void remove_trailing_slash(const char *uri,
response->response_code, response->response_text);
}
- user = authenticate_user(get_params, headers);
-
- if (!user && source == ARI_INVOKE_SOURCE_REST) {
- /* Per RFC 2617, section 1.2: The 401 (Unauthorized) response
- * message is used by an origin server to challenge the
- * authorization of a user agent. This response MUST include a
- * WWW-Authenticate header field containing at least one
- * challenge applicable to the requested resource.
- */
+ user = authenticate_user(get_params, headers, source);
+ if (!user) {
ast_ari_response_error(response, 401, "Unauthorized", "Authentication required");
-
- /* Section 1.2:
- * realm = "realm" "=" realm-value
- * realm-value = quoted-string
- * Section 2:
- * challenge = "Basic" realm
- */
- ast_str_append(&response->headers, 0,
- "WWW-Authenticate: Basic realm=\"%s\"\r\n",
- general->auth_realm);
+ if (source == ARI_INVOKE_SOURCE_REST) {
+ /* Per RFC 2617, section 1.2: The 401 (Unauthorized) response
+ * message is used by an origin server to challenge the
+ * authorization of a user agent. This response MUST include a
+ * WWW-Authenticate header field containing at least one
+ * challenge applicable to the requested resource.
+ */
+
+ /* Section 1.2:
+ * realm = "realm" "=" realm-value
+ * realm-value = quoted-string
+ * Section 2:
+ * challenge = "Basic" realm
+ */
+ ast_str_append(&response->headers, 0,
+ "WWW-Authenticate: Basic realm=\"%s\"\r\n",
+ 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) &&
+ }
+
+ if (source == ARI_INVOKE_SOURCE_REST && ser && 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",
ast_ari_response_error(response, 503, "Service Unavailable", "Asterisk not booted");
SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CLOSE, "Response: %d : %s\n",
response->response_code, response->response_text);
- } else if (user && user->read_only && method != AST_HTTP_GET && method != AST_HTTP_OPTIONS) {
+ } else if (user->read_only && method != AST_HTTP_GET && method != AST_HTTP_OPTIONS) {
ast_ari_response_error(response, 403, "Forbidden", "Write access denied");
SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n",
response->response_code, response->response_text);
RAII_VAR(void *, fixture, NULL, tear_down_invocation_test);
RAII_VAR(struct ast_ari_response *, response, NULL, response_free);
RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref);
- struct ast_variable *get_params = NULL;
- struct ast_variable *headers = NULL;
+ struct ast_variable *tail = NULL;
+ RAII_VAR(struct ast_variable *, get_params, NULL, ast_variables_destroy);
+ RAII_VAR(struct ast_variable *, headers, NULL, ast_variables_destroy);
switch (cmd) {
case TEST_INIT:
fixture = setup_invocation_test();
response = response_alloc();
- get_params = ast_variable_new("get1", "get-one", __FILE__);
- ast_assert(get_params != NULL);
- get_params->next = ast_variable_new("get2", "get-two", __FILE__);
- ast_assert(get_params->next != NULL);
-
- headers = ast_variable_new("head1", "head-one", __FILE__);
- ast_assert(headers != NULL);
- headers->next = ast_variable_new("head2", "head-two", __FILE__);
- ast_assert(headers->next != NULL);
-
- expected = ast_json_pack("{s: s, s: {s: s, s: s}, s: {s: s, s: s}, s: {}}",
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("get1", "get-one", __FILE__));
+ ast_assert(tail != NULL);
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("get2", "get-two", __FILE__));
+ ast_assert(tail != NULL);
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("api_key", "aritestro:aritestropw", __FILE__));
+ ast_assert(tail != NULL);
+
+ tail = ast_variable_list_append_hint(&headers, NULL, ast_variable_new("head1", "head-one", __FILE__));
+ ast_assert(tail != NULL);
+ tail = ast_variable_list_append_hint(&headers, NULL, ast_variable_new("head2", "head-two", __FILE__));
+ ast_assert(tail != NULL);
+
+ expected = ast_json_pack("{s: s, s: {s: s, s: s, s: s}, s: {s: s, s: s}, s: {}}",
"name", "foo_get",
"get_params",
"get1", "get-one",
"get2", "get-two",
+ "api_key", "aritestro:aritestropw",
"headers",
"head1", "head-one",
"head2", "head-two",
RAII_VAR(void *, fixture, NULL, tear_down_invocation_test);
RAII_VAR(struct ast_ari_response *, response, NULL, response_free);
RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref);
- struct ast_variable *get_params = NULL;
+ struct ast_variable *tail = NULL;
+ RAII_VAR(struct ast_variable *, get_params, NULL, ast_variables_destroy);
struct ast_variable *headers = NULL;
switch (cmd) {
fixture = setup_invocation_test();
response = response_alloc();
- expected = ast_json_pack("{s: s, s: {}, s: {}, s: {s: s}}",
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("api_key", "aritestro:aritestropw", __FILE__));
+ ast_assert(tail != NULL);
+
+ expected = ast_json_pack("{s: s, s: {s: s}, s: {}, s: {s: s}}",
"name", "bam_get",
"get_params",
+ "api_key", "aritestro:aritestropw",
"headers",
"path_vars",
"bam", "foshizzle");
return AST_TEST_PASS;
}
-AST_TEST_DEFINE(invoke_delete)
+static enum ast_test_result_state invoke_delete_common(struct ast_test_info *info, enum ast_test_command cmd,
+ struct ast_test *test, const char *creds, int expect_pass, int expect_rc)
{
RAII_VAR(void *, fixture, NULL, tear_down_invocation_test);
RAII_VAR(struct ast_ari_response *, response, NULL, response_free);
RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref);
- struct ast_variable *get_params = NULL;
+ struct ast_variable *tail = NULL;
+ RAII_VAR(struct ast_variable *, get_params, NULL, ast_variables_destroy);
struct ast_variable *headers = NULL;
- switch (cmd) {
- case TEST_INIT:
- info->name = __func__;
- info->category = "/res/ari/";
- info->summary = "Test DELETE of an HTTP resource.";
- info->description = "Test ARI binding logic.";
- return AST_TEST_NOT_RUN;
- case TEST_EXECUTE:
- break;
- }
-
fixture = setup_invocation_test();
response = response_alloc();
- expected = ast_json_pack("{s: s, s: {}, s: {}, s: {s: s}}",
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("api_key", creds, __FILE__));
+ ast_assert(tail != NULL);
+
+ expected = ast_json_pack("{s: s, s: {s: s}, s: {}, s: {s: s}}",
"name", "bang_delete",
"get_params",
+ "api_key", creds,
"headers",
"path_vars",
"bam", "foshizzle");
ast_ari_invoke(NULL, ARI_INVOKE_SOURCE_TEST, NULL, "foo/foshizzle/bang", AST_HTTP_DELETE, get_params, headers,
ast_json_null(), response);
- ast_test_validate(test, 1 == invocation_count);
- ast_test_validate(test, 204 == response->response_code);
- ast_test_validate(test, ast_json_equal(expected, response->message));
+ ast_test_validate(test, expect_pass == invocation_count);
+ ast_test_validate(test, expect_rc == response->response_code);
+ ast_test_validate(test, ast_json_equal(expected, response->message) == expect_pass);
return AST_TEST_PASS;
}
+AST_TEST_DEFINE(invoke_delete)
+{
+ switch (cmd) {
+ case TEST_INIT:
+ info->name = __func__;
+ info->category = "/res/ari/";
+ info->summary = "Test DELETE of an HTTP resource.";
+ info->description = "Test ARI binding logic.";
+ return AST_TEST_NOT_RUN;
+ case TEST_EXECUTE:
+ break;
+ }
+
+ return invoke_delete_common(info, cmd, test, "aritest:aritestpw", 1, 204);
+}
+
+AST_TEST_DEFINE(invoke_delete_forbidden)
+{
+ switch (cmd) {
+ case TEST_INIT:
+ info->name = __func__;
+ info->category = "/res/ari/";
+ info->summary = "Test forbidden DELETE of an HTTP resource.";
+ info->description = "Test ARI binding logic.";
+ return AST_TEST_NOT_RUN;
+ case TEST_EXECUTE:
+ break;
+ }
+
+ return invoke_delete_common(info, cmd, test, "aritestro:aritestropw", 0, 403);
+}
+
AST_TEST_DEFINE(invoke_post)
{
RAII_VAR(void *, fixture, NULL, tear_down_invocation_test);
RAII_VAR(struct ast_ari_response *, response, NULL, response_free);
RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref);
- struct ast_variable *get_params = NULL;
- struct ast_variable *headers = NULL;
+ struct ast_variable *tail = NULL;
+ RAII_VAR(struct ast_variable *, get_params, NULL, ast_variables_destroy);
+ RAII_VAR(struct ast_variable *, headers, NULL, ast_variables_destroy);
switch (cmd) {
case TEST_INIT:
fixture = setup_invocation_test();
response = response_alloc();
- get_params = ast_variable_new("get1", "get-one", __FILE__);
- ast_assert(get_params != NULL);
- get_params->next = ast_variable_new("get2", "get-two", __FILE__);
- ast_assert(get_params->next != NULL);
-
- headers = ast_variable_new("head1", "head-one", __FILE__);
- ast_assert(headers != NULL);
- headers->next = ast_variable_new("head2", "head-two", __FILE__);
- ast_assert(headers->next != NULL);
-
- expected = ast_json_pack("{s: s, s: {s: s, s: s}, s: {s: s, s: s}, s: {}}",
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("get1", "get-one", __FILE__));
+ ast_assert(tail != NULL);
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("get2", "get-two", __FILE__));
+ ast_assert(tail != NULL);
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("api_key", "aritest:aritestpw", __FILE__));
+ ast_assert(tail != NULL);
+
+ tail = ast_variable_list_append_hint(&headers, NULL, ast_variable_new("head1", "head-one", __FILE__));
+ ast_assert(tail != NULL);
+ tail = ast_variable_list_append_hint(&headers, NULL, ast_variable_new("head2", "head-two", __FILE__));
+ ast_assert(tail != NULL);
+
+ expected = ast_json_pack("{s: s, s: {s: s, s: s, s: s}, s: {s: s, s: s}, s: {}}",
"name", "bar_post",
"get_params",
"get1", "get-one",
"get2", "get-two",
+ "api_key", "aritest:aritestpw",
"headers",
"head1", "head-one",
"head2", "head-two",
{
RAII_VAR(void *, fixture, NULL, tear_down_invocation_test);
RAII_VAR(struct ast_ari_response *, response, NULL, response_free);
- struct ast_variable *get_params = NULL;
+ struct ast_variable *tail = NULL;
+ RAII_VAR(struct ast_variable *, get_params, NULL, ast_variables_destroy);
struct ast_variable *headers = NULL;
switch (cmd) {
fixture = setup_invocation_test();
response = response_alloc();
+
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("api_key", "aritest:aritestpw", __FILE__));
+ ast_assert(tail != NULL);
+
ast_ari_invoke(NULL, ARI_INVOKE_SOURCE_TEST, NULL, "foo", AST_HTTP_POST, get_params, headers,
ast_json_null(), response);
return AST_TEST_PASS;
}
-AST_TEST_DEFINE(invoke_not_found)
+AST_TEST_DEFINE(invoke_no_user)
{
RAII_VAR(void *, fixture, NULL, tear_down_invocation_test);
RAII_VAR(struct ast_ari_response *, response, NULL, response_free);
struct ast_variable *get_params = NULL;
struct ast_variable *headers = NULL;
+ switch (cmd) {
+ case TEST_INIT:
+ info->name = __func__;
+ info->category = "/res/ari/";
+ info->summary = "Test POST on a resource that doesn't support it.";
+ info->description = "Test ARI binding logic.";
+ return AST_TEST_NOT_RUN;
+ case TEST_EXECUTE:
+ break;
+ }
+
+ fixture = setup_invocation_test();
+ response = response_alloc();
+ ast_ari_invoke(NULL, ARI_INVOKE_SOURCE_TEST, NULL, "foo", AST_HTTP_POST, get_params, headers,
+ ast_json_null(), response);
+
+ ast_test_validate(test, 0 == invocation_count);
+ ast_test_validate(test, 401 == response->response_code);
+
+ return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(invoke_not_found)
+{
+ RAII_VAR(void *, fixture, NULL, tear_down_invocation_test);
+ RAII_VAR(struct ast_ari_response *, response, NULL, response_free);
+ struct ast_variable *tail = NULL;
+ RAII_VAR(struct ast_variable *, get_params, NULL, ast_variables_destroy);
+ struct ast_variable *headers = NULL;
+
switch (cmd) {
case TEST_INIT:
info->name = __func__;
fixture = setup_invocation_test();
response = response_alloc();
+
+ tail = ast_variable_list_append_hint(&get_params, NULL, ast_variable_new("api_key", "aritest:aritestpw", __FILE__));
+ ast_assert(tail != NULL);
+
ast_ari_invoke(NULL, ARI_INVOKE_SOURCE_TEST, NULL, "foo/fizzle/i-am-not-a-resource", AST_HTTP_GET, get_params, headers,
ast_json_null(), response);
AST_TEST_UNREGISTER(invoke_get);
AST_TEST_UNREGISTER(invoke_wildcard);
AST_TEST_UNREGISTER(invoke_delete);
+ AST_TEST_UNREGISTER(invoke_delete_forbidden);
AST_TEST_UNREGISTER(invoke_post);
AST_TEST_UNREGISTER(invoke_bad_post);
+ AST_TEST_UNREGISTER(invoke_no_user);
AST_TEST_UNREGISTER(invoke_not_found);
return 0;
}
AST_TEST_REGISTER(invoke_get);
AST_TEST_REGISTER(invoke_wildcard);
AST_TEST_REGISTER(invoke_delete);
+ AST_TEST_REGISTER(invoke_delete_forbidden);
AST_TEST_REGISTER(invoke_post);
AST_TEST_REGISTER(invoke_bad_post);
+ AST_TEST_REGISTER(invoke_no_user);
AST_TEST_REGISTER(invoke_not_found);
return AST_MODULE_LOAD_SUCCESS;
}