It did not work because the EXECUTE AS
clause of a CREATE {object}
statement can only reference a User, which is a database-level principal. CREATE DATABASE
is an instance-level permission, so it needs to be granted to a login (an instance-level principal), not to a user.
This is easy to accomplish using Module Signing:
SETUP
USE [tempdb];
CREATE LOGIN [DbCreator] WITH PASSWORD='create a DB';
CREATE USER [DbCreator] FOR LOGIN [DbCreator];
EXEC(N'CREATE PROCEDURE dbo.CreateDatabase
(
@NewDatabaseName sysname
)
AS
SET NOCOUNT ON;
DECLARE @SQL NVARCHAR(MAX);
SET @SQL = N''CREATE DATABASE '' + QUOTENAME(@NewDatabaseName);
EXEC(@SQL);
');
GRANT EXECUTE ON dbo.[CreateDatabase] TO [DbCreator];
APPLY MODULE SIGNING
-- 1) Create the Certificate:
CREATE CERTIFICATE [Permission$CreateDatabase]
ENCRYPTION BY PASSWORD = 'UseBetterPassword!'
WITH SUBJECT = 'CREATE DATABASE permission',
EXPIRY_DATE = '2099-12-31';
-- 2) Sign the Module:
ADD SIGNATURE
TO [dbo].[CreateDatabase]
BY CERTIFICATE [Permission$CreateDatabase]
WITH PASSWORD = 'UseBetterPassword!';
-- 3) Backup the Private Key to a VARBINARY string
-- to be copied and pasted somewhere safe (optional):
SELECT CERTPRIVATEKEY(CERT_ID(N'Permission$CreateDatabase'),
'NewPassword!', 'UseBetterPassword!');
-- 4) Remove the Private Key (optional):
ALTER CERTIFICATE [Permission$CreateDatabase]
REMOVE PRIVATE KEY;
-- 5) Copy Certificate to [master] (Public Key only):
DECLARE @Cert NVARCHAR(4000) =
CONVERT(NVARCHAR(4000),
CERTENCODED(CERT_ID(N'Permission$CreateDatabase')), 1);
EXEC (N'USE [master];
CREATE CERTIFICATE [Permission$CreateDatabase]
FROM BINARY = ' + @Cert);
-- 6) Create Login and Grant Permission:
EXEC (N'USE [master];
CREATE LOGIN [Permission$CreateDatabase]
FROM CERTIFICATE [Permission$CreateDatabase];
GRANT CREATE ANY DATABASE TO [Permission$CreateDatabase];');
TEST
EXECUTE AS LOGIN = N'DbCreator';
SELECT SESSION_USER;
-- DbCreator
CREATE DATABASE [_TEST:FAIL];
/*
Msg 262, Level 14, State 1, Line XXXXX
CREATE DATABASE permission denied in database 'master'.
*/
EXEC dbo.[CreateDatabase] N'_TEST:PASS';
-- Success!!
SELECT * FROM sys.databases WHERE [name] = N'_TEST:PASS';
REVERT;
SELECT SESSION_USER;
-- dbo
DROP DATABASE [_TEST:PASS];
For a detailed explanation of the steps taken to apply module signing, please see my post:
Safely and Easily Use High-Level Permissions Without Granting Them to Anyone: Server-level
For more information about module signing in general, and why you should use it instead of the easier yet more dangerous SET TRUSTWORTHY ON
, please see my post:
PLEASE, Please, please Stop Using Impersonation, TRUSTWORTHY, and Cross-DB Ownership Chaining