SQL Certificate Permissions to Run Jobs - sql

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].

Related

Allow login to run stored procedure without being able to select from table

I have a SQL Server with two databases:
Database1
Database2
Login 'MyLogin' has read access on Database2 only.
Database2 has a stored procedure as follows:
CREATE PROCEDURE Get_LogData
#ProductID int
AS
BEGIN
If Exists (Select (1) From Database2.dbo.Products Where ProductID = #ProductID)
Select LogTimeStamp
, ProductID
, Description
from Database1.dbo.Log Where ProductID = #ProductID
Else
Print 'Get_LogData: Unable to find ProductID ' + CAST(#ProductID AS VARCHAR)
END;
GO
BEGIN
GRANT EXEC ON Get_LogData TO [MyLogin];
END;
GO
When 'MyLogin' trys to run the stored procedure, we get the error:
The server principal "MyLogin" is not able to access the database
"Database1" under the current security context.
How do I allow 'MyLogin' the ability to run this stored proc and get the data from Database1, without allowing them to just run a normal Select query against Database1.Log
This is expanding on the answer linked by Ezlo (SQL Server EXECUTE AS trouble), on which I'm going to cover Enabling Cross-Database Access in SQL Server.
Firstly, let's set up a quick test to replicate the issue you have right now:
--Create a couple of sample databases
CREATE DATABASE SampleDB1;
CREATE DATABASE SampleDB2;
GO
USE SampleDB2;
GO
--Create a sample table
CREATE TABLE dbo.SampleTable (ID int, Somestring varchar(25));
GO
USE SampleDB1;
GO
--Create a sample SP and User/Login;
CREATE PROC dbo.SomeProc AS
SELECT ID,
SomeString
FROM SampleDB2.dbo.SampleTable;
GO
CREATE LOGIN SampleCredential WITH PASSWORD = 'abc123', CHECK_EXPIRATION = OFF, CHECK_POLICY = OFF, DEFAULT_LANGUAGE = BRITISH;
CREATE USER SampleCredential FOR LOGIN SampleCredential;
GRANT EXEC ON dbo.SomeProc TO SampleCredential;
GO
--Test
EXECUTE AS LOGIN = 'SampleCredential';
GO
--This will fail
EXEC dbo.SomeProc;
GO
REVERT;
GO
As you can see, if you run this script, you get an error along the lines of:
Msg 916, Level 14, State 1, Procedure SomeProc, Line 4 [Batch Start
Line 28] The server principal "SampleCredential" is not able to access
the database "SampleDB2" under the current security context.
So, what is Cross-Database access? To quote from the documentation:
Cross-database ownership chaining occurs when a procedure in one
database depends on objects in another database. A cross-database
ownership chain works in the same way as ownership chaining within a
single database, except that an unbroken ownership chain requires that
all the object owners are mapped to the same login account. If the
source object in the source database and the target objects in the
target databases are owned by the same login account, SQL Server does
not check permissions on the target objects.
Note, however, that there are major security flaws that can be introduced with this method. Thus, if this is a concern for your environment this is not the solution for you. Again, from the documentation:
Ownership chaining across databases is turned off by default.
Microsoft recommends that you disable cross-database ownership
chaining because it exposes you to the following security risks:
Database owners and members of the db_ddladmin or the db_owners database roles can create objects that are owned by other users. These
objects can potentially target objects in other databases. This means
that if you enable cross-database ownership chaining, you must fully
trust these users with data in all databases.
Users with CREATE DATABASE permission can create new databases and attach existing databases. If cross-database ownership chaining is
enabled, these users can access objects in other databases that they
might not have privileges in from the newly created or attached
databases that they create.
OK, now that caveat is out of the way, what do to. Continuing on from the script above, we need to enable cross db ownership chaining on the server if it isn't already. You can do so by running the following:
EXEC sp_configure 'show advanced', 1;
RECONFIGURE;
GO
EXEC sp_configure 'cross db ownership chaining', 1;
RECONFIGURE;
GO
EXEC sp_configure 'show advanced', 0;
RECONFIGURE;
GO
Now that is enabled you can enable DB_CHAINING on the 2 databases:
ALTER DATABASE SampleDB1 SET DB_CHAINING ON;
ALTER DATABASE SampleDB2 SET DB_CHAINING ON;
GO
You'll then need to create the user for your login on the other database, but won't need to give it any permissions, so this will be "fine" (it won't allow them to SELECT from, the table outside of the SP):
USE SampleDB2;
GO
CREATE USER SampleCredential FOR LOGIN SampleCredential;
GO
Finally, you can then test again:
USE SampleDB1;
--Test
EXECUTE AS LOGIN = 'SampleCredential';
GO
--It works (0 rows returned)
EXEC dbo.SomeProc;
GO
REVERT;
GO
If you want to double check the user can't select from the database, you can do so:
USE SampelDB1;
GO
--This will fail
SELECT *
FROM SampleDB2.dbo.SampleTable;
GO
And finally the cleanup (for good measure):
--Cleanup
USE master;
DROP DATABASE SampleDB2;
DROP DATABASE SampleDB1;
DROP LOGIN SampleCredential;
EXEC sp_configure 'show advanced', 1;
RECONFIGURE;
GO
EXEC sp_configure 'cross db ownership chaining', 0;
RECONFIGURE;
GO
EXEC sp_configure 'show advanced', 0;
RECONFIGURE;
GO
You want to use the execute as clause. It is unclear what the value should be, but to set permissions, often you want the owner of the stored procedure to have the permissions you want.
CREATE PROCEDURE Get_LogData (
#ProductID int
)
WITH EXECUTE AS OWNER AS
BEGIN
. . .
END;

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.

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

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?

SQL Server: granting very high privileges to a limited user inside a stored procedure

We are building a management app for our system, and one of the app's abilities is to create new databases for new users. This app needs to CREATE DATABASE, RESTORE DATABASE, CREATE USER, grant permissions, etc - so the user needs to have some very strong permissions.
We are contracting these services to an external company and we do not want to give them unrestricted access to our system, we only want them to be able to do what we allow them to do. So we thought about encapsulating the entire process in a stored procedure, granting EXECUTE on this to a specific domain user, and running it with EXECUTE AS 'SA'.
Unfortunately that is not possible - SA is not a database user and when we try to define it as one, we get the error
Msg 15405, Level 16, State 1, Line 1
Cannot use the special principal 'sa'.
We then thought about using DBO and setting up cross-database ownership chaining, but this is all beginning to be a serious headache.
Does anyone know of an elegant way to do this?
This is perfectly possible with module signing.
Create a procedure that executes the elevated code.
Add EXECUTE AS CALLER to the procedure
Create a certificate and private key
Sign the procedure
Drop the private key
Export the certificate
Import the certificate in [master]
Create a login derived from the certificate
Grant the required privileges to the certificate derived login
Note that any alteration to the procedure will invalidate the signature and will require to redo the procedure. Dropping the private key is very important because otherwise the vendor can sign a different procedure and get the elevated permissions on arbitrary code. See Signing an Activated Procedure for an example.
To manage things inside a database, you can use EXECUTE AS OWNER and make sure dbo owns the stored procedure. No problems there
However, CREATE DATABASE etc requires server level permissions. Note: you don't need sysadmin permissions.
With SQL Server 2012 you can use server roles, and GRANT CREATE DATABASE to this server role. For earlier versions, you can grant this directly to the login. Or use dbcreator if your prefer.
If you decide they need to manage logins however, securityadmin has the same effective permissions as sysadmin
create database admin;
GO
alter database admin set trustworthy on
GO
alter authorization on database::admin to [sa]
GO
use admin
GO
create procedure adminProc
with execute as 'dbo'
as
create database test
GO
create login tmp with password = 'tmp', check_policy = off
GO
create user tmp
GO
grant execute on adminProc to tmp
GO
execute as login = 'tmp'
GO
-- This fails
create database test
-- This works
exec adminProc
GO
revert
GO
drop database test
GO

SQL Server Execute Impersonation

What is the diffrence between...
execute as user = 'testuser'
AND
execute as login = 'testuser'
I am executing a cross database procedure under these logins and it works with the exececute as login but not the execute as user. It is saying the server principal "testuser" is nt able to access the database "xxx" under the securty context.
When i SELECT SYSTEM_USER after both commands I see that it is set to 'testuser'
execute as login provides impersonation to the entire server, since logins are on a server level. Since users are defined per database, execute as user impersonation applies only to a specific database, which is why you see the error when you cross databases.
The EXECUTE AS can be added to stored procs, functions, triggers, etc.
Example to Execute As:
CREATE PROCEDURE dbo.MyProcedure
WITH EXECUTE AS OWNER
In this case you are impersonating the owner of the module being called.
You can also impersonate SELF, OR the USER creating or altering the module OR...
impersonate CALLER , which will enable to module to take on the permissions of the current user, OR...
impersonate OWNER, which will take on the permission of the owner of the procedure being called OR...
impersonate 'user_name', which will impersonate a specific user OR...
impersonate 'login_name' with will impersonate a specific login.
Setting permission on objects like stored procedures can be accomplished with
GRANT EXECUTE ON <schema>.<procedurename> to <username>;
However, you may also want to grant security rights at both the login and user level.
You will want to determine and grant ONLY the necessary rights
for the objects that require access (such as execution). Consider use of the "EXECUTE AS" capability which enables impersonation of another user
to validate permissions that are required to execute the code WITHOUT having to grant all of the necessary rights to all of the underlying objects (e.g. tables).
MOST of the time, you will only need to grant EXECUTE rights to stored procs and then rights are granted to all objects referenced within the stored proc.
In this way, you do not need to give implicit rights (example: to update data or call additional procs). Ownership chaining handles this for you.
This is especially helpful for dynamic sql or if you need to create elevated security tasks such as CREATE TABLE. EXECUTE AS is a handy tool to consider for these.
This example may help clarify all of this:
--Create a user called NoPrivUser with public access to a database (e.g. dbadb)
USE [master]
GO
CREATE LOGIN [NoPrivUser] WITH PASSWORD=N'ABC5%', DEFAULT_DATABASE=[dbadb], CHECK_EXPIRATION=ON, CHECK_POLICY=ON
GO
USE [DBAdb]
GO
CREATE USER [NoPrivUser] FOR LOGIN [NoPrivUser]
GO
NOTE: CREATOR OR OWNER OF THIS PROCEDURE WILL REQUIRE CREATE TABLE RIGHTS within the target database.
use DBAdb
go
CREATE PROCEDURE dbo.MyProcedure
WITH EXECUTE AS OWNER
AS
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].MyTable') AND type in (N'U'))
CREATE TABLE MyTable (PKid int, column1 char(10))
INSERT INTO MyTable
VALUES (1,'ABCDEF')
GO
GRANT EXEC ON dbo.MyProcedure TO NoPrivUser;
GO
-- Now log into your database server as NoPrivUser and run the following.
use dbadb
go
EXEC dbo.MyProcedure
--(1 row(s) affected)
Now try to select from the new table while logged on as NoPrivuser.
You will get the following:
select * from MyTable
go
Msg 229, Level 14, State 5, Line 1 The SELECT permission was denied on
the object 'MyTable', database 'DBAdb', schema 'dbo'.
That is expected since you only ran the procedure under the security context of Owner while logged on as NoPrivUser.
NoPrivUser as no rights to actually read the table, Just to execute the procedure which creates and inserts the rows.
With the EXECUTE AS clause the stored procedure is run under the context of the object owner. This code successfully creates dbo.MyTable and rows are inserted successfully.
In this example, the user "NoPrivUser" has absolutey no granted rights to modify the table, or read or modify any of the data in this table.
It only takes on the rights needed to complete this specific task coded WITHIN the context of this procedure.
This method of creating stored procedures that can perform tasks that require elevated security rights without permanently assigning those rights come be very useful.
Login scope is at the server level while user scope is at the current database level
http://msdn.microsoft.com/en-us/library/ms181362.aspx