Recursion and Active Directory

Goog Feed, General, ColdFusion    Comments (5)


So, I have this project on my plate of building a "portal" type application where you can access all internal applications from one spot, with one login. We are using Active Directory to authenticate the user, but the permissions on each application is set on the application side of the house.

I quickly ran into a problem.

When I would use cfAuthenticate to login it would give me back all of the groups that I was a member of, I would then use cfLDAP to grab other important data needed for the user. The only problem was that when I would get my list of groups back from cfAuthenticate it wouldn't give me nested groups.

For instance if I was a member of "Group One" in AD as an individual it would see me as having "Group One" as a group I could access, however if I was a member of "Technology" and we put the entire technology group into "Group One" it wouldn't see me as having access to "Group One".

This was my first time messing with Active Directory so I didn't know anything about this, but here is what I came up with.

I could take that group that I originally got back from my cfAuthenticate call and pass it in to a function that would loop through them and see what groups they were a member of. This would allow me to loop through the "technology" group and see that it was a member of "Group One" therefore I should be able to access anything "Group One" had access to.

So here it is...

First, I authenticate the user and then go grab important info about them for later use.

<!---Authenticating to the domain.--->
<cfntauthenticate username="#trim(arguments.username)#" password="#trim(arguments.password)#" domain="#theDomain#" result="loginResult" listgroups="yes">

<!---Querying Active Directory for employee specific data for later use. (correct name, email, employeeID, locationID)--->
<cfldap server="my.ldap.server" port="123" action="query" name="UserInfo" start="ou=Employees,ou=Users,dc=my,dc=ldap,dc=server" attributes = "cn,mail,employeeID,houseIdentifier" filter="(sAMAccountName=#trim(arguments.username)#)" username="AD_UserName" password="AD_Password" maxrows="1">

If they are authenticated, I take the groups passed back and pass them into a function which I will later call recursively.

<cfset recursiveGroupCall = getAllGroups(groupList = loginResult["groups"]) />

The function then does its thing which is detailed below and builds a string with all of my groups.

<!---Initializing public access variables.--->
<cfset variables.groupString = "" />

<cffunction name="getAllGroups" access="public" returntype="string">
<cfargument name="groupList" type="string" required="yes" default="">

<cfset var newGroups = "" />
<cfset var removeDups = "" />

<!---Loop over the passed in list and get the groups that each item is a member of.--->
<cfloop list="#arguments.groupList#" index="i">
<cfldap server="myLDAPServer" port="123" action="query" name="user" start="DC=my,DC=ldap,DC=server" attributes = "memberof,cn" scope="subtree" separator="|" filter="(cn=#i#)" username="ADUsername" password="ADPassword">
<!---Loop over the query returned for the item.--->
<cfloop query="user">
<cfoutput>
<!---Loop over the members of the query item and grab the groups out.--->
<cfloop list="#user.memberof#" index="i" delimiters="|">
<cfif NOT len(newGroups)>
<cfset newGroups = listLast(listFirst(i),"=") />
<cfelse>
<cfset newGroups = newGroups & ',' & listLast(listFirst(i),"=") />
</cfif>
</cfloop>
</cfoutput>
</cfloop>
</cfloop>

<!---Set our overall group string to whatever is present.--->
<cfif NOT len(variables.groupString)>
<cfset variables.groupString = arguments.groupList & ',' & newGroups>
<cfelse>
<cfset variables.groupString = variables.groupString & ',' & newGroups>
</cfif>

<cfif len(newGroups)>
<!---If there are new groups then call this function again, until there is no more new groups.--->
<cfset recursivenav = getAllGroups(groupList=newGroups)>
<cfelse>
<!---If there is nothing in the new groups variable, return the entire group string.--->

<!---Removing dups from the list, and cleaning it up.--->
<cfset removeDups = StructNew()>
<cfloop index="i" list="#variables.groupString#">
<cfset removeDups[i] = "">
</cfloop>
<!--- Convert the set back to a list --->
<cfset variables.groupString = StructKeyList(removeDups)>

<!---Now we return it.--->
<cfreturn variables.groupString>
</cfif>
</cffunction>

Feedback is always appreciated, if I could have done this differently, or more efficiently let me know.

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Useing cfntauthenticate presumes that the cf server is a member of the queried domain. If you have multiple domains or the cf server is not a domain member this function will fail.
# Posted By Brian Deegan | 9/18/07 10:34 PM
Will this allow members of a group access to authenticate and deny access by non-members of that group? I am working on an Intranet project that monitors staff attendance and I want supervisers to access it then, only their staff. With the cfntauthenticate tag every member of AD can authenticate the site. I want maintain data intregrity.
# Posted By Khalif | 9/19/07 4:04 PM
Hello Khalif,

I am not the world expert on this but let me answer this from my point of view. This method will not deny access to members of the domain if they are domain users. But what you would want to do is loop over their domain groups that come back from the call to AD and then if a certain group you want is or is not in the list, allow or deny access based on that criteria.

Thanks,

Joe
# Posted By Joe Gautreau | 9/20/07 4:27 PM
Thanks for this post, it led me onto the right track when I had a similar problem: Checking if a user is member of a given AD group.

I tried recursing on the "member" attribute of the group in question, which was horribly inefficient - we have hundereds upon hundereds of deeply nested Exchange groups (literally) with thousands of users in them. The process easily took 3-5 seconds for a single membership check. Additionally, the AD controller's connection limit soon was reached, and CFLDAP started to fail erratically.

My second approach checked on the "memberOf" attribute (your contribution ;-), I replaced recursion by iteration and implemented a CFLDAP result caching mechanism. The function call now takes a few hundreds of a second at most. The cache is stored in APPLICATION scope, after a few requests the LDAP server is hardly queried at all.
# Posted By Martin Böhm | 10/19/07 12:08 PM
Martin,

I'm glad you found this useful. It's nice being able to find solutions that save us time. I love it when it happens to me.

Joe
# Posted By Joe Gautreau | 10/19/07 2:09 PM