/* Copyright (c) 2000, 2019, 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 */ #include "sql/auth/sql_auth_cache.h" #include #include #include #include #include "m_ctype.h" #include "m_string.h" // LEX_CSTRING #include "my_base.h" #include "my_compiler.h" #include "my_dbug.h" #include "my_loglevel.h" #include "my_macros.h" #include "mysql/components/services/log_builtins.h" #include "mysql/components/services/log_shared.h" #include "mysql/components/services/psi_mutex_bits.h" #include "mysql/plugin.h" #include "mysql/plugin_audit.h" #include "mysql/plugin_auth.h" // st_mysql_auth #include "mysql/psi/mysql_mutex.h" #include "mysql/psi/psi_base.h" #include "mysql/service_mysql_alloc.h" #include "mysqld_error.h" #include "prealloced_array.h" #include "sql/auth/acl_table_user.h" // Acl_user_attributes #include "sql/auth/auth_acls.h" #include "sql/auth/auth_common.h" // ACL_internal_schema_access #include "sql/auth/auth_internal.h" // auth_plugin_is_built_in #include "sql/auth/dynamic_privilege_table.h" #include "sql/auth/sql_authentication.h" // g_cached_authentication_plugins #include "sql/auth/sql_security_ctx.h" #include "sql/auth/sql_user_table.h" #include "sql/auth/user_table.h" // read_user_table #include "sql/current_thd.h" // current_thd #include "sql/debug_sync.h" #include "sql/error_handler.h" // Internal_error_handler #include "sql/field.h" // Field #include "sql/handler.h" #include "sql/item_func.h" // mqh_used #include "sql/key.h" #include "sql/mdl.h" #include "sql/mysqld.h" // my_localhost #include "sql/psi_memory_key.h" // key_memory_acl_mem #include "sql/records.h" // unique_ptr_destroy_only #include "sql/row_iterator.h" #include "sql/sql_audit.h" #include "sql/sql_base.h" // open_and_lock_tables #include "sql/sql_class.h" // THD #include "sql/sql_const.h" #include "sql/sql_error.h" #include "sql/sql_lex.h" #include "sql/sql_plugin.h" // my_plugin_lock_by_name #include "sql/sql_plugin_ref.h" #include "sql/sql_time.h" // str_to_time_with_warn #include "sql/system_variables.h" #include "sql/table.h" // TABLE #include "sql/thd_raii.h" #include "sql/thr_malloc.h" #include "sql/xa.h" #include "sql_string.h" #include "thr_lock.h" #include "thr_mutex.h" #define INVALID_DATE "0000-00-00 00:00:00" #include #include #include #include #include using std::make_unique; using std::min; using std::move; using std::string; using std::unique_ptr; PSI_mutex_key key_LOCK_acl_cache_flush; PSI_mutex_info all_acl_cache_mutexes[] = { {&key_LOCK_acl_cache_flush, "LOCK_acl_cache_flush", PSI_FLAG_SINGLETON, 0, PSI_DOCUMENT_ME}}; Acl_cache *g_acl_cache = NULL; Acl_cache *get_global_acl_cache() { return g_acl_cache; } ulong get_global_acl_cache_size() { return g_acl_cache->size(); } void init_acl_cache(); extern Role_index_map *g_authid_to_vertex; extern Granted_roles_graph *g_granted_roles; #include struct ACL_internal_schema_registry_entry { const LEX_CSTRING *m_name; const ACL_internal_schema_access *m_access; }; /** Internal schema registered. Currently, this is only: - performance_schema - information_schema, This can be reused later for: - mysql */ static ACL_internal_schema_registry_entry registry_array[2]; static uint m_registry_array_size = 0; MEM_ROOT global_acl_memory; MEM_ROOT memex; Prealloced_array *acl_users = NULL; Prealloced_array *acl_proxy_users = NULL; Prealloced_array *acl_dbs = NULL; Prealloced_array *acl_wild_hosts = NULL; Db_access_map acl_db_map; Default_roles *g_default_roles = NULL; std::vector *g_mandatory_roles = NULL; unique_ptr< malloc_unordered_multimap>> column_priv_hash; unique_ptr< malloc_unordered_multimap>> proc_priv_hash, func_priv_hash; malloc_unordered_map> db_cache{ key_memory_acl_cache}; collation_unordered_map *acl_check_hosts = nullptr; unique_ptr acl_restrictions = nullptr; /** A hashmap on user part of account name for quick lookup. */ typedef std::unordered_map< std::string, Acl_user_ptr_list, std::hash, std::equal_to, Acl_cache_allocator>> Name_to_userlist; Name_to_userlist *name_to_userlist = nullptr; bool initialized = false; bool skip_grant_tables(void) { return !initialized; } bool acl_cache_initialized = false; bool allow_all_hosts = 1; uint grant_version = 0; /* Version of priv tables */ bool validate_user_plugins = true; #define IP_ADDR_STRLEN (3 + 1 + 3 + 1 + 3 + 1 + 3) #define ACL_KEY_LENGTH (IP_ADDR_STRLEN + 1 + NAME_LEN + 1 + USERNAME_LENGTH + 1) /** Allocates the memory in the the global_acl_memory MEM_ROOT. */ void init_acl_memory() { init_sql_alloc(key_memory_acl_mem, &global_acl_memory, ACL_ALLOC_BLOCK_SIZE, 0); } /** Add an internal schema to the registry. @param name the schema name @param access the schema ACL specific rules */ void ACL_internal_schema_registry::register_schema( const LEX_CSTRING &name, const ACL_internal_schema_access *access) { DBUG_ASSERT(m_registry_array_size < array_elements(registry_array)); /* Not thread safe, and does not need to be. */ registry_array[m_registry_array_size].m_name = &name; registry_array[m_registry_array_size].m_access = access; m_registry_array_size++; } /** Search per internal schema ACL by name. @param name a schema name @return per schema rules, or NULL */ const ACL_internal_schema_access *ACL_internal_schema_registry::lookup( const char *name) { DBUG_ASSERT(name != NULL); uint i; for (i = 0; i < m_registry_array_size; i++) { if (my_strcasecmp(system_charset_info, registry_array[i].m_name->str, name) == 0) return registry_array[i].m_access; } return NULL; } const char *ACL_HOST_AND_IP::calc_ip(const char *ip_arg, long *val, char end) { long ip_val, tmp; if (!(ip_arg = str2int(ip_arg, 10, 0, 255, &ip_val)) || *ip_arg != '.') return 0; ip_val <<= 24; if (!(ip_arg = str2int(ip_arg + 1, 10, 0, 255, &tmp)) || *ip_arg != '.') return 0; ip_val += tmp << 16; if (!(ip_arg = str2int(ip_arg + 1, 10, 0, 255, &tmp)) || *ip_arg != '.') return 0; ip_val += tmp << 8; if (!(ip_arg = str2int(ip_arg + 1, 10, 0, 255, &tmp)) || *ip_arg != end) return 0; *val = ip_val + tmp; return ip_arg; } /** @brief Update the hostname. Updates ip and ip_mask accordingly. @param host_arg Value to be stored */ void ACL_HOST_AND_IP::update_hostname(const char *host_arg) { hostname = host_arg; // This will not be modified! hostname_length = hostname ? strlen(hostname) : 0; if (!host_arg || (!(host_arg = calc_ip(host_arg, &ip, '/')) || !(host_arg = calc_ip(host_arg + 1, &ip_mask, '\0')))) { ip = ip_mask = 0; // Not a masked ip } } /* @brief Comparing of hostnames. @TODO This function should ideally only be called during authentication and not from authorization code. You may authenticate with a hostmask, but all authentication should be against a specific security context with a specific authentication ID. @param host_arg Hostname to be compared with @param ip_arg IP address to be compared with @notes A hostname may be of type: 1) hostname (May include wildcards); monty.pp.sci.fi 2) ip (May include wildcards); 192.168.0.0 3) ip/netmask 192.168.0.0/255.255.255.0 A net mask of 0.0.0.0 is not allowed. @return true if matched false if not matched */ bool ACL_HOST_AND_IP::compare_hostname(const char *host_arg, const char *ip_arg) { long tmp; if (ip_mask && ip_arg && calc_ip(ip_arg, &tmp, '\0')) { return (tmp & ip_mask) == ip; } return (!hostname || (host_arg && !wild_case_compare(system_charset_info, host_arg, hostname)) || (ip_arg && !wild_compare(ip_arg, strlen(ip_arg), hostname, strlen(hostname), 0))); } ACL_USER::ACL_USER() { /* ACL_ACCESS is initialized by its constructor */ { /* USER_RESOURCES */ user_resource.questions = 0; user_resource.updates = 0; user_resource.conn_per_hour = 0; user_resource.user_conn = 0; user_resource.specified_limits = 0; } user = nullptr; { /* TLS restrictions */ ssl_type = SSL_TYPE_NONE; ssl_cipher = nullptr; x509_issuer = nullptr; x509_subject = nullptr; } plugin = EMPTY_CSTR; password_expired = false; can_authenticate = false; password_last_changed.time_type = MYSQL_TIMESTAMP_ERROR; password_lifetime = 0; use_default_password_lifetime = false; account_locked = false; is_role = false; password_history_length = 0; use_default_password_history = false; password_reuse_interval = 0; use_default_password_reuse_interval = false; password_require_current = Lex_acl_attrib_udyn::DEFAULT; /* Acl_credentials is initialized by its constructor */ } ACL_USER *ACL_USER::copy(MEM_ROOT *root) { ACL_USER *dst = (ACL_USER *)root->Alloc(sizeof(ACL_USER)); if (!dst) return 0; *dst = *this; dst->user = safe_strdup_root(root, user); dst->ssl_cipher = safe_strdup_root(root, ssl_cipher); dst->x509_issuer = safe_strdup_root(root, x509_issuer); dst->x509_subject = safe_strdup_root(root, x509_subject); /* If the plugin is built in we don't need to reallocate the name of the plugin. */ if (auth_plugin_is_built_in(dst->plugin.str)) dst->plugin = plugin; else { dst->plugin.str = strmake_root(root, plugin.str, plugin.length); dst->plugin.length = plugin.length; } for (int i = 0; i < NUM_CREDENTIALS; ++i) { dst->credentials[i].m_auth_string.str = safe_strdup_root(root, credentials[i].m_auth_string.str); dst->credentials[i].m_auth_string.length = credentials[i].m_auth_string.length; dst->credentials[i].m_salt_len = credentials[i].m_salt_len; memcpy(dst->credentials[i].m_salt, credentials[i].m_salt, credentials[i].m_salt_len); } dst->host.update_hostname(safe_strdup_root(root, host.get_host())); dst->password_require_current = password_require_current; return dst; } void ACL_PROXY_USER::init(const char *host_arg, const char *user_arg, const char *proxied_host_arg, const char *proxied_user_arg, bool with_grant_arg) { user = (user_arg && *user_arg) ? user_arg : NULL; host.update_hostname((host_arg && *host_arg) ? host_arg : NULL); proxied_user = (proxied_user_arg && *proxied_user_arg) ? proxied_user_arg : NULL; proxied_host.update_hostname( (proxied_host_arg && *proxied_host_arg) ? proxied_host_arg : NULL); with_grant = with_grant_arg; sort = get_sort(4, host.get_host(), user, proxied_host.get_host(), proxied_user); } void ACL_PROXY_USER::init(MEM_ROOT *mem, const char *host_arg, const char *user_arg, const char *proxied_host_arg, const char *proxied_user_arg, bool with_grant_arg) { init((host_arg && *host_arg) ? strdup_root(mem, host_arg) : NULL, (user_arg && *user_arg) ? strdup_root(mem, user_arg) : NULL, (proxied_host_arg && *proxied_host_arg) ? strdup_root(mem, proxied_host_arg) : NULL, (proxied_user_arg && *proxied_user_arg) ? strdup_root(mem, proxied_user_arg) : NULL, with_grant_arg); } void ACL_PROXY_USER::init(TABLE *table, MEM_ROOT *mem) { init(get_field(mem, table->field[MYSQL_PROXIES_PRIV_HOST]), get_field(mem, table->field[MYSQL_PROXIES_PRIV_USER]), get_field(mem, table->field[MYSQL_PROXIES_PRIV_PROXIED_HOST]), get_field(mem, table->field[MYSQL_PROXIES_PRIV_PROXIED_USER]), table->field[MYSQL_PROXIES_PRIV_WITH_GRANT]->val_int() != 0); } bool ACL_PROXY_USER::check_validity(bool check_no_resolve) { if (check_no_resolve && (hostname_requires_resolving(host.get_host()) || hostname_requires_resolving(proxied_host.get_host())) && strcmp(host.get_host(), "localhost") != 0) { LogErr(WARNING_LEVEL, ER_AUTHCACHE_PROXIES_PRIV_SKIPPED_NEEDS_RESOLVE, proxied_user ? proxied_user : "", proxied_host.get_host() ? proxied_host.get_host() : "", user ? user : "", host.get_host() ? host.get_host() : ""); } return false; } bool ACL_PROXY_USER::matches(const char *host_arg, const char *user_arg, const char *ip_arg, const char *proxied_user_arg, bool any_proxy_user) { DBUG_TRACE; DBUG_PRINT("info", ("compare_hostname(%s,%s,%s) &&" "compare_hostname(%s,%s,%s) &&" "wild_compare (%s,%s) &&" "wild_compare (%s,%s)", host.get_host() ? host.get_host() : "", host_arg ? host_arg : "", ip_arg ? ip_arg : "", proxied_host.get_host() ? proxied_host.get_host() : "", host_arg ? host_arg : "", ip_arg ? ip_arg : "", user_arg ? user_arg : "", user ? user : "", proxied_user_arg ? proxied_user_arg : "", proxied_user ? proxied_user : "")); return host.compare_hostname(host_arg, ip_arg) && proxied_host.compare_hostname(host_arg, ip_arg) && (!user || (user_arg && !wild_compare(user_arg, strlen(user_arg), user, strlen(user), true))) && (any_proxy_user || !proxied_user || (proxied_user && !wild_compare(proxied_user_arg, strlen(proxied_user_arg), proxied_user, strlen(proxied_user), true))); } bool ACL_PROXY_USER::pk_equals(ACL_PROXY_USER *grant) { DBUG_TRACE; DBUG_PRINT("info", ("strcmp(%s,%s) &&" "strcmp(%s,%s) &&" "wild_compare (%s,%s) &&" "wild_compare (%s,%s)", user ? user : "", grant->user ? grant->user : "", proxied_user ? proxied_user : "", grant->proxied_user ? grant->proxied_user : "", host.get_host() ? host.get_host() : "", grant->host.get_host() ? grant->host.get_host() : "", proxied_host.get_host() ? proxied_host.get_host() : "", grant->proxied_host.get_host() ? grant->proxied_host.get_host() : "")); return auth_element_equals(user, grant->user) && auth_element_equals(proxied_user, grant->proxied_user) && auth_element_equals(host.get_host(), grant->host.get_host()) && auth_element_equals(proxied_host.get_host(), grant->proxied_host.get_host()); } void ACL_PROXY_USER::print_grant(String *str) { str->append(STRING_WITH_LEN("GRANT PROXY ON '")); if (proxied_user) str->append(proxied_user, strlen(proxied_user)); str->append(STRING_WITH_LEN("'@'")); if (proxied_host.get_host()) str->append(proxied_host.get_host(), strlen(proxied_host.get_host())); str->append(STRING_WITH_LEN("' TO '")); if (user) str->append(user, strlen(user)); str->append(STRING_WITH_LEN("'@'")); if (host.get_host()) str->append(host.get_host(), strlen(host.get_host())); str->append(STRING_WITH_LEN("'")); if (with_grant) str->append(STRING_WITH_LEN(" WITH GRANT OPTION")); } int ACL_PROXY_USER::store_pk(TABLE *table, const LEX_CSTRING &hostname, const LEX_CSTRING &user, const LEX_CSTRING &proxied_host, const LEX_CSTRING &proxied_user) { DBUG_TRACE; DBUG_PRINT("info", ("host=%s, user=%s, proxied_host=%s, proxied_user=%s", hostname.str ? hostname.str : "", user.str ? user.str : "", proxied_host.str ? proxied_host.str : "", proxied_user.str ? proxied_user.str : "")); if (table->field[MYSQL_PROXIES_PRIV_HOST]->store( hostname.str, hostname.length, system_charset_info)) return true; if (table->field[MYSQL_PROXIES_PRIV_USER]->store(user.str, user.length, system_charset_info)) return true; if (table->field[MYSQL_PROXIES_PRIV_PROXIED_HOST]->store( proxied_host.str, proxied_host.length, system_charset_info)) return true; if (table->field[MYSQL_PROXIES_PRIV_PROXIED_USER]->store( proxied_user.str, proxied_user.length, system_charset_info)) return true; return false; } int ACL_PROXY_USER::store_with_grant(TABLE *table, bool with_grant) { DBUG_TRACE; DBUG_PRINT("info", ("with_grant=%s", with_grant ? "TRUE" : "FALSE")); if (table->field[MYSQL_PROXIES_PRIV_WITH_GRANT]->store(with_grant ? 1 : 0, true)) return true; return false; } int ACL_PROXY_USER::store_data_record(TABLE *table, const LEX_CSTRING &hostname, const LEX_CSTRING &user, const LEX_CSTRING &proxied_host, const LEX_CSTRING &proxied_user, bool with_grant, const char *grantor) { DBUG_TRACE; if (store_pk(table, hostname, user, proxied_host, proxied_user)) return true; if (store_with_grant(table, with_grant)) return true; if (table->field[MYSQL_PROXIES_PRIV_GRANTOR]->store(grantor, strlen(grantor), system_charset_info)) return true; return false; } /** Performs wildcard matching, aka globbing, on the input string with the given wildcard pattern, and the specified wildcard characters. This method does case insensitive comparisons. @param[in] cs character set of the input string and wildcard pattern @param[in] str input which should be matched against pattern @param[in] str_len length of the input string @param[in] wildstr pattern with wildcards @param[in] wildstr_len length of the wildcards pattern @return 0 if input string match with the pattern @return 1 otherwise */ int wild_case_compare(CHARSET_INFO *cs, const char *str, size_t str_len, const char *wildstr, size_t wildstr_len) { int flag; DBUG_TRACE; DBUG_PRINT("enter", ("str: '%s' wildstr: '%s'", str, wildstr)); const char *wildstr_end = wildstr + wildstr_len; const char *str_end = str + str_len; /* Empty string matches only if there is only a wild_many(%) char in the string to be matched with. */ if (str_len == 0) { bool ret_value = true; if (wildstr_len == 1) { ret_value = !(*wildstr == wild_many); } return ret_value; } while (wildstr != wildstr_end && str != str_end) { while (wildstr != wildstr_end && *wildstr != wild_many && *wildstr != wild_one) { if (*wildstr == wild_prefix && wildstr[1]) wildstr++; if (my_toupper(cs, *wildstr++) != my_toupper(cs, *str++)) return 1; } if (wildstr == wildstr_end) { return str != str_end; } if (*wildstr++ == wild_one) { ++str; if (str == str_end) /* One char; skip */ { return wildstr != wildstr_end; } } else { /* Found '*' */ if (wildstr == wildstr_end) { return 0; /* '*' as last char: OK */ } flag = (*wildstr != wild_many && *wildstr != wild_one); do { if (flag) { char cmp; if ((cmp = *wildstr) == wild_prefix && wildstr[1]) cmp = wildstr[1]; cmp = my_toupper(cs, cmp); while (str != str_end && my_toupper(cs, *str) != cmp) str++; if (str == str_end) return 1; } if (wild_case_compare(cs, str, str_end - str, wildstr, wildstr_end - wildstr) == 0) return 0; ++str; } while (str != str_end); return 1; } } return str != str_end; } int wild_case_compare(CHARSET_INFO *cs, const char *str, const char *wildstr) { return wild_case_compare(cs, str, strlen(str), wildstr, strlen(wildstr)); } /* Return a number which, if sorted 'desc', puts strings in this order: no wildcards strings containg wildcards and non-wildcard characters single muilt-wildcard character('%') empty string */ ulong get_sort(uint count, ...) { va_list args; va_start(args, count); ulong sort = 0; /* Should not use this function with more than 4 arguments for compare. */ DBUG_ASSERT(count <= 4); while (count--) { char *start, *str = va_arg(args, char *); uint chars = 0; uint wild_pos = 0; /* wild_pos 0 if string is empty 1 if string is a single muilt-wildcard character('%') first wildcard position + 1 if string containg wildcards and non-wildcard characters */ if ((start = str)) { for (; *str; str++) { if (*str == wild_prefix && str[1]) str++; else if (*str == wild_many || *str == wild_one) { wild_pos = (uint)(str - start) + 1; if (!(wild_pos == 1 && *str == wild_many && *(++str) == '\0')) wild_pos++; break; } chars = 128; // Marker that chars existed } } sort = (sort << 8) + (wild_pos ? min(wild_pos, 127U) : chars); } va_end(args); return sort; } /** Check if the given host name needs to be resolved or not. Host name has to be resolved if it actually contains *name*. For example: 192.168.1.1 --> false 192.168.1.0/255.255.255.0 --> false % --> false 192.168.1.% --> false AB% --> false AAAAFFFF --> true (Hostname) AAAA:FFFF:1234:5678 --> false ::1 --> false This function does not check if the given string is a valid host name or not. It assumes that the argument is a valid host name. @param hostname the string to check. @return a flag telling if the argument needs to be resolved or not. @retval true the argument is a host name and needs to be resolved. @retval false the argument is either an IP address, or a patter and should not be resolved. */ bool hostname_requires_resolving(const char *hostname) { /* called only for --skip-name-resolve */ DBUG_ASSERT(specialflag & SPECIAL_NO_RESOLVE); if (!hostname) return false; /* If the string contains any of {':', '%', '_', '/'}, it is definitely not a host name: - ':' means that the string is an IPv6 address; - '%' or '_' means that the string is a pattern; - '/' means that the string is an IPv4 network address; */ for (const char *p = hostname; *p; ++p) { switch (*p) { case ':': case '%': case '_': case '/': return false; } } /* Now we have to tell a host name (ab.cd, 12.ab) from an IPv4 address (12.34.56.78). The assumption is that if the string contains only digits and dots, it is an IPv4 address. Otherwise -- a host name. */ for (const char *p = hostname; *p; ++p) { if (*p != '.' && !my_isdigit(&my_charset_latin1, *p)) return true; /* a "letter" has been found. */ } return false; /* all characters are either dots or digits. */ } GRANT_COLUMN::GRANT_COLUMN(String &c, ulong y) : rights(y), column(c.ptr(), c.length()) {} void GRANT_NAME::set_user_details(const char *h, const char *d, const char *u, const char *t, bool is_routine) { /* Host given by user */ host.update_hostname(strdup_root(&memex, h)); if (db != d) { db = strdup_root(&memex, d); if (lower_case_table_names) my_casedn_str(files_charset_info, db); } user = strdup_root(&memex, u); sort = get_sort(3, host.get_host(), db, user); if (tname != t) { tname = strdup_root(&memex, t); if (lower_case_table_names || is_routine) my_casedn_str(files_charset_info, tname); } hash_key = user; hash_key.push_back('\0'); hash_key.append(db); hash_key.push_back('\0'); hash_key.append(tname); hash_key.push_back('\0'); } GRANT_NAME::GRANT_NAME(const char *h, const char *d, const char *u, const char *t, ulong p, bool is_routine) : db(0), tname(0), privs(p) { set_user_details(h, d, u, t, is_routine); } GRANT_TABLE::GRANT_TABLE(const char *h, const char *d, const char *u, const char *t, ulong p, ulong c) : GRANT_NAME(h, d, u, t, p, false), cols(c), hash_columns(system_charset_info, key_memory_acl_memex) {} GRANT_NAME::GRANT_NAME(TABLE *form, bool is_routine) { host.update_hostname(get_field(&memex, form->field[0])); db = get_field(&memex, form->field[1]); user = get_field(&memex, form->field[2]); if (!user) user = ""; sort = get_sort(3, host.get_host(), db, user); tname = get_field(&memex, form->field[3]); if (!db || !tname) { /* Wrong table row; Ignore it */ privs = 0; return; /* purecov: inspected */ } if (lower_case_table_names) { my_casedn_str(files_charset_info, db); } if (lower_case_table_names || is_routine) { my_casedn_str(files_charset_info, tname); } hash_key = user; hash_key.push_back('\0'); hash_key.append(db); hash_key.push_back('\0'); hash_key.append(tname); hash_key.push_back('\0'); if (form->field[MYSQL_TABLES_PRIV_FIELD_TABLE_PRIV]) { privs = (ulong)form->field[MYSQL_TABLES_PRIV_FIELD_TABLE_PRIV]->val_int(); privs = fix_rights_for_table(privs); } } GRANT_TABLE::GRANT_TABLE(TABLE *form) : GRANT_NAME(form, false), hash_columns(system_charset_info, key_memory_acl_memex) { if (!db || !tname) { /* Wrong table row; Ignore it */ cols = 0; return; } if (form->field[MYSQL_TABLES_PRIV_FIELD_COLUMN_PRIV]) { cols = (ulong)form->field[MYSQL_TABLES_PRIV_FIELD_COLUMN_PRIV]->val_int(); cols = fix_rights_for_column(cols); } else cols = 0; } GRANT_TABLE::~GRANT_TABLE() {} bool GRANT_TABLE::init(TABLE *col_privs) { int error; if (cols) { uchar key[MAX_KEY_LENGTH]; uint key_prefix_len; KEY_PART_INFO *key_part = col_privs->key_info->key_part; col_privs->field[0]->store(host.get_host(), host.get_host() ? host.get_host_len() : 0, system_charset_info); col_privs->field[1]->store(db, strlen(db), system_charset_info); col_privs->field[2]->store(user, strlen(user), system_charset_info); col_privs->field[3]->store(tname, strlen(tname), system_charset_info); key_prefix_len = (key_part[0].store_length + key_part[1].store_length + key_part[2].store_length + key_part[3].store_length); key_copy(key, col_privs->record[0], col_privs->key_info, key_prefix_len); col_privs->field[4]->store("", 0, &my_charset_latin1); error = col_privs->file->ha_index_init(0, 1); DBUG_EXECUTE_IF("wl7158_grant_table_1", col_privs->file->ha_index_end(); error = HA_ERR_LOCK_DEADLOCK;); if (error) { acl_print_ha_error(error); return true; } error = col_privs->file->ha_index_read_map(col_privs->record[0], (uchar *)key, (key_part_map)15, HA_READ_KEY_EXACT); DBUG_ASSERT(col_privs->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_DEADLOCK); DBUG_ASSERT(col_privs->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_WAIT_TIMEOUT); DBUG_EXECUTE_IF("wl7158_grant_table_2", error = HA_ERR_LOCK_DEADLOCK;); if (error) { bool ret = false; cols = 0; if (error != HA_ERR_KEY_NOT_FOUND && error != HA_ERR_END_OF_FILE) { acl_print_ha_error(error); ret = true; } col_privs->file->ha_index_end(); return ret; } do { String *res, column_name; GRANT_COLUMN *mem_check; /* As column name is a string, we don't have to supply a buffer */ res = col_privs->field[4]->val_str(&column_name); ulong priv = (ulong)col_privs->field[6]->val_int(); DBUG_EXECUTE_IF("mysql_grant_table_init_out_of_memory", DBUG_SET("+d,simulate_out_of_memory");); if (!(mem_check = new (*THR_MALLOC) GRANT_COLUMN(*res, fix_rights_for_column(priv)))) { /* Don't use this entry */ col_privs->file->ha_index_end(); DBUG_EXECUTE_IF("mysql_grant_table_init_out_of_memory", DBUG_SET("-d,simulate_out_of_memory");); return true; } hash_columns.emplace(mem_check->column, unique_ptr_destroy_only(mem_check)); error = col_privs->file->ha_index_next(col_privs->record[0]); DBUG_ASSERT(col_privs->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_DEADLOCK); DBUG_ASSERT(col_privs->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_WAIT_TIMEOUT); DBUG_EXECUTE_IF("wl7158_grant_table_3", error = HA_ERR_LOCK_DEADLOCK;); if (error && error != HA_ERR_END_OF_FILE) { acl_print_ha_error(error); col_privs->file->ha_index_end(); return true; } } while (!error && !key_cmp_if_same(col_privs, key, 0, key_prefix_len)); col_privs->file->ha_index_end(); } return false; } /** Build the lists of ACL_USERs which share name or have no name All accounts with same name will be chained so that they can be retrieved by a single lookup. These entries are sorted using ACL_compare to make sure that most specific account is picked up first. Anonymous user is added to each chain. */ void rebuild_cached_acl_users_for_name(void) { DBUG_TRACE; DBUG_PRINT("enter", ("acl_users size: %zu", acl_users->size())); DBUG_ASSERT(!current_thd || assert_acl_cache_write_lock(current_thd)); if (name_to_userlist) { name_to_userlist->clear(); } else { size_t size = sizeof(Name_to_userlist); myf_t flags = MYF(MY_WME | ME_FATALERROR); void *bytes = my_malloc(key_memory_acl_cache, size, flags); name_to_userlist = new (bytes) Name_to_userlist(); } std::list anons; /* first build each named list */ for (ACL_USER *acl_user = acl_users->begin(); acl_user != acl_users->end(); ++acl_user) { std::string name = acl_user->user ? acl_user->user : ""; (*name_to_userlist)[name].push_back(acl_user); /* keep track of anonymous acl_users */ if (!name.compare("")) anons.push_back(acl_user); } /* add the anonymous acl_users to each non-anon list */ for (auto it = name_to_userlist->begin(); it != name_to_userlist->end(); ++it) { std::string name = it->first; if (!name.compare("")) continue; auto *list = &it->second; for (auto it2 = anons.begin(); it2 != anons.end(); ++it2) { list->push_back(*it2); } list->sort(ACL_compare()); } } /** Fetch the list of ACL_USERs which share name or have no name @param [in] name User entry to be searched @returns List of users that share same name */ Acl_user_ptr_list *cached_acl_users_for_name(const char *name) { DBUG_TRACE; DBUG_PRINT("enter", ("name: '%s'", name)); DBUG_ASSERT(!current_thd || assert_acl_cache_read_lock(current_thd)); std::string user_name = name ? name : ""; auto it = name_to_userlist->find(user_name); if (it != name_to_userlist->end()) return &it->second; it = name_to_userlist->find(""); if (it != name_to_userlist->end()) return &it->second; return NULL; } /* Find first entry that matches the current user */ ACL_USER *find_acl_user(const char *host, const char *user, bool exact) { DBUG_TRACE; DBUG_PRINT("enter", ("host: '%s' user: '%s'", host, user)); DBUG_ASSERT(assert_acl_cache_read_lock(current_thd)); if (likely(acl_users)) { Acl_user_ptr_list *list = cached_acl_users_for_name(user); if (!list) { return 0; } for (auto it = list->begin(); it != list->end(); ++it) { ACL_USER *acl_user = (*it); DBUG_PRINT("info", ("strcmp('%s','%s'), compare_hostname('%s','%s'),", user, acl_user->user ? acl_user->user : "", host, acl_user->host.get_host() ? acl_user->host.get_host() : "")); if (acl_user->user || (user && !user[0])) { if (exact ? !my_strcasecmp(system_charset_info, host ? host : "", acl_user->host.get_host() ? acl_user->host.get_host() : "") : acl_user->host.compare_hostname(host, host)) { return acl_user; } } } } return 0; } /* Find user in ACL SYNOPSIS is_acl_user() thd Handle to THD host host name user user name RETURN false user not fond true there are such user */ bool is_acl_user(THD *thd, const char *host, const char *user) { bool res = true; /* --skip-grants */ if (!initialized) return res; Acl_cache_lock_guard acl_cache_lock(thd, Acl_cache_lock_mode::READ_MODE); if (!acl_cache_lock.lock(false)) return res; res = find_acl_user(host, user, true) != NULL; return res; } /** Validate if a user can proxy as another user @param user the logged in user (proxy user) @param host the hostname part of the logged in userid @param ip the ip of the logged in userid @param authenticated_as the effective user a plugin is trying to impersonate as (proxied user) @param [out] proxy_used True if a proxy is found @return proxy user definition @retval NULL proxy user definition not found or not applicable @retval non-null the proxy user data */ ACL_PROXY_USER *acl_find_proxy_user(const char *user, const char *host, const char *ip, char *authenticated_as, bool *proxy_used) { /* if the proxied and proxy user are the same return OK */ DBUG_TRACE; DBUG_PRINT("info", ("user=%s host=%s ip=%s authenticated_as=%s", user, host, ip, authenticated_as)); if (!strcmp(authenticated_as, user)) { DBUG_PRINT("info", ("user is the same as authenticated_as")); return NULL; } bool find_any = check_proxy_users && !*authenticated_as; if (!find_any) *proxy_used = true; for (ACL_PROXY_USER *proxy = acl_proxy_users->begin(); proxy != acl_proxy_users->end(); ++proxy) { if (proxy->matches(host, user, ip, authenticated_as, find_any)) { DBUG_PRINT("info", ("proxy matched=%s@%s", proxy->get_proxied_user(), proxy->get_proxied_host())); if (!find_any) { DBUG_PRINT( "info", ("returning specific match as authenticated_as was specified")); *proxy_used = true; return proxy; } else { // we never use anonymous users when mapping // proxy users for internal plugins: if (strcmp(proxy->get_proxied_user() ? proxy->get_proxied_user() : "", "")) { if (find_acl_user(proxy->get_proxied_host(), proxy->get_proxied_user(), true)) { DBUG_PRINT("info", ("setting proxy_used to true, as \ find_all search matched real user=%s host=%s", proxy->get_proxied_user(), proxy->get_proxied_host())); *proxy_used = true; strcpy(authenticated_as, proxy->get_proxied_user()); } else { DBUG_PRINT("info", ("skipping match because ACL user \ does not exist, looking for next match to map")); } if (*proxy_used) { DBUG_PRINT("info", ("returning matching user")); return proxy; } } } } } DBUG_PRINT("info", ("No matching users found, returning null")); return NULL; } void clear_and_init_db_cache() { db_cache.clear(); } /** Insert a new entry in db_cache @param [in] thd Handle to THD object @param [in] entry Entry to be inserted in db_cache */ static void insert_entry_in_db_cache(THD *thd, acl_entry *entry) { DBUG_TRACE; /* Either have WRITE lock or none at all */ DBUG_ASSERT(assert_acl_cache_write_lock(thd) || !assert_acl_cache_read_lock(thd)); Acl_cache_lock_guard acl_cache_lock(thd, Acl_cache_lock_mode::WRITE_MODE); /* In following cases release memory and return 1. Could not lock cache : This is ok because db_cache second level cache anyways. 2. Someone already inserted a similar entry. */ unique_ptr_my_free entry_ptr(entry); if (!acl_cache_lock.lock(false)) return; db_cache.emplace(std::string(entry->key, entry->length), std::move(entry_ptr)); } /** Get privilege for a host, user, and db combination. @note db_cache is not used if db_is_pattern is set. @param thd Thread handler @param host Host name @param ip Ip @param user user name @param db We look for the ACL of this database @param db_is_pattern @return Database ACL */ ulong acl_get(THD *thd, const char *host, const char *ip, const char *user, const char *db, bool db_is_pattern) { ulong host_access = ~(ulong)0, db_access = 0; size_t key_length, copy_length; char key[ACL_KEY_LENGTH], *tmp_db, *end; acl_entry *entry; DBUG_TRACE; copy_length = (strlen(ip ? ip : "") + strlen(user ? user : "") + strlen(db ? db : "")) + 2; /* Added 2 at the end to avoid buffer overflow at strmov()*/ /* Make sure that my_stpcpy() operations do not result in buffer overflow. */ if (copy_length >= ACL_KEY_LENGTH) return 0; Acl_cache_lock_guard acl_cache_lock(thd, Acl_cache_lock_mode::READ_MODE); if (!acl_cache_lock.lock(false)) return db_access; end = my_stpcpy( (tmp_db = my_stpcpy(my_stpcpy(key, ip ? ip : "") + 1, user) + 1), db); if (lower_case_table_names) { my_casedn_str(files_charset_info, tmp_db); db = tmp_db; } key_length = (size_t)(end - key); if (!db_is_pattern) { const auto it = db_cache.find(std::string(key, key_length)); if (it != db_cache.end()) { db_access = it->second->access; DBUG_PRINT("exit", ("access: 0x%lx", db_access)); return db_access; } } /* Check if there are some access rights for database and user */ for (ACL_DB *acl_db = acl_dbs->begin(); acl_db != acl_dbs->end(); ++acl_db) { if (!acl_db->user || !strcmp(user, acl_db->user)) { if (acl_db->host.compare_hostname(host, ip)) { /* Do the usual string comparision if partial_revokes is ON, otherwise do the wildcard grant comparision */ if (!acl_db->db || (db && (mysqld_partial_revokes() ? (!strcmp(db, acl_db->db)) : (!wild_compare(db, strlen(db), acl_db->db, strlen(acl_db->db), db_is_pattern))))) { db_access = acl_db->access; if (acl_db->host.get_host()) goto exit; // Fully specified. Take it break; /* purecov: tested */ } } } } if (!db_access) goto exit; // Can't be better exit: /* Save entry in cache for quick retrieval */ if (!db_is_pattern && (entry = (acl_entry *)my_malloc( key_memory_acl_cache, sizeof(acl_entry) + key_length, MYF(0)))) { entry->access = (db_access & host_access); entry->length = key_length; memcpy((uchar *)entry->key, key, key_length); acl_cache_lock.unlock(); insert_entry_in_db_cache(thd, entry); } DBUG_PRINT("exit", ("access: 0x%lx", db_access & host_access)); return db_access & host_access; } /* Check if there are any possible matching entries for this host NOTES All host names without wild cards are stored in a hash table, entries with wildcards are stored in a dynamic array */ static void init_check_host(void) { DBUG_TRACE; if (acl_wild_hosts != NULL) acl_wild_hosts->clear(); else acl_wild_hosts = new Prealloced_array( key_memory_acl_mem); size_t acl_users_size = acl_users ? acl_users->size() : 0; acl_check_hosts = new collation_unordered_map( system_charset_info, key_memory_acl_mem); if (acl_users_size && !allow_all_hosts) { for (ACL_USER *acl_user = acl_users->begin(); acl_user != acl_users->end(); ++acl_user) { if (acl_user->host.get_host()) { if (acl_user->host.has_wildcard()) { // Has wildcard ACL_HOST_AND_IP *acl = NULL; for (acl = acl_wild_hosts->begin(); acl != acl_wild_hosts->end(); ++acl) { // Check if host already exists if (!my_strcasecmp(system_charset_info, acl_user->host.get_host(), acl->get_host())) break; // already stored } if (acl == acl_wild_hosts->end()) // If new acl_wild_hosts->push_back(acl_user->host); } else { // Will be ignored if there's already an entry. acl_check_hosts->emplace(acl_user->host.get_host(), acl_user); } } } } acl_wild_hosts->shrink_to_fit(); } /* Rebuild lists used for checking of allowed hosts We need to rebuild 'acl_check_hosts' and 'acl_wild_hosts' after adding, dropping or renaming user, since they contain pointers to elements of 'acl_user' array, which are invalidated by drop operation, and use ACL_USER::host::hostname as a key, which is changed by rename. */ void rebuild_check_host(void) { delete acl_wild_hosts; acl_wild_hosts = NULL; delete acl_check_hosts; acl_check_hosts = nullptr; init_check_host(); } /* Gets user credentials without authentication and resource limit checks. This function is used to initialized a new Security_context. It's a terrible anti-pattern that needs to go. SYNOPSIS acl_getroot() thd Handle to THD sctx Context which should be initialized user user name host host name ip IP db current data base name RETURN false OK true Error */ bool acl_getroot(THD *thd, Security_context *sctx, const char *user, const char *host, const char *ip, const char *db) { int res = 1; ACL_USER *acl_user = 0; DBUG_TRACE; DBUG_PRINT("enter", ("Host: '%s', Ip: '%s', User: '%s', db: '%s'", (host ? host : "(NULL)"), (ip ? ip : "(NULL)"), user, (db ? db : "(NULL)"))); sctx->set_user_ptr(user, user ? strlen(user) : 0); sctx->set_host_ptr(host, host ? strlen(host) : 0); sctx->set_ip_ptr(ip, ip ? strlen(ip) : 0); sctx->set_host_or_ip_ptr(); if (!initialized) { /* here if mysqld's been started with --skip-grant-tables option. */ sctx->skip_grants(); return false; } sctx->set_master_access(0); sctx->cache_current_db_access(0); sctx->assign_priv_user("", 0); sctx->assign_priv_host("", 0); Acl_cache_lock_guard acl_cache_lock(thd, Acl_cache_lock_mode::READ_MODE); if (!acl_cache_lock.lock(false)) return true; /* Find acl entry in user database. This is specially tailored to suit the check we do for CALL of a stored procedure; user is set to what is actually a priv_user, which can be ''. */ for (ACL_USER *acl_user_tmp = acl_users->begin(); acl_user_tmp != acl_users->end(); ++acl_user_tmp) { if ((!acl_user_tmp->user && !user[0]) || (acl_user_tmp->user && strcmp(user, acl_user_tmp->user) == 0)) { if (acl_user_tmp->host.compare_hostname(host, ip)) { acl_user = acl_user_tmp; res = 0; break; } } } if (acl_user) { sctx->clear_active_roles(); List_of_auth_id_refs default_roles; Auth_id_ref authid = create_authid_from(acl_user); /* Needs Acl_cache_lock_guard in read mode */ get_default_roles(authid, default_roles); List_of_auth_id_refs::iterator it = default_roles.begin(); for (; it != default_roles.end(); ++it) { if (sctx->activate_role(it->first, it->second)) { sctx->clear_active_roles(); break; } } if (sctx->get_active_roles()->size() == 0) { for (ACL_DB *acl_db = acl_dbs->begin(); acl_db != acl_dbs->end(); ++acl_db) { if (!acl_db->user || (user && user[0] && !strcmp(user, acl_db->user))) { if (acl_db->host.compare_hostname(host, ip)) { /* Do the usual string comparision if partial_revokes is ON, otherwise do the wildcard grant comparision */ if (!acl_db->db || (db && (mysqld_partial_revokes() ? (!strcmp(db, acl_db->db)) : (!wild_compare(db, strlen(db), acl_db->db, strlen(acl_db->db), 0))))) { sctx->cache_current_db_access(acl_db->access); break; } } } // end if } // end for sctx->set_master_access(acl_user->access, acl_restrictions->find_restrictions(acl_user)); } // end if sctx->assign_priv_user(user, user ? strlen(user) : 0); sctx->assign_priv_host( acl_user->host.get_host(), acl_user->host.get_host() ? strlen(acl_user->host.get_host()) : 0); sctx->set_password_expired(acl_user->password_expired); sctx->lock_account(acl_user->account_locked); } // end if if (acl_user && sctx->get_active_roles()->size() > 0) { sctx->checkout_access_maps(); ulong db_acl = db ? sctx->db_acl({db, strlen(db)}) : 0; sctx->cache_current_db_access(db_acl); } return res; } /** Convert scrambled password to binary form, according to scramble type, Binary form is stored in user.salt. @param acl_user The object where to store the salt Despite the name of the function it is used when loading ACLs from disk to store the password hash in the ACL_USER object. Note that it works only for native and "old" mysql authentication built-in plugins. Assumption : user's authentication plugin information is available. @return Password hash validation @retval false Hash is of suitable length @retval true Hash is of wrong length or format */ bool set_user_salt(ACL_USER *acl_user) { bool result = false; plugin_ref plugin = NULL; plugin = my_plugin_lock_by_name(0, acl_user->plugin, MYSQL_AUTHENTICATION_PLUGIN); if (plugin) { st_mysql_auth *auth = (st_mysql_auth *)plugin_decl(plugin)->info; for (int i = 0; i < NUM_CREDENTIALS && !result; ++i) { result = auth->set_salt(acl_user->credentials[i].m_auth_string.str, acl_user->credentials[i].m_auth_string.length, acl_user->credentials[i].m_salt, &acl_user->credentials[i].m_salt_len); } plugin_unlock(0, plugin); } return result; } /** Iterate over the user records and check for irregularities. Currently this includes : - checking if the plugin referenced is present. - if there's sha256 users and there's neither SSL nor RSA configured */ static void validate_user_plugin_records() { DBUG_TRACE; if (!validate_user_plugins) return; lock_plugin_data(); for (ACL_USER *acl_user = acl_users->begin(); acl_user != acl_users->end(); ++acl_user) { struct st_plugin_int *plugin; if (acl_user->plugin.length) { /* rule 1 : plugin does exit */ if (!auth_plugin_is_built_in(acl_user->plugin.str)) { plugin = plugin_find_by_type(acl_user->plugin, MYSQL_AUTHENTICATION_PLUGIN); if (!plugin) { LogErr(WARNING_LEVEL, ER_AUTHCACHE_PLUGIN_MISSING, (int)acl_user->plugin.length, acl_user->plugin.str, acl_user->user, static_cast(acl_user->host.get_host_len()), acl_user->host.get_host()); } } if (Cached_authentication_plugins::compare_plugin(PLUGIN_SHA256_PASSWORD, acl_user->plugin) && sha256_rsa_auth_status() && !SslAcceptorContext::have_ssl()) { LogErr(WARNING_LEVEL, ER_AUTHCACHE_PLUGIN_CONFIG, acl_user->plugin.str, acl_user->user, static_cast(acl_user->host.get_host_len()), acl_user->host.get_host(), "but neither SSL nor RSA keys are"); } } } unlock_plugin_data(); } /** Audit notification for flush @param [in] thd Handle to THD */ void notify_flush_event(THD *thd) { mysql_audit_notify(thd, AUDIT_EVENT(MYSQL_AUDIT_AUTHENTICATION_FLUSH), 0, NULL, NULL, NULL, false, NULL, NULL); } /** Initialize roles structures from role tables handle. This function is called by acl_reload and may fail to initialize role structures if handle to role_edges and/or default_roles are NUL @param [in] thd Handle to THD object @param [in] tablelst Handle to Roles tables @returns status of cache update @retval false Success @retval true failure */ static bool reload_roles_cache(THD *thd, TABLE_LIST *tablelst) { DBUG_TRACE; DBUG_ASSERT(tablelst); sql_mode_t old_sql_mode = thd->variables.sql_mode; thd->variables.sql_mode &= ~MODE_PAD_CHAR_TO_FULL_LENGTH; /* Attempt to reload the role cache only if the role_edges and default_roles tables exist. */ if ((tablelst[0].table) && (tablelst[1].table) && populate_roles_caches(thd, tablelst)) { thd->variables.sql_mode = old_sql_mode; return true; } thd->variables.sql_mode = old_sql_mode; return false; } /* Initialize structures responsible for user/db-level privilege checking and load privilege information for them from tables in the 'mysql' database. SYNOPSIS acl_init() dont_read_acl_tables true if we want to skip loading data from privilege tables and disable privilege checking. NOTES This function is mostly responsible for preparatory steps, main work on initialization and grants loading is done in acl_reload(). RETURN VALUES 0 ok 1 Could not initialize grant's */ bool acl_init(bool dont_read_acl_tables) { THD *thd; bool return_val; DBUG_TRACE; init_acl_cache(); acl_cache_initialized = true; /* cache built-in authentication plugins, to avoid hash searches and a global mutex lock on every connect */ g_cached_authentication_plugins = new Cached_authentication_plugins(); unknown_accounts = new Map_with_rw_lock(0); if (!g_cached_authentication_plugins->is_valid()) return 1; if (dont_read_acl_tables) { return 0; /* purecov: tested */ } /* To be able to run this from boot, we allocate a temporary THD */ if (!(thd = new THD)) return 1; /* purecov: inspected */ thd->thread_stack = (char *)&thd; thd->store_globals(); /* Check storage engine type for every ACL table and output warning message in case it's different from supported one (InnoDB). We still allow server to start-up if tables are in different SE since this is necessary to be able to perform privilege table upgrade without extra server restarts. Account management statements do their own checks and refuse to operate if privilege tables are using unsupported SE. */ return_val = check_engine_type_for_acl_table(thd, false); /* Check all the ACL tables are intact and output warning message in case any of the ACL tables are corrupted. */ check_acl_tables_intact(thd, false); /* It is safe to call acl_reload() since acl_* arrays and hashes which will be freed there are global static objects and thus are initialized by zeros at startup. */ return_val |= acl_reload(thd, false); notify_flush_event(thd); thd->release_resources(); delete thd; return return_val; } void clean_user_cache() { if (name_to_userlist) name_to_userlist->clear(); acl_users->clear(); } /* Initialize structures responsible for user/db-level privilege checking and load information about grants from open privilege tables. SYNOPSIS acl_load() thd Current thread tables List containing open "mysql.host", "mysql.user", "mysql.db" and "mysql.proxies_priv", "mysql.global_grants" tables in that order. RETURN VALUES false Success true Error */ static bool acl_load(THD *thd, TABLE_LIST *tables) { TABLE *table; unique_ptr_destroy_only iterator; bool return_val = true; bool check_no_resolve = specialflag & SPECIAL_NO_RESOLVE; char tmp_name[NAME_LEN + 1]; sql_mode_t old_sql_mode = thd->variables.sql_mode; DBUG_TRACE; DBUG_EXECUTE_IF( "wl_9262_set_max_length_hostname", thd->security_context()->assign_priv_host("oh_my_gosh_this_is_a_long_" "hostname_look_at_it_it_has_60" "_char", 60); thd->security_context()->assign_host("oh_my_gosh_this_is_a_long_" "hostname_look_at_it_it_has_60" "_char", 60); thd->security_context()->set_host_or_ip_ptr();); thd->variables.sql_mode &= ~MODE_PAD_CHAR_TO_FULL_LENGTH; grant_version++; /* Privileges updated */ clear_and_init_db_cache(); // Clear locked hostname cache init_acl_memory(); // Allocate the memory blocks in the MEM_ROOT if (read_user_table(thd, tables[0].table)) goto end; /* Prepare reading from the mysql.db table */ iterator = init_table_iterator(thd, table = tables[1].table, NULL, false, /*ignore_not_found_rows=*/false); if (iterator == nullptr) goto end; table->use_all_columns(); acl_dbs->clear(); int read_rec_errcode; while (!(read_rec_errcode = iterator->Read())) { /* Reading record in mysql.db */ ACL_DB db; db.host.update_hostname( get_field(&global_acl_memory, table->field[MYSQL_DB_FIELD_HOST])); db.db = get_field(&global_acl_memory, table->field[MYSQL_DB_FIELD_DB]); if (!db.db) { LogErr(WARNING_LEVEL, ER_AUTHCACHE_DB_IGNORED_EMPTY_NAME); continue; } db.user = get_field(&global_acl_memory, table->field[MYSQL_DB_FIELD_USER]); if (check_no_resolve && hostname_requires_resolving(db.host.get_host()) && strcmp(db.host.get_host(), "localhost") != 0) { LogErr(WARNING_LEVEL, ER_AUTHCACHE_DB_SKIPPED_NEEDS_RESOLVE, db.db, db.user ? db.user : "", db.host.get_host() ? db.host.get_host() : ""); } db.access = get_access(table, 3, 0); db.access = fix_rights_for_db(db.access); if (lower_case_table_names) { /* convert db to lower case and give a warning if the db wasn't already in lower case */ (void)my_stpcpy(tmp_name, db.db); my_casedn_str(files_charset_info, db.db); if (strcmp(db.db, tmp_name) != 0) { LogErr(WARNING_LEVEL, ER_AUTHCACHE_DB_ENTRY_LOWERCASED_REVOKE_WILL_FAIL, db.db, db.user ? db.user : "", db.host.get_host() ? db.host.get_host() : ""); } } db.sort = get_sort(3, db.host.get_host(), db.db, db.user); if (table->s->fields <= 9) { // Without grant if (db.access & CREATE_ACL) db.access |= REFERENCES_ACL | INDEX_ACL | ALTER_ACL; } acl_dbs->push_back(db); } // END reading records from mysql.db tables iterator.reset(); if (read_rec_errcode > 0) goto end; std::sort(acl_dbs->begin(), acl_dbs->end(), ACL_compare()); acl_dbs->shrink_to_fit(); /* Prepare to read records from the mysql.proxies_priv table */ acl_proxy_users->clear(); if (tables[2].table) { iterator = init_table_iterator(thd, table = tables[2].table, NULL, false, /*ignore_not_found_rows=*/false); if (iterator == nullptr) goto end; table->use_all_columns(); while (!(read_rec_errcode = iterator->Read())) { /* Reading record in mysql.proxies_priv */ ACL_PROXY_USER proxy; proxy.init(table, &global_acl_memory); if (proxy.check_validity(check_no_resolve)) continue; if (acl_proxy_users->push_back(proxy)) { goto end; } } // END reading records from the mysql.proxies_priv table iterator.reset(); if (read_rec_errcode > 0) goto end; std::sort(acl_proxy_users->begin(), acl_proxy_users->end(), ACL_compare()); } else { LogErr(WARNING_LEVEL, ER_AUTHCACHE_TABLE_PROXIES_PRIV_MISSING); } acl_proxy_users->shrink_to_fit(); validate_user_plugin_records(); init_check_host(); /* Load dynamic privileges */ if (tables[3].table) { if (populate_dynamic_privilege_caches(thd, &tables[3])) { return_val = true; goto end; } } else { LogErr(WARNING_LEVEL, ER_MISSING_GRANT_SYSTEM_TABLE); } initialized = 1; return_val = false; end: thd->variables.sql_mode = old_sql_mode; DBUG_EXECUTE_IF("induce_acl_load_failure", return_val = true;); return return_val; } /** Clear second level cache on account names. */ void free_name_to_userlist() { if (!name_to_userlist) return; name_to_userlist->~unordered_map(); my_free(name_to_userlist); name_to_userlist = nullptr; } void acl_free(bool end /*= false*/) { free_name_to_userlist(); delete acl_users; acl_users = NULL; delete acl_dbs; acl_dbs = NULL; delete acl_wild_hosts; acl_wild_hosts = NULL; delete acl_proxy_users; acl_proxy_users = NULL; delete acl_check_hosts; acl_check_hosts = nullptr; if (!end) clear_and_init_db_cache(); else { shutdown_acl_cache(); if (acl_cache_initialized == true) { db_cache.clear(); delete g_cached_authentication_plugins; g_cached_authentication_plugins = 0; delete unknown_accounts; unknown_accounts = 0; acl_cache_initialized = false; } } free_root(&global_acl_memory, MYF(0)); } bool check_engine_type_for_acl_table(THD *thd, bool mdl_locked) { TABLE_LIST tables[ACL_TABLES::LAST_ENTRY]; uint flags = mdl_locked ? MYSQL_OPEN_HAS_MDL_LOCK | MYSQL_LOCK_IGNORE_TIMEOUT | MYSQL_OPEN_IGNORE_FLUSH : MYSQL_LOCK_IGNORE_TIMEOUT; /* Open the following ACL tables to check their consistency. Although we don't read here from the tables being opened we still request a lock type MDL_SHARED_READ_ONLY for the sake of consistency with other code. */ grant_tables_setup_for_open(tables, TL_READ, MDL_SHARED_READ_ONLY); bool result = open_and_lock_tables(thd, tables, flags); if (!result) { check_engine_type_for_acl_table(tables, false); if (!mdl_locked) commit_and_close_mysql_tables(thd); else close_thread_tables(thd); } return result; } /* This internal handler implements downgrade from SL_ERROR to SL_WARNING for acl_init()/handle_reload_request(). */ class Acl_ignore_error_handler : public Internal_error_handler { public: virtual bool handle_condition(THD *, uint sql_errno, const char *, Sql_condition::enum_severity_level *level, const char *) { switch (sql_errno) { case ER_CANNOT_LOAD_FROM_TABLE_V2: case ER_COL_COUNT_DOESNT_MATCH_CORRUPTED_V2: case ER_COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE_V2: (*level) = Sql_condition::SL_WARNING; break; default: break; } return false; } }; /** Helper function that checks the sanity of tables object present in the TABLE_LIST object. it logs a warning message when a table is missing @param thd Handle of current thread. @param tables A valid table list pointer @retval false OK. true Error. */ bool check_acl_tables_intact(THD *thd, TABLE_LIST *tables) { Acl_table_intact table_intact(thd, WARNING_LEVEL); bool result_acl = false; DBUG_ASSERT(tables); for (auto idx = 0; idx < ACL_TABLES::LAST_ENTRY; idx++) { if (tables[idx].table) { result_acl |= table_intact.check(tables[idx].table, (ACL_TABLES)idx); } else { LogErr(WARNING_LEVEL, ER_MISSING_ACL_SYSTEM_TABLE, tables[idx].table_name_length, tables[idx].table_name); result_acl |= true; } } /* say that we're still gonna give reading a try */ if (result_acl) LogErr(INFORMATION_LEVEL, ER_ACL_WRONG_OR_MISSING_ACL_TABLES_LOG); return result_acl; } /** Opens the ACL tables and checks their sanity. This method reports error only if it is unable to open or lock tables. It is called in situations when server has to continue even if a corrupt table was found - For example - acl_init() @param thd Handle of current thread. @param mdl_locked MDL is locked @retval false OK. true Unable to open the table(s). */ bool check_acl_tables_intact(THD *thd, bool mdl_locked) { TABLE_LIST tables[ACL_TABLES::LAST_ENTRY]; Acl_ignore_error_handler acl_ignore_handler; uint flags = mdl_locked ? MYSQL_OPEN_HAS_MDL_LOCK | MYSQL_LOCK_IGNORE_TIMEOUT | MYSQL_OPEN_IGNORE_FLUSH : MYSQL_LOCK_IGNORE_TIMEOUT; grant_tables_setup_for_open(tables, TL_READ, MDL_SHARED_READ_ONLY); bool result_acl = open_and_lock_tables(thd, tables, flags); thd->push_internal_handler(&acl_ignore_handler); if (!result_acl) { check_acl_tables_intact(thd, tables); if (!mdl_locked) commit_and_close_mysql_tables(thd); else close_thread_tables(thd); } thd->pop_internal_handler(); return result_acl; } /** Small helper function which allows to determine if error which caused failure to open and lock privilege tables should not be reported to error log (because this is expected or temporary condition). */ static bool is_expected_or_transient_error(THD *thd) { return !thd->get_stmt_da()->is_error() || // Interrupted/no error condition. thd->get_stmt_da()->mysql_errno() == ER_TABLE_NOT_LOCKED || thd->get_stmt_da()->mysql_errno() == ER_LOCK_DEADLOCK; } /* Forget current user/db-level privileges and read new privileges from the privilege tables. SYNOPSIS acl_reload() thd Current thread NOTE All tables of calling thread which were open and locked by LOCK TABLES statement will be unlocked and closed. This function is also used for initialization of structures responsible for user/db-level privilege checking. RETURN VALUE false Success true Failure */ bool acl_reload(THD *thd, bool mdl_locked) { MEM_ROOT old_mem; bool return_val = true; uint flags = mdl_locked ? MYSQL_OPEN_HAS_MDL_LOCK | MYSQL_LOCK_IGNORE_TIMEOUT | MYSQL_OPEN_IGNORE_FLUSH : MYSQL_LOCK_IGNORE_TIMEOUT; Prealloced_array *old_acl_users = nullptr; Prealloced_array *old_acl_dbs = nullptr; Prealloced_array *old_acl_proxy_users = nullptr; Granted_roles_graph *old_granted_roles = nullptr; Default_roles *old_default_roles = nullptr; Role_index_map *old_authid_to_vertex = nullptr; Acl_cache_lock_guard acl_cache_lock(thd, Acl_cache_lock_mode::WRITE_MODE); User_to_dynamic_privileges_map *old_dyn_priv_map; unique_ptr old_acl_restrictions = nullptr; DBUG_TRACE; // Interchange the global role cache ptrs with the local role cache ptrs. auto swap_role_cache = [&]() { std::swap(old_granted_roles, g_granted_roles); std::swap(old_default_roles, g_default_roles); std::swap(old_authid_to_vertex, g_authid_to_vertex); }; // Delete the memory pointed by the local role cache ptrs. auto delete_old_role_cache = [&]() { delete old_granted_roles; delete old_default_roles; delete old_authid_to_vertex; }; /* To avoid deadlocks we should obtain table locks before obtaining acl_cache->lock mutex. */ TABLE_LIST tables[6] = { TABLE_LIST("mysql", "user", TL_READ, MDL_SHARED_READ_ONLY), /* For a TABLE_LIST element that is inited with a lock type TL_READ the type MDL_SHARED_READ_ONLY of MDL is requested for. Acquiring strong MDL lock allows to avoid deadlock and timeout errors from SE level. */ TABLE_LIST("mysql", "db", TL_READ, MDL_SHARED_READ_ONLY), TABLE_LIST("mysql", "proxies_priv", TL_READ, MDL_SHARED_READ_ONLY), TABLE_LIST("mysql", "global_grants", TL_READ, MDL_SHARED_READ_ONLY), TABLE_LIST("mysql", "role_edges", TL_READ, MDL_SHARED_READ_ONLY), TABLE_LIST("mysql", "default_roles", TL_READ, MDL_SHARED_READ_ONLY)}; tables[0].next_local = tables[0].next_global = tables + 1; tables[1].next_local = tables[1].next_global = tables + 2; tables[2].next_local = tables[2].next_global = tables + 3; tables[3].next_local = tables[3].next_global = tables + 4; tables[4].next_local = tables[4].next_global = tables + 5; tables[0].open_type = tables[1].open_type = tables[2].open_type = tables[3].open_type = tables[4].open_type = tables[5].open_type = OT_BASE_ONLY; tables[3].open_strategy = tables[4].open_strategy = tables[5].open_strategy = TABLE_LIST::OPEN_IF_EXISTS; if (open_and_lock_tables(thd, tables, flags)) { /* Execution might have been interrupted; only print the error message if a user error condition has been raised. Also do not print expected/ transient errors about tables not being locked (occurs when user does FLUSH PRIVILEGES under LOCK TABLES) and MDL deadlocks. These errors can't occurr at start-up and will be reported to user anyway. */ if (!is_expected_or_transient_error(thd)) { LogErr(ERROR_LEVEL, ER_AUTHCACHE_CANT_OPEN_AND_LOCK_PRIVILEGE_TABLES, thd->get_stmt_da()->message_text()); } goto end; } if (!acl_cache_lock.lock()) goto end; old_acl_users = acl_users; old_acl_dbs = acl_dbs; old_acl_proxy_users = acl_proxy_users; old_acl_restrictions = move(acl_restrictions); swap_role_cache(); roles_init(); acl_users = new Prealloced_array(key_memory_acl_mem); acl_dbs = new Prealloced_array(key_memory_acl_mem); acl_proxy_users = new Prealloced_array( key_memory_acl_mem); acl_restrictions = make_unique(); // acl_load() overwrites global_acl_memory, so we need to free it. // However, we can't do that immediately, because acl_load() might fail, // and then we'd need to keep it. old_mem = move(global_acl_memory); delete acl_wild_hosts; acl_wild_hosts = NULL; delete acl_check_hosts; acl_check_hosts = NULL; old_dyn_priv_map = swap_dynamic_privileges_map(new User_to_dynamic_privileges_map()); /* Revert to the old acl caches, if either loading of acl cache or role cache failed. We do this because roles caches maintain the shallow copies of the ACL_USER(s). */ if ((return_val = acl_load(thd, tables)) || (return_val = reload_roles_cache( thd, (tables + 4)))) { // Error. Revert to old list DBUG_PRINT("error", ("Reverting to old privileges")); acl_free(); /* purecov: inspected */ acl_users = old_acl_users; acl_dbs = old_acl_dbs; acl_proxy_users = old_acl_proxy_users; global_acl_memory = move(old_mem); acl_restrictions = move(old_acl_restrictions); // Revert to the old role caches swap_role_cache(); // Old caches must be pointing to the global role caches right now delete_old_role_cache(); init_check_host(); delete swap_dynamic_privileges_map(old_dyn_priv_map); if (!old_dyn_priv_map) dynamic_privileges_init(); if (acl_users) rebuild_cached_acl_users_for_name(); } else { delete old_acl_users; delete old_acl_dbs; delete old_acl_proxy_users; delete old_dyn_priv_map; // Delete the old role caches delete_old_role_cache(); free_root(&old_mem, MYF(0)); } end: if (!mdl_locked) commit_and_close_mysql_tables(thd); else close_thread_tables(thd); get_global_acl_cache()->increase_version(); DEBUG_SYNC(thd, "after_acl_reload"); return return_val; } void acl_insert_proxy_user(ACL_PROXY_USER *new_value) { DBUG_TRACE; DBUG_ASSERT(assert_acl_cache_write_lock(current_thd)); acl_proxy_users->push_back(*new_value); std::sort(acl_proxy_users->begin(), acl_proxy_users->end(), ACL_compare()); } struct Free_grant_table { void operator()(GRANT_TABLE *grant_table) const { grant_table->~GRANT_TABLE(); } }; /* Free grant array if possible */ void grant_free(void) { DBUG_TRACE; column_priv_hash.reset(); proc_priv_hash.reset(); func_priv_hash.reset(); free_root(&memex, MYF(0)); } /** @brief Initialize structures responsible for table/column-level privilege checking and load information for them from tables in the 'mysql' database. @param skip_grant_tables true if the command line option --skip-grant-tables is specified, else false. @return Error status @retval false OK @retval true Could not initialize grant subsystem. */ bool grant_init(bool skip_grant_tables) { THD *thd; bool return_val; DBUG_TRACE; if (skip_grant_tables) return false; if (!(thd = new THD)) return 1; /* purecov: deadcode */ thd->thread_stack = (char *)&thd; thd->store_globals(); return_val = grant_reload(thd, false); if (return_val && thd->get_stmt_da()->is_error()) LogErr(ERROR_LEVEL, ER_AUTHCACHE_CANT_INIT_GRANT_SUBSYSTEM, thd->get_stmt_da()->message_text()); if (opt_mandatory_roles.length > 0) { return_val |= check_authorization_id_string(thd, opt_mandatory_roles); } thd->release_resources(); delete thd; return return_val; } /** @brief Helper function to grant_reload_procs_priv Reads the procs_priv table into memory hash. @param p_table A pointer to the procs_priv table structure. @see grant_reload @see grant_reload_procs_priv @return Error state @retval true An error occurred @retval false Success */ static bool grant_load_procs_priv(TABLE *p_table) { MEM_ROOT *memex_ptr; bool return_val = 1; int error; bool check_no_resolve = specialflag & SPECIAL_NO_RESOLVE; MEM_ROOT **save_mem_root_ptr = THR_MALLOC; DBUG_TRACE; proc_priv_hash.reset( new malloc_unordered_multimap>( key_memory_acl_memex)); func_priv_hash.reset( new malloc_unordered_multimap>( key_memory_acl_memex)); error = p_table->file->ha_index_init(0, 1); DBUG_EXECUTE_IF("wl7158_grant_load_proc_1", p_table->file->ha_index_end(); error = HA_ERR_LOCK_DEADLOCK;); if (error) { acl_print_ha_error(error); return true; } p_table->use_all_columns(); error = p_table->file->ha_index_first(p_table->record[0]); DBUG_ASSERT(p_table->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_DEADLOCK); DBUG_ASSERT(p_table->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_WAIT_TIMEOUT); DBUG_EXECUTE_IF("wl7158_grant_load_proc_2", error = HA_ERR_LOCK_DEADLOCK;); if (error) { if (error == HA_ERR_END_OF_FILE) return_val = 0; // Return Ok. else acl_print_ha_error(error); } else { memex_ptr = &memex; THR_MALLOC = &memex_ptr; do { GRANT_NAME *mem_check; malloc_unordered_multimap> *hash; if (!(mem_check = new (memex_ptr) GRANT_NAME(p_table, true))) { /* This could only happen if we are out memory */ goto end_unlock; } if (check_no_resolve) { if (hostname_requires_resolving(mem_check->host.get_host())) { LogErr(WARNING_LEVEL, ER_AUTHCACHE_PROCS_PRIV_SKIPPED_NEEDS_RESOLVE, mem_check->tname, mem_check->user, mem_check->host.get_host() ? mem_check->host.get_host() : ""); } } const enum_sp_type sp_type = to_sp_type(p_table->field[4]->val_int()); if (sp_type == enum_sp_type::PROCEDURE) { hash = proc_priv_hash.get(); } else if (sp_type == enum_sp_type::FUNCTION) { hash = func_priv_hash.get(); } else { LogErr(WARNING_LEVEL, ER_AUTHCACHE_PROCS_PRIV_ENTRY_IGNORED_BAD_ROUTINE_TYPE, mem_check->tname); continue; } mem_check->privs = fix_rights_for_procedure(mem_check->privs); if (!mem_check->ok()) { destroy(mem_check); } else { hash->emplace(mem_check->hash_key, unique_ptr_destroy_only(mem_check)); } error = p_table->file->ha_index_next(p_table->record[0]); DBUG_ASSERT(p_table->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_DEADLOCK); DBUG_ASSERT(p_table->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_WAIT_TIMEOUT); DBUG_EXECUTE_IF("wl7158_grant_load_proc_3", error = HA_ERR_LOCK_DEADLOCK;); if (error) { if (error == HA_ERR_END_OF_FILE) return_val = 0; else acl_print_ha_error(error); goto end_unlock; } } while (true); } end_unlock: p_table->file->ha_index_end(); THR_MALLOC = save_mem_root_ptr; return return_val; } /** @brief Initialize structures responsible for table/column-level privilege checking and load information about grants from open privilege tables. @param thd Current thread @param tables List containing open "mysql.tables_priv" and "mysql.columns_priv" tables. @see grant_reload @return Error state @retval false Success @retval true Error */ static bool grant_load(THD *thd, TABLE_LIST *tables) { bool return_val = 1; int error; TABLE *t_table = 0, *c_table = 0; bool check_no_resolve = specialflag & SPECIAL_NO_RESOLVE; sql_mode_t old_sql_mode = thd->variables.sql_mode; DBUG_TRACE; thd->variables.sql_mode &= ~MODE_PAD_CHAR_TO_FULL_LENGTH; column_priv_hash.reset( new malloc_unordered_multimap>( key_memory_acl_memex)); t_table = tables[0].table; c_table = tables[1].table; error = t_table->file->ha_index_init(0, 1); DBUG_EXECUTE_IF("wl7158_grant_load_1", t_table->file->ha_index_end(); error = HA_ERR_LOCK_DEADLOCK;); if (error) { acl_print_ha_error(error); goto end_index_init; } t_table->use_all_columns(); c_table->use_all_columns(); error = t_table->file->ha_index_first(t_table->record[0]); DBUG_ASSERT(t_table->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_DEADLOCK); DBUG_ASSERT(t_table->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_WAIT_TIMEOUT); DBUG_EXECUTE_IF("wl7158_grant_load_2", error = HA_ERR_LOCK_DEADLOCK;); if (error) { if (error == HA_ERR_END_OF_FILE) return_val = 0; // Return Ok. else acl_print_ha_error(error); } else { Swap_mem_root_guard guard(thd, &memex); do { GRANT_TABLE *mem_check = new (thd->mem_root) GRANT_TABLE(t_table); if (!mem_check) { /* This could only happen if we are out memory */ goto end_unlock; } if (mem_check->init(c_table)) { destroy(mem_check); goto end_unlock; } if (check_no_resolve) { if (hostname_requires_resolving(mem_check->host.get_host()) && strcmp(mem_check->host.get_host(), "localhost") != 0) { LogErr(WARNING_LEVEL, ER_AUTHCACHE_TABLES_PRIV_SKIPPED_NEEDS_RESOLVE, mem_check->tname, mem_check->user ? mem_check->user : "", mem_check->host.get_host() ? mem_check->host.get_host() : ""); } } if (!mem_check->ok()) { destroy(mem_check); } else { column_priv_hash->emplace( mem_check->hash_key, unique_ptr_destroy_only(mem_check)); } error = t_table->file->ha_index_next(t_table->record[0]); DBUG_ASSERT(t_table->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_DEADLOCK); DBUG_ASSERT(t_table->file->ht->db_type == DB_TYPE_NDBCLUSTER || error != HA_ERR_LOCK_WAIT_TIMEOUT); DBUG_EXECUTE_IF("wl7158_grant_load_3", error = HA_ERR_LOCK_DEADLOCK;); if (error) { if (error != HA_ERR_END_OF_FILE) acl_print_ha_error(error); else return_val = 0; goto end_unlock; } } while (true); } end_unlock: t_table->file->ha_index_end(); end_index_init: thd->variables.sql_mode = old_sql_mode; return return_val; } /** @brief Helper function to grant_reload. Reloads procs_priv table is it exists. @param table A pointer to the table list. @see grant_reload @return Error state @retval false Success @retval true An error has occurred. */ static bool grant_reload_procs_priv(TABLE_LIST *table) { DBUG_TRACE; /* Save a copy of the current hash if we need to undo the grant load */ unique_ptr< malloc_unordered_multimap>> old_proc_priv_hash(move(proc_priv_hash)); unique_ptr< malloc_unordered_multimap>> old_func_priv_hash(move(func_priv_hash)); bool return_val = false; if ((return_val = grant_load_procs_priv(table->table))) { /* Error; Reverting to old hash */ DBUG_PRINT("error", ("Reverting to old privileges")); proc_priv_hash = move(old_proc_priv_hash); func_priv_hash = move(old_func_priv_hash); } return return_val; } /** @brief Reload information about table and column level privileges if possible @param thd Current thread @param mdl_locked MDL lock status - affects open/close table operations Locked tables are checked by acl_reload() and doesn't have to be checked in this call. This function is also used for initialization of structures responsible for table/column-level privilege checking. @return Error state @retval false Success @retval true Error */ bool grant_reload(THD *thd, bool mdl_locked) { MEM_ROOT old_mem; bool return_val = true; uint flags = mdl_locked ? MYSQL_OPEN_HAS_MDL_LOCK | MYSQL_LOCK_IGNORE_TIMEOUT | MYSQL_OPEN_IGNORE_FLUSH : MYSQL_LOCK_IGNORE_TIMEOUT; Acl_cache_lock_guard acl_cache_lock(thd, Acl_cache_lock_mode::WRITE_MODE); DBUG_TRACE; /* Don't do anything if running with --skip-grant-tables */ if (!initialized) return 0; TABLE_LIST tables[3] = { /* Acquiring strong MDL lock allows to avoid deadlock and timeout errors from SE level. */ TABLE_LIST("mysql", "tables_priv", TL_READ, MDL_SHARED_READ_ONLY), TABLE_LIST("mysql", "columns_priv", TL_READ, MDL_SHARED_READ_ONLY), TABLE_LIST("mysql", "procs_priv", TL_READ, MDL_SHARED_READ_ONLY)}; tables[0].next_local = tables[0].next_global = tables + 1; tables[1].next_local = tables[1].next_global = tables + 2; tables[0].open_type = tables[1].open_type = tables[2].open_type = OT_BASE_ONLY; if (open_and_lock_tables(thd, tables, flags)) { if (!is_expected_or_transient_error(thd)) { LogErr(ERROR_LEVEL, ER_AUTHCACHE_CANT_OPEN_AND_LOCK_PRIVILEGE_TABLES, thd->get_stmt_da()->message_text()); } goto end; } if (!acl_cache_lock.lock()) goto end; { unique_ptr< malloc_unordered_multimap>> old_column_priv_hash(move(column_priv_hash)); /* Create a new memory pool but save the current memory pool to make an undo opertion possible in case of failure. */ old_mem = move(memex); init_sql_alloc(key_memory_acl_memex, &memex, ACL_ALLOC_BLOCK_SIZE, 0); /* tables[2].table i.e. procs_priv can be null if we are working with pre 4.1 privilage tables */ if ((return_val = (grant_load(thd, tables) || grant_reload_procs_priv( &tables[2])))) { // Error. Revert to old hash DBUG_PRINT("error", ("Reverting to old privileges")); column_priv_hash = move(old_column_priv_hash); /* purecov: deadcode */ free_root(&memex, MYF(0)); memex = move(old_mem); /* purecov: deadcode */ } else { // Reload successful old_column_priv_hash.reset(); free_root(&old_mem, MYF(0)); grant_version++; get_global_acl_cache()->increase_version(); } } end: if (!mdl_locked) commit_and_close_mysql_tables(thd); else close_thread_tables(thd); return return_val; } void acl_update_user(const char *user, const char *host, enum SSL_type ssl_type, const char *ssl_cipher, const char *x509_issuer, const char *x509_subject, USER_RESOURCES *mqh, ulong privileges, const LEX_CSTRING &plugin, const LEX_CSTRING &auth, const std::string &second_auth, const MYSQL_TIME &password_change_time, const LEX_ALTER &password_life, Restrictions &restrictions, acl_table::Pod_user_what_to_update &what_to_update) { DBUG_TRACE; DBUG_ASSERT(assert_acl_cache_write_lock(current_thd)); for (ACL_USER *acl_user = acl_users->begin(); acl_user != acl_users->end(); ++acl_user) { if ((!acl_user->user && !user[0]) || (acl_user->user && !strcmp(user, acl_user->user))) { if ((!acl_user->host.get_host() && !host[0]) || (acl_user->host.get_host() && !my_strcasecmp(system_charset_info, host, acl_user->host.get_host()))) { if (plugin.length > 0) { acl_user->plugin.str = plugin.str; acl_user->plugin.length = plugin.length; optimize_plugin_compare_by_pointer(&acl_user->plugin); if (!auth_plugin_is_built_in(acl_user->plugin.str)) acl_user->plugin.str = strmake_root(&global_acl_memory, plugin.str, plugin.length); /* Update auth string only when specified in ALTER/GRANT */ if (auth.str) { if (auth.length == 0) acl_user->credentials[PRIMARY_CRED].m_auth_string = EMPTY_CSTR; else acl_user->credentials[PRIMARY_CRED].m_auth_string.str = strmake_root(&global_acl_memory, auth.str, auth.length); acl_user->credentials[PRIMARY_CRED].m_auth_string.length = auth.length; set_user_salt(acl_user); if (password_change_time.time_type != MYSQL_TIMESTAMP_ERROR) acl_user->password_last_changed = password_change_time; } if (what_to_update.m_what & USER_ATTRIBUTES) { if (what_to_update.m_user_attributes & acl_table::USER_ATTRIBUTE_RETAIN_PASSWORD) { acl_user->credentials[SECOND_CRED].m_auth_string.str = strmake_root(&global_acl_memory, second_auth.c_str(), second_auth.length()); acl_user->credentials[SECOND_CRED].m_auth_string.length = second_auth.length(); } if (what_to_update.m_user_attributes & acl_table::USER_ATTRIBUTE_DISCARD_PASSWORD) { acl_user->credentials[SECOND_CRED].m_auth_string = EMPTY_CSTR; } set_user_salt(acl_user); } } DBUG_PRINT("info", ("Updates global privilege for %s@%s to %lu", acl_user->user, acl_user->host.get_host(), privileges)); acl_user->access = privileges; if (mqh->specified_limits & USER_RESOURCES::QUERIES_PER_HOUR) acl_user->user_resource.questions = mqh->questions; if (mqh->specified_limits & USER_RESOURCES::UPDATES_PER_HOUR) acl_user->user_resource.updates = mqh->updates; if (mqh->specified_limits & USER_RESOURCES::CONNECTIONS_PER_HOUR) acl_user->user_resource.conn_per_hour = mqh->conn_per_hour; if (mqh->specified_limits & USER_RESOURCES::USER_CONNECTIONS) acl_user->user_resource.user_conn = mqh->user_conn; if (ssl_type != SSL_TYPE_NOT_SPECIFIED) { acl_user->ssl_type = ssl_type; acl_user->ssl_cipher = (ssl_cipher ? strdup_root(&global_acl_memory, ssl_cipher) : 0); acl_user->x509_issuer = (x509_issuer ? strdup_root(&global_acl_memory, x509_issuer) : 0); acl_user->x509_subject = (x509_subject ? strdup_root(&global_acl_memory, x509_subject) : 0); } /* update details related to password lifetime, password expiry */ if (password_life.update_password_expired_column || what_to_update.m_what & PLUGIN_ATTR) acl_user->password_expired = password_life.update_password_expired_column; if (!password_life.update_password_expired_column && password_life.update_password_expired_fields) { if (!password_life.use_default_password_lifetime) { acl_user->password_lifetime = password_life.expire_after_days; acl_user->use_default_password_lifetime = false; } else acl_user->use_default_password_lifetime = true; } if (password_life.update_account_locked_column) { acl_user->account_locked = password_life.account_locked; } /* Update role graph */ string authid_role = create_authid_str_from(acl_user); Role_index_map::iterator it = g_authid_to_vertex->find(authid_role); if (it != g_authid_to_vertex->end()) { boost::property_map::type user_pacl_user; user_pacl_user = boost::get(boost::vertex_acl_user_t(), *g_granted_roles); boost::put(user_pacl_user, it->second, *acl_user); } /* update password history */ if (password_life.update_password_history) { acl_user->use_default_password_history = password_life.use_default_password_history; acl_user->password_history_length = password_life.use_default_password_history ? 0 : password_life.password_history_length; } /* update password history */ if (password_life.update_password_reuse_interval) { acl_user->use_default_password_reuse_interval = password_life.use_default_password_reuse_interval; acl_user->password_reuse_interval = password_life.use_default_password_reuse_interval ? 0 : password_life.password_reuse_interval; } /* update current password field value */ if (password_life.update_password_require_current != Lex_acl_attrib_udyn::UNCHANGED) { acl_user->password_require_current = password_life.update_password_require_current; } acl_restrictions->upsert_restrictions(acl_user, restrictions); /* search complete: */ break; } } } } void acl_users_add_one(THD *thd MY_ATTRIBUTE((unused)), const char *user, const char *host, enum SSL_type ssl_type, const char *ssl_cipher, const char *x509_issuer, const char *x509_subject, USER_RESOURCES *mqh, ulong privileges, const LEX_CSTRING &plugin, const LEX_CSTRING &auth, const LEX_CSTRING &second_auth, const MYSQL_TIME &password_change_time, const LEX_ALTER &password_life, bool add_role_vertex, Restrictions &restrictions) { DBUG_TRACE; ACL_USER acl_user; DBUG_ASSERT(assert_acl_cache_write_lock(thd)); /* All accounts can authenticate per default. This will change when we add a new field to the user table. Currently this flag is only set to false when authentication is attempted using an unknown user name. */ acl_user.can_authenticate = true; acl_user.user = user && *user ? strdup_root(&global_acl_memory, user) : 0; acl_user.host.update_hostname( host && *host ? strdup_root(&global_acl_memory, host) : 0); DBUG_ASSERT(plugin.str); if (plugin.str[0]) { acl_user.plugin = plugin; optimize_plugin_compare_by_pointer(&acl_user.plugin); if (!auth_plugin_is_built_in(acl_user.plugin.str)) acl_user.plugin.str = strmake_root(&global_acl_memory, plugin.str, plugin.length); acl_user.credentials[PRIMARY_CRED].m_auth_string.str = auth.str ? strmake_root(&global_acl_memory, auth.str, auth.length) : ""; acl_user.credentials[PRIMARY_CRED].m_auth_string.length = auth.str ? auth.length : 0; if (second_auth.length) { acl_user.credentials[SECOND_CRED].m_auth_string.str = strmake_root(&global_acl_memory, second_auth.str, second_auth.length); acl_user.credentials[SECOND_CRED].m_auth_string.length = second_auth.length; } else { acl_user.credentials[SECOND_CRED].m_auth_string = EMPTY_CSTR; } optimize_plugin_compare_by_pointer(&acl_user.plugin); } acl_user.access = privileges; acl_user.user_resource = *mqh; acl_user.sort = get_sort(2, acl_user.host.get_host(), acl_user.user); // acl_user.hostname_length=(uint) strlen(host); acl_user.ssl_type = (ssl_type != SSL_TYPE_NOT_SPECIFIED ? ssl_type : SSL_TYPE_NONE); acl_user.ssl_cipher = ssl_cipher ? strdup_root(&global_acl_memory, ssl_cipher) : 0; acl_user.x509_issuer = x509_issuer ? strdup_root(&global_acl_memory, x509_issuer) : 0; acl_user.x509_subject = x509_subject ? strdup_root(&global_acl_memory, x509_subject) : 0; /* update details related to password lifetime, password expiry, history */ acl_user.password_expired = password_life.update_password_expired_column; acl_user.password_lifetime = password_life.expire_after_days; acl_user.use_default_password_lifetime = password_life.use_default_password_lifetime; acl_user.password_last_changed = password_change_time; acl_user.account_locked = password_life.account_locked; acl_user.password_history_length = password_life.use_default_password_history ? 0 : password_life.password_history_length; acl_user.use_default_password_history = password_life.use_default_password_history; acl_user.password_reuse_interval = password_life.use_default_password_reuse_interval ? 0 : password_life.password_reuse_interval; acl_user.use_default_password_reuse_interval = password_life.use_default_password_reuse_interval; /* Assign the password_require_current field value to the ACL USER. if it was not specified then assign the default value */ if (password_life.update_password_require_current == Lex_acl_attrib_udyn::UNCHANGED) { acl_user.password_require_current = Lex_acl_attrib_udyn::DEFAULT; } else { acl_user.password_require_current = password_life.update_password_require_current; } acl_restrictions->upsert_restrictions(&acl_user, restrictions); set_user_salt(&acl_user); /* New user is not a role by default. */ acl_user.is_role = false; acl_users->push_back(acl_user); if (acl_user.host.check_allow_all_hosts()) allow_all_hosts = 1; // Anyone can connect /* purecov: tested */ if (add_role_vertex) { /* Add vertex to role graph. ACL_USER object is copied with a shallow copy */ create_role_vertex(&acl_user); } } void acl_insert_user(THD *thd MY_ATTRIBUTE((unused)), const char *user, const char *host, enum SSL_type ssl_type, const char *ssl_cipher, const char *x509_issuer, const char *x509_subject, USER_RESOURCES *mqh, ulong privileges, const LEX_CSTRING &plugin, const LEX_CSTRING &auth, const MYSQL_TIME &password_change_time, const LEX_ALTER &password_life, Restrictions &restrictions) { DBUG_TRACE; acl_users_add_one(thd, user, host, ssl_type, ssl_cipher, x509_issuer, x509_subject, mqh, privileges, plugin, auth, EMPTY_CSTR, password_change_time, password_life, true, restrictions); std::sort(acl_users->begin(), acl_users->end(), ACL_compare()); rebuild_cached_acl_users_for_name(); /* Rebuild 'acl_check_hosts' since 'acl_users' has been modified */ rebuild_check_host(); /* reparse mandatory roles variable */ opt_mandatory_roles_cache = false; } void acl_update_proxy_user(ACL_PROXY_USER *new_value, bool is_revoke) { DBUG_TRACE; DBUG_ASSERT(assert_acl_cache_write_lock(current_thd)); for (ACL_PROXY_USER *acl_user = acl_proxy_users->begin(); acl_user != acl_proxy_users->end(); ++acl_user) { if (acl_user->pk_equals(new_value)) { if (is_revoke) { DBUG_PRINT("info", ("delting ACL_PROXY_USER")); acl_proxy_users->erase(acl_user); } else { DBUG_PRINT("info", ("updating ACL_PROXY_USER")); acl_user->set_data(new_value); } break; } } } void acl_update_db(const char *user, const char *host, const char *db, ulong privileges) { DBUG_ASSERT(assert_acl_cache_write_lock(current_thd)); for (ACL_DB *acl_db = acl_dbs->begin(); acl_db < acl_dbs->end();) { if ((!acl_db->user && !user[0]) || (acl_db->user && !strcmp(user, acl_db->user))) { if ((!acl_db->host.get_host() && !host[0]) || (acl_db->host.get_host() && !strcmp(host, acl_db->host.get_host()))) { if ((!acl_db->db && !db[0]) || (acl_db->db && !strcmp(db, acl_db->db))) { if (privileges) acl_db->access = privileges; else { acl_db = acl_dbs->erase(acl_db); // Don't increment loop variable. continue; } } } } ++acl_db; } } /* Insert a user/db/host combination into the global acl_cache SYNOPSIS acl_insert_db() user User name host Host name db Database name privileges Bitmap of privileges NOTES Acl caches must be locked */ void acl_insert_db(const char *user, const char *host, const char *db, ulong privileges) { ACL_DB acl_db; DBUG_ASSERT(assert_acl_cache_write_lock(current_thd)); acl_db.user = strdup_root(&global_acl_memory, user); acl_db.host.update_hostname(*host ? strdup_root(&global_acl_memory, host) : 0); acl_db.db = strdup_root(&global_acl_memory, db); acl_db.access = privileges; acl_db.sort = get_sort(3, acl_db.host.get_host(), acl_db.db, acl_db.user); acl_dbs->push_back(acl_db); std::sort(acl_dbs->begin(), acl_dbs->end(), ACL_compare()); } void get_mqh(THD *thd, const char *user, const char *host, USER_CONN *uc) { ACL_USER *acl_user; Acl_cache_lock_guard acl_cache_lock(thd, Acl_cache_lock_mode::READ_MODE); if (initialized && acl_cache_lock.lock(false) && (acl_user = find_acl_user(host, user, false))) uc->user_resources = acl_user->user_resource; else memset(&uc->user_resources, 0, sizeof(uc->user_resources)); } /** Update the security context when updating the user Helper function. Update only if the security context is pointing to the same user and the user is not a proxied user for a different proxy user. And return true if the update happens (i.e. we're operating on the user account of the current user). Normalize the names for a safe compare. @param sctx The security context to update @param acl_user_ptr User account being updated @param expired new value of the expiration flag @return did the update happen ? */ bool update_sctx_cache(Security_context *sctx, ACL_USER *acl_user_ptr, bool expired) { const char *acl_host = acl_user_ptr->host.get_host(); const char *acl_user = acl_user_ptr->user; const char *sctx_user = sctx->priv_user().str; const char *sctx_host = sctx->priv_host().str; /* If the user is connected as a proxied user, verify against proxy user */ if (sctx->proxy_user().str && *sctx->proxy_user().str != '\0') { sctx_user = sctx->user().str; } if (!acl_host) acl_host = ""; if (!acl_user) acl_user = ""; if (!sctx_host) sctx_host = ""; if (!sctx_user) sctx_user = ""; if (!strcmp(acl_user, sctx_user) && !strcmp(acl_host, sctx_host)) { sctx->set_password_expired(expired); return true; } return false; } struct Acl_hash_entry { uint64 version; uchar *key; unsigned key_length; Acl_map *map; }; const uchar *hash_key(const uchar *el, size_t *length) { const Acl_hash_entry *entry = reinterpret_cast(el); *length = entry->key_length; return entry->key; } /** Allocate a new cache key based on active roles, current user and global cache version @param [out] out_key The resulting key @param [out] key_len Key length @param version Global Acl_cache version @param uid The authorization ID of the current user @param active_roles The active roles of the current user @return Success state @retval true OK @retval false Fatal error occurred. */ bool create_acl_cache_hash_key(uchar **out_key, unsigned *key_len, uint64 version, const Auth_id_ref &uid, const List_of_auth_id_refs &active_roles) { List_of_auth_id_refs::const_iterator it = active_roles.begin(); uint32 active_roles_size = 0; for (; it != active_roles.end(); ++it) { active_roles_size += it->first.length + it->second.length + 2; } auto auth_id(uid); *key_len = auth_id.first.length + auth_id.second.length + 2 + sizeof(uint64) + active_roles_size; *out_key = (uchar *)my_malloc(key_memory_acl_map_cache, *key_len, MYF(MY_WME)); if (out_key == NULL) return false; auth_id.first.str = auth_id.first.str ? auth_id.first.str : ""; auth_id.second.str = auth_id.second.str ? auth_id.second.str : ""; memcpy(*out_key, auth_id.first.str, auth_id.first.length); *(*out_key + uid.first.length) = '@'; memcpy(*out_key + auth_id.first.length + 1, auth_id.second.str, auth_id.second.length); uint offset = auth_id.first.length + auth_id.second.length + 1; /* Separator between version and role */ *(*out_key + offset) = '`'; ++offset; memcpy(*out_key + offset, reinterpret_cast(&version), sizeof(uint64)); it = active_roles.begin(); offset += sizeof(uint64); for (; it != active_roles.end(); ++it) { memcpy(*out_key + offset, it->first.str, it->first.length); *(*out_key + offset + it->first.length) = '@'; memcpy(*out_key + offset + it->first.length + 1, it->second.str, it->second.length); offset += it->first.length + it->second.length + 1; /* Separator between roles */ *(*out_key + offset) = '`'; ++offset; } DBUG_ASSERT(((offset - *key_len) == 0)); return true; } Acl_cache::Acl_cache() : m_role_graph_version(0L) { const char *category = "sql"; int count; count = static_cast(array_elements(all_acl_cache_mutexes)); mysql_mutex_register(category, all_acl_cache_mutexes, count); lf_hash_init(&m_cache, sizeof(Acl_hash_entry), LF_HASH_UNIQUE, 0, /* key offset */ 0, /* key length not used */ hash_key, &my_charset_bin); mysql_mutex_init(key_LOCK_acl_cache_flush, &m_cache_flush_mutex, MY_MUTEX_INIT_SLOW); } Acl_cache::~Acl_cache() { mysql_mutex_destroy(&m_cache_flush_mutex); lf_hash_destroy(&m_cache); } Acl_map::Acl_map(Security_context *sctx, uint64 ver) : m_reference_count(0), m_version(ver), m_restrictions(nullptr) { DBUG_TRACE; Acl_cache_lock_guard acl_cache_lock(current_thd, Acl_cache_lock_mode::READ_MODE); if (!acl_cache_lock.lock(false)) { DBUG_PRINT("error", ("Acl_map could not be constructed for user %s@%s => " "Could not lock Acl caches.", sctx->priv_user().str, sctx->priv_host().str)); return; } m_global_acl = 0; ACL_USER *acl_user = find_acl_user(sctx->priv_host().str, sctx->priv_user().str, true); if (acl_user == nullptr) { DBUG_PRINT("error", ("Acl_map could not be constructed for user %s@%s => " "No such user", sctx->priv_user().str, sctx->priv_host().str)); return; } List_of_granted_roles granted_roles; get_privilege_access_maps( acl_user, sctx->get_active_roles(), &m_global_acl, &m_db_acls, &m_db_wild_acls, &m_table_acls, &m_sp_acls, &m_func_acls, &granted_roles, &m_with_admin_acls, &m_dynamic_privileges, m_restrictions); } Acl_map::~Acl_map() { // Db_access_map is automatically destroyed and cleaned up. } Acl_map::Acl_map(const Acl_map &&map) : m_restrictions(nullptr) { operator=(map); } Acl_map &Acl_map::operator=(Acl_map &&map) { m_db_acls = move(map.m_db_acls); m_global_acl = map.m_global_acl; m_reference_count = map.m_reference_count.load(); m_table_acls = move(map.m_table_acls); m_sp_acls = move(map.m_sp_acls); m_func_acls = move(map.m_func_acls); m_with_admin_acls = move(map.m_with_admin_acls); m_version = map.m_version; m_restrictions = map.m_restrictions; map.m_reference_count = 0; return *this; } Acl_map &Acl_map::operator=(const Acl_map &) { return *this; } ulong Acl_map::global_acl() { return m_global_acl; } Db_access_map *Acl_map::db_acls() { return &m_db_acls; } Db_access_map *Acl_map::db_wild_acls() { return &m_db_wild_acls; } Table_access_map *Acl_map::table_acls() { return &m_table_acls; } Grant_acl_set *Acl_map::grant_acls() { return &m_with_admin_acls; } SP_access_map *Acl_map::sp_acls() { return &m_sp_acls; } SP_access_map *Acl_map::func_acls() { return &m_func_acls; } Dynamic_privileges *Acl_map::dynamic_privileges() { return &m_dynamic_privileges; } Restrictions &Acl_map::restrictions() { return m_restrictions; } void Acl_map::increase_reference_count() { ++m_reference_count; } void Acl_map::decrease_reference_count() { --m_reference_count; } void Acl_cache::increase_version() { DBUG_TRACE; ++m_role_graph_version; flush_cache(); } uint64 Acl_cache::version() { return m_role_graph_version.load(); } int32 Acl_cache::size() { return m_cache.count.load(); } /** Finds an Acl_map entry in the Acl_cache and increase its reference count. If no Acl_map is located, a new one is created with reference count one. The Acl_map is returned to the caller. @param sctx The target Security_context @param uid The target authid @param active_roles A list of active roles @return A pointer to an Acl_map @retval !NULL Success @retval NULL A fatal OOM error happened. */ Acl_map *Acl_cache::checkout_acl_map(Security_context *sctx, Auth_id_ref &uid, List_of_auth_id_refs &active_roles) { DBUG_TRACE; // CREATE KEY uchar *key; // allocated by create_hash_key and released by // Acl_cache::flush_cache unsigned key_len; uint64 version = m_role_graph_version.load(); if (!create_acl_cache_hash_key(&key, &key_len, version, uid, active_roles)) { /* OOM happened */ active_roles.clear(); return 0; } LF_PINS *pins = lf_hash_get_pins(&m_cache); Acl_hash_entry *entry = (Acl_hash_entry *)lf_hash_search(&m_cache, pins, key, key_len); if (entry == 0 || entry == MY_LF_ERRPTR) { lf_hash_search_unpin(pins); Acl_map *map = create_acl_map(version, sctx); // deleted in cache_flusher Acl_hash_entry new_entry; new_entry.version = version; new_entry.map = map; new_entry.key = key; new_entry.key_length = key_len; int rc = lf_hash_insert(&m_cache, pins, &new_entry); // shallow copy of entry if (rc != 0) { /* There was a duplicate; throw away the allocated memory */ lf_hash_put_pins(pins); my_free(key); delete map; DBUG_PRINT("info", ("Someone else checked out the cache key")); /* Potentially dangerous to dive here? */ return checkout_acl_map(sctx, uid, active_roles); } map->increase_reference_count(); lf_hash_put_pins(pins); DBUG_PRINT("info", ("Checked out new privilege map. Key= %s", key)); return map; } Acl_map *map = entry->map; map->increase_reference_count(); lf_hash_search_unpin(pins); lf_hash_put_pins(pins); my_free(key); DBUG_PRINT("info", ("Checked out old privilege map. Key= %s", key)); return map; } void Acl_cache::return_acl_map(Acl_map *map) { map->decrease_reference_count(); } /** This global is protected by the Acl_cache::m_cache_flush_mutex and used when iterating the Acl_map hash in Acl_cache::flush_cache @see Acl_cache::flush_cache */ uint64 l_cache_flusher_global_version; /** Utility function for removing all items from the hash. @param ptr A pointer to a Acl_hash_entry @return Always 0 with the intention that this causes the hash_search function to iterate every single element in the hash. */ static int cache_flusher(const uchar *ptr) { DBUG_TRACE; const Acl_hash_entry *entry = reinterpret_cast(ptr); if (entry != 0) { if (entry->map->reference_count() == 0 && entry->map->version() < l_cache_flusher_global_version) return 1; } return 0; } void Acl_cache::flush_cache() { DBUG_TRACE; LF_PINS *pins = lf_hash_get_pins(&m_cache); Acl_hash_entry *entry = 0; mysql_mutex_lock(&m_cache_flush_mutex); l_cache_flusher_global_version = version(); do { entry = static_cast( lf_hash_random_match(&m_cache, pins, &cache_flusher, 0)); if (entry && !lf_hash_delete(&m_cache, pins, entry->key, entry->key_length)) { // Hash element is removed from cache; safe to delete my_free(entry->key); delete entry->map; } lf_hash_search_unpin(pins); } while (entry != 0); lf_hash_put_pins(pins); mysql_mutex_unlock(&m_cache_flush_mutex); } Acl_map *Acl_cache::create_acl_map(uint64 version, Security_context *sctx) { Acl_map *map = new Acl_map(sctx, version); return map; } void *Acl_map::operator new(size_t size) { return my_malloc(key_memory_acl_map_cache, size, MYF(0)); } void Acl_map::operator delete(void *p) { my_free(p); } void init_acl_cache() { g_acl_cache = new Acl_cache(); g_mandatory_roles = new std::vector; opt_mandatory_roles_cache = false; } /** Shutdown the global Acl_cache system which was only initialized if the rwlocks were initialized. @see acl_init() */ void shutdown_acl_cache() { if (!acl_cache_initialized) return; /* This should clean up all remaining Acl_cache items */ g_acl_cache->increase_version(); DBUG_ASSERT(g_acl_cache->size() == 0); delete g_acl_cache; g_acl_cache = NULL; roles_delete(); dynamic_privileges_delete(); delete g_mandatory_roles; } /* Constants used by Acl_cache_lock_guard */ static const ulong ACL_CACHE_LOCK_TIMEOUT = 3600UL; static const MDL_key ACL_CACHE_KEY(MDL_key::ACL_CACHE, "", ""); /** Internal_error_handler subclass to suppress ER_LOCK_DEADLOCK, ER_LOCK_WAIT_TIMEOUT, ER_QUERY_INTERRUPTED and ER_QUERY_TIMEOUT. Instead, we will use Acl_cache_lock_guard::lock() to raise ER_CANNOT_LOCK_USER_MANAGEMENT_CACHES error. */ class Acl_cache_error_handler : public Internal_error_handler { public: /** Handle an error condition @param [in] thd THD handle @param [in] sql_errno Error raised by MDL subsystem @param [in] sqlstate SQL state. Unused. @param [in] level Severity level. Unused. @param [in] msg Message string. Unused. */ virtual bool handle_condition(THD *thd MY_ATTRIBUTE((unused)), uint sql_errno, const char *sqlstate MY_ATTRIBUTE((unused)), Sql_condition::enum_severity_level *level MY_ATTRIBUTE((unused)), const char *msg MY_ATTRIBUTE((unused))) { return (sql_errno == ER_LOCK_DEADLOCK || sql_errno == ER_LOCK_WAIT_TIMEOUT || sql_errno == ER_QUERY_INTERRUPTED || sql_errno == ER_QUERY_TIMEOUT); } }; /* MDL_release_locks_visitor subclass to release MDL for ACL_CACHE. */ class Release_acl_cache_locks : public MDL_release_locks_visitor { public: /** Lock releaser. Check details of given key and see it is of type ACL_CACHE and if key name it matches with m_partition. If so, release it. @param [in] ticket MDL Ticket returned by MDL subsystem @returns Whether ticket matches our criteria or not @retval true Ticket matches @retval false Ticket does not match */ virtual bool release(MDL_ticket *ticket) { return ticket->get_key()->mdl_namespace() == MDL_key::ACL_CACHE; } }; /** Acl_cache_lock_guard constructor. @param [in] thd THD Handle. @param [in] mode Lock mode */ Acl_cache_lock_guard::Acl_cache_lock_guard(THD *thd, Acl_cache_lock_mode mode) : m_thd(thd), m_mode(mode), m_locked(false) { DBUG_ASSERT(thd); } /** Explicitly take lock on Acl_cache_lock_cache object. If cache was already locked, just return. @param [in] raise_error Whether to raise error if we fail to acquire lock @returns status of lock @retval true Lock was acquired/already acquired. @retval false There was some problem acquiring lock. */ bool Acl_cache_lock_guard::lock(bool raise_error) /* = true */ { DBUG_ASSERT(!m_locked); if (already_locked()) return true; /* If we do not have MDL, we should not be holding LOCK_open */ mysql_mutex_assert_not_owner(&LOCK_open); MDL_request lock_request; MDL_REQUEST_INIT_BY_KEY( &lock_request, &ACL_CACHE_KEY, m_mode == Acl_cache_lock_mode::READ_MODE ? MDL_SHARED : MDL_EXCLUSIVE, MDL_EXPLICIT); Acl_cache_error_handler handler; m_thd->push_internal_handler(&handler); m_locked = !m_thd->mdl_context.acquire_lock(&lock_request, ACL_CACHE_LOCK_TIMEOUT); m_thd->pop_internal_handler(); if (!m_locked && raise_error) my_error(ER_CANNOT_LOCK_USER_MANAGEMENT_CACHES, MYF(0)); return m_locked; } /** Explicitly unlock all acquired locks. */ void Acl_cache_lock_guard::unlock() { /* It is possible that we did not take any lock. E.g. 1. Function 1 : Take shared lock on ACL caches 2. Function 1 : Take Lock_open 3. Call Function 2 4. Function 2 : Try to acquire shared lock on ACL caches again In such cases, at 4, we do not actually take MDL because it will be in conflict with LOCK_open If unlock() is called as part of destructor or directly in Function 2 above, we should not release any locks because we never acquired any in the first place. */ if (m_locked) { Release_acl_cache_locks lock_visitor; m_thd->mdl_context.release_locks(&lock_visitor); m_locked = false; } } /** Check whether lock is already obtained or not. @returns lock status @retval true Lock has already been taken. @retval false No lock is taken with this Acl_cache_lock_guard object */ bool Acl_cache_lock_guard::already_locked() { return m_thd->mdl_context.owns_equal_or_stronger_lock( MDL_key::ACL_CACHE, "", "", m_mode == Acl_cache_lock_mode::READ_MODE ? MDL_SHARED : MDL_EXCLUSIVE); } /** Assert that thread owns MDL_SHARED on partition specific to the thread @param [in] thd Thread for which lock is to be checked @returns thread owns required lock or not @retval true Thread owns lock @retval false Thread does not own lock */ bool assert_acl_cache_read_lock(THD *thd) { return thd->mdl_context.owns_equal_or_stronger_lock(MDL_key::ACL_CACHE, "", "", MDL_SHARED); } /** Assert that thread owns MDL_EXCLUSIVE on all partitions @param [in] thd Thread for which lock is to be checked @returns thread owns required lock or not @retval true Thread owns lock @retval false Thread does not own lock */ bool assert_acl_cache_write_lock(THD *thd) { return thd->mdl_context.owns_equal_or_stronger_lock(MDL_key::ACL_CACHE, "", "", MDL_EXCLUSIVE); } /** Global sysvar: the number of old passwords to check in the history. */ uint32 global_password_history = 0; /** Global sysvar: the number of days before a password can be reused. */ uint32 global_password_reuse_interval = 0; /** Reload all ACL caches @param [in] thd THD handle @param [in] mdl_locked MDL locks are taken @returns Status of reloading ACL caches @retval false Success @retval true Error */ bool reload_acl_caches(THD *thd, bool mdl_locked) { bool retval = true; DBUG_TRACE; if (check_engine_type_for_acl_table(thd, mdl_locked) || check_acl_tables_intact(thd, mdl_locked) || acl_reload(thd, mdl_locked) || grant_reload(thd, mdl_locked)) { goto end; } retval = false; end: return retval; } /** Determine sort order for two user accounts @param [in] a First user account's sort value @param [in] b Secound user account's sort value @returns Whether a comes before b or not */ bool ACL_compare::operator()(const ACL_ACCESS &a, const ACL_ACCESS &b) { return a.sort > b.sort; } /** Determine sort order for two user accounts @param [in] a First user account's sort value @param [in] b Secound user account's sort value @returns Whether a comes before b or not */ bool ACL_compare::operator()(const ACL_ACCESS *a, const ACL_ACCESS *b) { return a->sort > b->sort; } /** Construstor */ Acl_restrictions::Acl_restrictions() : m_restrictions_map(key_memory_acl_mem) {} /** Remove the Restrictions of the ACL_USER. @param [in] acl_user The ACL_USER for whom to remove the Restrictions */ void Acl_restrictions::remove_restrictions(const ACL_USER *acl_user) { DBUG_ASSERT(assert_acl_cache_write_lock(current_thd)); const Auth_id auth_id(acl_user); auto itr = m_restrictions_map.find(auth_id.auth_str()); if (itr != m_restrictions_map.end()) m_restrictions_map.erase(itr); } /** Update, insert or remove the Restrictions for the ACL_USER. If ACL_USER has a Restrictions - If specified Restrictions is not empty then update ACL_USER's Restrictions - Otherwise clear the ACL_USER's restriction Else if there no Restrictions for the ACL_USER then insert the specified Restrictions. @param [in] acl_user The ACL_USER for whom to alter the Restrictions @param [in] restrictions Restrictions to be inserted for the ACL_USER */ void Acl_restrictions::upsert_restrictions(const ACL_USER *acl_user, const Restrictions &restrictions) { DBUG_ASSERT(assert_acl_cache_write_lock(current_thd)); const Auth_id auth_id(acl_user); const std::string auth_str = auth_id.auth_str(); auto restrictions_itr = m_restrictions_map.find(auth_str); if (restrictions_itr != m_restrictions_map.end()) { if (restrictions.is_empty()) { /* Empty restrictions means we want to remove that from global cache */ m_restrictions_map.erase(restrictions_itr); } else { /* If there exists restrictions then update that in the global cache */ restrictions_itr->second = restrictions; } } else if (!restrictions.is_empty()) { /* Insert non-empty restrictions object in the global cache */ m_restrictions_map.emplace(auth_str, restrictions); } } /** Find the Restrictions of the ACL_USER. @param [in] acl_user The ACL_USER for whom to find the Restrictions @returns valid Restrictions if found otherwise empty Restrictions */ Restrictions Acl_restrictions::find_restrictions( const ACL_USER *acl_user) const { DBUG_ASSERT(assert_acl_cache_read_lock(current_thd)); const Auth_id auth_id(acl_user); auto restrictions_itr = m_restrictions_map.find(auth_id.auth_str()); if (restrictions_itr != m_restrictions_map.end()) return restrictions_itr->second; else return Restrictions(nullptr); } /** @returns the number of Restrictions present. It is not thread safe method. */ size_t Acl_restrictions::size() const { return m_restrictions_map.size(); } /** Method to check if there exists at least one partial revokes in the cache. If the cache is not initialized at the time of the method call then it returns no partial revokes exists. @param [in] thd THD handle @returns @retval true Partial revokes exists @retval false Otherwise */ bool is_partial_revoke_exists(THD *thd) { bool partial_revoke = false; if (thd) { Acl_cache_lock_guard acl_cache_lock(thd, Acl_cache_lock_mode::READ_MODE); if (!acl_cache_lock.lock(false)) { return true; } DBUG_ASSERT(acl_restrictions); partial_revoke = (acl_restrictions->size() > 0); } else { /* We need to determine the number of partial revokes at the time of server start. In that case thd(s) is not be available so it is safe to determine the number of partial revokes without lock. */ if (acl_restrictions) partial_revoke = (acl_restrictions->size() > 0); } return partial_revoke; }