Передача інформації про те, хто видалив запис, на тригер Видалення


11

Під час налаштування аудиторського контуру у мене немає проблеми з відстеженням того, хто оновлює або вставляє записи в таблицю, однак, відстежувати, хто видаляє записи, видається більш проблематичним.

Я можу відстежувати вставки / оновлення, включаючи в поле Вставити / оновити поле "Оновлено". Це дозволяє тригеру INSERT / UPDATE мати доступ до поля "UpdateBy" через inserted.UpdatedBy. Однак за допомогою тригера "Видалення" дані не вставляються / оновлюються. Чи є спосіб передати інформацію на тригер видалення, щоб він міг знати, хто видалив запис?

Ось тригер вставки / оновлення

ALTER TRIGGER [dbo].[trg_MyTable_InsertUpdate] 
ON [dbo].[MyTable]
FOR INSERT, UPDATE
AS  

INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
VALUES (inserted.ID, inserted.LastUpdatedBy)
FROM inserted 

Використання SQL Server 2012


1
Дивіться цю відповідь. SUSER_SNAME()є ключем до того, хто отримав запис.
Кін Шах

1
Спасибі Кін, проте я не думаю, що SUSER_SNAME()це спрацювало б у такій ситуації, як веб-додаток, коли один користувач може використовуватись для комунікації баз даних для всього додатка.
webworm

1
Ви не згадали, що телефонували в веб-програму.
Кін Шах

Вибачте, Кін, я повинен був бути більш конкретним до типу програми.
черв'як

Відповіді:


10

Чи є спосіб передати інформацію на тригер видалення, щоб він міг знати, хто видалив запис?

Так: за допомогою дуже класного (і за умови використання функції), що називається CONTEXT_INFO. По суті, це пам'ять сеансу, яка існує у всіх областях і не пов'язана транзакціями. Він може бути використаний для передачі інформації (будь-яка інформація - ну та будь-яка, яка вписується у обмежений простір) до тригерів, а також назад і назад між викликами sub-proc / EXEC. І я раніше його використовував для такої самої ситуації.

  • Інформація про контекст - ВАРБІНАРНА (128)

  • Встановити через: SET CONTEXT_INFO

  • Отримати через: CONTEXT_INFO ()

Перевірте наступне, щоб побачити, як це працює. Зверніть увагу , що я конвертування в CHAR(128)перед CONVERT(VARBINARY(128), ... Це потрібно змусити пусту накладку, щоб полегшити її перетворення під VARCHARчас виходу з неї, CONTEXT_INFO()оскільки VARBINARY(128)це правильно набито 0x00s.

SELECT CONTEXT_INFO();
-- Initially = NULL

DECLARE @EncodedUser VARBINARY(128);
SET @EncodedUser = CONVERT(VARBINARY(128),
                            CONVERT(CHAR(128), 'I deleted ALL your records! HA HA!')
                          );
SET CONTEXT_INFO @EncodedUser;

SELECT CONTEXT_INFO() AS [RawContextInfo],
       RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())) AS [DecodedUser];

Результати:

0x492064656C6574656420414C4C20796F7572207265636F7264732120484120484121202020202020...
I deleted ALL your records! HA HA!

ВСТАВЛЯЄТЬСЯ ВСІМ РАЗОМ:

  1. Додаток повинен викликати збережену процедуру "Видалити", яка передається в ім'я користувача (або будь-яке інше), що видаляє запис. Я припускаю, що це вже використовувана модель, оскільки це здається, що ви вже відстежуєте операції "Вставка та оновлення".

  2. Збережена процедура "Видалити" робить:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. Тригер аудиту робить:

    -- Set the INT value in LEFT (currently 50) to the max size of [UserWhoMadeChanges]
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, COALESCE(
                         LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50),
                         '<unknown>')
       FROM DELETED del;
  4. Зауважте, що, як в коментарі вказував @SeanGallardy, через інші процедури та / або спеціальні запити про видалення записів із цієї таблиці можливо, що:

    • CONTEXT_INFOне встановлено і все ще є NULL:

      З цієї причини я оновив вище, INSERT INTO AuditTableщоб використовувати значення за COALESCEзамовчуванням. Або, якщо ви не хочете за замовчуванням і потребуєте імені, ви можете зробити щось подібне до:

      DECLARE @UserName VARCHAR(50); -- set to the size of AuditTable.[UserWhoMadeChanges]
      SET @UserName = LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50);
      
      IF (@UserName IS NULL)
      BEGIN
         ROLLBACK TRAN; -- cancel the DELETE operation
         RAISERROR('Please set UserName via "SET CONTEXT_INFO.." and try again.', 16 ,1);
      END;
      
      -- use @UserName in the INSERT...SELECT
    • CONTEXT_INFOбуло встановлено значення, яке не є дійсним ім'ям користувача, і, отже, може перевищувати розмір AuditTable.[UserWhoMadeChanges]поля:

      З цієї причини я додав LEFTфункцію, щоб гарантувати, що те, що схоплене, CONTEXT_INFOне зламає INSERT. Як зазначено в коді, вам просто потрібно встановити 50фактичний розмір UserWhoMadeChangesполя.


ОНОВЛЕННЯ ДЛЯ SQL SERVER 2016 ТА НОВІ

SQL Server 2016 додав вдосконалену версію цієї пам’яті за сеанс: Контекст сесії. Новий контекст сесії - це, по суті, хеш-таблиця пар Key-Value з буттям "Key" типу sysname(тобто NVARCHAR(128)) і "Value" SQL_VARIANT. Значення:

  1. Зараз існує розділення значень, тому менше шансів на конфлікт з іншими напрямами
  2. Ви можете зберігати різні типи, більше не потрібно турбуватися про дивну поведінку при поверненні значення через CONTEXT_INFO()(детальніше, будь ласка, дивіться мій пост: Чому CONTEXT_INFO () не повертає точне значення, встановлене SET CONTEXT_INFO? )
  3. Ви отримуєте набагато більше місця: 8000 байт макс за "Значення", до 256 Кбіт усього для всіх клавіш (порівняно з 128 байт макс CONTEXT_INFO)

Докладніше див. На наступних сторінках документації:


Проблема такого підходу полягає в тому, що він ДУЖЕ мінливий. Будь-який сеанс може встановити це, як такий він може замінити будь-який раніше встановлений елемент. Хочете дійсно зламати свою заявку? мати єдиний розробник перезаписати те, що очікуєш. Я б дуже радив НЕ використовувати це і мати стандартний підхід, який може зажадати змін архітектури. Інакше ти граєш вогнем.
Шон Галларді

@SeanGallardy Чи можете ви надати фактичний приклад того, що відбувається? Сесія == @@SPID. Це пам'ять PER-сеансу / з'єднання. Один сеанс не може замінити інформацію про контекст іншого сеансу. І коли сеанс вимикається, значення зникає. Немає такого поняття, як "раніше встановлений предмет".
Соломон Руцький

1
Я не сказав "інший сеанс", я сказав, що будь-який об'єкт у межах сеансу може це зробити. Отже, один розробник пише відросток, щоб зберігати власну "контекстуальну" інформацію, і тепер ваша перезаписана. Була програма, з якою я мав справу з такою самою схемою, я спостерігав, як це відбувається ... це було HR-програмне забезпечення. Дозвольте розповісти, як щасливим людям НЕ було заплачено вчасно через "помилку" одним із розробників, які писали нове ІП, яке помилково оновило інформацію про контекст для сеансу з того, що воно повинно було бути. Просто наводячи приклад, я фактично був свідком того, чому б не використовувати цей метод.
Шон Галларді

@SeanGallardy Добре, дякую за уточнення цього пункту. Але це все-таки лише частково справедливий пункт. Для того, щоб трапилася така ситуація, потрібно було б викликати "іншу" процедуру всередині цієї. Або якщо ви говорите про якусь іншу процедуру, яка може бути видаленою з цієї таблиці та натисканням тригера, це те, на що можна перевірити. Це умова гонки, на яку потрібно враховувати (як і у всіх багатопотокових додатках), а не причина не використовувати цю техніку. І тому я зроблю незначне оновлення, щоб зробити саме це. Дякуємо, що підняли цю можливість.
Соломон Руцький

2
Я кажу, що безпека як думка є головною проблемою, і це не інструмент її вирішення. Пам'яткові структури або інші види використання, які не порушують додаток, я впевнений, що у мене немає проблем. Це абсолютно є причиною НЕ використовувати його. YMMV, але я б ніколи не використовував щось таке нестабільне і неструктуроване для чогось важливого, такого як безпека. Використання будь-якого сховища, що записується, для доступу до користувачів - загальна жахлива ідея. Правильний дизайн здебільшого усуває потребу в таких речах.
Шон Галларді

5

Це неможливо, якщо ви не хочете записати ідентифікатор користувача SQL-сервера, а не перший рівень програми.

Можна зробити м'яке видалення, встановивши стовпець під назвою DeletedBy і встановивши, що потрібно, тоді ваш тригер оновлення може зробити реальне видалення (або заархівувати запис, я, як правило, уникаю жорстких видалень, де це можливо і законно), а також оновлення аудиторського сліду. . Щоб змусити видалення виконувати таким чином, визначте on deleteтригер, який викликає помилку. Якщо ви не хочете додати стовпець до фізичної таблиці, ви можете визначити подання, яке додає стовпець, і визначити instead ofтригери для обробки оновлення базової таблиці, але це може бути зайвим.


Я бачу вашу думку. Я дійсно хотів би зареєструвати користувача рівня програми.
черв'як

Девіде, насправді ти можеш передати інформацію тригерам. Будь ласка, дивіться мою відповідь для деталей :).
Соломон Руцький

Гарна пропозиція тут, мені дуже подобається цей маршрут. Вбиває двох птахів, захоплюючи Кого на тому ж кроці, що ініціює справжнє видалення. Оскільки цей стовпець буде NULL для кожного запису в цій таблиці, схоже, було б добре використовувати SPARSEстовпчик SQL Server ?
Airn5475

2

Чи є спосіб передати інформацію на тригер видалення, щоб він міг знати, хто видалив запис?

Так, мабуть, є два способи ;-). Якщо є якісь застереження щодо використання, CONTEXT_INFOяк я запропонував у своїй іншій відповіді тут , я просто подумав про інший спосіб, який має більш чітке функціональне відділення від інших кодів / процесів: використовувати локальну тимчасову таблицю.

Ім'я таблиці темп повинно включати в себе ім'я таблиці, яке буде видалено, оскільки це допоможе зберегти її окремо від будь-якого іншого коду, який, можливо, запуститься в одному сеансі. Щось у напрямку:
#<TableName>DeleteAudit

Однією з переваг для локальної таблиці темпів CONTEXT_INFOє те, що якщо хтось із іншого процесора - це якимось чином виклик цієї конкретної програми "Видалити" - просто трапиться неправильне використання того самого імені таблиці темп, підпроцес a) створить нову локальну таблиця temp запитуваного імені, яка буде відокремлена від цієї початкової таблиці temp (навіть якщо вона має те саме ім’я), і b) будь-які операції DML проти нової локальної таблиці temp у підпроцесі не впливатимуть на будь-які дані в локальна таблиця темпів, створена тут у батьківському процесі, отже, не буде перезапису даних. Звичайно, якщо підпроцесами випусків в DML заяву проти цього темп імені таблиці без першого оформивши CREATE TABLE з таким же ім'ям, то ці заяви DML будуть впливати на дані в цій таблиці. Але, в цей момент ми отримуємо дійснокрайовий кейс тут, навіть більше, ніж з ймовірністю перекриття використання CONTEXT_INFO(так, я знаю, що це сталося, саме тому я кажу "крайній випадок", а не "це ніколи не станеться").

  1. Додаток повинен викликати збережену процедуру "Видалити", яка передається в ім'я користувача (або будь-яке інше), що видаляє запис. Я припускаю, що це вже використовувана модель, оскільки це здається, що ви вже відстежуєте операції "Вставка та оновлення".

  2. Збережена процедура "Видалити" робить:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. Тригер аудиту робить:

    -- Set the datatype and length to be the same as the [UserWhoMadeChanges] field
    DECLARE @UserName VARCHAR(50);
    IF (OBJECT_ID(N'tempdb..#TriggerTestDeleteAudit') IS NOT NULL)
    BEGIN
       SELECT @UserName = UserName
       FROM #TriggerTestDeleteAudit;
    END;
    
    -- catch the following conditions: missing table, no rows in table, or empty row
    IF (@UserName IS NULL OR @UserName NOT LIKE '%[a-z]%')
    BEGIN
      /* -- uncomment if undefined UserName == badness
       ROLLBACK TRAN; -- cancel the DELETE operation
       RAISERROR('Please set UserName via #TriggerTestDeleteAudit and try again.', 16 ,1);
       RETURN; -- exit
      */
      /* -- uncomment if undefined UserName gets default value
       SET @UserName = '<unknown>';
      */
    END;
    
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, @UserName
       FROM DELETED del;

    Я перевірив цей код на тригері, і він працює, як очікувалося.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.