© Jérôme Radix, December 22, 2006
NOTE: This article may be circulated freely as long as the copyright notice is included.
mysql> create database applidata; Query OK, 1 row affected (0.00 sec)This is where authorized users of your database will see the application data.
mysql> create user user1 identified by 'user1'; Query OK, 0 rows affected (0.00 sec) mysql> grant select, insert, update, delete on applidata.* to user1@'%'; Query OK, 0 rows affected (0.00 sec) mysql> flush privileges; Query OK, 0 rows affected (0.00 sec)You're kind of forced to create a Users Table which store all your different application users. Let's put all this security stuff into a particular database named fgac :
mysql> create database fgac; Query OK, 1 row affected (0.00 sec) mysql> use fgac Database changed mysql> create table my_users (username varchar(81) primary key, password -> varchar(81)) engine = memory; Query OK, 0 rows affected (0.02 sec) mysql> mysql> insert into my_users (username, password) values ('userA', 'userA'); Query OK, 1 row affected (0.00 sec) mysql> insert into my_users (username, password) values ('userB', 'userB'); Query OK, 1 row affected (0.00 sec) mysql> insert into my_users (username, password) values ('userC', 'userC'); Query OK, 1 row affected (0.00 sec) mysql> insert into my_users (username, password) values ('userD', 'userD'); Query OK, 1 row affected (0.00 sec) mysql> mysql> alter table my_users add index(username, password); Query OK, 4 rows affected (0.00 sec) Records: 4 Duplicates: 0 Warnings: 0You'll see later why password should be in an index too. You can notice I've put this table in memory in order to quicken all this.
mysql> create table credit_cards(num int primary key, expire_date date, owner varchar(81)); Query OK, 0 rows affected (0.00 sec) mysql> insert into credit_cards(num, expire_date, owner) values (1234, NOW(), 'userA'); Query OK, 1 row affected (0.00 sec) mysql> insert into credit_cards(num, expire_date, owner) values (5678, NOW(), 'userB'); Query OK, 1 row affected (0.00 sec) mysql> select * from credit_cards; +------+-------------+-------+ | num | expire_date | owner | +------+-------------+-------+ | 1234 | 2006-12-22 | userA | | 5678 | 2006-12-22 | userB | +------+-------------+-------+ 2 rows in set (0.00 sec)So now, I want to leave a restricted access to this table to my users. But they use all the same MySQL account ! Let's pretend all your application data is accessible from the applidata database :
mysql> use applidata Database changedFirst, you have to invent a mean for application users to identify themselves. Once identified, we could construct a view of the credit_cards table which would show only data available for the identified user.
delimiter // drop procedure authenticate; // create procedure authenticate(name varchar(81), password varchar(81)) begin declare the_password varchar(81); select password into the_password from my_users where username = name; if password = the_password then create temporary table if not exists fgac.session (username varchar(81)) engine = MEMORY; delete from fgac.session where username = name; insert into fgac.session (username) values (name); end if; end; //The problem is that you cannot use this session table to form the view because MySQL cannot create view from temporary tables. Another mean for the user to identify himself is to let him set two variables : one containing his username, the other containing his password. The problem, now, is that MySQL cannot create views using variables !
delimiter // drop function getusername; // create function getusername() returns varchar(81) begin return @name; end; // drop function getpassword; // create function getpassword() returns varchar(81) begin return @pwd; end; // create or replace view applidata.credit_cards as select num, expire_date from fgac.credit_cards, fgac.my_users where username = fgac.getusername() and password = fgac.getpassword() and owner = username;It's seams great, but unfortunately, you cannot delete from this view as you use a join of two tables (MySQL strangly allows updates on data of this view...) So better way to write your view is :
create or replace view applidata.credit_cards as select num, expire_date from fgac.credit_cards where 1 = (select 1 from my_users where username = getusername() and password = getpassword()) and owner = fgac.getusername();No more join ! just a nested select in the where clause. Now, connect to your database through the user1 account (the one and only account your administrator let's you employ for nominal operation of your application) and try to select some credit cards :
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | applidata | +--------------------+ 2 rows in set (0.00 sec) mysql> use applidata Database changed mysql> show tables; +---------------------+ | Tables_in_applidata | +---------------------+ | credit_cards | +---------------------+ 1 row in set (0.00 sec) mysql> select * from credit_cards; Empty set (0.00 sec) mysql> set @name = 'userA'; Query OK, 0 rows affected (0.00 sec) mysql> set @pwd = 'userA'; Query OK, 0 rows affected (0.00 sec) mysql> select * from credit_cards; +------+-------------+ | num | expire_date | +------+-------------+ | 1234 | 2006-12-22 | +------+-------------+ 1 rows in set (0.00 sec)Great, you only see your own credit card without knowing that this "table" stores all credit cards of all users ! No more security code in you where clauses ! You can even update credit card information directly from this view.
delimiter // drop trigger credit_cards_before_insert_trg; // create trigger credit_cards_before_insert_trg before insert on credit_cards for each row set new.owner = @name; // drop trigger credit_cards_after_insert_trg; // -- simulate rollback when user is not correctly identified ! create trigger credit_cards_after_insert_trg after insert on credit_cards for each row begin declare userok boolean default false; select true into userok from fgac.my_users where username = @name and password = @pwd; if not userok then delete from fgac.credit_cards where num = new.num; end if; end; // drop trigger credit_cards_after_delete_trg; // -- simulate rollback when user is not correctly identified ! -- Performance is poor here because it recreates all delete rows. -- Clearly : it's horrible. create trigger credit_cards_after_delete_trg after delete on credit_cards for each row begin declare userok boolean default false; select true into userok from fgac.my_users where username = @name and password = @pwd; if not userok then insert into fgac.credit_cards(num, expire_date, owner) values (old.num, old.expire_date, old.owner); end if; end; //Well, performance matters, and this solution should be investigated in front of numerous user connections... but, functionnality is here ! you can now insert into applidata.credit_cards without problem ! Using user1 connection:
mysql> select * from credit_cards; +------+-------------+ | num | expire_date | +------+-------------+ | 1234 | 2006-12-22 | +------+-------------+ 1 row in set (0.00 sec) mysql> insert into credit_cards (num, expire_date) values (9012, now()); Query OK, 1 row affected (0.00 sec) mysql> select * from credit_cards; +------+-------------+ | num | expire_date | +------+-------------+ | 1234 | 2006-12-22 | | 9012 | 2006-12-22 | +------+-------------+ 2 rows in set (0.00 sec)If you use your root connection, you can examine your fgac.credit_cards table :
mysql> select * from fgac.credit_cards; -> // +------+-------------+-------+ | num | expire_date | owner | +------+-------------+-------+ | 1234 | 2006-12-22 | userA | | 5678 | 2006-12-27 | userB | | 9012 | 2006-12-22 | userA | +------+-------------+-------+ 3 rows in set (0.00 sec)Let's try to update some credit card info using the user1 connection :
mysql> update credit_cards set expire_date = adddate(now(), interval 1 day); Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0 mysql> select * from credit_cards; +------+-------------+ | num | expire_date | +------+-------------+ | 1234 | 2006-12-23 | | 9012 | 2006-12-23 | +------+-------------+ 2 rows in set (0.00 sec)And using the root account :
mysql> select * from fgac.credit_cards; +------+-------------+-------+ | num | expire_date | owner | +------+-------------+-------+ | 1234 | 2006-12-23 | userA | | 5678 | 2006-12-22 | userB | | 9012 | 2006-12-23 | userA | +------+-------------+-------+ 3 rows in set (0.02 sec)And now, let's delete all the userA credit cards using the user1 connection:
mysql> delete from credit_cards; Query OK, 2 rows affected (0.00 sec) mysql> select * from credit_cards; Empty set (0.01 sec)And on the root side, you have :
mysql> select * from fgac.credit_cards; +------+-------------+-------+ | num | expire_date | owner | +------+-------------+-------+ | 5678 | 2006-12-22 | userB | +------+-------------+-------+ 1 row in set (0.00 sec)What if a user (userA) try to insert a duplicate key through the use of the credit_cards view :
mysql> select * from credit_cards; Empty set (0.01 sec) mysql> insert into credit_cards (num, expire_date) values (5678, now()); ERROR 1062 (23000): Duplicate entry '5678' for key 1Now, a well-identified user (@name and @pwd correctly set) can now select, update, insert and delete rows into credit_cards table without knowing this table stores all credit_cards of all users. This is what we call Fine-Grained Access Control.
mysql> set @name = 'foo'; Query OK, 0 rows affected (0.00 sec) mysql> set @pwd = 'bar'; Query OK, 0 rows affected (0.00 sec) mysql> select * from credit_cards; Empty set (0.02 sec) mysql> insert into credit_cards (num, expire_date) values (9999, now()); ERROR 1329 (02000): No data - zero rows fetched, selected, or processed mysql> delete from credit_cards; ERROR 1329 (02000): No data - zero rows fetched, selected, or processed
use applidata create or replace view applidata.credit_cards(num, expire_date) as select num, expire_date from fgac.credit_cards where owner = SUBSTRING_INDEX(USER(),'@',1);That's all ! you can drop all the triggers and now you do Fine Grained Access Control the right way (the MySQL way in fact). You can try select, update, delete, insert : it all works fine. That's definitively the way you want to implement Fine Grained Access Control in MySQL 5.0.