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 ComponentTo 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:
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="Logout user 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 IP to Debug Restriction List"> <cfargument name="debugip" required="no" type="string" hint="List of one or more IP Address 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 qualified path to the database file"> <cfargument name="driver" required="no" type="string" default="MSAccessJet" hint="JDBC driver 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="Database username"> <cfargument name="password" required="no" type="string" default="" hint="Database password"> <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="Throws errors 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 provides an interface to the Admin API. Administrators make this component available to their users in a secured area of the server. Keeping the facade.cfc in a secure area helps minimize the risk of unauthorized or malicious access. In this example, the facade.cfc enables users to create a data source and modify the debugging IP restriction list. The following uses the facade.cfc to add the current IP address to the debugging list: <cfobject name="REQUEST.myAdminObj" component="ows.chapter11.façade"> <cfset REQUEST.myAdminObj.setIP()> 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). Figure 11.2. Component Explorer view of the façade component (facade.cfc).TIP Best practice is to secure the Admin API directory with a sandbox and only allow access to it from the custom module. The front-end to the facade.cfc is a simple self-submitting form that accepts settings from the user. The form processes the user input and makes the appropriate call to the façade methods. For example, to create a new DSN based on submitted values, the form action code calls as follows: <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#)> Similarly, here is a call to register an IP address for debugging: <cfif isDefined('FORM.submit')> <cfset REQUEST.myAdminObj.setIP(FORM.ip)> <cfelseif isDefined('FORM.currentIP')> <cfset REQUEST.myAdminObj.setIP()> </cfif> 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. 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 submit button 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 fails or 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 new Microsoft 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 border="0"> <caption align="top" style="font-size:medium; font-weight:bold; color: #00A3DD;">Current ColdFusion 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 setIP uses 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><cfinput type="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> |