6. Triggers, Events, and Stored Routines
A few other constructions that are treated specially when logged are
stored programs—that is,
triggers, events, and stored routines (the last is a collective name for stored procedures and stored functions). Their treatment
with respect to the binary log contains some elements in common, so they
will be covered together in this section. The explanation distinguishes
statements of two types: statements that define or destroy stored
programs and statements that invoke them.
6.1. Statements that define or destroy stored programs
The following discussion shows triggers in the examples, but the
same principles apply to definition of events and stored routines. To
understand why the server needs to handle these features specially
when writing them to the binary log, consider the code in Example 5.
In the example, a table named employee
keeps information about all employees of an imagined system and a
table named log keeps a log of interesting
information. Note that the log table has
a timestamp column
that notes the time of a change and that the name column in the
employee table is the primary key for the table.
There is also a status column to
tell whether the addition succeeded or failed.
To track information about employee information changes—for
example, for auditing purposes—three triggers are created so that
whenever an employee is added, removed, or changed, a log entry of the
change is added to a log table.
Notice that the triggers are after triggers, which means that
entries are added only if the executed statement is successful. Failed
statements will not be logged. We will later extend the example so
that unsuccessful attempts are also logged.
Example 5. Definitions of tables and triggers for employee
administration
CREATE TABLE employee (
name CHAR(64) NOT NULL,
email CHAR(64),
password CHAR(64),
PRIMARY KEY (name)
);
CREATE TABLE log (
id INT AUTO_INCREMENT,
email CHAR(64),
message TEXT,
ts TIMESTAMP,
PRIMARY KEY (id)
);
CREATE TRIGGER tr_employee_insert_after AFTER INSERT ON employee FOR EACH ROW
INSERT INTO log(email, status, message)
VALUES (NEW.email, 'OK', CONCAT('Adding employee ', NEW.name));
CREATE TRIGGER tr_employee_delete_after AFTER DELETE ON employee FOR EACH ROW
INSERT INTO log(email, status, message)
VALUES (OLD.email, 'OK', 'Removing employee');
delimiter $$
CREATE TRIGGER tr_employee_update_after AFTER UPDATE ON employee FOR EACH ROW
BEGIN
IF OLD.name != NEW.name THEN
INSERT INTO log(email, status, message)
VALUES (OLD.email, 'OK',
CONCAT('Name change from ', OLD.name, ' to ', NEW.name));
END IF;
IF OLD.password != NEW.password THEN
INSERT INTO log(email, status, message)
VALUES (OLD.email, 'OK', 'Password change');
END IF;
IF OLD.email != NEW.email THEN
INSERT INTO log(email, status, message)
VALUES (OLD.email, 'OK', CONCAT('E-mail change to ', NEW.email));
END IF;
END $$
delimiter ;
|
With these trigger definitions, it is now possible to add and
remove employees as shown in Example 3-6. Here an
employee is added, modified, and removed, and as you can see, each of
the operations is logged to the log table.
The operations of adding, removing, and modifying employees may
be done by a user who has access to the employee
table, but what about access to the log table? In
this case, a user who can manipulate the employee
table should not be able to make changes to the
log table. There are many reasons for this, but
they all boil down to trusting the contents of the
log table for purposes of maintenance, auditing,
disclosure to legal authorities, etc. So the DBA may choose to make
access to the employee table available to many
users while keeping access to the log table very
restricted.
To make sure the triggers can execute successfully against a
highly protected table, they are executed as the user who defined the
trigger, not as the user who changed the contents of the
employee table. So, the CREATE TRIGGER
statements in Example 5 are executed by
the DBA, who has privileges to make additions to the
log table, whereas the statements altering
employee information in Example 6 are executed
through a user management account that only has privileges to change
the employee table.
When the statements in Example 6 are executed,
the employee management account is used for updating entries in the
employee table, but the DBA privileges are used
to make additions to the log table. The employee
management account cannot be used to add or remove entries from the
log table.
As an aside, Example 6 assigns
passwords to a user variable before using them in the
statement. This is done to avoid sending sensitive data in plain text
to another server; more details can be found in Security and the Binary Log.
Example 6. Adding, removing, and modifying users
master> SET @pass = PASSWORD('xyzzy');
Query OK, 0 rows affected (0.00 sec)
master> INSERT INTO employee VALUES ('mats', '[email protected]', @pass);
Query OK, 1 row affected (0.00 sec)
master> UPDATE employee SET name = 'matz' WHERE email = '[email protected]';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
master> SET @pass = PASSWORD('foobar');
Query OK, 0 rows affected (0.00 sec)
master> UPDATE employee SET password = @pass WHERE email = '[email protected]';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
master> DELETE FROM employee WHERE email = '[email protected]';
Query OK, 1 row affected (0.00 sec)
master> SELECT * FROM log;
+----+------------------+-------------------------------+---------------------+
| id | email | message | ts |
+----+------------------+-------------------------------+---------------------+
| 1 | [email protected] | Adding employee mats | 2010-01-13 18:56:08 |
| 2 | [email protected] | Name change from mats to matz | 2010-01-13 18:56:11 |
| 3 | [email protected] | Removing employee | 2010-01-13 18:57:11 |
+----+------------------+-------------------------------+---------------------+
3 rows in set (0.00 sec)
|
In general, a user with REPLICATION
SLAVE privileges has privileges to read everything that occurs on the
master and should therefore be secured so that the account cannot be
compromised.
Make it impossible to log into the account from outside
the firewall. Log all attempts to log into the account, and place the
log on a separate secure server. Encrypt the connection between the master and the slave
using, for example, MySQL’s built-in SSL (Secure Sockets Layer) support.
Even if the account has been secured, there is information
that does not have to be in the binary log, so it makes sense not to
store it there in the first place. One of the more common types of sensitive information is
passwords. Events containing passwords can be written to the binary log
when executing statements that change tables on the server and that
include the password required for access to the tables. A typical example is: UPDATE employee SET pass = PASSWORD('foobar')
WHERE email = '[email protected]';
If replication is in place, it is better to rewrite this
statement without the password. This is done by computing and
storing the hashed password into a user-defined variable and then using that in the expression: SET @password = PASSWORD('foobar');
UPDATE employee SET pass = @password WHERE email = '[email protected]';
Since the SET statement is
not replicated, the original password will not be stored in the
binary log, only in the memory of the server while executing the
statement. As long as the password hash, rather than
the plain-text password, is stored in the table, this technique
works. If the raw password is stored directly in the table, there is
no way to prevent the password from ending up in the binary log. But
storing hashes for passwords is a standard good practice in any
case, to prevent someone who gets his hands on the raw data from
learning the passwords. Encrypting the connection between the master and the slave offers
some protection, but if the binary log itself is compromised,
encrypting the connection doesn’t help. |
If you recall the earlier discussion about implicit information,
you may already have noticed that both the user executing a line of
code and the user who defines a trigger are implicit. Neither the definer nor
the invoker of the trigger is critical to executing the trigger on the
slave, and the user information is effectively ignored when the slave
executes the statement. However, the information
is important when the binary log is played back
to a server—for instance, when doing a PITR.
To play back a binary log to the server without problems in
handling privileges on all the various tables, it is necessary to
execute all the statements as a user with SUPER privileges.
But the triggers may not have been defined using SUPER privileges, so it is important to
re-create the triggers with the correct user as the trigger’s definer.
If a trigger is defined with SUPER
privileges instead of by the user who defined the trigger originally,
it might cause a privilege escalation.
To permit a DBA to specify the user under which to execute a
trigger, the CREATE
TRIGGER syntax includes an optional DEFINER clause. If a DEFINER is not given to the statements—as is
the case in Example 7—the statement
will be rewritten for the binary log to add a DEFINER clause and use the current user as
the definer. This means that the definition of the insert trigger
appears in the binary log, as shown in Example 3-7. It lists the
account that created the trigger (root@localhost) as the definer, which is
what we want in this case.
Example 7. A CREATE TRIGGER statement in the binary log
master> SHOW BINLOG EVENTS FROM 92236 LIMIT 1\G
*************************** 1. row ***************************
Log_name: master-bin.000038
Pos: 92236
Event_type: Query
Server_id: 1
End_log_pos: 92491
Info: use `test`; CREATE DEFINER=`root`@`localhost` TRIGGER ...
1 row in set (0.00 sec)
|
6.2. Statements that invoke triggers and stored routines
Moving over from definitions to invocations, we can ask how the
master’s triggers are handled during replication. Well, actually
they’re not handled at all.
The statement that invokes the trigger is logged to the binary
log, but it is not linked to the particular trigger. Instead, when the
slave executes the statement, it automatically executes any triggers
associated with the tables affected by the statement. This means that
there can be different triggers on the master and the slave, and the
triggers on the master will be invoked on the master while the
triggers on the slave will be invoked on the slave. For example, if
the trigger to add entries to the log table is not necessary on the
slave, performance can be improved by eliminating the trigger from the
slave.
Still, any context events necessary for replicating correctly
will be written to the binary log before the statement that invokes
the trigger, even if it is just the statements in the trigger that
require the context events. Thus, Example 8 shows the binary
log after executing the INSERT statement in
Example 5.
Note that the first event writes the INSERT
ID for the log table’s primary key.
This reflects the use of the log table in the trigger, but it might
appear to be redundant because the slave will not use the
trigger.
You should, however, note that using different triggers on the
master and slave—or no trigger at all on either the master or slave—is
the exception and that the INSERT
ID is necessary for replicating the INSERT statement correctly when the trigger
is both on the master and slave.
Example 8. Contents of the binary log after executing INSERT
master> SHOW BINLOG EVENTS FROM 93340\G
*************************** 1. row ***************************
Log_name: master-bin.000038
Pos: 93340
Event_type: Intvar
Server_id: 1
End_log_pos: 93368
Info: INSERT_ID=1
*************************** 2. row ***************************
Log_name: master-bin.000038
Pos: 93368
Event_type: User var
Server_id: 1
End_log_pos: 93396h
Info: @`pass`=_latin1
0x2A39423530303334334243353245323931313137324542353241... COLLATE
latin1_swedish_ci
*************************** 3. row ***************************
Log_name: master-bin.000038
Pos: 93396
Event_type: Query
Server_id: 1
End_log_pos: 93537
Info: use `test`; INSERT INTO employee VALUES ...
3 rows in set (0.00 sec)
|