Prevent User Usage of "dbo" in User Databases SQL Server - sql-server

I am attempting to prevent usage of the default schema of "dbo" in my SQL Server databases. This is being applied to an existing long term project with ongoing maintenance where the developers also manage the SQL Server (are all sysadmin).
This is for the main reason to allow better dependency tracking between code and the SQL Server objects so that we can slowly migrate to a better naming convention. Eg. "dbo.Users", "dbo.Projects", "dbo.Categories" in a DB are nearly impossible to find in code once created because the "dbo." is often left out of SQL Syntax.
However a proper defined schema requires the usage in code. Eg. "Tracker.Users", "Tracker.Projects", etc ...
Even though we have standards set to not use "dbo" for objects it is still accidentally occurring due to management/business pressures for speed to develop.
Note: I'm creating this question simply to provide a solution someone else can find useful
EDIT: As pointed out, for non-sysadmin users the security option stated is a viable solution, however the DDL Trigger solution will also work on sysadmin users. The case for many small teams who have to manage there own boxes.

I feel like it would be 10,000 times simpler to just DENY ALTER on the dbo schema:
DENY ALTER ON SCHEMA::dbo TO [<role(s)/user(s)/group(s)>];
That's not too handy if everyone connects as sa but, well, fix that first.

The following Database DLL Trigger causes error feedback in both the SQL Manager GUI and via Manual TSQL code attempts to create an object for the types specified.
It includes a means to have a special user and provides clear feedback to the user attempting the object creation. It also works to raise the error with users who are sysadmin.
It does not affect existing objects unless the GUI/SQL tries to DROP and CREATE an existing "dbo" based object.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [CREATE_Prevent_dbo_Usage_2] ON DATABASE
FOR CREATE_TABLE, CREATE_VIEW, CREATE_PROCEDURE, CREATE_FUNCTION
AS
DECLARE #E XML = EVENTDATA();
DECLARE #CurrentDB nvarchar(200)=#E.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(2000)');
DECLARE #TriggerFeedbackName nvarchar(max)=#CurrentDB+N'.CREATE_Prevent_dbo_Usage'; -- used to feedback the trigger name on a failure so the user can disable it (or know where the issue is raised from)
DECLARE #temp nvarchar(2000)='';
DECLARE #SchemaName nvarchar(2000)=#E.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(2000)');
DECLARE #ObjectName nvarchar(2000)=#E.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(2000)');
DECLARE #LoginName nvarchar(2000)=#E.value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(2000)');
DECLARE #CurrentObject nvarchar(200)=''; -- Schema.Table
IF #LoginName NOT IN ('specialUser') BEGIN -- users to exclude in evaluation.
IF CASE WHEN #SchemaName IN ('dbo') THEN 1 ELSE 0 END = 1 BEGIN -- is a DBO attempt to create.
SET #CurrentObject = #SchemaName+'.'+#ObjectName; -- grouped here for easy cut and paste/modify.
SET #temp='Cannot create "'+#CurrentObject+'".
This Database "'+#CurrentDB+'" has had creation of "dbo" objects restricted to improve code maintainability.
Use an existing schema or create a new one to group the purpose of the objects/code.
Disable this Trigger TEMPORARILY if you need to do some advanced manipulation of it.
(This message was produced by "'+#TriggerFeedbackName+'")';
throw 51000,#temp,1;
END
END
GO
ENABLE TRIGGER [CREATE_Prevent_dbo_Usage] ON DATABASE
GO

Related

Using variables in TSQL and keep formatting in SQL Server Management Studio

I'm creating some views with a lot of references to tables in another database.
At some point the other database needs to change.
I want to make it easy for the next developer to change the scripts to use another database.
This obviously work like it should:
CREATE VIEW ViewName
AS
SELECT *
FROM AnotherDatabase.SchemaName.TableName;
But when I do:
DECLARE #DB CHAR(100)
SET #DB = 'AnotherDatabase'
GO
CREATE VIEW ViewName
AS
SELECT *
FROM #DB.SchemaName.TableName;
I get the error:
Msg 137, Level 15, State 2, Procedure ViewName, Line 3
Must declare the scalar variable "#DB".
I could do something like:
DECLARE #SQL ...
SET #SQL = ' ... FROM ' + #DB + ' ... '
EXEC (#SQL)
But that goes against the purpose of making it easier for the next developer - because this dynamic SQL approach removed the formatting in SSMS.
So my question is: how do I make it easy for the next developer to maintain T-SQL code where he needs to swap out the database reference?
Notes:
I'm using SQL Server 2008 R2
The other database is on the same server.
Consider using SQLCMD variables. This will allow you to specify the actual database name at deployment time. SQL Server tools (SSMS, SQLCMD, SSDT) will replace the SQLCMD variable names with the assigned string values when the script is run. SQLCMD mode can be turned on for the current query windows from the menu option Query-->SQLCMD mode option.
:SETVAR OtherDatabaseName "AnotherDatabaseName"
CREATE VIEW ViewName AS
SELECT *
FROM $(OtherDatabaseName).SchemaName.TableName;
GO
This approach works best when SQL objects are kept under source control.
When you declare variables, they only live during the execution of the statement. You can not have a variable as part of your DDL. You could create a bunch of synonyms, but I consider that over doing it a bit.
The idea that your database names are going to change over time seems a bit out of the ordinary and conceivably one-time events. However, if you do still require to have the ability to quickly change over to point to a new database, you could consider creating a light utility directly in SQL to automatically generate the views to point to the new database.
An implementation may look something like this.
Assumptions
Assuming we have the below databases.
Assuming that you prefer to have the utility in SQL instead of building an application to manage it.
Code:
create database This;
create database That;
go
Configuration
Here I'm setting up some configuration tables. They will do two simple things:
Allow you to indicate the target database name for a particular configuration.
Allow you to define the DDL of the view. The idea is similar to Dan Guzman's idea, where the DDL is dynamically resolved using variables. However, this approach does not use the native SQLCMD mode and instead relies on dynamic SQL.
Here are the configuration tables.
use This;
create table dbo.SomeToolConfig (
ConfigId int identity(1, 1) primary key clustered,
TargetDatabaseName varchar(128) not null);
create table dbo.SomeToolConfigView (
ConfigId int not null
references SomeToolConfig(ConfigId),
ViewName varchar(128) not null,
Sql varchar(max) not null,
unique(ConfigId, ViewName));
Setting the Configuration
Next you set the configuration. In this case I'm setting the TargetDatabaseName to be That. The SQL that is being inserted into SomeToolConfigView is the DDL for the view. I'm using two variables, one {{ViewName}} and {{TargetDatabaseName}}. These variables are replaced with the configuration values.
insert SomeToolConfig (TargetDatabaseName)
values ('That');
insert SomeToolConfigView (ConfigId, ViewName, Sql)
values
(scope_identity(), 'dbo.my_objects', '
create view {{ViewName}}
as
select *
from {{TargetDatabaseName}}.sys.objects;'),
(scope_identity(), 'dbo.my_columns', '
create view {{ViewName}}
as
select *
from {{TargetDatabaseName}}.sys.columns;');
go
The tool
The tool is a stored procedure that takes a configuration identifier. Then based on that identifier if drops and recreates the views in the configuration.
The signature for the stored procedure may look something like this:
exec SomeTool #ConfigId;
Sorry -- I left out the implementation, because I have to scoot, but figured I would respond sooner than later.
Hope this helps.

Schema lookup for stored procedure different to tables (bug?)

If you run the following in sql server...
CREATE SCHEMA [cp]
GO
CREATE TABLE [cp].[TestIt](
[ID] [int] NULL
) ON [PRIMARY]
GO
CREATE PROCEDURE cp.ProcSub
AS
BEGIN
Print 'Proc Sub'
END
GO
CREATE PROCEDURE cp.ProcMain
AS
BEGIN
Print 'Proc Main'
EXEC ProcSub
END
GO
CREATE PROCEDURE cp.ProcMain2
AS
BEGIN
Print 'Proc Main2'
SELECT * FROM TestIt
END
GO
exec cp.ProcMain2
GO
exec cp.ProcMain
GO
You get the error
Could not find stored procedure 'ProcSub'
Which makes it impossible to compartmentalize procedures into a schema without hard coding the schema into the execution call. Is this by design or a bug as doing a select on tables looks in the procedures schema first.
If anyone has a work around I'd be interested to hear it, although the idea is that I can give a developer two stored procedures which call each other and can put into whatever schema they like in 'their' database that they can run for the purpose of being a utility that looks at the objects of another given schema in the same database.
I have looked to see if I might be able to get round it with Synonyms but they seem to have the same problem associated with them.
This is by design, because the default schema is not set for the user calling the procedure.
When schema is not specified in the query, sql server will try the default schema first and then the dbo (if different) schema. So you need to set the default schema or make your procedure creation using fully qualified names.
Check out:
Beginning with SQL Server 2005, each user has a default schema. The
default schema can be set and changed by using the DEFAULT_SCHEMA
option of CREATE USER or ALTER USER. If DEFAULT_SCHEMA is left
undefined, the database user will have dbo as its default schema.
https://technet.microsoft.com/en-us/library/ms190387%28v=sql.105%29.aspx

Writing stored procedures when using dynamic schema names in sql server

The application, I have been currently working with has different schema names for its tables, for example Table1 can have multiple existence say A.Table1 and B.Table1. All my stored procedures are stored under dbo. I'm writing the below stored procedures using dynamic SQL. I'm currently using SQL Server 2008 R2 and soon it will be migrated to SQL Server 2012.
create procedure dbo.usp_GetDataFromTable1
#schemaname varchar(100),
#userid bigint
as
begin
declare #sql nvarchar(4000)
set #sql='select a.EmailID from '+#schemaname+'.Table1 a where a.ID=#user_id';
exec sp_executesql #sql, N'#user_id bigint', #user_id=#userid
end
Now my questions are,
1. Is this type of approach affects the performance of my stored procedure?
2. If performance is affected, then how to write procedures for this kind of scenario?
The best way around this would be a redesign, if at all possible.
You can even implement this retrospectively by adding a new column to replace the schema, for example: Profile, then merge all tables from each schema into one in a single schema (e.g. dbo).
Then your procedure would appear as follows:
create procedure dbo.usp_GetDataFromTable1
#profile int,
#userid bigint
as
begin
select a.EmailID from dbo.Table1 a
where a.ID = #user_id
and a.Profile = #profile
end
I have used an int for the profile column, but if you use a varchar you could even keep your schema name for the profile value, if that helps to make things clearer.
I would look at a provisioning approach, where you dynamically create the tables and stored procedures as part of some up-front process. I'm not 100% sure of your scenario, but perhaps this could be when you add a new user. Then, you can call these SP's by convention in the application.
For example, new user creation calls an SP which creates c.Table and c.GetDetails SP.
then in the app you can call c.GetDetails based on "c" being a property of the user definition.
This gets you around any security concerns from using dynamic SQL. It's still dynamic, but is built once up front.
Dynamic schema and same table structure is quite unusual, but you can still obtain what you want using something like this:
declare #sql nvarchar(4000)
declare #schemaName VARCHAR(20) = 'schema'
declare #tableName VARCHAR(20) = 'Table'
-- this will fail, as the whole string will be 'quoted' within [..]
-- declare #tableName VARCHAR(20) = 'Category; DELETE FROM TABLE x;'
set #sql='select * from ' + QUOTENAME(#schemaName) + '.' + QUOTENAME(#tableName)
PRINT #sql
-- #user_id is not used here, but it can if the query needs it
exec sp_executesql #sql, N'#user_id bigint', #user_id=0
So, QUOTENAME should keep on the safe side regarding SQL injection.
1. Performance - dynamic SQL cannot benefit from some performance improvements (I think procedure associated statistics or something similar), so there is a performance risk.
However, for simple things that run on rather small amount of data (tens of millions at most) and for data that is not heavily changes (inserts and deletes), I don't think you will have noticeable problems.
2. Alternative -bukko has suggested a solution. Since all tables have the same structure, they can be merged. If it becomes huge, good indexing and partitioning should be able to reduce query execution times.
There is a work around for this if you know what schemas you are going to be using. You stated here that schema name is created on signup, we use this approach on login. I have a view which I add or remove unions from on session startup/dispose. Example below.
CREATE VIEW [engine].[vw_Preferences]
AS
SELECT TOP (0) CAST (NULL AS NVARCHAR (255)) AS SessionID,
CAST (NULL AS UNIQUEIDENTIFIER) AS [PreferenceGUID],
CAST (NULL AS NVARCHAR (MAX)) AS [Value]
UNION ALL SELECT 'ZZZ_7756404F411B46138371B45FB3EA6ADB', * FROM ZZZ_7756404F411B46138371B45FB3EA6ADB.Preferences
UNION ALL SELECT 'ZZZ_CE67D221C4634DC39664975494DB53B2', * FROM ZZZ_CE67D221C4634DC39664975494DB53B2.Preferences
UNION ALL SELECT 'ZZZ_5D6FB09228D941AC9ECD6C7AC47F6779', * FROM ZZZ_5D6FB09228D941AC9ECD6C7AC47F6779.Preferences
UNION ALL SELECT 'ZZZ_5F76B619894243EB919B87A1E4408D0C', * FROM ZZZ_5F76B619894243EB919B87A1E4408D0C.Preferences
UNION ALL SELECT 'ZZZ_A7C5ED1CFBC843E9AD72281702FCC2B4', * FROM ZZZ_A7C5ED1CFBC843E9AD72281702FCC2B4.Preferences
The first select top 0 row is a fall back so I always have a default definition, and a static table definition. You can select from the view and filter by a session id with
SELECT PreferenceGUID, Value
FROM engine.vw_Preferences
WHERE SessionID = 'ZZZ_5D6FB09228D941AC9ECD6C7AC47F6779';
The interesting part here though is how the execution plan is generated when you have static values inside a view. the unions that would not produce results are not evaluated by the code, leaving a basic execution plan without any joins or unions...
You can test this, it and it is just as efficient as reading directly from the table (to within a margin of error so minor nobody would care). It is even possible to replace the write back processes by using "instead" triggers and then building dynamic sql in the background. The dynamic sql is less efficient on writes but it means you can update any table via the view, usually only possible with a single table view.
Dynamic Sql usually effects both performance and security, most of the times for the worst. However, since you can't parameterize identifiers, this is probably the only way for you unless you are willing to duplicate your stored procedures for each schema:
create procedure dbo.usp_GetDataFromTable1
#schemaname varchar(100),
#userid bigint
as
begin
if #schemaname = 'a'
begin
select EmailID from a.Table1 where ID = #user_id
end
else if schemaname = 'b'
begin
select EmailID from b.Table1 where ID = #user_id
end
end
The only reason I can think of for doing this is satisfying multiple tenants. You're close but the approach you are taking is wrong.
There are 3 solutions for multi-tenancy which I'm aware of: Database per tenant, single database schema per tenant, or single database single schema (aka, tenant by row).
Two of these have already been mentioned by other users here. The one that hasn't really been detailed is schema per tenant which is what it looks like you fall under. For this approach you need to change the way you see the database. The database at this point is just a container for schemas. Each schema can have their own design, stored procs, triggers, queues, functions, etc. The main goal is data isolation. You don't want tenant A seeing tenant Bs stuff. The advantage of the schema per tenant approach is you can be more flexible with tenant specific database changes. It also allows you to scale easier than a database per tenant approach.
Answer: Instead of writing dynamic SQL to take into account the schema using the DBO user you should instead create the same stored proc for each schema (create procedure example: schema_name.stored_proc_name). In order to run the stored proc for a schema you'll need to impersonate a user that is tied to the schema in question. It would look something like this:
execute as user = 'tenantA'
exec sp_testing
revert --revert will take us back to the original user, most likely DBO in your case.
Data collation across all tenants is a little harder. The only solution that I'm aware of is to run using the DBO user and "union all" the results across all schemas separately, kind of tedious if you have a ton of schemas.

Does anyone see a performance issue with my logon Trigger?

Does anyone see a performance issue with my logon Trigger?
I'm trying to reduce the overhead and prevent any performance issues before I push this trigger to my production SQL Server.
I currently have the logon trigger working on my Development sql server. I let it run over the past weekend and it put 50,000+ rows into my audit log table. I noticed that 95% of the records where for the logon 'NT AUTHORITY/SYSTEM'. So I decided to filter anything with 'NT AUTHORITY%' and just not insert those records. My thinking is if I do filter these 'NT AUTHORITY' records that the amount of resources I'll save on those inserts will make up for the cost of the IF statement check. I also have been watching Prefmon and don't see anything unusual while the trigger is enabled, but then again my Development server dosn't see the same amount of activity as production.
USE [MASTER]
GO
CREATE TRIGGER AuditServerAuthentication
ON ALL SERVER
WITH EXECUTE AS SELF
FOR LOGON
AS BEGIN
DECLARE #event XML, #Logon_Name VARCHAR(100)
SET #Event = EVENTDATA()
SET #Logon_Name = CAST(#event.query('/EVENT_INSTANCE/LoginName/text()') AS VARCHAR(100))
IF #Logon_Name NOT LIKE 'NT AUTHORITY%'
BEGIN
INSERT INTO Auditing.Audit.Authentication_Log
(Post_Time,Event_Type,Login_Name,Client_Host,Application_Name,Event_Data)
VALUES
(
CAST(CAST(#event.query('/EVENT_INSTANCE/PostTime/text()') AS VARCHAR(64)) AS DATETIME),
CAST(#event.query('/EVENT_INSTANCE/EventType/text()') AS VARCHAR(100)),
CAST(#event.query('/EVENT_INSTANCE/LoginName/text()') AS VARCHAR(100)),
CAST(#event.query('/EVENT_INSTANCE/ClientHost/text()') AS VARCHAR(100)),
APP_NAME(),
#Event
)
END
END
GO
I use a very similar trigger in my servers, and i haven't experience any performance issues. The production DB gets about 10 logins per second. This creates a huge amount of data over time which translates to bigger backups, etc.
For some servers i created a table with the users that logins shouldn't be logged, with this is also possible to refuse logins according to working hours
The difference with my trigger is that I've created a DB for auditing purposes, in which i created some stored procedures that i call in the trigger. The trigger looks like this
alter TRIGGER [tr_AU_LogonLog] ON ALL SERVER
WITH EXECUTE AS 'AUDITUSER'
FOR LOGON
AS
BEGIN
DECLARE
#data XML
, #rc INT
SET #data = EVENTDATA()
EXEC #rc = AuditDB.dbo.LogonLog #data
END ;
The production DB gets about 10 logins per second. This creates a huge amount of data over time which translates to bigger backups, etc.
EDIT: oh i forgot, its recommended if you create a specific user for the trigger, execute as self can be dangerous in some scenarios.
EDIT2: There's some useful information about the execute as statement here. oh and be careful while implementing triggers, you can accidentally lock yourself out, i recommend keeping a connection open just in case :)
It doesn't look like a costly IF statement at all to me (it's not like you are selecting anything from the database) and, as you say, would be far less costly than performing an INSERT that is isn't necessary 95% of the time. However, I should add I am just a database programmer and not a DBA, so am open to being corrected here.
I am, though, slightly curious as to why you are doing this? Doesn't SQL Server already have a built-in mechanism for Login Auditing that you can use?
There's nothing immediately obvious. It's effectively an IF that protects an INSERT. The only thing I would validate is how expensive the XML parsing is - I've not used it in SQL Server yet, so it's an unknown to me.
Admittedly, it would seem odd for Microsoft to supply an easy way to get metadata (EVENTDATA()) yet make it expensive to parse, yet stranger things have happened...

Disable messages in SQL2008 result set

further to these two questions, is there a way to disable the messages that may get sent along with the resultset in SQL2008?
(please note this is nothing to do with the ANSI_WARNINGS setting. Or NOCOUNT.)
Thanks for any help.
Edit: It's not a problem with compatibility settings or table owners. And it's nothing to do with NOCOUNT. Trust me.
No, there's not a way to disable all messages that get sent along with the result sets. Set nocount on/off doesn't have an effect on these types of messages.
You need the NOCOUNT in the body of the Sproc anyway (I appreciate that you've tested it with and without)
In circumstances like this I get the actual call to the Sproc (either from a Debug in my APP, or using SQL Profiler) and then plug that into SSMS or whatever IDE you use, wrapping it in a ROLLBACK transaction (so it can't accidentally make any changes). Note: Log on to SQL Server, with your IDE, using the same credentials as the App will use.
BEGIN TRANSACTION
EXEC StaffEnquirySurnameSearch #searchterm = 'FOOBAR'
ROLLBACK
and see what you get. Use TEXT mode for output, rather than GRID mode which might hide something
Just to show how I think NOCOUNT shoud be added to your SProc:
CREATE PROCEDURE StaffEnquirySurnameSearch
#searchterm varchar(255)
AS
SET NOCOUNT ON
SELECT AD.Name, AD.Company, AD.telephoneNumber, AD.manager, CVS.Position,
CVS.CompanyArea, CVS.Location, CVS.Title, AD.guid AS guid,
AD.firstname, AD.surname
FROM ADCVS AD
LEFT OUTER JOIN CVS ON
AD.Guid=CVS.Guid
WHERE AD.SurName LIKE #searchterm
ORDER BY AD.Surname, AD.Firstname
GO
I note that you are not prefixing the tables with a database owner (most commonly "dbo") which might mean that there are additional copies owned by whomever and that they turn out to be the default from the applications permissions perspective, although I don't think that will change the resultsets [between SQL versions], However, same thing applies to ownership of the Sproc, and there you might be calling some earlier version, created for a different owner.
Ditto where your Sproc name is defined in your ASP.NET code (which I can't seem to find in your linked question) should also have the owner defined, i.e.
EXEC dbo.StaffEnquirySurnameSearch #searchterm = 'FOOBAR'
Did you change the compatibility level when you upgraded from SQL 2000 to 2008? If it is some sort of backward compatibility warning message that might cure it.
Have you tried running the same CONTAINS query without the "OR"?
i.e.:
SELECT * FROM my_table
WHERE CONTAINS(my_column, 'a monkey') -- "a" is a noise word
instead of
SELECT * FROM my_table
WHERE CONTAINS(my_column, 'a OR monkey') -- "a" is a noise word
You can wrap it in a try catch... more info in books online
For example:
CREATE TABLE Test_ShortString(
ShortString varchar(10) NULL
)
begin Try
insert into
Test_ShortString (ShortString)
values ('123456789012345')
End Try
Begin catch
--Select Error_Number() as ErrorNumber
end catch

Resources