Trigger to detect whether DELETE or UPDATE is called by stored proc - sql

I have a scenario where certain users must have the rights to update or delete certain records in production. What I want to do put in a safeguard to make sure they do not accidentally update or delete a large section (or entirety) of the table, but only a few records as per their need. So I wrote a simple trigger to accomplish this.
CREATE TRIGGER delete_check
ON dbo.My_table
AFTER UPDATE,DELETE AS
BEGIN
IF (SELECT COUNT(*) FROM Deleted) > 15
BEGIN
RAISERROR ('Bulk deletes from this table are not allowed', 16, 1)
ROLLBACK
END
END --end trigger
But here is the problem. There is a stored procedure that can do bulk updates to the table. The users can and should be allowed to call the stored procedure, as it's scope is more constrained. So my trigger would unfortunately preclude them from calling the stored proc when they need to.
The only solution I have thought of is to run the stored proc as an impersonated user, then modify the trigger to exclude that user from the logic. But that will bring up other issues in my environment. Not unsurmountable, but annoying. Nevertheless, this seems the only viable option.
Am I thinking about this the right way, or is there a better approach?

You can add a check of ##NESTLEVEL in the trigger. The value will be 1 for an ad-hoc statement or 2 when called from the stored procedure.
CREATE TRIGGER delete_check
ON dbo.My_table
AFTER UPDATE,DELETE AS
BEGIN
IF (SELECT COUNT(*) FROM Deleted) > 15
AND ##NESTLEVEL = 1 --ad-hoc delete
BEGIN
RAISERROR ('Bulk deletes from this table are not allowed', 16, 1);
ROLLBACK;
END;
END;

I usually handle this with CONTEXT_INFO(). This gives you a better control than ##NESTLEVEL because you can actually identify the specific stored procedure doing the calling and handle them individually if required. You do this as follows:
Add the procedure name to CONTEXT_INFO() e.g.
-- START OF STORED PROCEDURE
-- Tell the trigger who we are, and that we can be trusted.
declare #OldContext char(128), #NewContext varbinary(128);
-- Get existing context_info()
set #OldContext = coalesce(convert(char(128), context_info()), '');
-- Add new info to context_info
set #NewContext = convert(varbinary(128),convert(char(128), 'dbo.MyProcedureName'));
-- Store new context info
set context_info #NewContext;
-- STORED PROCEDURE CONTENT
-- END OF STORED PROCEDURE
-- Restore context_info
set #NewContext = convert(varbinary(128), #OldContext);
set context_info #NewContext;
In the trigger return early if the CONTEXT_INFO() is from a trusted source e.g.
-- START OF TRIGGER
declare #NewContext char(128) = coalesce(convert(char(128),context_info()),'');
if #NewContext in ('dbo.MyProcedureName') begin
return;
end;
Another advantage of this approach (for other trigger uses) is you can avoid carrying out logic in a trigger when called from a SP. Because often you put logic in a trigger to ensure that it happens regardless of how the insert/update/delete happens. But when done in an SP you can ensure the required logic is carried out within the SP which avoids the need to do it in a trigger. Especially useful if you end up with performance issues due to too much logic in the trigger.
Note: For SQL Server 2016+ you can use SESSION_CONTEXT() in a similar way.

One way of doing this would be:
Create a stored procedure to perform the desired work
If the criteria varies greatly, create several procedures for each "kind" of work
Grant EXECUTE permissions to the desired users on those procedures ("kind of work") that they are permitted to do. (You are using Database Roles and Domain Groups, right?)
Revoke permissions to make ad hoc data modifications on the table
It can be fussy to set up, but it supports the "Principle of Least Permissions", permitting users to do only what they are supposed to do.

Related

Should I use sp_getapplock to prevent multiple instances of a stored procedure that conditionally inserts?

Hear me out! I know this use case sounds suspect, but...
I have a stored procedure which checks a table (effectively a cache) for data for a given requested ID. If it doesn't find any data for that ID, or deems it out of date, it executes a second stored procedure which will pull data from a separate database (using dynamic SQL, source DB name is based on the requested ID) and insert it into the local table. It then selects from this table.
If the data is in the table, everything returns quickly (ms), but if it needs to be brought in from the other database, it takes about 10 seconds. We're seeing race conditions where two concurrent instances check the local cache, see something is missing, and queue up sequential ingestions of the remote data into the cache. To avoid double-insertion, the cache-populating procedure will clear whatever is already there for this id, but this just means the first instance of the procedure can selecting no rows because the second instance deleted the just-inserted records before re-inserting them itself.
I think I want to put a lock around the entire procedure (checking the cache, potentially populating the cache, selecting from the cache) - although I'm open to other solutions. I think the overall caching approach has to remain on-demand though, the remote databases come and go by the hundreds, and we only want to cache the ones actually requested by reporting as-needed.
BEGIN TRANSACTION;
BEGIN TRY
-- Take out a lock intended to prevent anyone else modifying the cache while we're reading and potentially modifying it
EXEC sp_getapplock #Resource = '[private].[cache_entries]', #LockOwner='Transaction', #LockMode = 'Exclusive', #LockTimeout = 120000;
-- Invoke a stored procedure that ingests any required data that is not already cached
EXEC [private].populate_cache #required_dbs
-- CALCULATIONS
-- ... SELECT FROM [private].cache_entries
COMMIT TRANSACTION; -- Free the lock
END TRY
BEGIN CATCH --Ensure we release our lock on failure
ROLLBACK TRANSACTION;
THROW
END CATCH;
The alternative to sp_getapplock is to use locking hints with your transaction. Both are reasonable approaches. Locking hints can be complex, but they protect the target object itself rather than a single code path. So sometimes necessary. sp_getapplock is simple (with Transaction as owner), and reliable.
You can do this without sp_getapplock, which tends to inhibit concurrency a lot.
The way to do this is to continue do your checks within a transaction, but to apply a HOLDLOCK hint, as well as a UPDLOCK hint.
HOLDLOCK aka the SERIALIZABLE isolation level, will place a lock not only on the ID you specify, but even on the absence of such data, in other words it will prevent anyone else inserting into that ID.
You must use both these hints, as well as have an index that matches that SELECT, otherwise you could run into major blocking and deadlocking problems due to full table scans.
Also, you don't need a CATCH and ROLLBACK. Just use SET XACT_ABORT ON; which ensures a rollback in any event of an error.
SET XACT_ABORT ON; -- always have this set
BEGIN TRANSACTION;
DECLARE #SomeData nvarchar(100) = (
SELECT ce.SomeColumn
FROM [private].cache_entries ce WITH (HOLDLOCK, UPDLOCK)
WHERE ce.SomeCondition = 1
);
IF #SomeData IS NULL
BEGIN
-- Invoke a stored procedure that ingests any required data that is not already cached
EXEC [private].populate_cache #required_dbs
END
-- CALCULATIONS
-- ... SELECT FROM [private].cache_entries
COMMIT TRANSACTION; -- Free the lock

Does PL/SQL Procedure Automatically Commit When it Exits? [duplicate]

I have 3 tables in oracle DB. I am writing one procedure to delete some rows in all the 3 tables based on some conditions.
I have used all three delete statements one by one in the procedure. While executing the mentioned stored procedure, is there any auto-commit happening in the at the time of execution?
Otherwise, Should I need to manually code the commit at the end?
There is no auto-commit on the database level, but the API that you use could potentially have auto-commit functionality. From Tom Kyte.
That said, I would like to add:
Unless you are doing an autonomous transaction, you should stay away from committing directly in the procedure: From Tom Kyte.
Excerpt:
I wish PLSQL didn't support commit/rollback. I firmly believe
transaction control MUST be done at the topmost, invoker level. That
is the only way you can take these N stored procedures and tie them
together in a transaction.
In addition, it should also be noted that for DDL (doesn't sound like you are doing any DDL in your procedure, based on your question, but just listing this as a potential gotcha), Oracle adds an implicit commit before and after the DDL.
There's no autocommit, but it's possible to set commit command into stored procedure.
Example #1: no commit
create procedure my_proc as
begin
insert into t1(col1) values(1);
end;
when you execute the procedure you need call commit
begin
my_proc;
commit;
end;
Example #2: commit
create procedure my_proc as
begin
insert into t1(col1) values(1);
commit;
end;
When you execute the procedure you don't nee call commit because procedure does this
begin
my_proc;
end;
There is no autocommit with in the scope of stored procedure. However if you are using SQL Plus or SQL Developer, depending on the settings autocommit is possible.
You should handle commit and rollback as part of the stored procedure code.

ALTER PROCEDURE with TRANSACTION

I need to modify approx. 24 huge UDP and for production deployment i need to do a BEGIN TRANSACTION / ROLLBACK / COMMIT PROCESS.
How can I add the ALTER PROCEDURE my_proc between BEGIN TRANSACTION and COMMIT or ROLLBACK?
Note: EXEC('ALTER PROCEDURE..') can NOT be implemented.
Thanks
Update: there is a way to alter a procedure and rollback if it fails?
why can't you the regular way.
BEGIN TRANSACTION
GO
CREATE PROCEDURE testProcedure
AS
SELECT 1
GO
SELECT OBJECT_ID('testProcedure') ObjectID --this will return the object ID
GO
rollback TRANSACTION
SELECT OBJECT_ID('testProcedure') ObjectID --this will return NULL because the proc creation was rolled back
GO
You cannot have BEGIN TRY and BEGIN CATCH around batches. However you can use the last batch to check that all previous steps have succeeded (by examining the catalog views like sys.objects for example). Then you can decide if the batch all succeeded and either commit or roll back.
(Leandro, I’m adding a new answer because it would be too long for a compent)
I’ve been thinking. I don’t think this is a solution I would ever implement, but based on your requirements (and specially your restrictions), here is an idea that would work:
There is a modify_date on the sys.objects catalog so, why don’t you store the dates off all your objects before you run your updates and compare with the dates after you ran your updates. If ALL the dates are different, it means that all of them were updated correctly, if one of the dates is equal, it means that one failed and then you run a rollback script (you will need the rollback code, won’t be easy as just type ROLLBACK)

Regarding SQL Server delete trigger

I want to capture the user's name who deletes a row, when delete action is taken then a stored procedure deletes a row from table. We are providing all required parameters to the stored procedure and also the user's name from the front end who is deleting data.
Basically I want to capture the user's name who is deleting from stored procedure from delete trigger. But it is not possible. We can do one thing that before deleting record we can put the user's name into a temp table and get that name from trigger. But there is one problem that at a time two users can delete two different rows. So what will be the best solution? I don't want use any shadow tables. please discuss. thanks
You are basically asking how to pass the parameter into the trigger?
You can use set CONTEXT_INFO inside the procedure and read this inside the trigger.
DECLARE #name VARBINARY(128)
SET #name = CAST('Martin' AS VARBINARY(128));
SET CONTEXT_INFO #name /*Set it*/
SELECT CAST(CONTEXT_INFO() AS VARCHAR(128)) /*Read it*/
SET CONTEXT_INFO 0x /*Reset it*/
If you've already got a stored procedure, why bother with triggers? Have the stored procedure log who deleted the row.

Is it possible for a trigger to find the name of the stored procedure that modified data?

There are a few stored procedures that routinely get called by a few different systems to do maintenance on a few tables in our database. Some are automated, some aren't.
One of the tables has a column where the number is sometimes off, and we don't know for sure when or why this is happening. I want to put a trigger on the table so I can see what is being changed and when, but it'd also be helpful to know which procedure initiated the modification.
Is it possible to get the name of the stored procedure from the trigger? If not, is there any other way to tell what caused something to be modified? (I'm not talking about the user either, the name of the user doesn't help in this case).
you can try: CONTEXT_INFO
here is a CONTEXT_INFO usage example:
in every procedure doing the insert/delete/update that you want to track, add this:
DECLARE #string varchar(128)
,#CONTEXT_INFO varbinary(128)
SET #string=ISNULL(OBJECT_NAME(##PROCID),'none')
SET #CONTEXT_INFO =cast('Procedure='+#string+REPLICATE(' ',128) as varbinary(128))
SET CONTEXT_INFO #CONTEXT_INFO
--do insert/delete/update that will fire the trigger
SET CONTEXT_INFO 0x0 --clears out the CONTEXT_INFO value
here is the portion of the trigger to retrieve the value:
DECLARE #string varchar(128)
,#sCONTEXT_INFO varchar(128)
SELECT #sCONTEXT_INFO=CAST(CONTEXT_INFO() AS VARCHAR) FROM master.dbo.SYSPROCESSES WHERE SPID=##SPID
IF LEFT(#sCONTEXT_INFO,9)='Procedure'
BEGIN
SET #string=RIGHT(RTRIM(#sCONTEXT_INFO),LEN(RTRIM(#sCONTEXT_INFO))-10)
END
ELSE
BEGIN --optional failure code
RAISERROR('string was not specified',16,1)
ROLLBACK TRAN
RETURN
END
..use the #string
Our system is already using the CONTEXT_INFO variable for another purpose so that is not available. I also tried the DBCC INPUTBUFFER solution which almost worked. The draw back to the inputbuffer is that it returns only the outside calling procedure. Ex: procA calls procB which fires a trigger. The trigger runs DBCC INPUTBUFFER which only shows procA. Since my trigger was looking for procB, this approach failed.
What I have done in the meantime is to create a staging table. Now procA calls procB. procB inserts a line in the staging table then fires the trigger. The trigger checks the staging table and finds the procB entry. Upon return procB deletes its entry from the staging table. It's a shell game but it works. I would be interested in any feedback on this.
I've not tried this but ##PROCID looks like it might return what you want.