7

I am a database developer mostly writing SSRS reports. I would like to create stored procedures and assure the DBAs that those stored procedures cannot write to the production databases, without the DBAs having to carefully review the stored procedures.

I create the stored procedures in DatabaseA. I need the stored procedures to SELECT data in DatabaseB.

I want any user to be able to safely EXECUTE a stored procedure in DatabaseA but not be able to use that stored procedure to INSERT/UPDATE/DELETE data in DatabaseB, regardless of any other permissions which the user may have.

Can the DBAs set permissions at the database or procedure level to enforce this?

This is for SQL Sever 2016.

0

3 Answers 3

7

Can the DBAs set permissions at the database or procedure level to enforce this?

Yes, your DBA can sign reporting stored procedures with a certificate than denies write permissions on DatabaseB tables. DENY takes precedence over caller GRANT permissions as long as ownership chaining does not apply (the default behavior for cross-database queries as @AMtwo detailed) and the caller is not a privileged user (sysadmin role member or the owner of DatabaseB).

If cross-database chaining is allowed (DB_CHAINING database option or cross db ownership chaining server option), those will need to be turned off (or the proc owner changed) so that permissions are evaluated.

Below is a complete annotated example script of the certificate method to DENY permissions. Once the cert is created, your DBA can add the signature to new or altered reporting procs going forward.

CREATE DATABASE DatabaseA;
CREATE DATABASE DatabaseB;
GO

--create demo user to test permissions
CREATE LOGIN DemoUser WITH PASSWORD = 'S*39NOO7756sfv*&6';
GO

USE DatabaseB;
GO
CREATE USER DemoUser;
CREATE TABLE dbo.Table1(Col1 int);
--grant permissions to demo user
GRANT SELECT, INSERT, UPDATE, DELETE ON dbo.Table1 TO DemoUser;
GO
--create cert and user from cert
CREATE CERTIFICATE DenyWriteCertificate
   ENCRYPTION BY PASSWORD = 'S%^aap34577Hbw'
   WITH SUBJECT = 'Deny INSERT UPDATE DELETE';
CREATE USER DenyWriteCertificateUser FROM CERTIFICATE DenyWriteCertificate;
--deny writes to cert user
ALTER ROLE db_denydatawriter ADD MEMBER DenyWriteCertificateUser;
--copy cert to DatabaseA
DECLARE @cert_id int = cert_id('DenyWriteCertificate')
DECLARE @public_key  varbinary(MAX) = certencoded(@cert_id),
        @private_key varbinary(MAX) =
           certprivatekey(@cert_id,
              'S%^aap34577Hbw',
              'S%^aap34577Hbw')
DECLARE @sql nvarchar(MAX) =
      'CREATE CERTIFICATE DenyWriteCertificate
       FROM  BINARY = ' + convert(varchar(MAX), @public_key, 1) + '
       WITH PRIVATE KEY (BINARY = ' +
          convert(varchar(MAX), @private_key, 1) + ',
          DECRYPTION BY PASSWORD = ''S%^aap34577Hbw'',
          ENCRYPTION BY PASSWORD = ''S%^aap34577Hbw'')'
EXEC DatabaseA.sys.sp_executesql @sql;
GO

USE DatabaseA;
CREATE USER DemoUser;
GO

CREATE PROC dbo.ReadProcedure
AS
SELECT Col1 FROM DatabaseB.dbo.Table1;
GO
GRANT EXECUTE ON dbo.ReadProcedure TO DemoUser;
GO

CREATE PROC dbo.WriteProcedure
AS
INSERT INTO DatabaseB.dbo.Table1(Col1) VALUES(1);
GO
GRANT EXECUTE ON dbo.WriteProcedure TO DemoUser;
GO

--these both succeed because caller has read and write permissions
EXECUTE AS LOGIN = 'DemoUser';
GO
EXECUTE dbo.ReadProcedure;
GO
EXECUTE dbo.WriteProcedure;
GO
REVERT;
GO

--add deny cert to report procs
ADD SIGNATURE TO dbo.ReadProcedure BY CERTIFICATE DenyWriteCertificate WITH PASSWORD = 'S%^aap34577Hbw';
ADD SIGNATURE TO dbo.WriteProcedure BY CERTIFICATE DenyWriteCertificate WITH PASSWORD = 'S%^aap34577Hbw';
GO

--read proc succeeds, write proc fails
EXECUTE AS LOGIN = 'DemoUser';
GO
EXECUTE dbo.ReadProcedure;
GO
EXECUTE dbo.WriteProcedure;
GO
REVERT;
GO
3
  • Thank you Dan. I pass this on to our DBAs. They should find it very useful. Commented Dec 22, 2021 at 15:51
  • 1
    @JohnLofgren, FYI, certs are most often used to elevate permissions that can't be conferred via ownership chaining. This is the first time I've seen a use case for reducing caller permissions but it should make your DBA's life easier.
    – Dan Guzman
    Commented Dec 22, 2021 at 15:55
  • Also if the stored procedures were in the same database as the target tables, you could simply drop them in a schema owned by a low-privilege user and set them to EXECUTE AS OWNER. Commented Dec 23, 2021 at 16:56
7

Ownership chaining says "no."

The way ownership chaining works in SQL Server, granting EXEC on a stored procedure implicitly grants permission to all dependent objects, so that the stored procedure can do whatever it wants within that database.

The traditional answer to this is that you need to trust the process that creates and changes the stored procedure. If you don't trust that process, then you can't trust granting execute to a low-privileged user.

Quick demo

Take a look at how ownership chaining affects things. In this example, the ReadOnlyUser is explicitly denied INSERT on the table, and explicitly granted EXEC` on a stored procedure (which just does an insert into that table).

You'll see that the user is denied the ability to insert directly, but is able to run the stored procedure, which successfully inserts.

USE [TestingPermissions]

CREATE TABLE dbo.ReadOnlyPlease (
    SomeColumn varchar(100)
    );
GO
INSERT INTO dbo.ReadOnlyPlease (SomeColumn)
VALUES ('One'),('Two'),('Three');

GO
CREATE PROCEDURE dbo.CanItWrite
AS
SET NOCOUNT ON;

INSERT INTO dbo.ReadOnlyPlease (SomeColumn)
SELECT CONVERT(varchar(100),GETDATE(),121);
GO

CREATE LOGIN ReadOnlyUser WITH PASSWORD = 'TheSecure1!';
CREATE USER ReadOnlyUser FROM LOGIN [ReadOnlyUser];
DENY UPDATE ON dbo.ReadOnlyPlease TO [ReadOnlyUser];
GRANT EXECUTE ON dbo.CanItWrite TO [ReadOnlyUser];
GO


EXECUTE AS LOGIN = 'ReadOnlyUser';
--This will fail
INSERT INTO dbo.ReadOnlyPlease (SomeColumn)
SELECT CONVERT(varchar(100),GETDATE(),121);
--this will succeed!
EXEC dbo.CanItWrite;

REVERT;

SELECT * FROM dbo.ReadOnlyPlease;

But wait, there's more!

I said earlier:

the stored procedure can do whatever it wants within that database.

Within the database.

Cross-database ownership chaining is a whole separate thing. It is disabled by default. That means you can put your stored procedure into a different database, and get around the ownership chaining implicit permissions.

If you create a separate database for just your reporting stored procedures, you'll need to manage every permission for every object that is accessed by the procedure (either granularly or via high level grant/role membership like db_datareader).

Demo time

Here, I create a separate database (ReportingProcedures), create a synonym to point across at the table in the other database, and create the same exact stored procedure. I could have edited the procedure to use 3-part names, but I like synonyms, so I used them.

Permissions-wise:

  • There is a user for both databases (TestingPermissions and ReportingProcedures)
  • INSERT is explicitly denied on the table itself
  • EXEC is granted on the identical procedures in the two databases

You'll see that the procedure in TestingPermissions succeeds because the data & code are in the same database (thanks ownership chaining!), but the procedure in ReportingProcedures fails because there is no ownership chaining!

CREATE DATABASE ReportingProcedures;
GO
USE ReportingProcedures

CREATE SYNONYM dbo.ReadOnlyPlease FOR TestingPermissions.dbo.ReadOnlyPlease;
GO
CREATE PROCEDURE dbo.CanItWrite
AS
SET NOCOUNT ON;

INSERT INTO dbo.ReadOnlyPlease (SomeColumn)
SELECT CONVERT(varchar(100),GETDATE(),121);
GO

CREATE USER ReadOnlyUser FROM LOGIN [ReadOnlyUser];
GRANT EXECUTE ON dbo.CanItWrite TO [ReadOnlyUser];
GO

EXECUTE AS LOGIN = 'ReadOnlyUser';
--This is the one that worked; still works
EXEC TestingPermissions.dbo.CanItWrite;
--This one fails though
EXEC ReportingProcedures.dbo.CanItWrite;

Different strokes for different folks

In the way that I used two databases as "containers" to separate the code from the data, you could also use different schemas with different owners. As long as the two schemas are owned by different users, then you can also successfully "break" the permission inheritance of ownership chaining

1
  • Unless the stored procedure uses dynamic SQL. That will execute as the caller, and if the caller has permissions, it could modify the target database. If the stored procedure doesn't execute as caller, the cross-database access will fail with an impersonation error. Commented Dec 21, 2021 at 22:09
-3

It can be done, for example, via a linked server call to the server itself (loopback) and fixed security context (sql account). This account should have db_datareader permission in DatabaseB. Your stored procedure will select data using 4 part naming convention:

  select field1, field2, field3 from [loopback].[DatabaseB].[dbo].[Table] where...

How to create:

EXEC master.dbo.sp_addlinkedserver @server = N'loopback', @srvproduct=N'',
 @provider=N'SQLNCLI', @datasrc=@@SERVERNAME

More details: https://www.sqlshack.com/how-to-create-and-configure-a-linked-server-in-sql-server-management-studio/

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.