CHAPTER 24

image

Triggers

SQL Server supports three types of triggers. The first type is made up of DDL (data definition language) triggers, which fire in response to a DDL statement being executed, such as CREATE, ALTER, or DROP. You can use DDL triggers for auditing or for limiting DBA activity. The second type, logon triggers, fire when a session to the instance is established. You can use these triggers to stop a user from establishing a connection to an instance. The final type, DML triggers, fire as a result of a DML (data manipulation language) statement being executed, such as an INSERT, UPDATE, or DELETE statement. Developers use DML triggers to maintain complex logic and data integrity rules. This type of trigger is rarely used for administrative purposes, however, and is therefore beyond the scope of this book.

Triggers are code modules, similar to stored procedures. The difference is that triggers fire automatically when an event occurs in the instance, as opposed to being executed manually or via a script. This chapter discusses how DBAs can implement DDL and logon triggers to improve security and regulate administrative tasks.

Image Note  You can create triggers using T-SQL or in a .NET language. If you write them in a .NET language, you can then import them into assemblies in SQL Server through CLR (Common Language Runtime) integration. CLR triggers are beyond the scope of this book, since they are seldom used for administrative purposes.

DDL Triggers

The following sections first help you understand the concepts behind DDL triggers before moving on to discuss appropriate implementations.

Understanding DDL Triggers

You can create DDL triggers at either the instance level or the database level. When created at the instance level, they are said to be server scoped and can be used to respond to events such as new objects or logins being created. When triggers are scoped at the database level, they can respond to events such as tables being altered or dropped. You can also create triggers that respond to all database-level events at the server level so that they respond to events in all databases.

DDL triggers can provide you with a mechanism for auditing or limiting DBA activity, which can be very useful in environments where development or support teams need elevated permissions to instances or databases. You can use DLL triggers to limit the activity DBAs carry out, which can also prove useful if you have inexperienced or junior DBAs and want to ensure their actions are audited.

DDL triggers always execute after the event that caused them to fire, but within the context of the same transaction. This means that you can use a ROLLBACK statement within the body of the trigger to undo the actions the original statement performed. So, for example, if you implement a trigger at the instance level to restrict users from creating new databases, when a user runs a CREATE DTABASE statement, a ROLLBACK statement within the trigger undoes the action before it is committed.

Image Warning  Note, however, that you should not use triggers as a substitute for security. For example, imagine that a user runs a series of statements inside a transaction, which takes an hour to execute, just to have all their changes rolled back by a trigger. For a detailed discussion of transactions, please refer to Chapter 18.

While a trigger is executing, you have access to a system function called EVENTDATA(). This function returns a well-formed XML document, which conforms to the Event_Schema XSD schema. You can find full details of this schema at http://schemas.microsoft.com/sqlserver/2006/11/eventdata/events.xsd. The data included within this XML result set includes the SPID of the user who executed the original statement, when the event began, and the statement itself. This means that you can interrogate the XML result set using XQuery and insert the results into an audit table to ensure full reputability.

As well to respond to individual DDL events, you can also configure triggers to respond to DDL event groups. For example, the DDL_SERVER_LEVEL_EVENTS event group contains all events that occur only at the instance level. You can generate a list of the event groups available in SQL Server by running the query in Listing 24-1. This query returns each event name, along with the event group that contains it.

You can modify this query so that it returns a list of events within a specific event group. You do this by having it return all type names from the instance of the table aliased as a and filter on the type_name column of the table aliased as b. For example, the query in Listing 24-2 returns all events captured by the DDL_TABLE_EVENTS event group, as shown in Figure 24-1.

9781484207116_Fig24-01.jpg

Figure 24-1. Events included within the DDL_TABLE_EVENTS event group

You can use the script in Listing 24-3 to view the complete event group hierarchy of an event. In this instance, we again focus on the CREATE, ALTER, and DROP TABLE events.

The results of this query are displayed in Figure 24-2.

9781484207116_Fig24-02.jpg

Figure 24-2. TABLE DDL event hierarchy

Image Tip  You can find a complete list of DDL events at https://technet.microsoft.com/en-us/library/bb522542(v=sql.120).aspx.

Implementing DDL Triggers

DDL triggers can respond to the following statements being issued:

  • CREATE
  • ALTER
  • DROP
  • GRANT
  • DENY
  • REVOKE
  • UPDATE STATISTICS

To create DDL triggers, use the CREATE TRIGGER DDL statement. This statement has the basic structure shown in Listing 24-4.

This statement accepts the arguments detailed in Table 24-1.

Table 24-1. CREATE TRIGGER Arguments

Argument

Description

ON

Specifies the scope of the DDL trigger.

  •    ALL SERVER specifies that the trigger is created at the instance level.

  •    DATABASE specifies that the trigger is created at the database level.

WITH

ENCRYPTION and EXECUTE AS are acceptable options for the WITH clause and have the following uses:

  •    ENCRYPTION obfuscates the definition of the trigger so that it cannot be discovered, even via system tables. The obfuscation process is not supported by Microsoft, so you should ensure your original code is kept within source control, or you will not be able to alter the definition.

  •    EXECUTE AS changes the security context of the trigger. This allows you to elevate the permissions that the trigger has, while minimizing the impact on the security footprint of the user executing the code. These are the possible values:

  • LOGIN, which is followed by the name of a login to impersonate
  • USER, which is followed by the name of a user, within the same database, who should be impersonated
  • SELF, which executes under the context that either created, or last altered, the code
  • OWNER, which executes under the context that owns the schema in which the module resides
  • CALLER the default behavior; executes under the context that executed the module

FOR | AFTER

FOR and AFTER are interchangeable in the context of DDL triggers. This argument specifies that the trigger will fire after the original statement completes (but before it commits). This is the only valid option for a DDL trigger. This keyword is followed by a comma-separated list of events to which the trigger will respond. For example CREATE_DATABASE or ALTER_TABLE, DROP_TABLE.

AS

Specifies either the SQL statements that define the code body of the trigger or the EXTERNAL NAME clause to point to a CLR trigger.

Implementing Server Scope DDL Triggers

Imagine a scenario in which development teams are creating test databases on an instance without seeking necessary approval. This kind of issue is surprisingly common and can make an environment very challenging to manage. We can resolve this issue by creating a server-scoped DDL trigger with a definition matching that of Listing 24-5. In this script, we create a trigger called PreventDatabaseCreation. In the body of the trigger, we declare a variable named @Message and populate this message with the login name of the user who ran the CREATE DATABASE statement, followed by a message informing the user that they are not following the correct process. We finally roll back the transaction. Because the trigger fires in the context of the same transaction as the original statement (even if the statement runs in autocommit mode as opposed to as an explicit transaction) the CREATE DATABASE statement is rolled back.

If we use the statement in Listing 24-6 to attempt to create the Chapter24 database, we see the result illustrated in Figure 24-3. This result indicates that the transaction has been rolled back and it also displays our custom message to the user.

9781484207116_Fig24-03.jpg

Figure 24-3. Result of attempting to create a database

After implementing such a trigger, you obviously need to change your process for creating new databases so that your team still has the ability to create databases in a controlled fashion. The easiest way to do this is to ensure that all CREATE DATABASE statements are preceded by a statement to disable the trigger and succeeded by a statement to reenable the trigger. You can achieve this by using the DISABLE TRIGGER and ENABLE TRIGGER statements, as demonstrated in Listing 24-7.

To enable or disable all triggers at a particular scope, you can use the ALL keyword. For example, the command in Listing 24-8 disables all triggers at the server scope before reenabling them.

Implementing Database Scope DDL Triggers

Imagine a scenario in which you require an audit on any changes to programmable object definitions within the Chapter24 database. You should not prevent the changes from occurring, but you want all changes logged to an audit table. The first step in implementing a solution for this scenario is to create an audit table in the Chapter24 database. We can do this using the script in Listing 24-9.

Next, we need to create the trigger itself. Listing 24-10 demonstrates how to create this trigger. We name the trigger DDLAudit and define it to fire after any programmable objects, or other objects that contain code, are created, altered, or dropped. The WITH EXECUTE AS SELF clause means that the trigger executes under the security context of the user who created the trigger. This is beneficial, because it means we do not have to grant permissions to the Audit table to all users who issue DDL statements. This is possible because both objects are owned by the same user, within the same database.

Image Tip  Types, rules (which are now deprecated), and defaults do not have a corresponding ALTER statement. To change them, you must first create them and then drop them. Therefore, there are no corresponding event types for these statements.

In the body of the trigger, we use the XQuery Value method, which returns scalar values from XML, to pull data from the results of the EVENTDATA() function. We then use this data to populate the Audit table.

In order to test our trigger, we create a simple stored procedure called TestTrigger in the Chapter24 database. We can do this using the script in Listing 24-11.

If you run a SELECT * statement against our dbo.Audit table, you see results similar to those in Figure 24-4.

9781484207116_Fig24-04.jpg

Figure 24-4. Entry in dbo.Audit

Trigger Metadata

You can find metadata regarding server-scoped DDL triggers in the sys.server_triggers catalog view. This view returns the columns detailed in Table 24-2.

Table 24-2. Columns Returned by sys.server_triggers

Column

Description

name

The name of the trigger.

object_id

The unique object ID that has been assigned to the trigger.

parent_class

The parent class of the trigger. As the trigger is defined at the server scope, the parent class is always 100, which means server.

parent_class_desc

A textual description of the parent class of the trigger. Since the trigger is defined at the server scope, the parent class description is always server.

parent_id

The parent ID is always 0 for server-scoped triggers.

type

The value for type can be one of the following:

  •    TA—CLR trigger

  •    TR—T-SQL trigger

type_desc

A textual description of the type. Values can be one of these:

  •    CLR_TRIGGER

  •    SQL_TRIGGER

create_date

The date and time that the trigger was created.

modify_date

The data and time that the trigger was last modified using an ALTER TRIGGER statement.

is_ms_shipped

Specifies if the trigger is a system trigger or user defined.

  •    0—User-defined trigger

  •    1—System trigger

is_disabled

Specifies if the trigger is currently enabled.

  •    0—Enabled

  •    1—Disabled

You can use the sys.triggers catalog view to find details of triggers defined within the scope of a database. Table 24-3 describes the columns that this catalog view returns.

Table 24-3. Columns Returned by sys.triggers

Column

Description

name

The name of the trigger.

object_id

The unique object ID that has been assigned to the trigger.

parent_class

The parent class of the trigger.

  •    This value is always 0 for DDL triggers.

  •    This catalog view also contains details of DML triggers; therefore, a value of 1 indicates a table or column.

parent_class_desc

The description of the parent class of the trigger.

  •    This value is always DATABASE for DDL triggers.

  •    DML triggers have a parent_class_description of OBJECT_OR_COLUMN.

parent_id

The ID of the parent object.

  •    This is always 0 for DDL triggers.

  •    For DML triggers, this is the object ID of the table or view on which they are defined.

type

The value for type can be one of the following:

  •    TA—CLR trigger

  •    TR—T-SQL trigger

type_desc

A textual description of the type. Values can be one of the following:

  •    CLR_TRIGGER

  •    SQL_TRIGGER

create_date

The date and time that the trigger was created.

modify_date

The data and time that the trigger was last modified.

is_ms_shipped

Specifies if the trigger is a system trigger or user defined.

  •    0—User-defined trigger

  •    1—System trigger

is_disabled

Specifies if the trigger is currently enabled.

  •    0—Enabled

  •    1—Disabled

is_not_for_replication

Only applies to DML triggers, since DDL triggers cannot be replicated. Specifies if the trigger is marked NOT_FOR_REPLICATION.

  •    0 indicates it will be replicated.

  •    1 indicates it will not be replicated.

is_instead_of_trigger

Specifies if the trigger is defined as INSTEAD OF or AFTER.

  •    DDL triggers can only be defined as AFTER triggers and therefore always have a value of 0, meaning AFTER.

  •    DML triggers can have the following values:

  • 1, indicating INSTEAD OF
  • 0, indicating AFTER

You can use the sys.triggers and sys.server_triggers catalog views together to produce a query that returns a list of all user-defined DDL triggers within the instance, along with their scope. The script in Listing 24-12 achieves this by inserting the names of the server-scoped triggers into a global temporary table before using the XML data() function to insert the triggers for each database into the global temporary table. You can then access the temporary table with a simple SELECT statement. Finally, this script cleans up the temporary table, which makes the script rerunnable.

Logon Triggers

Logon triggers are very similar to DDL triggers, except that instead of firing in response to DDL events, they fire in response to a LOGON event occurring on the instance. This is the same event as the AUDIT_LOGON trace event. The advantage that a logon trigger offers over tracing and auditing is its ability to actually stop a user from establishing a connection. This is because the trigger fires synchronously with the event, as opposed to asynchronously.

Logon triggers prove most useful when you need to limit connections to the instance. For example, imagine that you have a reporting application that has a complex ETL process that runs between 8PM and 11PM every evening; however, you have an issue with staff accessing the application between these times and seeing an inconsistent version of the data. In this scenario, you may wish to use a logon trigger to stop any users, other than the service account that runs the ETL processes, from accessing the instance.

Image Note  If any connections exist at the start of the ETL window, you need to kill them using a separate process, since the trigger only fires at the point of login.

The script in Listing 24-13 creates a logon trigger, called PreventingLoginDuringETL, to avoid any users establishing sessions between 8PM and 11PM, with the exception of the service account that runs the ETL loads. The script uses the time portion of the GETDATE() system function to establish the current server time and the ORIGINAL_NAME() system function to determine the login.

Another example of when a logon trigger is useful for limiting connections is a scenario in which you are hosting a data-tier application that has a limited number of user licenses associated with it, and you need to ensure that the number of concurrent users accessing the database does not exceed this limit. For example, if our instance is hosting a data-tier application with a license for a maximum of 100 concurrent users, then we can use the script in Listing 24-14 to create a trigger called EnforceLicensing. This trigger performs a count of the concurrent logins to the instance and if this exceeds 100, then subsequent connections fail to establish a connection until the number of concurrent users drops below the threshold.

If a connection is rolled back due to trigger execution, then the user receives a message similar to the one illustrated in Figure 24-5.

9781484207116_Fig24-05.jpg

Figure 24-5. Error received if logon fails due to trigger execution

Controlling Trigger Order

It is possible to create multiple DDL (and logon) triggers that respond to the same event. If you create multiple triggers that are triggered by the same transaction, then you have some, but not complete, control over the order in which they fire.

Image Tip  Controlling the order in which triggers fire is particularly useful if you have one trigger that consumes significantly fewer resources than another trigger that responds to the same event, and setting an order may negate the need for the resource intensive trigger to fire. For example, imagine that you have two triggers created against the CREATE TABLE event. If one of these triggers audits every DDL statement and the other trigger rolls back unauthorized table creations, then it is very sensible to fire the trigger that may perform the rollback first. This means that there will be instances where you roll back the transaction and do not need the audit trigger, which requires you to invoke disk activity.

You can exercise this control by using the sp_settriggerorder system stored procedure. This procedure allows you to control the first and the last trigger that are fired. If you have more than three triggers that respond to the same event, then you have no way to guarantee the order of the triggers that are not marked as the first trigger or the last trigger. The sp_settriggerorder procedure accepts the parameters detailed in Table 24-4.

Table 24-4. sp_settriggerorder Parameters

Parameter

Description

@triggername

The name of the trigger that you wish to mark as first or last.

@order

Acceptable values are First, Last, and None. If triggers exist that are marked as First or Last, then None indicates that the trigger is not the first or the last trigger.

@stmttype

The type of statement that causes the trigger to fire. For logon triggers, this is the LOGON event. For DDL triggers, this can be any DDL event; however, specifying an event class is not permitted. For DML triggers, the value can be INSERT, UPDATE, or DELETE.

@namespace

Specifies the scope of the trigger. This can be specified as SERVER, DATABASE, or NULL. If NULL is passed, then it indicates that the trigger is a DML trigger.

It is important to note that it is possible to mark triggers as First and Last for both the server scope and the database scope. The limitation here, however, is that the server trigger marked as First will fire before the database trigger marked as First. This is because all server-level triggers fire before any database-level triggers fire.

The script in Listing 24-15 configures our EnforceLicensing logon trigger to fire before our PreventLoginDuringETL trigger. If we subsequently create any further logon triggers on the instance, then they fire after the EnforceLicensing trigger but before the PreventLoginTrigger trigger.

Image Note  If we alter a trigger that is marked as First or Last, then its Order property reverts back to None. This means that your change control processes should take this into account and that you should follow any ALTER TRIGGER statement immediately by calling sp_settriggerorder to remark it as the first or last trigger.

Image Caution  Creating multiple triggers on the same event is often not a good idea for performance reasons. However, it is important to understand how to control the order in case you discover them in third-party applications.

Summary

You can use DDL triggers to restrict changes to objects or audit user activity. They always fire after the event that caused them to fire, but within the context of the same transaction. This means that you can always roll back the action that caused the trigger to fire if you need to. You can scope DDL triggers at the instance level or at the database level, and you can configure them to respond to specific events or groups of events.

Within the context of a DDL trigger, you have access to the EVENTDATA() function, which contains information about the action that caused the trigger to fire. This information includes the time that the event occurred, the user that executed the event, and the statement itself. This data is provided in XML format, so it can be interrogated using XML and can be used for auditing purposes.

Logon triggers are always scoped at the instance level and fire when a connection is made to the instance. Logon triggers fire synchronously with the logon, and thus you can use them to prevent a connection from being established. This can be useful in circumstances in which you want to impose a limit on the number of concurrent users.

You can find metadata regarding server-scoped triggers in the sys.server_triggers catalog view and metadata regarding database-scoped triggers in the sys.triggers catalog view from within the context of the relevant database. To a limited extent, you can control the order in which triggers fire by using the sp_settrigger order system stored procedure. This procedure allows you to mark a trigger as either first or last. When more than three triggers fire in response to the same event, it is not possible to control the order of the other triggers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.239.41