Flux: Rule-based Authorisation for WebGUI

Posted on July 7th, 2008 in Perl by Patrick

Recently I've been plugging away at an experimental "WebGUI_flux" branch of the awesome Perl based content management system / web application framework WebGUI that wG founder JT created for me a month or so ago. I'm happy to say that the code has progressed to the point where I'm ready to announce Flux to the world and invite anyone who is interested to have a play with the demo server, explore the API, give me feedback and if you like the look of it, contribute some plugins of your own.

Here's the original impetus for building Flux:

As per most content management systems, WebGUI's built-in authorization mechanism is based on User Group membership. This is perfectly adequate for a large class of websites: content managers define one or more User Groups and then set the "Who Can View" and "Who Can Edit" Security Tab options on their Wobjects to the appropriate group. However in one important aspect such sites are static in nature: the set of pages that a user can access is fixed according to what groups they belong to. Authorization can be made dynamic by manipulating group membership (manually, which isn't really dynamic, or via custom code) or by writing custom Wobject authorization logic. Both of these approaches work, however, custom code leaves content managers out of the loop.

You can find the latest version of the design doc in SVN in pdf and odt formats. Flux is quite large in scope and hard to condense into a single post, so please refer to the design doc if you want more details, but I'll try to give a quick summary here.

For content managers:

  • Flux is a rule-based authorization layer for WebGUI content managers. What that means is that you can define Rules (using a GUI) that define the conditions that must be met for a user to be given access to an Asset. Each time a user tries to access an Asset, Flux evaluates the Rule(s) you have defined to see if the user should be granted access.
  • By default Flux is disabled, meaning zero performance hit and no change to the UI.
  • You can enable/disable Flux at the site-wide level (via the Flux Admin Console), and also on individual Assets (via the asset's Security Tab).
  • With Flux turned on for an Asset, the Security Tab shows a combo box containing all Flux Rules that you have defined. These combo boxes appear for each action your Asset supports (e.g. "Who Can View", "Who Can Edit", etc..). If you pick a Rule for an action, authorization logic for that action is delegated to that particular Flux Rule.
  • Rules are powerful, flexible, and pluggable.
  • You define Rules via a simple, intuitive step-by-step wizard in the Flux Admin Console.
  • Rules can depend on static information, user-specific information, time-dependent information, asset-specific information, and anything else we plug into the framework.
  • Rules can also depend on other Rules, meaning that you can construct an arbitrarily complex graph of connected Rules. Flux makes sure that infinite loops don't occur, and can even dynamically generate a visualisation of your Flux Graph showing all interrelationship between Rules
That's all content managers need to know to drive Flux. The interface is designed to be a minimal knowledge "monkey-see, monkey do" type affair.

For developers:

  • Rules are made up of one or more Expressions
  • Each Expression is comprised of an Operator and two Operands.
  • The UI guides content managers through the process of picking these, displaying helpful messages and prompting for any additional information required along the way
  • The following Operators are built:
    • IsEqualTo, IsNotEqualTo
    • LessThan, LessThenOrEqualTo, GreaterThan, GreaterThanOrEqualTo
    • MatchesPartialText, DoesNotMatchPartialText
  • The following Operands have been built:
    • TextValue - allows the content manager to enter a simple text value
    • NumericValue - allows the content manager to enter a numeric value
    • TruthValue - allows the content manager to enter yes/no
    • DateTime - allows the content manager to enter a date from a date-picker
    • Group - allows the content manager to choose a group from the wG Group combo box. The Operand evaluates to true/false depending on whether the user being tested against belongs to that group
    • UserProfileField - allows the content manager to specify the name of a User Profile field. The Operand evaluates to the value of that user profile field for the user being tested against.
    • FluxRule - allows the content manager to choose another Flux Rule from a combo box. The Operand evaluates to true/false depending on the result of testing that Flux Rule against the user.
  • Operands can prompt the user for an arbitrary number of extra arguments as part of the step-by-step wizard. These arguments are stored as a JSON-encoded string.
  • Modifiers can be registered against Operand return types, causing the UI to prompt the content manager for extra information when an Operand is chosen during Expression building. For example, the following two Modifiers have been built and registered against the DateTime type:
    • DateTimeFormat - prompts the content manager for date format patterns (as per strftime()) so that the DateTime object can be formatted as a string. Also prompts the content manager for a timezone to use (any valid timezone can be specified, or 'user' which indicates that the timezone of the user being tested against should be used). This is useful so that you can e.g. compare a DateTime object to a string such as "Monday".
    • DateTimeCompareToNow - prompts the content manager for a timezone and a Duration unit to use (e.g. 'hours', 'days', etc..). This allows you to construct Rules that evaluate to true a certain number of days since a particular date etc..
  • By default, multiple Expressions are ANDed together. But if you like you can specify an arbitrarily complex boolean logical expression instead (using AND, OR, NOT and parenthesis).

I've included a contrived example, which might makes things clearer. It might also make things more confusing since you don't have the nice UI to guide you through the process of building the Expressions, but see how you go..

Imagine you have a WebGUI site with some content that you want to show to your French users only.

  • You create a Rule called "French Only" and then add an Expression to it that has:
    • operand1: UserProfileField
    • operand1Args: {field: homeCountry} # note that this is JSON-encoded
    • operator: IsEqualTo
    • operand2: TextValue
    • operand2Args: {value: "France"}
  • You then enable Flux on the Security Tab for that content and choose the "French Only" Rule from the drop-down list for "Who Can View"… voila!

Now say you've been using good old-fashioned Group membership on your site. Let's assume you have a "Premium Members" Group. On a whim, you decide your content should be restricted to French Premium Members:

  • You add a second Expression to the "French Only" Rule to require the Premium Members group:
    • operand1: Group
    • operand1Args: {groupId: <id of Premium Members group as chosen from group combo box>}
    • operator: IsEqualTo
    • operand2: TruthValue
    • operand2Args: {value: 1}

A few weeks later, you decide to add an extra page that French Premium Members can see on their birthday.

  • You create a new Rule called "Bon Anniversaire" and add an Expression linking to your earlier "French Only" Rule:
    • operand1: FluxRule
    • operand1Args: {fluxRuleId: <id of the "French Only" Flux Rule as chosen from a combo box>}
    • operator: IsEqualTo
    • operand2: TruthValue
    • operand2Args: {value: 1}
  • ..and a second Expression requiring that today be the user's birthday (in their time zone of course):
    • operand1: UserProfileField
    • operand1Args: {field: birthdate}
    • operand1Modifier: DateTimeCompareToNow
    • operand1ModifierArgs: {units: "days", time_zone: "user"} # Modifier args are JSON-encoded too
    • operator: IsEqualTo
    • operand2: NumericValue
    • operand2Args: {value: 0}

Cool huh?

Hopefully that gives you a feel for the power of the framework. Be sure to check out the design docs for more details (did I mention it also supports Workflow triggers?). The test suite (in t/Flux) is pretty comprehensive (currently at >85% coverage) - the Operand-specific tests in particular should get you up to speed pretty quickly on how to drive Flux.

Test Coverage Report:

---------------------------- ------ ------ ------ ------ ------ ------ ------

File                           stmt   bran   cond    sub    pod   time  total

---------------------------- ------ ------ ------ ------ ------ ------ ------

lib/WebGUI/Flux.pm             88.1   71.9   50.0  100.0   75.0    2.4   82.3

...WebGUI/Flux/Expression.pm   93.3   85.7   60.0   89.5  100.0   17.2   89.1

...lux/Expression/Builder.pm  100.0    n/a    n/a  100.0    n/a    0.1  100.0

lib/WebGUI/Flux/Modifier.pm    84.9   61.9   44.4  100.0  100.0    2.9   77.0

...r/DateTimeCompareToNow.pm  100.0  100.0    n/a  100.0    0.0    0.2   92.6

...odifier/DateTimeFormat.pm  100.0   50.0    n/a  100.0    0.0    0.3   87.5

lib/WebGUI/Flux/Operand.pm     85.9   66.7   44.4  100.0  100.0   14.7   78.9

.../Flux/Operand/DateTime.pm  100.0    n/a    n/a  100.0    0.0    0.2   91.3

.../Flux/Operand/FluxRule.pm   97.1   83.3    n/a  100.0    0.0    1.3   90.0

...GUI/Flux/Operand/Group.pm  100.0    n/a    n/a  100.0    0.0    0.1   90.5

...x/Operand/NumericValue.pm  100.0    n/a    n/a  100.0    0.0    0.1   89.5

...Flux/Operand/TextValue.pm  100.0    n/a    n/a  100.0    0.0    0.6   89.5

...lux/Operand/TruthValue.pm  100.0    n/a    n/a  100.0    0.0    0.1   89.5

...erand/UserProfileField.pm  100.0    n/a    n/a  100.0    0.0    0.1   90.5

lib/WebGUI/Flux/Operator.pm    84.5   53.1   33.3  100.0  100.0   14.5   75.0

...oesNotMatchPartialText.pm  100.0    n/a    n/a  100.0    0.0    0.1   95.5

...lux/Operator/IsEqualTo.pm  100.0  100.0  100.0  100.0    0.0    1.8   96.6

...Operator/IsGreaterThan.pm  100.0  100.0  100.0  100.0    0.0    0.2   96.6

...IsGreaterThanOrEqualTo.pm  100.0  100.0  100.0  100.0    0.0    0.2   96.6

...ux/Operator/IsLessThan.pm  100.0  100.0   66.7  100.0    0.0    0.2   93.1

...or/IsLessThanOrEqualTo.pm  100.0  100.0  100.0  100.0    0.0    0.2   96.6

.../Operator/IsNotEqualTo.pm  100.0  100.0  100.0  100.0    0.0    0.2   96.6

...tor/MatchesPartialText.pm  100.0    n/a    n/a  100.0    0.0    0.1   95.5

lib/WebGUI/Flux/Rule.pm        94.9   88.9   84.4   93.3   93.8   42.4   92.3

Total                          93.1   75.7   70.2   97.8   53.2  100.0   87.4

---------------------------- ------ ------ ------ ------ ------ ------ ------

Implementation status:

  • You can do just about everything through the API (check out the test suite for more info)
  • You can write your own Operands and Operators as plugins. To give you an idea of how easy it is to add your own plugins, most of the current Operands require 1-3 lines of unique code. Operators are simple too.
  • The Flux Admin Console is working, although not ajaxified yet
  • Per-asset Flux authorization UI options are available on some Assets, although only tested on Article and PageLayout thus far and not pretty.
  • I've only implemented a simple CRUD interface for manipulating Rules and Expressions through the Admin console. The current interface is really just a simple slap-dash job put together so that people can play with the underlying framework without needing to write code. The Expression builder in particular is quite clunky to use - you need to pass in fully-formed JSON Operand arguments. The UI has inline documentation to help you do this. The design doc has mock-ups of what the finished UI will look like.
  • I haven't implemented Wobject-bound Rules, although I will soon - this is where Flux teams up with next generation WebGUI Wobjects such as Thingy and Survey 2.0 to do some really cool things (check the design doc).

I've set up a demo server where you can play with Flux. The demo sites come pre-bundled with some Rules to get you started: go the the Flux item in the Admin Console and click on Flux Graph to generate your version of the image attached to this email. As mentioned, you'll probably find the Expression builder pretty awkward since it nice step-by-step wizard isn't built yet.

You can also download the latest version of the code from SVN and run it yourself - be aware that visualisation of the Flux Graph is currently done using GraphViz so you will need to install it on your system using something similar to "apt-get install graphviz gsfonts" and "cpan GraphViz".

The big caveat for getting Flux included in a future version of WebGUI is performance. Obviously checking Flux Rules is more expensive than doing a single call to $user->isInGroup(). I've done quite a bit of work on benchmarking, profiling and optimisation, and so far things look good but there's still work to be done. I'll save that discussion for a later post.

I'm looking forward to your feedback. Anyone who wants to get involved please don't hesitate to drop me a line or ping me on #webgui on freenode. I'll also be at the 2008 WebGUI User Conference if you want to talk to me about Flux in person.

Getting lazy in Perl

Posted on October 1st, 2007 in Perl by Patrick

Maybe it's a sign I've been spending too much time playing with javascript lately, but when I revisited some code I'd written in Perl recently I suddenly realised that the tangled web of private object initialisers I was staring at would be so much simpler if I used lazy initialisation instead. Javascript makes lazy initialisation easy because its a functional programming language:

function lazy() {
        var lazyVar = "initialValue";
        lazy = function() {return lazyVar;};
        return lazyVar;
}
console.log("Before first call: " + lazy.toString());
console.log("First call returns: " + lazy());
console.log("After first call: " + lazy.toString());
console.log("Second call returns: " + lazy());

If you run the above in a javascript console such as Firebug, you'll see that after the first call which initialises lazyVar, the function is redefined to be the closure which eliminates the initialisation step on all subsequent calls.

So anyway, I started wondering how I'd achieve the same thing in Perl. It's easy enough to see how you'd cause the initialisation to be lazy (without actually redefining the subroutine to speed up subsequent calls):

sub lazySub {
        my $self = shift;
        my $priv = "_lazy";
        return $self->{$priv} if exists $self->{$priv};
       
        return $self->{$priv} = 'initialValue';
}

The above is a handy template, but things would be a lot easier to use if we encapulated the above into a subroutine of its own:

sub lazyInit(\&) {
        my $self = shift;
        my $priv = shift;
        my $f    = shift;
        return $self->{$priv} if exists $self->{$priv};
        return $self->{$priv} = &$f;
}
sub lazy {
        my $self = shift;
        return $self->lazyInit("_lazy", sub {
                return 'initialValue';
        });
}

Now that's looking better, we're passing in a string to use as the private identifier and an anonymous subroutine to use as the initialiser.

Now when I actually started using this, I realised that the private identifier that I was using was always the name of the subroutine (with an underscore prefix to indicate that it's private). So I eliminated the first argument altogether by using caller();

sub lazyInit(\&) {
        my $self = shift;
        my $f    = shift;
       
        my $priv = "_" . (split("::", (caller(1))[3]))[-1];
       
        return $self->{$priv} if exists $self->{$priv};
       
        return $self->{$priv} = &$f;
}
sub lazy {
        my $self = shift;
        return $self->lazyInit(
                sub {
                        return "initalValue";
                }
        );
}

That's all well and good, but what if we want to be able to overwrite the value? Or force the initialising function to be called again?

sub lazyInit(\&) {
        my ($self, $initialiser, %args) = @_;
       
        my $priv = "_" . (split("::", (caller(1))[3]))[-1];
               
                if (exists $args{overwrite}) {
                        return $self->{$priv} = $args{overwrite};
                } elsif (not exists $self->{$priv} or defined $args{forceReload}) {
                return $self->{$priv} = &$initialiser;
            } else {
                return $self->{$priv};
            }
}
sub lazy {
        my ($self, %args) = @_;
        return $self->lazyInit(
                sub {
                        return "lazyValue";
                }, %args
        );
}
$self->lazy; # get value
$self->lazy(overwrite => 'newLazyValue'); # overwrite value
$self->lazy(reload => 1); # cause initialisation subroutine to be called again, returning value to 'lazyValue'
 

And you're free to use other paramaters of your own chosing in the anonymous closure (just don't use 'overwrite' or 'forceReload' to avoid conflicts!)

Finally, if the value only needs to be read-only, we can redine the subroutine after its first use (yes you can do this in Perl!) so that subsequent calls don't require the unnecessary (if defined) test:

sub lazyInit(\&) {
        my $self   = shift;
        my $f      = shift;
        my $overrideName = shift;
       
        my $priv;
        my $method;
        if (defined $overrideName) {
                $priv = "_" . (split("::", $overrideName))[-1];
                $method = $overrideName;
        } else {
                $priv = "_" . (split("::", (caller(1))[3]))[-1];
                $method = (caller(1))[3];
        }
       
        #       $self->debug("slow - $method");
        $self->{$priv} = &$f;
        {
                no warnings 'redefine';
                no strict 'refs';
                *{ $method } = sub {
                        # $self->debug("fast - $method");
                        return $self->{$priv};
                };
        }
        return $self->{$priv};
}

If you uncomment the comments and run this, you will see that the first time it is run "slow - $method" is printed, but on all subsequent calls "fast - $method" is called. I've also added the ability to override the method name used, which actually lets you achieve read-write if you call lazyInit() from a different function and have it redifine a suboutine of your chosing.

Cool huh?

  • Comments Off

Perl, the first postmodern computer language

Posted on September 26th, 2007 in Perl by Patrick

"In Perl, AND has higher precedence than OR does. There you have it. That proves Perl is a postmodern language."  (link)

  • Comments Off