Microsoft SQL Server Specific Information
-
Currently, the SQL Server proxy only supports SQL Server logins. If the proxy is running on Windows, it is possible to configure internal authentication on the SQL Server JDBC driver. Please contact support for more details if this is necessary.
-
When using the Microsoft Azure Database, the setCatalog() function is not usable to change the catalog on a connection. To support an application selecting the database on the initial connection, use the string ${database} in the JDBC URL to set the database, i.e. ";databaseName=${database}" at the end of the url. The value ${database} will be extracted from the client login to populate this value.
-
When performing explain extraction, the option checkTableMismatch can be used (any value) to trigger testing and printing if a the tables extracted from the explain match what is used internally by Heimdall. This can be used to track down table extraction issues.
-
If using Mirroring (not Always-On) for redundancy, transactions will fail. This can apply if using SQL Server in AWS for versions prior to 2016, or older instances spun up before May 2019 with failover. Please use Always-On, as transaction handling with mirroring is not easily supported.
-
Applications that depend on session recovery are likely to not work properly, in particular if session data is used as part of record/table locking mechanisms, such as the SP "applock_mode" with the lock owner of "session". One example of a library that uses this is "Hangfire".
-
Applications that require MARS (multiple active result sets) may not work properly. The proxy will actively negotiate this option to be off with clients, which normally is sufficient to allow the applications to work. In some rare cases however, code needs to be modified to read one full result-set into memory before opening a new result-set.
Schema Assist for Dependencies
When using SQL Server, parsing is performed to detect view dependencies along with fixed (not dynamic) dependencies in stored procedures, in order to automate invalidation. This is only performed at proxy start at this time.
Trigger Invalidation with SQL Server
Connection properties:
- dbTriggerTracking=true
- dbChanges=select ts,name from heimdall.table_change where lastupdate > @ts
- dbTimeMs=select heimdall.get_time_ms()
CREATE DATABASE [heimdall]
GO
/*** create the trigger tracking table ***/
USE [heimdall]
CREATE TABLE [dbo].[table_change](
[ts] [bigint] NULL,
[name] [varchar](64) NOT NULL,
CONSTRAINT [PK_table_change] PRIMARY KEY ([name]));
CREATE FUNCTION [dbo].[get_time_ms]() RETURNS BIGINT
AS
BEGIN
DECLARE @timems BIGINT
set @timems= (SELECT cast(DATEDIFF(s, '1970-01-01 00:00:00', GETUTCDATE()) as BIGINT)*1000+cast(DATEDIFF(ms,CONVERT(VARCHAR,GETUTCDATE(),120),GETUTCDATE()) as BIGINT))
return @timems
END
/*** Create the tracking procedure, which will be used by the triggers ***/
CREATE OR ALTER PROCEDURE [heimdall].[dbo].[track_changes]
@tablename NVARCHAR(128)
as
SET NOCOUNT OFF
UPDATE [heimdall].[dbo].[table_change] SET ts=[heimdall].[dbo].get_time_ms() WHERE name = @tablename
IF @@ROWCOUNT != 0
RETURN
INSERT INTO [heimdall].[dbo].[table_change] VALUES ( dbo.get_time_ms(), @tablename )
GO
/*** trigger to apply to each table that needs trigger based eviction ***/
/**** for table vevo.dbo.Product ***/
USE [vevo2]; CREATE OR ALTER TRIGGER [dbo].[Product_changed]
ON [vevo2].[dbo].[Product]
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
exec [heimdall].[dbo].[track_changes] 'vevo2.dbo.Product'
END
GO
Active Directory Password & Group Synchronization
CREATE OR ALTER PROCEDURE heimdall.sync_user
(
@username nvarchar(max),
@password nvarchar(max),
@ldapgroups nvarchar(max),
@createRolesIfMissing BIT = 1 -- Optional parameter, if set to 1 roles are created and membership is added to the user
)
AS
BEGIN
--Logic variables
DECLARE @sql nvarchar(max);
DECLARE @roleName nvarchar(max);
DECLARE @currentRow INT = 1;
DECLARE @rowCount INT;
CREATE TABLE #groupList (id INT IDENTITY(1,1), g varchar(max)); --table variable can't be used in dynamic SQL
CREATE TABLE #groupsToRevoke (id INT IDENTITY(1,1), g varchar(max));
CREATE TABLE #oneToManyMappings (id INT IDENTITY(1,1), g varchar(max));
--Configurable variables
DECLARE @currentSchemaName varchar(max) = 'heimdall';
DECLARE @lookupTableName varchar(max) = 'lookup_table';
DECLARE @lookupTableKey varchar(max) = 'ldap_group';
DECLARE @lookupTableValue varchar(max) = 'role_name';
-- Put ldapgroups to table
INSERT INTO #groupList (g)
SELECT LTRIM(RTRIM(value))
FROM STRING_SPLIT(@ldapgroups, ',');
-- Check if lookup_table exists
IF (EXISTS (SELECT *
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = @currentSchemaName
AND TABLE_NAME = @lookupTableName))
BEGIN
-- One to many mappings of ldap_groups
SET @sql = CONCAT(
'INSERT INTO #oneToManyMappings (g)
SELECT lt.', QUOTENAME(@lookupTableKey),
' FROM ', QUOTENAME(@lookupTableName), ' lt
INNER JOIN #groupList gl ON lt.', QUOTENAME(@lookupTableKey), ' = gl.g
GROUP BY lt.', QUOTENAME(@lookupTableKey),
' HAVING COUNT(1) > 1;');
EXEC sp_executesql @sql;
PRINT @sql;
-- Delete groups which has one to many mapping
SET @sql = 'DELETE t1 FROM #groupList t1 JOIN #oneToManyMappings t2 ON t1.g = t2.g'
EXEC sp_executesql @sql;
PRINT @sql;
-- Insert all one to many mappings (corresponding roles)
SET @sql = CONCAT(
'INSERT INTO #groupList
SELECT ', QUOTENAME(@lookupTableValue),
' FROM ', QUOTENAME(@lookupTableName),
' lt INNER JOIN #oneToManyMappings otmm ON lt.', QUOTENAME(@lookupTableKey), ' = otmm.g;');
EXEC sp_executesql @sql;
PRINT @sql
-- Replace ldap groups with it's corresponding role names from lookup_table
SET @sql = CONCAT(
'UPDATE gl SET gl.g = lt.', QUOTENAME(@lookupTableValue),
' FROM #groupList gl JOIN ',
QUOTENAME(@currentSchemaName), '.', QUOTENAME(@lookupTableName), ' lt ON gl.g = lt.', QUOTENAME(@lookupTableKey));
EXEC sp_executesql @sql;
PRINT @sql;
END
-- Create login if not exists, alter otherwise
IF NOT EXISTS
(SELECT name
FROM master.sys.server_principals
WHERE name = @username)
BEGIN
SET @sql = CONCAT('CREATE LOGIN ', QUOTENAME(@username), ' WITH PASSWORD = ', QUOTENAME(@password, ''''));
EXEC sp_executesql @sql;
PRINT @sql;
END
ELSE
BEGIN
SET @sql = CONCAT('ALTER LOGIN ', QUOTENAME(@username), ' WITH PASSWORD = ', QUOTENAME(@password, ''''));
EXEC sp_executesql @sql;
PRINT @sql;
END
-- Create user if not exists, alter otherwise
IF NOT EXISTS
(SELECT 1
FROM sys.sysusers
WHERE name = @username)
BEGIN
SET @sql = CONCAT('CREATE USER ', QUOTENAME(@username), ' FOR LOGIN ', QUOTENAME(@username));
EXEC sp_executesql @sql;
PRINT @sql;
END
ELSE
BEGIN
SET @sql = CONCAT('ALTER USER ', QUOTENAME(@username), ' WITH LOGIN = ', QUOTENAME(@username));
EXEC sp_executesql @sql;
PRINT @sql;
END
-- Get roles which user is not a member of anymore
INSERT INTO #groupsToRevoke (g)
(
SELECT r.name
FROM sys.database_role_members drm
JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id
JOIN sys.database_principals u ON drm.member_principal_id = u.principal_id
WHERE u.name = @username
EXCEPT
SELECT g
FROM #groupList
);
SET @rowCount = (SELECT COUNT(*) FROM #groupsToRevoke);
-- Drop membership of roles which user is not a member of anymore
WHILE @currentRow <= @rowCount
BEGIN
SET @roleName = (SELECT g FROM #groupsToRevoke WHERE id = @currentRow);
SET @sql = CONCAT('ALTER ROLE ', QUOTENAME(@roleName), ' DROP MEMBER ', QUOTENAME(@username));
EXEC sp_executesql @sql;
PRINT @sql;
SET @currentRow += 1;
END
SET @currentRow = 1;
SET @rowCount = (SELECT MAX(id) FROM #groupList)
-- Iterate over groupList and create roles
WHILE @currentRow <= @rowCount
BEGIN
SET @roleName = (SELECT g FROM #groupList WHERE id = @currentRow);
IF @roleName IS NOT NULL AND @roleName <> '' AND @createRolesIfMissing = 1 -- If 'roleName' is empty then no role is created and granted to user
BEGIN
-- Create role if not exists
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = @roleName AND type = 'R')
BEGIN
SET @sql = CONCAT('CREATE ROLE ', QUOTENAME(@roleName));
EXEC sp_executesql @sql;
PRINT @sql;
END
-- Grant role to the user
SET @sql = CONCAT('ALTER ROLE ', QUOTENAME(@roleName), ' ADD MEMBER ', QUOTENAME(@username));
EXEC sp_executesql @sql; -- Won't throw an exception if user is already a member of the given role
PRINT @sql;
END
SET @currentRow += 1;
END
-- Make sure we drop these to prevent reuse issues on the same connection
DROP TABLE #groupList;
DROP TABLE #groupsToRevoke;
END
Active Directory Password & Group Synchronization extra options
By default, a role with the same name is created in the database for each extracted ldap group. By creating the table below for each key found (ldap_group), a role with the specified name in the value (role) will be created instead. Composite Primary Key allows to map more than one role to particular ldap_group; If 'role' is empty then no role is created and granted to user for this particular ldap_group.
CREATE TABLE dbo.lookup_table (
ldap_group varchar(255),
role_name varchar(64),
PRIMARY KEY (ldap_group, role_name)
)
Amazon RDS for SQLServer
Adjustments to the 'master' database are presently limited within Amazon RDS for SQL Server. When the 'master' database is configured in the datasource or left unspecified, certain functionalities, such as LDAP synchronization, may not work properly. To resolve this issue, clients are recommended to specify the default database in the Datasource tab of the JDBC URL. A representative JDBC URL format is demonstrated as follows: "jdbc:sqlserver://{server_name}:1433;databaseName=${database}", then i properties define defaultCatalog with created database name, in our case [testDB].
If we would to set up a proxy with a new user on Amazon RDS for SQLServer, we can use the following example: Connect with admin user to database master. Then execute queries similar to those:
CREATE DATABASE [testDB];
USE [testDB];
CREATE LOGIN [test_user] WITH PASSWORD = 'test_user', DEFAULT_DATABASE=[testDB];
CREATE USER [test_user] FOR LOGIN [test_user];
CREATE SCHEMA heimdall;
GRANT ALTER, CONTROL, EXECUTE, SELECT, INSERT, UPDATE, DELETE
TO [test_user];
GRANT CREATE TABLE TO [test_user];
GRANT CREATE PROCEDURE TO [test_user];
While using ldap in VDB the command for syncing user will look like this:
EXECUTE testDB.heimdall.sync_user N'${user}', N'${password}', N'${ldapgroups}'
For using load-balancing features we will need to run:
GRANT VIEW DATABASE STATE TO [test_user];
GRANT VIEW SERVER STATE TO [test_user]; -- has to be run on master database
Database name, and username should be adjusted to the clients need. At the end, created user should be placed in Datasource tab, and JDBC URL should point into defined database.