Access rights in WebSnap

By: Nick Hodges

Abstract: This article shows how to limit access to specific pages based on a user's access rights. By Nick Hodges.

When last we left our trusty WebSnap application, we were able to save user session information in a database and make the data persistent. Users were able to log in, and we could ensure that they saw only those pages that logged-in users were supposed to see. Not bad for a dozen lines of code or so.

But what if you have content that shouldn't be available to all of your logged-in users? Perhaps some of your registered users get premium, extra-double-special content that not just anyone gets to see?

That is where Access Rights come into play.

Access rights are given to logged-in users granting them specific privileges to see specific content. If you have worked through the code in my previous articles, you may have noticed the AccessRights property of the TWebUserList component. So far we have left that blank, but in a minute we are going to fill it in and grant specific access to specific users. 

As always, the code for this article is available on CodeCentral. This article assumes that you have downloaded this code, or at least the code from the previous article. This article also assumes that you understand the basics of WebSnap: adding pages, viewing a page's HTML, and so on.

GETTING STARTED

First, add two pages to the project. Call one AccessRightsPage and call the other SorryPage. On the access rights page, add some HTML in the body that declares how lucky users are to be granted access to this very secret page. On the SorryPage, add a note chastising them for daring to attempt access to the secret part of the site. You can take a look at the sample application to see how to do this. Save the units as wmAccessRights.pas and wmSorry.pas.

Now we'll give the AccessRightsPage unit a value that shows that the page can be viewed and seen only by users with the proper string in their AccessRights property. This is done in a rather subtle way.

As you may have noticed, each unit containing a WebSnap page has in its initialization section something that looks like this:

initialization
  if WebRequestHandler <> nil then
    WebRequestHandler.AddWebModuleFactory(TWebAppPageModuleFactory.Create(THome, 
      TWebPageInfo.Create([wpPublished { , wpLoginRequired}], '.html'), caCache));

This call is the code that creates the webpagemodule and registers it with WebSnap's application object. Notice that nestled in the Factory constructor is the creation of another object, TWebPageInfo. This object holds all of the basic information about a Web page. The constructor looks fairly simple, but is more complicated than it looks because the Create call has a number of default parameters. Here's the declaration of TWebPageInfo.Create:

  TWebPageInfo = class(TBaseWebPageInfo)
  public
    constructor Create(
      AAccess: TWebPageAccess = [wpPublished];
      const APageFile: string = '.html'; const APageName: string = '';
      const ACaption: string = ''; const ADescription: string = '';
      const AViewAccess: string = '');
  end;

Compare the constructor with the code used to call it, and you see that a bunch of default parameters are left blank. These values can contain valuable information if you choose to fill them in.

The one we are interested in is the last one, the AViewAccess parameter. Go to your AccessRightsPage and make the initialization section look this:

initialization
  if WebRequestHandler <> nil then
    // Alter the TWebPageInfo constructor to hold the AccessRights value, 
    // the last parameter.  This isn't how the wizard does it -- 
    // it actually  doesn't even give you a chance to fill in 
    // the blank parameters below.  
    WebRequestHandler.AddWebModuleFactory(TWebPageModuleFactory.Create(TAccessRightsPage, 
      TWebPageInfo.Create([wpPublished, wpLoginRequired], '.html', 
      '', '', '', 'ViewAccessRightsPage'), crOnDemand, caCache));

Notice that the code leaves three parameters blank and fills in the fourth with a value: ViewAccessRightsPage. When using default values, you have to fill in all of them before the last parameter that you provide. In this case, since the one we want is the last one, we have to fill them all in. (Note to self: Write these guys an article about the rest of the parameters and how to use them) Filling in the AViewAccess parameter gives you a value that you can check against. If the user doesn't have that value as part of her AccessRights, then she doesn't get to see the page. And as you'll see in a little bit, we can make it so she won't even know the page exists.

Next we have to authorize a user to view the page. We do that by going to the Home page and opening the WebUserList.UserItems property editor. Select "nick" and set his AccessRights property to ViewAccessRightsPage. Note that this is the same value passed to the page in the code above. Leave Pamela's blank and set Calvin's to "Not Allowed." This will allow us to test for people with the correct AccessRights, those with no AccessRights, and those with the wrong AccessRights.

Now we have a page that allows only authorized users access. Plus a user whose access rights match up, and users whose rights don't.

SORTING THE USERS

Next, of course, we have to check for all of these cases, and that involves a little code. Interestingly, all of it is written as event handlers for the components on the Home page.

The first thing we need to be able to do is to quickly get at the current user's AccessRights property. The easiest way to do that is to make the value a field on the EndUserSessionAdapter. Go to that component on the home page and right click on it. Select "Fields Editor..." and add a simple AdapterField. Call it UserRightsField. Then go to the Object Inspector and make the field's OnGetValue event handler look like this:

procedure THome.UserRightsFieldGetValue(Sender: TObject; var Value: Variant);
var
  WebUserItem: TWebUserItem;
begin
  Value := ''; // Assume there is no value
  if not VarIsEmpty(EndUserSessionAdapter.UserID) then  // Don't try to access an empty variant
  begin
    WebUserItem := WebUserList.UserItems.FindUserID(EndUserSessionAdapter.UserId);   // Get the data for the given user
    if WebUserItem <> nil then
      Value := WebUserItem.AccessRights;  // Grab that user's AccessRights
  end;
end;

This code simply finds the current user in the WebUserList component and grabs the value for the user's AccessRights property. It does this in a "safe" way by making sure that the pertinent values actually exist.

Once we know we can always get the values, we need to compare the user's rights with the rights required by the page. Pages are requested and dispatched via the PageDispatcher component, and it has an OnCanViewPage event. Go to that page, and make the event handler look like this:

procedure THome.PageDispatcherCanViewPage(Sender: TObject;
  const PageName: String; var CanView, AHandled: Boolean);
var
  aPageInfo: TAbstractWebPageInfo;
  UserRights,
  PageRights: String;
begin
  CanView := False;  // Assume that you can't view the page
  UserRights := UserRightsField.Value;  // Get the user's AccessRights string from the EndUserSessionAdapter
  // Returns True if the page is found, False otherwise
  if WebContext.FindPageInfo(PageName, [fpLoginRequired], aPageinfo) then 
  begin
    PageRights := aPageInfo.ViewAccess; // Get the value as set in the constructor
  end else
  begin
    PageRights := '';
  end;
  // You can view the page if your PageRights string is part or all of your 
  // AccessRights, or if they both are empty strings
  CanView := (Pos(PageRights, UserRights) >= 0);
  aHandled := not CanView;
end;

This code first sets the value for the local variable UserRights by calling the Adapter we created. Then it looks up the PageInfo for the page being dispatched, and if it finds that information, grabs the ViewAccess value for that page. This is the string value passed in the TWebPageInfo constructor that we modified in the initialization section. If the PageInfo is not found, the string is set to an empty value. Then, of course, it is a simple matter of checking to see if the UserAccess variable contains the value held in PageRights.

The user's rights string can be in any format that you like. The fact that all the AccessRights values are strings means that you can pretty much use any scheme you like to manage them. WebSnap expects that the value be a string, or a series of strings separated by a semicolon, a comma, or a space, and will parse these values into a TStrings value for you at different times during a page request.

The procedure then sets CanView to True if the user's rights match those of the page.

FINAL TOUCHES

That's all well and good, but if you run the application, log in without rights, and try to go to the AccessRightsPage, you'll get an ugly error message that is quite unappealing.

Let's not do this to our users. Instead, let's steer them to the SorryPage, where we can chastise them for trying to get to content they aren't allowed to see. That is done in the PageDispatcher's OnPageAccessDenied event. Make the event handler look like this:

procedure THome.PageDispatcherPageAccessDenied(Sender: TObject;
  const PageName: String; Reason: TPageAccessDenied; var Handled: Boolean);
begin
  if Reason = adCantView then // do something only if we are here because the user is denied access
  begin
    DispatchPageName(sSorryPageName, Response, []); // Show the sorry page
    Handled := True; // Quit dealing with this event after this
  end;
end;

This code should be pretty self-explanatory. It merely sends users to the SorryPage when they are denied access to a page.

Now that's all well and good...but why should the user be allowed to see a link to the page at all if he isn't allowed access to it? Good question, and I'm glad you asked. Let's do something about that. We'll do that in the HTML of each page. Yes, we are actually going to write some JScript code! Exciting, huh?

The only downer is that we have to open up all the HTML files for each page to make the change. (Note to self: Write these guys an article on how to add custom HTML templates to the New WebSnap Page Wizard.) Thus, for each of the HTML pages associated with each of the webmodules, change line 32 from this:

if ((e.item().Published)

to this:

if ((e.item().Published) && (e.item().CanView))

Notice that this script will call the code that you wrote in the PageDispatcher -- a perfect example of the power of WebSnap. You can write code and objects in Delphi that can be used seamlessly in your server-side script.

Now, when you run the application, the AccessRightsPage link in the standard menu will not appear unless you are logged in and you have the right to view the page.

That ought to do it. Now you can easily limit access to any pages that you add to this application buy setting the page's AccessRights property and the user's access rights.

Note that the solution given here is not page-specific -- it doesn't limit access to the AccessRightsPage only, but to any page in the application that has a value set in its ViewAccess property.

This is just the first glance into access rights. WebSnap can give you even finer control over what users are allowed to see. Later, we'll take a look at how you can limit access to specific fields, or limit a user's ability to access or edit data.

Nick Hodges is Top Dog at HardThink Inc., a consulting shop specializing in Delphi Development. He is a TeamB member and hasn't had a drop of coffee in his whole life. But mostly he like to hang with his family and enjoy their new home in St. Paul, MN.


Server Response from: ETNASC03