This post is the seventh post of a series of articles on extending MySQL with the Component Infrastructure, the list above will be updated as new articles are published:
- Extending MySQL using the Component Infrastructure – part 1
- Extending MySQL using the Component Infrastructure – part 2: building the server
- Extending MySQL using the Component Infrastructure – part 3: component services
- Extending MySQL using the Component Infrastructure – part 4: error logging
- Extending MySQL using the Component Infrastructure – part 5: privileges
- Extending MySQL using the Component Infrastructure – part 6: functions
- Extending MySQL using the Component Infrastructure – part 7: messages to users
In the previous post, we coded the check for the right privilege to be able to run our function. We were able to log a message in error log but the user didn’t receive any message.
Additionally, logging a message in error log is only recommended when we want an admin to see it, now we want to user to get the message.
This is exactly what we gonna fix in this article.
As usual, the component infrastructure provides a service we can use to also print an error to the user: the MySQL Runtime Error Service included in mysql/components/services/mysql_runtime_error_service.h
.
Let’s modify our scan.h
to include that file and add the required service place holder:
/* Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. | |
This program is free software; you can redistribute it and/or modify | |
it under the terms of the GNU General Public License, version 2.0, | |
as published by the Free Software Foundation. | |
This program is also distributed with certain software (including | |
but not limited to OpenSSL) that is licensed under separate terms, | |
as designated in a particular file or component or in included license | |
documentation. The authors of MySQL hereby grant you an additional | |
permission to link the program and your derivative works with the | |
separately licensed software that they have included with MySQL. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License, version 2.0, for more details. | |
You should have received a copy of the GNU General Public License | |
along with this program; if not, write to the Free Software | |
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ | |
#define LOG_COMPONENT_TAG "viruscan" | |
#include <mysql/components/component_implementation.h> | |
#include <mysql/components/services/log_builtins.h> /* LogComponentErr */ | |
#include <mysqld_error.h> /* Errors */ | |
#include <mysql/components/services/dynamic_privilege.h> | |
#include <mysql/components/services/udf_metadata.h> | |
#include <mysql/components/services/udf_registration.h> | |
#include <mysql/components/services/security_context.h> | |
#include <mysql/components/services/mysql_current_thread_reader.h> | |
#include <mysql/components/services/mysql_runtime_error_service.h> | |
#include <list> | |
#include <string> | |
extern REQUIRES_SERVICE_PLACEHOLDER(log_builtins); | |
extern REQUIRES_SERVICE_PLACEHOLDER(log_builtins_string); | |
extern REQUIRES_SERVICE_PLACEHOLDER(dynamic_privilege_register); | |
extern REQUIRES_SERVICE_PLACEHOLDER(udf_registration); | |
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_udf_metadata); | |
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_thd_security_context); | |
extern REQUIRES_SERVICE_PLACEHOLDER(global_grants_check); | |
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_current_thread_reader); | |
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_runtime_error); | |
extern SERVICE_TYPE(log_builtins) * log_bi; | |
extern SERVICE_TYPE(log_builtins_string) * log_bs; |
This is what we did on lines 29 and 44.
Now we can also update scan.cc
to require that service and update our viruscan_udf
function to use that service using the right error message (ER_SPECIFIC_ACCESS_DENIED_ERROR
) to replace the message in error log on line 132 (you can find those message in include/mysqld_ername.h
):
/* Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. | |
This program is free software; you can redistribute it and/or modify | |
it under the terms of the GNU General Public License, version 2.0, | |
as published by the Free Software Foundation. | |
This program is also distributed with certain software (including | |
but not limited to OpenSSL) that is licensed under separate terms, | |
as designated in a particular file or component or in included license | |
documentation. The authors of MySQL hereby grant you an additional | |
permission to link the program and your derivative works with the | |
separately licensed software that they have included with MySQL. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License, version 2.0, for more details. | |
You should have received a copy of the GNU General Public License | |
along with this program; if not, write to the Free Software | |
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ | |
#define LOG_COMPONENT_TAG "viruscan" | |
#define NO_SIGNATURE_CHANGE 0 | |
#define SIGNATURE_CHANGE 1 | |
#include <components/viruscan/scan.h> | |
REQUIRES_SERVICE_PLACEHOLDER(log_builtins); | |
REQUIRES_SERVICE_PLACEHOLDER(log_builtins_string); | |
REQUIRES_SERVICE_PLACEHOLDER(dynamic_privilege_register); | |
REQUIRES_SERVICE_PLACEHOLDER(udf_registration); | |
REQUIRES_SERVICE_PLACEHOLDER(mysql_udf_metadata); | |
REQUIRES_SERVICE_PLACEHOLDER(mysql_thd_security_context); | |
REQUIRES_SERVICE_PLACEHOLDER(global_grants_check); | |
REQUIRES_SERVICE_PLACEHOLDER(mysql_current_thread_reader); | |
REQUIRES_SERVICE_PLACEHOLDER(mysql_runtime_error); | |
SERVICE_TYPE(log_builtins) * log_bi; | |
SERVICE_TYPE(log_builtins_string) * log_bs; | |
static const char *SCAN_PRIVILEGE_NAME = "VIRUS_SCAN"; | |
class udf_list { | |
typedef std::list<std::string> udf_list_t; | |
public: | |
~udf_list() { unregister(); } | |
bool add_scalar(const char *func_name, enum Item_result return_type, | |
Udf_func_any func, Udf_func_init init_func = NULL, | |
Udf_func_deinit deinit_func = NULL) { | |
if (!mysql_service_udf_registration->udf_register( | |
func_name, return_type, func, init_func, deinit_func)) { | |
set.push_back(func_name); | |
return false; | |
} | |
return true; | |
} | |
bool unregister() { | |
udf_list_t delete_set; | |
/* try to unregister all of the udfs */ | |
for (auto udf : set) { | |
int was_present = 0; | |
if (!mysql_service_udf_registration->udf_unregister(udf.c_str(), | |
&was_present) || | |
!was_present) | |
delete_set.push_back(udf); | |
} | |
/* remove the unregistered ones from the list */ | |
for (auto udf : delete_set) set.remove(udf); | |
/* success: empty set */ | |
if (set.empty()) return false; | |
/* failure: entries still in the set */ | |
return true; | |
} | |
private: | |
udf_list_t set; | |
} * list; | |
namespace udf_impl { | |
bool have_virus_scan_privilege(void *opaque_thd) { | |
// get the security context of the thread | |
Security_context_handle ctx = nullptr; | |
if (mysql_service_mysql_thd_security_context->get(opaque_thd, &ctx) || !ctx) { | |
LogComponentErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, | |
"problem trying to get security context"); | |
return false; | |
} | |
if (mysql_service_global_grants_check->has_global_grant( | |
ctx, SCAN_PRIVILEGE_NAME, strlen(SCAN_PRIVILEGE_NAME))) | |
return true; | |
return false; | |
} | |
const char *udf_init = "udf_init", *my_udf = "my_udf", | |
*my_udf_clear = "my_clear", *my_udf_add = "my_udf_add"; | |
static bool viruscan_udf_init(UDF_INIT *initid, UDF_ARGS *, char *) { | |
const char* name = "utf8mb4"; | |
char *value = const_cast<char*>(name); | |
initid->ptr = const_cast<char *>(udf_init); | |
if (mysql_service_mysql_udf_metadata->result_set( | |
initid, "charset", | |
const_cast<char *>(value))) { | |
LogComponentErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, "failed to set result charset"); | |
return false; | |
} | |
return 0; | |
} | |
static void viruscan_udf_deinit(__attribute__((unused)) UDF_INIT *initid) { | |
assert(initid->ptr == udf_init || initid->ptr == my_udf); | |
} | |
const char *viruscan_udf(UDF_INIT *initid, UDF_ARGS *args, char *outp, | |
unsigned long *length, char *is_null, char *error) { | |
MYSQL_THD thd; | |
mysql_service_mysql_current_thread_reader->get(&thd); | |
if(!have_virus_scan_privilege(thd)) { | |
mysql_error_service_printf( | |
ER_SPECIFIC_ACCESS_DENIED_ERROR, 0, | |
SCAN_PRIVILEGE_NAME); | |
return 0; | |
} | |
strcpy(outp, "we do nothing yet"); | |
*length = strlen(outp); | |
return const_cast<char *>(outp); | |
} | |
} /* namespace udf_impl */ | |
static mysql_service_status_t viruscan_service_init() { | |
mysql_service_status_t result = 0; | |
log_bi = mysql_service_log_builtins; | |
log_bs = mysql_service_log_builtins_string; | |
LogComponentErr(INFORMATION_LEVEL, ER_LOG_PRINTF_MSG, "initializing…"); | |
// Registration of the privilege | |
if (mysql_service_dynamic_privilege_register->register_privilege(SCAN_PRIVILEGE_NAME, strlen(SCAN_PRIVILEGE_NAME))) { | |
LogComponentErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, | |
"could not register privilege 'VIRUS_SCAN'."); | |
result = 1; | |
} else { | |
LogComponentErr(INFORMATION_LEVEL, ER_LOG_PRINTF_MSG, | |
"new privilege 'VIRUS_SCAN' has been registered successfully."); | |
} | |
list = new udf_list(); | |
if (list->add_scalar("virus_scan", Item_result::STRING_RESULT, | |
(Udf_func_any)udf_impl::viruscan_udf, | |
udf_impl::viruscan_udf_init, | |
udf_impl::viruscan_udf_deinit)) { | |
delete list; | |
return 1; /* failure: one of the UDF registrations failed */ | |
} | |
return result; | |
} | |
static mysql_service_status_t viruscan_service_deinit() { | |
mysql_service_status_t result = 0; | |
if (mysql_service_dynamic_privilege_register->unregister_privilege(SCAN_PRIVILEGE_NAME, strlen(SCAN_PRIVILEGE_NAME))) { | |
LogComponentErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, | |
"could not unregister privilege 'VIRUS_SCAN'."); | |
result = 1; | |
} else { | |
LogComponentErr(INFORMATION_LEVEL, ER_LOG_PRINTF_MSG, | |
"privilege 'VIRUS_SCAN' has been unregistered successfully."); | |
} | |
if (list->unregister()) return 1; /* failure: some UDFs still in use */ | |
delete list; | |
LogComponentErr(INFORMATION_LEVEL, ER_LOG_PRINTF_MSG, "uninstalled."); | |
return result; | |
} | |
BEGIN_COMPONENT_PROVIDES(viruscan_service) | |
END_COMPONENT_PROVIDES(); | |
BEGIN_COMPONENT_REQUIRES(viruscan_service) | |
REQUIRES_SERVICE(log_builtins), | |
REQUIRES_SERVICE(log_builtins_string), | |
REQUIRES_SERVICE(dynamic_privilege_register), | |
REQUIRES_SERVICE(mysql_udf_metadata), | |
REQUIRES_SERVICE(udf_registration), | |
REQUIRES_SERVICE(mysql_thd_security_context), | |
REQUIRES_SERVICE(global_grants_check), | |
REQUIRES_SERVICE(mysql_current_thread_reader), | |
REQUIRES_SERVICE(mysql_runtime_error), | |
END_COMPONENT_REQUIRES(); | |
/* A list of metadata to describe the Component. */ | |
BEGIN_COMPONENT_METADATA(viruscan_service) | |
METADATA("mysql.author", "Oracle Corporation"), | |
METADATA("mysql.license", "GPL"), METADATA("mysql.dev", "lefred"), | |
END_COMPONENT_METADATA(); | |
/* Declaration of the Component. */ | |
DECLARE_COMPONENT(viruscan_service, | |
"mysql:viruscan_service") | |
viruscan_service_init, | |
viruscan_service_deinit END_DECLARE_COMPONENT(); | |
/* Defines list of Components contained in this library. Note that for now | |
we assume that library will have exactly one Component. */ | |
DECLARE_LIBRARY_COMPONENTS &COMPONENT_REF(viruscan_service) | |
END_DECLARE_LIBRARY_COMPONENTS |
Let’s test it after compiling the new code and running mtr again (see part 2):
Excellent, this is exactly what we were looking for.
Extra
On lines 91 and 92 we get the security context. I would like to show that from that security context we can for example get the user and host:
Security_context_handle ctx = nullptr; mysql_service_mysql_thd_security_context->get(thd, &ctx); MYSQL_LEX_CSTRING user; MYSQL_LEX_CSTRING host; mysql_service_mysql_security_context_options->get(ctx, "priv_user", &user); mysql_service_mysql_security_context_options->get(ctx, "priv_host", &host);
Conclusion
In this article we saw how we can return messages to the user using the appropriate component service.
The next article will cover how to use libclamav from our component.
Stay tuned, happy coding and enjoy MySQL !
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]
[…] Extending MySQL using the Component Infrastructure – part 7: messages to users […]