Estou trabalhando em um aplicativo que possui vários módulos herdados que dependem fortemente do procedimento armazenado (sem ORM, portanto, todas as buscas e persistência de dados são feitas por meio de procedimentos armazenados).
A segurança dos módulos legados depende SUSER_NAME()
de obter o usuário atual e aplicar regras de segurança.
Estou migrando para usar um ORM (Entity Framework) e o conector SQL usará um usuário genérico para se conectar ao banco de dados (SQL Server), então tenho que fornecer o nome de usuário atual para muitos procedimentos.
Para evitar alterações no código .NET, pensei em "injetar" de alguma forma o usuário atual no contexto quando uma nova conexão é feita:
CREATE TABLE dbo.ConnectionContextInfo
(
ConnectionContextInfoId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_ConnectionContextInfo PRIMARY KEY,
Created DATETIME2 NOT NULL CONSTRAINT DF_ConnectionContextInfo DEFAULT(GETDATE()),
SPID INT NOT NULL,
AttributeName VARCHAR(32) NOT NULL,
AttributeValue VARCHAR(250) NULL,
CONSTRAINT UQ_ConnectionContextInfo_Info UNIQUE(SPID, AttributeName)
)
GO
Quando uma conexão é aberta (ou reutilizada, como um pool de conexão é usado), o seguinte comando é usado:
exec sp_executesql N'
DELETE FROM dbo.ConnectionContextInfo WHERE SPID = @@SPID AND AttributeName = @UsernameAttribute;
INSERT INTO dbo.ConnectionContextInfo (SPID, AttributeName, AttributeValue) VALUES (@@SPID, @UsernameAttribute, @Username);
',N'@UsernameAttribute nvarchar(8),@Username nvarchar(16)',@UsernameAttribute=N'Username',@Username=N'domain\username'
go
(0 CPU, ~15 leituras, <6 ms)
Uma função escalar permite obter facilmente o usuário atual:
alter FUNCTION dbo.getCurrentUser()
RETURNS VARCHAR(250)
AS
BEGIN
DECLARE @ret VARCHAR(250) = (SELECT AttributeValue FROM ConnectionContextInfo where SPID = @@SPID AND AttributeName = 'Username')
-- fallback to session current, if no data is found on current SPID (i.e. call outside of the actual application)
RETURN ISNULL(@ret, SUSER_NAME())
END
GO
Existem ressalvas (robustez, desempenho, etc.) nessa abordagem do ponto de vista da camada de dados?
Obrigado.
Em termos de desempenho, você incorrerá na sobrecarga do
DELETE
eINSERT
toda vez que a conexão for aberta. Como alternativa, você pode usar a conexão interna CONTEXT_INFO para essa finalidade. O exemplo abaixo armazena as informações em uma estrutura de 48 bytes de comprimento fixo.Além disso, sp_set_session_context e SESSION_CONTEXT() estão disponíveis no SQL Server 2016 e no Banco de Dados SQL do Azure. Esse é um método muito mais limpo, se disponível para você.
Sugiro pré-alocar linhas na tabela de contexto de uma forma que ajude a minimizar a contenção da página.
Esta é uma das poucas vezes em que realmente recomendo usar um GUID gerado aleatoriamente como a chave de clustering da tabela. Essa chave atuará como um randomizador para o local da página para qualquer SPID específico para reduzir a contenção da página.
Isso preencherá previamente a tabela com as linhas necessárias, uma por combinação de spid/atributo.
Se você realmente precisa usar
sp_executesql
, eu faria assim:Os resultados para o meu spid:
Em vez de usar
sp_executesql
, recomendo usar um procedimento armazenado para fazer a atualização, para que você possa incluir facilmente algum tratamento de erros e atualizar livremente esse código no lado do servidor sem afetar o cliente. Por exemplo:Isso definirá @RetVal como 0 se o
@AttributeName
passado for inválido:Eu posso ver três problemas menores e um grande problema com esta abordagem:
Problemas menores:
Sua
DELETE
declaração em sua consulta ad hoc usa o seguinte predicado:Em vez disso, você deve apenas filtrar,
SPID = @@SPID
pois não deseja valores obsoletos da última instância desse SPID por aí, misturados com seus valores atuais.Em sua
ConnectionContextInfo
tabela, ambas asAttribute%
colunas são definidas comoVARCHAR
, mas na consulta ad hoc você tem os parâmetros definidos comoNVARCHAR
e até prefixa as strings comN
. Você deve atualizar a tabela para que também seja definida comoNVARCHAR
.Dados obsoletos de valores SPID mais altos que são inseridos durante os horários de pico de uso permanecerão por algum tempo, pois não haverá
DELETE
chamada até a próxima vez que o SPID for usado, o que pode ser nunca. Você pode criar um trabalho do SQL Server Agent para ser executado uma vez por dia eDELETE
linhas criadas há X dias.Problema principal (e uma solução):
Com base no comentário que você fez na resposta de @Dan, você não pode usar
CONTEXT_INFO
devido ao tamanho que limita sua capacidade de adicionar mais atributos no futuro, especialmente se estiver usandoNVARCHAR
.Felizmente, você não precisa de uma tabela permanente. Você pode simplificar isso um pouco usando uma tabela temporária global. Isso eliminaria a necessidade de
DELETE
linhas anteriores e provavelmente faria com que um trabalho do SQL Agent limpasse registros obsoletos OU pré-alocasse várias linhas, mas ainda precisaria atualizar todas as linhas correspondentes a esse SPID para uma string vazia ouNULL
por cada conexão.Ao estabelecer a conexão, basta criar a tabela. Em seguida, insira quaisquer pares de chave/valor que desejar. A tabela será descartada automaticamente quando a conexão for fechada (conexões não agrupadas e agrupadas) ou quando a próxima sessão a reutilizar essa conexão executar sua primeira instrução e o
sp_reset_connection
processo interno for executado (conexões agrupadas).Agora, você deve estar se perguntando:
A tabela temporária não será limpa quando o procedimento armazenado (ou consulta ad hoc) terminar?
Se fosse uma tabela temporária local (ou seja
#Name
, ), então sim, ela seria limpa quando o processo/subprocesso em que foi criado terminar e não estará disponível no contexto pai. Mas as Tabelas Temporárias Globais (ou seja,##Name
) sobrevivem ao final do processo em que foram criadas e estão disponíveis no contexto pai.Como as tabelas temporárias são globais, elas não podem compartilhar o mesmo nome.
Correto, usar um nome de tabela padrão na
CREATE TABLE
instrução não funcionará porque várias sessões entrarão em conflito umas com as outras. Precisamos apenas de uma maneira de diferenciar o nome da tabela usando algo disponível para a Session, exclusivo da Session, mas não passado do aplicativo porque o código funcionaria apenas do aplicativo e o objetivo é não alterar o código do aplicativo. Portanto, basta anexar o@@SPID
valor a um prefixo fixo conhecido e, em seguida, você pode inferir o nome da tabela em qualquer ponto dessa sessão.Algo como:
Ao contrário da UDF mostrada na pergunta, essa abordagem requer SQL dinâmico e acesso a tabelas temporárias, nenhuma das quais é permitida.
Correto, nenhum deles pode ser feito em funções T-SQL, mas podem ser feitos de duas outras maneiras:
OUTPUT
parâmetro ou"Context Connection = true"
, e o assembly pode ser marcado comoWITH PERMISSION_SET = SAFE
. SQLCLR UDFs podem executar SQL dinâmico e acessar tabelas temporárias locais.