I'm looking for a recommended best practice for dividing functions and stored procedures into a 'public API' and 'private implementation details'.
I'm working on a security system and I want client apps to only access a handful of top-level functions. For example:
CanUserEditObject(@personId, @objectId)
— called to see if an edit is possibleEditableBy(@personId)
— returns a list of editable object ids
These form the API I want client apps to use.
To implement that, I need some other functions which I don't want client apps to call;
GetLocationsAdministeredBy(@personId)
IsUserSysadmin(@personId)
Client apps have no need to call them, and I don't promise to support them going forward — they're just an implementation detail. So I'd like to hide them. Ideally, any attempt to call it by a client application will fail. Ideally ideally, I'd be able to call them in SQL Server Management Studio to develop them.
Basically, this is inspired by the OO principle of information hiding and language constructs like public
and private
access modifiers in C++/Java/C#/etc.
I don't need it to be a guaranteed, hard-security kind of solution. I basically trust my clients here, but I just don't want some plucky developer to spot a function I've written and use it, then complain when I rewrite it later.
This is mainly for SQL Server 2014, but I'm interested if there are any 'standard' SQL techniques.
Also, we have a team where some of us are better at SQL than others. The SQL people would add their own objects, but they wouldn't use my 'private' functions. The other devs might discover existing functions rather than add them. I'm looking for a way to differentiate so that no-one accidentally relies on an unsupported function. Something as simple as a naming convention would do but I am interested how other people muddle through with an issue that most non-SQL languages have good support for.
Best Answer
At the very basic level (and this is for SQL Server only as each vendor handles security slightly differently), IF others were not creating their own database objects (Stored Procedures, Functions, etc) and relying solely upon your API, then this is handled inherently through "Ownership Chaining". Ownership Chaining allows for implied permissions on objects referenced within a module so long as as the referenced objects have the same "owner" as the "owner" of the module being executed, and those referenced objects are not being referenced within Dynamic SQL.
Ownership Chaining
The following example illustrates this behavior. The
Test
Login cannot directly access the Function because noEXECUTE
permission has been granted on it. But when it is used in a Stored Procedure that theTest
Login / User has been grantedEXECUTE
permission on, then it works because both are in the same Schema (i.e.dbo
). However, the ownership chain is broken by Dynamic SQL, which is why the second Stored Procedure,PublicDoSomethingDS
, gets an error.BUT, if others can create their own objects -- Stored Procedures, Functions, Views, etc -- then Ownership Chaining does't get you anywhere since someone creating their own module in the
dbo
Schema would then have access to those "private" modules.In that case, you should be able to accomplish this separation with a little more work by using signatures and module signing.
Module Signing
Module Signing is using an Asymmetric Key or Certificate to create a Login and/or User from, assign the desired permission(s) to that Asymmetric Key-based or Certificate-based Login and/or User, and then "sign" the modules that should be given those permissions using the ADD SIGNATURE command (introduced in SQL Server 2008).
This approach is a little more complicated and takes a little more time to set up, but allows for very granular control of permissions.
Assuming you have fully run the example code shown above, now run the following:
We created a new Schema and put the new Function in it, yet the
Test
User was still able to executePublicDoSomethingPrvt
. Why? Because the "owner" of thePrivate
Schema isdbo
, which is the same "owner" of thedbo
Schema. Hence Ownership Chaining is still allowing this to work.But we can finally break that by running the following:
Now there is no more Ownership Chain. And in fact, even if you were to grant
EXECUTE
on the[Private].[GetRowCount]
Function to theTest
User, they would still get an error because there is no more automatic link between the Function and the Table that it selects from.So now what? Well, now we create the Asymmetric Key, then the User from that Key, then grant that User the desired permission(s), and then sign the Stored Procedure with that same Asymmetric Key:
Almost there. The
PublicDoSomethingPrvt
Stored Procedure (not theTest
User!) can now execute the Function, but that Function still has no inherent link to the Table, which is in another Schema that has a different owner. Now we just add that final permission to the Asymmetric Key-based User and then sign the Function:As you can see, the
Test
User only has the permissions explicitly granted to it, namelyEXECUTE
on thePublicDoSomethingPrvt
Stored Procedure.Notes:
ADD SIGNATURE
).EXECUTE AS
clause of theCREATE
statement. This ensures that someone doesn't change a situation that you were fine with into something that you aren't ok with. So when definitions change, you need to re-run theADD SIGNATURE
command.MrX
andPrvtUser
), that was just for the demo code. You could use the Asymmetric Key-based User to own the "Private" Schema.EXECUTE
on thePrivate
Schema,SELECT
on thedbo
Schema, etc).Private
Schema. Then, create another Asymmetric Key and associated User to allow the "private" modules access to objects in the "public" Schema(s).