# Copyright (c) 2013, 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 import grt from workbench.log import log_warning, log_error from workbench.db_utils import parse_mysql_ids class PrivilegeTarget(object): """ This class holds the information of the priv_level item at the GRANT statement. It will be used not only to hold the information but also to provide functions for the matching against other priv_level definitions. """ def __init__(self): self.schema = "*" self.object = "*" self.object_type = "*" def set_from_string(self, data): if data != '*' and data != '*.*': values = parse_mysql_ids(data) if len(values) == 2: self.schema, self.object = tuple(values) else: self.object = data def identical(self, target_string): return target_string == '%s.%s' % (self.schema, self.object) def matches(self, other): """ There are three scope types considered: - Global: schema and table are * - Schema: schema is * - Table: Both are not * """ ret_val = False # Global privilege matches any target definition if self.schema == '*' and self.object == '*': ret_val = True elif self.schema == other.schema: if self.object == '*': ret_val = True else: ret_val = (self.object == other.object) return ret_val def get_target_type(self): """ Returns the privilege level based on the data. Note: COLUMN is not considered at this point. """ ret_val = '' if self.schema == '*' and self.object == '*': ret_val = 'GLOBAL' elif self.object != '*': ret_val = 'OBJECT' else: ret_val = 'DATABASE' return ret_val class UserHostPrivileges(object): """ This class is intended to load the privileges applicable to a user/host pair. Once loaded it will be able to verify if specific privileges are granted to the user or not. Initialization parameters: - user: the username for which the privileges will be verified. - host: the real hostname from which the user will be connecting to the database. (No wildcard should be used). - context: needed to query the data from the database. """ def __init__(self, user, host, context): self.user = user self.host = host self.context = context # Creates the privilege lists available at the different levels self._define_privilege_tables() # Will hold the configured hosts that apply to the target host self.applicant_hosts = [] # Will be used to accummulate the privileges applicable to the user/host # combination. # The format is a key/value pair where key is the target definition # And the key is the binary representation of the granted privileges # on that target. self._granted_privileges = {} self._character_sets = self.context.ctrl_be.editor.connection.driver.owner.characterSets self._target_version = self.context.ctrl_be.editor.serverVersion # Sets the server version as needed for the parser version = self.context.ctrl_be.target_version if version: self._server_version = version.majorNumber * 10000 + version.minorNumber * 100 + version.releaseNumber else: self._server_version = 0 def _define_privilege_tables(self): """ This method is used just to create the privilege lists at the different levels available in MySQL """ self._privileges = {} self._privileges['COLUMN'] = ['INSERT','SELECT', 'UPDATE'] self._privileges['OBJECT'] = [] self._privileges['OBJECT'].extend(self._privileges['COLUMN']) self._privileges['OBJECT'].extend(['ALTER', 'ALTER ROUTINE', 'CREATE', 'CREATE ROUTINE', 'CREATE VIEW', 'DELETE', 'DROP', 'EXECUTE', 'GRANT OPTION', 'INDEX', 'SHOW VIEW']) self._privileges['DATABASE'] = [] self._privileges['DATABASE'].extend(self._privileges['OBJECT']) self._privileges['DATABASE'].extend(['CREATE TEMPORARY TABLES', 'LOCK TABLES']) self._privileges['GLOBAL'] = [] self._privileges['GLOBAL'].extend(self._privileges['DATABASE']) self._privileges['GLOBAL'].extend(['FILE', 'PROCESS', 'RELOAD', 'REPLICATION CLIENT', 'REPLICATION SLAVE', 'SHOW DATABASES', 'SHUTDOWN', 'SUPER', 'CREATE USER']) def get_privilege_value(self, privileges, level): """ This function will return a binary value indicating the set of privileges at the indicated level. """ ret_val = 0 if level in self._privileges: new_value = 0 # Gets the value of each privilege in privileges and appends its # value to the binary representation of all the privileges for priv in privileges: try: index = self._privileges[level].index(priv) new_value = 2 ** index; except ValueError: if priv == 'ALL' or priv == 'ALL PRIVILEGES': new_value = (2 ** len(self._privileges[level])) - 1 new_value -= self.get_privilege_value(['GRANT OPTION'], 'OBJECT') ret_val |= new_value return ret_val def add_privileges(self, target, priv_list): # Converts the privilege lists to its binary representation priv_value = self.get_privilege_value(priv_list, target.get_target_type()) # Creates the entry for the target if it doesn't exist target_to_use = None for tgt in list(self._granted_privileges.keys()): if tgt.matches(target): target_to_use = tgt if target_to_use is None: self._granted_privileges[target] = 0 target_to_use = target # Appends the new privileges to the existing ones self._granted_privileges[target_to_use] |= priv_value def check_privileges(self, target, privileges): for tgt in list(self._granted_privileges.keys()): if tgt.matches(target): # Gets the included privileges includes = self._granted_privileges[tgt] & privileges # Unsets the already included privileges privileges -= includes # Returns the remaining privileges return privileges def load_hosts(self): """ Will identify the host in the database that are applicable to a given host (Host in database can be defined with wildcard so it is not a straight comparison) """ hosts = None # note the comparison of the host field is backwards than normal. # this in on purpose to allow identifying using SQL all the hosts in the grant definitions # that do match the host being queried. result = self.context.ctrl_be.exec_query("""SELECT host FROM mysql.user WHERE user = '%s' AND '%s' LIKE host""" % (self.user, self.host)) if result: hosts = [] while result.nextRow(): hosts.append(result.stringByIndex(1)) else: log_warning('There are no grants defined for the user %s on hosts matching %s\n' % (self.user, self.host)) self.applicant_hosts = hosts return hosts def load_privileges(self, host): # Clears any previously loaded privileges self._granted_privileges = {} if host in self.applicant_hosts: # If there are hosts it means there are privileges applicable for the user # On the indicated host result = self.context.ctrl_be.exec_query("SHOW GRANTS FOR `%s`@`%s`" % (self.user, host)) context = grt.modules.MySQLParserServices.createNewParserContext(self._character_sets, self._target_version, 'STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION', 1) if result: while result.nextRow(): statement = result.stringByIndex(1) grant_data = None try: grant_data = grt.modules.MySQLParserServices.parseStatementDetails(context, statement) if not grant_data['error']: # Gets the target scope for the privileges target_string = grant_data['target'] target = None # Search for an already existing target for tgt in list(self._granted_privileges.keys()): if tgt.identical(target_string): target = tgt # If not found, creates one if not target: target = PrivilegeTarget() target.set_from_string(target_string) # Gets the privilege list priv_list = grant_data['privileges'] # Adds the privileges to the granted list self.add_privileges(target, priv_list) else: log_error('An error occurred parsing GRANT statement: %s\n -> %s\n' % (statement, grant_data['error'])) except Exception as exc: log_error('An error occurred parsing GRANT statement: %s\n -> %s\n' % (statement, exc)) else: log_warning('There are no grants defined for %s@%s\n' % (self.user, self.host)) def includes_privileges(self, target_str, privileges): target = PrivilegeTarget() target.set_from_string(target_str) priv_val = self.get_privilege_value(privileges, target.get_target_type()) return self.check_privileges(target, priv_val) == 0