web-dev-qa-db-ja.com

SQL Serverに、トリガーによってのみ挿入できるテーブルを作成する方法はありますか?

タイトルはかなり自明ですが、なぜ私がこれを必要としているのかについて知りたい場合は、これが必要なのは、アクティブなテーブルの過去の値を保存するアーカイブ/ログテーブルがあり、このためにデータが危険にさらされるリスクを避けたいからです。テーブルに挿入する必要があるのは、アクティブテーブルに変更を記録するために作成したトリガーだけです。まれに、ログテーブルを手動で編集する必要がある場合があります(存在する場合)、「挿入ロック」をオフにします。

SQL Management StudioでSQL Server 2012 Enterpriseを使用しています

5
Tyler Benzing

これは、証明書とモジュール署名を使用して実現できます(つまり、 ADD SIGNATURE )。 EXECUTE ASを介したなりすましの使用は煩雑になる可能性があり、他の誰かが「許可された」ユーザーになりすますか、EXECUTE ASを使用しているモジュールのコンテンツを変更する可能性があります。ただし、モジュール署名:証明書ベースのユーザーを偽装することはできず(最終テストケースを参照)、証明書のパスワードを知らないと、別のモジュールに署名できません。また、署名したモジュール(トリガーなど)を誰かが変更した場合、署名は自動的に削除され、その変更を警告してから、現在の変更で署名を再署名するか、変更を拒否するかを決定できます;-)。

また、トリガーでApplicationName/ProgramNameをトラップすることは、ConnectionStringでその値を渡すのが簡単であるため、信頼できません。

所有権の連鎖を防ぐため、Auditテーブルはメインテーブルとは別のスキーマ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

テスト

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.

[〜#〜]更新[〜#〜]

その他の注意事項:

  1. @Paulが彼の回答で述べたように、このメソッド(示されている)は、特権ユーザーが直接挿入を行うことを妨げません。ただし、監査テーブルのトリガーを介して、証明書で署名されたコードから開始されていないDMLアクションをブロックすることは引き続き可能です。 **。しかし、db_owner固定データベースロールの誰もがトリガーを無効にできるはずであり、db_datawriter固定データベースの誰かが使用できる可能性のある回避策が少なくとも1つあるため、ほとんどの場合、偶発的な挿入が防止されます。彼らがかなり狡猾な場合の役割。
  2. 上記の方法では、証明書を使用する必要はありません。非対称キーを使用して同じ設定を行うことができます。証明書の優れた点は、ファイルにバックアップできるため、移植性が高いことです。SQLServer 2012以降では、 [〜#〜] certencoded [を使用して証明書とその秘密キーを抽出できます。 〜#〜] および [〜#〜] certprivatekey [〜#〜] 関数。これにより、まったく同じ証明書を他のデータベースや他のインスタンスで作成することができます。これは、データベース間の機能があり、データベース間の所有権の連鎖やTRUSTWORTHYを有効にしたくない場合に非常に役立ちます。
  3. ここのいずれかの回答に示されているテストケースからは明らかではないため、2つの方法の根本的な違いを指摘します(これもたまたまモジュール署名を好む理由です):
    • EXECUTE ASを使用すると、現在のセキュリティコンテキストが変更されます。それは本質的に言っています:私はログイン/ユーザーAですが、当面はログイン/ユーザーBINSTEAD OFの権限を使用してください。
    • モジュール署名を使用すると、現在のセキュリティコンテキストにアクセス許可が追加されます。それは本質的に言っています:私はログイン/ユーザーAですが、今のところ、ログイン/ユーザーBの許可を使用してください私に追加してください。

** 証明書によって署名されたコード以外からの更新を許可しない、監査テーブルのトリガーのほぼ完全なサンプルコード(約75%完了)がありますが、それを完了するには時間が足りません。概念は、プロセス中に証明書がロックされ、ロックエントリに証明書IDが含まれることです。証明書IDが目的の証明書であること、およびトランザクションで証明書が使用されていないか、証明書が使用されていない場合はROLLBACKであることを確認できます。問題は、VIEW SERVER STATEを使用するにはsys.dm_tran_locksが必要であることでした。ただし、証明書ベースのログインを介して付与することができるため、解決するのはかなり簡単な問題であり、同じ証明書でもかまいません。その場合、そこからログインを作成するために、証明書をバックアップしてmasterに復元できます。次に、ログインにVIEW SERVER STATE権限を付与し、最後に同じ証明書を使用して監査テーブルのトリガーに署名します(ベーステーブルのトリガーの署名に使用されていたため、そのDBで既に使用されています)。

10
Solomon Rutzky

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
);

ユーザー

-- 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;

引き金

これは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;

テスト

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;

片付ける

DROP TABLE
    dbo.Test,
    dbo.TestArchive;

DROP USER ArchiveInsert;
DROP USER NormalUser;

db_datawriterロールのメンバーやデータベース所有者など、非常に特権のあるユーザーがアーカイブテーブルに直接書き込むことを妨げるものではないことに注意してください。証明書ベースの応答も行いません。適切な解決策は、現在のアクセス許可の設定方法、さまざまなユーザーをどの程度信頼しているか、および妄想のレベルによって異なります。

6
Paul White 9