SQL Trigger to grant 'dbowner' permissions upon new DB creation - sql

I put together the following SQL script to create a trigger when a new DB is created, to grant db_owner permissions to a specific account:
CREATE TRIGGER ddl_trig_database
ON ALL SERVER
FOR CREATE_DATABASE
AS
DECLARE #DatabaseName NVARCHAR(128), #SQL NVARCHAR(4000)
SELECT #DatabaseName = EVENTDATA().value('(/EVENT_INSTANCE/DatabaseName)[1]','NVARCHAR(128)');
SET #SQL = '
USE ' + #DatabaseName + ';
EXEC sp_addrolemember N''db_owner'', N''[accountname]'';'
EXEC(#SQL)
However, I get the following error when I try to create a new DB to test this Trigger:
Message: User or role '[accountname]' does not exist in this database.
Could not find database ID 45, name '45'. The database may be offline.
Wait a few minutes and try again.
I put this together using some examples found on the web. It appears that the Trigger is occurring right when DB is being created, instead of running after DB has been created. Is there a way to delay it?

Few things to be mentioned here:
The trigger is executed AFTER the database is created as this is the default behaviour
Not being able to find the database may point to lack of required permissions. Make sure you have enough permission by impersonating as a user that has been granted enough permissions i.e. some database owner. Use the WITH EXECUSE AS clause.
Make sure you have the "[accountname]" existing at the new DB or at the server level (depends what kind of account you are trying to add). You can add a database user, database role, Windows login, or Windows group.
References:
https://msdn.microsoft.com/en-us/library/ms189799.aspx
https://msdn.microsoft.com/en-us/library/ms187750.aspx
SQL Server 2008 - Does a trigger run with the same permissions as the login/user?

Related

SQL Certificate Permissions to Run Jobs

We have a need where a user with low permissions can kick off 1 job, through a proc. So, we:
created a proc (in master db) to kick off the job
created a certificate and signed the proc with the certificate
created a login from the certificate and gave higher level access to that login
created a role in the master db and granted EXECUTE to that one proc for this role
put the initial user with low permissions into this new role
My question is why will this not work? We use something similar to allow a user to restore a database on one of our servers. The only way to get this to work for me right now is to give the low permissions user higher permissions in msdb db. This obviously isn't ideal and undoes exactly what we're trying to avoid.
The sample permissions code is below and then the proc code is below that.
USE [master];
GO
CREATE CERTIFICATE [TestCert]
ENCRYPTION BY PASSWORD = N'ComplicatedPassword'
WITH SUBJECT = N'Certificate to allow user to kick off one job';
GO
ADD SIGNATURE
TO [dbo].[ProcThatCallsJob]
BY CERTIFICATE [TestCert]
WITH PASSWORD = N'ComplicatedPassword';
GO
CREATE LOGIN [TestLoginFromCert] FROM CERTIFICATE [TestCert]
GO
ALTER SERVER ROLE [sysadmin] ADD MEMBER [TestLoginFromCert]
GO
CREATE ROLE [RoleToExecuteProc]
GO
GRANT EXECUTE ON OBJECT::[dbo].[ProcThatCallsJob]
TO [RoleToExecuteProc];
GO
CREATE USER [TestUser_LowPermissions]
FOR LOGIN [TestUser]
GO
ALTER ROLE [RoleToExecuteProc] ADD MEMBER [TestUser_LowPermissions]
GO
USE [msdb]
GO
CREATE USER [TestUserFromCert]
FOR LOGIN [TestLoginFromCert]
GO
ALTER ROLE [db_datareader] ADD MEMBER [BDWReportUserRunJobs]
GO
ALTER ROLE [SQLAgentOperatorRole] ADD MEMBER [BDWReportUserRunJobs]
GO
The proc, [dbo].[ProcThatCallsJob]:
USE [master]
GO
CREATE PROCEDURE [dbo].[ProcThatCallsJob]
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'USE msdb
EXEC msdb.dbo.sp_start_job #job_name = ''10171_TestJob''
'
EXECUTE sp_executesql #sql;
END
Is the issue in any way related to the fact that the proc is signed with a certificate in master db (associated with a login with sysadmin privileges)? I can put the certificate in msdb db, but I wouldn't have anything to sign it to there.
EDIT - I created the proc and certificate in msdb db as a test of my hypothesis that change databases was causing some concern. No luck. The [TestUser_LowPermissions] still gets an error that they cannot execute sp_start_job. So, I'm still at a loss as to why the signed proc is not inheriting the [TestLoginFromCert] permissions of sysadmin.
From MSDN forum:
Everything you did so far was correct in itself. The problem lies within the nested system stored procedures themselves.
The thing is: sp_start_job is doing another permission/role check, hard-coded:
-- Check permissions beyond what's checked by the sysjobs_view
-- SQLAgentReader role can see all jobs but
-- cannot start/stop jobs they do not own
IF (#job_owner_sid <> SUSER_SID() -- does not own the job
AND (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 0) -- is not sysadmin
AND (ISNULL(IS_MEMBER(N'SQLAgentOperatorRole'), 0) = 0)) -- is not SQLAgentOperatorRole
BEGIN
RAISERROR(14393, -1, -1);
RETURN(1) -- Failure
END
Since you don’t want to mess with system code, there are not many options left:
1. Work with Impersonation to sa: highly risky
2. Use a Trustworthy database – I personally dislike openly saying that a lot, because it is hard enough to educate the average users NOT to use this concept. But you can secure this if it is your very own code-only database
3. Use a combination of [SQLAgentOperatorRole] and your own procedure. Meaning: Make the user member of that role and then deny execute on sp_startjob (and other procs). This way you can grant the execute via your own proc and circumvent the hard-coded check by giving him the [SQLAgentOperatorRole].

How to allow a read-only login/user with execute permissions to run stored procedure that drops and creates databases?

I have a SQL Server 2016 instance with a Tools database. The Tools database has a RestoreDBFromPath procedure that can restore a backup file to a database on the server. If the database already exists it drops the database and restores the backup. While restoring the backup it also writes some data to some logging tables on the Tools databases.
Example Call:
USE [Tools]
GO
DECLARE #Path NVARCHAR(MAX), #DBName NVARCHAR(MAX)
SET #Path = 'C:\Backups\Mybackup.bak'
SET #DBName = 'RestoredDB'
EXECUTE [dbo].[LoadDatabaseFromBackup] #Path, #DBName
GO
Right now, I have a 2 logins on the server:
Admin - sysadmin that can do anything
ReadOnlyUser - Can view any database/definition on the server as well as connect to any database
The Tools database has 1 user (Admin can connect since its a sysadmin):
ReadOnlyUser - can view any definition, read from any table, and execute RestoreDBFromPath on the Tools database.
If I execute the RestoreDBFromPath procedure while logged in as Admin, then everything works fine. However trying to run it as ReadOnlyUser causes it to fail since that user can't write to tables on Tools and it especially can't drop/create/restore databases on the server.
What is a simple and secure way that I can allow ReadOnlyUser to execute RestoreDBFromPath successfully just like Admin can?
Things I've Tried:
Changing the SP signature to CREATE PROCEDURE dbo.RestoreDBFromPath #Path NVARCHAR(MAX), #DBName NVARCHAR(MAX) WITH EXECUTE AS OWNER
creating a user without login on the Tools database with full permissions and setting the SP to execute as them
Things I've tried, but may not have done correctly:
Made a certificate on Tools via a password, created a user from that certificate, given the user full permissions, signed the RestoreDBFromPath SP with the certificate
Things I've heard about but found no good examples:
Using an Asymetric Key
In order for the ReadOnlyUser to execute the stored procedure on the Tools database,
they need to have execute permission on the stored procedure
For dropping and creating any database on the server, the ReadOnlyUser needs to be a dbCreator on the server. (the drawback is that they can drop any database).
In order to write to the log tables in the Tools database, grant write permission to the ReadOnlyUser on the log tables.

Problems using WITH EXECUTE AS OWNER with Trigger

I just implemented the WITH EXECUTE AS OWNER code on a new table trigger and now regular users who insert to the table are receiving the following error: Cannot execute as the database principal because the principal "dbo" does not exist, cannot be impersonated, or you do not have permission.
Users who are setup as sysadmins have no problem inserting to the table, no errors. What type of rights need to be granted to users/roles in order for them to be able to use WITH EXECUTE AS OWNER?
Apparently problem was unrelated to permissions after all but instead related to the fact that "dbo does not exist". Current db owner was set to an old login which no longer exists.
Fixed this by running the following SQL statement:
ALTER DATABASE [DB]
SET SINGLE_USER
GO
EXEC sp_changedbowner 'sa'
GO
ALTER DATABASE [DB]
SET MULTI_USER

sp_setapprole and creating users / logins

We have set up SQL Server and our Application in such a way that an SQL Server login has … well no permissions when accessing the database using any tool, except our application. This is needed because some people might fire up access (or SQL Server Management Studio) and would be able to manipulate the data in that way.
So we set up an Application Role and gave that Application Role the necessary permissions. Everything works nicely, users can't access our tables unless they use our Application.
Now comes the problem. In our application it should be possible to Create additional logins / users, or set some specific users as being Administrators (only admins can do user related stuff). I'm not 100% certain but in that case I think the user needs SecurityAdmin role. So we have some code in our application which looks like this :
IF NOT EXISTS ( SELECT name
FROM sys.server_principals
WHERE name = #LoginName )
BEGIN
SET #SQL = 'CREATE LOGIN [' + #LoginName + '] WITH PASSWORD = '''
+ #Password + ''', DEFAULT_DATABASE=[' + #DBNAME
+ '], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF';
EXECUTE(#SQL);
END
IF NOT EXISTS ( SELECT name
FROM sys.database_principals
WHERE name = #LoginName )
BEGIN
SET #SQL = 'CREATE USER [' + #LoginName + '] FOR LOGIN ['
+ #LoginName + ']';
EXECUTE(#SQL);
END
IF EXISTS ( SELECT name
FROM sys.server_principals
WHERE name = #LoginName )
BEGIN
EXEC sp_addsrvrolemember #LoginName, 'securityadmin'
END
The problem is of course that the server_principals is in the Master Database, and there is no way I can give my Application Role those permissions. Is there any way to work-around this problem or an easy way to solve this ?
In SQL Server, when you activate an Application Role (AppRole) , its security context completely supplants the security context of the User. Although this can be reverted in the session(connection), the two security contexts (AppRole and User) cannot both be active at the same time.
Therefore, the usual way to use these two together is exactly how you have done it so far(*): that is, use the User/Login only to enable the right to connect to the database initially, then switch to the AppRole, to gain access to the databases contents.
However, if you want to enable different levels of permissions/rights within the database, you cannot do it through the Users/Logins (not solely anyway). There are several different ways to approach this, but the simplest way to accomplish what you want is probably to have different AppRoles for different classes of application users. So you could have an ApplUser role for most users, and an ApplAdmin role for users who are administrators, etc. You would then grant these additional AppRoles, enhanced permissions and access within the database, as needed.
(* And big kudos for that. This is a very good security scheme for SQL Server, but few application developers go through the effort to implement it.)
You should have a look at the EXECUTE AS clause in SQL Server:
EXECUTE AS Clause
You can then put the logic into a stored procedure in the database, set this to execute as a different user (or owner if the owner of the database has the permissions to perform the operation). All you need to do then is give the Application Role the permissions to execute the procedure, nothing more and the code itself will be executed as the other user.

CREATE USER in Dynamic SQL on SQL Azure

I want to write a stored procedure that checks the environment a database resides in (Based on the name) and creates the appropriate user and role for our application.
This would allow us to automate setting up permissions if we move a database between environments (Currently due to the limitations of Windows Azure SQL Database we have to manually run a script which is not ideal and prone to human error).
So the syntax we are using is:
DECLARE #UserToAdd VARCHAR(50) = (
SELECT CASE
WHEN #Environment = 'Development' THEN 'DevelopmentApplicationUser'
WHEN #Environment = 'Test' THEN 'TestingApplicationUser'
ELSE ''
END
)
IF (#UserToAdd != '')
BEGIN
EXEC ('CREATE USER [' + #UserToAdd + '] FOR LOGIN [' + #UserToAdd + '];')
EXEC ('EXEC sp_addrolemember N''WebUser'', N''' + #UserToAdd + ''';')
END
This works correctly on our development server (SQL Server 2008 R2) but in Windows Azure SQL Database we get the below error:
The CREATE USER statement must be the only statement in the batch
Now the MSDN documentation does state:
If the CREATE USER statement is the only statement in a SQL batch, Windows Azure SQL Database supports the FOR | FROM LOGIN clause. If the CREATE USER statement is not the only statement in a SQL batch or is executed in dynamic SQL, the FOR | FROM LOGIN clause is not supported.
However this means that we cannot automate our permissions whatsoever.
Has anyone got around this issue and been able to produce dynamic sql that creates a user? Alternatively is there a way around this in a stored procedure?
I opened a support case with Microsoft to see if this is possible and the response was that if you want to check the existence of a login or a user and then create that login or user, you MUST use separate connections for the check and then the creation. It is NOT possible to check and create if not exists in the same transaction or batch. In Sql Azure.
Hth,
Oli
I speak under correction here.
you can create the user without FOR LOGIN
then use sp_change_users_login to map the user to a login after the fact