Quando você diz, "sem o uso de gatilhos", que quer dizer qualquer gatilhos ou apenas remar-by-gatilhos de linha em tabelas?
Eu pergunto, porque você pode conseguir o que deseja com o uso criterioso da CONTEXT_INFO()
função, mas precisaria garantir que a SET CONTEXT_INFO
chamada fosse correta antes das operações.
Um lugar para fazer isso pode ser um gatilho de logon no nível do servidor (ou seja, não um gatilho no banco de dados / no objeto), como:
USE master
GO
CREATE TRIGGER tr_audit_login
ON ALL SERVER
WITH EXECUTE AS 'sa'
AFTER LOGON
AS BEGIN
BEGIN TRY
DECLARE @eventdata XML = EVENTDATA();
IF @eventdata IS NOT NULL BEGIN
DECLARE @spid INT;
DECLARE @client_host VARCHAR(64);
SET @client_host = @eventdata.value('(/EVENT_INSTANCE/ClientHost)[1]', 'VARCHAR(64)');
SET @spid = @eventdata.value('(/EVENT_INSTANCE/SPID)[1]', 'INT');
-- pack the required data into the context data binary
-- (spid is just an example of packing multiple data items in a single field: you would probably use @@SPID at the point of use, instead)
DECLARE @context_data VARBINARY(128);
SET @context_data = CONVERT(VARBINARY(4), @spid)
+ CONVERT(VARBINARY(64), @client_host);
-- persist the spid and host into session-level memory
SET CONTEXT_INFO @context_data;
END
END TRY
BEGIN CATCH
/* do better error handling here...
* logon trigger can lock all users out of server, so i am just swallowing everything
*/
DECLARE @msg NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR('%s', 10, 1, @msg) WITH LOG;
END CATCH
END
Você pode adicionar a restrição padrão à sua tabela, para armazenar o contexto (para velocidade de inserção):
ALTER TABLE cdc.schema_table_CT
ADD ContextInfo varbinary(128) NULL DEFAULT(CONTEXT_INFO())
Depois disso, você pode consultar essa ContextInfo
coluna com um pouco de fatia e dados:
SELECT *
,spid = CONVERT(INT, SUBSTRING(ContextInfo, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(ContextInfo, 5, 64))
FROM cdc.schema_table_CT
Tecnicamente, você pode fazer isso SUBSTRING
e CONVERT
outras coisas como parte de sua restrição padrão e apenas armazenar o IP do cliente lá, mas pode ser mais rápido armazenar todo o contexto lá (como é feito em todos INSERT
) e extrair apenas os valores em um SELECT
quando você precisar deles.
Eu posso estar inclinado a agrupar todas as minhas chamadas SUBSTRING
e CONVERT
em uma função com valor de tabela em linha de linha única, o que faria CROSS APPLY
quando necessário. Isso mantém a lógica de descompactação em um só lugar:
CREATE FUNCTION fn_context (
@context_info VARBINARY(128)
)
RETURNS TABLE
AS RETURN (
SELECT
spid = CONVERT(INT, SUBSTRING(@context_info, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(@context_info, 5, 64))
)
GO
SELECT *
FROM cdc.schema_table_CT s
CROSS APPLY dbo.fn_context(s.ContextInfo) c
Observe que CONTEXT_INFO
é apenas um de 128 bytes VARBINARY
. Se você precisar de mais dados do que pode caber em 128 bytes, eu criaria uma tabela para armazenar todos esses dados, insira como linha para essa 'sessão' na tabela no gatilho de logon e defina CONTEXT_INFO
o valor da chave substituta dessa tabela
Você também deve observar que, como é apenas uma restrição padrão, é trivial para um usuário com privilégios adequados substituir esses dados de contexto na tabela em repouso. Obviamente, o mesmo vale para todas as outras colunas nas tabelas no estilo 'audit'.
Seria bom se pudesse ser uma coluna computada persistente, em vez de um padrão, mas a CONTEXT_INFO()
função é não determinística, por isso é um não-go (você pode usar alguns FUNCTION
truques em torno de um VIEW
, mas eu não )
Também é trivial para esse usuário com acesso suficiente se chamar SET CONTEXT_INFO
e estragar o seu dia (por exemplo, com valores falsos ou injeção armazenada especialmente criada), portanto, trate o conteúdo com suspeita e cuidado, codifique-o antes da exibição e lide com exceções bem.
Quanto ao nome do host, acho que o ClientHost
elemento EVENTDATA()
fornece o endereço IP (ou um <local machine>
indicador). Embora você possa tecnicamente usar o CLR para fazer pesquisas de DNS reverso no nome do host, elas tendem a ser muito lentas para todos INSERT
, por isso recomendo que não o façam.
Se você precisar ter um nome de host, convém usar uma tarefa do SQL Agent para preencher periodicamente uma tabela separada com as concessões atuais do servidor DHCP local ou do arquivo de zona DNS, como um processo fora de banda, e LEFT JOIN
para isso em consultas futuras (ou agrupe em um escalar FUNCTION
para fornecer um valor a uma restrição padrão, para um determinado momento).
Novamente, você deve observar que, se o aplicativo tiver algum tipo de componente voltado para o público, os endereços IP e os nomes de host não são confiáveis (por exemplo, devido ao NAT). Mesmo que não seja voltado para o público, há um determinado componente baseado em tempo na maioria dos mapas de IP / nome de host, que talvez você precise levar em consideração.
Por fim, antes de implementar seu gatilho de login, pode valer a pena ativar a conexão administrativa dedicada do servidor. Se o gatilho de login for interrompido de alguma forma, poderá impedir que todos os usuários efetuem login (incluindo as contas sysadmin):
USE master
GO
-- you may want to do this, so you have a back-out if the login trigger breaks login
EXEC sp_configure 'remote admin connections', 1
GO
RECONFIGURE
GO
Se você ficar bloqueado, o DAC pode ser usado para descartar ou desativar o gatilho de login:
C:\> sqlcmd -S localhost -d master -A
1> DISABLE TRIGGER tr_audit_login ON ALL SERVER
2> GO