Azure SQL: Audit Triggers and Ownership Chaining - triggers

I have two tables, T1 and T2, in two different schemas, S1 and S2, respectively. I have written a trigger, TR1 (with no EXECUTE AS clause), on T1 which logs inserts (I), updates (U), and deletes (D) into T2, which has an identical schema as T1, with some additional metadata columns. S1, T1, S2, T2, and TR1 all are owned by dbo.
I have created a role, R1, which has S, I, U, and D rights to S1 (and therefore, T1). The role also allows S on S2 (and therefore T2), but denies I, U, and D. I have created a user, U1, and assigned role R1 to this user.
Under the user context of U1, if I try to I, U, or D on T2, that is denied, as expected. However, if I I, U, or D into T1, the audit rows are successfully inserted into T2. This is the behavior that I wanted, but was wondering the reason for this, as U1 has been explicitly denied these privileges.
Is this because of ownership chaining, such that U1's privileges are never checked on T2 when TR1 runs, or something else?
Azure SQL version is Microsoft SQL Azure (RTM) - 12.0.2000.8 Jul 23 2021 13:14:19 Copyright (C) 2019 Microsoft Corporation
--
Trigger code added:
CREATE TRIGGER TRG ON dbo.T1
FOR INSERT, UPDATE, DELETE
AS
BEGIN;
DECLARE #Operation CHAR(1);
SET #Operation = (
CASE
WHEN EXISTS(SELECT 1 FROM INSERTED) AND EXISTS(SELECT 1 FROM DELETED) THEN 'U'
WHEN EXISTS(SELECT 1 FROM INSERTED) THEN 'I'
WHEN EXISTS(SELECT 1 FROM DELETED) THEN 'D'
ELSE NULL
END
);
IF #Operation = 'I'
BEGIN;
INSERT INTO adt.T1(Operation, ID, C1)
SELECT #Operation, ID, C1
FROM INSERTED;
END;
IF #Operation = 'D'
BEGIN;
INSERT INTO adt.T1 (Operation, ID, C1)
SELECT #Operation, ID, C1
FROM DELETED;
END;
IF #Operation = 'U'
BEGIN;
INSERT INTO adt.T1 (Operation, ID, C1)
SELECT #Operation, i.ID, i.C1
FROM INSERTED i
INNER JOIN DELETED d
ON i.ID = d.ID
-- Hash indicated columns of INSERTED and DELETED to determine if there are any real changes.
WHERE (SELECT HASHBYTES('MD5', (SELECT i.ID, i.C1 FROM (SELECT NULL AS X) t FOR XML AUTO)))
<>
(SELECT HASHBYTES('MD5', (SELECT d.ID, d.C1 FROM (SELECT NULL AS X) t FOR XML AUTO)));
END;
END;

Ok, yes this is ownership chaining in action, because you are securing at the schema level.
Understanding SQL Server Ownership Chaining
When there’s an ownership chain, security is ignored on the object being referenced.
In this case there is an owership chain because both schemas have the same owner, and this means that the privileges are NOT re-evaluated for the trigger execution when it accesses the assumed secured schema.
To be clear, the execution context has not changed, there is no EXECUTE AS or impersonation going on. The operations against table T2 are still operating within the context of the original caller, but the ownership chaining rules means that access rules are simple not re-evaluated, it doesn't even try to check.
Ownership chaining is an optimisation feature of SQL Server, in many cases it improves query throughput by allowing access rules to be evaluated once rather than re-evaluating every possible securable context it the engine only re-evaluates when the contexts are secured by a different owner.
This is a primary reason why the Owner of a schema matters, and why you might specify different arbitrary owners created specifically for different schemas.
In the case of Audit/Change logging, we can take advantage of this behaviour to maintain the integrity of our data by blocking users who deliberately attempt to modify the audit records, while still allowing those users to execute queries and commands that might have side effects that will insert rows into the audit tables.
Because the user context has not been tampered with we can still capture and record information about the current user context and include that in the metadata that you may be recording about the operation.
For strictly non-audit based scenarios you need to be aware that ownership chaining can expose your secured tables to updates you might not have been expecting.

Related

How to select for update one row from table A and all joined rows from table B in Postgres?

I have a table that is a queue of tasks with each task requiring exclusive access to several resources. I want my query to select a single task that doesn't need resources claimed by other similar sessions.
If each task had to work on a single resource I would've written something like this:
select *
from tasks t
inner join resources r
on r.id = t.resource_id
order by t.submitted_ts
limit 1
for update skip locked
But since I have multiple resources I somehow have to lock them all:
select *
from tasks t
inner join task_details td
on t.id = td.task_id
inner join resources r
on r.id = td.resource_id
order by t.submitted_ts, t.id
limit ???
for update skip locked
I cannot limit by 1, since I need to lock all joined rows of resources.
It also seems to me that I should try and lock all rows of resources, so it must be not skip locked, but nowait for resources and skip locked for tasks.
First I had to create a helper function that either locks all linked rows or not:
create or replace function try_lock_resources(p_task_id bigint)
returns boolean
language plpgsql
as $$
begin
perform *
from task_details td
join resources r
on td.resource_id = r.resource_id
where td.task_id = p_task_id
for update of r nowait;
return true;
exception when lock_not_available then
return false;
end;
$$;
Then I needed to invoke this function for each row:
select *
from tasks
where processing_status = 'Unprocessed'
and try_lock_resources(task_id)
order by created_ts
limit 1
for update skip locked
After this query is run, only the returned row and its associated resources are locked. I verified that an identical query from another session returns the first unprocessed tasks that has no resources in common with the one returned by the first session.
P.S.: the original answer used a different query (which you shouldn't use as is):
with unprocessed_tasks as materialized (
select *
from tasks t
where processing_status = 'Unprocessed'
order by created_ts
)
select *
from unprocessed_tasks
where try_lock_resources(task_id)
limit 1
for update skip locked
The problem with this query is that the following could (and did happen):
session A runs the query, locks task X and starts working on it
session B starts running the query, the materialized CTE is run first, returning task X among other tasks
session A commits the transaction and releases all locks
session B finishes running the query, locks task X and starts working on it
LIMIT clause applies to joined table.
Instead of table A use subquery with it's own LIMIT.
SELECT
"table a"."учебный год",
"table b".семестр
FROM
(SELECT
"Учебный год"."учебный год"
FROM
"Учебный год"
ORDER BY
"Учебный год"."учебный год"
LIMIT 1) "table a"
INNER JOIN "Семестр" "table b" ON "table b"."учебный год" = "table a"."учебный год"

PostgreSQL: prevent lock on self table update with left join

I'm on PostgreSQL 9.3. I'm the only one working on the database, and my code run queries sequentially for unit tests.
Most of the times the following UPDATE query run without problem, but sometimes it makes locks on the PostgreSQL server. And then the query seems to never ends, while it takes only 3 sec normally.
I must precise that the query run in a unit test context, i.e. data is exactly the same whereas the lock happens or not. The code is the only process that updates the data.
I know there may be lock problems with PostgreSQL when using update query for a self updating table. And most over when a LEFT JOIN is used.
I also know that a LEFT JOIN query can be replaced with a NOT EXISTS query for an UPDATE but in my case the LEFT JOIN is much faster because there is few data to update, while a NOT EXISTS should visit quite all row candidates.
So my question is: what PostgreSQL commands (like Explicit Locking LOCK on table) or options (like SELECT FOR UPDATE) I should use in order to ensure to run my query without never-ending lock.
Query:
-- for each places of scenario #1 update all owners that
-- are different from scenario #0
UPDATE t_territories AS upt
SET id_owner = diff.id_owner
FROM (
-- list of owners in the source that are different from target
SELECT trg.id_place, src.id_owner
FROM t_territories AS trg
LEFT JOIN t_territories AS src
ON (src.id_scenario = 0)
AND (src.id_place = trg.id_place)
WHERE (trg.id_scenario = 1)
AND (trg.id_owner IS DISTINCT FROM src.id_owner)
-- FOR UPDATE -- bug SQL : FOR UPDATE cannot be applied to the nullable side of an outer join
) AS diff
WHERE (upt.id_scenario = 1)
AND (upt.id_place = diff.id_place)
Table structure:
CREATE TABLE t_territories
(
id_scenario integer NOT NULL,
id_place integer NOT NULL,
id_owner integer,
CONSTRAINT t_territories_pk PRIMARY KEY (id_scenario, id_place),
CONSTRAINT t_territories_fkey_owner FOREIGN KEY (id_owner)
REFERENCES t_owner (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE RESTRICT
)
I think that your query was locked by another query. You can find this query by
SELECT
COALESCE(blockingl.relation::regclass::text,blockingl.locktype) as locked_item,
now() - blockeda.query_start AS waiting_duration, blockeda.pid AS blocked_pid,
blockeda.query as blocked_query, blockedl.mode as blocked_mode,
blockinga.pid AS blocking_pid, blockinga.query as blocking_query,
blockingl.mode as blocking_mode
FROM pg_catalog.pg_locks blockedl
JOIN pg_stat_activity blockeda ON blockedl.pid = blockeda.pid
JOIN pg_catalog.pg_locks blockingl ON(
( (blockingl.transactionid=blockedl.transactionid) OR
(blockingl.relation=blockedl.relation AND blockingl.locktype=blockedl.locktype)
) AND blockedl.pid != blockingl.pid)
JOIN pg_stat_activity blockinga ON blockingl.pid = blockinga.pid
AND blockinga.datid = blockeda.datid
WHERE NOT blockedl.granted
AND blockinga.datname = current_database()
This query I've found here http://big-elephants.com/2013-09/exploring-query-locks-in-postgres/
Also can use ACCESS EXCLUSIVE LOCK to prevent any query to read and write table t_territories
LOCK t_territories IN ACCESS EXCLUSIVE MODE;
More info about locks here https://www.postgresql.org/docs/9.1/static/explicit-locking.html

chaining sql queries in a WITH clause to eventually perform an update

I am trying to populate a test db with a bunch of objects for a very specific integration test and I am having trouble using the returned id's from a one of the queries as the values for the subsequent object's fields.
-- create a user with an account with salesforce integration
WITH p AS (
INSERT INTO person (email, confirmed, first_name, last_name) VALUES
('test3#example.com', TRUE, 'Salesforce', 'Guy')
RETURNING id
),
ac AS (
INSERT INTO account (subscription_type, subscription_expires_on, salesforce_integration) VALUES (0, '2024-10-10', TRUE)
RETURNING id
),
am AS (
INSERT INTO account_member (person_id, account_id, admin) VALUES
((SELECT p.id FROM p), (SELECT ac.id FROM ac), TRUE)
RETURNING account_id, person_id
),
la AS (
-- create a salesforce linked_account
INSERT INTO linked_account (person_id, provider_id, access_token) VALUES
((SELECT p.id FROM p), 'salesforce', '00DC00000016x37!AQIAQIt5EpCIgTFl9hg2qF9Ed6vzLJmTg9Nrd.uxvVva5WaxzMChn4sBBgV6KXiICCBoJgcFYbrTqpFtFJwpd.B7fe5kG9_z')
RETURNING id
),
v AS (
-- create a video and take
INSERT INTO video (person_id, account_id) VALUES
((SELECT p.id FROM p), (SELECT ac.id FROM ac))
RETURNING id
),
t AS (
INSERT INTO take (video_id, key, duration, state, thumbnail_selected) VALUES
((SELECT v.id FROM v), 'g1/g1546ad07eff44c397e356be7c4bea49/g1546ad07eff44c397e356be7c4bea49', 35, 2, 1)
RETURNING id
)
-- update video with selected take
UPDATE video SET selected_take_id = (SELECT id FROM take WHERE take.video_id=video.id);
The issue I am running into is when the test is run it states that the video (v) I created does not have a selected_take_id set, which means that "update video with selected take" query at the bottom did not in fact work.
It is important to note that this DB is not empty when this script is run, there are 2 other similar seed files that run before this so there areat least 2 videos, 2 takes etc. already stored in the db. If anyone knows how to make this work without having to provide static id's as values for the foreign keys it would save me a ton of time.
Thanks!
It looks like you're expecting the UPDATE (the "main query") to find a row that is inserted in the same SQL statement, a row that doesn't exist at the start of the statement.
If that's the case, this can't work as documented in Data-Modifying Statements in WITH :
The sub-statements in WITH are executed concurrently with each other
and with the main query. Therefore, when using data-modifying
statements in WITH, the order in which the specified updates actually
happen is unpredictable. All the statements are executed with the same
snapshot (see Chapter 13), so they cannot "see" each others' effects
on the target tables. This alleviates the effects of the
unpredictability of the actual order of row updates, and means that
RETURNING data is the only way to communicate changes between
different WITH sub-statements and the main query
You probably want to use a DO block instead with several queries laid out in procedural form in pl/pgsql.

When is a Deadlock not a Deadlock?

I'm asking this question because I'm getting a deadlock from time to time that I don't understand.
This is the scenario:
Stored Procedure that updates table A:
UPDATE A
SET A.Column = #SomeValue
WHERE A.ID = #ID
Stored Procedure that inserts into a temp table #temp:
INSERT INTO #temp (Column1,Column2)
SELECT B.Column1, A.Column2
FROM B
INNER JOIN A
ON A.ID = B.ID
WHERE B.Code IN ('Something','SomethingElse')
I see that there could possibly be a lock wait but I fail to see how a deadlock would occur, am I missing something obvious?
EDIT:
The SPs that I typed here are obviously simplified versions but I'm using the columns involved. The structure of both tables would be:
CREATE TABLE A (ID IDENTITY
CONSTRAINT PRIMARY KEY,
Column VARCHAR (100))
CREATE TABLE B (ID IDENTITY
CONSTRAINT PRIMARY KEY,
Code VARCHAR (100))
Try this since its causeing locks specify for the tables name the table hint and keyword:
WITH(NOLOCK)
So some thing like this for your scenario:
INSERT INTO #temp (Column1,Column2)
SELECT B.Column1, A.Column2
FROM B WITH(NOLCOK)
INNER JOIN A WITH(NOLOCK)
ON A.ID = B.ID
WHERE B.Code IN ('Something','SomethingElse')
See how you go then.
You can lookup table hint also for tsql, sql server to see which one suits you best. The one I specified NOLCOK will not cause locks and also it will skip locked rows as some other process is using them, so if you dont care you can use it.
I am not sure with temp tables but you can also use table hints with INSERT, INSERT INTO WITH(TABLE_HINT).

How to do a safe "SELECT FOR UPDATE" with a WHERE condition over multiple tables on a DB2?

Problem
On a DB2 (version 9.5) the SQL statement
SELECT o.Id FROM Table1 o, Table2 x WHERE [...] FOR UPDATE WITH RR
gives me the error message SQLSTATE=42829 (The FOR UPDATE clause is not allowed because the table specified by the cursor cannot be modified).
Additional info
I need to specify WITH RR, because I'm running on isolation level READ_COMMITTED, but I need my query to block while there is another process running the same query.
Solution so far...
If I instead query like this:
SELECT t.Id FROM Table t WHERE t.Id IN (
SELECT o.Id FROM Table1 o, Table2 x WHERE [...]
) FOR UPDATE WITH RR
everything works fine.
New problem
But now I occasionally get deadlock exceptions when multiple processes perform this query simultaneously.
Question
Is there a way to formulate the FOR UPDATE query without introducing a place where a deadlock can occur?
First, for having isolation level READ_COMMITTED you do not need to specify WITH RR, because this results in the isolation level SERIALIZABLE. To specify WITH RS (Read Stability) is enough.
To propagate the FOR UPDATE WITH RS to the inner select you have to specify additionally USE AND KEEP UPDATE LOCKS.
So the complete statement looks like this:
SELECT t.Id FROM Table t WHERE t.Id IN (
SELECT o.Id FROM Table1 o, Table2 x WHERE [...]
) FOR UPDATE WITH RS USE AND KEEP UPDATE LOCKS
I made some tests on a DB2 via JDBC and it worked without deadlocks.