Inspired by various schema related questions I've seen...
Ownership chaining allows me to GRANT EXECUTE on a stored procedure without explicit permissions on tables I use, if both stored procedure and tables are in the same schema.
If we use separate schemas then I'd have to explicitly GRANT XXX on the the different-schema tables. The ownership chaining example demonstrates that. This means the stored proc executing user can read/write your tables directly.
This would be like having direct access to your instance variables in a class, bypassing getter/setters, breaking encapsulation.
We also use row level security to restrict what someone sees and we apply this in the stored procedures.
So, how can we maintain schema separation and prevent direct table access?
Of course, the question won't apply if you use an ORM or don't use stored procs. But I'm not asking if I should use an ORM or stored proc in case anyone feels the need to enlighten me...
Edit, example
CREATE USER OwnsMultiSchema WITHOUT LOGIN GO CREATE SCHEMA MultiSchema1 AUTHORIZATION OwnsMultiSchema GO CREATE SCHEMA MultiSchema2 AUTHORIZATION OwnsMultiSchema GO CREATE USER OwnsOtherSchema WITHOUT LOGIN GO CREATE SCHEMA OtherSchema AUTHORIZATION OwnsOtherSchema GO CREATE TABLE MultiSchema1.T1 (foo int) GO CREATE TABLE MultiSchema2.T2 (foo int) GO CREATE TABLE OtherSchema.TA (foo int) GO CREATE PROC MultiSchema1.P1 AS SELECT * FROM MultiSchema1.T1 SELECT * FROM MultiSchema2.T2 SELECT * FROM OtherSchema.TA Go EXEC AS USER = 'OwnsMultiSchema' GO --gives error on OtherSchema EXEC MultiSchema1.P1 GO REVERT GO CREATE PROC OtherSchema.PA AS SELECT * FROM MultiSchema1.T1 SELECT * FROM MultiSchema2.T2 SELECT * FROM OtherSchema.TA Go GRANT EXEC ON OtherSchema.PA TO OwnsMultiSchema GO EXEC AS USER = 'OwnsMultiSchema' GO --works EXEC OtherSchema.PA GO REVERT GO
Edit 2:
The U1 user has the CREATE VIEW permission on the database and the SELECT permission on the S1 schema. Therefore, the U1 user can create a view in the S1 schema to query data from the denied object T1, and then access the denied object T1 by using the view.
In SSMS, if you follow the path [Database] > Security > Schemas and view any schema properties, you have a tab "permissions" that list all the permissions that every user have on that specific schema.
To create an object specified within the CREATE SCHEMA statement, the user must have the corresponding CREATE permission. To specify another user as the owner of the schema being created, the caller must have IMPERSONATE permission on that user.
I fear that either your description or your conception of Ownership Chaining is unclear, so let me start with that:
"Ownership Chaining" simply refers to that fact that when executing a Stored Procedure (or View) on SQL Server, the currently executing batch temporarily acquires the rights/permissions of the sProc's Owner (or the sProc's schema's Owner) while executing that SQL code. So in the case of a sProc, the User cannot use those privs to do anything that the sProc code does not implement for them. Note especially that it never acquires the Identity of the Owner, only it's rights, temporarily (however, EXECUTE AS... does do this).
So the typical approach to leverage this for security is to:
Put all of the Data Tables (and all non-security Views as well) into their own Schema, let's call it [data] (though typically [dbo] is used because it's already there and too privileged for the User's schema). Make sure that no existing Users, Schemas or Owners have access to this [data] schema.
Create a schema called [exec] for all of the sProcs (and/or possibly any security Views). Make sure that the owner of this schema has access to the [data] schema (this is easy if you make dbo the owner of this schema).
Create a new db-Role called "Users" and give it EXECUTE access to the [exec] schema. Now add all users to this role. Make sure that your users only have Connect rights and have no granted access to any other schema, including [dbo].
Now your users can access the data only by executing the sProcs in [exec]. They cannot access any other data or execute any other objects.
I am not sure if this answers your question (because I was uncertain what the question was exactly), so feel free to redirect me.
As for row-level security, here is how I always do it with the security scheme above:
I always implement row-level security as a series of Views that mirror-wrap every table and compare the User's identity (usually with Suser_Sname() or one of the others) to a security list keyed from a security code in the row itself. These are the Security-Views.
Create a new schema called [rows], give it's owner access to the [data] schema and nothing else. Put all of the Security-Views in this schema.
Revoke the [exec] owner's access to the [data] schema and instead grant it data access to the [rows] schema.
Done. Now row-level security has been implemented by transparently slipping it between the sProcs and the tables.
Finally, here is a stored procure that I use to help me remember how much of this obscure security stuff works and interacts with itself (oops, corrected version of code):
CREATE proc [TestCnxOnly].[spShowProc_Security_NoEX] as --no "With Execute as Owner" for this version --create User [UserNoLogin] without login --Grant connect on database :: TestSecurity to Guest --alter database TestSecurity set trustworthy on --Show current user context: select current_user as current_ , session_user as session , user_name() as _name , suser_name() as [suser (sproc)] , suser_sname() as sname , system_user as system_ --Execute As Login = 'UserNoLogin' select current_user as current_ , session_user as session , user_name() as _name , suser_name() as [suser (after exec as)] , suser_sname() as sname , system_user as system_ EXEC('select current_user as current_ , session_user as session , user_name() as _name , suser_name() as [suser (in Exec(sql))] , suser_sname() as sname , system_user as system_') EXEC sp_ExecuteSQL N'select current_user as current_ , session_user as session , user_name() as _name , suser_name() as [suser (in sp_Executesql)] , suser_sname() as sname , system_user as system_' --Revert select current_user as current_ , session_user as session , user_name() as _name , suser_name() as [suser (aftr revert)] , suser_sname() as sname , system_user as system_
[EDIT: corrected version of code)
My 2c: Ownership chaining is legacy. It dates from days when there was no alternatives, and compared with today's alternatives is unsecure and coarse.
I say the alternative is not schema permissions, the alternative is code signing. With code signing you can grant the needed permissions on the signature of the procedure, and grant wide execute access on the procedure while the data access is tightly controlled. Code signing offers more granular and more precise control, and it cannot be abused the way ownership chaining can. It works inside the schema, it works across the schema, it works across the database and does not require the huge security hole of cross database ownership chaining to be open. And it doesn't require the hijacking of the object ownership for access purposes: the owner of the procedure can be any user.
As for your second question about row level security: row level security doesn't really exist in SQL Server versions 2014 and earlier, as a feature offered by the engine. You have various workarounds, and those workarounds work actually better with code signing than with ownership chaining. Since sys.login_token contains the context signatures and countersignatures, you can actually do more complex checks than you could in an ownership chaining context.
Since version 2016 SQL Server fully supports row level security.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With