Securing access to data for admin and dbo

In this blog, I want to explore what you can do to block the owner of a database from doing stuff in the database they “own”.  Own is a strange term, because really there is just one user that is listed as owner, but there are there are three users who essentially are owner level, super-powered users in a database:

1. A login using server rights, usually via the sysadmin server role (or a server permission to view all data)
2. The user dbo in a database, acquired either as a sysadmin, or as being the user listed as the owner of the database
3. Members of the db_owner database role

Sometimes, in the context of a database, these all start to blur together. But they are definitely all three independent things. Let’s write some code and see the differences, and one of the cases may be surprising to you. To do this, I will just be using the SELECT permission on a single table, but other rights will generally behave similarly. Note that another tool in your toolbox is Row Level Security in SQL Server 2016+. It is different, in that you can include a predicate that excludes the dbo, which you can read more about here in some of my earlier blogs.

For our tests, we will use the following security objects. 

The sysadmin role is generally consider the super, ignore all other security user, and this is generally the case. Let’s start the example by creating a server standard login, and make it a part of the sysadmin server role. I am using a standard loging to cover pretty much all typical cases, including how things work for you using a domain account in Windows Authentication, in Linux, etc. If you are working on a laptop where you log in using a hotmail/outlook account to log in, things behave in an atypical manner, so the standard login is the simplest.

--If you wish, you can use the user you are logged in as instead of the TypicalAdminLogin, but this works for any case
CREATE LOGIN TypicalAdminLogin WITH PASSWORD = '33nqq0u2cnX$v3'; 
GO
ALTER SERVER ROLE sysadmin ADD MEMBER TypicalAdminLogin;
GO

A second login will be used to show what happens when a user that is not the owner of the database, but is sysadmin tries to access the data. We will use this login to try out the server level permissions also

CREATE LOGIN AnotherTypicalAdminLogin WITH PASSWORD = '33nqq0u2cnX$v3';
GO
ALTER SERVER ROLE sysadmin ADD MEMBER AnotherTypicalAdminLogin;
GO

If you wish, you can use the user you are logged in or disconnect and relogin as the TypicalAdminLogin login for the demonstration.

Next create a new db, as that login, so you are logged in as the owner of the database:

CREATE DATABASE TestDatabaseOwnerSecurity;
GO
USE TestDatabaseOwnerSecurity;
GO

To test access, I will create a table named testRights, with a single test row, and will not grant any user SELECT privileges on this table:

CREATE TABLE dbo.TestRights
(
     value varchar(20) CONSTRAINT PKTestRights PRIMARY KEY
);
INSERT INTO dbo.testRights (value)
VALUES ('Can See');
GO

To enable easy checking of status of each scenario, I will create the following procedure. It will give me the owner of the db, and the context of the user. I used EXECUTE with a literal to avoid the security aspects of stored procedures, so it is simply shorthand to not repeat a lot of code throughout the blog;

CREATE OR ALTER PROCEDURE dbo.Security$CheckStatus
AS
EXECUTE ('SELECT SUSER_SNAME(owner_sid) AS DatabaseOwner,
           SUSER_SNAME() AS ServerPrincipal,
           USER_NAME() AS DBPrincipal,
           IS_MEMBER(''db_owner'') AS memberDBO,
           IS_SRVROLEMEMBER(''sysadmin'') AS memberSysAdmin
FROM sys.databases
WHERE name = ''testDatabaseOwnerSecurity'';

SELECT value
FROM dbo.TestRights;')
GO
GRANT EXECUTE ON dbo.Security$CheckStatus TO PUBLIC; --everyone should be able to execute this

Now, let’s see the output when the user that created the database is :

EXECUTE dbo.Security$CheckStatus

DatabaseOwner ServerPrincipal     DBPrincipal    memberDBO   memberSysAdmin
TypicalAdminLogin   TypicalAdminLogin   dbo            1 1
         
value        
Can See        

As expected. We have not granted any rights for this user to have access, so we know that we cannot REVOKE any rights to change anything. So applying a DENY is our only chance to remove the rights of the DBO. So let’s start by attempting to remove rights from the db_owner role:

DENY SELECT ON OBJECT::dbo.TestRights TO db_owner;
DENY SELECT ON OBJECT::dbo.TestRights TO dbo;

To which you are greeted with:

Msg 4617, Level 16, State 1, Line 76
Cannot grant, deny or revoke permissions to or from special roles.

Cannot grant, deny, or revoke permissions to sa, dbo, entity owner, information_schema, sys, or yourself.

The first is an error message, the next a message without error message number, but clearly, that did NOT work. So clearly we cannot revoke the rights from the group directly. What about indirectly? The error message may lean towards the following not working, but let’t DENY access to the object to the public role, since everyone is a member of this role and it will be simpler. Doesn’t have to be public, it can be any other group that the users you are trying to DENY are a member of

DENY SELECT ON OBJECT::dbo.TestRights TO public; 

Ok, so what is this going to do? More than you may expect, actually. Clearly no user who is not one of the special roles is going to have access. But what about the three cases we have set up?

1. Members of a server role
2. The user dbo in a database
3. Members of the db_owner database role

Members of a server role

The obvious example, which we will start on is the sysadmin of the server. Later, we will also see how the SELECT ALL USER SECURABLES permission works.

Let’s try in our current context as the sysadmin/owner of the database

EXECUTE dbo.Security$CheckStatus;

Perhaps expected, the user can see still the data as they are dbo, sysadmin, and dbowner role.

DatabaseOwner ServerPrincipal     DBPrincipal    memberDBO   memberSysAdmin
TypicalAdminLogin   TypicalAdminLogin   dbo            1 1
         
value        
Can See        

 

Now, let’s try as the admin that we created that is not the owner of the database

EXECUTE AS LOGIN = 'AnotherTypicalAdminLogin';
GO
EXECUTE dbo.Security$CheckStatus;
GO
REVERT;

This returns:

DatabaseOwner ServerPrincipal     DBPrincipal    memberDBO   memberSysAdmin
TypicalAdminLogin   AnotherTypicalAdminLogin dbo            1 1
         
value        
Can See        

 

Expected. But now, let’s try creating a user for that login in the database, and use DENY to try to stop access:

CREATE USER AnotherTypicalAdminLogin FOR LOGIN AnotherTypicalAdminLogin;
DENY SELECT ON OBJECT::dbo.TestRights TO AnotherTypicalAdminLogin;
GO

Execute the Security$CheckStatus procedure as before, and you will see the exact same results, so sysadmin is sysadmin, no matter any users in the database. If you remove the sysadmin server role from the server user.

ALTER SERVER ROLE sysadmin DROP MEMBER AnotherTypicalAdminLogin;

Then this would be a completely different setup, with this login having access to the database from their user (which is automatically granted the CONNECT database right), and you will see things change to no access to the table, and not a member of any special role:

DatabaseOwner ServerPrincipal     DBPrincipal    memberDBO   memberSysAdmin
TypicalAdminLogin   AnotherTypicalAdminLogin AnotherTypicalAdminLogin 0 0

 

Msg 229, Level 14, State 5, Line 141
The SELECT permission was denied on the object 'testRights', database 'TestDatabaseOwnerSecurity', schema 'dbo'.

So sysadmin is not affected by a DENY, but what about other server security? For example, using the following 3 GRANT statements, we can configure the user to have access to all of the data on a server, but will the DENY affect this?

USE Master;
--These rights are an awesome way to create a read-only admin role
GRANT VIEW ANY DATABASE to AnotherTypicalAdminLogin; --see any database
GRANT CONNECT ANY DATABASE to AnotherTypicalAdminLogin; --set context to any database
GRANT SELECT ALL USER SECURABLES to AnotherTypicalAdminLogin; --see any data in databases
GO
USE TestDatabaseOwnerSecurity;

Now trying to access the data again:

EXECUTE AS LOGIN = 'AnotherTypicalAdminLogin'
GO
EXECUTE dbo.Security$CheckStatus
GO
REVERT;

And you will see the very same failure as before, which is exactly what you probably desire. Hence, these rights do NOT override local rights. This is definitely a very fringe case to configure, because it would be very rare to DENY data to public.. so you would need the login to have a user in the database to DENY them, but it is what it is.

Removing the DENY from the USER, and even dropping the user will not change anything:

REVOKE SELECT ON OBJECT::dbo.TestRights TO AnotherTypicalAdminLogin;
DROP USER AnotherTypicalAdminLogin;

Only if you were to REVOKE the deny to public will the AnotherTypicalAdminLogin (now a server login only if you try both of those two statements), will then open up rights

REVOKE SELECT ON OBJECT::dbo.TestRights TO public;
GO
EXECUTE AS LOGIN = 'AnotherTypicalAdminLogin';
GO
EXECUTE dbo.Security$CheckStatus;
GO
REVERT;
GO

This now allows the user to execute the code:

DatabaseOwner ServerPrincipal     DBPrincipal    memberDBO   memberSysAdmin
TypicalAdminLogin   AnotherTypicalAdminLogin AnotherTypicalAdminLogin 0 0
         
value        
Can See        

 

This is really great news for the system administrator. Using server rights, you can give a login access to either:

* ALL data on the server as a sysadmin
* All un-denied data on the server using SELECT ALL USER SECURABLES and rights to connect to a database.

--put the DENY back for the rest of the examples
DENY SELECT ON OBJECT::dbo.TestRights TO public;

The user dbo in a database.

The next case we will try is when a user is mapped to the dbo user, specifically. We have already shown that we can’t stop the sysadmin, nor can we deny the dbo specifically. Now, lets attempt to add a login, make it the owner of the db, but no other rights in the database or server, other than ownership of the database:

CREATE LOGIN NonAdminLogin WITH PASSWORD = '33nqq0u2cnX$v3';
ALTER AUTHORIZATION ON DATABASE::TestDatabaseOwnerSecurity TO NonAdminLogin;
GO

Now try to access the date using this login, that is the database owner:

EXECUTE AS LOGIN = 'NonAdminLogin'
GO
EXECUTE dbo.Security$CheckStatus
GO
REVERT;
GO

This user is translated, as are sysadmin role logins, to the dbo user in the database, and as such, has full access:

DatabaseOwner ServerPrincipal     DBPrincipal    memberDBO   memberSysAdmin
NonAdminLogin  NonAdminLogin dbo 0 0
         
value        
Can See        

 

So the user dbo cannot be denied, either by statement, or by public.

Members of the db_owner database role

Finally, let’s try one more thing, that intuitively, seems unnecessary to try. I know, at least by name, I expected that “member of db_owner database role” = “dbo”. But… is that true? Created a user named TestDbo in the datbase and make it a member of the db_owner role . It has no login to get security from, so this is the only right applied.

CREATE USER TestDbo WITHOUT LOGIN;

ALTER ROLE db_owner ADD MEMBER TestDbo;
GO

Executing the query, you get probably, NOT what you expect (I didn’t!):

EXECUTE AS User = 'TestDbo'
GO
EXECUTE dbo.Security$CheckStatus
GO
REVERT;
GO

DatabaseOwner ServerPrincipal     DBPrincipal    memberDBO   memberSysAdmin
NonAdminLogin  S-1-9-3-2245412934-107... testDbo 1 0

Msg 229, Level 14, State 5, Line 282
The SELECT permission was denied on the object 'testRights', database 'TestDatabaseOwnerSecurity', schema 'dbo'.

Membership in the db_owner role is not exactly the same as being the dbo. The dbouser, like sysadmin server role members, is not subject to any “typical” security, but role members are. 

Summary

Security is a MESSY subject, and can be difficult to configure correctly. Users with too much security are very dangerous to the health and accessibility of your data. Too little and people are annoyed, but too much and you may end up sharing more to the user than expected.

Here we have covered the ways that you can affect a user’s access to data in several ways (Ignoring Row Level Security, which can also be useful):

Using server rights and roles. Sysadmin logins are all powerful, but using the SELECT ALL USER SECURABLES server permission can be counteracted by using DENY at the database level.

The user dbo in a database is all powerful, but members of the db_owner database role are not equivalent to the dbo user. These users are subject to a DENY, unlike the DBO.