Storing DATABASE_PRINCIPAL_ID in sql server table - sql-server

We're using stored procedures to restrict the access of some of our database users. They need access to specific parts of the database (not just tables/views, but also specific rows), and the sproc should check if the user is allowed to see the table rows he's requesting.
To store the authorization rules, we're planning to use a table dbo.AuthRules like that:
|ID|UserId |AccessFrom |AccessTo | ...
===============================================
| 1| 1| 01.01.2013 | 31.12.2013 | ...
| 2| 2| 31.05.2012 | 31.12.2015 | ...
The stored procedure would then query that table to check if the current user has access to the requested data. To make it clear: We cannot just use GRANT PERMISSION because we need fine-grained access rules down to the rows in the DB.
We're not sure about the UserId column. The best solution would be some kind of foreign key to the system view sys.database_principals, but there are no foreign keys to views.
Should we just store the principal_id column of sys.database_principals, without any constraint?
Would it be better to store the name column instead of principal_id?
Are there other options to store a reference to the DB user?

Q: Should we just store the principal_id column of sys.database_principals, without any constraint?
A: If you are evaluating only a constraint-based solution, you don't really have any choice. However, if you are willing to write a trigger, that would be the way to at least 'virtually' create constraint. Otherwise you would have to reference system sysowners table and this isn't possible without some hacking.
Q: Would it be better to store the name column instead of principal_id?
A: To answer that you have to ask the what-if question. What if principal_id changes? What if name changes? What about updates/deletes? I would always rather have an id-based solution.
Q: Are there other options to store a reference to the DB user?
A: As stated in answer#1, I don't think there are any possibilities except creating a trigger...

Consider this design method.
USE "master";
-- Create our test logins (don't forget to tidy these up later!)
CREATE LOGIN "batman"
WITH PASSWORD = N''
, CHECK_EXPIRATION = OFF
, CHECK_POLICY = OFF;
CREATE LOGIN "robin"
WITH PASSWORD = N''
, CHECK_EXPIRATION = OFF
, CHECK_POLICY = OFF;
USE "playdb";
-- Create the corresponding users
CREATE USER "batman"
FOR LOGIN "batman";
CREATE USER "robin"
FOR LOGIN "robin";
-- Grant them some select permissions
EXEC sp_addrolemember N'db_datareader', N'batman';
EXEC sp_addrolemember N'db_datareader', N'robin';
-- Create our "user access" table
DECLARE #users table (
username sysname NOT NULL
, access_from date NOT NULL DEFAULT '1900-01-01' -- Deliberately not allowing nulls and setting the widest possible value range
, access_to date NOT NULL DEFAULT '9999-12-31' -- this will make our queries so much easier!
);
-- Give one user full permissions
INSERT INTO #users (username)
VALUES ('batman');
-- Limit what this chappy can see
INSERT INTO #users (username, access_from, access_to)
VALUES ('robin', '2011-01-01', '2012-01-01');
-- Example data table
DECLARE #data table (
date_field date NOT NULL
);
-- ...with example data
INSERT INTO #data (date_field)
VALUES ('2010-01-01')
, ('2011-01-01')
, ('2012-01-01')
, ('2012-01-01')
, ('2013-01-01')
, ('2014-01-01');
-- Let's mimic our full access user
SETUSER 'batman';
SELECT System_User As current_username
, *
FROM #data As data
WHERE EXISTS ( -- Limit the records to only those this user is allowed to see
SELECT *
FROM #users
WHERE username = System_User
AND data.date_field >= access_from
AND data.date_field < access_to
);
SETUSER; -- Reset user
-- Imitate the chap with restricted access
SETUSER 'robin';
-- Exacty same query as before
SELECT System_User As current_username
, *
FROM #data As data
WHERE EXISTS (
SELECT *
FROM #users
WHERE username = System_User
AND data.date_field >= access_from
AND data.date_field < access_to
);

Related

How create table inline function for security policy row filter depending on values in configuration table in SQL Server?

I would like to create a row level security policy.
My input is a user_id for users who connect to the database through a middle-tier application.
I would like to:
Query a configuration table (let's call it conf_table) to get the department name of user_id
Depending on value department, I want to filter on another table called customers on type_of_customers.
Example:
conf_table:
user_id
department
toto
sidney
Customers:
customer_no
typ_customer
0001
A
0002
B
Function:
IF conf_table.user_id = 'toto' AND conf_table.department = 'sidney'`
SELECT *
FROM customers
WHERE typ_customer = A`
ELSE
SELECT *
FROM customers
WHERE typ_customer = B`
Many thanks in advance for your help!
The simplest way is to do this :
DECLARE #type VARCHAR(1) = 'B'
IF EXISTS(SELECT * FROM conf_table WHERE user_id = 'toto' AND department = 'sidney')
SET #type = 'A'
SELECT * FROM customers WHERE typ_customer = #type
Ideally, each row in conf_table would have a typ_customer associated with it, alternatively you would join to a Departments table to get that value.
But without that, you can just use a CASE expression.
Note the usage of a function parameter to be able to pass in the typ_customer value from the Customers table that is being filtered.
CREATE OR ALTER FUNCTION Security.YourPredicateTVF (#typ_customer AS char(1))
RETURNS TABLE WITH SCHEMABINDING
AS RETURN
SELECT 1 AS tvf_securitypredicate_result
FROM Security.conf_table c
WHERE c.user_id = USER_NAME()
AND CASE c.department = 'sidney' THEN 'A' ELSE 'B' END = #typ_customer;
GO
Then simply define a security policy, passing in the typ_customer column to the function.
CREATE SECURITY POLICY Security.YourPolicyName
ADD FILTER PREDICATE
Security.YourPredicateTVF (typ_customer)
ON dbo.Customers;
You may want to change FILTER to BLOCK and/or add DML filters also, depending on your use case.
Be aware that Row-Level Security is subject to side-channel attacks, and is therefore not a completely secure feature. For example, triggering a divide-by-zero error could tell the user what is stored in a row without actually seeing it.

How to select the default values of a table?

In my app, when letting the user enter a new record, I want to preselect the database's default values.
Let's for example take this table:
CREATE TABLE pet (
ID INT NOT NULL,
name VARCHAR(255) DEFAULT 'noname',
age INT DEFAULT 1
)
I would like to do something like this:
SELECT DEFAULT VALUES FROM pet -- NOT WORKING
And it should return:
ID | name | age
--------------------
NULL | noname | 1
I would then let the user fill in the remaining fields, or let her change one of the defaults, before she clicks on "save".
How can I select the default values of a sql server table using tsql?
You don't "SELECT" the Default values, only insert them. A SELECT returns the rows from a table, you can't SELECT the DEFAULT VALUES as there's no such row inside the table.
You could do something silly, like use a TRANSACTION and roll it back, but as ID doesn't have a default value, and you don't define a value for it with DEFAULT VALUES, it'll fail in your scenario:
CREATE TABLE pet (
ID INT NOT NULL,
name VARCHAR(255) DEFAULT 'noname',
age INT DEFAULT 1
)
GO
BEGIN TRANSACTION;
INSERT INTO dbo.pet
OUTPUT inserted.*
DEFAULT VALUES;
ROLLBACK;
Msg 515, Level 16, State 2, Line 13
Cannot insert the value NULL into column 'ID', table 'Sandbox.dbo.pet'; column does not allow nulls. INSERT fails.
You can, therefore, just supply the values for your non-NULL columns:
BEGIN TRANSACTION;
INSERT INTO dbo.pet (ID)
OUTPUT inserted.*
VALUES(1);
ROLLBACK;
Which will output the "default" values:
ID|name |age
--|------|---
1|noname|1
Selecting the default values of all columns is not very straight-forward, and as Heinzi wrote in his comment - does require a level of permissions you normally don't want your users to have.
That being said, a simple workaround would be to insert a record, select it back and display to the user, let the user decide what they want to change (if anything) and then when they submit the record - update the record (or delete the previous record and insert a new one).
That would require you to have some indication if the record was actually reviewed and updated by the user, but that's easy enough to accomplish by simply adding a bit column and setting it to 1 when updating the data.
As I have commented before. There is no need for this query since you can press alt + f1 on any table in your editor in Management Studio and provide you every information you need for the table.
select sys1.name 'Name',replace(replace(
case
when object_definition(sys1.default_object_id) is null then 'No Default Value'
else object_definition(sys1.default_object_id)
end ,'(',''),')','') 'Default value',
information_schema.columns.data_type 'Data type'
from sys.columns as sys1
left join information_schema.columns on sys1.name = information_schema.columns.column_name
where
object_id = object_id('table_name')
and information_schema.columns.table_name = 'table_name'
It seems like this might be solution:
SELECT * FROM (
SELECT
sys1.name AS COLUMN_NAME,
replace(replace(object_definition(sys1.default_object_id),'(',''),')','') AS DEFAULT_VALUE
FROM sys.columns AS sys1
LEFT JOIN information_schema.columns ON sys1.name = information_schema.columns.column_name
WHERE object_id = object_id('pet')
AND information_schema.columns.table_name = 'pet'
) AS SourceTable PIVOT(MAX(DEFAULT_VALUE) FOR COLUMN_NAME IN(ID, name, age)) AS PivotTable;
It returns:
ID |name |age
----|------|---
NULL|noname|1
Probably the column types are incorrect - but maybe I can live with that.
Thanks for #Nissus to provide an intermediate step to this.

Stored procedure to create or update

I am working on creating a Web API which will get account as the input parameter, which will have to create / update records in the table in SQL Server. So the web service will need to call the stored procedure which will accept the account. I created a sample table in the database with just two columns called Account and CounterSeq. I am trying to creating a stored procedure to create or update the records in the table.
Each account should have a CounterSeq associated with it. If the Account doesn't exists in the table, create Account name and associate CounterSeq = 001 to it. If the Account name already exists, just update like CounterSeq to CounterSeq + 1
+---------+----------------+
| Account | CounterSeq |
+---------+----------------+
| ABC | 001 |
| DEF | 002 |
+---------+----------------+
For this I create a TableType like this
USE [Demo]
GO
-- Create the data type
CREATE TYPE projectName_TT AS TABLE
(
Account nvarchar(50),
CounterSeq int
)
GO
And the stored procedure as below but I am missing how to insert the new record like for a new Account how to set the CounterSeq to 001 ?
USE [Demo]
GO
ALTER PROCEDURE [dbo].[uspInserorUpdateProjectName]
#projectName_TT AS projectName_TT READONLY
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION;
UPDATE prj
SET prj.Account = tt.Account,
prj.CounterSeq = tt.CounterSeq + 1
FROM dbo.[ProjectName] prj
INNER JOIN #projectName_TT tt ON prj.Account = tt.Account
INSERT INTO [dbo].[ProjectName](Account, CounterSeq)
SELECT tt.Account, tt.CounterSeq
FROM #projectName_TT tt
WHERE NOT EXISTS (SELECT 1
FROM [dbo].[ProjectName]
WHERE Account = tt.Account)
COMMIT TRANSACTION;
END;
First, I believe that the table you passed in should only have one field (based on your description If the Account doesn't exists in the Database create Account name and associate CounterSeq 001 to it. If the Account name already exists just update like CounterSeq to CounterSeq+1)
You can use this query (put it in your stored procedure between the BEGIN TRANSACTION, COMMIT TRANSACTION.
MERGE dbo.[ProjectName] prj
USING #projectName_TT tt
ON prj.Account = tt.Account
WHEN MATCHED THEN UPDATE SET prj.CounterSeq = prj.CounterSeq+1
WHEN NOT MATCHED THEN INSERT (Account,CounterSeq)
VALUES (tt.Account, 1);

SQL use a variable as TABLE NAME in a FROM

We install our database(s) to different customers and the name can change depending on the deployment.
What I need to know is if you can use a variable as a table name.
The database we are in is ****_x and we need to access ****_m.
This code is part of a function.
I need the #metadb variable to be the table name - Maybe using dynamic SQL with
sp_executesql. I am just learning so take it easy on me.
CREATE FUNCTION [dbo].[datAddSp] (
#cal NCHAR(30) -- calendar to use to non-working days
,#bDays INT -- number of business days to add or subtract
,#d DATETIME
)
RETURNS DATETIME
AS
BEGIN
DECLARE #nDate DATETIME -- the working date
,#addsub INT -- factor for adding or subtracting
,#metadb sysname
SET #metadb = db_name()
SET #metadb = REPLACE (#metadb,'_x','_m')
SET #metadb = CONCAT (#metadb,'.dbo.md_calendar_day')
SET #ndate = #d
IF #bdays > 0
SET #addsub = 1
ELSE
SET #addsub = -1
IF #cal = ' ' OR #cal IS NULL
SET #cal = 'CA_ON'
WHILE #bdays <> 0 -- Keep adding/subtracting a day until #bdays becomes 0
BEGIN
SELECT #ndate = dateadd(day, 1 * #addsub, #ndate) -- increase or decrease #ndate
SELECT #bdays = CASE
WHEN (##datefirst + datepart(weekday, #ndate)) % 7 IN (0, 1) -- ignore if it is Sat or Sunday
THEN #bdays
WHEN ( SELECT 1
FROM #metadb -- **THIS IS WHAT I NEED** (same for below) this table holds the holidays
WHERE mast_trunkibis_m.dbo.md_calendar_day.calendar_code = #cal AND mast_trunkibis_m.dbo.md_calendar_day.calendar_date = #nDate AND mast_trunkibis_m.dbo.md_calendar_day.is_work_day = 0
) IS NOT NULL -- ignore if it is in the holiday table
THEN #bdays
ELSE #bdays - 1 * #addsub -- incr or decr #ndate
END
END
RETURN #nDate
END
GO
The best way to do this, if you aren't stuck with existing structures is to keep all of the table structures and names the same, simply create a schema for each customer and build out the tables in the schema. For example, if you have the companies: Global Trucking and Super Store you would create a schema for each of those companies: GlobalTrucking and SuperStore are now your schemas.
Supposing you have products and payments tables for a quick example. You would create those tables in each schema so you end up with something that looks like this:
GlobalTrucking.products
GlobalTrucking.payments
and
SuperStore.products
SuperStore.payments
Then in the application layer, you specify the default schema name to use in the connection string for queries using that connection. The web site or application for Global Trucking has the schema set to GlobalTrucking and any query like: SELECT * FROM products; would actually automatically be SELECT * FROM GlobalTrucking.products; when executed using that connection.
This way you always know where to look in your tables, and each customer is in their own segregated space, with the proper user permissions they will never be able to accidentally access another customers data, and everything is just easier to navigate.
Here is a sample of what your schema/user/table creation script would look like (this may not be 100% correct, I just pecked this out for a quick example, and I should mention that this is the Oracle way, but SQL Server should be similar):
CREATE USER &SCHEMA_NAME IDENTIFIED BY temppasswd1;
CREATE SCHEMA AUTHORIZATION &SCHEMA_NAME
CREATE TABLE "&SCHEMA_NAME".products
(
ProductId NUMBER,
Description VARCHAR2(50),
Price NUMBER(10, 2),
NumberInStock NUMBER,
Enabled VARCHAR2(1)
)
CREATE TABLE "&SCHEMA_NAME".payments
(
PaymentId NUMBER,
Amount NUMBER(10, 2),
CardType VARCHAR2(2),
CardNumber VARCHAR2(15),
CardExpire DATE,
PaymentTimeStamp TIMESTAMP,
ApprovalCode VARCHAR2(25)
)
GRANT SELECT ON "&SCHEMA_NAME".products TO &SCHEMA_NAME
GRANT SELECT ON "&SCHEMA_NAME".payments TO &SCHEMA_NAME
;
However, with something like the above, you only have 1 script that you need to keep updated for automation of adding new customers. When you run this, the &SCHEMA_NAME variable will be populated with whatever you choose for the new customer's username/schemaname, and an identical table structure is created every time.

SQL Server stored procedure with C# enum values

I am writing some stored procedures for my application. Within the C# code I have lots of enums. As an example:
public enum Status
{
Active = 1
Inactive = 2
}
Now when I write my stored procedures, is there any way of replicating enums in the database? I want to avoid using a table to hold them all, but I was wondering if you can tuck them all away in an object somewhere, and reference this in other stored procedures?
So I want my stored procedures to use the style of:
SELECT *
FROM Users
WHERE status = Status.Active
Instead of using raw numerical values like this:
SELECT *
FROM Users
WHERE status = 1
Does anyone have any ideas on how this might be achieved?
It turns out the best way to do this is to use a SQL Server Scalar function for the job as follows:
ALTER FUNCTION [dbo].[fn_GetEnumValue]
(
#enumName varchar(512)
)
RETURNS INT
AS
BEGIN
DECLARE #returnVal INT = 0
SELECT #returnVal = (
CASE
WHEN #enumName = 'Active' THEN 1
WHEN #enumName = 'Inactive' THEN 2
ELSE 0
END
)
RETURN #returnVal
END
Then you can use it like this:
SELECT * FROM Users WHERE status = dbo.fn_GetEnumValue('Active');
You could create a set of single row tables, each one representing an enumeration. So for user statuses:
CREATE TABLE Enum_Status
(
Active TINYINT NOT NULL DEFAULT 1,
Inactive TINYINT NOT NULL DEFAULT 2
);
INSERT INTO Enum_Status VALUES( 1 , 2 );
Then your SELECT statement would look quite neat (if not as neat as the equivalent in Oracle):
SELECT Users.*
FROM Users, Enum_Status Status
WHERE status = Status.Active;
To keep things tidy I would be tempted to put the enumeration tables in their own schema and grant all users the relevant permissions.
As i understand, your aim is to ensure that the value passed to the stored procedure is valid.
You could create a Status table as follows
CREATE TABLE Status
(
StatusID int,
StatusName nvarchar,
)
insert the valid data here.
Then, in your query, you could do an INNER JOIN to validate.
SELECT *
FROM Users
INNER JOIN Status ON Users.status = Status.StatusID
WHERE StatusID = #StatusID

Resources