![]() |
|
|
|
By: David Bird | Read Comments (3) | Related Tips: More > Security |
How do you grant a SQL Server developer truncate table permission without giving them the ability to alter the table? Check out the examples and explanation in this tip.
The TRUNCATE TABLE SQL statement is a great time and log space saver when deleting all rows from a large table. However to use this statement, the executing user requires ALTER permission on the table, which is contrary to the best practices of limiting permissions. To over come this, there is an option available to allow a developer to truncate a table, but not have ALTER permission. This is accomplished by a DBA creating a stored procedure with the TRUNCATE TABLE statements and then granting execute permission on the stored procedure to those who need it.
In addition to the permission restriction, several other rules prevent the usage of TRUNCATE TABLE. These rules are listed below.
The FOREIGN KEY restriction is only for the parent table. A stored procedure can be used to truncate a table and overcome the FOREIGN KEY restriction. This is accomplished by including drop/create foreign key SQL with the TRUNCATE TABLE statements. This article provides stored procedure samples to address this challenge and other types of foreign key configurations.
Create the sample database, TestSQL, using this script SampleDB_TestSQL.sql. The database contains five populated tables used to demonstrate how a stored procedure can be used to truncate tables using one of these four FOREIGN KEY configurations:
The main difference between a SQL id and a user proxy id is that the proxy id cannot log into the database because no login is created for it.
Use the following scripts to create the User Proxy ID and grant permission on the table.
-- Create User Proxy in the User Database USE [TestSQL] GO CREATE USER [truncate_proxy] WITHOUT LOGIN; GO GRANT ALTER TO [truncate_proxy]; GO
After you create the proxy id, connect to the instance using SQL Server Management Studio (SSMS) and you will see the user proxy id listed as a user for the database but not as an instance login. In addition, the id can only be used by those granted permission to IMPERSONATE it or have SYSADMIN permission. This user proxy id should only be used in truncate table stored procedures to prevent confusion of its purpose. SYSADMIN's by default are the only ones who can create a stored procedure that uses this id and hence retains full control over which tables can be truncated and by whom.
A truncate table stored procedure requires only a few SQL statements when the table has no restrictive foreign keys. The EXECUTE AS statement specifies the user proxy whose permission is used while the stored procedure is executing. The TRUNCATE TABLE identifies the table to be truncated. The SELECT statement in this stored procedure sample is used to show the specified EXECUTE AS 'user id' is being used. Once you are confident the correct permission is being used, remove it.
-- Create Stored Procedure
CREATE PROCEDURE dbo.Truncate_Table_Loner
WITH EXECUTE AS 'truncate_proxy'
AS
-- Remove this select, it is only used to show the USER being used during execution.
SELECT SYSTEM_USER [SYSTEM_USER], USER [USER], name, type, usage
FROM sys.user_token;
TRUNCATE TABLE [dbo].[Loner];
GO
After the stored procedure is created, perform a quick test by executing it using the test script below. The BEGIN TRANS and ROLLBACK statements are used to keep the data changes from being committed and allow you to repeat the test. This script also demonstrates that TRUNCATE TABLE can be rolled back.
-- Quick Test BEGIN TRAN SELECT COUNT(*) CNT FROM [dbo].[Loner]; EXEC dbo.Truncate_Table_Loner; SELECT COUNT(*) CNT FROM [dbo].[Loner]; ROLLBACK; GO
Create a test Login and User id then grant it execute permission on the stored procedure Truncate_Table_Loner. This id will be used to perform the truncate.
-- Grant Execute Permission -- Setup ID on Database with Connect permission USE master GO CREATE LOGIN [test_user_id] WITH PASSWORD = 'JustConnect123'; GO USE TestSQL GO CREATE USER [test_user_id] FOR LOGIN [test_user_id]; GO -- Grant Permission GRANT EXECUTE ON dbo.Truncate_Table_Loner TO [test_user_id]; GO
Note: Since this id can connect to the database, it will also have permission to anything granted to PUBLIC in the database and instance
As SYSADMIN, one can IMPERSONATE a Login, in this test that is just what we do. The test user id has been granted execute on the stored procedure. Using the EXECUTE AS we IMPERSONATE "test_user_id" and execute the stored procedure.
This test script besides executing the stored procedure, displays the user name being use before each SQL execution. The REVERT command switches the connection back to your user id.
-- Test Truncate BEGIN TRAN SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; SELECT COUNT(*) CNT FROM [dbo].[Loner]; EXECUTE AS LOGIN = 'test_user_id'; SELECT COUNT(*) CNT FROM [dbo].[Loner]; -- Denied because id does not have select SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; EXEC dbo.Truncate_Table_Loner; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; REVERT; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; SELECT COUNT(*) CNT FROM [dbo].[Loner]; ROLLBACK; GO
Truncating the "child" table in a foreign key relationship is no different than truncating a table with no foreign keys. The only change in the stored procedure is the table being truncated.
-- Child Only FK CREATE PROCEDURE dbo.Truncate_Table_ChildTable2 WITH EXECUTE AS 'truncate_proxy' AS -- Remove this select, its just used to show which USER is being used during execution. SELECT SYSTEM_USER [SYSTEM_USER], USER [USER], name, type, usage FROM sys.user_token; TRUNCATE TABLE [dbo].[ChildTable2]; GO -- Grant Permission GRANT EXECUTE ON dbo.Truncate_Table_ChildTable2 TO [test_user_id]; GO
Test this stored procedure by running the following script.
-- Test Truncate BEGIN TRAN SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; SELECT COUNT(*) CNT FROM [dbo].[ChildTable2]; EXECUTE AS LOGIN = 'test_user_id' SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; EXEC dbo.Truncate_Table_ChildTable2; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; REVERT; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; SELECT COUNT(*) CNT FROM [dbo].[ChildTable2]; ROLLBACK; GO
The syntax for truncating a self-referencing foreign key table is again the same as a table without any foreign keys. The only change in the stored procedure is the table being truncated.
-- Self Referencing FK CREATE PROCEDURE dbo.Truncate_Table_SelfRefTable WITH EXECUTE AS 'truncate_proxy' AS -- Remove this select, its just used to show which USER is being used during execution. SELECT SYSTEM_USER [SYSTEM_USER], USER [USER], name, type, usage FROM sys.user_token; TRUNCATE TABLE [dbo].[SelfRefTable]; GO -- Grant Permission GRANT EXECUTE ON dbo.Truncate_Table_SelfRefTable TO [test_user_id]; GO
Test this stored procedure by running the following script.
-- Test Truncate BEGIN TRAN SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; SELECT COUNT(*) CNT FROM [dbo].[SelfRefTable]; EXECUTE AS LOGIN = 'test_user_id'; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; EXEC dbo.Truncate_Table_SelfRefTable; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; REVERT; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; SELECT COUNT(*) CNT FROM [dbo].[SelfRefTable]; ROLLBACK; GO
TRUNCATE TABLE statement will not work on a table that is the parent table in a Foreign Key to other tables. Disabling foreign keys will not work. Truncating child table first will not work. You have to remove the foreign keys for it to work. This can be accomplished in a stored procedure.
Here are the steps to complete this task:
-- Grant Reference permission in the Database to allow creation of foreign keys
USE [TestSQL]
GO
GRANT REFERENCES TO [truncate_proxy];
GO
-- Truncate Parent table with child tables
CREATE PROCEDURE dbo.Truncate_Table_Parent_and_Child_tables
WITH EXECUTE AS 'truncate_proxy'
AS
-- Remove this select, its just used to show which USER is being used during execution.
SELECT SYSTEM_USER [SYSTEM_USER], USER [USER], name, type, usage FROM sys.user_token;
ALTER TABLE [dbo].[ChildTable1] DROP CONSTRAINT [FK_ChildTable1_ParentTable];
ALTER TABLE [dbo].[ChildTable2] DROP CONSTRAINT [FK_ChildTable2_ParentTable];
TRUNCATE TABLE [dbo].[ChildTable1];
TRUNCATE TABLE [dbo].[ChildTable2];
TRUNCATE TABLE [dbo].[ParentTable];
ALTER TABLE [dbo].[ChildTable1] WITH CHECK
ADD CONSTRAINT [FK_ChildTable1_ParentTable] FOREIGN KEY([Child_PK_id])
REFERENCES [dbo].[ParentTable] ([Parent_PK_id]);
ALTER TABLE [dbo].[ChildTable2] WITH CHECK
ADD CONSTRAINT [FK_ChildTable2_ParentTable] FOREIGN KEY([Child_PK_id])
REFERENCES [dbo].[ParentTable] ([Parent_PK_id]);
GO
-- Grant Permission
GRANT EXECUTE ON dbo.Truncate_Table_Parent_and_Child_tables TO [test_user_id];
GO
Test this stored procedure by running the following script.
-- Test Truncate on Parent table BEGIN TRAN SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; SELECT 'Parent', COUNT(*) CNT FROM [dbo].[ParentTable] UNION ALL SELECT 'Child1', COUNT(*) CNT FROM [dbo].[ChildTable1] UNION ALL SELECT 'Child2', COUNT(*) CNT FROM [dbo].[ChildTable2] ; EXECUTE AS LOGIN = 'test_user_id'; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; EXEC dbo.Truncate_Table_Parent_and_Child_tables; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; REVERT; SELECT SUSER_NAME() [SUSER_NAME], USER_NAME() [USER_NAME]; SELECT 'Parent', COUNT(*) CNT FROM [dbo].[ParentTable] UNION ALL SELECT 'Child1', COUNT(*) CNT FROM [dbo].[ChildTable1] UNION ALL SELECT 'Child2', COUNT(*) CNT FROM [dbo].[ChildTable2] ; ROLLBACK; GO
| Tuesday, January 03, 2012 - 8:25:48 AM - RichS | Read The Tip |
|
I really never like to give anyone truncate rights, but this is a great set of scripts and examples. |
|
| Tuesday, January 03, 2012 - 2:57:22 PM - Ray | Read The Tip |
|
I like the approach and the implementation. I wonder, should you really truncate the child tables? The Foreign Key relationship is not always mandatory (i.e., 0 or more) and in that case it is likely that it would be preferable to delete the child rows where the FK value is not null rather than simply truncate the table. Of course, this can become a real hassle if the key relationships are nested or even recursive :). In a previous life, I started on a script that would save the definition of every FK that referenced a table and then drop the FK. A later part of the script would re-create the FK. It got complicated. ;P |
|
| Thursday, March 15, 2012 - 10:13:26 AM - ScottPletcher | Read The Tip |
|
Good article and approach, and very thorough. Still, I'd be too nervous about potential future changes to hard-code the FK definitions. I would rather dynamically capture them and re-create the FKs based on their current definition. |
|
|
privacy | disclaimer | copyright | advertise | about authors | contribute | feedback | giveaways | user groups Some names and products listed are the registered trademarks of their respective owners. Edgewood Solutions LLC | MSSharePointTips.com | MSSQLTips.com |