Building Custom Admin Consoles
Most ISPs or hosting companies provide site owners with a control panel for their site. A control panel is an application that provides site administration to the site owners, including such functionality as domain management, email configuration, FTP access, database administration, bandwidth statistics, disk space calculations, and the like. Conversely, intranet and other shared-host administrators are less inclined to provide site administration features to their customers. There is usually some sort of change review process that takes place before these administrators will implement even the slightest DSN change for developers. This is where the Admin API fits into the process.The Admin API allows ISPs to extend their current control-panel applications to allow site owners to have customized ColdFusion administration. Some control-panel applications may already leverage the ColdFusion ServiceFactory to provide the same functionality; however, the Admin API is the preferredand only supportedmethod. It also enables intranet administrators to provide end-user access to ColdFusion Administrator functionality without compromising security. This eliminates some of the overhead in change-control processes and facilitates the development life cycle.NOTE
Although access to the ServiceFactory is legitimate coding and there are a number of sites and articles that explain how to mimic ColdFusion Administrator functionality (such as disabling data sources), Macromedia will only officially support issues originating from the Admin API.
The Façade Component
To properly and securely extend the Admin API to users, custom modules should be built that provide minimal exposure to its methods. These modules should implement a façade design pattern. In Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley 1995), the "Gang of Four" (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) state the following as the purpose of the façade:
"Provide a unified interface to a set of interfaces in a subsystem. Façade defines a higher-level interface that makes the subsystem easier to use."
For custom admin consoles, the façade is a ColdFusion component that interfaces with the Admin API modules (Figure 11.1). Code the façade component to expose only the Admin API methods that make sense to the client. For example, suppose the customer has a dedicated server and you want to allow the customer to manage data sources and debugging and logging. You would code a component that would interface with those Admin API modules. Listing 11.1 illustrates a façade component that limits access just to creating a MS Access Unicode data source, and adding an IP address to the debugging IP restriction list.
Listing 11.1. façade.cfcFaçade Access to Admin API
[View full width]
<cfcomponent displayname="Facade" hint="Provides custom interface to the Admin API">
<!---####
File name: facade.cfc
Description: Template that provides access to Admin API methods
Assumptions: Access to the Admin API code base (/CFIDE/adminapi)
Author name and e-mail: Sarge (ssargent@macromedia.com)
Date Created: February 28, 2005
####--->
<cffunction name="login" access="remote" returntype="any" output="false"hint="Authenticate user to the Admin API">
<cfargument name="adminUser" type="string" required="true">
<cfset adminObj= CreateObject("Component", "CFIDE.adminapi.administrator").login(ARGUMENTS.adminUser)>
<!---#### Return an instance of the facade object ####--->
<cfreturn THIS>
</cffunction>
<cffunction name="logout" access="remote" returntype="void" output="false" hint="Logoutuser from the Admin API">
<cfset adminObj= CreateObject("Component", "CFIDE.adminapi.administrator").logout()>
</cffunction>
<cffunction name="getDatasources" access="remote" returntype="struct" output="false"hint="Returns structure of DSNs">
<cfargument name="dsnname" required="no" hint="Name of a data source to retrieve">
<cfobject name="dsnObj" component="CFIDE.adminapi.datasource">
<cfif IsDefined("ARGUMENTS.dsnname")>
<cfreturn dsnObj.getDatasources(ARGUMENTS.dsnname)>
<cfelse>
<cfreturn dsnObj.getDatasources()>
</cfif>
</cffunction>
<cffunction name="getIPList" access="remote" returntype="any" output="false"hint="Returns a list of IP in the Debug Restriction List">
<cfobject name="debugObj" component="CFIDE.adminapi.debugging">
<cfreturn debugObj.getIPList()>
</cffunction>
<cffunction name="setIP" access="remote" returntype="void" output="false" hint="Adds IPto Debug Restriction List">
<cfargument name="debugip" required="no" type="string" hint="List of one or more IPAddress to add to the debugging list.">
<cftry>
<cfobject name="debugObj" component="CFIDE.adminapi.debugging">
<cfif IsDefined("ARGUMENTS.debugip")>
<cfset debugObj.setIP(ARGUMENTS.debugip)>
<cfelse>
<cfset debugObj.setIP(debugObj.getCurrentIP())>
</cfif>
<cfcatch>
<cfthrow detail="#CFCATCH.detail#" message="#CFCATCH.message#">
</cfcatch>
</cftry>
</cffunction>
<cffunction name="setMSAccessUnicode" access="remote" returntype="any" output="true"hint="Creates a new DSN">
<cfargument name="name" required="yes" type="string" hint="ColdFusion data source name">
<cfargument name="databasefile" required="yes" type="string" hint="Fully qualifiedpath to the database file">
<cfargument name="driver" required="no" type="string" default="MSAccessJet" hint="JDBCdriver for this DSN">
<cfargument name="class" required="no" type="string" default="com.inzoom.jdbcado.Driver" hint="Fully qualified JDBC driver class name">
<cfargument name="username" required="no" type="string" default=" hint="Databaseusername">
<cfargument name="password" required="no" type="string" default=" hint="Databasepassword">
<cfargument name="encryptpassword" required="no" type="boolean" default="true"hint="Encrypt password stored in neo-query.xml">
<cfargument name="description" required="no" type="string" hint="Data source description">
<cfset var success = "false">
<cfset var dbObj = ">
<cfscript>
try {
dbObj = createObject("component", "CFIDE.adminapi.datasource");
if (not dbObj.verifyDSN(ARGUMENTS.name)) {
dbObj.setMSAccessUnicode(argumentCollection=ARGUMENTS);
success=dbObj.verifyDSN(ARGUMENTS.name);
} else {
throw("Sorry but that data source name (#ARGUMENTS.name#) already exists!","Application");
}
}
catch (Any ex) {
throw(ex.message, ex.type, ex.detail);
}
</cfscript>
<cfreturn success>
</cffunction>
<cffunction name="throw" access="private" returntype="void" output="false" hint="Throwserrors in cfscript block">
<cfargument name="message" required="yes" default=" hint="Error message to display">
<cfargument name="type" required="no" default="any" hint="Type of exception thrown">
<cfargument name="detail" required="no" default=" hint="Description of exception">
<cfargument name="errorCode" required="no" default=" hint="Custom error code">
<cfargument name="extendedInfo" required="no" default=" hint="Custom error information">
<cfthrow message="#ARGUMENTS.message#" type="#ARGUMENTS.type#" detail="#ARGUMENTS.detail#" errorcode="#ARGUMENTS.errorcode#" extendedinfo="#ARGUMENTS.extendedInfo#">
</cffunction>
</cfcomponent>
Figure 11.1. A façade pattern for accessing the Admin API components.

The facade.cfc in Listing 11.1 illustrates one method of exposing only a subset of the Admin API. Limiting the functionality in the façade prevents unintentional system wide damage or disclosure of sensitive data. The ColdFusion Component Explorer shows the methods and properties of the facade.cfc (Figure 11.2).
<cfobject name="REQUEST.myAdminObj" component="ows.chapter11.façade">
<cfset REQUEST.myAdminObj.setIP()>
Figure 11.2. Component Explorer view of the
faç ade component ( facade.cfc).[View full size image]

Similarly, here is a call to register an IP address for debugging:
<cfif isDefined('FORM.submit')>
<cfset VARIABLES.argCol = StructNew()>
<cfloop index="i" list="#FORM.fieldNames#">
<cfif NOT FindNoCase("submit", i)>
<cfset StructInsert(VARIABLES.argCol, i, FORM[i])>
</cfif>
</cfloop>
<cfset REQUEST.myAdminObj.setMSAccessUnicode(argumentCollection=#VARIABLES.argCol#)>
Listings 11.2 and 11.3 show the completed forms for the IP and DSN functionality. You can add as much complexity or simplicity to your admin console as you want. For example, you could develop a front-end that leverages an LDAP to provide authentication and authorization. You could also extend the LDAP entries to store properties (such as a list of CFXs, DSNs, and mappings) for individual sites, which you would then allow the authenticated user to administer for their site. The main idea is to have one façade component as the access point to the Admin API.
<cfif isDefined('FORM.submit')>
<cfset REQUEST.myAdminObj.setIP(FORM.ip)>
<cfelseif isDefined('FORM.currentIP')>
<cfset REQUEST.myAdminObj.setIP()>
</cfif>
Listing 11.2. dsnForm.cfnAdding a Data Source
[View full width]
<cfsetting enablecfoutputonly="yes">
<!---####
File name: dsnForm.cfm
Description: Form for adding new DSN with the MS Access Unicode driver
Assumptions: Access to the Admin API code base (/CFIDE/adminapi) via facade.cfc
Author name and e-mail: Sarge (ssargent@macromedia.com)
Date Created: February 28, 2005
####--->
<!---#### Create a local variable to hold the structure of current ColdFusion DSNs. ####--->
<cfset VARIABLES.currentDSN = REQUEST.myAdminObj.getDatasources()>
<cfif isDefined('FORM.submit')>
<cftry>
<!---#### Create a local variable structure to pass to the facade method. ####--->
<cfset VARIABLES.argCol = StructNew()>
<!---#### Loop over the FORM fields and populate VARIABLES.argCol, removing the submitbutton value. ####--->
<cfloop index="i" list="#FORM.fieldNames#">
<cfif NOT FindNoCase("submit", i)>
<cfset StructInsert(VARIABLES.argCol, i, FORM[i])>
</cfif>
</cfloop>
<!---#### Call the facade method to create the DSN, it will throw an error if it failsor the DSN is a duplicate. ####--->
<cfset REQUEST.myAdminObj.setMSAccessUnicode(argumentCollection=#VARIABLES.argCol#)>
<cfcatch type="any">
<cfoutput><h3 style="color: red">#CFCATCH.message#</h3></cfoutput>
</cfcatch>
</cftry>
</cfif>
<cfsetting enablecfoutputonly="no"><!---#### Display form to accept input for DSN. ####--->
<cfform method="post" name="dsnForm" format="xml" skin="Blue" width="450" preservedata="yes">
<cfformitem type="html"><span>Complete the form to create a newMicrosoft Access Unicode DSN. Check the table below the form to ensure the DSN does not
already exist.</span></cfformitem>
<cfformgroup type="vertical">
<cfinput name="name" label="Name" required="yes" type="text" size="15">
<cfinput type="text" name="databasefile" size="25" label="Database File"tooltip="Fully qualified path to the database file" required="yes">
<cfinput name="username" label="Username" required="no" type="text">
<cfinput name="password" label="Password" type="password">
</cfformgroup><cftextarea name="description" label="Description"></cftextarea>
<cfformgroup type="horizontal">
<cfinput type="submit" name="submit" value="Create">
<cfinput type="reset" name="reset" value="Reset">
</cfformgroup>
</cfform>
<cfsetting enablecfoutputonly="no">
<!---#### Display currently configured DSNs ####--->
<p><table id="DSNs" border="0">
<caption align="top" style="font-size:medium; font-weight:bold; color: #00A3DD;">CurrentColdFusion Data Sources</caption>
<tr><th>Name</th><th>Driver</th><th>Class</th></tr>
<cfoutput><cfloop list="#ListSort(structKeyList(VARIABLES.currentDSN),"textnocase")#"index="d">
<tr><td>#d#</td><td>#VARIABLES.currentDSN[d].driver#</td><td>#VARIABLES.currentDSN[d].class#</td></tr>
</cfloop></cfoutput>
</table></p>
Listing 11.3. ipForm.cfmAdding an IP for Debugging
[View full width]
<cfsetting enablecfoutputonly="yes">
<!---####
File name: ipForm.cfm
Description: Form for adding IP addresses to the Debugging IP Restriction List
Assumptions: Access to the Admin API code base (/CFIDE/adminapi) via facade.cfc
Author name and e-mail: Sarge (ssargent@macromedia.com)
Date Created: February 28, 2005
####--->
<cftry><!---#### If an IP is submitted the pass it to the setIP method, otherwise setIPuses the current IP. ####--->
<cfif isDefined('FORM.submit')>
<cfset REQUEST.myAdminObj.setIP(FORM.ip)>
<cfelseif isDefined('FORM.currentIP')>
<cfset REQUEST.myAdminObj.setIP()>
</cfif>
<cfcatch type="any"><!---#### Display any error messages. ####--->
<cfoutput><h3 style="color: red">#CFCATCH.message#</h3></cfoutput>
</cfcatch>
</cftry>
<cfsetting enablecfoutputonly="no">
<p>Add an IP address or submit the current IP address</p>
<cfform name="ipForm">
<table border="0">
<tr><td>IP Address</td><td><cfinput type="text" name="ip"></td></tr>
<tr><td><cfinput type="submit" name="submit" value="Submit"></td><td><cfinputtype="submit" name="currentip" value="Add Current"></td></tr>
</table>
</cfform>
<hr><!---#### Display a list of current IPs ####--->
<table border="0" width="250">
<tr><th>Current IP Addresses</th></tr>
<cfoutput><cfloop list="#REQUEST.myAdminObj.getIPList()#" index="i">
<tr><td>#i#</td></tr>
</cfloop></cfoutput>
</table>