Building your first Model-Glue application (part 10)

Welcome to the tenth installment of “Building your first Model-Glue application.” This is the final entry dealing with actually building the site, and will be followed by an entry that talks about what I did right, what I did wrong, what I didn’t do, etc. Today’s entry will deal with the gallery security problem. If you remember, our photo galleries had three different types of access:

  • Public - Anyone can view it.
  • Private - Only the owner can view it.
  • Password - Anyone can view it if they provide the password. </ul> Let's start adding this logic to our application. The first thing I did was to logon to the application (remember you can demo it here) and view a public gallery. I then switched to the Devil's Browser and pasted in the same URL. I noticed that it asked to me logon, a totally wrong response for a public gallery. I remembered that my ViewGallery event was running a security check, so I simply removed it from there: <event-handler name="ViewGallery"> <broadcasts> <message name="getGallery" /> <message name="getImagesForGallery" /> </broadcasts> <views> <include name="body" template="dspGallery.cfm" /> <include name="main" template="dspTemplate.cfm" /> </views> <results> <result name="badGallery" do="Home" redirect="yes" /> </results> </event-handler> This change was enough to let IE view the gallery, and since the rest of the site was protected, if that user clicked elsewhere, they would still be prompted to logon. The next problem was hiding the "Add Image" form. While I'm sure folks don't mind sharing their galleries, I doubt they want just anyone adding to the gallery. There are a few ways to solve this. One way that occurred to me would be to simply if the current user is logged in, and if so, compare their username to the galleries owner. I already passed the gallery to the view, so why not pass the current user. This seems like something that would be appealing to all my events probably. Model-Glue controllers come with a onRequestStart and onRequestEnd method that are automatically called on each request. This is a great place to put such logic. I've modified onRequestStart to pass in the current user, if one exists. <cffunction name="OnRequestStart" access="Public" returntype="void" output="false" hint="I am an event handler."> <cfargument name="event" type="ModelGlue.Core.Event" required="true"> <cfif isAuthenticated()> <cfset arguments.event.setValue("userBean", session.userBean)> </cfif> </cffunction> Just in case it isn't clear, the above code will set the userBean into the viewState for every event, if it exists. Let's now modify the ViewGallery view to check for this. <cfset userBean = viewState.getValue("userBean")> <!--- show form? ---> <cfif isSimpleValue(userBean) or userBean.getUsername() neq gallery.getUsername()> <cfset showForm = false> <cfelse> <cfset showForm = true> </cfif> This code first grabs the userBean from the viewState. Remember that viewState.getValue will return an empty string if the value doesn't exist. You can also pass a second value as a default if you want. So my logic simply needs to see if userBean is a simple value, or if the username's don't match up. If so, we hide the form. (I just added a cfif to the form on the bottom of the view.) Last but not least, now let's work in the controller to ensure that the gallery is actually pubic if the user isn't logged in. I begin with this modification to getGallery() in the controller: <!--- security check ---> <cfif not isAuthenticated() and not gallery.getIsPublic() and not gallery.getIsPassword()> <cfset arguments.event.addResult("badGallery")> </cfif> Nice and short. Basically, if I'm not logged in and the gallery isn't public or password protected, add the badGallery event. To test, I simply returned to Firefox, edited the gallery, and reloaded IE, and I was immediately sent away. Now let's tackle the next simple security check - the private gallery. I modified the if block above like so: <!--- security check ---> <cfif not isAuthenticated() and not gallery.getIsPublic() and not gallery.getIsPassword()> <cfset arguments.event.addResult("badGallery")> <cfelseif gallery.getIsPrivate() and (not isAuthenticated() or gallery.getUsername() neq session.userBean.getUsername())> <cfset arguments.event.addResult("badGallery")> </cfif> This time the logic is - if the gallery is private and either I'm not logged in or the usernames don't match, throw the badGallery result. Last but not least - how to handle password protected galleries. The first problem I have is - how do I know what gallery you have access to? I mean sure I can prompt you for a password, ensure it matches, and that works great, for one gallery. How do I then re-prompt you for another, different password protected gallery? I think I'll simply use the session scope. I will store a nice list of gallery IDs that you have been authenticated with. This has one problem. It's possible the gallery owner could change the security settings on the gallery. If the gallery is changed to private, then our security system will catch it. If they change the password, then it will not. I don't think this is a huge big deal though so I won't worry about it. Let's add that check now. <!--- security check ---> <cfif not isAuthenticated() and not gallery.getIsPublic() and not gallery.getIsPassword()> <cfset arguments.event.addResult("badGallery")> <cfelseif gallery.getIsPrivate() and (not isAuthenticated() or gallery.getUsername() neq session.userBean.getUsername())> <cfset arguments.event.addResult("badGallery")> <cfelseif gallery.getIsPassword() and (not isAuthenticated() or gallery.getUsername() neq session.userBean.getUsername()) and (not isGalleryAuthenticated(gallery.getID()))> </cfif> Notice that I now check to see if the gallery is password protected, and if so, I check a new function, isGalleryAuthenticated(). This is basic abstraction (hey, isn't that a bad Sharon Stone movie?) but also a reflection of me not being 100% sure about the best way to store the fact that you have been authenticated with a gallery. Call this "Planning for my own stupidity." When I figure out a smarter way to do things later on, I'll just modify the isGalleryAuthenticated() method. And speaking of that - here it is: <cffunction name="isGalleryAuthenticated" access="private" returnType="boolean" output="false" hint="Internal method to return if a user is ok for a gallery."> <cfargument name="galleryid" type="numeric" required="true"> <cfif not structKeyExists(session,"galleryauthenticationlist")> <cfreturn false> </cfif> <cfreturn listFind(session.galleryauthenticationlist, arguments.galleryid) gte 1> </cffunction> This is rather trivial code. It simply checks for a list of IDs in the session scope. If it exists, and if the gallery ID we are checking for is in the list, then return true. If you scan back up the page, you saw that I returned a new result, passwordGallery, if the user needs to authenticate. Let's add that to the Model-Glue config event handler for ViewGallery: <event-handler name="ViewGallery"> <broadcasts> <message name="getGallery" /> <message name="getImagesForGallery" /> </broadcasts> <views> <include name="body" template="dspGallery.cfm" /> <include name="main" template="dspTemplate.cfm" /> </views> <results> <result name="badGallery" do="Home" redirect="yes" /> <result name="passwordGallery" do="PasswordGallery" redirect="yes" append="id"/> </results> </event-handler> Now I add the new event: <event-handler name="PasswordGallery"> <broadcasts /> <views> <include name="body" template="dspPasswordGallery.cfm" /> <include name="main" template="dspTemplate.cfm" /> </views> <results /> </event-handler> Nothing too special here. Basically I tell the event which view to use. Let's look at the view: <cfset viewState.setValue("title", "PhotoGallery Authentication")> <cfset badLogon = viewState.getValue("badLogon", false)> <cfset id = viewState.getValue("id", 0)> <cfoutput> <p> The gallery you want to view is password protected, please enter the password below. </p> <cfif badLogon> <p> <b> The password you entered was not correct.<br /> Please try again. </b> </p> </cfif> <p> <form action="#viewstate.getValue("myself")#passwordgalleryattempt" method="post"> <input type="hidden" name="id" value="#id#"> <table> <tr> <td>password:</td> <td><input type="password" name="password"></td> </tr> <tr> <td> </td> <td><input type="submit" name="auth" value="Authenticate"></td> </tr> </table> </form> </p> </cfoutput> Nothing too special here. I basically copied the dspLogon.cfm view and modified it to only prompt for a password. I won't have an array of errors this time, but simply a flag saying my logon was bad. Notice how I use the default value argument to getValue. I'm also grabbing the ID from the viewState. This was the ID of the gallery that I'm trying to view. Now let's add the passwordgalleryattempt event to the config: <event-handler name="PasswordGalleryAttempt"> <broadcasts> <message name="authenticateGallery" /> </broadcasts> <results> <result name="galleryAuthenticated" do="ViewGallery" redirect="yes" append="id"/> <result name="notGalleryAuthenticated" do="PasswordGallery" redirect="yes"/> </results> </event-handler> This acts much like the Logon event. Broadcast a message - and either get a good or bad response. If it is good, then go ahead and view the gallery. Otherwise, prompt for the password again. The event broadcasts authenticateGallery which I need to add to my controller block: <message-listener message="authenticateGallery" function="authenticateGallery" /> And of course then add to the controller: <cffunction name="authenticateGallery" access="public" returnType="void" output="false"> <cfargument name="event" type="ModelGlue.Core.Event" required="true"> <cfset var password = arguments.event.getValue("password")> <cfset var id = arguments.event.getValue("id")> <cfset gallery = variables.galleryDAO.read(id)> <cfif gallery.getIsPassword() and gallery.getPassword() is password> <cfif not structKeyExists(session, "galleryauthenticationlist")> <cfset session.galleryauthenticationlist = ""> </cfif> <cfif not listFind(session.galleryauthenticationlist, id)> <cfset session.galleryauthenticationlist = listAppend(session.galleryauthenticationlist, id)> </cfif> <cfset arguments.event.addResult("galleryAuthenticated")> <cfelse> <cfset arguments.event.setValue("badLogon", true)> <cfset arguments.event.addResult("notGalleryAuthenticated")> </cfif> </cffunction> As with the user logon, I do the authentication (this time comparing the password against the gallery password), and if things work out, I update their session value that stores their gallery authentication list. If not, I set the flag for my view and add the proper result. Guess what - we are done! The application is definitely far from perfect. In my next entry I'll talk a lot about what I didn't do and what I should have done different. Consider this an open call to folks to rip me a new one. I've done that plenty of times in the contests, so I figure it's time for the community to strike back (grin). I am as flawless as Microsoft and I think people can learn from my mistakes as well as my advice, so those of you who have been holding back - just get ready for the next entry. As a reminder, you can view the application here: pg1.camdenfamily.com If folks want to share images, please post a comment along with the password (if password protected) or just the URL if public. Summary:
    • In this entry I finally added the security to the ViewGallery event.
    • The security needed to do different things depending on the access level of the gallery.
    • I use another session variable to store what galleries you are logged in to. This is important as I need to differentiate between various galleries that you can view with passwords. </ul>

      Download attached file.

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate. He focuses on JavaScript, serverless and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Comments