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.