Logon-триггеры, которые появились в MS SQL 2005 предоставляют удобный механизм для логирования подключений. Подключения могут протоколироваться и средствами профайлера и другими методами слежения за процессами. Однако Logon-триггеры превратили процесс подключения в транзакцию, которую при желании можно откатить. Например, можно выполнять проверки для разрешения или запрета на подключение к серверу.
Приведем такой пример. Создадим базу данных для хранения настроек и логов:
use master
go
if db_id ( N'LogStore' ) is null
begin
create database LogStore
name = LogDataLog,
filename = N'C:\Users\В\Desktop\LogDataLog.ldf',
size = 100 Mb,
filegrowth = 100 Mb,
go
begin
create database LogStore
on primary
(
name = LogData,
filename = N'C:\Users\В\Desktop\LogData.mdf',
size = 100 Mb,
filegrowth = 100 Mb,
maxsize = unlimited
)
log on
(name = LogDataLog,
filename = N'C:\Users\В\Desktop\LogDataLog.ldf',
size = 100 Mb,
filegrowth = 100 Mb,
maxsize = unlimited
)
end
go
Создадим таблицу, где будут перечислены компьютеры, с которых запрещено подключаться к серверу:
use LogStore
go
if object_id ( N'dbo.ExHosts', N'U' ) is null
use master
with
password = N'qw3@',
check_policy = off,
default_database = LogStore,
default_language = russian
end
use LogStore
from login LogLogin
with default_schema = dbo
end
authorization dbo
grant select on object::dbo.ExHosts to LogRole
alter role LogRole add member LogUser
use master
if not exists
(
select *
from sys.server_triggers
where name = N'CheckHost'
)
begin
exec sp_executesql N'create trigger CheckHost on all server for logon as return'
alter trigger CheckHost on all server
with exec as 'LogLogin'
for logon
(
select *
from LogStore.dbo.ExHosts
where vcHost = isnull ( host_name (), N'' )
)
begin
; throw 60000, N'Попытка входа с запрещенного хоста.', 1
if @@trancount > 0
begin
rollback tran
end
end
end
go
go
begin
create table dbo.ExHosts
(
iHostId int identity ( 1, 1 ) not null,
vcHost nvarchar ( 200 ) not null,
constraint PK_ExHosts_iHostId primary key clustered ( iHostId asc ) on [primary],
constraint AK_ExHosts_vcHost unique nonclustered ( vcHost asc ) on [primary]
)
end
go
Логон-триггер может проверять компьютер соединения и откатывать транзакцию, если окажется, что в таблице есть строка с таким компьютером. Перед созданием триггера надо определиться с тем под какой учетной записью он будет работать (ведь не стоит давать всем доступ на базу и на таблицу). Создадим такую учетную запись и предоставим ей права:
go
if suser_id ( N'LogLogin' ) is null
begin
create login LogLogin
end
go
go
if database_principal_id ( N'LogUser' ) is null
begin
create user LogUser
end
go
if database_principal_id ( N'LogRole' ) is null
begin
create role LogRole
end
go
go
go
Теперь можно создать триггер на вход:
go
(
select *
)
begin
exec sp_executesql N'create trigger CheckHost on all server for logon as return'
end
go
as
begin
set nocount, xact_abort on
if existsend
go
При попытке зайти на сервер с запрещенного компьютера произойдет ошибка. Например, если попытаться сделать такое подключение в Management Studio, то появится сообщение об ошибке:
Текста сообщения об ошибке, написанного в коде триггера, здесь не видно. Но такие ошибки попадают в журнал сервера, где их можно обнаружить:
Казалось бы все неплохо. Но при работе с logon-триггерами есть один подводный камень, состоящий в следующем. Код в таких триггерах выполняется в контексте неявной транзакции, опция xact_abort всегда включена. Любое исключение, возникающее из-за процессов, которые не предусмотрел разработчик, приводит к откату этой транзакции и, как следствие, невозможности подключения к серверу. В худшем случае, когда ошибка происходит при любой попытке входа, создается ситуация, когда никто не может подключиться к серверу. Никакая обработка ошибок, например, с помощью try/catch, не поможет, поскольку, как было отмечено выше, транзакция уже откатилась, и ничто не поможет. В этой ситуации можно входить на сервер используя некоторые специальные режимы. Например, режим приоритетного соединения администратора позволяет зайти на сервер, поскольку на такое соединение триггеры не действуют. Для этого требуется ввести в командной строке: sqlcmd -S <имя сервера> -E -A. После чего требуется ввести текст команды: disable trigger CheckHost on all server. Вместо командной строки можно использовать и Management Studio, если в окне подключения перед именем сервера написать: admin:. Возможно, также потребуется произвести эти действия с серверного компьютера (если отключена опция remote admin connection).
Хотел бы предложить подход, который минимизирует вероятность ошибок в работе триггера. Во-первых, логика работы кода должна быть как можно более простой. Пусть это будут простые проверки. Если требуются более сложные способы обработки данных, не связанные с проверкой возможности подключения, то лучше вынести их в другие, нетранзакционные, средства отслеживания событий. Во-вторых, нужно проанализировать все выполняемые в триггере действия по порядку. В нашем случае они такие:
1. Триггер работает от имени логина LogLogin. Пока триггер работает в его контексте удалить логин не получится. Здесь все в порядке.
2. Делается попытка просмотра объекта в базе данных LogStore. Значит, чтобы все было хорошо, требуется, чтобы существовала база данных с таким именем, она должна быть доступной, и текущая учетная запись должна иметь права на подключение к ней.
3. Производится попытка запроса к таблице dbo.ExHosts. Для ее успеха требуется наличие таблицы с таким именем и права на запросы к ней для текущей учетной записи.
4. В запросе к таблице делается условие на столбец. Поэтому требуется удостовериться, что у заданной таблицы существует столбец с таким именем и что его тип данных такой какой мы ожидаем: nvarchar.
Итак, вставим в текст триггера все эти проверки, сделав его работу максимально безопасной. Ниже идет полный код с моими комментариями.
alter trigger CheckHost on all server
with exec as 'LogLogin'
for logon
as
begin
set nocount, xact_abort on
-- проверим возможность выполнения запросов к системныму каталогу с перечнем баз данных
if isnull ( has_perms_by_name ( 'sys.databases', 'object', 'select', null, null ), 0 ) = 0
begin
return
end
-- существование базы данных с заданным именем в состоянии online
if not exists
(
select *
from sys.databases
where name = 'LogStore' and state_desc = 'ONLINE'
)
begin
return
end
-- возможность подключения к базе данных
if isnull ( has_perms_by_name ( 'LogStore', 'database', 'connect', null, null ), 0 ) = 0
begin
return
end
-- наличие таблицы
if object_id ( N'LogStore.dbo.ExHosts', N'U' ) is null
begin
return
end
-- наличие доступов на select к таблице (эта функция корректно учтет наличие несколько доступов и запретов)
if isnull ( has_perms_by_name ( 'LogStore.dbo.ExHosts', 'object', 'select', null, null ), 0 ) = 0
begin
return
end
-- проверка существования столбца с определенным именем
declare @vcSql nvarchar ( max ), @iColId int
set @vcSql = N'use LogStore set @iColId = columnproperty ( object_id ( N''dbo.ExHosts'', N''U'' ), N''vcHost'', N''ColumnId'' )'
exec sp_executesql @vcSql, N'@iColId int out', @iColId = @iColId out
if @iColId is null
begin
return
end
-- возможность выполнения запросов в системному представлению с перечнем столбцов
declare @bPerm int
set @vcSql = N'use LogStore set @bPerm = has_perms_by_name ( ''LogStore.sys.columns'', ''object'', ''select'', null, null )'
exec sp_executesql @vcSql, N'@bPerm int out', @bPerm = @bPerm out
if @bPerm = 0
begin
return
end
-- наличие у столбца нужного типа данных
if not exists
(
select *
from LogStore.sys.columns
where
name = 'vcHost' and
object_id = object_id ( N'LogStore.dbo.ExHosts', N'U' ) and
type_name ( system_type_id ) = 'nvarchar' and
type_name ( user_type_id ) = 'nvarchar' and
max_length = 400
)
begin
return
end
if exists
(
select *
from LogStore.dbo.ExHosts
where vcHost = isnull ( host_name (), N'' )
)
begin
; throw 60000, N'Попытка входа с запрещенного компьютера.', 1
if @@trancount > 0
begin
rollback tran
end
end
end
go
В коде делается проверка возможности всех действий, которые выполняются при проверке законности входа. Причем, если для проверки возможности действия, происходит обращение к системному объекту, то предварительно делается проверка возможности обращения к системному объекту. Теперь, если, скажем, изменить имя столбца (exec LogStore..sp_rename 'dbo.ExHosts.vcHost', 'vcName', 'column') или сделать запрет для учетной записи на запросы к таблице (exec ( N'use LogStore deny select on object::dbo.ExHosts to LogUser' )) или вообще удалить учетную запись, триггер спокойно продолжит свою работу. В этой ситуации он, конечно, не сможет выполнять свои прямые обязанности по проверке законности подключений. Но в большинстве случаев это лучше чем совсем никого не пускать.
with exec as 'LogLogin'
for logon
as
begin
set nocount, xact_abort on
-- проверим возможность выполнения запросов к системныму каталогу с перечнем баз данных
if isnull ( has_perms_by_name ( 'sys.databases', 'object', 'select', null, null ), 0 ) = 0
begin
return
end
-- существование базы данных с заданным именем в состоянии online
if not exists
(
select *
from sys.databases
where name = 'LogStore' and state_desc = 'ONLINE'
)
begin
return
end
-- возможность подключения к базе данных
if isnull ( has_perms_by_name ( 'LogStore', 'database', 'connect', null, null ), 0 ) = 0
begin
return
end
-- наличие таблицы
if object_id ( N'LogStore.dbo.ExHosts', N'U' ) is null
begin
return
end
-- наличие доступов на select к таблице (эта функция корректно учтет наличие несколько доступов и запретов)
if isnull ( has_perms_by_name ( 'LogStore.dbo.ExHosts', 'object', 'select', null, null ), 0 ) = 0
begin
return
end
-- проверка существования столбца с определенным именем
declare @vcSql nvarchar ( max ), @iColId int
set @vcSql = N'use LogStore set @iColId = columnproperty ( object_id ( N''dbo.ExHosts'', N''U'' ), N''vcHost'', N''ColumnId'' )'
exec sp_executesql @vcSql, N'@iColId int out', @iColId = @iColId out
if @iColId is null
begin
return
end
-- возможность выполнения запросов в системному представлению с перечнем столбцов
declare @bPerm int
set @vcSql = N'use LogStore set @bPerm = has_perms_by_name ( ''LogStore.sys.columns'', ''object'', ''select'', null, null )'
exec sp_executesql @vcSql, N'@bPerm int out', @bPerm = @bPerm out
if @bPerm = 0
begin
return
end
-- наличие у столбца нужного типа данных
if not exists
(
select *
from LogStore.sys.columns
where
name = 'vcHost' and
object_id = object_id ( N'LogStore.dbo.ExHosts', N'U' ) and
type_name ( system_type_id ) = 'nvarchar' and
type_name ( user_type_id ) = 'nvarchar' and
max_length = 400
)
begin
return
end
if exists
(
select *
from LogStore.dbo.ExHosts
where vcHost = isnull ( host_name (), N'' )
)
begin
; throw 60000, N'Попытка входа с запрещенного компьютера.', 1
if @@trancount > 0
begin
rollback tran
end
end
end
go
В коде делается проверка возможности всех действий, которые выполняются при проверке законности входа. Причем, если для проверки возможности действия, происходит обращение к системному объекту, то предварительно делается проверка возможности обращения к системному объекту. Теперь, если, скажем, изменить имя столбца (exec LogStore..sp_rename 'dbo.ExHosts.vcHost', 'vcName', 'column') или сделать запрет для учетной записи на запросы к таблице (exec ( N'use LogStore deny select on object::dbo.ExHosts to LogUser' )) или вообще удалить учетную запись, триггер спокойно продолжит свою работу. В этой ситуации он, конечно, не сможет выполнять свои прямые обязанности по проверке законности подключений. Но в большинстве случаев это лучше чем совсем никого не пускать.
Комментариев нет:
Отправить комментарий