Search code examples
sqlsessionpermissionscoldfusioncfloop

Best way to store permissions for the user account?


I have permission records that are tied to each account in my application. Each account can have one or multiple permission records based on account type. Here is example:

<cfquery name="qryUserPerm" datasource="#Application.dsn#">
    SELECT AccessType, AccessLevel, State, City, Building
    FROM Permissions
    WHERE AccountID = <cfqueryparam cfsqltype="cf_sql_integer" value="#trim(session.AccountID)#">
</cfquery>

Query above will produce data like this for one of the accounts:

RecID   AccountID   AccessType  AccessLevel State     City    Building
70      285A637D82B9    F            B        NY    New York    8010
71      285A637D82B9    F            B        NY    New York    5412
73      285A637D82B9    F            B        NY    New York    6103
74      285A637D82B9    F            B        NY    New York    3106

As you can see above this account have 4 records assigned to them. Access Type can be Full F or View Only V. Access Level can be State 'S', City 'C' or Building 'B'. User can have only one access level assigned to them at the time, so for example there is no situation where user can have assigned City and State level. My question is what would be the best way to organize the data from the query for specific access level? In this case I have to merge 4 records in list or array. State level only can have one permission record assigned, City and Building can have multiple records. Here is example of what I have:

<cfset local.permissionType = "">
<cfset local.permissionLevel = "">
<cfset local.permissionList = "">

<cfloop query="qryUserPerm">
    <cfif qryUserPerm.AccessLevel EQ "S">
         <cfset local.permissionType = qryUserPerm.AccessType>
         <cfset local.permissionLevel = qryUserPerm.AccessLevel>
         <cfset local.permissionList = listAppend(permissionList, "", ",")>
    <cfelseif qryUserPerm.AccessLevel EQ "C">
         <cfset local.permissionType = qryUserPerm.AccessType>
         <cfset local.permissionLevel = qryUserPerm.AccessLevel>
         <cfset local.permissionList = listAppend(permissionList, qryUserPerm.City, ",")>
    <cfelseif qryUserPerm.AccessLevel EQ "B">
         <cfset local.permissionType = qryUserPerm.AccessType>
         <cfset local.permissionLevel = qryUserPerm.AccessLevel>
         <cfset local.permissionList = listAppend(permissionList, qryUserPerm.Building, ",")>
    <cfelse>
         <cfset local.permissionType = "">
         <cfset local.permissionLevel = "">
         <cfset local.permissionList = listAppend(permissionList, "", ",")>
    </cfif>
</cfloop>

It seems redundant to keep permissionType and permissionLevel inside of the loop but I do not know better way currently to avoid that. Also this makes process very dificult in case when I have to compare permission list. I would have to run this same process and build the list in order to compare that with Session.premissionList in case where currently logged user change their permissions. Is there any way to merge these records with SQL? Or this approach is better option?


Solution

  • This can be done in SQL itself, which may be more performant than manipulating the data in code.

    One issue with the data is that the State, City and Building columns need to be unpivoted to then be turned into a comma-delimited list.

    Since you are using SQL 2008, you have access to the functionality you need.

    The query is: http://sqlfiddle.com/#!18/0f4f7/1

    ; WITH cte AS (
      SELECT
          AccountID, AccessType, AccessLevel
          , CASE AccessLevel
              WHEN 'S' THEN State
              WHEN 'C' THEN City
              WHEN 'B' THEN Building
            END AS Permissions
      FROM Permissions
      WHERE AccountID = 
        <cfqueryparam cfsqltype="cf_sql_integer" value="#session.AccountID#"> 
        /* Dynamic variable here */
    )
    SELECT DISTINCT AccountID, AccessType, AccessLevel
      , CASE 
          WHEN AccessLevel = 'S' THEN Permissions 
          ELSE LEFT(ca.pl, COALESCE(LEN(ca.pl),0)-1)
        END AS PermissionList
    FROM cte
    CROSS APPLY (
      SELECT p.Permissions + ', '
      FROM cte p
      WHERE p.AccountID = cte.AccountID
        AND p.AccessType = cte.AccessType
        AND p.AccessLevel = cte.AccessLevel
      FOR XML PATH('')
    ) ca (pl)     ;
    

    I start with a CTE to build out the "unpivoted" list of Permissions based on the AccessLevel. If this can be put in a SQL View, you can just leave out the WHERE statement here and just call it when you call the View. A View would be my preference, if you can get it into your database.

    After I have the CTE, I just select the base columns (AccountID, AccessType and AccessLevel, and then I CROSS APPLY a comma-delimited list of the Permissions. I use FOR XML PATH('') to build that comma-delimited list.

    If this is able to be converted to a View, it would be a simple

    <cfquery name="qryUserPerm" datasource="#Application.dsn#">
        SELECT AccessType, AccessLevel, PermissionList
        FROM myPermissionsView
        WHERE AccountID = <cfqueryparam cfsqltype="cf_sql_integer" value="#trim(session.AccountID)#">
    </cfquery>
    

    If not, you'll have to try running the above full query inside the cfquery tag.

    This should give you back a dataset like:

    | AccessType | AccessLevel |         PermissionList |
    |------------|-------------|------------------------|
    |          F |           B | 8010, 5412, 6103, 3106 |
    

    You only have one result to work with and won't have to loop.

    ======================================================================

    If you want to go the in-code route, I'd still recommend trying to use cfscript to build out the structs. But, if you can have more than one AccessLevel, your results may not be what you think they should be. You'll have to doublecheck your data.

      local.permissionType = q2.AccessType ;
      local.permissionLevel = q2.AccessLevel ;
    
      switch( q2.AccessLevel ) {
        case "S" :  local.permissionList = q2.State ;
          break ;
        case "C" :  local.permissionList = ListRemoveDuplicates(ValueList(q2.City)) ;
          break ;
        case "B" :  local.permissionList = ListRemoveDuplicates(ValueList(q2.Building)) ;
          break ;
      }
    

    https://trycf.com/gist/e811ec86f0d5a52fd9ce703f897cb5aa/acf2016?theme=monokai