3

I am trying to set a database to single user using SQL Server SMO and have the code below. This code is an abridged excerpt from a larger restore script

$Credential = Get-Credential

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | Out-Null;
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SmoExtended") | Out-Null;

$srv = New-Object Microsoft.SqlServer.Management.Smo.Server "MY-SERVER1";
$srv.ConnectionContext.NonPooledConnection = $true;
$srv.ConnectionContext.ConnectTimeout = 0;
$srv.ConnectionContext.StatementTimeout = 0;

$srv.ConnectionContext.LoginSecure = $false;
$srv.ConnectionContext.set_Login($Credential.username);
$srv.ConnectionContext.set_SecurePassword($Credential.password);
$srv.ConnectionContext.DatabaseName = "master";

$srv.ConnectionContext.Connect()

($srv.ConnectionContext.ExecuteWithResults("SELECT DB_NAME(),@@SERVERNAME")).Tables # verify database and servername we are connected to

$srv.Databases.Item("MyDB").UserAccess = "Single";
$srv.KillAllProcesses("MyDB");

try
{
    $srv.Databases.Item("MyDB").Alter([Microsoft.SqlServer.Management.Smo.TerminationClause]::RollbackTransactionsImmediately);
}
catch
{
    Write-Host $_.Exception.GetBaseException().Message;
    Write-Host "";
}

$srv.ConnectionContext.Disconnect();

The account I pass into $credential has the following permissions in SQL Server:

GRANT CREATE ANY DATABASE TO [MyUser]
GRANT VIEW SERVER STATE TO [MyUser] 
GRANT ALTER ANY DATABASE TO [MyUser] 
GRANT ALTER ANY CONNECTION TO [MyUser]
ALTER SERVER ROLE [dbcreator] ADD MEMBER [MyUser]

When I run the Powershell code above, it fails on the $srv.Databases.Item("MyDB").Alter line with the error

The server principal "MyUser" is not able to access the database "MyDB" under the current security context.

when I open a profiler trace, I can see that it is running USE [MyDB] and MyUser doesn't exist in that database which explains the error.

if I wanted to do this in Management studio, I would run the ALTER DATABASE statement against the master database:

USE master;
ALTER DATABASE [MyDB]  SET SINGLE_USER WITH ROLLBACK IMMEDIATE;

which works when run as MyUser in SSMS.

How do I stop the SMO alter command trying to switch the database context to the MyDB my user doesn't have access to?

If possible, I am looking to amend the existing code using SMO, rather than re-write using Invoke-SqlCmd or something else as this script is used extensively in production. The script was written originally using SMO as that was the best way identified to Set a database to single user mode and Restore the database using the same SPID

2
  • can't you just use Invoke-Sqlcmd ? Commented Nov 25, 2021 at 16:16
  • I thought about using Invoke-SqlCmd, however, this SMO script is currently in production so I would like to amend it if possible rather than a full re-write
    – SE1986
    Commented Nov 25, 2021 at 20:44

1 Answer 1

1
+200

I don't know about the PowerShell / SMO side of things, but there is something you can do that will work without requiring any modification to the PowerShell script. Just allow MyUser to connect to any database (without adding that login to any particular database) by granting the CONNECT ANY DATABASE instance/server -level permission:

GRANT CONNECT ANY DATABASE TO [MyUser];

I don't see this as a security risk given that you're already granting this login the CREATE ANY DATABASE, ALTER ANY DATABASE, and VIEW SERVER STATE permissions.

For example:

SETUP

USE [master];
CREATE LOGIN [MyUser] WITH PASSWORD=N'Pop-Pop';

GRANT CREATE ANY DATABASE TO [MyUser]
GRANT VIEW SERVER STATE TO [MyUser] 
GRANT ALTER ANY DATABASE TO [MyUser] 
GRANT ALTER ANY CONNECTION TO [MyUser]
ALTER SERVER ROLE [dbcreator] ADD MEMBER [MyUser]

TEST 1

EXECUTE AS LOGIN = N'MyUser';

USE [_SecurityTest];
-- error (as noted in the question)

REVERT;

APPLY FIX

GRANT CONNECT ANY DATABASE TO [MyUser];

TEST 2

EXECUTE AS LOGIN = N'MyUser';

USE [_SecurityTest]; -- Success!

SELECT USER_ID(), SESSION_USER;
-- 0    MyUser
-- 0 = public role

REVERT;
6
  • 1
    Hi @solomon, thanks for this. This will probably be a suitable workaround if an SMO solution isn't possible. What I didn't want to do was explicitly create the user in every database I need to restore but your GRANT CONNECT ANY DATABASE is a bit cleaner than having to do that :)
    – SE1986
    Commented Nov 30, 2021 at 15:40
  • @SEarle1986 Hi there. Makes sense to wait for a PowerShell/SMO answer, but it looks like one isn't being provided (and this doesn't look like something that would be easily resolvable at the PowerShell/SMO level). It might be too late, but would you consider either accepting my answer or awarding the bounty? Else it will default to only awarding 50% if the time runs out. Thanks. Commented Dec 6, 2021 at 4:45
  • awarded - thanks
    – SE1986
    Commented Dec 6, 2021 at 11:58
  • - Apologies, it looks like I only accepted the answer, not awarded the bounty. I though accepting it awarded the bounty Sorry
    – SE1986
    Commented Dec 7, 2021 at 14:47
  • @SEarle1986 Is quite ok. I didn't say anything because the system automatically awards the full bounty to an accepted answer so long as that answer was posted within the bounty period. So in a sense, accepting does award the bounty, just not immediately. If you want to award the bounty to a non-accepted answer, or an accepted answer that was posted prior to the bounty being created, then you must do that manually. So, thanks for accepting 😺. Commented Dec 7, 2021 at 14:52

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.