Cuando dice "sin usar desencadenantes", ¿quiere decir algún desencadenante o solo desencadenantes fila por fila en las tablas?
Pregunto porque puede obtener lo que desea con un uso juicioso de la CONTEXT_INFO()
función, pero deberá asegurarse de que SET CONTEXT_INFO
se haya llamado correctamente antes de que se realicen sus operaciones.
Un lugar para hacerlo podría ser un desencadenador de inicio de sesión a nivel de servidor (es decir, no un desencadenador a nivel de base de datos / objeto), de esta manera:
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
Luego puede agregar la restricción predeterminada a su tabla, para almacenar el contexto (para la velocidad de inserción):
ALTER TABLE cdc.schema_table_CT
ADD ContextInfo varbinary(128) NULL DEFAULT(CONTEXT_INFO())
Una vez que tenga eso, puede consultar esa ContextInfo
columna con un poco de cortar y cortar:
SELECT *
,spid = CONVERT(INT, SUBSTRING(ContextInfo, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(ContextInfo, 5, 64))
FROM cdc.schema_table_CT
Técnicamente, podría hacer eso SUBSTRING
y otras CONVERT
cosas como parte de su restricción predeterminada, y simplemente almacenar la IP del cliente allí, pero puede ser más rápido almacenar todo el contexto allí (como se hace en todos INSERT
), y solo extraer los valores en un SELECT
cuando los necesites
Yo podría estar inclinado para envolver todos mis SUBSTRING
y CONVERT
llamadas en una función con valores de tabla en línea de una sola fila, lo que lo haría CROSS APPLY
cuando sea necesario. Eso mantiene la lógica de desempaque en un solo 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
Tenga en cuenta que CONTEXT_INFO
solo es un byte de 128 VARBINARY
. Si necesita más datos de los que puede caber en 128 bytes, crearía una tabla para contener todos esos datos, inserte como fila para esa 'sesión' en la tabla en el disparador de inicio de sesión y establezca CONTEXT_INFO
el valor de clave sustituto de esa tabla
También debe tener en cuenta que, dado que es solo una restricción predeterminada, es trivial que un usuario con privilegios adecuados sobrescriba esos datos de contexto en la tabla en reposo. Por supuesto, lo mismo es cierto para todas las otras columnas en las tablas de estilo 'auditoría' también.
Sería bueno si pudiera ser una columna computada persistente, en lugar de una predeterminada, pero la CONTEXT_INFO()
función no es determinista, por lo que es un no-go (es posible que pueda usar algunos FUNCTION
trucos alrededor de a VIEW
, pero no lo haría )
También es trivial para ese usuario con acceso suficiente para llamarse a SET CONTEXT_INFO
sí mismo y arruinar su día (por ejemplo, con valores falsos o inyección almacenada especialmente diseñada), así que trate el contenido con sospecha y cuidado, codifíquelo antes de mostrarlo y maneje las excepciones. bien.
En cuanto al nombre de host, creo que el ClientHost
elemento de EVENTDATA()
le da la dirección IP (o un <local machine>
indicador). Si bien técnicamente podría usar CLR para realizar búsquedas de DNS inverso de nuevo al nombre de host, estos tienden a ser demasiado lentos para todos INSERT
, por lo que recomendaría no hacerlo.
Si tiene que tener un nombre de host, es posible que desee utilizar un trabajo de Agente SQL para llenar periódicamente una tabla separada con las concesiones actuales de su servidor DHCP local o archivo de zona DNS, como un proceso fuera de banda, y LEFT JOIN
para eso en consultas futuras (o ajuste en un escalar FUNCTION
para proporcionar un valor a una restricción predeterminada, para un punto en el tiempo).
Nuevamente, debe tener en cuenta que, si la aplicación tiene algún tipo de componente público, las direcciones IP y los nombres de host no son confiables (por ejemplo, debido a NAT). Incluso si no está dirigido al público, hay un cierto componente basado en el tiempo para la mayoría de los mapas de IP / nombre de host, que es posible que deba tener en cuenta.
Finalmente, antes de implementar su desencadenador de inicio de sesión, puede valer la pena activar la conexión de administrador dedicada de su servidor. Si el activador de inicio de sesión se rompe de alguna manera, puede evitar que todos los usuarios inicien sesión (incluidas las cuentas de administrador del sistema):
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
Si queda bloqueado, el DAC se puede usar para soltar o deshabilitar el desencadenador de inicio de sesión:
C:\> sqlcmd -S localhost -d master -A
1> DISABLE TRIGGER tr_audit_login ON ALL SERVER
2> GO