It is true that you cannot grant EXEC
permissions on a function that returns a table. This type of function is effectively more of a view than a function. You need to grant SELECT instead, e.g.:
GRANT SELECT ON dbo.Table_Valued_Function TO [testuser];
So your script would look more like this (sorry, but I absolutely loathe INFORMATION_SCHEMA
and much prefer to use the catalog views, which also don't need functions like OBJECTPROPERTY
):
DECLARE
@sql NVARCHAR(MAX) = N'',
@username VARCHAR(255) = 'testuser';
SELECT @sql += CHAR(13) + CHAR(10) + N'GRANT ' + CASE
WHEN type_desc LIKE 'SQL_%TABLE_VALUED_FUNCTION'
OR type_desc = 'VIEW'
THEN ' SELECT ' ELSE ' EXEC ' END
+ ' ON ' + QUOTENAME(SCHEMA_NAME([schema_id]))
+ '.' + QUOTENAME(name)
+ ' TO ' + @username + ';'
FROM sys.all_objects
WHERE is_ms_shipped = 0 AND
(
type_desc LIKE '%PROCEDURE'
OR type_desc LIKE '%FUNCTION'
OR type_desc = 'VIEW'
);
PRINT @sql;
-- EXEC sp_executesql @sql;
Now you can grant EXEC
on a schema, and always create these procedures in that schema (actually one of the purposes of schemas!), which @jgardner04 already suggested, however in order for this solution to apply to table-valued functions as well, you'd also have to grant SELECT
. Which is okay if you're not storing any data in tables in that schema (or at least that you want to hide from them), but it will apply to any tables and views as well, which might not be your intention.
Another idea (e.g. if you can't, or don't want to, use schemas) is to write a DDL Trigger that captures the CREATE_PROCEDURE
, CREATE_FUNCTION
and CREATE_VIEW
events, and grants permissions to a user (or a set of users, if you want to store them in a table):
CREATE TRIGGER ApplyPermissionsToAllProceduressAndFunctions -- be more creative!
ON DATABASE FOR CREATE_PROCEDURE, CREATE_FUNCTION, CREATE_VIEW
AS
BEGIN
SET NOCOUNT ON;
DECLARE
@sql NVARCHAR(MAX),
@EventData XML = EVENTDATA();
;WITH x ( sch, obj )
AS
(
SELECT
@EventData.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(255)'),
@EventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(255)')
)
SELECT @sql = N'GRANT ' + CASE
WHEN o.type_desc LIKE 'SQL_%TABLE_VALUED_FUNCTION'
OR o.type_desc = 'VIEW'
THEN ' SELECT '
ELSE ' EXEC ' END
+ ' ON ' + QUOTENAME(x.sch)
+ '.' + QUOTENAME(x.obj)
+ ' TO testuser;' -- hard-code this, use a variable, or store in a table
FROM x
INNER JOIN sys.objects AS o
ON o.[object_id] = OBJECT_ID(QUOTENAME(x.sch) + '.' + QUOTENAME(x.obj));
EXEC sp_executesql @sql;
END
GO
The drawback I find with DDL Triggers is that you can quickly forget that they're there. So a year down the road when you decide to stop granting these permissions to all new objects, it might take a while to troubleshoot why it's still happening. At my last job we logged all actions invoked by DDL triggers to a central "event log" of sorts, and that was our go-to place for tracking down any actions that happened on the server that nobody seems to remember (and it was a DDL trigger about half the time). So you may consider adding some additional logic that will help with that.
EDIT
Adding code for schema-based, and I'll mention again that this will grant permissions on any procedures, functions and tables created in the foo schema.
CREATE SCHEMA foo;
GRANT EXEC, SELECT ON SCHEMA::foo TO testuser;
Now if you create the following procedure, testuser will be able to execute:
CREATE PROCEDURE foo.proc1
AS
BEGIN
SET NOCOUNT ON;
SELECT 1;
END
GO
Sounds like you've already made the decision to use T-SQL for your back-end. For OWASP reasons, you are correct in that calling only stored procedures from your data access layer is the safest approach to avoid SQL injection.
Stored procedures and functions are similar in that you can pass arguments for the parameter list. They also can do similar things. Code re-use is a good thing and will save you in the end when fixing bugs or making enhancements in the future. So find the logic that is similar and re-factor that code to functions. If you want to return tables (single result set), use table based functions. If you want to return single values, use scalar functions. Note, you won't be able to perform permanent environment changes with functions (no CRUD operations are allowed... CREATE/INSERT, READ, UPDATE, DELETE). It's also good practice to wrap your stored procedures with error/exception handling and return those exceptions to the server, to let your logging handle them appropriately. Also note that it's common to return multiple result sets in a single call to a stored procedure (3 SELECT statements for example). In your application code, you'd handle each table individually in the response.
Here are some differences:
Best Answer
To find unused stored procs, please see the answer as explained by Aaron Bertrand.
Also, there is an good read to identify as explained here in the article How to find unused Stored Procedures in SQL Server 2005/2008
a) Generate a List of non-recently-used code (by querying the plan cache).
b) Script the Views/Sprocs/etc that aren’t in cache (for whatever reason) using ALTER scripts. (i.e. the SSMS ‘Modify’ option when right-clicking on an object).
c) Execute the ALTER script generated by SSMS in production. Since the object wasn’t in cache, there’s no harm and no foul in worrying about recompilation overhead. And since the script is an ALTER (not a DROP/CREATE), permissions are retained intact.
d) But, if the object references invalid objects, it’ll throw an error and I can definitively know that it’s not only NOT used, but one of the culprits that’s causing me grief during replication snapshot procedures.
Also read as mentioned by Jonnathan@ here