Skip to main content

🛡️ Lesson 19: User Management & Permissions

So far you've been working as the root user — the all-powerful superuser who can do anything: read, write, delete, create users, drop databases, the works. In production, that's a disaster waiting to happen. One mistyped DROP DATABASE and everything is gone. Proper user management gives each person (or application) only the permissions they need — no more, no less. This is the principle of least privilege, and it's the foundation of database security.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Create and manage MySQL user accounts
  • Grant specific privileges with GRANT
  • Revoke privileges with REVOKE
  • View a user's privileges with SHOW GRANTS
  • Understand privilege levels (global, database, table, column)
  • Use roles to manage groups of privileges (MySQL 8+)
  • Apply the principle of least privilege

Estimated Time: 40 minutes

Prerequisites: Transactions (Lesson 18)

📑 In This Lesson

Why User Management Matters

Consider these scenarios:

  • A junior developer accidentally runs DELETE FROM customers without a WHERE clause — every customer record gone
  • A reporting dashboard only needs to read data, but its database account can also write and delete
  • An intern with root access drops the production database
  • A web application's database credentials are compromised — the attacker has full admin access

All of these are preventable with proper user management. Each user or application gets only the permissions required for their job.

graph TD R["root (superuser)"] R --> A["app_user
SELECT, INSERT, UPDATE
on bookstore only"] R --> B["report_user
SELECT only
on bookstore only"] R --> C["admin_user
All privileges
on bookstore only"] R --> D["backup_user
SELECT, LOCK TABLES
on all databases"]

Creating Users

Basic Syntax


CREATE USER 'username'@'host' IDENTIFIED BY 'password';
                

A MySQL user is identified by two parts: the username and the host they connect from. The same username from different hosts is treated as different accounts.

Common Host Patterns

Host Meaning Use Case
'localhost' Only from the same machine Local applications, CLI access
'%' From any host (wildcard) Remote applications (use with caution)
'192.168.1.%' From any IP in that subnet Internal network access
'10.0.0.5' From one specific IP Locked to one server

Examples


-- User that can only connect locally
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'SecurePass123!';

-- User that can connect from any host
CREATE USER 'remote_user'@'%' IDENTIFIED BY 'RemotePass456!';

-- User locked to a specific IP
CREATE USER 'api_service'@'10.0.0.50' IDENTIFIED BY 'ApiKey789!';
                

⚠️ Strong Passwords

Always use strong passwords with a mix of uppercase, lowercase, numbers, and special characters. Never use 'password', '123456', or the username as the password. In production, use a password manager or secrets management system.

See All Users


-- List all user accounts
SELECT User, Host FROM mysql.user;
                

Example Output:


+------------------+-----------+
| User             | Host      |
+------------------+-----------+
| root             | localhost |
| mysql.sys        | localhost |
| app_user         | localhost |
| remote_user      | %         |
+------------------+-----------+
                    

Granting Privileges

Creating a user gives them zero permissions — they can connect but can't see or do anything. You must explicitly grant what they need.

Basic Syntax


GRANT privilege_list ON database.table TO 'user'@'host';
                

Common Privileges

Privilege Allows
SELECT Read data from tables
INSERT Add new rows
UPDATE Modify existing rows
DELETE Remove rows
CREATE Create tables and databases
DROP Delete tables and databases
ALTER Modify table structure
INDEX Create and drop indexes
EXECUTE Run stored procedures and functions
CREATE VIEW Create views
ALL PRIVILEGES Everything (on the specified scope)

Examples


-- Read-only access to the bookstore database
GRANT SELECT ON bookstore.* TO 'report_user'@'localhost';

-- Full CRUD access to bookstore
GRANT SELECT, INSERT, UPDATE, DELETE ON bookstore.* TO 'app_user'@'localhost';

-- All privileges on bookstore (but not on other databases)
GRANT ALL PRIVILEGES ON bookstore.* TO 'admin_user'@'localhost';

-- Read access to a specific table only
GRANT SELECT ON bookstore.books TO 'catalog_viewer'@'localhost';

-- Apply changes (required in older MySQL versions)
FLUSH PRIVILEGES;
                

💡 FLUSH PRIVILEGES

When you use GRANT and REVOKE, MySQL updates privileges immediately. FLUSH PRIVILEGES is only necessary if you modified the privilege tables directly (e.g., with INSERT into mysql.user). In practice, many people include it out of habit — it's harmless but usually unnecessary with GRANT/REVOKE.

GRANT with Grant Option

If you want a user to be able to grant their own privileges to others:


-- admin_user can grant privileges to other users
GRANT ALL PRIVILEGES ON bookstore.*
TO 'admin_user'@'localhost'
WITH GRANT OPTION;
                

⚠️ Use WITH GRANT OPTION Carefully

A user with GRANT OPTION can give their privileges to anyone. This can cascade out of control. Only give it to trusted administrators.

Privilege Levels

Privileges can be scoped at different levels — from "everything on the server" to "one column in one table."

Level Syntax Scope
Global ON *.* All databases, all tables on the server
Database ON database_name.* All tables in one database
Table ON database_name.table_name One specific table
Column GRANT SELECT (col1, col2) ON db.table Specific columns in a table
Routine GRANT EXECUTE ON PROCEDURE db.proc A specific stored procedure or function

Examples at Each Level


-- Global: SELECT on every database (e.g., monitoring tool)
GRANT SELECT ON *.* TO 'monitor'@'localhost';

-- Database: Full access to bookstore only
GRANT ALL PRIVILEGES ON bookstore.* TO 'admin_user'@'localhost';

-- Table: Read only the books table
GRANT SELECT ON bookstore.books TO 'catalog_viewer'@'localhost';

-- Column: See customer names but not emails or phones
GRANT SELECT (id, first_name, last_name) ON bookstore.customers
TO 'limited_viewer'@'localhost';

-- Routine: Run a specific stored procedure
GRANT EXECUTE ON PROCEDURE bookstore.place_order TO 'app_user'@'localhost';
                
graph TD G["Global (*.*)
All databases"] --> D["Database (db.*)
All tables in one DB"] D --> T["Table (db.table)
One table"] T --> C["Column (col1, col2)
Specific columns"] D --> R["Routine (PROCEDURE/FUNCTION)
One stored routine"]

💡 Narrower Is Safer

Always grant at the narrowest level possible. If a user only needs to read the books table, grant SELECT on bookstore.books — not on bookstore.* and definitely not on *.*.

Viewing Privileges


-- See your own privileges
SHOW GRANTS;

-- See another user's privileges
SHOW GRANTS FOR 'app_user'@'localhost';
                

Example Output:


+-----------------------------------------------------------------------+
| Grants for app_user@localhost                                         |
+-----------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `app_user`@`localhost`                          |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `bookstore`.* TO `app_user`@… |
+-----------------------------------------------------------------------+
                    

USAGE means "no privileges" at the global level — it's the default grant that says "this account exists and can connect, but has no global permissions." The real permissions come from the second line.

Checking Current User


-- Who am I?
SELECT CURRENT_USER();

-- What's my connection info?
SELECT USER();
                

Revoking Privileges

Remove privileges with REVOKE — the syntax mirrors GRANT:


-- Remove DELETE privilege (keep SELECT, INSERT, UPDATE)
REVOKE DELETE ON bookstore.* FROM 'app_user'@'localhost';

-- Remove all privileges on bookstore
REVOKE ALL PRIVILEGES ON bookstore.* FROM 'app_user'@'localhost';

-- Remove the grant option
REVOKE GRANT OPTION ON bookstore.* FROM 'admin_user'@'localhost';
                

Verify After Revoking


-- Always verify the change took effect
SHOW GRANTS FOR 'app_user'@'localhost';
                

💡 REVOKE Doesn't Delete the User

Revoking all privileges leaves the user account intact — they can still connect, they just can't do anything. To fully remove a user, use DROP USER (next section).

Modifying & Deleting Users

Change a Password


-- Change another user's password (as root)
ALTER USER 'app_user'@'localhost' IDENTIFIED BY 'NewSecurePass789!';

-- Change your own password
ALTER USER CURRENT_USER() IDENTIFIED BY 'MyNewPassword!';

-- Older syntax (still works):
SET PASSWORD FOR 'app_user'@'localhost' = 'NewSecurePass789!';
                

Rename a User


RENAME USER 'old_name'@'localhost' TO 'new_name'@'localhost';
                

Lock/Unlock an Account


-- Temporarily disable an account (MySQL 5.7.6+)
ALTER USER 'app_user'@'localhost' ACCOUNT LOCK;

-- Re-enable it
ALTER USER 'app_user'@'localhost' ACCOUNT UNLOCK;
                

Delete a User


-- Remove a user entirely (account + all privileges)
DROP USER 'app_user'@'localhost';

-- Safe version (no error if user doesn't exist)
DROP USER IF EXISTS 'app_user'@'localhost';
                

⚠️ DROP USER Is Permanent

Dropping a user removes the account and all its privilege grants. If the user was the owner of stored procedures or views, those objects may become inaccessible. Verify dependencies before dropping accounts.

Roles (MySQL 8+)

A role is a named collection of privileges. Instead of granting the same set of privileges to 20 users individually, you define a role once and assign it to users. Change the role's privileges and all assigned users are updated automatically.

Create and Use a Role


-- Step 1: Create the role
CREATE ROLE 'bookstore_reader';

-- Step 2: Grant privileges to the role
GRANT SELECT ON bookstore.* TO 'bookstore_reader';

-- Step 3: Assign the role to users
GRANT 'bookstore_reader' TO 'alice'@'localhost';
GRANT 'bookstore_reader' TO 'bob'@'localhost';
GRANT 'bookstore_reader' TO 'carol'@'localhost';

-- Step 4: Activate the role for a user's session
SET DEFAULT ROLE 'bookstore_reader' TO 'alice'@'localhost';
-- Or for all roles:
SET DEFAULT ROLE ALL TO 'alice'@'localhost';
                

Multiple Roles


-- Define roles for different access levels
CREATE ROLE 'bookstore_reader', 'bookstore_writer', 'bookstore_admin';

GRANT SELECT ON bookstore.* TO 'bookstore_reader';
GRANT SELECT, INSERT, UPDATE, DELETE ON bookstore.* TO 'bookstore_writer';
GRANT ALL PRIVILEGES ON bookstore.* TO 'bookstore_admin';

-- A user can have multiple roles
GRANT 'bookstore_reader', 'bookstore_writer' TO 'app_user'@'localhost';
SET DEFAULT ROLE ALL TO 'app_user'@'localhost';
                

Drop a Role


DROP ROLE 'bookstore_reader';
                

✅ When to Use Roles

Roles shine when you have many users with the same access needs. Instead of running 20 GRANT statements when a new table is added, update the role once. Roles are especially valuable in teams where people join, leave, or change positions regularly.

graph TD subgraph roles["Roles"] RR["bookstore_reader
SELECT"] RW["bookstore_writer
SELECT, INSERT, UPDATE, DELETE"] RA["bookstore_admin
ALL PRIVILEGES"] end subgraph users["Users"] U1["alice"] U2["bob"] U3["carol"] U4["dave"] end RR --> U1 RR --> U2 RW --> U3 RA --> U4

The Principle of Least Privilege

The principle of least privilege (PoLP) states: every user and application should have only the minimum permissions needed to do their job — nothing more.

Why It Matters

  • Limits damage from mistakes: A user with only SELECT can't accidentally delete data
  • Limits damage from breaches: If credentials are compromised, the attacker can only do what the account allows
  • Creates accountability: Each account's purpose is clear and auditable
  • Meets compliance requirements: Many security standards (SOC 2, HIPAA, PCI DSS) require least privilege

Applying It in Practice

User/Application Needs Grant
Reporting dashboard Read data only SELECT ON bookstore.*
Web application Read, create, update records SELECT, INSERT, UPDATE ON bookstore.*
Customer support tool View customer info, not financials SELECT ON bookstore.customers
Backup script Read all data + lock tables SELECT, LOCK TABLES ON bookstore.*
Database admin Full management access ALL PRIVILEGES ON bookstore.*

⚠️ Never Use Root for Applications

Your web application should never connect as root. Create a dedicated user with only the privileges the app needs. If the app is compromised, the attacker can only do what app_user can do — not drop every database on the server.

Common User Setups

Here's a complete setup for a typical bookstore application:


-- 1. Application user: reads and writes data, runs procedures
CREATE USER 'bookstore_app'@'localhost' IDENTIFIED BY 'AppPass!2026';
GRANT SELECT, INSERT, UPDATE, DELETE ON bookstore.* TO 'bookstore_app'@'localhost';
GRANT EXECUTE ON bookstore.* TO 'bookstore_app'@'localhost';

-- 2. Reporting user: read-only access
CREATE USER 'bookstore_reports'@'localhost' IDENTIFIED BY 'ReportPass!2026';
GRANT SELECT ON bookstore.* TO 'bookstore_reports'@'localhost';

-- 3. Admin user: full database management (not server-wide)
CREATE USER 'bookstore_admin'@'localhost' IDENTIFIED BY 'AdminPass!2026';
GRANT ALL PRIVILEGES ON bookstore.* TO 'bookstore_admin'@'localhost';

-- 4. Backup user: read-only + lock for consistent dumps
CREATE USER 'bookstore_backup'@'localhost' IDENTIFIED BY 'BackupPass!2026';
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON bookstore.*
TO 'bookstore_backup'@'localhost';

-- 5. Support team: limited view of customers only
CREATE USER 'bookstore_support'@'localhost' IDENTIFIED BY 'SupportPass!2026';
GRANT SELECT (id, first_name, last_name, email) ON bookstore.customers
TO 'bookstore_support'@'localhost';

-- Verify all setups:
SHOW GRANTS FOR 'bookstore_app'@'localhost';
SHOW GRANTS FOR 'bookstore_reports'@'localhost';
SHOW GRANTS FOR 'bookstore_admin'@'localhost';
SHOW GRANTS FOR 'bookstore_backup'@'localhost';
SHOW GRANTS FOR 'bookstore_support'@'localhost';
                

✅ Testing Permissions

After creating a user, log in as that user and verify they can do what they need — and can't do what they shouldn't. Try a DELETE as a read-only user to confirm it's denied:


-- In a terminal:
-- mysql -u bookstore_reports -p bookstore
-- Then try:
DELETE FROM books WHERE id = 1;
-- ERROR 1142: DELETE command denied to user 'bookstore_reports'@'localhost'
                    

Exercises

🏋️ Exercise 1: Create Users & Grant Privileges

Write SQL to:

  1. Create a user called inventory_mgr that can connect from localhost with password InvPass!2026
  2. Grant SELECT, INSERT, and UPDATE on the bookstore.books table only
  3. Create a user called public_api that can connect from any host (%) with password ApiPass!2026
  4. Grant SELECT only on bookstore.books and bookstore.authors
✅ Solution

-- 1
CREATE USER 'inventory_mgr'@'localhost' IDENTIFIED BY 'InvPass!2026';

-- 2
GRANT SELECT, INSERT, UPDATE ON bookstore.books
TO 'inventory_mgr'@'localhost';

-- 3
CREATE USER 'public_api'@'%' IDENTIFIED BY 'ApiPass!2026';

-- 4
GRANT SELECT ON bookstore.books TO 'public_api'@'%';
GRANT SELECT ON bookstore.authors TO 'public_api'@'%';
                        

🏋️ Exercise 2: View & Revoke Privileges

Write SQL to:

  1. Show the grants for inventory_mgr@localhost
  2. Revoke the UPDATE privilege from inventory_mgr@localhost
  3. Verify the privilege was removed
  4. Revoke all privileges from public_api@% and drop the user
✅ Solution

-- 1
SHOW GRANTS FOR 'inventory_mgr'@'localhost';

-- 2
REVOKE UPDATE ON bookstore.books FROM 'inventory_mgr'@'localhost';

-- 3
SHOW GRANTS FOR 'inventory_mgr'@'localhost';
-- Should show SELECT, INSERT only

-- 4
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'public_api'@'%';
DROP USER 'public_api'@'%';
                        

🏋️ Exercise 3: Design a Permission Scheme

Design users and permissions for a school database with tables: students, teachers, courses, enrollments, grades.

Create users for:

  1. A teacher who can view students and courses, and add/update grades
  2. A registrar who can manage students, courses, and enrollments but not grades
  3. A student portal (application) that can only read course info and the student's own grades
✅ Solution

-- Teacher: view students/courses, manage grades
CREATE USER 'teacher'@'localhost' IDENTIFIED BY 'TeachPass!2026';
GRANT SELECT ON school.students TO 'teacher'@'localhost';
GRANT SELECT ON school.courses TO 'teacher'@'localhost';
GRANT SELECT ON school.enrollments TO 'teacher'@'localhost';
GRANT SELECT, INSERT, UPDATE ON school.grades TO 'teacher'@'localhost';

-- Registrar: manage students/courses/enrollments, no grades
CREATE USER 'registrar'@'localhost' IDENTIFIED BY 'RegPass!2026';
GRANT SELECT, INSERT, UPDATE, DELETE ON school.students TO 'registrar'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON school.courses TO 'registrar'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON school.enrollments TO 'registrar'@'localhost';

-- Student portal: read-only on courses and grades
CREATE USER 'student_portal'@'localhost' IDENTIFIED BY 'PortalPass!2026';
GRANT SELECT ON school.courses TO 'student_portal'@'localhost';
GRANT SELECT ON school.grades TO 'student_portal'@'localhost';
-- Note: Filtering by student ID would be done in the application layer
                        

🎯 Quick Quiz

Question 1: What does a newly created user (with no GRANT statements) have access to?

Question 2: What does the host part of 'app_user'@'%' mean?

Question 3: What is the principle of least privilege?

Question 4: What advantage do roles provide over individual GRANT statements?

Summary

🎉 Key Takeaways

  • A MySQL user is 'username'@'host' — the host controls where they can connect from
  • CREATE USER creates accounts; DROP USER removes them
  • GRANT adds privileges; REVOKE removes them
  • New users have zero privileges — you must explicitly grant everything
  • Privileges can be scoped at global (*.*), database, table, column, or routine level
  • SHOW GRANTS FOR 'user'@'host' shows what a user can do
  • Roles (MySQL 8+) group privileges for easier management of multiple users
  • The principle of least privilege: grant only what's needed, nothing more
  • Never use root for application connections
  • Always test permissions by logging in as the user and trying allowed/denied operations

Quick Reference

Command Purpose
CREATE USER 'u'@'h' IDENTIFIED BY 'p' Create a user account
DROP USER IF EXISTS 'u'@'h' Remove a user account
GRANT privs ON db.tbl TO 'u'@'h' Grant privileges
REVOKE privs ON db.tbl FROM 'u'@'h' Revoke privileges
SHOW GRANTS FOR 'u'@'h' View a user's privileges
ALTER USER 'u'@'h' IDENTIFIED BY 'new' Change password
ALTER USER 'u'@'h' ACCOUNT LOCK Disable an account
CREATE ROLE 'role_name' Create a role (MySQL 8+)
GRANT 'role' TO 'u'@'h' Assign a role to a user
SELECT User, Host FROM mysql.user List all user accounts

📚 Additional Resources

🚀 What's Next?

That completes Module 6: Advanced Features! You've built reusable logic (procedures/functions), protected data integrity (transactions), and locked down access (users/permissions). In the final module, Lesson 20: Importing & Exporting Data covers the essential real-world skill of moving data in and out of MySQL — backups with mysqldump, CSV imports, and more.

🎉 Module 6 Complete!

You've mastered the advanced side of MySQL — stored routines for reusable logic, transactions for bulletproof data integrity, and user management for security. Your databases are now powerful, safe, and secure.