En un script de ansible que debí revisar mejor no agregué el GRANT OPTION de root@localhost y de root@%.
Obvio terminé sin un usuario con capacidad de dar permisos.
Intenté arreglarlo ejecutando mariadb --skip-grant-tables pero al ejecutar
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
seguía diciendo
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
Intenté preguntando a la IA y sólo daba vueltas sin respuestas útiles.
Intenté DuckDuckeando infructuosamente la respuesta.
Sí encontré que hay muchas personas que quieren reestablecer la contraseña de root pero pocas personas desdichadas que sí tienen acceso a root pero sin GRANT.
¡Ya dime cómo arreglarlo!
Éste es el TL;DR pero más abajo está La explicación
1. Detén la base de datos
sudo systemctl stop mysql
2. Inicia el MariaDB en modo seguro
sudo mysqld_safe --skip-networking --safe-mode --skip-grant-tables &
3. En otra terminal, conéctate a la base
mysql -u root
Si te dice que aún así no tienes acceso, intenta ponerle el host
mysql -u root -h localhost
4. En el prompt de MariaDB, revisa si el usuario root ya tiene el GRANT OPTION
SELECT
Host,
User,
IF(JSON_VALUE(Priv, '$.access') & 1024, 'Y', 'N') AS Grant_priv
FROM mysql.global_priv
WHERE User = 'root';
En mi caso, me arrojó esta respuesta:
+-----------+------+------------+
| Host | User | Grant_priv |
+-----------+------+------------+
| localhost | root | N |
| % | root | N |
+-----------+------+------------+
La 'N' es que NO tiene el GRANT OPTION
También puedes ver los permisos que tienes con este comando
SHOW GRANTS;
Y te responde algo así:
+-----------------------------------------------------------------------------------------------+
| Grants for root@localhost |
+-----------------------------------------------------------------------------------------------+
| GRANT ALL PRIVILEGES ON *.* TO `root`@`localhost` IDENTIFIED BY PASSWORD '*VARIOS_CARACTERES' |
+-----------------------------------------------------------------------------------------------+
Donde lo que hay que notar es que al final NO dice WITH GRANT OPTION
5. Agrega el GRANT OPTION
¡Éste es el paso importante!
UPDATE mysql.global_priv
SET
Priv = JSON_REPLACE(
Priv,
'$.access',
JSON_VALUE(Priv, '$.access') | 1024
)
WHERE
User = 'root'
;
FLUSH PRIVILEGES;
6. Revisa que ya tengas el GRANT OPTION
SHOW GRANTS;
Ya hora puedes ver que dice
+-----------------------------------------------------------------------------------------------------------------+
| Grants for root@localhost |
+-----------------------------------------------------------------------------------------------------------------+
| GRANT ALL PRIVILEGES ON *.* TO `root`@`localhost` IDENTIFIED BY PASSWORD '*VARIOS_CARACTERES' WITH GRANT OPTION |
+-----------------------------------------------------------------------------------------------------------------+
Y ya trae el WITH GRANT OPTION (hasta la derecha)
¡Listo!
Ya tienes permisos, sólo falta regresar el servicio a su funcionamiento habitual.
7. Termina el proceso mariadbd que está ejecutando en modo seguro
sudo pkill -f mariadbd
8. Inicia el servicio regular
sudo systemctl start mariadb
La explicación
En un cambio de 2018-12-20, al llegar MariaDB 10.4.1, en las Notas de lanzamiento retiran la tabla mysql.user y es reemplazada por una vista.
Ahora los permisos están guardados de manera más granular en la tabla mysql.global_priv y ésta, a su vez, guarda los permisos de acceso en la columna Priv que es un JSON y dentro de ese JSON, tiene la propiedad access que el dato que buscamos.
Los permisos se guardan como un entero y cada uno de los permisos es un bit encendido de ese entero. Es decir, que si tienes en mysql.global_priv.Priv.access, el siguiente valor
549755812863 -> 0b111111111111111111111111111101111111111
Se traduce a binario y para MariaDB cada uno de esos bits es un permiso distinto.
Al revisar el DDL de la vista mysql.user podemos ver qué es cada uno de esos permisos; y en particular vemos que el permiso Grant_priv que es el que nos falta es justamente el cero que está en los permisos actuales.
-- CREATE ALGORITHM = UNDEFINED DEFINER = `mariadb.sys`@`localhost` SQL SECURITY DEFINER VIEW `user` AS
SELECT
`global_priv`.`Host` AS `Host`,
`global_priv`.`User` AS `User`,
if(json_value(`global_priv`.`Priv`, '$.plugin') in ('mysql_native_password', 'mysql_old_password'), ifnull(json_value(`global_priv`.`Priv`, '$.authentication_string'),''),'') AS `Password`,
if(json_value(`global_priv`.`Priv`, '$.access') & 1 ,'Y','N') AS `Select_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 2 ,'Y','N') AS `Insert_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 4 ,'Y','N') AS `Update_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 8 ,'Y','N') AS `Delete_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 16 ,'Y','N') AS `Create_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 32 ,'Y','N') AS `Drop_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 64 ,'Y','N') AS `Reload_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 128 ,'Y','N') AS `Shutdown_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 256 ,'Y','N') AS `Process_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 512 ,'Y','N') AS `File_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 1024 ,'Y','N') AS `Grant_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 2048 ,'Y','N') AS `References_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 4096 ,'Y','N') AS `Index_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 8192 ,'Y','N') AS `Alter_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 16384 ,'Y','N') AS `Show_db_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 32768 ,'Y','N') AS `Super_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 65536 ,'Y','N') AS `Create_tmp_table_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 131072 ,'Y','N') AS `Lock_tables_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 262144 ,'Y','N') AS `Execute_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 524288 ,'Y','N') AS `Repl_slave_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 1048576 ,'Y','N') AS `Repl_client_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 2097152 ,'Y','N') AS `Create_view_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 4194304 ,'Y','N') AS `Show_view_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 8388608 ,'Y','N') AS `Create_routine_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 16777216 ,'Y','N') AS `Alter_routine_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 33554432 ,'Y','N') AS `Create_user_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 67108864 ,'Y','N') AS `Event_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 134217728 ,'Y','N') AS `Trigger_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 268435456 ,'Y','N') AS `Create_tablespace_priv`,
if(json_value(`global_priv`.`Priv`, '$.access') & 536870912 ,'Y','N') AS `Delete_history_priv`,
elt(ifnull(json_value(`global_priv`.`Priv`, '$.ssl_type'), 0) + 1, '', 'ANY', 'X509', 'SPECIFIED') AS `ssl_type`,
ifnull(json_value(`global_priv`.`Priv`, '$.ssl_cipher'), '') AS `ssl_cipher`,
ifnull(json_value(`global_priv`.`Priv`, '$.x509_issuer'), '') AS `x509_issuer`,
ifnull(json_value(`global_priv`.`Priv`, '$.x509_subject'), '') AS `x509_subject`,
cast(ifnull(json_value(`global_priv`.`Priv`, '$.max_questions'), 0) as unsigned) AS `max_questions`,
cast(ifnull(json_value(`global_priv`.`Priv`, '$.max_updates'), 0) as unsigned) AS `max_updates`,
cast(ifnull(json_value(`global_priv`.`Priv`, '$.max_connections'), 0) as unsigned) AS `max_connections`,
cast(ifnull(json_value(`global_priv`.`Priv`, '$.max_user_connections'), 0) as signed) AS `max_user_connections`,
ifnull(json_value(`global_priv`.`Priv`, '$.plugin'), '') AS `plugin`,
ifnull(json_value(`global_priv`.`Priv`, '$.authentication_string'), '') AS `authentication_string`,
if(ifnull(json_value(`global_priv`.`Priv`, '$.password_last_changed'), 1) = 0,'Y','N') AS `password_expired`,
elt(ifnull(json_value(`global_priv`.`Priv`, '$.is_role'), 0) + 1,'N', 'Y') AS `is_role`,
ifnull(json_value(`global_priv`.`Priv`, '$.default_role'), '') AS `default_role`,
cast(ifnull(json_value(`global_priv`.`Priv`, '$.max_statement_time'), 0.0) as decimal(12, 6)) AS `max_statement_time`
FROM `global_priv`;
Sólo falta hacer que el entero del permiso cambie a uno con el Grant_priv activado. i. e. queremos que el permiso que está así
549755812863 -> 0b111111111111111111111111111101111111111
||||||||||1 Select_priv
|||||||||2 Insert_priv
||||||||4 Update_priv
|||||||8 Delete_priv
||||||16 Create_priv
|||||32 Drop_priv
||||64 Reload_priv
|||128 Shutdown_priv
||256 Process_priv
|512 File_priv
1024 Grant_priv
Cambie a
549755813887 -> 0b111111111111111111111111111111111111111
No deberías poner ese número a mano en el valor de mysql.global_priv.Priv.access pues este modelo de permisos está pensado para poder agregar más permisos granulares en el futuro, así que una mejor manera es sólo agregar el permiso que nos falta.
MariaDB tiene funciones para manejar los campos en JSON.
UPDATE mysql.global_priv
SET
Priv = JSON_REPLACE(
Priv,
'$.access',
JSON_VALUE(Priv, '$.access') | 1024
)
WHERE
User = 'root'
;
FLUSH PRIVILEGES;
En particular, la función JSON_REPLACE( json_doc, path, val) nos da lo que necesitamos, que es:
- una fuente (json_doc),
- un parámetro para buscar el elemento del JSON que deseamos cambiar (path) y
- el valor por el que queremos cambiarlo (val).
La fuente json_doc es la misma columna Priv.
$.access es la ruta dentro del documento JSON de Priv que contiene el entero donde están codificados los permisos. Es un documento sencillo que trae un objeto llave valor y access es una de las llaves
Sólo nos falta obtener el valor y encender el bit de Grant_priv, que como vimos en el DDL de la vista mysql.user es 1024.
Para obtener el valor usamos JSON_VALUE(json_doc, path)
Nuevamente, la fuente json_doc es la columna Priv y la ruta path es $.access.
Y al hacer un OR lógico con 1024, encendemos el bit necesario. i. e. ésta línea es la que calcula el entero con el permiso nuevo:
JSON_VALUE(Priv, '$.access') | 1024
Y finalmente
FLUSH PRIVILEGES;
que recarga los permisos de la base mysql y habilita los privilegios si es que usamos --skip-grant-table, que sí usamos para iniciar MariaDB el modo seguro.
Síganme para más consejos de como arruinar servidores.