Golo has asked for the wisdom of the Perl Monks concerning the following question:

Dear Monks,

I really like the idea of CGI::Application, but a problem I encountered several times was about a good aproach on how to provide a userbased access to certain funtions.

As simple example take a guestbook class. One might have the run modes for viewing and posting, but what about the admin part, like viewing the posters IP's and deleting bad entries?

My thoughts:

1. The module should be reusable as a subclass in larger projects.

2. I don't want to include any kind of authentication, as I feel this would cause problems with point 1. Also I think the underlying webserver and OS can do a better job on that anyways.

3. Subclassing the admin part seems not to be the best solution to me, as a Super-class should not know anything on it's childs. And how could I then do things like extending the view part for showing the IP's as well only when in "admin-mode"?


What I've come up with so far:
In the guestbook class...
package Guestbook; use base 'CGI::Application'; use strict; # define all run modes here to allow different sets, # the actual access control should then be enforced using # multiple instance scripts, which in turn are placed in a # secure area and only enable the desired set(s) sub _RUN_MODES { DEFAULT => { view => 'view_entries', sign => 'sign_guestbook' }, RESTRICTED => { delete => 'delete_entry' } } sub enable_rm_set { my $self = shift; my %run_modes = _RUN_MODES; foreach (@_) { $self->run_modes( %{$run_modes{$_}} ); #ensure that info on loaded sets is avaible $self->param($_ => '1'); } } sub setup { my $self = shift; $self->enable_rm_set('DEFAULT'); $self->start_mode('view'); } sub view_entries { my $self = shift; my ($ip, $del_link); # are we in "admin mode"? if ($self->param('RESTRICTED')) { $ip = "with IP's"; $del_link = '<a href="?rm=delete">+ del option</a>'; } return "showing guesbook entries $ip"; } sub sign_guestbook { my $self = shift; return "sign guestbook"; } # restricted sub delete_entry { my $self = shift; # bail out if authentification did not happen # (of course this check should be during setup ;) return _no_auth() unless defined($ENV{UserName}); return "delete entry"; } sub _no_auth { return "oops, authentification did not happen"; } 1;

In the admin instance-script...

which will be placed into a separated and secured area (e.g. /admin/guestbook.pl)
use Guestbook; my $gb = Guestbook->new(); # enable admin mode $gb->enable_rm_set('RESTRICTED'); $gb->run();

In the normal instance-script...

which will be placed into the public area (e.g. /guestbook.pl)
use Guestbook; my $gb = Guestbook->new(); $gb->run();

What do you think, is this a good way to go?

Thanks a lot in advance!
Golo

Updates:

  • added some more code to make my intention more clear
  • please see Re: Re: CGI::Application with access control on certain functions/run modes for a more comprehensive explanation why splitting into multiple classes might not be desirable in this case
  • added a more complex scenario in Re: Re: CGI::Application with access control on certain functions/run modes
  • Replies are listed 'Best First'.
    Re: CGI::Application with access control on certain functions/run modes
    by tilly (Archbishop) on Mar 28, 2004 at 07:31 UTC
      One approach is to nest CGI::Applications within each other. Then put authorization on the individual C::As. See Re: Why CGI::Application? for an outline of this approach.

      In general I'd suggest that scattering authorization logic around is somewhat fragile, and will make it hard to determine who can do what, and that someone who shouldn't be able to X really has no way to get that privilege. A missed authorization step is very easy to overlook both while coding and testing. So try to make that step very rarely.

      Note that I'm drawing a critical but subtle distinction between authentication and authorization. When you authenticate someone you are determining that they are who they say they are. There are multiple ways that you can do that (cookies, Basic, Digest, etc). This is orthogonal to how you authorize that person to have permission to do critical things.

      In your design, assume that there is a standard way of knowing who someone claims to be. In your authorization logic you don't have to care what that is, just that it must be available in some specified and convenient way. When you set up authentication, make sure that that is satisfied. (This can be as simple as turning on Digest authentication and having the webserver put the user name in an environment variable for you.)

        "One approach is to nest CGI::Applications within each other. Then put authorization on the individual C::As"
        I see that this would be the prefered way when your individual C::As implement a functionality which requires new run-modes anyways or you don't mind overwritting/rewriting some runmode with altered functionality. But that's exactly where my problem is:

        In my guestbook example I want to come around having to overwrite or rewrite the view method, just to add that extra piece of information (showing the IP of the poster, and maybe adding a link to the "delete" run-mode). I don't see a way of accomplishing that with creating an additional C::A (but maybe I shouldn't be that lazy?)

        Besides from that other C::A part my strategy in regards to authentification and authorization goes a very similar way as described in Re: Why CGI::Application?.
        Instead of having the intance script calling another C::A, my instance script enables the accessible run-modes via the enable_rm_set('X', 'Y', 'Z') method. If a user tries to call one of the restricted run modes (rm=delete in this example), he run's into the same error as when calling it from a different C::A. In both cases, the C::A does not know the called run mode.
        "In general I'd suggest that scattering authorization logic around is somewhat fragile"
        Agreed, but in a case like this the only required steps is to check if a privileged "run-mode set" has been loaded and if authentification has been performed.

        The first is set in the enable_rm_set() by setting $self->param("run-mode set" => '1').

        The second is done only in the "delete" run-mode (out of laziness, to spare me copying the admin.pl to a secured and unsecured area on the webserver, and having to type the different links). It really should be placed into the setup() like this:
        if ($self->param('RESTRICTED')) { return _no_auth() unless defined($ENV{UserName}); }
        This should catch configuration mistakes in regards to authorization, e.g. if one forgot to enable access control to the admin.pl. This is following the logic of "if there was no authentification there can't be authorization, as we know it should be restricted in someway (not caring about the specific groups/users to be allowed)".

        The only "scattering of authorization logic" happens in the view run mode when adding those extra bits of information for the admin mode:
        if ($self->param('RESTRICTED')) { # add the infos for admins only }

        But that's the goal I am going for in this case.

        In the end it seems to come down to:

        Is there a way of achieving this (not having to overwrite/rewrite the view for that extra bit) without that bit of "authorization logic scattering"?

        If not, is this prize to pay a fair one or shall I better forget about the first point at all?
          I have a User object, which has all the authorization logic built into it. (It also has the authentication logic as well, but that's a separate concern.)

          Basically, setup() creates a User object and passes in the value of a cookie. This value can be anything from a userid (insecure) to a session id (secure). The User object knows how to authenticate that cookie value. At that point, I can then look that authenticated user up in some datastore and determine what privileges (or authorities) that user has. Then, a User object is returned. setup() then stores it as a param() of the C::A object.

          Then, when you need to determine if a given User has an authority, you ask the User object. It's that simple.

          This design principle is called "Separation of Concerns". Basically, it boils down to "Only those who care should know", or "Who cares?"

          ------
          We are the carpenters and bricklayers of the Information Age.

          Then there are Damian modules.... *sigh* ... that's not about being less-lazy -- that's about being on some really good drugs -- you know, there is no spoon. - flyingmoose

    Re: CGI::Application with access control on certain functions/run modes
    by simon.proctor (Vicar) on Mar 28, 2004 at 14:20 UTC
      You could extend your idea by simply attaching a privilege set to the user. You set up your privileges in your pre-run logic.

      Whenever someone hits a run time that requires some privilege have it test for it. If it fails it can redirect to a login screen. Someone who fails a login or does not have one can simply be treated as an anonymous user.

      It would be easy tie this with CGI::Session and encode all the required url parameters, redirect referers (etc) as well as the user details in that.

      However, I prefer to separate admin and user functionality. So while you may have users who may do things (requiring privileges) they aren't really administrators of your site (unless your app calls for that). Most of the apps I write have this functionality in a separate program which is locked down right from the start and all attempts at use require a login via the pre_run function. In your case, I would do that.

      Hope thats a useful slant on what you have.
        "You could extend your idea by simply attaching a privilege set to the user. You set up your privileges in your pre-run logic. Whenever someone hits a run time that requires some privilege have it test for it. ..."
        In my real-world project that is exactly my intention :-)
        "It would be easy tie this with CGI::Session ..."
        But instead of using sessions and managing the privilege sets myself, I plan to rely on the webservers and OS's access control mechanism. It's going to run under NT with IIS, as one of the requirements is to use existing NT accounts.
        "However, I prefer to separate admin and user functionality. So while you may have users who may do things (requiring privileges) ..."
        You hit my intentions again, in my real-life project I need user functionality requiring privileges (thanks for the good wording :). The project is ment to demonstate a web-app which fully supports the required workflow/process (and thus eleminating a lot of manual work). It's about a "anonymous" user submitting a request, which will need to be linked to the existing helpdesk system, then to be approved (or rejected) and finally implemented whislt keeping the original requestor updated on progess.

        So I have to separate funtionality for at least these groups: requestors, approvers, implementors as well as auditors and administration. The last two groups are my candidates to get completly separated, while the other groups basically have the same run_modes plus some group depending add-ons (like the "approve/reject/implement" buttons).

        I plan to disable anonymous access to all priviledged instance scripts for the authentification and then use file system permissions to handle the authorization.
        The guestbook example was more the try to cut the problem down to the essential part (adding those small bits and pieces without having to write additional run modes).
        "Hope thats a useful slant on what you have."
        It definatly is!