🛡️ 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 customerswithout 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.
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';
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.
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:
- Create a user called
inventory_mgrthat can connect from localhost with passwordInvPass!2026 - Grant SELECT, INSERT, and UPDATE on the
bookstore.bookstable only - Create a user called
public_apithat can connect from any host (%) with passwordApiPass!2026 - Grant SELECT only on
bookstore.booksandbookstore.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:
- Show the grants for
inventory_mgr@localhost - Revoke the UPDATE privilege from
inventory_mgr@localhost - Verify the privilege was removed
- 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:
- A teacher who can view students and courses, and add/update grades
- A registrar who can manage students, courses, and enrollments but not grades
- 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 USERcreates accounts;DROP USERremoves themGRANTadds privileges;REVOKEremoves 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
- MySQL Docs — CREATE USER
- MySQL Docs — GRANT Statement
- MySQL Docs — REVOKE Statement
- MySQL Docs — Using Roles
- MySQL Docs — Privileges Provided by MySQL
🚀 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.