22 January 2014

Shared Services Stupid Trick No. 4

Challenges

I like them.  No, not impossible ones, like cleaning out the Augean Stables.  I’m no Hercules.  Those who have seen my manly physique know I am more like the before than the after in this famous-if-you-ever-read-comic-books ad.  And before you get any ideas, know that yr. obdnt. srvnt. does not habitually kick chairs when frustrated, seek out “dance floor hogs” for a damn good whacking, or practice “dynamic tension”.  I do like hiking, however.  And isometrics are not a bad way of building anaerobic strength.

What I really enjoy is being told that something is “impossible” when I have a sneaking suspicion that maybe it isn’t.  And then trying to see who’s right – Mr. Know-All or me.  Sometimes I win, sometimes I lose but no matter the outcome I’m the wiser for trying.  This is one of those posts.

The genesis

Over on Network54’s Essbase board, there was a rather long thread about Things That We Wish Oracle EPM Did.  And in that thread, there was a comment or at least an implication that SQL queries couldn’t join Shared Services provisioning to Planning access but that XML processing could.  On the face of it, this didn’t make sense – why would one technology be better than another when it came to matching up data?  There was also the admission that the LCM->XML->MS Access code took over an hour.  Surely I could beat that performance with good old SQL.   Maybe.

The challenge

It seems pretty straightforward.  Based on that thread post, using SQL, could I:
  1. Match Shared Services provisioning and roles with Planning application security
  2. Do so in less than an hour

Of course this is a challenge that I set for myself and if I ended up not being able to do it, well, I tried, failed, and maybe would have to rethink the power of SQL.  And if I solved it, hey presto – an excuse for a blog post.  As you’re reading a blog post, I reckon not many of you are sitting on the proverbial edge.

Okay, I cheated

There’s cheating and then there’s cheating.  I was pretty sure that I could do this because I had already written a four part series on Planning security and a three part series on Shared Services security.  I think I know the basics of querying Oracle EPM security.  So not cheating, but at least an idea that this maybe it wouldn’t be all that difficult to do.

And for once I was right

In fact, this query was simply the mashup of the direct Shared Services and implicit Planning queries.  Joining across databases (this is SQL Server – Oracle would just be joining across schemas and works the same way) is easy:  just specify the database name before the table and Bob’s Your Uncle.  It took me all of three minutes to do this – it is nice when one has the code already done although when I wrote it I never thought I would join the results.

The code

USE HYP_SharedServices
GO

--    Declare the anchor member as a variable as it will be referenced more than once
DECLARE @TopCat AS nvarchar(4000)
SET @TopCat = 'PLN_SampApp1' ;

/**********************************************************************************/
/*    NOTE NOTE NOTE NOTE -- The below is direct Shared Services assignment.        */
/**********************************************************************************/
WITH TopCat AS
(
   --    Set up the anchor member
   SELECT
       G.LOWER_IDENTITY_ID AS 'Parent_ID'
       , G.LOWER_IDENTITY_ID AS 'Child_ID'
       --    Need to CAST as VARCHAR(200) to allow UNION ALL
        , G.NAME AS 'Parent'
       , CAST('' AS VARCHAR(200)) AS 'Child'
       , 1 AS 'Gen'
   FROM CSS_GROUPS G
   -- This is where you put in the topmost member of the tree
   WHERE G.NAME = @TopCat
),
Related AS
(
   --    Almost Parent/Child, but not quite
   --    I made this a separate CTE because it's cleaner down in the true Parent/Child query
   SELECT
       GM.LOWER_GROUP_IDENTITY AS 'Parent_ID'
       , GM.LOWER_MEMBER_IDENTITY AS 'Child_ID'
       , G1.NAME AS 'Parent'
       -- 1 = user, 2 = group; Cast both as VARCHAR(200) to allow UNION ALL
       , CASE
           --    Subqueries for Users and Groups to get the names
               WHEN
                   GM.MEMBER_TYPE = 1
               THEN
                   (SELECT
                       CAST(U.NAME AS VARCHAR(200))
                   FROM CSS_USERS U
                   WHERE U.LOWER_IDENTITY_ID = GM.LOWER_MEMBER_IDENTITY)
               WHEN
                   GM.MEMBER_TYPE = 2
               THEN
                   (SELECT
                       CAST(G2.NAME AS VARCHAR(200))
                   FROM CSS_GROUPS G2
                   WHERE G2.LOWER_IDENTITY_ID = GM.LOWER_MEMBER_IDENTITY)
           END
       AS 'Child'
   FROM CSS_GROUP_MEMBERS GM
   INNER JOIN CSS_GROUPS G1 ON
       GM.LOWER_GROUP_IDENTITY = G1.LOWER_IDENTITY_ID
),
ParentChild AS
(
   -- Set the anchor
   SELECT
       T.*
   FROM TopCat T
   UNION ALL
   -- Oooh, fancy, we're going recursive here
   SELECT
       R.Parent_ID
       , R.Child_ID
       , R.Parent
       , R.Child
       , PC.Gen + 1
   FROM Related R     
   --    This is the recursive JOIN that ties child to parent
   INNER JOIN ParentChild PC
       ON R.Parent_ID = PC.Child_ID
   ),
--    Figure out all of the possible roles in each project
RolesByProduct AS
(
   SELECT
       R.LOWER_IDENTITY_ID
       , R.ROLE_ID
       , R.PRODUCTCODE
       , R.PRE_CONFIG_STATUS
       , L.VERSION
       , L.NAME
       , L.DESCRIPTION
   FROM CSS_ROLES R
   INNER JOIN CSS_ROLE_LOCALES L
       ON R.LOWER_IDENTITY_ID = L.LOWER_IDENTITY_ID
   WHERE
       LOWER(L.LOCALE) IN (LOWER('en_US'))  
       AND L.LOWER_NAME LIKE LOWER(N'%')
       AND LOWER(L.VERSION) = LOWER (N'11.1.2.0')
),   
--    Figure out direct security assignments for all levels of the hieararchy.
--    Use the undocumented view VW_APPLICATIONS to get a pretty descriptions of the
--    products and projects.
DirectSharedServicesSecurity AS
   (
   SELECT
       --PC.*
       --, I.LOWER_ROLE_IDENTITY
       --, I.LOWER_APPLICATION_ID
       PC.Parent
       , PC.Child
       , PC.Gen
       , VA.PROJECT_NAME
       , VA.APP_DISPLAY_NAME
       , R.NAME
       , R.DESCRIPTION
       , VA.PRODUCT_NAME
   FROM ParentChild PC
   LEFT OUTER JOIN CSS_PROVISIONING_INFO I
       ON PC.Child_ID = I.LOWER_MEMBER_IDENTITY
   LEFT OUTER JOIN VW_APPLICATIONS VA
       ON I.LOWER_APPLICATION_ID = LOWER(VA.APPLICATION_ID)       
   LEFT OUTER JOIN RolesByProduct R
       ON I.LOWER_ROLE_IDENTITY = R.LOWER_IDENTITY_ID    ),
/**********************************************************************************/
/*    NOTE NOTE NOTE NOTE -- The below is Planning implicit security assignment.    */
/**********************************************************************************/
 -- Dimensions and all of their members
    Dimensions AS
    (
    SELECT DISTINCT
        O.OBJECT_ID,
        O.OBJECT_NAME AS "Member",
         (SELECT OB.OBJECT_NAME
            FROM HYP_PLN_RestoreApp.dbo.HSP_OBJECT OB
            WHERE OB.OBJECT_ID = M.DIM_ID) AS "Dimension"
    FROM HYP_PLN_RestoreApp.dbo.HSP_OBJECT O
    INNER JOIN
        HYP_PLN_RestoreApp.dbo.HSP_MEMBER M
        ON M.MEMBER_ID = O.OBJECT_ID
    --ORDER BY O.OBJECT_ID ASC
    ),
 -- All of the other object types, including the ones that aren't documented or in HYP_PLN_RestoreApp.dbo.HSP_OBJECT_TYPE
 ObjType AS
    (
     /*
       1 to 50 defined in HYP_PLN_RestoreApp.dbo.HSP_OBJECT_TYPE
       103 = Menus
       107 = Composite forms
       115 = Deployed Business Rule
       116 = Looks like Business Rules, but don't exist in CM?  So orphaned?
       117 = Calculation Manager variables
       118 = Business Rule Folder
       119 = CalcMgrRulesets -- that's actually the OBJECT_NAME, so defined by system?
       120 = There are four valies in a three Plan Type Planning app:
             CalcMgrVariables
             CalcMgrVariablesPTName1
             CalcMgrVariablesPTName2
             CalcMgrVariablesPTName3        
     */
     SELECT DISTINCT
       OBJECT_TYPE AS "OBJECT_TYPE",
       TYPE_NAME AS "TYPE_NAME"
     FROM HYP_PLN_RestoreApp.dbo.HSP_OBJECT_TYPE
     UNION
     SELECT
       CONVERT(INT, '103') AS "OBJECT_TYPE",
       'Menu' AS "TYPE_NAME"
     UNION
     SELECT
       CONVERT(INT, '107') AS "OBJECT_TYPE",
       'Composite' AS "TYPE_NAME"
     UNION
     SELECT
       CONVERT(INT, '115') AS "OBJECT_TYPE",
       'Business Rule' AS "TYPE_NAME"
     UNION
     SELECT
       CONVERT(INT, '118') AS "OBJECT_TYPE",
       'Business Rule Folder' AS "TYPE_NAME"
     --ORDER BY OBJECT_TYPE, TYPE_NAME    
    ),
 --  Get every object in the application
 ObjectID AS
   (
     SELECT
       OBJECT_ID,
       OBJECT_NAME,
       OBJECT_TYPE,
       SECCLASS_ID
     FROM HYP_PLN_RestoreApp.dbo.HSP_OBJECT
   ),
 -- This is almost the end of the road, but it doesn't take into account implicit security
 -- Stop here if that isn't important
 FinalCTE AS
   (
     SELECT --DISTINCT
       --OT.TYPE_NAME AS "Type",
       --  If the OBJECT_TYPE = 50 then it is a user-defined or custom dimension
       --  so do a subquery to pull the dimension name
       CASE
         WHEN O_ID.OBJECT_TYPE != 50 THEN OT.TYPE_NAME
         ELSE (SELECT D."Dimension"
                 FROM Dimensions D
                 WHERE O_ID.OBJECT_ID = D.OBJECT_ID)
       END AS "Type",
       O_ID.OBJECT_NAME AS "Object",
       CASE
       -- Subquery to get user or group type
           (SELECT OA.OBJECT_TYPE
               FROM HYP_PLN_RestoreApp.dbo.HSP_OBJECT OA
           WHERE OA.OBJECT_ID = AC.USER_ID)
           WHEN 5 THEN 'User'
           WHEN 6 THEN 'Group'
       END AS "Security Type",
      (SELECT OA.OBJECT_NAME
           FROM HYP_PLN_RestoreApp.dbo.HSP_OBJECT OA
           WHERE OA.OBJECT_ID = AC.USER_ID) AS "User/Group Name",
       CASE AC.ACCESS_MODE
           WHEN 1 THEN 'Read'
           WHEN 2 THEN 'Write'
           WHEN 3 THEN 'Write'
           WHEN 4 THEN 'Launch'
           WHEN -1 THEN 'Deny'
       END AS "Read/Write",
       CASE AC.FLAGS
         WHEN 0 THEN '"' + O_ID.OBJECT_NAME + '"'
         WHEN 5 THEN '@CHI("' + O_ID.OBJECT_NAME + '")'
         WHEN 6 THEN '@ICHI("' + O_ID.OBJECT_NAME + '")'
         WHEN 8 THEN '@DES("' + O_ID.OBJECT_NAME + '")'
         WHEN 9 THEN '@IDES("' + O_ID.OBJECT_NAME + '")'
        END AS "Hierarchy function"      
     FROM ObjectID O_ID
     INNER JOIN
       ObjType OT ON OT.OBJECT_TYPE = O_ID.OBJECT_TYPE
     INNER JOIN HYP_PLN_RestoreApp.dbo.HSP_ACCESS_CONTROL AC
       ON O_ID.OBJECT_ID = AC.OBJECT_ID
   ),
 --  Figure out what the relationship is between users and groups
 --    All users in all groups
 SuperSet AS
   (
     SELECT
       O1.OBJECT_NAME AS "Group",
       O2.OBJECT_NAME AS "User"
     FROM HYP_PLN_RestoreApp.dbo.HSP_USERSINGROUP G
     INNER JOIN HYP_PLN_RestoreApp.dbo.HSP_OBJECT O1
       ON G.GROUP_ID = O1.OBJECT_ID
     INNER JOIN HYP_PLN_RestoreApp.dbo.HSP_OBJECT O2
       ON G.USER_ID = O2.OBJECT_ID  
   ),
 --  Just the users in the specified group(s)
 --  NB -- This could be expanded to multiple groups if need be
 SpecificGroup AS
   (
     SELECT
       O1.OBJECT_NAME AS "Group",
       O2.OBJECT_NAME AS "User"
     FROM HYP_PLN_RestoreApp.dbo.HSP_USERSINGROUP G
     INNER JOIN HYP_PLN_RestoreApp.dbo.HSP_OBJECT O1
       ON G.GROUP_ID = O1.OBJECT_ID
     INNER JOIN HYP_PLN_RestoreApp.dbo.HSP_OBJECT O2
       ON G.USER_ID = O2.OBJECT_ID  
     --WHERE
     --  O1.OBJECT_NAME LIKE '%EMEA'
   ),
 -- Use an IN statement with a subquery to limit the users from the SuperSet CTE to the users in the
 -- SpecificGroup CTE
 UsersInGroups AS
   (
     SELECT
       S.*
     FROM SuperSet S
     WHERE S."User" IN (SELECT "User" FROM SpecificGroup)      
   ),
 -- Use an IN statement with a subquery against a DISTINCT to limit the groups from the SuperSet
 -- CTE.  This will return all of the inherited groups.    
 GroupsInGroups AS
    (
        SELECT DISTINCT
            S."Group"
        FROM SuperSet S
        WHERE S."User" IN (SELECT "USER" FROM SpecificGroup)
    ),
 -- Get the security that is specifically assigned to users
 -- Use an IN statement with a subquery to limit the users from the FinalCTE CTE to the users in the
 -- SpecificGroup CTE
 UserDefinedSecurity AS
 (
   SELECT
       F."Type",
       F."Object",
       F."Security Type",
       F."User/Group Name",
       --'User-assigned' AS "Parent Group",
       F."Read/Write",
       F."Hierarchy function"
   FROM FinalCTE F
   WHERE
        "Security Type" = 'User'
        AND F."User/Group Name" IN (SELECT "USER" FROM SpecificGroup)
 ),
 --  Get the security that is specifically assigned to groups
 --  The join between the CTE UsersInGroups and FinalCTE is the key to implicit security
 GroupDefinedSecurity AS
 (
   SELECT
       F."Type",
       F."Object",
       F."Security Type",
       G."Group" AS "User/Group Name",
       --F."User/Group Name" AS "Parent Group",
       F."Read/Write",
       F."Hierarchy function"
   FROM FinalCTE F
   INNER JOIN GroupsInGroups G
     ON G."Group" = F."User/Group Name"
 ),
 --  UNION the explicit to the user and the implicit via a group security
 UserAndGroupDefinedSecurity AS
 (
   SELECT
     *
   FROM UserDefinedSecurity  
   UNION
   SELECT * FROM GroupDefinedSecurity
 ),
 PlanningSecurity AS  
 (--  Now report out however you like
 SELECT
   "User/Group Name",
   "Security Type",
   --"Parent Group",
   "Type",
   "Object",
   "Read/Write",
   "Hierarchy function"
 FROM UserAndGroupDefinedSecurity
 )
--  Join the two query threads on user/group
SELECT
    DS.Parent AS 'SS Parent'
    , DS.Child AS 'SS Child'
    , DS.PROJECT_NAME AS 'SS Project'
    , DS.APP_DISPLAY_NAME AS 'SS App'
    , DS.NAME AS 'SS Role'
    , DS.DESCRIPTION AS 'SS Role Desc'
    , DS.PRODUCT_NAME AS 'SS Product'
    , PS.[Security Type] AS 'PLN Security Type'
    , PS.Type AS 'PLN Object'
    , PS.[Read/Write] AS 'PLN R/W'
    , PS.[Hierarchy function] AS 'PLN Hierarchy'
FROM DirectSharedServicesSecurity DS
LEFT OUTER JOIN PlanningSecurity PS
    ON DS.Child = PS."User/Group Name"

That’s a fair bit of code, about 350 lines.  In the real world, I would probably stick the Shared Services and Planning sides of the query into views and then join across the views to get the results to make things a bit more readable but that is an exercise for you, Gentle Reader, if you are so inclined.

Note that I used a LEFT OUTER JOIN to ensure that a lack of Planning security didn’t suppress Shared Services provisioning/roles.


The time

I suspect I will never know enough SQL to actually tune my queries.  The good news is that the set of what gets queried out of the EPM repositories is so small that efficiency is not much of an issue.  As an example, this query took exactly zero seconds to return the result:

I took a look at SQL Profiler to see if I could get more detail.  Instantaneous isn’t totally correct, but pretty darn close:
 
Based on the time, I think that 13 milliseconds beat 60 minutes quite handily.  :)

What have we learnt?

A couple of things, in order of increasing importance:
  1. SQL is really cool.
  2. Simplicity (yes, the query is long, but this is simpler than LCM, XML, and MS Access) trumps complexity.
  3. There are few absolutes when it comes to code.  Or perhaps what I mean is that it is dangerous to state (or to be fair, imply) that something is impossible, or even that one approach is “better” than another.  I for one have been shown many times that my impossible is someone else’s common practice.  Sometimes I laugh at my ignorance, other times I wince, but I always come away from the experience that little bit wiser.  Heaven knows I need the help.
  4. I better not really believe that what I did is “the best”, cf. the above point.  At best it is an approach that works quickly and was easy to write.  Pride goeth before the fall and all that.

So with those points and caveats, did I meet the challenge I set for myself?  Yes, I was able to use publicly available (albeit unsupported) code (yeah, my own, but still it is public) and join together security information from multiple EPM products with really very little effort.  Huzzah for me, etc., and I do enjoy that all too rare pat on the back even if I gave it to myself.

But that’s mere ego and thus not important.  What is really important is that you too, Gentle Reader, can combine the queries and code that I and others (Celvin, I am looking at you) are putting out there.  I really was very pleasantly surprised that it was actually so easy.  If you’re not taking advantage of all of these great resources…well, you need to interview your brains.

Be seeing you.

4 comments:

  1. You keep proving that "Nothing is impossible" ;) I might steal your idea and add it to next version of NUMSys

    ReplyDelete
  2. Celvin,

    Thank you for your kind words.

    You're welcome to use the code or even just the idea. Please mention my name in the comments. :)

    Regards,

    Cameron Lackpour

    ReplyDelete
  3. Hi Cameron,

    What the code ro getting list of all users provisioned to a planning application?

    ReplyDelete
  4. Anon,

    Take a look at the third from the bottom graphic. Then look at the very bottom four rows. Those are the users who have implicit provisioning to the application. The query should also handle explicit provisioning to the application.

    I'll think about what it would take to write a query that just returned users and nothing else. It shouldn't be that hard.

    Regards,

    Cameron Lackpour

    ReplyDelete