SQL, Analysis Services & related stories.

  • SQL 2000 to SQL 2005 migration: temporary tables reuse issue.

    During the same project the next issue was found: temporary table reuse. Here is an example on “how to reproduce”:

    USE tempdb

    GO

     

    CREATE PROCEDURE dbo.usp_step_one

    AS

    BEGIN

                    CREATE TABLE #temp (ID INT NOT NULL, NUM INT)

                    ALTER TABLE #temp ADD CONSTRAINT PK_ID PRIMARY KEY (ID)

                    INSERT INTO #temp SELECT 1, 1

    END

    GO

     

    CREATE PROCEDURE dbo.usp_step_two

    AS

    BEGIN

                    CREATE TABLE #temp (ID INT NOT NULL, NUM INT)

                    INSERT INTO #temp SELECT 1, 1

    END

    GO

     

    BEGIN TRAN

    EXEC dbo.usp_step_one

    PRINT 'step one 1'

     

    EXEC dbo.usp_step_one

    PRINT 'step one 2'

     

    EXEC dbo.usp_step_two

    PRINT 'step two 1'

    COMMIT

    GO

     

    DROP PROCEDURE dbo.usp_step_one

    DROP PROCEDURE dbo.usp_step_two

    GO

     

    SQL 2000 will process this batch without errors, but SQL 2005 will get you the following results:

     

    step one 1

    Msg 2714, Level 16, State 4, Procedure usp_step_one, Line 6

    There is already an object named 'PK_ID' in the database.

    Msg 1750, Level 16, State 0, Procedure usp_step_one, Line 6

    Could not create constraint. See previous errors.

     

    The problem is in temporary tables caching during transaction. In SQL 2000 it always used to drop local temp tables as soon as they get out of scope. In SQL 2005, the drop is delayed until the transaction is gone (rollback / commit). You can find some more details about temp tables caching here. As a workaround you can:

    -          Use table variables instead of temporary tables

    -          Avoid named constraint creation on temporary objects like

     

    CREATE TABLE #temp (ID INT NOT NULL PRIMARY KEY CLUSTERED, NUM INT)

     

  • SQL 2000 to 2005 migration: execution plan change issue.

    Last week I was engaged on SQL 2000 to SQL 2005 migration project, and we had identified some issues related to significant changes in execution plans.  Before migration we tested the application with using Upgrade Advisor (test passed without any caution), but hopefully we also had made an additional “trace replace” test. During this stage it was found that some stored procedures works fine in SQL 2000 but always failed in SQL 2005. Here is a script on “how to reproduce” :

    declare @xmlDoc int

     

    exec sp_xml_preparedocument @xmlDoc output, '<?xml  version=''1.0'' encoding=''windows-1251'' ?><filter><first-name></first-name><mid-name></mid-name><last-name>John</last-name><table-number></table-number><login></login><active>1</active></filter>'

     

    select * into #temp_xml  from openxml(@xmlDoc, '/filter')

     

    exec sp_xml_removedocument @xmlDoc

     

    select a.attr_id, b.attr_val_id

    from

      (

          select t1.id id, cast(cast(t2.text as varchar(10)) as int) attr_id

          from #temp_xml t1

                join #temp_xml t2 on t1.id = t2.parentid

          where t1.localname = 'AttributeID'

                and t2.localname  = '#text'

      ) a

          left outer join

      (

          select t1.parentid parentid, cast(cast(t2.text as varchar(10)) as int) attr_val_id

          from #temp_xml t1

                join #temp_xml t2 on t1.id = t2.parentid

          where t1.localname = 'AttrValueID'

                and t2.localname  = '#text'

      ) b on a.id = b.parentid

    where a.attr_id is not null

    order by a.attr_id

     

    drop table #temp_xml

     

    As mentioned above, SQL 2000 gives us an empty resultset, but SQL 2005 failed with

    Msg 245, Level 16, State 1, Line 14

    Conversion failed when converting the varchar value 'John' to data type int.

     

    We compared execution plans,  and  it was found, that the following part has different execution rule for different SQL version.

    select t1.id id, cast(cast(t2.text as varchar(10)) as int) attr_id

          from #temp_xml t1

                join #temp_xml t2 on t1.id = t2.parentid

          where t1.localname = 'AttributeID'

                and t2.localname  = '#text'

     

    SQL 2000 gives us the following:

      |--Compute Scalar(DEFINE:([Expr1002]=Convert(Convert([t2].[text]))))

           |--Hash Match(Inner Join, HASH:([t1].[id])=([t2].[parentid]), RESIDUAL:([t2].[parentid]=[t1].[id]))

                |--Filter(WHERE:([t1].[localname]='AttributeID'))

                |    |--Table Scan(OBJECT:([tempdb].[dbo].[aaa] AS [t1]))

                |--Filter(WHERE:([t2].[localname]='#text'))

                     |--Table Scan(OBJECT:([tempdb].[dbo].[aaa] AS [t2]))

     

    SQL 2005’s version:

      |--Hash Match(Inner Join, HASH:([t1].[id])=([t2].[parentid]), RESIDUAL:([tempdb].[dbo].[aaa].[parentid] as [t2].[parentid]=[tempdb].[dbo].[aaa].[id] as [t1].[id]))

           |--Table Scan(OBJECT:([tempdb].[dbo].[aaa] AS [t1]), WHERE:([tempdb].[dbo].[aaa].[localname] as [t1].[localname]=N'AttributeID'))

           |--Compute Scalar(DEFINE:([Expr1006]=CONVERT(int,CONVERT(varchar(10),[tempdb].[dbo].[aaa].[text] as [t2].[text],0),0)))

                |--Table Scan(OBJECT:([tempdb].[dbo].[aaa] AS [t2]), WHERE:([tempdb].[dbo].[aaa].[localname] as [t2].[localname]=N'#text'))

     

    As we can see SQL 2000 include two additional steps, but absolutely correct. SQL 2005 is much shorter, but absolutely incorrect and far from optimal (because of CONVERT applied to much more records than necessary). Tested with public 9.0.3159 version.

    Summary: Upgrade advisor will never give you a 100% guaranty on safe migration. If possible please always collect some traces and replace it during testing phase of migration project.

  • How to avoid 1000 rows limitation when querying active directory (AD) from SQL 2005 with using custom code.

    As all of you know it is possible to query active directory from SQL Server with using ADSI provider as linked server. This solution works fine until you will have a lot of users in active directory. According to best practice guide windows system engineers always configured AD to return no more then 1000 rows per one query. Of course you can avoid this limitation too (with using range keyword or some other ways, as for me require deep AD knowledge). Some other extremily complicated T-SQL scripts can be found in internet. My current post's goal is to show two main things:

    1. how to register a couple of assemblies (your own and related system assemblies) in SQL Server with UNSAFE permission WITHOUT setting TRUSTWORTHY ON for your database according to best practice security guide. Security check will passed in this way according to certificate permissions.

    2. how to create a simple CLR procedures for quirying AD as any other SQL datasource without creating linked server.

    You can find MSADHelper2.rar project attached. It contains project files; you can extract MSADHelper.dll assembly from here, or build a new one by yourself (in that case please generate a new strong key, my is not included in project files).

    P.S. I am not an expert in C#, so you can improve this source as you wish to add some dispose or other required methods.

    Here is installation script:

    SET NOCOUNT ON
    GO
    USE Master
    GO

    --sp_configure 'clr enabled', 1
    --reconfigure with override

    --ALTER DATABASE <your_database_name> SET TRUSTWORTHY OFF
    --GO

    -- create keys from assembly 

    CREATE ASYMMETRIC KEY MSADHelperAsKey FROM EXECUTABLE FILE = 'C:\distrib\ADSI\MSADHelper2.dll' -- specify correct path here.
    GO
    CREATE ASYMMETRIC KEY SystemDirectoryServicesKey FROM EXECUTABLE FILE = 'C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.DirectoryServices.dll'
    GO
    --SELECT * FROM sys.asymmetric_keys
    --GO

    -- create logins for special goals
    CREATE LOGIN SQLCLRHelperLogin FROM ASYMMETRIC KEY MSADHelperAsKey
    GO
    CREATE LOGIN SQLCLRSysDirServLogin FROM ASYMMETRIC KEY SystemDirectoryServicesKey
    GO

    -- grant necessary (UNSAFE) permissions
    GRANT UNSAFE ASSEMBLY TO SQLCLRHelperLogin
    GO
    GRANT UNSAFE ASSEMBLY TO SQLCLRSysDirServLogin
    GO

    -----------------------------------------------------------------------
    ---
    --- **************************************************************
    ---
    -----------------------------------------------------------------------

    USE <your_database_name>
    GO

    CREATE ASSEMBLY [System.DirectoryServices]
    FROM 'C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.DirectoryServices.dll'
    WITH PERMISSION_SET = UNSAFE
    GO
    CREATE ASSEMBLY [MsForClient.SqlServer.SqlClrToolkit.MSADHelper]
    FROM 'C:\distrib\ADSI\MSADHelper2.dll' -- please specify correct path here
    WITH PERMISSION_SET = UNSAFE
    GO

    --Have a look at the assembly within the database
    --SELECT * FROM sys.assemblies
    --SELECT * FROM sys.assembly_files
    GO

    --Create procedures from the assembly

    -- return list of registered providers (from registry)
    CREATE PROCEDURE dbo.usp_GetListOfRegisteredDirectoryProviders
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_GetListOfRegisteredDirectoryProviders
    GO

    -- test methods - is it possible to create AD object? 

    CREATE PROCEDURE dbo.usp_TryAuthenticate(@pAdsiPath nvarchar(4000), @pSecure int)
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_TryAuthenticate
    GO

    -- by default any method will execute under SQL Server Service account. If this account don't have enought privileges - please specify another one as @pUserName and

    -- @pPassword to access AD

    CREATE PROCEDURE dbo.usp_TryAuthenticateAsUser(@pAdsiPath nvarchar(4000), @pUserName sysname, @pPassword nvarchar(100), @pSecure int)
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_TryAuthenticateAsUser
    GO

    -- enum AD structure - be carefull, to much info!

    CREATE PROCEDURE dbo.usp_FillInfoByPath(@pAdsiPath nvarchar(4000), @pUserName sysname, @pPassword nvarchar(100), @pSecure int)
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_FillInfoByPath
    GO

    -- get AD object properties

    CREATE PROCEDURE dbo.usp_GetNodeProperties(@pAdsiPath nvarchar(4000), @pUserName sysname, @pPassword nvarchar(100), @pSecure int)
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_GetNodeProperties
    GO

    -- extract user list

    CREATE PROCEDURE dbo.usp_GetUserList(@pAdsiPath nvarchar(4000), @pOutputFieldList NVarChar(2000), @pUserName sysname, @pPassword nvarchar(100), @pSecure int, @pScope nvarchar(10))
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_GetUserList
    GO

    -- extract group list

    CREATE PROCEDURE dbo.usp_GetGroupList(@pAdsiPath nvarchar(4000), @pOutputFieldList NVarChar(2000), @pUserName sysname, @pPassword nvarchar(100), @pSecure int, @pScope nvarchar(10))
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_GetGroupList
    GO

    -- extract list of computers

    CREATE PROCEDURE dbo.usp_GetComputerList(@pAdsiPath nvarchar(4000), @pOutputFieldList NVarChar(2000), @pUserName sysname, @pPassword nvarchar(100), @pSecure int, @pScope nvarchar(10))
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_GetComputerList
    GO

    -- extract members of AD group

    CREATE PROCEDURE dbo.usp_GetGroupMembers(@pAdsiPath nvarchar(4000), @pOutputFieldList NVarChar(2000), @pUserName sysname, @pPassword nvarchar(100), @pSecure int)
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_GetGroupMembers
    GO

    -- enum user membership

    CREATE PROCEDURE dbo.usp_GetUserMembership(@pAdsiPath nvarchar(4000), @pOutputFieldList NVarChar(2000), @pUserName sysname, @pPassword nvarchar(100), @pSecure int)
    AS EXTERNAL NAME [MsForClient.SqlServer.SqlClrToolkit.MSADHelper].[MsForClient.SqlServer.SqlClrToolkit.MSADHelper].usp_GetUserMembership
    GO

    -- execute & test section

    EXEC dbo.usp_GetListOfRegisteredDirectoryProviders
    GO

    -- try to check - if net framework carefully installed and AD objects created without any issue
    DECLARE @pStr nvarchar(4000)
    SET @pStr = N'LDAP://DC=HQ,DC=corp,DC=xxxxxxxx,DC=ru'
    EXEC dbo.usp_TryAuthenticate @pStr, 1
    GO
    -- try to run under another account

    DECLARE @pStr nvarchar(4000), @pUserName sysname, @pPassword nvarchar(100)
    SELECT @pStr = N'LDAP://DC=HQ,DC=corp,DC=xxxxxxxx,DC=ru', @pUserName = N'SQLSvc', @pPassword = N'xxxxxxx'

    EXEC dbo.usp_TryAuthenticateAsUser @pStr, @pUserName, @pPassword, 1
    GO

    -- extract AD info as relational tree (use ID & ParentID to build object tree)
    DECLARE @pStr nvarchar(4000), @pUserName sysname, @pPassword nvarchar(100)
    SET @pStr = N'LDAP://DC=HQ,DC=corp,DC=xxxxxxx,DC=ru'

    EXEC dbo.usp_FillInfoByPath @pStr, @pUserName, @pPassword, 0
    GO

    --- get all properites of object
    DECLARE @pStr nvarchar(4000), @pUserName sysname, @pPassword nvarchar(100)
    --SET @pStr = N'LDAP://CN=User01,CN=Users,DC=HQ,DC=corp,DC=xxxxxxx,DC=ru'
    SET @pStr = N'LDAP://CN=Domain Admins,CN=Users,DC=HQ,DC=corp,DC=xxxxxxxxxx,DC=ru'

    EXEC dbo.usp_GetNodeProperties @pStr, @pUserName, @pPassword, 0
    GO
    ---- get user list and attributes

    DECLARE @pStr nvarchar(4000), @pOutputFieldList nvarchar(2000), @pUserName sysname, @pPassword nvarchar(100)
    -- to extract users from exact OU
    -- 'LDAP://OU=TestGroup,DC=HQ,DC=corp,DC=xxxxxxxx,DC=ru'
    -- all users from AD
    -- @pStr = N'LDAP://DC=HQ,DC=corp,DC=xxxxxxx,DC=ru'
    SELECT @pStr = N'LDAP://DC=HQ,DC=corp,DC=xxxxxxxxx,DC=ru',
     @pOutputFieldList = 'cn,name,description,distinguishedName,whenChanged,primaryGroupID,memberOf', @pUserName = '', @pPassword = N''

    EXEC dbo.usp_GetUserList @pStr, @pOutputFieldList, @pUserName, @pPassword, 0, 'Subtree'
    GO

    ---- extract group list
    DECLARE @pStr nvarchar(4000), @pOutputFieldList nvarchar(2000), @pUserName sysname, @pPassword nvarchar(100)
    -- to extract groups from exact group
    -- 'LDAP://CN=Domain Admins,CN=Users,DC=HQ,DC=corp,DC=xxxxxxx,DC=ru'
    -- all groups from AD
    -- @pStr = N'LDAP://DC=HQ,DC=corp,DC=xxxxxx,DC=ru'
    SELECT @pStr = N'LDAP://DC=HQ,DC=corp,DC=xxxxxx,DC=ru',
     @pOutputFieldList = 'cn,name,description,distinguishedName,whenChanged', @pUserName = '', @pPassword = N''

    EXEC dbo.usp_GetGroupList @pStr, @pOutputFieldList, @pUserName, @pPassword, 0, 'Subtree'
    GO

    ---- get computer list
    DECLARE @pStr nvarchar(4000), @pOutputFieldList nvarchar(2000), @pUserName sysname, @pPassword nvarchar(100)
    SELECT @pStr = N'LDAP://DC=HQ,DC=corp,DC=xxxxxxxxx,DC=ru',
     @pOutputFieldList = 'cn,name,distinguishedName,whenChanged', @pUserName = '', @pPassword = N''

    EXEC dbo.usp_GetComputerList @pStr, @pOutputFieldList, @pUserName, @pPassword, 0, 'Subtree'
    GO

    ------- get group members
    DECLARE @pStr nvarchar(4000), @pOutputFieldList nvarchar(2000), @pUserName sysname, @pPassword nvarchar(100)
    --SELECT @pStr = N'LDAP://CN=Domain Admins,CN=Users,DC=HQ,DC=corp,DC=xxxxxxxxx,DC=ru',
    -- @pOutputFieldList = 'sAMAccountName'
    SELECT @pStr = N'LDAP://CN=Administrators,CN=Users,DC=HQ,DC=corp,DC=xxxxxxxxx,DC=ru',
     @pOutputFieldList = 'Name', @pUserName = '', @pPassword = N''

    EXEC dbo.usp_GetGroupMembers @pStr, @pOutputFieldList, @pUserName, @pPassword, 0
    GO

    ----- enlist user membership
    DECLARE @pStr nvarchar(4000), @pOutputFieldList nvarchar(2000), @pUserName sysname, @pPassword nvarchar(100)
    SELECT @pStr = N'LDAP://CN=Administrator,CN=Users,DC=HQ,DC=corp,DC=xxxxxxxxx,DC=ru',
     @pOutputFieldList = 'Name', @pUserName = '', @pPassword = N''

    EXEC dbo.usp_GetUserMembership @pStr, @pOutputFieldList, @pUserName, @pPassword, 0
    GO


    -- Here is a section of cleanup script

    USE <your_database_name>
    GO

    -- delete procedures
    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_GetListOfRegisteredDirectoryProviders]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_GetListOfRegisteredDirectoryProviders]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_TryAuthenticate]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_TryAuthenticate]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_TryAuthenticateAsUser]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_TryAuthenticateAsUser]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_FillInfoByPath]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_FillInfoByPath]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_GetNodeProperties]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_GetNodeProperties]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_GetUserList]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_GetUserList]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_GetGroupList]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_GetGroupList]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_GetComputerList]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_GetComputerList]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_GetGroupMembers]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_GetGroupMembers]
    GO

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[usp_GetUserMembership]') AND type in (N'P', N'PC'))
     DROP PROCEDURE [dbo].[usp_GetUserMembership]
    GO

    -- delete assemblies
    IF  EXISTS (SELECT * FROM sys.assemblies asms WHERE asms.name = N'MsForClient.SqlServer.SqlClrToolkit.MSADHelper')
     DROP ASSEMBLY [MsForClient.SqlServer.SqlClrToolkit.MSADHelper]
    GO

    IF  EXISTS (SELECT * FROM sys.assemblies asms WHERE asms.name = N'System.DirectoryServices')
     DROP ASSEMBLY [System.DirectoryServices]
    GO

    USE Master
    GO

    -- drop logins

    IF  EXISTS (SELECT * FROM sys.server_principals WHERE name = N'SQLCLRSysDirServLogin')
     DROP LOGIN [SQLCLRSysDirServLogin]
    GO

    IF  EXISTS (SELECT * FROM sys.server_principals WHERE name = N'SQLCLRHelperLogin')
     DROP LOGIN [SQLCLRHelperLogin]
    GO

    -- drop keys 

    IF EXISTS(SELECT * FROM sys.asymmetric_keys WHERE [name] = 'MSADHelperAsKey')
     DROP ASYMMETRIC KEY MSADHelperAsKey
    GO

    IF EXISTS(SELECT * FROM sys.asymmetric_keys WHERE [name] = 'SystemDirectoryServicesKey')
     DROP ASYMMETRIC KEY SystemDirectoryServicesKey
    GO

    -- Seems to me finished :-)

    Dec 08, 2007 :

    i updated source code to fix bug - many thanks to Taylor Gerring

  • Switching context with using EXECUTE AS ... not always switch it in fact as you wish.

    In this demo i will try to explain why SET TRUSTWORTHY ON on some databases may make the sysadmin job unsafe. In previous posts i explained how db owner (or any developer) can try to implemet simplest luring attack against server sysadmin. According to BOL to avoid this problem sysadmin should switch to the context of the account/login with lowest possible privileges. Lets try to test this approach.

    USE master
    GO

    -- create test database
    CREATE DATABASE [TestDB] ON PRIMARY ( NAME = N'TestDB', FILENAME = N'C:\TestDB.mdf', SIZE = 3072KB, FILEGROWTH = 1024KB )
     LOG ON ( NAME = N'TestDB_log', FILENAME = N'C:\TestDB_log.ldf', SIZE = 1024KB, FILEGROWTH = 10%)
    GO

    -- set option
    EXEC dbo.sp_dbcmptlevel @dbname=N'TestDB', @new_cmptlevel=90
    GO

    -- first time test with TRUSTWORTHY = OFF

    ALTER DATABASE [TestDB] SET TRUSTWORTHY OFF
    GO

    -- create login Test
    USE [master]
    GO
    CREATE LOGIN [Test] WITH PASSWORD=N'111', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
    GO

    USE [TestDB]
    GO

    -- create user test
    CREATE USER [Test] FOR LOGIN [Test]
    GO

    -- create test DDL trigger - fired on any DENY event
    CREATE TRIGGER [ddl_test_trigger]
    ON DATABASE
    FOR DENY_DATABASE
    AS
     
    SET NOCOUNT ON

     IF IS_SRVROLEMEMBER ('sysadmin') = 1
      PRINT 'Sysadmin found, security can be broken easily.'
     ELSE
      PRINT 'Sysadmin not found, you are in safe.'
     
    GO

    -- test under sysadmin (sa) account
    DECLARE @cmd nvarchar(1000)

    SET @cmd = 'DENY BACKUP DATABASE TO [Test]'
    EXEC (@cmd)
    GO

    -- you'll see "Sysadmin found, security can be broken easily."

    -- so the next step according to BOL - switch to lowest privileges context for desired operation
    DECLARE @cmd nvarchar(1000)
    SET @cmd = 'DENY BACKUP DATABASE TO [Test]'
    EXEC (@cmd) AS USER = 'dbo'
    GO

    -- you'll see now "Sysadmin not found, you are in safe."
    -- Thats is our desired goal - to be in safe. Lets try to set TRUSTWORTHY ON

    ALTER DATABASE [TestDB] SET TRUSTWORTHY ON
    GO

    -- try again the "safe" version
    DECLARE @cmd nvarchar(1000)
    SET @cmd = 'DENY BACKUP DATABASE TO [Test]'
    EXEC (@cmd) AS USER = 'dbo'
    GO

    -- you'll see again "Sysadmin found, security can be broken easily."

    -- clear objects
    USE Master
    GO

    DROP DATABASE [TestDB]
    GO

    DROP LOGIN [Test]
    GO

    --------------------------------------------------------

    As as result of this demo i would recommend you do not use TRUSTWORTHY ON on your databases. Otherwise before making any
    changes under 'sa' account in unknown databases please check TRUSTWORTHY setting for the database you are
    working with, because simple EXECUTE AS .... (even with cookie) will not protect you from loosing you privileges or
    GRANT 'sa' privileges to everybody (as explained in my previous articles). And be always in safe :-).

    --------------------------------------------------------

     

  • Workaround for “Unable to set Default Schema for a group”.

    Let assume you are using Windows Authentication with SQL Server 2005,  you've added a new server account for domain group and would like to give it a default schema.  The properties window is the same for users and groups but the default schema field is enabled only for user entities.  As a result you cannot add a default schema to a group. So if any member of [domain]\TestGroup will try to create table without explicit schema pointed in a statement (like CREATE TABLE t1 (ID int)), (s)he will always get an error.

     

    You can see a long thread about this issue here

     

    http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=79418&SiteID=1&PageID=0

     

    I’d like to show you a simple workaround.

    Before testing please create a Windows group like [domain]\TestGroup, create a dummy user [domain]\TestUser and make this user be a member of TestGroup.

     

    USE [master]
    GO

     

    -- create login for the group
    CREATE LOGIN [FCOD\TestGroup] FROM WINDOWS WITH DEFAULT_DATABASE=[Northwind]

    GO


    -- switch to database
    USE Northwind

    GO


    -- create dummy schema

    CREATE SCHEMA [dummy] – this is a schema for testing.

    GO

     

    -- create a database user for TestGroup (Windows group)
    CREATE USER [TestGroup] FOR LOGIN [FCOD\TestGroup]

    GO

     

    ALTER AUTHORIZATION ON SCHEMA::[dummy] TO [TestGroup]

    GO

     

    -- grant CREATE TABLE privilege, if you need others – please assign theirs here.
    GRANT CREATE TABLE TO [TestGroup]

    GO

     

    That’s all. Now TestUser can connect to SQL Server, Northwind database, and create table. Please reboot your computer, logon like TestUser and connect to SQL Server. When you will run

     

    CREATE TABLE t1 ( ID int )

     

    you’ll see, that SQL engine will automatically:

    -          create [domain]\TestUser schema in Northwind

    -          create [domain]\TestUser database user in Northwind

    -          create table [domain\TestUser].t1 in Northwind

     

    Any user (member of [domain]\TestGroup) currently can connect to desired server, work with database and even create own tables in own schemas. No other actions required, and no rules required for any new member of TestGroup.

     

    If you like all table be created in dummy schema, you can use explicit schema like 

     

    CREATE TABLE dummy.t1 ( ID int )

  • SQL Server 2005: building security model based on DDL triggers.

    Last month I’m working with a client to create something like non-standard security model. He asked for the following features:

     

    -          server–level management only for sa (server admin).

    -          database–level management for dbo (one or more dbo per database). Dbo is responsible for database's user management, update / refresh structure etc.; but dbo should not be able to alter database.

    -          dbo as well as other database persons should not be able to make a database backup (or log file backup). This is necessary because database backups covered by Veritas software and any unwanted backup may hang database recovery process.

    -          strong security, to restrict sa rights from been captured by dbo.

     

     So we’ve decided to start from server-level permissions. To capture server control somebody can run

     

    GRANT CONTROL SERVER TO test_user.

     

    To run this statement you should be a member of sysadmin server role, but this is not a big problem for dbo at all (see below).

    To prevent form this issue happens we’ve decided to use server-level events. Lets have a look :

     

    CREATE TRIGGER [ddl_restrict_any_serverlevelpermission]

    ON ALL SERVER

    -- i'm using event groups here, but it is possible to track events more precise.

    FOR DDL_GDR_SERVER_EVENTS

    AS

     

    DECLARE @data XML

    SET @data = EVENTDATA()

    --SELECT @data

     

    /*

    <EVENT_INSTANCE>

      <EventType>GRANT_SERVER</EventType>

      <PostTime>2007-01-12T19:28:39.580</PostTime>

      <SPID>63</SPID>

      <ServerName>NAME</ServerName>

      <LoginName>sa</LoginName>

      <Grantor>sa</Grantor>

      <Permissions>

        <Permission>control server</Permission>

      </Permissions>

      <Grantees>

        <Grantee>test_user</Grantee>

      </Grantees>

      <AsGrantor />

      <GrantOption>0</GrantOption>

      <CascadeOption>0</CascadeOption>

      <TSQLCommand>

        <SetOptions ANSI_NULLS="ON" ANSI_NULL_DEFAULT="ON" ANSI_PADDING="ON" QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" />

        <CommandText>GRANT CONTROL SERVER TO test_user</CommandText>

      </TSQLCommand>

    </EVENT_INSTANCE>

    */

     

    DECLARE @event_type nvarchar(200), @login_name sysname, @grantor sysname, @grantee sysname

     

    -- actualy you can find spid here, but this value useless

    SELECT @event_type = @data.value('(/EVENT_INSTANCE/EventType)[1]','sysname')

                , @login_name = @data.value('(/EVENT_INSTANCE/LoginName)[1]','sysname')

                , @grantor = @data.value('(/EVENT_INSTANCE/Grantor)[1]','sysname')

                , @grantee = @data.value('(/EVENT_INSTANCE/Grantees/Grantor)[1]','sysname')

     

    -- send email to sa before you will raise error

    --EXEC msdb.dbo.sp_send_dbmail @recipients='server_admin@Adventure-Works.com',

    --    @subject = 'Attempt to catch server-level permission',

    --    @body = @data,

    --    @body_format = 'HTML'

     

    -- additional features.

    -- 1. we can analyze <grantor> here to add some additional logic here.

    -- 2. you can analyze <grantee> list here to restrict the list of persons from been granted server control (for example, by mistake or misprint).

    -- 3. for demostration purposes we will restrict any kind of server-level permissions at all.

     

    RAISERROR ('GRANT/DENY/REVOKE SERVER level permission denied! Ask your server administrator.',10, 1)

    ROLLBACK

     

    GO

     

    That’s all. This trigger will inform server admin for any attempt to grant / deny / revoke any server level permission.

     After that we will prevent dbo from altering database the same way:

     

    CREATE TRIGGER [ddl_restrict_alterdatabase]

    ON ALL SERVER

    FOR ALTER_DATABASE, DROP_DATABASE

    AS

     

    DECLARE @data xml

    DECLARE @trigger_name sysname, @LoginName sysname, @UserName sysname, @dbname sysname

     

    SET @data = EVENTDATA()

    --SELECT @data

     

    /*

    <EVENT_INSTANCE>

      <EventType>ALTER_DATABASE</EventType>

      <PostTime>2007-01-12T20:05:27.527</PostTime>

      <SPID>65</SPID>

      <ServerName>NAME</ServerName>

      <LoginName>sa</LoginName>

      <DatabaseName>TestSecurity</DatabaseName>

      <TSQLCommand>

        <SetOptions ANSI_NULLS="ON" ANSI_NULL_DEFAULT="ON" ANSI_PADDING="ON" QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" />

        <CommandText>ALTER DATABASE [TestSecurity] SET RECOVERY SIMPLE WITH NO_WAIT

    </CommandText>

      </TSQLCommand>

    </EVENT_INSTANCE>

    */

     

    -- send email to self before you will raise error

    -- EXEC msdb.dbo.sp_send_dbmail @recipients='server_admin@Adventure-Works.com',

    --    @subject = 'Attempt to alter database',

    --    @body = @data,

    --    @body_format = 'HTML'

     

    -- If you want to allow ALTER DATABASE operation to some person – please use <login_name> tag and add some logic here

    -- If you want to exclude your personal databases from been tracked by this trigger – please pay attention on <DatabaseName> tag.

     

    RAISERROR ('ALTER DATABASE DISABLED!',10, 1)

    ROLLBACK

    GO

     

    Also we will protect sa from any server-level authorization attemts:

     

    CREATE TRIGGER [ddl_deny_alter_serverauthorization]

    ON ALL SERVER

    FOR DDL_AUTHORIZATION_SERVER_EVENTS

    AS

     

    DECLARE @data XML

    SET @data = EVENTDATA()

    --SELECT @data

     

    -- send email to self before you will raise error

    --EXEC msdb.dbo.sp_send_dbmail @recipients='server_admin@Adventure-Works.com',

    --    @subject = 'Attempt to alter server-level object ownership',

    --    @body = @data,

    --    @body_format = 'HTML'

     

    RAISERROR ('Alter server-level authorization denied. Ask your server administrator.',10, 1)

    ROLLBACK

     

    GO

     

    That’s all for server-level section. Below we will describe some database-level triggers. To improve your audit procedures you (server administrator) can use WITH ENCRYPTION option to prevent dbo from viewing the sources.

     

    The first goal is to protect database from any attempt of making backup. How we will cover this requirement:

    -          sa will create a script, this script will DENY BACKUP DATABASE, BACKUP LOG for each database user. This option will restrict even dbo from making backup. Nobody from db_backupoperator will be able to make backup J. Suddenly dbo or member of db_securityadmin still be able to return this privilege back with using GRANT BACKUP DATABASE TO <account>. Lets restrict these persons from been doing that.

     

    CREATE TRIGGER [ddl_denybackup_for_new_user]

    ON DATABASE

    FOR CREATE_USER

    AS

     

    DECLARE @data XML

    SET @data = EVENTDATA()

    --SELECT @data

     

    DECLARE @user sysname, @stmt nvarchar(4000)

     

    SET @user = @data.value('(/EVENT_INSTANCE/ObjectName)[1]','sysname')

    --SELECT @user

     

    SET @stmt = 'DENY BACKUP DATABASE, BACKUP LOG TO ' + @user

     -- here it is necessary to add some logic to check for SQL injection, sysname (nvarchar(256)) are to long. Please do that J.

     EXEC(@stmt)

     

    GO

     

    This trigger will deny backup privilege for any new database user. The following trigger will prevent anybody from restoring BACKUP DATABASE privilege.

     

    CREATE TRIGGER [ddl_prevent_grant_events]

    ON DATABASE

    FOR GRANT_DATABASE

    AS

     

    DECLARE @data XML

    SET @data = EVENTDATA()

    --SELECT @data

     

    DECLARE @permission sysname, @login_name sysname, @user_name sysname, @dbname sysname

                , @grantor sysname, @grantee sysname

     

    SELECT @permission = @data.value('(/EVENT_INSTANCE/Permissions/Permission)[1]','sysname')

                , @login_name = @data.value('(/EVENT_INSTANCE/LoginName)[1]','sysname')

                , @user_name = @data.value('(/EVENT_INSTANCE/UserName)[1]','sysname')

                , @dbname = @data.value('(/EVENT_INSTANCE/DatabaseName)[1]','sysname')

                , @grantor = @data.value('(/EVENT_INSTANCE/Grantor)[1]','sysname')

                , @grantee = @data.value('(/EVENT_INSTANCE/Grantees/Grantee)[1]','sysname')

     

    IF UPPER(@permission) in ('CONTROL', 'BACKUP DATABASE', 'BACKUP LOG')

    BEGIN

     

                -- send email to self before you will raise error

                -- EXEC msdb.dbo.sp_send_dbmail @recipients='server_admin@Adventure-Works.com',

                --    @subject = 'Attempt to take database ownership',

                --    @body = @data,

                --    @body_format = 'HTML'

     

                RAISERROR ('Option restricted. Please contact system administrator.', 16, 1)

                ROLLBACK

     

    END

     

    That’s all what you need. You can create this trigger, log into database as dbo and check the following:

     

    Use <test_security_database>

    GO

     

    GRANT CONTROL TO <any_user>

    GRANT BACKUP DATABASE TO <any_user>

     

    Suddenly hungry dbo can drop your triggers from his/her database. Lets protect our trigger for dbo too.

    Be very careful to create this trigger on your production database – you will have to think how to drop it after that J.

     

    CREATE TRIGGER [ddl_prevent_drop_trigger]

    ON DATABASE

    FOR DROP_TRIGGER, ALTER_TRIGGER

    AS

     

    DECLARE @data xml

    DECLARE @trigger_name sysname, @LoginName sysname, @UserName sysname, @dbname sysname

     

    SET @data = EVENTDATA()

     

    SELECT @trigger_name = @data.value('(/EVENT_INSTANCE/ObjectName)[1]','sysname')

                , @LoginName = @data.value('(/EVENT_INSTANCE/LoginName)[1]','sysname')

                , @UserName = @data.value('(/EVENT_INSTANCE/UserName)[1]','sysname')

                , @dbname = @data.value('(/EVENT_INSTANCE/DatabaseName)[1]','sysname')

     

    --SELECT [trigger_name] = @trigger_name

    --          , [LoginName] = @LoginName

    --          , [UserName] = @UserName

    --          , [DBname] = @dbname

     

    IF @trigger_name in ('ddl_prevent_drop_trigger', 'ddl_denybackup_for_new_user',

                            'ddl_prevent_grant_events')

     

                BEGIN

     

                            -- send email to self before you will raise error

                            -- EXEC msdb.dbo.sp_send_dbmail @recipients='server_admin@Adventure-Works.com',

                            --    @subject = 'Attempt to drop trigger',

                            --    @body = @data,

                            --    @body_format = 'HTML'

     

                            RAISERROR ('Please don''t touch this object!', 16, 1)

                            ROLLBACK

                END

     

    -- drop all other trigger

     

    GO

     

    This trigger will protect self and other triggers. This technology can be used to protect important information from been deleted / altered by mistake.

     

    Here is the end of successful story. You can build your additional security model with using server / database events or event groups. For more detail please have a look here and here.

     

    Now I will show you how to pass through this “security” and why this kind of security doesn’t make sense (at least with using SQL Server 2005 sp1).

     Let’s assume you are dbo and you’ve decided to grant control over the server. You will create the trigger for each your database:

     

    CREATE TRIGGER dbo.MyNiceTrigger

    ON DATABASE

    WITH ENCRYPTION

    FOR ALTER_OBJECT, DROP_TRIGGER, ALTER_TRIGGER – put here all possible database events

    AS

    BEGIN

    SET NOCOUNT ON;

    IF IS_SRVROLEMEMBER ('sysadmin') = 1

    begin

    SET @sqlstr='use master

    GRANT CONTROL SERVER TO <dbo_account> '

    EXEC sp_executesql @sqlstr

    End

    END

    GO

    After that ask server admin (sa) to make any changes in your database (or wait for a while when this will happens). After sa will change something this trigger fired and…. Server-level trigger will catch your attempt. Lets improve our trigger a little. As you can see there is no event to subscribe for disable trigger action. So lets try to use it J.

     

    CREATE TRIGGER dbo.MyNiceTrigger

    ON DATABASE

    WITH ENCRYPTION

    FOR ALTER_OBJECT, DROP_TRIGGER, ALTER_TRIGGER – put here all possible database events

    AS

    BEGIN

    SET NOCOUNT ON;

    IF IS_SRVROLEMEMBER ('sysadmin') = 1

    Begin

    SET @sqlstr='DISABLE Trigger ALL ON ALL SERVER'

    EXEC sp_executesql @sqlstr

    SET @sqlstr='DISABLE Trigger ALL ON DATABASE'

    EXEC sp_executesql @sqlstr

    SET @sqlstr='use master

    GRANT CONTROL SERVER TO <dbo_account> '

    EXEC sp_executesql @sqlstr

    End

    END

    GO

    Common conclusion. Nobody can protect sa.

     

    Conclusion for sa:

    n      if you are working with databases – please don’t use sa privileges, please re-logon under appropriate account with minimal privileges.

    n      create some audit procedures for your server. It is possible to scan triggers / procedures bodies for harm code by phrases (at least try to do that).

    n      create small trigger for CREATE_TRIGGER and CREATE_PROCEDURE event in every database). Try to scan object body for potential problems.

     

    Conclusion for dbo:

    n      …. It’s simple. You are always winner in this situation. Hope DISABLE_TRIGGER will be in the list of allowed events soon.

  • db_securityadmin is very powerfull and … dangerous.

    Few days ago I worked for one client. He uses the following business model:

    -          dbo usually responsible for high level database design and maintenance;

    -          all database users organized in additional security groups for security purposes;

    -          dedicated person is responsible for user’s security maintenance, (s)he is able to assign user to explicit (one or more) application security group. This dedicated person is a department’s secretary, (s)he is trusted person, has only right to run simple SELECT statement on some tables, but (s)he is member of db_securityadmin database role.

     

    Firstly it seems to me that security rules are appropriate and nothing can happen. Secretary is unable to maintain built-in database roles like db_datawriter etc.

     

    Please login under account – member of db_securityadmin to check

     

    SELECT * FROM fn_my_permissions(N'db_name', N'DATABASE')

    you will get the following list of privileges (according to BOL).

     

    --  ALTER ANY APPLICATION ROLE
    --  ALTER ANY ROLE
    --  CREATE SCHEMA
    --  VIEW DEFINITION

     

    But if you run

     

    GRANT CONTROL TO <secretary> -- grant database control to self

     

    and try to run

     

    SELECT * FROM fn_my_permissions(N'db_name', N'DATABASE')

    after that to ensure <secretary> now become dbo!

     

    Please be careful to use odd database security groups. Also only trusted person should be assigned to this group. As for now (SQL Server 2005 member of db_securityadmin) equal to database owner.

    Next article I will show you potential problems in more details.

  • ALTER DATABASE ... ALTER COLLATION (FORCED).

    As all of you know Microsoft has made a small problem for all SQL DBA and developers. If you have a database with any of SQL_xxxxx collations (for more details please have a look at the TERTIARY_WEIGHTS() function here), you will defenetily have a significant problem with index search and ordering performance if you'll try to upgrade your SQL Server with 2005 version. Some of developers was excited when first time reading this article, really :-).

    Suddenly ALTER DATABASE .. ALTER COLLATION command will change database collation for you, but all objects will stay "as is" with old collation (by design). The only way to change collation for all existing objects is to create a new empty database with new desired collation and copy data into this new database (copy wizzard, for example).

    To sort this issue out i created this small script (in attachment). It was tested both manually and with using VSTS DBPro, and i can confirm that you can change the collation of your database without any problem and manual work. As an input you should provide it with database with one collation, and as output you will have the same database with another (desired) collation. So as for me this is an analoque of ALTER DATABASE ... ALTER COLLATION FORCED command :-). How it works:

    - you should use sqlcmd or SSMS in sqlcmd mode,

    - specify database name as a parameter,

    - specify desired collation as a parameter,

    - run it and wait for results.

    What is not covered by the script: table and index partitioning. I guess that if you are using these advanced options, you are smart enough to change this script to add required feature. Mostly i created this script for the following scenario: (1) detach database from SQL Server 6.5/7.0/200 version; (2) attach it to SQL Server 2005; (3) run this script; (4) use your database asap.

    Some words about performance: than more tables with computed columns (computed column not at the end of the table) you have than more time you will have to wait. 100 Gigabyte db can be easily transformed within one hour on AI64 4way with EVA8000.

    Also it is possible to use this script to study SQL 2005 system views and relationship between them.

    Many thanks for any reply and comments.

     ---- January 2008

    Many thanks to WaitForPete, script updated to fix these (and some other) issues.


© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker