<cfcomponent output="No" displayname="Active Directory Service" hint="Provides General Active Directory Communication">

<!--- Create component-wide instance variable. --->
<cfset Instance = StructNew()>

<cffunction name="Init" returntype="ActiveDirectoryService" access="public" output="No" displayname="Active Directory Service Initializer" hint="Initializes the Active Directory Communications Service.">

	<cfargument name="ServerList" type="string" required="Yes" displayname="Comma-separated list of LDAP servers" hint="Comma-separated list of LDAP servers.">
	<cfargument name="UserName" type="string" required="Yes" displayname="Active Directory LDAP account user name" hint="User account that has access to Active Directory.  This account will need to be a domain admin in order to modify another domain admins attributes.">
	<cfargument name="Password" type="string" required="Yes" displayname="Active Directory LDAP account password" hint="Password for the user account in the form of 'acenet\username'.">
	<cfargument name="Port" type="numeric" required="Yes" displayname="Active Directory LDAP port" hint="Active Directory LDAP query connection port.">
	<cfargument name="QueryTimeout" type="numeric" required="Yes" displayname="Active Directory query timeout" hint="Active Directory LDAP query connection timeout.">

	<cfif Arguments.QueryTimeout lt 1>
		<cfthrow
			message="Active Directory Communication Service could not be initialized."
			detail="The QUERYTIMEOUT argument must be a numeric value greater than zero."
			type="AceLink.Exception.ActiveDirectoryService.Init.InvalidValue">
	</cfif>

	<cfset Instance.LDAPServers = ListToArray(Arguments.ServerList)>
	<cfset Instance.LDAPUserName = Arguments.UserName>
	<cfset Instance.LDAPPassword = Arguments.Password>
	<cfset Instance.LDAPPort = Arguments.Port>
	<cfset Instance.LDAPTimeout = Arguments.QueryTimeout>

	<!--- Return a "pointer" to this instance. --->
	<!--- This allows a single statement to create, and initilize, an instance. --->
	<!--- Example: objAD = CreateObject("component", "cfc.ActiveDirectoryService").Init() --->
	<cfreturn this>

</cffunction>

<cffunction name="LDAPQuery" returntype="query" access="public" output="No" displayname="LDAP Query" hint="Provides basic Active Directory read-only queries.">

	<cfargument name="Start" type="string" required="Yes" displayname="Search Start" hint="LDAP query starts in this Active Directory container (OU).">
	<cfargument name="Scope"  type="string" required="Yes" displayname="Search Scope" hint="LDAP query is limited to BASE|ONELEVEL|SUBTREE.">
	<cfargument name="LDAPAttributes" type="string" required="Yes" displayname="LDAP Query Attributes" hint="LDAP query will return this comma-separated list of attributes.">
	<cfargument name="Filter" type="string" required="Yes" displayname="Query Results Filter" hint="LDAP query results will be filtered according to this string.">
	<cfargument name="SortOrder" type="string" required="No" default="cn" displayname="Query Results Sort Order" hint="LDAP query results will be sorted according to this comma-separated list of attributes.">
	<cfargument name="Separator" type="string" required="No" default="|" displayname="Query Results Value Separator Character" hint="LDAP query results that contain multi-value attributes will be separated by this character.">

	<cfset var Results = "">

	<!--- Ensure the instance has been properly initialized prior to calling this method. --->
	<cfif StructIsEmpty(Instance)>
		<cfthrow
			message="LDAPQuery request could not be completed."
			detail="The Active Directory Communication Service has not been initialized."
			type="AceLink.Exception.ActiveDirectoryService.Init">
	</cfif>

	<!--- Ensure the SCOPE argument contains a valid value. --->
	<cfif not ReFindNoCase("(^Base$)|(^OneLevel$)|(^SubTree$)", Arguments.Scope)>
		<cfthrow
			message="LDAPQuery request could not be completed."
			detail="Incorrect SCOPE argument value passed.  Valid values are 'BASE', 'ONELEVEL' and 'SUBTREE'"
			type="AceLink.Exception.LDAPQuery.InvalidScope">
	</cfif>

	<!--- Attempt to perform the LDAP query against each server until a successful query is performed. --->
	<cfloop from="1" to="#ArrayLen(Instance.LDAPServers)#" index="ThisServer">
		<cftry>
			<cfldap
				action="query"
				name="Results"
				start="#Arguments.Start#"
				scope="#Arguments.Scope#"
				attributes="#Arguments.LDAPAttributes#"
				separator="#Arguments.Separator#"
				filter="#Arguments.Filter#"
				sort="#Arguments.SortOrder#"
				server="#Instance.LDAPServers[ThisServer]#"
				port="#Instance.LDAPPort#"
				username="#Instance.LDAPUserName#"
				password="#Instance.LDAPPassword#"
				timeout="#Instance.LDAPTimeOut#">

			<cfreturn Results>

			<cfcatch type="Any">
				<!--- Do nothing in order to try all ldap servers. --->
			</cfcatch>
		</cftry>
	</cfloop>

	<!--- If we got here, we were not able to perform a successful LDAP query. --->
	<cfthrow
		message="LDAPQuery request could not be completed."
		detail="A domain controller could not be contacted, invalid Active Directory credentials were used or incorrect attributes were passed to the LDAP query."
		extendedinfo="Ensure at least one domain controller is available to the ColdFusion server.  Ensure the LDAPQuery account has the appropriate permissions to query Active Directory.  Ensure a properly-formed LDAP query is passed to the LDAPQuery method.  Some attributes, such as 'memberOf' cannot be used to sort the results in some instances."
		type="AceLink.Exception.LDAPQuery">

</cffunction>

<cffunction name="DNListToDNArray" returntype="array" access="public" output="No" displayname="DN List To DN Array" hint="Converts single- or multi-value Distinguished Name (DN) attribute string to an array.">

	<!--- Active Directory can store multiple values in a single attribute such as 'memberOf'. --->
	<!--- An LDAP query with a character other than a comma will be properly suited for this method. --->
	<!--- Example multi-value DN: CN=md40, OU=Employees, OU=AcenetUsers, dc=evansville, dc=edu|CN=rb38, OU=Employees, OU=AcenetUsers, dc=evansville, dc=edu --->
	<!--- This method will return each DN as a separate array index. --->
	<!--- The most-suitable delimiter is NOT a comma. --->
	<cfargument name="DNList" type="string" required="Yes" displayname="DN List" hint="Contains a delimited list of Distinguished Names (DN) values.">
	<cfargument name="Condensed" type="boolean" required="No" default="No" displayname="Condensed" hint="Determines if the returned array contains a fully-qualified DN or compact CN value.">
	<cfargument name="ListDelim" type="string" required="No" default="|" displayname="CN List Delimiter" hint="Character used to delimit multi-value attribute values in LDAP query.">

	<cfset var Temp = "">

	<cfset Temp = ListToArray(Arguments.DNList, ListDelim)>

	<cfif Arguments.Condensed>
		<cfloop from="1" to="#ArrayLen(Temp)#" index="ThisOne">
			<cfset Temp[ThisOne] = ParseCNFromDN(Temp[ThisOne])>
		</cfloop>
	</cfif>

	<cfset ArraySort(Temp, "TextNoCase", "Asc")>

	<cfreturn Temp>

</cffunction>

<cffunction name="ParseCNFromDN" returntype="string" access="public" output="No" displayname="Parse CN From DN" hint="Returns only the Common Name (CN) value from a fully-qualified Distinguished Name (DN).">

	<!--- This method will take a fully-qualified distinguished name (DN) and strip all characters --->
	<!--- that are not considered part of the Common Name (CN) and return the parsed CN value. --->
	<!--- Example of fully-qualified DN: CN=md40, OU=Employees, OU=AcenetUsers, dc=evansville, dc=edu --->
	<!--- Value returned from this method: md40 --->
	<cfargument name="DN" type="string" required="Yes" displayname="DN Value" hint="Distinguished Name (DN) value to be reduced to a single Common Name (CN) value.">

	<cfreturn ReplaceNoCase(ListFirst(Arguments.DN), "CN=", "", "All")>

</cffunction>

<cffunction name="GetArrayOfGroupsForUser" returntype="array" access="public" output="No" displayname="Get Array Of Groups For User" hint="Returns an array of Active Directory group membership based on a single domain user account.">

	<cfargument name="UserName" type="string" required="Yes" displayname="User Name" hint="User Name for which to get the Active Directory group membership.">
	<cfargument name="Condensed" type="boolean" required="No" default="No" displayname="Condensed" hint="Determines if the returned array contains a fully-qualified DN or compact CN value.">

	<cfset var Temp = "">

	<!--- Query Active Directory for the user account's group membership. --->
	<cfset Temp = LDAPQuery("dc=evansville, dc=edu", "SubTree", "memberOf", "(&(objectClass=User)(Name=#Arguments.UserName#))")>

	<!--- Convert the returned attribute from a list format to an array. --->
	<cfset Temp = DNListToDNArray(Temp.memberOf, Arguments.Condensed)>

	<cfreturn Temp>

</cffunction>

<cffunction name="IsValidDomainUser" returntype="boolean" access="public" output="No" displayname="Is Valid Domain User" hint="Ensure the user name is of a valid Active Directory domain user account.">

	<cfargument name="UserName" type="string" required="Yes" displayname="User Name" hint="User Name for which to validate in Active Directory.">

	<cfset var Results = "">

	<cfset Results = LDAPQuery("dc=evansville, dc=edu", "SUBTREE", "sAMAccountName", "(&(objectClass=User)(sAMAccountName=#Arguments.UserName#))")>

	<cfif Results.RecordCount>
		<cfreturn 1>
	<cfelse>
		<cfreturn 0>
	</cfif>

</cffunction>

<cffunction name="IsValidDomainGroup" returntype="boolean" access="public" output="No" displayname="Is Valid Domain Group" hint="Ensure the group name is of a valid Active Directory domain group.">

	<cfargument name="Group" type="string" required="Yes" displayname="Group" hint="Group for which to validate in Active Directory.">

	<cfset var Results = "">

	<cfset Results = LDAPQuery("dc=evansville, dc=edu", "SUBTREE", "Name", "(&(objectClass=group)(Name=#Arguments.Group#))")>

	<cfif Results.RecordCount>
		<cfreturn 1>
	<cfelse>
		<cfreturn 0>
	</cfif>

</cffunction>

</cfcomponent>


