Storing ASP.NET style security in SQL Server xml column? - c#

I would like to store security information for records in a SQL Server database. The security info would ideally be in the same form as what you might see in a config file, for consistency purposes:
<authorization>
<allow roles="Admins"/>
<allow users="SomeGuy,SomeOtherGuy"/>
<deny users="*"/>
</authorization>
I'd then like to be able to query the database for everything that a particular user is permitted access to, given their username and a list of their roles.
Does anyone have a suggestion on how best to do this? Or am I going about this the wrong way?
An easy brute force solution would be to just read every row in the database and pull each security rule XML into some class that will do the evaluation for me - but obviously that's going to be slow and on large tables will be unreasonable.
Another thing that comes to mind is making a child table of some kind which includes a priority of some kind to indicate the order in which each allow or deny node should be applied. However, I have quite a few tables that need this feature, and if I can avoid creating a ton of child tables, that would be ideal.
Though I have limited experience with XML columns in SQL Server, I can probably build an XML query to determine if a user is allowed - something starting with (/authorization/allow/#users)[1], perhaps. However, the order of the nodes matters, so while I could probably find a node that matches a given name or role, I don't know how to do any sort of set-based operation to check whether the user is denied or allowed based on which comes first.
So, given a user name and a comma delimited list of roles, what is the best way to check that person's access rights on a particular row in the database?

Well, i've come up with a solution, but it's not ideal. For 10,000 records, it takes 5 seconds to return all of the rows which match the security profile. This isn't a total disaster, and it does work, but i'll have to come back to this problem later to improve it.
Here's how i solved it. Keep in mind that i only worked on this for a few hours.
Before i could really do anything, i knew i was going to need a function to compare two comma delimited lists. I need to have a user's roles in a list, and see if any of those roles appear in the authorization settings stored in my xml column, as detailed in the original post. For this, i made two functions.
The first function is a commonly seen one to do string splitting using xml:
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnSplitStrings')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnSplitStrings
GO
CREATE FUNCTION dbo.ufnSplitStrings
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
With that function established, i could then create another function which would then do the comparison i wanted:
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnContainsAny')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnContainsAny
GO
CREATE FUNCTION dbo.ufnContainsAny(#List1 NVARCHAR(MAX), #List2 NVARCHAR(MAX))
RETURNS int
AS
BEGIN
DECLARE #Ret AS INT = 0
SELECT #Ret = COUNT(*) FROM dbo.ufnSplitStrings(#List1, ',') x
JOIN dbo.ufnSplitStrings(#List2, ',') y ON x.Item = y.Item
RETURN #Ret
END;
GO
Finally, i could use that function to assemble my main UserIsAuthorized function.
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnUserIsAuthorized')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnUserIsAuthorized
GO
CREATE FUNCTION dbo.ufnUserIsAuthorized(#SecurityRules XML, #UserName NVARCHAR(64), #UserRoles NVARCHAR(MAX))
RETURNS int
AS
BEGIN
DECLARE #ret int = 0;
DECLARE #AuthType NVARCHAR(32);
DECLARE #authRules Table (a nvarchar(32), u nvarchar(max), r nvarchar(max), o int)
INSERT INTO #authRules
SELECT
a = value.value('local-name(.[1])', 'varchar(32)'),
u = ',' + value.value('#users', 'varchar(max)') + ',',
r = ',' + value.value('#roles', 'varchar(max)') + ',',
o = value.value('for $i in . return count(../*[. << $i]) + 1', 'int')
FROM #SecurityRules.nodes('//allow,//deny') AS T(value)
SELECT TOP 1 #AuthType = a FROM #authRules
WHERE CHARINDEX(',' + #UserName + ',', u) > 0 OR CHARINDEX(',*,', u) > 0 OR dbo.ufnContainsAny(r, #UserRoles) > 0 OR CHARINDEX(',*,', r) > 0
GROUP BY a
ORDER BY MIN(o)
IF (#AuthType IS NOT NULL AND #AuthType = 'allow')
SET #ret = 1;
RETURN #ret;
END;
That function splits up the xml allow and deny nodes into a table which contains the authorization type (allow or deny), the users list, the roles list, and finally the order in which the particular node appears in the document. Finally, i can grab the first node where i find the user or one of the user's roles. If that node is "allow", then i return a 1.
Yeah, it's a bit horrendous because we're declaring a table in every single call. I tried various little tests where i only looked for the user name (to avoid having to make any calls to the ufnContainsAny), but the performance didn't change. I also tried changing the "o" column to a simple identity column, since i'm selecting all nodes - this would allow it to skip what i thought might be a time consuming calc of getting the order of the node. But that also didn't affect the performance.
So, not surprisingly this method needs work. If anyone has any suggestions, i'm all ears.
My initial usage of this feature will be very few rows, so i can use this in the interim until i come up with a better solution (or abandon this method altogether).
EDIT:
The performance can be dramatically improved by just skipping the DECLARE table / INSERT. Instead, we can do this:
SELECT TOP 1 #AuthType = a FROM
(
SELECT
a = value.value('local-name(.[1])', 'varchar(32)'),
u = ',' + value.value('#users', 'varchar(max)') + ',',
r = ',' + value.value('#roles', 'varchar(max)') + ',',
o = value.value('for $i in . return count(../*[. << $i]) + 1', 'int')
FROM #SecurityRules.nodes('//allow,//deny') AS T(value)
) AS sec
WHERE CHARINDEX(',' + #UserName + ',', u) > 0 OR CHARINDEX(',*,', u) > 0 OR dbo.ufnContainsAny(r, #UserRoles) > 0 OR CHARINDEX(',*,', r) > 0
GROUP BY a
ORDER BY MIN(o)

Related

How can I elegantly modify the number of affected rows returned by a Stored Procedure to ExecuteNonQuery() via the DONE_IN_PROC token?

I use a custom ORM generator that calls stored procedures, and validates the number of rows affected by UPDATE and DELETE statements using ExecuteNonQuery(), along these lines:
// Execute the stored procedure and get the number of rows affected.
int result = command.ExecuteNonQuery();
// Exactly one row was deleted, as expected.
if (result == 1)
return true;
// No rows were deleted. Maybe another user beat us to it. Fine.
if (result == 0)
return false;
// We don't know how many rows were deleted. Oh well.
if (result == -1)
return false;
// Something awful has happened; it's probably a bug in the stored procedure.
throw new Exception("Too many rows were deleted!");
When my stored procedures are mundane T-SQL updates and deletes against local tables, this system works fine.
CREATE PROCEDURE [widgets].[Update]
#WidgetID int,
#NewName varchar(10)
AS
BEGIN
UPDATE Widgets SET Name = #NewName WHERE WidgetID = #WidgetID
END
However, sometimes I need to EXEC against a Linked Server:
CREATE PROCEDURE [widgets].[Update]
#WidgetID int,
#NewName varchar(10)
AS
BEGIN
DECLARE #OpenQuery varchar(max)
SET #OpenQuery = 'execute function mydata:widgets_Update(' + CAST(#WidgetID as varchar())+ ',''' + #NewName + ''')'
DECLARE #Query varchar(max)
SET #Query = 'SELECT * FROM OPENQUERY(INFORMIX, ''' + #OpenQuery +''')'
EXEC (#Query)
END
If I'm not directly issuing INSERT, UPDATE or DELETE statements in T-SQL, SQL Server (by design) returns the value -1 to ExecuteNonQuery() via the DONE_IN_PROC token. My ORM code can't do anything useful with this, so I'm willing to cheat a little.
First, I modify the remote query on the linked server to return the number of affected rows as an integer. For the stored procedure widgets_Update() on my remote Informix server, for example, I'll add this to the end:
-- Return the number of rows affected.
return DBINFO('sqlca.sqlerrd2');
Then I consume that number in order fake out the ##ROWCOUNT/DONE_IN_PROC mechanism:
-- Turn off row counts for the moment.
SET NOCOUNT ON
-- Create a dummy table to get the result from EXEC into a local variable
DECLARE #Rowcount Table(n int)
INSERT #Rowcount EXEC (#Query)
DECLARE #N int = (SELECT n FROM #Rowcount)
-- Create a dummy table to receive the inserted rows.
DECLARE #Table table (n int)
-- Modify the number of Affected Rows returned in the DONE_IN_PROC token by inserting exactly the right number of dummy rows into a dummy table.
SET NOCOUNT OFF
INSERT #Table SELECT * FROM ModifyROWCOUNT(#N)
The inline table-valued function ModifyROWCOUNT() simply generates empty rows (in the spirit of a numbers table) on the fly, using code I cribbed from another answer:
CREATE FUNCTION [dbo].[ModifyROWCOUNT]
(
#Rowcount int
)
RETURNS TABLE
AS
RETURN
(
WITH
L0 AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)), -- 2^1
L1 AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B), -- 2^2
L2 AS (SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B), -- 2^4
L3 AS (SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B), -- 2^8
L4 AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B), -- 2^16
L5 AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B), -- 2^32
Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS k FROM L5)
SELECT null as n
FROM nums
WHERE k <= #Rowcount
)
So far, this approach seems to work, and performs just fine; however, I've only tested it when updating or deleting one record at a time. Fortunately, this is the primary use case. My concern is that the code-to-effect ratio is so high that this solution smells bad to me, and I have a significant number of remote queries to build out this way.
My question is this: If my stored procedure must use EXEC and OPENQUERY to insert, update or delete records on a remote server, is there a superior way to return the actual affected row count from T-SQL so I can consume it with ExecuteNonQuery()?
Please assume that I can't add OUTPUT parameters to these stored procedures, which would require the use of ExecuteScalar() instead. And note that there are mechanisms (namely SELECT statements) that will modify ##ROWCOUNT, but still return -1 in the DONE_IN_PROC token.

SQL: Pull List Of Tables From Specified Database While Attached To Another

I am facing a peculiar issue with loading a list of tables from a specific database (well rather a group of databases) while attached to the master database. Currently my query loads all of the databases on the server, then loops through those databases sending information back to the client via RAISERROR. As this loop is executing I need a nested loop to load all of the tables for the current database for later transmission as a SELECT once the query has completed. The issue I'm running into is that this will be executed as a single query inside of C# code. Ideally I would like to load everything in SQL and return it to the client for processing. For example:
WHILE (#dbLoop < #dbCount) BEGIN
-- Do cool things and send details back to client.
SET #dbName = (SELECT _name FROM dbTemp WHERE _id = #dbLoop);
-- USE [#dbName]
-- Get a count of the tables from info schema on the newly specified database.
WHILE (#tableLoop < #tableCount) BEGIN
-- USE [#dbName]
-- Do super cool things and load tables from info schema.
SET #tableLoop += 1;
END
SET #dbLoop += 1;
END
-- Return the list of tables from all databases to the client for use with SQLDataAdapter.
SELECT * FROM tableTemp;
This topic is pretty straight forward; I just need a way to access tables in a specified database (preferably by name) without having to change the connection on the SqlConnection object, and without having to have a loop inside of my C# code to process the same query on each database on the C# side. It would be more efficient to load everything in SQL and send it back to the application. Any help that can be provided on this would be great!
Thanks,
Jamie
All the tables are in the meta data you can just do a query against that and join to your list of schemas you want to look at.
SELECT tab.name
FROM sys.tables AS tab
JOIN sys.schemas AS sch on tab.schema_id = sch.schema_id
JOIN dbTemp temp on sch.name = temp.[_name]
This returns a list of the table to return back as a result set.
The statement USE [#dbName] takes effect AFTER it is run (usually via the GO statement.
USE [#dbName]
GO
The above 2 lines would make you start using the new Database. You cannot use this in the middle of your SQL or SP.
One other option which you can use is to use the dot notation, i.e., dbname..tablename syntax to query your tables.
double dot notation post
Okay, after spending all day working on this, I have finally come up with a solution. I load all the databases into a table variable, then I begin looping through those databases and send back their details to the client. After the database details themselves have been sent to the client via RAISERROR I then utilize sp_executesql to execute a new sub-query with the current database specified to get the list of tables for processing at the end of the primary. The example below demonstrates the basic structure of this process for others experiencing this issue in the future.
Thank you all once again for your help!
-Jamie
DECLARE #LoopCounter INT = 1, #DatabaseCount INT = 0;
DECLARE #SQL NVARCHAR(MAX), #dbName NVARCHAR(MAX);
DECLARE #Databases TABLE ( _id INT, _name NVARCHAR(MAX) );
DECLARE #Tables TABLE ( _name NVARCHAR(MAX), _type NVARCHAR(15) );
INSERT INTO #Databases
SELECT ROW_NUMBER() OVER(ORDER BY name) AS id, name
FROM sys.databases
WHERE name NOT IN ( 'master', 'tempdb', 'msdb', 'model' );
SET #DatabaseCount = (SELECT COUNT(*) FROM #Databases);
WHILE (#LoopCounter <= #DatabaseCount) BEGIN
SET #dbName = (SELECT _name FROM #Databases WHERE _id = #LoopCounter);
SET #SQL NVARCHAR(MAX) = 'SELECT TABLE_NAME, TABLE_TYPE
FROM [' + #dbName + '].INFORMATION_SCHEMA.TABLES';
INSERT INTO #Tables EXEC sp_executesql #SQL;
SET #LoopCounter += 1;
END

Is sql injection possible in stored procedures with text param?

Im using such a query in my stored procedure
SET #Statement =
'SELECT Id,Title,Content,Status,ROW_NUMBER()
OVER (ORDER BY ' + #Sort + ') AS StudentReport
FROM YearBook
WHERE ' + #Criteria + ')
AS ArticleNumber
WHERE StudentReport> ' + CONVERT(NVARCHAR, #StartRowIndex) + ' AND
StudentReport<= (' + CONVERT(NVARCHAR, #StartRowIndex + #MaximumRows);
Just want to know whether its possible to do sql injection to this stored procedure. If yes, how can i prevent it? Need Help !!!
Yes it's possible. Quite easy, even. Try setting
#Criteria = "\r\nGO\r\nexec sp_addlogin 'hacker', 'broken'\r\nGO";
The batch will product errors, but the part in between will run nevertheless so welcome your new login.
The correct way to do your query could be something like this.
CREATE PROC FindSomething
#StartRowIndex int,
#MaximumRows int,
#Sort int, -- 1-4 representing the columns, say in a dropdown
#Id int,
#Content varchar(max),
#Title varchar(max),
#Status int
AS
SELECT Id,Title,Content,Status
FROM
(
SELECT Id,Title,Content,Status,
ROW_NUMBER() OVER (ORDER BY
CASE when #Sort = 1 then Id
when #Sort = 4 then Status
end,
CASE when #sort = 2 then Title
when #sort = 3 then Content
end) AS StudentReport
FROM YearBook
WHERE (#id is null or #id = Id)
AND (#Content is null or #Content = Content)
AND (#Title is null or #Title = Title)
AND (#status is null or #Status = Status)
) Numbered
WHERE StudentReport >= #StartRowIndex
AND StudentReport <= #StartRowIndex + #MaximumRows
OPTION (RECOMPILE);
GO
Read here on more about dynamic searching: www.sommarskog.se/dyn-search.html
Note: I split up 1/4 and 2/3 in the sort because each branch of a CASE statement must produce the same type, or that is compatible. int/varchar is very bad mix to have in a case statement.
Assuming the above is a string that you are building and then executing with EXEC or sp_executesql then Yes, SQL injection is possible.
How to prevent it depends on what you are trying to do. Perhaps you need to rethink your approach.
Yes, it will. You can still do things to help defend against it tho'
For example, #Sort is a column name, so you can escape that properly (and ensure that if someone tried to inject something into is it won't work, because it has been properly escaped. For that use QUOTENAME.
QUOTENAME(#Sort)
#Criteria is more difficult because you are actually expecting a fragment on SQL code so it becomes very difficult to work out what is valid and what is malicious. You might want to reconsider what you are trying do do here. If you must use Criteria then ensure that the security model is set up so that only the application(s) that absolutely needs it has access to the stored proc that does this. Make sure that the validation in the application before it sends of the SQL to ensure that anything it is doing isn't going to be damaging.
It looks like you are trying to make a pretty generic search stored procedure with paging. These are difficult to implement properly in t-sql only, and can become maintenance headaches down the road due to the branching logic, or additional supporting stored procedures you need to add...
I would start to look at other options outside of a pure sql approach. Using an orm, or micro orm could help a lot. Actually, take a look at what Sam Saffron came up with...
http://samsaffron.com/archive/2011/09/05/Digging+ourselves+out+of+the+mess+Linq-2-SQL+created

passing an operand as an sql parameter

I am currently working on an asp.net application that has sql server 2008 as its backend. I want to give the user the ability to specify what they want to filter by on the SQL statement.
On the interface I am giving them the option to select the following as a dropdown:
equals to
greater than
Less than
etc
I want to pass this as a parameter on the sql query to be executed. How best can I achieve this?
for eg;
Select amount, deduction, month from loan where amount #operant 10000;
the #operand is the return values of the above dropdown which is = < > <= >=
Assuming all positive integers < 2 billion, this solution avoids multiple queries and dynamic SQL. OPTION (RECOMPILE) helps thwart parameter sniffing, but this may not be necessary depending on the size of the table, your parameterization settings and your "optimize for ad hoc workload" setting.
WHERE [Amount] BETWEEN
CASE WHEN #operand LIKE '<%' THEN 0
WHEN #operand = '>' THEN #operant + 1
ELSE #operant END
AND
CASE WHEN #operand LIKE '>%' THEN 2147483647
WHEN #operand = '<' THEN #operant - 1
ELSE #operant END
OPTION (RECOMPILE);
I would write few "IF" statements. Code is not very short, but should be fast.
IF(#operand = '=')
Select..
ELSE IF(#operand = '>=')
Select..
...
Also, i would say, that Top (#someRowCount) could be great idea.
You need dynamic sql for this scenario
For your example this can be
DECLARE #sql AS nvarchar(max) -- Use max if you can, if you set
-- this to a specific size then your assignment later can be
-- truncated when maintained and still be valid.
SET #sql = 'Select amount, deduction, month from dbo.loan where amount '
+ #operand + ' 10000'
EXEC sp_executesql #sql
Update 1
There are 2 ways to execute dynamic sql : Exec() and sp_executesql
Read the comments why sp_executesql is preferred (still, beware of sql injections!)
I also prefix the table with the dbo so that the execution plan can be cached between different users
More info in the awesome paper at http://www.sommarskog.se/dynamic_sql.html#queryplans

Select only first four lines, from Sql text field

Sql Server 2008 Express >> Visual Web Developer >> C#
I'm pulling records out of the table like this:
SELECT Name, Category, Review FROM ReviewTable
This works fine but the Review field Type in SQL Server is text and is very long (think a magazine article).
I only want to pull the first four lines from the Review field for each row, and display them in my repeater control. These lines will be like a teaser of the article.
Is this possible? How can it be accomplished?
This will return this first 1000 characters from the review.
SELECT Name, Category, CAST(Review AS VARCHAR(1000) FROM ReviewTable
If you must have the first 4 lines you need to use some split function. This could work:
CREATE FUNCTION [dbo].[Split]
(
#SearchString VARCHAR(8000),
#Separator VARCHAR(5),
#MaxItems INT
)
RETURNS #strtable TABLE (strval VARCHAR(8000))
AS
BEGIN
DECLARE #tmpStr VARCHAR(8000), #intSeparatorLength INT, #counter int
IF #MaxItems IS NULL
SET #MaxItems = 2147483647 -- max int
SET #intSeparatorLength = LEN(#Separator)
SET #Counter = 0
SET #tmpStr = #SearchString
WHILE 1=1 BEGIN
INSERT INTO #strtable VALUES ( SUBSTRING(#tmpStr, 0 ,CHARINDEX(#Separator, #tmpStr)))
SET #tmpStr = SUBSTRING(#tmpStr,CHARINDEX(#Separator,#tmpStr)+LEN(#Separator),8000)
SET #counter = #counter + 1
IF (CHARINDEX(#Separator,#tmpStr) < 1 OR #counter >= #MaxItems)
BREAK
END
RETURN
END
Usage: select * from dbo.split('aaa**bbbb**CCCC**dddd**eeee**dffff**ggggg', '**', 4)
Well ,the first for lines may be a bit more difficult, but why don't you just put out the first 250 characters or so?
SELECT Name, Category, SubString(Review, 1, 250) AS Review FROM ReviewTable
If your database server is in the same local network as your web server, I think I'd probably select the entire field, since you're accessing it at all. You'll still have to do a lookup to access any data in that field, so sql performance-wise for finding the data is a non-issue. The only downside of retrieving the entire field would be the amount of data passed between the servers. Thus: if they're in the same network, I'd say this would definitely be cheaper than tampering with each record during selection. It also gives you the ability to cache your response, so that you don't have to hit the database again when the user wants to see the full version of the text.
But, to answer your question, the below should probably do it, altho it looks rather tacky
SELECT Name, Category, left(convert(varchar(8000), Review), charindex('\n', convert(varchar(8000), Review), charindex('\n', convert(varchar(8000), Review), charindex('\n', convert(varchar(8000), Review), charindex('\n', convert(varchar(8000), Review))+1)+1)+1)-1) FROM ReviewTable
...hrrm, yeah, really, i'd consider my first paragraph

Categories