Lorsque vous dites «sans utiliser de déclencheurs», voulez-vous dire des déclencheurs ou simplement des déclencheurs ligne par ligne sur les tables?
Je pose la question parce que vous pourrez peut- être obtenir ce que vous voulez avec une utilisation judicieuse de la CONTEXT_INFO()
fonction, mais vous devrez vous assurer qu'elle a SET CONTEXT_INFO
été appelée correctement avant que vos opérations aient lieu.
Un endroit pour le faire pourrait être un déclencheur d'ouverture de session au niveau du serveur (c'est-à-dire pas un déclencheur au niveau de la base de données / objet), comme ceci:
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
Vous pouvez ensuite ajouter la contrainte par défaut à votre table, pour stocker le contexte (pour la vitesse d'insertion):
ALTER TABLE cdc.schema_table_CT
ADD ContextInfo varbinary(128) NULL DEFAULT(CONTEXT_INFO())
Une fois que vous avez cela, vous pouvez interroger cette ContextInfo
colonne avec un peu de tranches et de dés:
SELECT *
,spid = CONVERT(INT, SUBSTRING(ContextInfo, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(ContextInfo, 5, 64))
FROM cdc.schema_table_CT
Techniquement, vous pouvez faire cela SUBSTRING
et CONVERT
faire des choses dans le cadre de votre contrainte par défaut, et simplement y stocker l'adresse IP du client, mais il peut être plus rapide de stocker le contexte entier là-bas (comme cela se fait sur tous les INSERT
), et d'extraire uniquement les valeurs dans un SELECT
quand vous en avez besoin.
Je pourrais être enclin à envelopper tous mes appels SUBSTRING
et CONVERT
dans une fonction table en ligne à une seule ligne, ce que je ferais CROSS APPLY
si nécessaire. Cela maintient la logique de déballage en un seul endroit:
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
Notez que ce CONTEXT_INFO
n'est qu'un 128 octets VARBINARY
. Si vous avez besoin de plus de données que vous ne pouvez en contenir en 128 octets, je créerais une table pour contenir toutes ces données, insérer comme ligne pour cette `` session '' dans la table dans le déclencheur d'ouverture de session et définir CONTEXT_INFO
la valeur de clé de substitution de cette table
Vous devez également noter que, comme il ne s'agit que d'une contrainte par défaut, il est trivial pour un utilisateur disposant de privilèges appropriés d'écraser ces données de contexte dans la table au repos. Bien sûr, il en va de même pour toutes les autres colonnes des tableaux de style «audit».
Ce serait bien s'il pouvait s'agir d'une colonne calculée persistante, plutôt que d'une valeur par défaut, mais la CONTEXT_INFO()
fonction n'est pas déterministe, donc c'est un non-aller (vous pourriez être en mesure d'utiliser une FUNCTION
ruse autour d'un VIEW
, mais je ne le ferais pas ).
Il est également trivial pour cet utilisateur disposant d'un accès suffisant de s'appeler SET CONTEXT_INFO
et de gâcher votre journée (par exemple avec de fausses valeurs ou une injection stockée spécialement conçue), alors traitez le contenu avec suspicion et soin, codez-le avant de l'afficher et gérez les exceptions bien.
Quant au nom d'hôte, je pense que l' ClientHost
élément de EVENTDATA()
vous donne l'adresse IP (ou un <local machine>
indicateur). Bien que vous puissiez techniquement utiliser CLR pour effectuer des recherches DNS inverses sur le nom d'hôte, celles-ci ont tendance à être trop lentes à faire pour tous INSERT
, donc je recommanderais de ne pas le faire.
Si vous devez avoir un nom d'hôte, vous souhaiterez peut-être utiliser un travail de l'Agent SQL pour remplir périodiquement une table distincte avec les baux actuels de votre serveur DHCP local ou du fichier de zone DNS, en tant que processus hors bande, et LEFT JOIN
à cela dans futures requêtes (ou encapsuler dans un scalaire FUNCTION
pour fournir une valeur à une contrainte par défaut, pour un point dans le temps).
Encore une fois, vous devez noter que, si l'application a tout type de composant accessible au public, les adresses IP et les noms d'hôte ne sont pas fiables (par exemple en raison de NAT). Même si elle n'est pas accessible au public, la plupart des mappages IP / noms d'hôte doivent prendre en compte un certain temps.
Enfin, avant d'implémenter votre déclencheur de connexion, il peut être utile d'activer la connexion d'administration dédiée de votre serveur. Si le déclencheur de connexion est interrompu de quelque façon que ce soit, il peut empêcher tous les utilisateurs de se connecter (y compris les comptes 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
Si vous êtes verrouillé, le DAC peut être utilisé pour supprimer ou désactiver le déclencheur de connexion:
C:\> sqlcmd -S localhost -d master -A
1> DISABLE TRIGGER tr_audit_login ON ALL SERVER
2> GO