Blog ala Vidar

SQL, AppFrame and other cool technologies

Monthly Archives: July 2007

Omega’s security conecpt

Wouldn’t it be a great world if everyone had access to everything, but only used the things they should be using? Well, unfortunately humans do things they are not supposed to do. Either it’s because of curiosity, or stupidity. Because of this we have a security concept in Omega built on both SQL server security and our own AppFrame Security. Today I’m going to go into details about how this works.

We separate the security into two sections, SQL and AppFrame. First, let’s have a look at how SQL security is built up.

SQL Security

Login – The account you are using when authenticating to the server
User – The account in the database which is linked up to your login, to specify which databases you’ve got access to.
Roles – A database “group” which is used for adding permissions. Instead of adding permissions to ALL (maybe a couple of thousand) users, you add them to a role and add permissions to this role.

We got one role, called af_user. All users should be a member of this role. This role got “SELECT *” permissions to all views, EXECUTE permissions on stored procedures and functions. When it comes to tables, we have a special concept here. We give af_user only access to the PrimKey and Timestamp fields on the tables. This is because PrimKey is the PRIMARY KEY on the table. Timestamp is the row-version field, so the application knows if someone else has updated the row since you last picked it up. af_user also got full INSERT, UPDATE and DELETE permissions to the tables. This is actually why we are using views (tbv, tbx). The user got full access to the view. When he’s trying to update data from the view, it will update the table. When access tries to update (or delete) a record, it uses a property called “unique table”. Here you specify which table should be updated. So, access actually uses the table, and not the view, when running updates (delete, insert etc). Therefore, the user needs access to SELECT PrimKey and Timestamp in the table.

Example:

Recordsource: SELECT * FROM stbv_System_Groups

You try to change the Description of the Administrators group in the Omega Domain. Access will then run something like this query to the server:

UPDATE stbl_System_Groups
    SET Description='Immortal users'
    WHERE PrimKey='C35F02E6-BA01-4DFD-A697-D4A2A6B8744A'
      AND Timestamp='0x000000000BB2BAC5'

Notice it is using stbl, and not stbv, even though you’re using stbv in the recordsource. This is because access reads the view and sets the unique table property itself if it’s not set by the developer.

AppFrame Security

In AppFrame we got both MasterGroups and Groups. The difference between these is that Groups are domain dependant. MasterGroup is not. So, what is a domain? Well, it’s used to separate data into areas where they belong. For example, in Statoil they use one domain for each project. Snohvit got one domain, OrmenLange got one domain, KEP2010 got one etc. This is because the people working with Snohvit doesn’t care about what’s happening at Langeled. So, therefore we got Groups which are domain dependant. But, we also got (Master) Groups which are not domain dependant. Typical groups like Administrators, CMS RO and CMS RW. Master Groups is like a template for Groups. Take “Administrators” as an example. We add all permissions (I’ll come back to this subject) we need as administrator (mainly SUID on all tables), and then assign it to domains. What actually happens when you assign a Master Group to domains is that you make a new group in that domain, based on the Master Groups permissions. If a user is member of the Master Group, he will automatically be a member of the Groups which “belongs” to the Master Group. If you want one user to not be an Administrator in all domains, only Snohvit, you add him to the Group in the Snohvit domain, and not in the Master Group. Smart? Well, be careful! If you delete a member of the Master Group, you will also delete that member from the Groups which has this Master Groups as a parent.

You might have noticed, we use both tbv and tbx as table views.

Tbv’s are used to get data from a table in your current domain.
Tbx’ are used to get data from a table in ALL domains you’ve got access to.

Table Rows Permissions

In each Group we got a set of Table Rows Permissions. This table contains the following fields:

Domain – to specify which domain this permission set belongs to.
GroupRef – The PrimKey for the group this permission set belongs to.
TableID – The table name this permission set belongs to.
PGrant – Which permissions the group got on this table. S for SELECT, U for UPDATE, I for INSERT and D for DELETE. Min: S, max: SUID
Criteria1, Criteria2, Criteria1Revoke, Criteria2Revoke – I’ll get back to these later.
MasterTRRef – Defines if this record has a Master Table Row. If it does, this group is controlled by a Master Group.

All our tbv’s should look something like this:

SELECT *
	FROM dbo.stbl_System_Groups (NOLOCK)
	WHERE EXISTS (SELECT *
			FROM sviw_System_MyPermissionsCurrentDomain (NOLOCK)
			WHERE ISNULL(Domain, '') = stbl_System_Groups.Domain
			  AND TableID = 'stbl_System_Groups')

sviw_System_MyPermissions_CurrentDomain will return ALL your permissions in your current domain. Your current domain is saved in the field CurrentDomain in stbl_System_Users. This query will look for records with TableID = ‘stbl_System_Groups’ and make sure it only return records where stbl_System_Groups.Domain is like your current domain. This is how the SELECT works.

Triggers

Now, let’s move on to UPDATE, INSERT and DELETE. These actions are handled by triggers. stbl_System_Groups_ITrig is the INSERT trigger for this table. In this trigger you’ll find:

IF @@RowCount = 0 RETURN
IF IS_MEMBER('db_owner') = 1 GOTO ContinueTransaction

IF EXISTS (SELECT MyP.*
		FROM dbo.sviw_System_MyPermissions MyP
		JOIN Inserted
			ON ISNULL(MyP.Domain, '') = ISNULL(Inserted.Domain, '')
		WHERE MyP.TableID='stbl_System_Groups'
		  AND MyP.PGrant LIKE '%I%') GOTO ContinueTransaction

RollbackTransaction:
    RAISERROR ('---- No insert permissions on table: stbl_System_Groups ----',18,1) ROLLBACK TRANSACTION
    RETURN

ContinueTransaction:
    SET NOCOUNT ON

What happens here is that it first checks if there’s any records “thrown” at the trigger. If not, it quits the trigger. Then it checks if you are a member of the builtin db_owner database role. If you are, you should have access to the table, so no need in checking AppFrame permissions. If you are not a member of this role, it will continue, to check if you got records with INSERT permissions on this table. If you do, you continue to ContinueTransaction, if not, you get an error saying “—- No insert permissions on table: stbl_System_Groups —-“. 18 is the severity level of this message. 18 is the highest normal (mortal) users are allowed to use. This will cause an MsgBox to show in access. If this message is raised, the trigger will rollback all changes.

DBTables

I said I would come back to Criteria1, Criteria2 etc later. Now it is “later”. Till now I’ve only told you about table level security. Now, let’s discuss ROW level security. As a part of AppFrame Security, you’ve got the privilege of using row level security. This is actually one of the main reasons we’re not only using SQL security. MSSQL are not supporting row level security. They said they would in 2005, then they said they would in 2008. Still no support for it though! In other words, we got something more fancy than Microsoft 🙂 Anyhow. This means that you can specify WHICH rows you want a user to be able to see, add, update or remove by the use of a filter. This is actually what Criteria1 and Criteria2 are used for. Here you specify the filter. Which fields to filter on is desided in stbl_System_DBTables. This table contains the following fields (I’ve removed the fields you do not need to worry about):

  • ObjectName – The table name
  • CField1 – The first field to have a filter on if using row level security. This uses LIKE
  • CField2 – The second field to have a filter on if using row level security. This uses LIKE
  • CField1Revoke – (bit) Set this to 1 if you’d like CField1 to use “NOT LIKE” instead of “LIKE”.
  • CField2Revoke – (bit) Set this to 1 if you’d like CField2 to use “NOT LIKE” instead of “LIKE”.
  • PredecessorTable – If you have parent-child tables, your child table may have its parent table in this field. This way the child table will inherit the parent’s permissions.
  • NoDataLog – (bit) Skip INSERT to stbl_System_DataLog in the triggers
  • ExtendedDataLogging – (bit) If this is set, the trigger will log every change to all fields in stbl_System_DataLog
  • StandardSecurity – (bit) This table has tbv (and tbx) and standard triggers.
  • BypassSecurityForDBO – (bit) This adds the following line to the triggers: IF IS_MEMBER(‘db_owner’) = 1 GOTO ContinueTransaction
  • DenyafUserUpdatePermissions – (bit) This makes ApplyPermissions REMOVE UPDATE, INSERT and DELETE permissions from af_user on the table. Do NOT use this unless you’re only using SPs on this table!
  • GrantAccessForClusteredIndex – (bit) This makes ApplyPermissions also add SELECT permissions on all fields that are in the clustered index on the table, in addition to PrimKey and Timestamp. The reason is that when you use Access on SQL 2005, it will use the clustered index, instead of the primary key, for SELECT when updating.

To change the settings in DBTables, open Query Analyzer, rightclick the table you want to change and choose “Security (Views and Triggers)”

Tuning SQL code

Have you ever though of tuning your car? Yeah, why not?
It makes your car run faster and use less fuel. Well, for starters it’s a bit expensive. But, tuning SQL is free, and it’s even more fun than tuning your car. Tuning your SQL means your queries run faster and use less fuel (CPU, memory etc)!

I’m at Conocophillips Indonesia at the moment upgrading to SQL Server 2005. I’ve used a couple of days to tune different queries, and at the most I got a query to run from an average of 4 minutes, to about 10 seconds, with basically the same code! Do NOT tell me that this was a temporary solution, because my solution was even easier! 🙂

When writing SQL code, keep these simple rules in mind:
Do not use sub-queries unless it’s ABSOLUTELY necessary
If you are setting several variables from the values of fields in tables/views, use one SELECT statement instead of three SET statements.

For example, do not use:

SET @Variable1 = 'Test'
SET @Variable2 = (SELECT Test FROM Test1 WHERE Test3='asdf')
SET @Variable3 = (SELECT Test2 FROM Test1 WHERE Test3='asdf')

Use:

SELECT TOP 1
  @Variable1 = 'Test',
  @Variable2 = Test,
  @Variable3 = Test2
    FROM Test1
    WHERE Test3 = 'asdf'

You can also use SELECT instead of SET to set static values
If possible, do not use CASE or UNION (ALL)
Use EXISTS, instead of 0 < (SELECT COUNT(*) something)
Use TOP 1. Always remember that a sub-query MAY return more than one record. Your query might then fail if you’re using Field = (SELECT SubField FROM subtable).

These rules are things that every developer should keep in mind when writing SQL code.

Of course, there might be places where you’ve followed every rule, but it still takes minutes to run. Then you might want to see the execution-plan. There are a couple of major things too look for here. For example, Scans (Index Scan, Clustered Index Scan, Table Scan etc) is NOT good. Seeks are a good thing.

You might also want to see what’s really happening when you’re running your query. To turn on execution-plan in Server Management Studio, press ctrl + M and rerun your query. It will appear as a new tab beside Results and Messages.

And, as always; Profiler might be a good tool. If you let it run for a couple of hours you will discover queries which take longer time than others. For example, you can set a filter on the Reads field, for >= 50000.

Do you have any other simple roules you’d like to share with us? Please mail them to me, and I’ll publish them here!