TRUNCATE Permission: Letting a User Empty One Table

TRUNCATE permission does not exist as a grant you can hand out on its own. The caller needs ALTER on the table, which is far too much. A small procedure can give a loader account that one action and nothing else.

A cake lifter removes one cake layer while neighboring layers remain untouched

Why DELETE is not enough

Here is a question I get a lot. The nightly loader can delete every row in the staging table, so why can’t it truncate that same table? Because TRUNCATE is treated as a schema change. It needs ALTER on the table.

And ALTER is a big key. It also lets the caller add columns, drop columns and change the table design. A loader account that only wants a clean table does not need any of that. Handing it ALTER to save a few minutes of scripting is how a staging table ends up with a surprise column.

The demo makes a staging table with two rows. The user StageLoader has no login, which is enough for testing permissions. It can read, insert and delete, but nothing more.

DROP TABLE IF EXISTS dbo.LoadStage;
CREATE TABLE dbo.LoadStage (Id int IDENTITY PRIMARY KEY, ValueText varchar(20));
INSERT dbo.LoadStage (ValueText) VALUES ('first'), ('second');
GO
IF USER_ID(N'StageLoader') IS NOT NULL DROP USER StageLoader;
CREATE USER StageLoader WITHOUT LOGIN;
GRANT SELECT, INSERT, DELETE ON dbo.LoadStage TO StageLoader;

Now let StageLoader try a plain TRUNCATE. It fails with error 1088. The message says the object cannot be found or you lack permissions, which is SQL Server’s polite way of saying no.

EXECUTE AS USER = N'StageLoader';
BEGIN TRY
    TRUNCATE TABLE dbo.LoadStage;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS error_number, ERROR_MESSAGE() AS error_message;
END CATCH;
REVERT;

Wrap the one action in a procedure

The fix is a procedure that does exactly one thing. WITH EXECUTE AS OWNER means it runs with the owner’s rights, not the caller’s. The owner has ALTER, so the TRUNCATE works.

Notice the table name is typed right into the procedure. There is no parameter for it. If you let callers pass a table name, you have built a tool that empties any table they can name.

CREATE OR ALTER PROCEDURE dbo.EmptyLoadStage
WITH EXECUTE AS OWNER
AS
BEGIN
    SET NOCOUNT ON;
    TRUNCATE TABLE dbo.LoadStage;
END;
GO
GRANT EXECUTE ON dbo.EmptyLoadStage TO StageLoader;
What lets a loader empty one table

Check what the caller can really do

Run the next block as StageLoader. The first result says CanAlterTable is 0, so the caller still has no ALTER on the table. The procedure runs anyway. The second result says StageRows is 0, so the table is empty.

Then I insert one new row. Before the truncate the table held ids 1 and 2, so a plain insert would have got 3. The new row gets Id 1, because TRUNCATE also resets the identity counter.

EXECUTE AS USER = N'StageLoader';
SELECT HAS_PERMS_BY_NAME(N'dbo.LoadStage', N'OBJECT', N'ALTER') AS CanAlterTable;
EXEC dbo.EmptyLoadStage;
REVERT;

SELECT COUNT(*) AS StageRows FROM dbo.LoadStage;

INSERT dbo.LoadStage (ValueText) VALUES ('after truncate');
SELECT Id FROM dbo.LoadStage;
Procedure results show no ALTER permission, an empty staging table, and identity value one
The caller has no ALTER on the table, yet the procedure empties it. The next row gets Id 1.

Foreign keys still say no

The procedure does not bypass every rule. If another table has a foreign key pointing at the staging table, TRUNCATE fails with error 4712, even for the owner. Here a small table named LoadStageNote points at LoadStage, and the caller gets that error.

Please do not drop the foreign key just to make the cleanup work. That key is protecting something. Either delete from the child table first, or use DELETE for this table.

DROP TABLE IF EXISTS dbo.LoadStageNote;
CREATE TABLE dbo.LoadStageNote (NoteId int PRIMARY KEY,
                                StageId int NOT NULL REFERENCES dbo.LoadStage (Id));

EXECUTE AS USER = N'StageLoader';
BEGIN TRY
    EXEC dbo.EmptyLoadStage;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS error_number, ERROR_MESSAGE() AS error_message;
END CATCH;
REVERT;

Keep the procedure narrow

This design is only as safe as the procedure stays small. Anyone who can alter it can make it do more, and it will run with the owner’s rights. So keep the list of people who can change it short. If you later edit it, test the same caller again.

Try this on your own server with a test table first. Run it as a login that has only EXECUTE, and confirm that HAS_PERMS_BY_NAME says 0 for ALTER. The last block removes everything the demo created.

DROP TABLE IF EXISTS dbo.LoadStageNote;
DROP PROCEDURE IF EXISTS dbo.EmptyLoadStage;
DROP USER IF EXISTS StageLoader;
DROP TABLE IF EXISTS dbo.LoadStage;

Give your loader the one action it needs, and keep the keys to the table in your own pocket.

A cleanup right is not table ownership, it is one action you chose to expose.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL Server Security, SQL Stored Procedure, SQL Table Operation
Previous Post
MySQL – Change the Limit of Row Retrieved in MySQL Workbench
Next Post
Data Governance: Catalog, Lineage and Data Quality

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.