summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVicențiu Ciorbaru <cvicentiu@gmail.com>2022-01-13 18:18:02 +0200
committerVicențiu Ciorbaru <cvicentiu@gmail.com>2022-02-28 17:02:58 +0200
commit8dc38d653fb2e6ea78fbb80d9cf85e40c5794bc3 (patch)
tree724b79eadda4fa0f41255f56b0718e34f80e081a
parenta6afc5df7de60c82e15eb02f7bc21b8da8088404 (diff)
downloadmariadb-git-8dc38d653fb2e6ea78fbb80d9cf85e40c5794bc3.tar.gz
MDEV-14443: DENY <global> privileges
This commit introduces the logic to write to mysql.global_priv for global level denies. It also ensures that internal data structures within ACL_USER_BASE are updated appropriately.
-rw-r--r--mysql-test/suite/deny/deny_datastructures.result22
-rw-r--r--mysql-test/suite/deny/deny_datastructures.test22
-rw-r--r--sql/privilege.h37
-rw-r--r--sql/sql_acl.cc370
-rw-r--r--sql/sql_acl.h1
5 files changed, 438 insertions, 14 deletions
diff --git a/mysql-test/suite/deny/deny_datastructures.result b/mysql-test/suite/deny/deny_datastructures.result
new file mode 100644
index 00000000000..91f7732024d
--- /dev/null
+++ b/mysql-test/suite/deny/deny_datastructures.result
@@ -0,0 +1,22 @@
+create user foo;
+create role bar;
+#
+# Test deny format for global privileges.
+#
+deny select on *.* to foo;
+deny insert on *.* to bar;
+select user, host, JSON_EXTRACT(priv, '$.deny') from mysql.global_priv where user = 'foo';
+user host JSON_EXTRACT(priv, '$.deny')
+foo % {"global": 1}
+select user, host, JSON_EXTRACT(priv, '$.deny') from mysql.global_priv where user = 'bar';
+user host JSON_EXTRACT(priv, '$.deny')
+bar {"global": 2}
+flush privileges;
+show grants for foo;
+Grants for foo@%
+GRANT USAGE ON *.* TO `foo`@`%`
+show grants for bar;
+Grants for bar
+GRANT USAGE ON *.* TO `bar`
+drop user foo;
+drop role bar;
diff --git a/mysql-test/suite/deny/deny_datastructures.test b/mysql-test/suite/deny/deny_datastructures.test
new file mode 100644
index 00000000000..43300c8d1f9
--- /dev/null
+++ b/mysql-test/suite/deny/deny_datastructures.test
@@ -0,0 +1,22 @@
+create user foo;
+create role bar;
+
+--echo #
+--echo # Test deny format for global privileges.
+--echo #
+deny select on *.* to foo;
+deny insert on *.* to bar;
+
+select user, host, JSON_EXTRACT(priv, '$.deny') from mysql.global_priv where user = 'foo';
+select user, host, JSON_EXTRACT(priv, '$.deny') from mysql.global_priv where user = 'bar';
+
+flush privileges;
+
+#TODO(cvicentiu) Show denies where appropriate.
+show grants for foo;
+show grants for bar;
+
+#TODO(cvicentiu) Update results with further deny types.
+
+drop user foo;
+drop role bar;
diff --git a/sql/privilege.h b/sql/privilege.h
index e7708de8835..1756eee2c2e 100644
--- a/sql/privilege.h
+++ b/sql/privilege.h
@@ -754,5 +754,42 @@ static inline privilege_t get_rights_for_procedure(privilege_t access)
((A & GRANT_ACL) >> 8));
}
+enum PRIV_TYPE
+{
+ NO_PRIV = 0,
+ GLOBAL_PRIV = (1 << 0),
+ DATABASE_PRIV = (1 << 1),
+ TABLE_PRIV = (1 << 2),
+ COLUMN_PRIV = (1 << 3),
+ ROUTINE_PRIV = (1 << 4),
+ PROXY_PRIV = (1 << 5)
+};
+
+static inline constexpr PRIV_TYPE operator|(PRIV_TYPE a, PRIV_TYPE b)
+{
+ return static_cast<PRIV_TYPE>(static_cast<ulonglong>(a) |
+ static_cast<ulonglong>(b));
+}
+
+static inline constexpr PRIV_TYPE operator|(PRIV_TYPE a, ulonglong b)
+{
+ return static_cast<PRIV_TYPE>(static_cast<ulonglong>(a) |
+ static_cast<ulonglong>(b));
+}
+
+static inline constexpr PRIV_TYPE operator&(PRIV_TYPE a, PRIV_TYPE b)
+{
+ return static_cast<PRIV_TYPE>(static_cast<ulonglong>(a) &
+ static_cast<ulonglong>(b));
+}
+static inline PRIV_TYPE& operator|=(PRIV_TYPE &a, PRIV_TYPE b)
+{
+ return a= a | b;
+}
+
+static inline PRIV_TYPE& operator&=(PRIV_TYPE &a, PRIV_TYPE b)
+{
+ return a= a & b;
+}
#endif /* PRIVILEGE_H_INCLUDED */
diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc
index 572e86869db..d6e2d111147 100644
--- a/sql/sql_acl.cc
+++ b/sql/sql_acl.cc
@@ -141,11 +141,140 @@ class Priv_spec
{
public:
const bool revoke; // Whether the privilege bits are to be cleared.
+ const bool deny; // Whether the privilege bits need to be applied as deny.
const privilege_t access; // Privilege bits that need to be applied / removed.
+ PRIV_TYPE spec_type;
+ const LEX_CSTRING db;
+ const LEX_CSTRING table;
+ const LEX_CSTRING column;
+ const LEX_CSTRING routine;
+
+ Priv_spec(privilege_t access, bool revoke, bool deny=false):
+ revoke{revoke}, deny{deny}, access{access}, spec_type{PRIV_TYPE::GLOBAL_PRIV},
+ db{nullptr, 0}, table{nullptr, 0}, column{nullptr, 0}, routine{nullptr, 0}
+ { }
- Priv_spec(privilege_t access, bool revoke):
- revoke{revoke}, access{access}
- {}
+};
+
+
+class Deny_spec
+{
+ PRIV_TYPE specified_denies;
+ privilege_t global_denies;
+
+public:
+ Deny_spec() : specified_denies{NO_PRIV}, global_denies{NO_ACL} {}
+
+ ~Deny_spec()
+ {
+ }
+
+ privilege_t get_global() const { return global_denies; }
+ PRIV_TYPE get_specified_denies() const { return specified_denies; }
+
+ /*
+ {
+ "global": int
+ "db": [
+ {
+ "name": str
+ "access": int
+ },
+ ]
+ "table": [
+ {
+ "name": str
+ "access": int
+ },
+ ]
+ */
+ bool get_json_repr(String *s) const
+ {
+ bool append_comma= false;
+ s->length(0);
+ s->append('{');
+ if (specified_denies & GLOBAL_PRIV)
+ {
+ char v[FLOATING_POINT_BUFFER + 1];
+ size_t vlen= longlong10_to_str(global_denies, v, -10) - v;
+ s->append(STRING_WITH_LEN("\"global\":"));
+ s->append(v, vlen);
+ append_comma= true;
+ }
+ (void) append_comma; // TODO(cvicentiu) to be used for other deny types.
+ s->append('}');
+ return false;
+ }
+
+ bool update_deny(const Priv_spec &priv_spec)
+ {
+
+ /* Global */
+ switch (priv_spec.spec_type) {
+ case GLOBAL_PRIV:
+ {
+ if (priv_spec.revoke)
+ global_denies&= ~priv_spec.access;
+ else
+ global_denies|= priv_spec.access;
+ break;
+ }
+ case DATABASE_PRIV:
+ // TODO(cvicentiu)
+ DBUG_ASSERT(0);
+ break;
+ case TABLE_PRIV:
+ // TODO(cvicentiu)
+ DBUG_ASSERT(0);
+ break;
+ case COLUMN_PRIV:
+ // TODO(cvicentiu)
+ DBUG_ASSERT(0);
+ break;
+ case ROUTINE_PRIV:
+ // TODO(cvicentiu)
+ DBUG_ASSERT(0);
+ break;
+ case PROXY_PRIV:
+ // TODO(cvicentiu)
+ DBUG_ASSERT(0);
+ break;
+ case NO_PRIV:
+ /* TODO(cvicentiu) test this. */
+ DBUG_ASSERT(0);
+ return false;
+ }
+ specified_denies|= priv_spec.spec_type;
+ return false;
+ }
+
+ bool load_deny(const char *json_repr, size_t len)
+ {
+ const char* start= json_repr;
+ const char* end= start + len;
+ const char* v;
+ int int_vl, err;
+ enum json_types value_type;
+
+ value_type= json_get_object_key(start, end, "global", &v, &int_vl);
+
+ if (value_type != JSV_NUMBER && value_type != JSV_NOTHING)
+ return true;
+
+ if (value_type != JSV_NOTHING)
+ {
+ const char *v_end= v + int_vl;
+ global_denies= static_cast<privilege_t>(my_strtoll10(v, (char **)&v_end,
+ &err));
+ if (err)
+ return true;
+
+ if (global_denies)
+ specified_denies= GLOBAL_PRIV;
+ }
+ // TODO(cvicentiu) continue loading db denies and other json denies.
+ return false;
+ }
};
@@ -172,7 +301,7 @@ class ACL_USER_BASE :public ACL_ACCESS, public Sql_alloc
public:
ACL_USER_BASE()
- :flags(0), user(null_clex_str)
+ :flags(0), user(null_clex_str), denies(nullptr)
{
bzero(&role_grants, sizeof(role_grants));
}
@@ -180,9 +309,11 @@ public:
LEX_CSTRING user;
/* list to hold references to granted roles (ACL_ROLE instances) */
DYNAMIC_ARRAY role_grants;
+ const Deny_spec* denies;
const char *get_username() { return user.str; }
};
+
class ACL_USER_PARAM
{
public:
@@ -906,6 +1037,8 @@ class User_table: public Grant_table_base
virtual bool set_auth(const ACL_USER &u) const = 0;
virtual privilege_t get_access() const = 0;
virtual void set_access(const privilege_t rights) const = 0;
+ virtual bool set_deny(const Deny_spec *deny) const = 0;
+ virtual Deny_spec *get_deny() const = 0;
char *get_host(MEM_ROOT *root) const
{ return ::get_field(root, m_table->field[0]); }
@@ -1089,6 +1222,18 @@ class User_table_tabular: public User_table
m_table->field[i]->store(priv & rights ? 2 : 1, 0);
}
+ /* Does not support denies. TODO(cvicentiu): test*/
+ Deny_spec *get_deny() const
+ {
+ return nullptr;
+ }
+
+ /* Does not support denies. TODO(cvicentiu): test*/
+ bool set_deny(const Deny_spec* spec) const
+ {
+ return true;
+ }
+
SSL_type get_ssl_type () const
{
Field *f= get_field(end_priv_columns, MYSQL_TYPE_ENUM);
@@ -1633,6 +1778,36 @@ class User_table_json: public User_table
set_int_value("access", (longlong) (rights & GLOBAL_ACLS));
set_int_value("version_id", (longlong) MYSQL_VERSION_ID);
}
+
+ bool set_deny(const Deny_spec* spec) const
+ {
+ String s;
+ spec->get_json_repr(&s);
+ set_value("deny", s.c_ptr(), s.length(), false);
+ // TODO(cvicentiu) see if this can be ommited.
+ set_int_value("version_id", (longlong) MYSQL_VERSION_ID);
+ return false;
+ }
+
+ Deny_spec *get_deny() const
+ {
+ const char *value_start;
+ size_t vl;
+
+ // TODO(cvicentiu) figure out a better way to do error reporting.
+ if (get_value("deny", JSV_OBJECT, &value_start, &vl))
+ return nullptr;
+
+ Deny_spec *result= new Deny_spec;
+
+ if (result->load_deny(value_start, vl))
+ {
+ delete result;
+ return nullptr;
+ }
+ return result;
+ }
+
const char *unsafe_str(const char *s) const
{ return s[0] ? s : NULL; }
@@ -2159,16 +2334,22 @@ static bool is_invalid_role_name(const char *str)
return true;
}
+static void free_acl_user_base(ACL_USER_BASE *base)
+{
+ delete base->denies;
+}
static void free_acl_user(ACL_USER *user)
{
delete_dynamic(&(user->role_grants));
+ free_acl_user_base(user);
}
static void free_acl_role(ACL_ROLE *role)
{
delete_dynamic(&(role->role_grants));
delete_dynamic(&(role->parent_grantee));
+ free_acl_user_base(role);
}
static my_bool check_if_exists(THD *, plugin_ref, void *)
@@ -2569,6 +2750,7 @@ static bool acl_load(THD *thd, const Grant_tables& tables)
is_role= user_table.get_is_role();
user.access= user_table.get_access();
+ user.denies= user_table.get_deny();
user.sort= get_magic_sort("hu", user.host.hostname, user.user.str);
user.hostname_length= safe_strlen(user.host.hostname);
@@ -4146,7 +4328,8 @@ ACL_USER::ACL_USER(THD *thd, const LEX_USER &combo,
static int acl_user_update(THD *thd, ACL_USER *acl_user, uint nauth,
const LEX_USER &combo,
const Account_options &options,
- const privilege_t privileges)
+ const privilege_t privileges,
+ const Deny_spec *deny_spec)
{
ACL_USER_PARAM::AUTH *work_copy= NULL;
if (nauth)
@@ -4240,6 +4423,20 @@ static int acl_user_update(THD *thd, ACL_USER *acl_user, uint nauth,
break;
}
+ /* Update denies only if it was modified.
+ TODO(cvicentiu) this current implementation is problematic.
+ We are re-creating the User's DENY specification every time it is
+ changed, even though only a single entry gets modified.
+ To keep changes to a minimum and because we expect the number of denies
+ to be limited per user, we have this implementation. In the future,
+ we can opt to be smarter about this and only add the extra fields.
+ */
+ if (deny_spec)
+ {
+ delete acl_user->denies;
+ acl_user->denies= deny_spec;
+ }
+
return 0;
}
@@ -5369,6 +5566,7 @@ static int replace_user_table(THD *thd, const User_table &user_table,
TABLE *table= user_table.table();
ACL_USER new_acl_user, *old_acl_user= 0;
privilege_t access;
+ Deny_spec *deny_spec= nullptr;
DBUG_ENTER("replace_user_table");
mysql_mutex_assert_owner(&acl_cache->lock);
@@ -5444,14 +5642,36 @@ static int replace_user_table(THD *thd, const User_table &user_table,
auth->plugin= guess_auth_plugin(thd, auth->auth_str.length);
}
- /* Update table columns with new privileges */
access= user_table.get_access();
- if (priv_spec.revoke)
- access&= ~priv_spec.access;
+
+ if (priv_spec.deny)
+ {
+ deny_spec= user_table.get_deny();
+ if (!deny_spec)
+ deny_spec= new Deny_spec;
+
+ if (!deny_spec)
+ {
+ error= 1;
+ goto end;
+ }
+ if (deny_spec->update_deny(priv_spec) ||
+ user_table.set_deny(deny_spec))
+ {
+ error= 1;
+ goto end;
+ }
+ }
else
- access|= priv_spec.access;
- user_table.set_access(access);
- access= user_table.get_access();
+ {
+ /* Update table columns with new privileges */
+ if (priv_spec.revoke)
+ access&= ~priv_spec.access;
+ else
+ access|= priv_spec.access;
+ user_table.set_access(access);
+ access= user_table.get_access();
+ }
if (handle_as_role)
{
@@ -5479,7 +5699,7 @@ static int replace_user_table(THD *thd, const User_table &user_table,
new_acl_user= old_row_exists ? *old_acl_user :
ACL_USER(thd, *combo, lex->account_options, access);
if (acl_user_update(thd, &new_acl_user, nauth,
- *combo, lex->account_options, access))
+ *combo, lex->account_options, access, deny_spec))
goto end;
if (user_table.set_auth(new_acl_user))
@@ -5487,7 +5707,8 @@ static int replace_user_table(THD *thd, const User_table &user_table,
my_error(ER_COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE, MYF(0),
user_table.name().str, 3, user_table.num_fields(),
static_cast<int>(table->s->mysql_version), MYSQL_VERSION_ID);
- DBUG_RETURN(1);
+ error= 1;
+ goto end;
}
switch (lex->account_options.ssl_type) {
@@ -5611,6 +5832,9 @@ end:
}
}
}
+ else // Cleanup if something went wrong.
+ delete deny_spec;
+
DBUG_RETURN(error);
}
@@ -13050,6 +13274,120 @@ wsrep_error_label:
}
+static bool maria_deny(THD *thd, const User_table &user_table,
+ List<LEX_USER> &list,
+ Priv_spec &priv_spec,
+ bool create_new_users, bool no_auto_create_users)
+{
+ List_iterator<LEX_USER> it(list);
+ bool result;
+ DBUG_ENTER("maria_deny");
+
+ /* go through users in user_list */
+ for (LEX_USER *user= it++; user != NULL; user= it++)
+ {
+ if (check_if_auth_can_be_changed(user, thd) ||
+ replace_user_table(thd, user_table,
+ user,
+ priv_spec,
+ create_new_users,
+ no_auto_create_users))
+ result= true;
+
+ if (user->is_role())
+ {
+ switch (priv_spec.spec_type) {
+ case GLOBAL_PRIV:
+ propagate_role_grants(find_acl_role(user->user.str),
+ PRIVS_TO_MERGE::GLOBAL);
+ break;
+ case DATABASE_PRIV:
+ propagate_role_grants(find_acl_role(user->user.str),
+ PRIVS_TO_MERGE::DB);
+ break;
+ case TABLE_PRIV:
+ case COLUMN_PRIV:
+ propagate_role_grants(find_acl_role(user->user.str),
+ PRIVS_TO_MERGE::TABLE_COLUMN);
+ break;
+ case PROXY_PRIV:
+ /* TODO(cvicentiu) figure out what needs to happen to roles when
+ * propagating deny proxy */
+ DBUG_ASSERT(0);
+ break;
+ case ROUTINE_PRIV:
+ /* TODO(cvicentiu) figure out if we need to call this twice and why. */
+ propagate_role_grants(find_acl_role(user->user.str),
+ PRIVS_TO_MERGE::FUNC);
+ propagate_role_grants(find_acl_role(user->user.str),
+ PRIVS_TO_MERGE::PROC);
+ DBUG_ASSERT(0);
+ break;
+ /* TODO(cvicentiu) what happens to PACKAGE_SPEC and PACKAGE_BODY
+ * and other kinds of grants? */
+ default:
+ /* TODO(cvicentiu) are we going to implement REVOKE DENY ALL? */
+ break;
+ }
+ }
+ }
+ DBUG_RETURN(result);
+}
+
+
+bool Sql_cmd_grant_table::execute_deny(THD *thd)
+{
+ Grant_tables tables;
+ bool result;
+ const bool no_auto_create_users= MY_TEST(thd->variables.sql_mode &
+ MODE_NO_AUTO_CREATE_USER);
+
+ Priv_spec priv_spec{m_gp.object_privilege(), is_revoke(), true};
+
+ privilege_t required_access= m_gp.object_privilege() |
+ m_gp.column_privilege_total() |
+ GRANT_ACL;
+
+ DBUG_ASSERT(thd->lex->first_select_lex()->table_list.first == NULL);
+
+ if (check_access(thd, required_access, m_gp.db().str, NULL, NULL, 1, 0))
+ return true;
+
+ if (grant_stage0(thd))
+ return true;
+
+
+ WSREP_TO_ISOLATION_BEGIN(WSREP_MYSQL_DB, NULL, NULL);
+
+ if (tables.open_and_lock(thd, Table_user, TL_WRITE))
+ return true;
+
+ mysql_rwlock_wrlock(&LOCK_grant);
+ mysql_mutex_lock(&acl_cache->lock);
+ grant_version++;
+
+ result= maria_deny(thd, tables.user_table(),
+ m_resolved_users,
+ priv_spec,
+ m_create_new_users, no_auto_create_users);
+
+ mysql_mutex_unlock(&acl_cache->lock);
+ /* Write to binlog if statement succeeded. */
+ if (!result)
+ result= write_bin_log(thd, TRUE, thd->query(), thd->query_length());
+ mysql_rwlock_unlock(&LOCK_grant);
+
+ if (!result)
+ my_ok(thd);
+
+ return false;
+
+#ifdef WITH_WSREP
+wsrep_error_label:
+ return true;
+#endif // WITH_WSREP
+}
+
bool Sql_cmd_grant_table::execute_grant_global(THD *thd)
{
Grant_tables tables;
@@ -13057,6 +13395,8 @@ bool Sql_cmd_grant_table::execute_grant_global(THD *thd)
const bool no_auto_create_users= MY_TEST(thd->variables.sql_mode &
MODE_NO_AUTO_CREATE_USER);
+ Priv_spec priv_spec{m_gp.object_privilege(), is_revoke(), m_deny};
+
WSREP_TO_ISOLATION_BEGIN(WSREP_MYSQL_DB, NULL, NULL);
if (tables.open_and_lock(thd, Table_user, TL_WRITE))
@@ -13067,7 +13407,7 @@ bool Sql_cmd_grant_table::execute_grant_global(THD *thd)
grant_version++;
result= mysql_grant_global(thd, tables,
m_resolved_users,
- {m_gp.object_privilege(), is_revoke()},
+ priv_spec,
m_create_new_users,
no_auto_create_users);
mysql_mutex_unlock(&acl_cache->lock);
@@ -13126,6 +13466,8 @@ bool Sql_cmd_grant_table::execute_grant_database_or_global(THD *thd)
bool Sql_cmd_grant_table::execute(THD *thd)
{
TABLE_LIST *table= thd->lex->first_select_lex()->table_list.first;
+ if (m_deny)
+ return execute_deny(thd);
if (table)
return execute_grant_table(thd, table);
return execute_grant_database_or_global(thd);
diff --git a/sql/sql_acl.h b/sql/sql_acl.h
index fa84f18e594..c8b43a84f1a 100644
--- a/sql/sql_acl.h
+++ b/sql/sql_acl.h
@@ -391,6 +391,7 @@ class Sql_cmd_grant_table: public Sql_cmd_grant_object
bool execute_grant_global(THD *thd);
bool execute_grant_database(THD *thd);
bool execute_grant_table(THD *thd, TABLE_LIST *table);
+ bool execute_deny(THD *thd);
#endif
public:
Sql_cmd_grant_table(enum_sql_command command, Grant_privilege &grant)