Enhancing the UA Bash extension for Resources

0 Likes
Fernando Freitas wrote an awesome script that makes the User Application web service endpoint for Roles available as shell commands in bash. This is much more useful than you would think at first. After all, we can do them in a GUI, in SOAP UI, or other SOAP tools, what does bash give us?

Well if you are a shell command kind of guy or gal, it gives you a lot. Now you can use any other script framework, and just call out to these functions. As a simple example, I grabbed some sample scripts that take commands, processes them, and then executes a series of tasks, and then added in support to read input from a CSV file. Then adding in this bash shell extension, I was able to do User App role operations, in bulk, from a command line script in Linux. That turns out to be mighty useful.

For example, we had a customer who had thousands of roles, and needed to rename 900 of them in a hurry. Sure we could have done it in the GUI with enough time. We THINK we might have been able to do it by generating LDIF files to change the two attributes that seem to hold the information, but we were not sure if that would break anything. Instead we needed to call the setRoleLocalizedString() API call twice, once to set the name, a second time to set the Description. Using the CSV reading script, we were able to call this 900 times (twice each time) with one command.

As you can see, this can be really useful. Now imagine you have 18,000 users you need to add to one or many roles. Lots of possible ways to do this, but having a script like this that can read input from a CSV sure makes it easy. You could for example use a Delimited Text driver (I recommend Stefaan Van Cauwenberge's Generic Text driver, it is free and way better than the NetIQ one) that has a policy to call the Add Role token for every line it reads in.

He has put a bunch of his plugins and tools available on his web site at: http://vancauwenberge.info

You could do it in a Workflow that accepts text in a box, that is a list of DN's to add to a Role, and then call an Integration Activity to make the SOAP call. This has an issue that the User App will run out of memory as it loops pretty quickly, limiting the number of entries that can be processed in a single run.

However, using this script, you would be making many thousands of calls, but they queue up in order and get processed as fast as they can be processed. You can easily add a pause into the script to delay it if needed.

All this is nice to know, and if you read my series on how you might add a new function that Fernando missed, you will see how the script works and how you might extend it.

Series: Adding New Functions to the UA Bash extension

As I discussed in those articles, Fernando only implemented the Role service endpoint. I needed a createResource function, so I implemented that as an example in those articles. I then wrote an article about how I would extend the createRole function to support more features in the SOAP API. But that got me thinking... What else in the Resource endpoint WSDL is interesting to me. I am too lazy to go do them all. I assume Fernando has some way to cheat and do them fast, since I would never go through each API and do them one by one, that is too much work for me.

But as I looked through the API I saw two easy ones that I think would be useful by themselves and figured they would make good topics for articles. Those functions are refreshCodeMap, requestResourceGrant (and by extension, requestResourceRevoke), and then if I do not get bored by then, modifyResource.

The first thing to do when considering this it to get a copy of Fernando's script and read the previous articles to get familiar with what needs to be done. The script is available at this link.
https://www.netiq.com/communities/cool-solutions/cool_tools/bash-functions-perform-soap-calls-rbpm/

Next get the WSDL from the User Application endpoint. Before 4.5, there was a GUI location to get them, as a User App Administrator (Maybe just or requiring as well Role and Resource Administrator) but in 4.5 I think that is gone, so now you just have to follow the convention of typing the URL for the endpoint followed by ?wsdl thus it would be:
https://myUAServer.com/IDMProv/resource/service/?wsdl

Then save the contents as a text file (it is just XML, read it, it is quite interesting to see how they handle it). Then open the WSDL in a tool that can do so, like SOAP UI, or even the Integration Activity in Designer's workflow area. Find the API you want and copy it into place.

Let's start with refeshCodeMap() since this actually changed at some point between 4.0 and 4.5. The new function has the ability to call a refresh on only a single entitlement, which is much more useful. A system with 20 drivers, each of which has many entitlements, some with thousands of values (Groups, internal Roles, whatever you are querying for) can cause issues if you try to update them all at once. For certain it will take longer than you would prefer, if you have that many, and there is in fact a time out value you can run into, I think it is 10 minutes. You can still trigger a refresh of all the drivers in the User Application GUI. Login as a User Application administrator, go to the Role and Resource Catalog tab. On the bottom left will be a Configure RBPM Security link or the like, and then once that page loads, scroll down to the Entitlements Query section, and you can click on the Refresh Now. This will query all the entitlements on all the drivers for all the values. Too many alls in that sentence for our own sake.

However, with the release of PCRS (Permission Collection and Reconciliation Service) packages, they needed a way to quickly update just one drivers entitlements so this got added to the User Application to support PCRS.

Let us look at the SOAP Document we need to send:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://www.novell.com/resource/service">
<soapenv:Header/>
<soapenv:Body>
<ser:refreshCodeMapRequest>
<!--Optional:-->
<ser:entitlementDN>?</ser:entitlementDN>
</ser:refreshCodeMapRequest>
</soapenv:Body>
</soapenv:Envelope>


This is probably as simple an example as we can find. I suspect getVersion is simpler since it has no parameters but one is hard to beat.

I am going to basically copy an example from Fernando's script and then start modifying it to match.

First up, clean up the name, usage, description nodes so they look something more like this:

### Function: refreshCodeMap
# Usage:
# refreshCodeMap $username $password $rbpm_url $output_file $entitlementDN
#
refreshCodeMap()
{

USAGE="Function Usage:

refreshCodeMap username password rbpm_url entitlementDN
This will refresh the code map table for only the specified IDM driver.
entitlementDN should be the DN of the Driver object in full LDAP format to root
of the tree, but no need for T=TREENAME.
rbpm_url should be in the format:
protocol://server:port/servicename
for example:
https://rbpm.lab.novell.com:8543/IDMProv";



This gives you internal descriptions, and usage when you make a typo on the command line, or miss a parameter.

The test for the DEBUG variable being set is the same in all these functions and needs no changes. The Initial Parameters check should be edited down to the proper number. This is nice, since if you miss a parameter then you see the usage.

# Initial Parameters check
if | -z "$2" || -z "$3" || -z "$4" || -z "$5"
then
echo "$USAGE"
return 1
fi


Next up is the usual setup for password support, and allowing the -W value to indicate enter the password at the command line. I have a wrapper script I use that probably should be updated to use this setting but I am mostly too lazy to go through and add that in.

The next part of interest is setting the base URL properly, it is the URL you provide on the command line, followed by the rest of the endpoint URL which changes with each endpoint. Fernando's all set it to role/service, and this of course is a resource/service URL.

# Setup for the SOAP call
URL="${3}/resource/service"
ACTION="SOAPAction: 'http://www.novell.com/resource/service/refreshCodeMap'"


Next step is the actual XML that needs to built into the POST variable.

# Build SOAP XML envelope and call to be issued
POST="<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:ser='http://www.novell.com/resource/service'>\
<soapenv:Header/>\
<soapenv:Body>\
<ser:refreshCodeMapRequest>\
<ser:entitlementDN>${4}</ser:entitlementDN>\
</ser:refreshCodeMapRequest>\
</soapenv:Body>\
</soapenv:Envelope>"


Use the fourth command line parameter (the ${4} notation) to get the value as the entitlementDN node. The rest of the function is the same as all the rest in the script, if debug is enabled trace out the full XML being submitted and finally use curl to actually post the SOAP call. The debug output of the full SOAP document is great, since it shows you the XML being submitted, which you can copy and paste into something like SOAP UI and work on it there, testing changes in a faster manner than trying to figure it out in a script.

Here would be the complete function.

### Function: refreshCodeMap
# Usage:
# refreshCodeMap $username $password rbpm_url $output_file $entitlementDN
#
refreshCodeMap()
{

USAGE="Function Usage:

refreshCodeMap username password rbpm_url output_file entitlementDN
This will refresh the code map table for only the specified IDM driver.
driverDN should be the DN of the Driver object in full LDAP format to root
of the tree, but no need for T=TREENAME.
rbpm_url should be in the format:
protocol://server:port/servicename
for example:
https://rbpm.lab.novell.com:8543/IDMProv";

if "X$_RBPM_SOAP_ROLE_DEBUG" = "Xtrue"
then
dbgparams=$#
dbgparam=1
while [ "$dbgparam" -le "$dbgparams" ]
do
echo -n "Parameter "
echo -n \$$dbgparam
echo -n " = "
eval echo \$$dbgparam
(( dbgparam ))
done
fi

# Initial Parameters check
if | -z "$2" || -z "$3" || -z "$4" || -z "$5"
then
echo "$USAGE"
return 1
fi

if "X$2" = "X-W"
then
read -sp "Please enter the password for user $1: " SENHA
echo
else
SENHA=$2
fi

# Setup for the SOAP call
URL="${3}/resource/service"
ACTION="SOAPAction: 'http://www.novell.com/resource/service/refreshCodeMap'"
CTYPE='Content-Type: text/xml;charset=UTF-8'

# Build SOAP XML envelope and call to be issued
POST="<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:ser='http://www.novell.com/resource/service'>\
<soapenv:Header/>\
<soapenv:Body>\
<ser:refreshCodeMapRequest>\
<ser:entitlementDN>${4}</ser:entitlementDN>\
</ser:refreshCodeMapRequest>\
</soapenv:Body>\
</soapenv:Envelope>"

if "X$_RBPM_SOAP_ROLE_DEBUG" = "Xtrue"
then
echo
echo POST data:
echo $POST
echo
fi

# Issue the request
curl $_CURL_OPTIONS -u "$1:$SENHA" -H "$CTYPE" -H "$ACTION" -d "$POST" "$URL" -o "$4"
}




The next function of interest to me in the Resource endpoint is requestResourceGrant and it's counterpart requestResourceRevoke. This one is a bit different than the requestRoleAssignment call in the Role endpoint, since there is a separate Grant and Revoke function, which although I have not yet looked at the XML I am guessing are almost identical, so a copy of requestResourceGrant should be trivial to modify to support requestResourceRevoke. In the case of Roles there is an action node in the SOAP document that can be grant, revoke, or it turns out extend. Extend is useful if you have an expiration on your role and need to change it. Rather than use modifyRole, you can simply request it again with the new date. I did check, you can extend into the future, and can extend backwards, but not into the past. I.e. If the role expires in 2 years, you could use extend to make it expire tomorrow, but not yesterday.

As usual, lets get our paws on the XML of interest, since we first have to understand what is going on here, before we can effectively use it.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://www.novell.com/resource/service">
<soapenv:Header/>
<soapenv:Body>
<ser:requestResourceGrantRequest>
<!--Optional:-->
<ser:resourceTarget>?</ser:resourceTarget>
<!--Optional:-->
<ser:requester>?</ser:requester>
<!--Optional:-->
<ser:userTarget>?</ser:userTarget>
<!--Optional:-->
<ser:reasonForRequest>?</ser:reasonForRequest>
<!--Optional:-->
<ser:requestParams>
<!--Zero or more repetitions:-->
<ser:resourcerequestparam>
<ser:name>?</ser:name>
<ser:value>?</ser:value>
</ser:resourcerequestparam>
</ser:requestParams>
<!--Optional:-->
<ser:correlationId>?</ser:correlationId>
</ser:requestResourceGrantRequest>
</soapenv:Body>
</soapenv:Envelope>


For fun, here is the requestResourceRevoke XML which looks pretty much the same, with only two minor differences.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://www.novell.com/resource/service">
<soapenv:Header/>
<soapenv:Body>
<ser:requestResourceRevokeRequest>
<!--Optional:-->
<ser:resourceTarget>?</ser:resourceTarget>
<!--Optional:-->
<ser:requester>?</ser:requester>
<!--Optional:-->
<ser:userTarget>?</ser:userTarget>
<!--Optional:-->
<ser:reasonForRequest>?</ser:reasonForRequest>
<!--Optional:-->
<ser:requestParams>
<!--Zero or more repetitions:-->
<ser:resourcerequestparam>
<ser:name>?</ser:name>
<ser:value>?</ser:value>
</ser:resourcerequestparam>
</ser:requestParams>
<!--Optional:-->
<ser:instanceGuid>?</ser:instanceGuid>
<!--Optional:-->
<ser:correlationId>?</ser:correlationId>
</ser:requestResourceRevokeRequest>
</soapenv:Body>
</soapenv:Envelope>


Beyond the difference in the name of the functions, I see a single extra node in the revoke case of:
<ser:instanceGuid>?</ser:instanceGuid>


Working through the process, lets figure out our command line of needed parameters and start modifying a copy of a function from our script starting at the top.

First up we have resourceTarget which makes sense, the DN of the resource of interest, as usual in LDAP format DN. Requester is the LDAP DN of the person requesting the change. The userTarget is obvious from the name it is the LDAP DN of the user to whom the Resource should apply. The node reasonForRequest is just a string. This all makes sense when you think of the Web interface for assigning a Resource. You as the logged in user are the requester, you select a Resource object, and then select a user to get the assignment. Finally you have to provide a comment, which is the reason in our API.

The next part is the only slightly tricky part. My understanding of requestParams is that they are optional, and possibly how you might pass in a value to the resource, if it were a valued Resource, that is set to request a value at grant time. This makes sense in the grant case, and initially I was going to say not in the revoke case, but actually it does. You can have a resource granted multiple times with different values. You want to be able to revoke the specific resource with the specific value so you need to set it, even in the revoke case.

Personally I never use valued Resources in this fashion but I can see how this is an important thing, so lets support it with two final command line parameters that are optional.

That gives us a command line something like:

requestResourceGrant $username $password $rbpm_url $output_file $resourceDN $requesterDN $userTargetDN $reason $param-name $param-value

When done with all the changes, the script looks basically like the following:

### Function: requestResourceGrant
# Usage:
# requestResourceGrant $username $password $rbpm_url $output_file $resourceDN $requesterDN $userTargetDN $reason
# $param-name $param-value
#
requestResourceGrant()
{

USAGE="Function Usage:

requestResourceGrant username password rbpm_url output_file resourceDN requesterDN
userTargetDN reason param-name param-value
This will Grant a Resource assignment to a user. All DN's in LDAP format.
rbpm_url should be in the format:
protocol://server:port/servicename
for example:
https://rbpm.lab.novell.com:8543/IDMProv";

if "X$_RBPM_SOAP_ROLE_DEBUG" = "Xtrue"
then
dbgparams=$#
dbgparam=1
while [ "$dbgparam" -le "$dbgparams" ]
do
echo -n "Parameter "
echo -n \$$dbgparam
echo -n " = "
eval echo \$$dbgparam
(( dbgparam ))
done
fi

# Initial Parameters check
if | -z "$2" || -z "$3" || -z "$4" || -z "$5" || -z "$6" || -z "$7" || -z "$8"
then
echo "$USAGE"
return 1
fi

if "X$2" = "X-W"
then
read -sp "Please enter the password for user $1: " SENHA
echo
else
SENHA=$2
fi

# Setup for the SOAP call
URL="${3}/resource/service"
ACTION="SOAPAction: 'http://www.novell.com/resource/service/requestResourceGrant'"
CTYPE='Content-Type: text/xml;charset=UTF-8'

# Build SOAP XML envelope and call to be issued
POST="<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:ser='http://www.novell.com/resource/service'>\
<soapenv:Header/>\
<soapenv:Body>\
<ser:requestResourceGrantRequest>\
<ser:resourceTarget>${5}</ser:resourceTarget>\
<ser:requester>${6}</ser:requester>\
<ser:userTarget>${7}</ser:userTarget>\
<ser:reasonForRequest>8</ser:reasonForRequest>\
<ser:requestParams>"

if "X$9" != "X"
then
POST="${POST}<ser:resourcerequestparam>\
<ser:name>${9}</ser:name>\
<ser:value>${10}</ser:value>\
</ser:resourcerequestparam>"
fi


POST="${POST}</ser:requestResourceGrantRequest>\
</soapenv:Body>\
</soapenv:Envelope>"

if "X$_RBPM_SOAP_ROLE_DEBUG" = "Xtrue"
then
echo
echo POST data:
echo $POST
echo
fi

# Issue the request
curl $_CURL_OPTIONS -u "$1:$SENHA" -H "$CTYPE" -H "$ACTION" -d "$POST" "$URL" -o "$4"
}



The interesting part is the POST variable and building the SOAP XML document, as text strings, bit by bit. Not much magic there in this one, basically the only tricky bit is if there is a param-name parameter then we send in the <ser:resourcerequestparams> set of nodes. As usual we take command line parameters and use them to fill in the data in the XML sample document as needed.

My next thought was to do the same task for the requestResourceRevoke function, but as I started thinking about it, I vacillated between two options. If you look at the XML of the SOAP call, they are basically identical, the parameters (except for the extra instanceGUID in the case of the revoke) are the same. I could probably have added an action parameter to the command line, and then use an if-then block to define a variable SOAPCALL like is used in the createRole command. Then you could use one script command and just specify the action.

But I decided to be lazy and do it as two separate calls, only because the SOAP API defines it as two, so why get confusing by getting clever.

Therefore, with the few minor changes required let me show the revoke example below:


### Function: requestResourceRevoke
# Usage:
# requestResourceRevoke $username $password $rbpm_url $output_file $resourceDN $requesterDN $userTargetDN $reason
# $param-name $param-value
#
requestResourceRevoke()
{

USAGE="Function Usage:

requestResourceRevoke username password rbpm_url output_file resourceDN requesterDN
userTargetDN reason param-name param-value
This will Revoke a Resource assignment to a user. All DN's in LDAP format.
rbpm_url should be in the format:
protocol://server:port/servicename
for example:
https://rbpm.lab.novell.com:8543/IDMProv";

if "X$_RBPM_SOAP_ROLE_DEBUG" = "Xtrue"
then
dbgparams=$#
dbgparam=1
while [ "$dbgparam" -le "$dbgparams" ]
do
echo -n "Parameter "
echo -n \$$dbgparam
echo -n " = "
eval echo \$$dbgparam
(( dbgparam ))
done
fi

# Initial Parameters check
if | -z "$2" || -z "$3" || -z "$4" || -z "$5" || -z "$6" || -z "$7" || -z "$8"
then
echo "$USAGE"
return 1
fi

if "X$2" = "X-W"
then
read -sp "Please enter the password for user $1: " SENHA
echo
else
SENHA=$2
fi

# Setup for the SOAP call
URL="${3}/resource/service"
ACTION="SOAPAction: 'http://www.novell.com/resource/service/requestResourceRevoke'"
CTYPE='Content-Type: text/xml;charset=UTF-8'

# Build SOAP XML envelope and call to be issued
POST="<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:ser='http://www.novell.com/resource/service'>\
<soapenv:Header/>\
<soapenv:Body>\
<ser:requestResourceRevokeRequest>\
<ser:resourceTarget>${5}</ser:resourceTarget>\
<ser:requester>${6}</ser:requester>\
<ser:userTarget>${7}</ser:userTarget>\
<ser:reasonForRequest>8</ser:reasonForRequest>\
<ser:requestParams>"

if "X$9" != "X"
then
POST="${POST}<ser:resourcerequestparam>\
<ser:name>${9}</ser:name>\
<ser:value>${10}</ser:value>\
</ser:resourcerequestparam>"
fi


POST="${POST}</ser:requestResourceRevokeRequest>\
</soapenv:Body>\
</soapenv:Envelope>"

if "X$_RBPM_SOAP_ROLE_DEBUG" = "Xtrue"
then
echo
echo POST data:
echo $POST
echo
fi

# Issue the request
curl $_CURL_OPTIONS -u "$1:$SENHA" -H "$CTYPE" -H "$ACTION" -d "$POST" "$URL" -o "$4"
}


As you can see, it is pretty straightforward using this approach to add new API calls and modify existing ones to be more useful. This just elevates the value of the tool Fernando provided, since as a framework it has as much or more value than it's specific implementation.

Of course you will notice that you could potentially do any SOAP web service you needed this way, from a command line, which may mean Fernando snagged the script framework from elsewhere, in the grand tradition of command line scripters everywhere.

Next on my list to try is back in the Role endpoint, the modifyResource function. This one I am not sure exactly how it works. Things like, how do you blank a previously filled field? (An XML attribute of xsi:nil I think but I am not sure). But also, can you send in just the changes you want to make and anything not sent is skipped, or are they nulled out? Hard to know without some experimentation. What I really want to work on is modifyRole, however I think that starting with a potentially simpler example of modifyResource might be a smarter way to get started than jumping in to the big one first. Stay tuned for the next article in this exciting (as watching paint dry) series.

I will probably bundle up all the script additions into a file you can append to Fernando's script, and likely will pass it back to him to consider including himself in a newer version.

Labels:

How To-Best Practice
Comment List
Anonymous
Related Discussions
Recommended