CodeSmith Community
Your Code. Your Way. Faster!

Row Level Security and features added recently.

Latest post 11-21-2007 2:48 AM by Lachlan McIntosh. 8 replies.
  • 03-16-2007 1:29 AM

    Row Level Security and features added recently.

    Hi,

    First of all I would really like to say thanks to everybody who work on this stuff. It's been a couple of months since I was last here. I have been busy developing an application using the 2.0 nettiers release.  We have released that application and it is getting fantastic feedback from our users, so I am thanking you for enabling us to create such a great application so quickly. Thanks a heap.

    Now the time has come to regenerate a new library, we've had schema changes etc.  I downloaded the latest release, 2.0.1 and see you guys have been very busy.  The first thing I saw you added was something to the entities using the System.ComponentModel.ISite.  This feature caused issues with the fact that our database has a table named Site also.  I ran in to this problem with the 2.0 version because our database also has a table named TimeZone and that also caused conflicts with the System type of the same name.  I got around that problem by using the Alias file and renaming it to TimeZones. A hack yes, but it did the trick and not a huge issue.  Site however, is more of an issue because we now have applications built and to update them would be a pain. Solution, I had to update the EntityInstanceBase.generated.cst file and the ListBase.cst file to fully qualify these properties.  Is that the correct/best way to solve that issue or is there something else I can do to solve naming conflicts when they arise in the future?

    I am not sure if anyone is interested, but I also add a few customisations to your code each time I download it that enables me to add some extra security to our database.  We use the .NET 2.0 SQL Authentication and Role Providers in our ASP.NET app (although we have customised that a little also) and this enables me to pass the identity of the user making the call off the thread.  I added some template code to the xsl that generates the SQL to perform a standard check to ensure that the person making the call to the stored procedure is actually authorised to be running that stored procedure.  I also have some additional checks that added extra where clauses to statements to ensure they have permission to update, select, delete the specific row(s).  It works very well, but I have not manged to fully add this as a feature to the templates that you could turn on or off.  I am getting closer with each tweak I make, but I think using the new mapping file feature adds some scope for further improvement to how I was going to do it.  I'd be happy to share what I have with people if anyone else thinks they would benefit from this. We find it useful because it now means that by using a standard authentication provider, we are able to secure our database at the stored procedure level, ensuring that no matter what client is accessing it, if an applicaiton developer introduced a bug, the database should stop unauthorised access to data.  I also managed to do a proof of concept when I first did this that used a security token service to authenticate the user and enabled this to work using the web services also.  We have not been using the web services as we currently do not have clients that require them, but it will work when needed.

    I also had to add some code to handle rowguid columns.  Our database is replicated and these columns were not allowed to be updated.  I am not sure what anyone else was doing to get around this problem, but I can also share that code if anyone is interested and is in fact very simple.

    Regards,
    Mark

    • Post Points: 65
  • 03-16-2007 1:53 PM In reply to

    • Alex
    • Top 10 Contributor
    • Joined on 07-26-2005
    • Australia, Canberra
    • Posts 526
    • Points 10,645

    Re: Row Level Security and features added recently.

    Hi, Mark!

    That would be great to look at the code and small docs about how it can be used...

    Best regards,
    Alex.
    • Post Points: 35
  • 03-20-2007 2:41 AM In reply to

    Re: Row Level Security and features added recently.

    Here is a brief document I put together that explains the concept of what I have done.  When I have some more documentation that doesn't have any sensitive information included I will try and upload it somewhere.  Sorry for some of the formatting but cut and paste didn't copy the formatting.

    Background

    When trying to secure data in web applications, there are several methods that can be used to secure the data at the database level.  These methods however require you to use some form of authentication with the database that restricts your design decisions and also limit your scalability of the application. 

    Requirements

    1. Authentication must be heterogeneous to enable authentication of clients developed using technologies other than Microsoft .NET.
    2. Authentication must be available to partners and clients that are not included in the Active Directory.
    3. Authentication must be scalable to potentially hundreds of thousands of users.
    4. Authentication should ideally be consistent across all of the applications in the suite of products provided by Worldsmart Technology.
    5. Management of users is to be managed through the Merchant Access web application.
    6. Users with applicable roles must be able to create new users in the application and be able assign applicable roles to those users.
    7. Users with applicable roles must be able to view and edit only users that are associated with their own or related Merchants.
    8. System Administrators must be able to view and edit all users.
    9. System Administrators must be able to add/remove roles from the application.
    10. System Administrators must be able to add/remove the roles that can be granted by roles with the ability to add/edit users.
    11. Authentication is required to support database replication scenarios.

    Overview

    Authentication is provided by a solution based on the SQL Authentication Providers shipped as part of the Microsoft .NET 2.0 Framework.  The provider has been extended where necessary to provide the functionality required to satisfy the requirements.

    CodeSmith and .NetTiers templates are used to create and maintain the data access layers of the application and have been modified where necessary to satisfy the requirements.

    A User is assigned to a Merchant, and Merchants are stored in a hierarchical structure in the database.  A User has the ability to view information related to their Merchant and any Merchants below them in the hierarchy.  The user is restricted in the functions they can perform by the role to which they have been granted.

    A System Administrator is able to modify the stored procedures to which each user has access.

    Solution

    Using the SQL Authentication Provider, the schema was extended as shown in Diagram 1.  Custom tables were added with care taken not to modify existing schema.  Customised tables were added with the prefix aspnet_cust_ to differentiate these tables from the tables used by the provider.

    aspnet_cust_RoleProcedures - This table is used to assign a role with permission to execute a stored procedure.

    aspnet_cust_RoleGrantPermissions – This table is used to defined the list of roles that another role has permission to grant (e.g. Merchant Administrators have permission to grant Merchant User role and, Licensor Administrator has permission to grant Licensor Administrator, Licensor User, Group Administrator, Group User, Merchant Administrator and Merchant User roles).

    aspnet_cust_UsersMerchants – This table is used to assign the user to a Merchant.

    Authentication Schema

    Diagram 1: Authentication Schema

     

    Stored Procedures are secured using the following methods:

    ALTER PROCEDURE [dbo].[wst_Merchant_Get_List]

    (

         @SecurityUsername VARCHAR(50)

    )

    AS

     

         DECLARE @SecurityIdentityGuid UNIQUEIDENTIFIER

         SET @SecurityIdentityGuid = dbo.SecurityGetIdentityGuidByUsername(@SecurityUsername);

        

         -- Verify the identity has permission to perform task on the specified object

        IF NOT EXISTS (

               SELECT

                    RoleId

               FROM

                    [dbo].[fnSecurityGetIdentityObjectRolesForProcedure](@SecurityIdentityGuid, 'dbo.wst_Merchant_Get_List'))

         BEGIN

     

               RAISERROR('identity is not a member of a valid role', 12 ,1)

               RETURN -1

     

         END

         ELSE

         BEGIN

    The first couple of lines get the GUID for the username that is running the stored procedure.  The next section calls a function that gets the role to which the user is assigned and that have been granted permission to execute the stored procedure.  If there are no records for this the user does not have the permission required and an error is raised.  Otherwise the user has permission and the stored procedure is able to execute as normal.  This section is simply added to the scriptsql.xsl file in the templates

     

    The second part of the customisations made to the stored procedures created by the .NetTiers application is the part that performs the row level filtering.

     

    SELECT

          [MerchantId],

          [MerchantName],

          [MerchantTypeId],

          [RelationMerchantId],

          [GroupMerchantId],

                :

                :

    FROM

          dbo.[Merchant]

    WHERE

          [MerchantId] IN

                (SELECT

                      sec.MerchantId

                FROM

                      [dbo].[fnSecurityGetUserMerchants] (@SecurityIdentityGuid) sec)

    Select @@ROWCOUNT

     

    The templates use columns names identified as requiring security filtering and adds the highlighted code to the WHERE clause.  This function uses a hierarchical query to determine the MerchantIds that the specified user has permissions to.  This functionality is added by modifying the StoredProcedureXml.cst file:

               // [ab 012605] nonKeys/cols sans computed/read-only columns. This is for Insert/Update operations

               ColumnSchemaCollection colsUpdatable = new ColumnSchemaCollection(cols.Count);

               ColumnSchemaCollection nonKeysUpdatable = new ColumnSchemaCollection(nonKeys.Count);

               // markd - Added to define the collection of column names that we want to secure through filtering

               ColumnSchemaCollection colsSecured = new ColumnSchemaCollection(cols.Count);

     

               // Holds the RowVersion column is there's any

               ColumnSchema RowVersion = null;

              

               foreach (ColumnSchema column in cols)

               {

                    // markd - Added IsRowGuidColumn check to ensure that rowguid columns are not marked as updateable

                    //         as this causes errors at runtime from SQL Server

                    if ( ! IsIdentityColumn(column) && ! IsComputed(column) && ! IsRowGuidColumn(column) )

                         colsUpdatable.Add(column);

                        

                    if (column.NativeType.ToLower() == "timestamp")

                         RowVersion = column;

                   

                    // markd - Added to populate the columns in this table that match the criteria for requiring filtering

                    if (column.Name.ToLower() == "merchantid"

                         || column.Name.ToLower() == "siteid"

                         || column.Name.ToLower() == "locationid"

                         || column.Name.ToLower() == "productlocationid"

                         || column.Name.ToLower() == "terminalid")

                         colsSecured.Add(column);

               }

     

    The above code contains several modifications to this section.  First added a collection to hold the columns found in the table that require securing. Second added an if statement that looks at the list column name and compares it to the ones I have identified that need securing and if a match adds the column to this collection.

    The third is a bug fix and not directly related to the security side of things, it now checks the rowguid property when determining if a column is updateable or not.

     

    Then you need to modify the sections where the stored procedure types are generated.  Here is an example of modification made to the GetList stored procedure section:

                // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

                // GetList

                if (IncludeGetList)

                {%>

               

                <procedure owner="<%=SourceTable.Owner%>" name="<%= ProcedurePrefix + commandStem + SelectSuffix + SelectAllSuffix %>" <%if (GrantUser != "") {%>grant="<%=GrantUser%>"<%}%>>

                      <comment><![CDATA[<%= GetProcSqlComment(string.Format("Gets all records from the {0} table", name), tableComment) %>]]></comment>

                      <parameters/>

                      <body><![CDATA[

                            <%= GetSetTransactionIsolationLevelStatement() %>

                            SELECT

                                  <% for (int i = 0; i < cols.Count; i++) { %>

                                  [<%= colsIdea.Name %>]<% if (i < cols.Count - 1) { %>,<% } %>

                                  <% } %>

                            FROM

                                  <%=owner%>[<%= SourceTable.Name %>]

                            <% if (colsSecured.Count > 0) { %>

                            WHERE

                            <% for(int index=0; index < colsSecured.Count; index++) { %>

                                  <% if (index > 0) { %>AND <% } %><% = colsSecured[index].Name %> IN

                                     (SELECT

                                          sec.<% = colsSecured[index].Name %>

                                      FROM

                                          [dbo].[fnSecurityGet<% = colsSecured[index].Name %>] (@SecurityIdentityGuid) sec)

                            <% } %>

                            <% } %>

                            Select @@ROWCOUNT

                      ]]></body>

                </procedure>

     

    As you can see I simply added some logic that checks if the collection of secured fields has any members and creates the necessary where clause that calls the function that has been designed to return the list of id’s applicable to that user.

     

    The last part of the puzzle is how you pass the username to the stored procedure without making it a parameter that could simply be set by someone using the library.

     

    By modifying the StoreProcedureProvider.cst file I add the parameter to the stored procedure automatically when the GetCommandWrapper is requested.  When the Command Wrapper is requested the @SecurityUsername is added and the value is populated with the username on the thread.

    NOTE: I have not tested this section fully yet as I am still having issues with the tiers and some of the changes in our schema.  In my previous iteration I added this in each section of the SqlEntityProviderBase.Generated.cst file and this new method is much neater providing it works.

     

               /// <summary>

               /// Get the command wrapper, either from the stored procedures or from the embedded queries.

               /// </summary>

               /// <param name="database">The database instance.</param>

               /// <param name="commandName">the name of the sp.</param>

               /// <param name="useStoredProcedure">Indicates if we want stored procedure.</param>

               /// <returns></returns>

               public static DbCommand GetCommandWrapper(Database database, string commandName, bool useStoredProcedure)

               {

                    if (useStoredProcedure)

                    {

                         DbCommand command = database.GetStoredProcCommand(commandName);

                    command.CommandTimeout = DataRepository.Provider.DefaultCommandTimeout;

                         database.AddInParameter(command, "@SecurityUsername", DbType.String, Thread.CurrentPrincipal.Identity.Name)

                    return command;

                    }

                    else

                    {

                    DbCommand command =

                        database.GetSqlStringCommand(StoredProcedureProvider.GetProcedureBodyFromEmbeddedResource(commandName));

                    command.CommandTimeout = DataRepository.Provider.DefaultCommandTimeout;

                    return command;

                }

               }

    Web Services

    I have tested this by building a Security Token Service that is able to issue SAML tokens providing a solution for heterogeneous clients and meaning the Web Service layer is able to attach the users identity to the thread and therefore the users identity is able to be tracked and verified through the applications layers.

    References

    http://www.microsoft.com/technet/prodtechnol/sql/2005/multisec.mspx

    http://www.gotdotnet.com/codegallery/codegallery.aspx?id=8da852b9-2c0d-4eb7-a2de-77222a4075f6

    http://nettiers.com

    http://codesmithtools.com

     

    • Post Points: 5
  • 03-20-2007 6:16 AM In reply to

    • Lex
    • Top 50 Contributor
    • Joined on 09-28-2006
    • Posts 72
    • Points 1,945

    Re: Row Level Security and features added recently.

    Hi,

     This looks very interesting. Would you accept to share your code ? If yes, pelase send me an email.

    Bad luck for me, I am currently working on the same stuff ... but so far it is still not as succesfull as your implementation.

    • Post Points: 35
  • 03-20-2007 6:03 PM In reply to

    Re: Row Level Security and features added recently.

    Happy to share the code. I just have to first make sure any possibly confidential parts are removed.  That is going to take me a couple of days. But I hope that my previous posting is enough of the theory to get you all started. The functions that perform the security checks are pretty simple and the theory holds whether you have hierarchical structure or some other method of filtering the users data.  Those functions simply need to return a list of the keys to which the user does have access.  The stored procedure security is also very simply, as you can see from the asnet_cust_RoleProcedures, the table is populated with a RoleId and the name of the procedure.  If the user has been granted a role that has matching record for that procedure name they get execute access.

    As for the modifications to the templates, I am not sure where to post ideas for template changes as the only place to put code is in source control and as I said it is no where near ready to be considered ready for general use. But the changes are what I have documented above with the exception of change to the CommonSqlCode.cs so here is is:

      /// <summary>
      /// Check if a column is a rowguid column for replication
      /// </summary>
      /// <param name="column">DB table column to be checked</param>
      /// <returns>Identity?</returns>
      public bool IsRowGuidColumn(ColumnSchema column)
      {
       if (column.ExtendedProperties["CS_IsRowGuidCol"] != null)
        return (bool)column.ExtendedProperties["CS_IsRowGuidCol"].Value;
        
       return false;
      }

    And the XSL so here that is:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                    xmlns:sc="http://serialcoder.net"
                    extension-element-prefixes="msxsl sc"
                    xml:space="preserve"
        >

    <xsl:output method="text"/>

    <xsl:template match="/">
    Use [<xsl:value-of select="/root/database"/>]
    Go
    SET QUOTED_IDENTIFIER ON
    GO
    <xsl:apply-templates select="//procedures/procedure[not(@skip)]"/>
    </xsl:template>

    <xsl:template match="procedure">
     
    <xsl:if test="/root/database[@includeDrop='true']">
    -- Drop the <xsl:value-of select="@owner"/>.<xsl:value-of select="@name"/> procedure
    IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'<xsl:value-of select="@owner"/>.<xsl:value-of select="@name"/>') AND OBJECTPROPERTY(id, N'IsProcedure') = 1)
    DROP PROCEDURE <xsl:value-of select="@owner"/>.<xsl:value-of select="@name"/>
    GO
    </xsl:if>
    <xsl:value-of select="comment"/>

    CREATE PROCEDURE <xsl:value-of select="@owner"/>.<xsl:value-of select="@name"/>
    (
     @SecurityUsername VARCHAR(50)<xsl:if test="count(parameters/parameter)!=0">,
    <xsl:apply-templates select="parameters/parameter"/></xsl:if>
    )
    AS

     DECLARE @SecurityIdentityGuid UNIQUEIDENTIFIER
     SET @SecurityIdentityGuid = dbo.SecurityGetIdentityGuidByUsername(@SecurityUsername);
     
     -- Verify the identity has permission to perform task on the specified object
        IF NOT EXISTS (
      SELECT
       RoleId
      FROM
       dbo.[sec_GetIdentityObjectRolesForProcedure](@SecurityIdentityGuid, '<xsl:value-of select="@owner"/>.<xsl:value-of select="@name"/>'))
     BEGIN

      RAISERROR('identity is not a member of a valid role', 12 ,1)
      RETURN -1

     END
     ELSE
     BEGIN
      <xsl:if test="count(parameters/parameter[@name='@LastModifiedUserGuid'])!=0 and not(contains(@name, '_Find'))">
      -- Set the value of this parameter to the Guid of the user executing the procedure
      SET @LastModifiedUserGuid = @SecurityIdentityGuid;</xsl:if>
      <xsl:if test="count(parameters/parameter[@name='@LastModifiedDateTime'])!=0 and not(contains(@name, '_Find'))">
      -- Set the value of this parameter to the System Time (UTC)
      SET @LastModifiedDateTime = GETUTCDATE();</xsl:if>
      
    <xsl:value-of select="body" disable-output-escaping="yes"/>

     END
    <xsl:if test="@grant and string-length(@grant) &gt; 0">
    GO
    GRANT EXEC ON <xsl:value-of select="@owner"/>.<xsl:value-of select="@name"/> TO <xsl:value-of select="@grant"/>
    </xsl:if>
    GO
    SET QUOTED_IDENTIFIER ON
    GO
    </xsl:template>

    <xsl:template match="parameter"><xsl:value-of select="@name"/> <xsl:value-of select="@type"/> <xsl:value-of select="@param"/> <xsl:if test="@nulldefault = 'null'"> = null</xsl:if> <xsl:if test="@direction = 'Output'"> OUTPUT</xsl:if><xsl:if test="last()!=position()">,</xsl:if>
    </xsl:template>

    <msxsl:script implements-prefix="sc" language="Javascript">
    <![CDATA[
    function align(ctx,length){

     var str = '         ' + ctx;
     str = str.substr(str.length - length);
     return str;
     
    }

    function crlf()
    {
     return "\r\n";
    }

    function spaces(nb) {
     var str = '';
     for(var i=0; i<nb;i++) {
      str += ' ';
     }
     return str;
    }

    function FillRightSpace(ctx, width)
    {
     var str = spaces(width);
     str = ctx + str;
     
     return str.substr(0,width);
    }

    function DropCR(ctx)
    {
     var str = '' + ctx;
     str = str.Replace("\r","");
     return str;
    }

    ]]>
    </msxsl:script>

    </xsl:stylesheet>

    I hope this helps until I can work out where to post the code.  I can maybe put this on my website zipped up or something, but will work something out.

    • Post Points: 35
  • 03-22-2007 10:47 AM In reply to

    • Lex
    • Top 50 Contributor
    • Joined on 09-28-2006
    • Posts 72
    • Points 1,945

    Re: Row Level Security and features added recently.

    Thanks. The procedure code is very interesting.

    We are still doing our own implementation anyway, since there are many specificities in teh project, such as very complicated nested levels of hierarchies.

    • Post Points: 35
  • 03-22-2007 10:55 AM In reply to

    Re: Row Level Security and features added recently.

    I'm interested in seeing your solution, this looks great!

    On 3/22/07, Lex wrote:
    >
    >
    > Thanks. The procedure code is very interesting.
    >
    > We are still doing our own implementation anyway, since there are many
    > specificities in teh project, such as very complicated nested levels of
    > hierarchies.
    >
    >
    >

    Robert Hinojosa
    -------------------------------------
    Member of the Codesmith Tools, .netTiers, teams
    http://www.nettiers.com
    -------------------------------------
    • Post Points: 35
  • 03-23-2007 6:31 AM In reply to

    • Lex
    • Top 50 Contributor
    • Joined on 09-28-2006
    • Posts 72
    • Points 1,945

    Re: Row Level Security and features added recently.

    Hi Robert,

    Sure, I will release the code in april so that it can be added to NetTiers core.

    • Post Points: 35
  • 11-21-2007 2:48 AM In reply to

    Re: Row Level Security and features added recently.

    Lex:

    Sure, I will release the code in april so that it can be added to NetTiers core.

    Did this change ever make it in?

     

    Lachlan

    • Post Points: 5
Page 1 of 1 (9 items) | RSS
Copyright © 2008 CodeSmith Tools, LLC
Powered by Community Server (Commercial Edition), by Telligent Systems