This is fairly easy to accomplish (including handling multiple databases and Dynamic SQL) without any Impersonation (IMPERSONATE
permission), cross-database ownership chaining (Server / Instance or Database setting), or TRUSTWORTHY
(Database setting). What you need to do, generally speaking, is:
- Create a Certificate in Database A
- Sign Stored Procedure in Database A using ADD SIGNATURE
- Create the same Certificate in Database B
- Create a User in Database B based on that Certificate
- Assign the new User the permissions needed to access the desired tables
Example:
CLEANUP
USE [master];
GO
IF EXISTS (SELECT 1 FROM [sys].[databases] WHERE [name] = N'DatabaseA')
BEGIN
PRINT 'Dropping [DatabaseA] DB...';
ALTER DATABASE [DatabaseA] SET OFFLINE WITH ROLLBACK IMMEDIATE;
ALTER DATABASE [DatabaseA] SET ONLINE;
DROP DATABASE [DatabaseA];
END;
IF EXISTS (SELECT 1 FROM [sys].[databases] WHERE [name] = N'DatabaseB')
BEGIN
PRINT 'Dropping [DatabaseB] DB...';
ALTER DATABASE [DatabaseB] SET OFFLINE WITH ROLLBACK IMMEDIATE;
ALTER DATABASE [DatabaseB] SET ONLINE;
DROP DATABASE [DatabaseB];
END;
IF (SUSER_ID(N'JohnnyLunchbucket') IS NOT NULL)
BEGIN
PRINT 'Dropping [JohnnyLunchbucket] Login...';
DROP LOGIN [JohnnyLunchbucket];
END;
IF (OBJECT_ID(N'tempdb..#CertInfo') IS NOT NULL)
BEGIN
PRINT 'Dropping [#CertInfo] Temp Table...';
DROP TABLE #CertInfo;
END;
SETUP
USE [master];
EXECUTE AS LOGIN = N'sa';
PRINT 'Creating databases...';
CREATE DATABASE [DatabaseA] COLLATE Latin1_General_100_CI_AS_SC;
CREATE DATABASE [DatabaseB] COLLATE Latin1_General_100_CI_AS_SC;
REVERT;
GO
ALTER DATABASE [DatabaseA] SET DB_CHAINING OFF;
ALTER DATABASE [DatabaseA] SET TRUSTWORTHY OFF;
ALTER DATABASE [DatabaseB] SET DB_CHAINING OFF;
ALTER DATABASE [DatabaseB] SET TRUSTWORTHY OFF;
GO
CREATE LOGIN [JohnnyLunchbucket] WITH PASSWORD = 'OhSoSecure;)';
USE [DatabaseA];
CREATE USER [JohnnyLunchbucket] FOR LOGIN [JohnnyLunchbucket];
GO
CREATE PROCEDURE dbo.RunReport
AS
SET NOCOUNT ON;
SELECT * FROM [DatabaseB].[dbo].[RestrictedTable];
GO
GRANT EXECUTE ON dbo.RunReport TO [JohnnyLunchbucket];
CREATE CERTIFICATE [PermissionsCert]
AUTHORIZATION [dbo]
ENCRYPTION BY PASSWORD = 'WeakPassword'
WITH SUBJECT = 'Used to test granting permissions to code',
EXPIRY_DATE = '2099-12-31';
ADD SIGNATURE TO [dbo].[RunReport]
BY CERTIFICATE [PermissionsCert]
WITH PASSWORD = 'WeakPassword';
SELECT CERTENCODED(CERT_ID(N'PermissionsCert')) AS [PublicKey],
CERTPRIVATEKEY(CERT_ID(N'PermissionsCert'), 'OtherPassword', 'WeakPassword')
AS [PrivateKey]
INTO #CertInfo;
GO
USE [DatabaseB];
DECLARE @SQL NVARCHAR(MAX);
SELECT @SQL = N'CREATE CERTIFICATE [PermissionsCert] AUTHORIZATION [dbo] FROM BINARY = '
+ CONVERT(NVARCHAR(MAX), [PublicKey], 1)
+ N' WITH PRIVATE KEY (BINARY = '
+ CONVERT(NVARCHAR(MAX), [PrivateKey], 1)
+ N', DECRYPTION BY PASSWORD = N''OtherPassword'''
+ N', ENCRYPTION BY PASSWORD = ''WeakPassword'');'
FROM #CertInfo;
PRINT @SQL;
EXEC (@SQL);
CREATE USER [PermissionsUser] FROM CERTIFICATE [PermissionsCert];
CREATE TABLE dbo.[RestrictedTable]
(
[ID] INT NOT NULL IDENTITY(1, 1)
CONSTRAINT [PK_RestrictedTable] PRIMARY KEY,
[Other] VARCHAR(50)
);
GRANT SELECT ON [dbo].[RestrictedTable] TO [PermissionsUser];
INSERT INTO dbo.[RestrictedTable] ([Other]) VALUES ('Ta da!');
GO
TEST
-- Quick test to show that [PermissionsUser] cannot be Impersonated:
USE [DatabaseB];
EXECUTE AS USER = 'PermissionsUser';
/*
Msg 15517, Level 16, State 1, Line 123
Cannot execute as the database principal because the principal "PermissionsUser" does not
exist, this type of principal cannot be impersonated, or you do not have permission.
*/
-- Main test:
USE [DatabaseA];
EXECUTE AS LOGIN = 'JohnnyLunchbucket';
SELECT SESSION_USER AS [User], ORIGINAL_LOGIN() AS [OriginalLogin];
GO
SELECT * FROM [DatabaseB].[dbo].[RestrictedTable];
/*
Msg 916, Level 14, State 1, Line 144
The server principal "JohnnyLunchbucket" is not able to access the database "DatabaseB"
under the current security context.
*/
EXEC [dbo].[RunReport]; -- SUCCESS!!!
-- 1 Ta da!
REVERT;
SELECT SESSION_USER AS [User], ORIGINAL_LOGIN() AS [OriginalLogin];
GO
For more info on Impersonation vs Module Signing you can review this other answer of mine (and the answer linked in it):
SQL Server Impersonation is just NOT working