SQL Server 中有没有办法使表只能通过触发器插入?

Tyl*_*ing 5 trigger sql-server permissions sql-server-2012

标题是不言自明的,但如果您对我为什么想要这个感到好奇;我想要这个是因为我有一个存档/日志表来存储活动表的过去值,因此我不希望数据有以任何方式受到损害的风险。唯一应该在表上插入的是我在活动表上创建的触发器以记录其更改。在极少数情况下,我们可能需要手动编辑日志表,我将关闭(如果存在)“插入锁”

我将 SQL Server 2012 Enterprise 与 SQL Management Studio 结合使用

Sol*_*zky 10

这可以使用证书和模块签名(即ADD SIGNATURE)来完成。使用 Impersonation viaEXECUTE AS可能会变得混乱,并且可能会导致其他人冒充“允许的”用户,或更改使用EXECUTE AS. 但是对于模块签名:无法模拟基于证书的用户(请参阅最终测试用例),在不知道证书密码的情况下无法对另一个模块进行签名,并且如果有人更改了您签名的任何模块(例如触发器),则签名会自动删除,提醒您该更改,然后您可以决定是使用当前更改退出还是拒绝更改;-)。

此外,在触发器中捕获 ApplicationName / ProgramName 并不可靠,因为很容易在 ConnectionString 中传递该值。

请注意,审计表位于不同的架构中 -- Auditing-- 与主表不同,dbo以防止所有权链接,假设大多数存储过程也将在dbo架构中。

设置

USE [...];
GO

CREATE CERTIFICATE [AuditCert]
    ENCRYPTION BY PASSWORD = 'Password Goes Here.'
    WITH SUBJECT = 'Restrict Insert Test';
GO

CREATE USER [AuditUser]
    FROM CERTIFICATE [AuditCert];
    -- no DEFAULT_SCHEMA for Certificate-based Users
GO

CREATE SCHEMA [Auditing]
    AUTHORIZATION [AuditUser];
GO

-- DROP TABLE [Auditing].[AuditLog];
CREATE TABLE [Auditing].[AuditLog]
(
    AuditLogID INT IDENTITY(1, 1) NOT NULL,
    AuditDate DATETIME2 NOT NULL
        CONSTRAINT [DF_AuditLog_AuditDate] DEFAULT (SYSDATETIME()),
    ImportantStuffID INT,
    Column2 VARCHAR(50),
    CONSTRAINT [PK_AuditLog] PRIMARY KEY CLUSTERED (AuditLogID ASC)
);
GO

CREATE TABLE [dbo].[ImportantStuff]
(
    ImportantStuffID INT IDENTITY(1, 1) NOT NULL,
    Column2 VARCHAR(50),
    CONSTRAINT [PK_ImportantStuff] PRIMARY KEY CLUSTERED (ImportantStuffID ASC)
);
GO

CREATE TRIGGER [dbo].[AuditImportantStuff]
ON [dbo].[ImportantStuff]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;

INSERT INTO [Auditing].[AuditLog] ([ImportantStuffID], [Column2])
    SELECT  ins.[ImportantStuffID], ins.[Column2]
    FROM        inserted ins;
END;
GO


ADD SIGNATURE TO [dbo].[AuditImportantStuff]
    BY CERTIFICATE [AuditCert]
    WITH PASSWORD = 'Password Goes Here.';
GO


CREATE PROCEDURE [dbo].[AttemptDirectInsert]
(
    @ImportantStuffID INT,
    @Column2 VARCHAR(50)
)
AS
SET NOCOUNT ON;

INSERT INTO [Auditing].[AuditLog] ([ImportantStuffID], [Column2])
    VALUES (@ImportantStuffID, @Column2);
GO


CREATE PROCEDURE [dbo].[ImportantStuff_AddData]
(
    @ValueForColumn2 VARCHAR(50)
)
AS
SET NOCOUNT ON;

INSERT INTO [dbo].[ImportantStuff] ([Column2])
    VALUES (@ValueForColumn2);
GO


CREATE USER [TestUser]
    WITHOUT LOGIN
    WITH DEFAULT_SCHEMA = [dbo];
GO

GRANT EXECUTE ON [dbo].[AttemptDirectInsert] TO [TestUser];
GRANT EXECUTE ON [dbo].[ImportantStuff_AddData] TO [TestUser];
GO
Run Code Online (Sandbox Code Playgroud)

考试

SELECT SESSION_USER, ORIGINAL_LOGIN();

INSERT INTO [Auditing].[AuditLog] ([ImportantStuffID], [Column2]) VALUES (-1, 'test 1');


EXECUTE AS USER = 'TestUser';

SELECT SESSION_USER, ORIGINAL_LOGIN();

INSERT INTO [Auditing].[AuditLog] ([ImportantStuffID], [Column2]) VALUES (-2, 'test 2');
-- Msg 229, Level 14, State 5, Line 102
-- The INSERT permission was denied on the object 'AuditLog', database '...',
--   schema 'Auditing'.


EXEC [dbo].[AttemptDirectInsert]
    @ImportantStuffID = -3,
    @Column2 = 'test 3';
-- Msg 229, Level 14, State 5, Procedure AttemptDirectInsert, Line 115
-- The INSERT permission was denied on the object 'AuditLog', database '...',
--   schema 'Auditing'.


INSERT INTO [dbo].[ImportantStuff] ([Column2]) VALUES ('test 4');
-- Msg 229, Level 14, State 5, Line 114
-- The INSERT permission was denied on the object 'ImportantStuff', database '...',
--   schema 'dbo'.


EXEC [dbo].[ImportantStuff_AddData]
    @ValueForColumn2 = 'test 5';
-- woo hoo!


SELECT * FROM [Auditing].[AuditLog];
-- Msg 229, Level 14, State 5, Line 122
-- The SELECT permission was denied on the object 'AuditLog', database '...',
--   schema 'Auditing'.


REVERT;

SELECT SESSION_USER, ORIGINAL_LOGIN();

SELECT * FROM [Auditing].[AuditLog];

EXECUTE AS USER = 'AuditUser';
-- Msg 15517, Level 16, State 1, Line 143
-- Cannot execute as the database principal because the principal "AuditUser" does not
--  exist, this type of principal cannot be impersonated, or you do not have permission.
Run Code Online (Sandbox Code Playgroud)

更新

补充说明:

  1. 正如@Paul 在他的回答中提到的,这种方法(如图所示)不会阻止特权用户进行直接插入。但是,仍然可以通过审计表**上的触发器阻止不是从使用证书签名的代码启动的 DML 操作。但这主要是为了防止意外插入,因为db_owner固定数据库角色中的任何人都应该能够禁用触发器,并且db_datawriter如果固定数据库角色中的某个人相当狡猾,那么他们可能至少可以使用 1 个变通方法。
  2. 上面描述和显示的方法不需要使用证书。可以使用非对称密钥进行相同的设置。证书的好处在于它们更易于移植,因为它们可以备份到文件中,或者您可以从 SQL Server 2012 开始,使用CERTENCODEDCERTPRIVATEKEY函数提取证书及其私钥。这允许在其他数据库甚至其他实例中创建完全相同的证书。当存在跨数据库功能并且您不想启用跨数据库所有权链接和/或TRUSTWORTHY.
  3. 由于这里的任何一个答案中显示的任何测试用例都不是很明显,我将指出这两种方法之间的根本区别(这也恰好是我更喜欢模块签名的原因):
    • 使用会EXECUTE AS 更改当前的安全上下文。这基本上等于在说:我登录/用户A,但就目前而言,请使用登录/用户B的权限INSTEAD OF矿。
    • 使用模块签名将为当前安全上下文添加权限。这基本上等于在说:我登录/用户A,但就目前而言,请使用登录/用户B的权限,除了我的。

**我确实有大部分完整的示例代码(大约完成了 75%),用于审计表上的触发器,该代码将禁止更新除证书签名的代码之外的任何内容,但没有时间完成它。其概念是在此过程中对证书进行锁定,并且锁定条目包括证书 ID。您可以验证证书 ID 是否是所需的证书,ROLLBACK如果不是或没有证书在交易中使用。问题VIEW SERVER STATE是需要使用它来从中创建登录名。然后只授予该登录权限,最后使用相同的证书在审计表上签署触发器(已经在该数据库中,因为它用于在基表上签署触发器)。sys.dm_tran_locks. 然而,这是一个相当容易解决的问题,因为它可以通过基于证书的登录来授予,甚至可以是相同的证书。在这种情况下,证书可以被备份并恢复到masterVIEW SERVER STATE


Pau*_*ite 6

如果您喜欢使用EXECUTE AS(信任可以模拟的用户),另一种方法是:

CREATE TABLE dbo.Test
(
    TestID integer IDENTITY PRIMARY KEY,
    SomeDate datetime NOT NULL
);
GO
CREATE TABLE dbo.TestArchive
(
    TestID integer PRIMARY KEY,
    SomeDate datetime NOT NULL
);
Run Code Online (Sandbox Code Playgroud)

用户

-- Ordinary user with the ability to insert to the Test table
CREATE USER NormalUser WITHOUT LOGIN;
GRANT INSERT ON dbo.Test TO NormalUser;
GRANT SHOWPLAN TO NormalUser; -- Not required, for testing only
GO
-- User used by the trigger to move rows to the Archive table
CREATE USER ArchiveUser WITHOUT LOGIN;
GRANT SHOWPLAN TO ArchiveUser; -- Required if normal users have this permission
GO
-- Give ownership of the Archive table to the Archive user
-- to prevent ownership chaining skipping permission checks
ALTER AUTHORIZATION 
ON OBJECT::dbo.TestArchive
TO ArchiveUser;
Run Code Online (Sandbox Code Playgroud)

扳机

这用于EXECUTE AS作为 ArchiveUser 执行存档

CREATE TRIGGER dbo_Test_AI
ON dbo.Test
WITH EXECUTE AS 'ArchiveUser'
AFTER INSERT
AS
BEGIN
    -- Insert deleted rows
    INSERT dbo.TestArchive
    (
        TestID, 
        SomeDate
    )
    SELECT
        D.TestID, 
        D.SomeDate
    FROM 
    (
        -- Remove rows ready to be archived
        DELETE dbo.Test
        OUTPUT Deleted.TestID, Deleted.SomeDate
        WHERE SomeDate <= DATEADD(DAY, -7, GETUTCDATE())
    ) AS D;
END;
Run Code Online (Sandbox Code Playgroud)

测试

EXECUTE AS USER = 'NormalUser';
GO
-- Able to insert Test rows
INSERT dbo.Test (SomeDate)
VALUES 
    (DATEADD(DAY, -6, GETUTCDATE())),
    (DATEADD(DAY, -5, GETUTCDATE())),
    (DATEADD(DAY, -4, GETUTCDATE())),
    (DATEADD(DAY, -3, GETUTCDATE())),
    (DATEADD(DAY, -2, GETUTCDATE())),
    (DATEADD(DAY, -1, GETUTCDATE()));
GO
-- Able to insert a Test row that gets archived
INSERT dbo.Test (SomeDate)
VALUES 
    (DATEADD(DAY, -7, GETUTCDATE()));
GO
-- Not able to insert to the archive directly
INSERT dbo.TestArchive (TestID, SomeDate)
VALUES (100, GETUTCDATE());
GO
REVERT;
Run Code Online (Sandbox Code Playgroud)

整理

DROP TABLE
    dbo.Test,
    dbo.TestArchive;

DROP USER ArchiveInsert;
DROP USER NormalUser;
Run Code Online (Sandbox Code Playgroud)

请注意,这种安排不会阻止非常有特权的用户直接写入存档表,例如db_datawriter角色的成员或数据库所有者。基于证书的答案也没有。适合您的解决方案完全取决于您当前的权限设置方式、您对各种用户的信任程度以及您的偏执程度。