Using SQL Server 2008R2
I am working on a LOGON trigger to capture users who have logged in. I have created a trigger, but it is capturing the SQL Agent account and is inserting it like mad. I would like to exclude it from the list, but I'm not sure how to modify the trigger to make it so. The different attempts locked me out and I had to use DAC to drop the trigger.
The original code:
USE B_DBA;
GO
CREATE TRIGGER LogonTrigger ON ALL SERVER FOR LOGON
AS
BEGIN
IF SUSER_SNAME() <> 'sa'
INSERT INTO B_DBA.dbo.LogonAudit (UserName, LogonDate, spid)
VALUES (SUSER_SNAME(), GETDATE(), ##SPID)
END;
GO
ENABLE TRIGGER LogonTrigger ON ALL SERVER;
What I'd like to add is basically a where clause, but I haven't been able to add it successfully and it would not work on the IF section.
Where not in (Select service_account from sys.dm_server_services)
Any help would be appreciated.
Untested, but it seems like you should be able to modify your IF statement to check sys.dm_server_services:
USE B_DBA;
GO
CREATE TRIGGER LogonTrigger ON ALL SERVER FOR LOGON
AS
BEGIN
IF SUSER_SNAME() <> 'sa'
AND NOT EXISTS (SELECT service_account FROM sys.dm_server_services WHERE service_account = SUSER_SNAME())
INSERT INTO B_DBA.dbo.LogonAudit (UserName, LogonDate, spid)
VALUES (SUSER_SNAME(), GETDATE(), ##SPID)
END;
GO
ENABLE TRIGGER LogonTrigger ON ALL SERVER;
Related
I have the following issue:
I have two different databases, db1 and db2. I have an application that loads data into db2 or db3. db1 has a few tables that the application uses to determine behavior, including which db the application should load data into.
Users need to have write access to db1 to operate the application (there is a console application that writes to tables in db1, operating with windows authentication).
Users should not have DML privileges to db2 and db3, with the exception of a few predetermined operations. We grant AD groups database roles to control access from and organization perspective. Specifically, I'm trying to build a stored procedure in db1 that operators can use to reverse data loaded to db2 or db3 with appropriate logging.
I'm attempting to use create proc ... execute as owner to accomplish this, but it does not seem to be working when I try to hit tables in db2/db3 (I'm thinking that "execute as owner" operates on db level users an not server level logins?). The following causes a permission error stating that the owner (myself) does not have permissions to db2/db3.
use db1
go
create proc dbo.wrapper #recordid int
as begin
/*
capturing user
*/
declare #usr varchar(255) = SUSER_SNAME()
exec dbo.inner #usr , #recordid
end
use db1
go
create proc dbo.inner #usr varchar(255), #recordid int
with execute as owner
as begin
/*
logic to determine whether to update db2 or db3 goes here
*/
insert db2.rolled_back
select * , #usr from db2.transactions where id = #recordid
delete from db2.transactions where id = #recordid
insert db3.rolled_back
select * , #usr from db3.transactions where id = #recordid
delete from db3.transactions where id = #recordid
end
Is there a way to get this to work? I've heard that certificate signing could do this, does anyone have any experience using certificate users. Our DBA's would rather not have to maintain certificates, so if there is a way to get this to work without certificates that would be best.
Any advice would be helpful.
Thank You!
I'm going to cover the cross database chaining side of thing here. note that there are certainly security considerations when using this method. For example someone with permissions to create objects in one database can give themselves access to data in another database with the owner, when they themselves have no access to the other database. The security concerns, however, are out of scope of this answer.
Firstly, let's create a couple of test databases.
USE master;
GO
CREATE DATABASE Chain1;
CREATE DATABASE Chain2;
Now I'm going to CREATE a LOGIN, which is disable and make that the owner of these 2 databases. The databases having the same owner is important, as otherwise the chaining won't work.
CREATE LOGIN ChainerOwner WITH PASSWORD = N'SomeSecurePassword123';
ALTER LOGIN ChainerOwner DISABLE;
GO
ALTER AUTHORIZATION ON DATABASE::Chain1 TO ChainerOwner;
ALTER AUTHORIZATION ON DATABASE::Chain2 TO ChainerOwner;
I'm also going to create a LOGIN which we're going to use to test on:
CREATE LOGIN SomeUser WITH PASSWORD = N'SomeSecurePassword123';
Great, now I can create a few objects; a table in Chain1, a PROCEDURE in Chain2 that accesses the TABLE, and a USER in both databases for SomeUser. In Chain1 the USER will be given no permissions, and in Chain2 the user will be given the permsision to EXECUTE the PROCEDURE:
USE Chain1;
GO
CREATE TABLE dbo.SomeTable (I int IDENTITY,
S varchar(10));
INSERT INTO dbo.SomeTable (S)
VALUES ('abc'),
('xyz');
GO
CREATE USER SomeUser FOR LOGIN SomeUser;
GO
USE Chain2;
GO
CREATE PROC dbo.CrossDBProc #I int AS
BEGIN
SELECT I,
S
FROM Chain1.dbo.SomeTable
WHERE I = #I;
END;
GO
CREATE USER SomeUser FOR LOGIN SomeUser;
GO
GRANT EXECUTE ON dbo.CrossDBProc TO SomeUser;
GO
Great, all the objects are created, now let's try to EXECUTE that PROCEDURE:
EXECUTE AS LOGIN = 'SomeUser';
GO
EXEC dbo.CrossDBProc 1; --This fails
GO
REVERT;
GO
This fails, with a permission error:
The SELECT permission was denied on the object 'SomeTable', database 'Chain1', schema 'dbo'.
This is expected, as there is no ownership chaining. let's, therefore enable that now.
USE master;
GO
ALTER DATABASE Chain1 SET DB_CHAINING ON;
ALTER DATABASE Chain2 SET DB_CHAINING ON;
Now if I try the same again, the same SQL works:
USE Chain2;
GO
EXECUTE AS LOGIN = 'SomeUser';
GO
EXEC dbo.CrossDBProc 1; --This now works
GO
REVERT;
GO
This successfully returns the result set
I
S
1
abc
So, yes you can chain cross database, but it requires some set up, and (again) there are security considerations you need think about.
Clean up:
USE master;
GO
DROP DATABASE Chain1;
DROP DATABASE Chain2;
GO
DROP LOGIN ChainerOwner;
DROP LOGIN SomeUser;
I have a trigger in "Contracts" and I also have a table called "Audits" (self explanatory).
Everything is working fine. If I insert, edit or delete, a row is inserted into Audits table by the trigger...
The problem here is that Trigger does not accept parameters... and I have a table column called "TriggeredBy" inside of the Audits table... which is supposed to have the User's ID (whoever did the insert, delete or UPDATE).
Is there a workaround that I can use so I can pass that value to that trigger?
If you have the db connection opened for the duration of the application, you can keep track of who is associated with the current db session by having a table with session if, user id.
SessionId int,
UserId varchar(20)
At login time, use ##SPID to store the session ID and associated user.
The trigger can then use ##SPID and retrieve the user ID from the table and insert it into the log table.
Option 2:
Use an application role. Allow users to connect to SQL server database using Windows Integrated Security. Call sp_setapprole to set the role. Users should be given no access to any table. The app role should have insert update delete.
You can now determine the user in your trigger.
If the desktop application used Windows authentication, you could simply use ORIGINAL_LOGIN() or SUSER_SNAME() to get the end user account name in trigger code.
With a shared SQL login, one method is to store the end user name in SQL session context for use by the trigger. Session context allows you to store name/value pairs using the sp_set_session_context procedure and read current session values with the SESSION_CONTEXT function. Call sp_set_session_context with the current user name after opening a new SQL connection so that it can be used by triggers to identify the end user.
Example T-SQL code below. Also, see this answer for other methods to set/use session level values.
CREATE TRIGGER TR_YourTable
ON dbo.YourTable
FOR INSERT, UPDATE, DELETE
AS
DECLARE #TriggeredBy sysname = COALESCE(CAST(SESSION_CONTEXT(N'end-user-name') AS sysname), N'unknown');
IF EXISTS(SELECT 1 FROM inserted) AND EXISTS(SELECT 1 FROM deleted)
BEGIN
INSERT INTO dbo.YourAuditTable (Action, SomeColumn, TriggeredBy)
SELECT 'updated', SomeColumn, #TriggeredBy
FROM deleted;
END
ELSE
BEGIN
IF EXISTS(SELECT 1 FROM inserted)
BEGIN
INSERT INTO dbo.YourAuditTable (Action, SomeColumn, TriggeredBy)
SELECT 'inserted', SomeColumn, #TriggeredBy
FROM inserted;
END
ELSE
BEGIN
INSERT INTO dbo.YourAuditTable (Action, SomeColumn, TriggeredBy)
SELECT 'deleted', SomeColumn, #TriggeredBy
FROM deleted;
END;
END;
GO
--Example T-SQL usage. Queries should be parameterized in application code.
EXEC sp_set_session_context N'end-user-name', N'me';
INSERT INTO dbo.YourTable (SomeColumn) VALUES('example');
GO
I have a below DDL Trigger on my server
CREATE TRIGGER [DDLForLogin] ON ALL SERVER
FOR LOGON
AS BEGIN
DECLARE #data XML
SET #data = EVENTDATA()
IF EXISTS(SELECT * FROM sys.Databases WHERE NAME = 'DatabaseMaintenance') Begin
INSERT INTO DatabaseMaintenance.dbo.MyTable (UserName, HostName, ApplicationName, EventDataValue)
VALUES (CURRENT_USER, HOST_Name(), APP_NAME(),#Data)
END
END;
a login as Windows Authentication insert one row on MyTable but a login as SQL Server Authentication raised below error :
Logon failed for login 'xxx' due to trigger execution.
Changed database context to 'master'.
Changed language setting to us_english. (Microsofr SQL Server, Error: 17892)
EDIT
I GRANT INSERT on my table to PUBLIC.
but no change on raised error.
EDIT2
I change my trigger and add With Execute AS 'sa' on the trigger
but no change on raised error.
Try adding an appropriate EXECUTE AS clause to your trigger - the default is CALLER, so unless that user has permissions to insert into your audit table1, the trigger will fail.
Also, then use ORIGINAL_LOGIN() in the trigger to get the correct login information
1 Which you normally don't want - because otherwise every user can forge entries alleging that other users have logged in.
Must change trigger to below code:
CREATE TRIGGER [DDLForLogin] ON ALL SERVER
WITH EXECUTE AS 'sa'
FOR LOGON
AS BEGIN
DECLARE #data XML
SET #data = EVENTDATA()
DECLARE #IsPooled int
SET #IsPooled = #data.value('(/EVENT_INSTANCE/IsPooled)[1]', 'int')
IF EXISTS(SELECT * FROM sys.Databases WHERE NAME = 'DatabaseMaintenance')AND (#IsPooled=0) Begin
insert into DatabaseMaintenance.dbo.Login (UserName, HostName, ApplicationName, EventDataValue)
values (ORIGINAL_LOGIN(), HOST_Name(), APP_NAME(),#Data)
END
END;
One thing that I've always hated more than just about anything in MS SQL Server is the way that security works. The security context constantly switches if you look at the server funny and it's often very hard (for me anyway) to predict or debug.
In dealing with an issue today, I though, "I wish I could just add a line to my code that would display the security context that SQL Server is using when this code runs." Does such a command exist? For example, SELECT security_context()
To be a little clearer... if I'm in a stored procedure and am therefor subject to the security context of the owner of the SP then I'd like to see that. If I'm in code that was called by sp_executesql and it's causing the security to be under the context of the SQL Server service account, then I would want to see that.
At least then I might be able to figure out why SQL Server thinks that I shouldn't have access to something.
Thanks!
EXAMPLE
-- Set up
CREATE USER Test_User WITHOUT LOGIN
CREATE TABLE Test_Security_Context (my_id INT)
INSERT INTO Test_Security_Context VALUES (1)
DENY SELECT ON Test_Security_Context TO Test_User
GO
CREATE PROCEDURE Test_Security_Context_SP
AS
SELECT SUSER_SNAME()
SELECT * FROM Test_Security_Context -- This will return ok
EXEC('SELECT SUSER_SNAME(); SELECT * FROM Test_Security_Context') -- SUSER_SNAME() will match above but select fails
GO
GRANT EXECUTE ON Test_Security_Context_SP TO Test_User
GO
-- Switch to the new user
SETUSER 'Test_User'
GO
-- Do the test
EXEC Test_Security_Context_SP
GO
-- Clean up
SETUSER
DROP PROCEDURE Test_Security_Context_SP
DROP TABLE Test_Security_Context
DROP USER Test_User
GO
Yes, there is such a pair of views that represents your current security context, considering all the details like EXECUTE AS or code signing:
sys.login_token for the server wide context
sys.user_token for the current database context
Every single access you get is ultimately derived from a row in the return of these results. Note that some access are implicit from hard coded role membership (like db_datareader database role or sysadmin server role).
Other that that:
ownership chaining is not related to security context: you are not under the 'context' of the SP owner. Ownership chaining simply states that access checks are skipped for objects owned by the same owner as current object (SP, View).
sp_executesql does not change the security context in any way
Not sure if this is what you mean by security context, but you can retrieve the user associated with your session like:
select SYSTEM_USER
This works for both a SQL Server login or a WIndows login. It even works inside stored procedures with execute as owner. For example,
create procedure dbo.Test
with execute as owner
as
select SYSTEM_USER
go
exec dbo.Test
select SYSTEM_USER
Prints:
sa
MyMachine\MyName
If you're looking for the Windows account that SQL Server is using to do things on your behalf, you could try to run whoami from the command like:
EXEC sp_configure 'show advanced options', 1
RECONFIGURE
EXEC sp_configure 'xp_cmdshell', 1
RECONFIGURE
EXEC master..xp_cmdshell 'whoami'
For me, that returns nt authority\network service.
I think you want to use CURRENT_USER to see the current security context. Here's an example:
SELECT CURRENT_USER AS 'Current User Name';
GO
EXECUTE AS LOGIN = 'junk'
GO
SELECT CURRENT_USER AS 'Current User Name';
GO
REVERT
SELECT CURRENT_USER AS 'Current User Name';
GO
with output (note: I'm admin on my SQL Server for this)
Current User Name
------------------
dbo
(1 row(s) affected)
Current User Name
------------------
Junk
(1 row(s) affected)
Current User Name
------------------
dbo
(1 row(s) affected)
I need to check if a specific login already exists on the SQL Server, and if it doesn't, then I need to add it.
I have found the following code to actually add the login to the database, but I want to wrap this in an IF statement (somehow) to check if the login exists first.
CREATE LOGIN [myUsername] WITH PASSWORD=N'myPassword',
DEFAULT_LANGUAGE=[us_english],
CHECK_EXPIRATION=OFF,
CHECK_POLICY=OFF
GO
I understand that I need to interrogate a system database, but not sure where to start!
Here's a way to do this in SQL Server 2005 and later without using the deprecated syslogins view:
IF NOT EXISTS
(SELECT name
FROM master.sys.server_principals
WHERE name = 'LoginName')
BEGIN
CREATE LOGIN [LoginName] WITH PASSWORD = N'password'
END
The server_principals view is used instead of sql_logins because the latter doesn't list Windows logins.
If you need to check for the existence of a user in a particular database before creating them, then you can do this:
USE your_db_name
IF NOT EXISTS
(SELECT name
FROM sys.database_principals
WHERE name = 'Bob')
BEGIN
CREATE USER [Bob] FOR LOGIN [Bob]
END
From here
If not Exists (select loginname from master.dbo.syslogins
where name = #loginName and dbname = 'PUBS')
Begin
Select #SqlStatement = 'CREATE LOGIN ' + QUOTENAME(#loginName) + '
FROM WINDOWS WITH DEFAULT_DATABASE=[PUBS], DEFAULT_LANGUAGE=[us_english]')
EXEC sp_executesql #SqlStatement
End
As a minor addition to this thread, in general you want to avoid using the views that begin with sys.sys* as Microsoft is only including them for backwards compatibility. For your code, you should probably use sys.server_principals. This is assuming you are using SQL 2005 or greater.
You can use the built-in function:
SUSER_ID ( [ 'myUsername' ] )
via
IF [value] IS NULL [statement]
like:
IF SUSER_ID (N'myUsername') IS NULL
CREATE LOGIN [myUsername] WITH PASSWORD=N'myPassword',
DEFAULT_LANGUAGE=[us_english],
CHECK_EXPIRATION=OFF,
CHECK_POLICY=OFF
GO
https://technet.microsoft.com/en-us/library/ms176042(v=sql.110).aspx
In order to hande naming conflict between logins, roles, users etc. you should check the type column according to Microsoft sys.database_principals documentation
In order to handle special chacters in usernames etc, use N'<name>' and [<name>] accordingly.
Create login
USE MASTER
IF NOT EXISTS (SELECT 1 FROM master.sys.server_principals WHERE
[name] = N'<loginname>' and [type] IN ('C','E', 'G', 'K', 'S', 'U'))
CREATE LOGIN [<loginname>] <further parameters>
Create database user
USE [<databasename>]
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE
[name] = N'<username>' and [type] IN ('C','E', 'G', 'K', 'S', 'U'))
CREATE USER [<username>] FOR LOGIN [<loginname>]
Create database role
USE [<databasename>]
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE
[name] = N'<rolename>' and Type = 'R')
CREATE ROLE [<rolename>]
Add user to role
USE [<databasename>]
EXEC sp_addrolemember N'<rolename>', N'<username>'
Grant rights to role
USE [<databasename>]
GRANT SELECT ON [<tablename>] TO [<rolename>]
GRANT UPDATE ON [<tablename>] ([<columnname>]) TO [<rolename>]
GRANT EXECUTE ON [<procedurename>] TO [<rolename>]
The SQL is tested on SQL Server 2005, 2008, 2008 R2, 2014, 2016, 2017, 2019
Try this (replace 'user' with the actual login name):
IF NOT EXISTS(
SELECT name
FROM [master].[sys].[syslogins]
WHERE NAME = 'user')
BEGIN
--create login here
END
This is for Azure SQL:
IF (EXISTS(SELECT TOP 1 1 FROM sys.sql_logins WHERE [name] = '<login>'))
DROP LOGIN [<login>];
Source: How to check whether database user already exists in Azure SQL Database
This works on SQL Server 2000.
use master
select count(*) From sysxlogins WHERE NAME = 'myUsername'
on SQL 2005, change the 2nd line to
select count(*) From syslogins WHERE NAME = 'myUsername'
I'm not sure about SQL 2008, but I'm guessing that it will be the same as SQL 2005 and if not, this should give you an idea of where t start looking.
what are you exactly want check for login or user ?
a login is created on server level and a user is created at database level so a login is unique in server
also a user is created against a login, a user without login is an orphaned user and is not useful as u cant carry out sql server login without a login
maybe u need this
check for login
select 'X' from master.dbo.syslogins where loginname=<username>
the above query return 'X' if login exists else return null
then create a login
CREATE LOGIN <username> with PASSWORD=<password>
this creates a login in sql server .but it accepts only strong passwords
create a user in each database you want to for login as
CREATE USER <username> for login <username>
assign execute rights to user
GRANT EXECUTE TO <username>
YOU MUST HAVE SYSADMIN permissions or say 'sa' for short
you can write a sql procedure for that on a database
create proc createuser
(
#username varchar(50),
#password varchar(50)
)
as
begin
if not exists(select 'X' from master.dbo.syslogins where loginname=#username)
begin
if not exists(select 'X' from sysusers where name=#username)
begin
exec('CREATE LOGIN '+#username+' WITH PASSWORD='''+#password+'''')
exec('CREATE USER '+#username+' FOR LOGIN '+#username)
exec('GRANT EXECUTE TO '+#username)
end
end
end
Starting SQL 2016:
DROP USER IF EXISTS [userName]
CREATE USER [userName] FOR LOGIN [loginName]
First you have to check login existence using syslogins view:
IF NOT EXISTS
(SELECT name
FROM master.sys.server_principals
WHERE name = 'YourLoginName')
BEGIN
CREATE LOGIN [YourLoginName] WITH PASSWORD = N'password'
END
Then you have to check your database existence:
USE your_dbname
IF NOT EXISTS
(SELECT name
FROM sys.database_principals
WHERE name = 'your_dbname')
BEGIN
CREATE USER [your_dbname] FOR LOGIN [YourLoginName]
END