I'd like to propse the following (untested) code as how isApproved() and isGods() should behave. The idea is to make isApproved() more flexible by allowing it to take a string as the second argument, its adds a third argument to allow accessrules to operate on a "per-node" basis. It also sets up a return set of data in the $HTMLVARS{isApproved} that can be used by the caller to obtain information about why the user was not approved.

The most important thing is the per session caching of isGods() and isApproved() lookups. I've noticed that when you review the code these two routines get called many many times. Caching them per session will mean that this overhead is considerably reduced. Despite the way the caches look (HoH and HoHoH) I beleive that per session they will have relatively little data in them. And since %HTMLVARS is reset every session I dont beleive we have data synchornization problems or the like.

Anyway, comments appreciated. Also, i say the code is untested insofar as I havent actually put it into play and tested it on the pmdev server. It does compile, and seems to behave as expected in the dumper prompt and should be pretty close to bug free.

BTW, i was considering a fourth argument to isApproved(), a flag that would determine if gods should really always be approved. If the flag was true then the gods would only be approved if the gods were in the group, otherwise the behaviour would remain unchanged. IE when this flag was set the result would be a true membership/rule check and not implicitly put the gods in every group.

Update: Testing on the pmdev server indicates there is a flaw in the code below. I need to figure out what it is. /gah Ok, all fixed now. This code is running on the test server right now. Also there is some instrumnetation code in there to determine how often the cache is used, and how often the two subs are called. On the dev server there is code to see the results, and in my testing around 85% or more of the calls were resolvable from the session-cache. The sample total was 42 calls, with 36 in total resolved from cache. When you look to the two seperately, the numbers were 1:7 for isGods and 4:30 for isApproved. IMO this makes it clear this is an improvement. I have to say the feel of the site on pmdev seems to be more responsive too.

###################################################################### +####### # Sub # isApproved # # Purpose # Checks to see if the given user is approved within a given gro +up # # Parameters # $USER - reference to a user node hash (-1 if super user) # $GROUP - reference to a nodegroup that the user might be in # $NODE - optional reference to the item being tested against. # its prescence allows rules to be applied against the # the item. # $NotGod - If the last parameter is the string 'notgod' # then isApproved will not consider gods to # to be automatically approved. # # Returns # true if the user is authorized, false otherwise # # Uses %Everything::HTML::HTMLVARS as a per-session cache # of the results as well as populating # $Everything::HTML::HTMLVARS{isApproved} # with a hash of the results. This hash should be treated # as read only, and may contain the following keys: # group => the last $GROUP checked # user => the last $USER checked # node => the last $NODE # rule => if a rule was responsible for a failure # it will be provided here # error => if an error occured during processing # a rule the value will be here # ret => the actual return value (0/1) # # Note this cache is per session, which means on average # it will be for one $USER and most likely three or four # groups. It wont be large. sub isApproved { my( $this, $USER, $GROUP, $NODE, $notgod )= @_; ($notgod,$NODE)=($NODE,$notgod) if $NODE eq 'notgod'; my $vars=\%Everything::HTML::HTMLVARS; $vars->{isApproved_calls}++; my $cachekey = join "|", map { ref $_ ? $_->{node_id} : $_ } $USER,$GROUP,$NODE?$NODE:0,$notgod?1:0 ; if (my $hash=$vars->{'isApproved.Lookup'}{$cachekey}) { $vars->{isApproved_calls_cachedret}++; $vars->{isApproved}=$hash; return $hash->{ret}; } else { $vars = $vars->{'isApproved.Lookup'}{$cachekey} = $vars->{isApproved} = {}; } $vars->{user}=$USER; $vars->{group}=$GROUP; $vars->{node}=$NODE; $vars->{notgod}=$notgod; return $vars->{isApproved}=0 if ! $USER || ! $GROUP; if ( !ref $GROUP ) { if ($GROUP=~/^\d+$/) { $GROUP=$this->getNodeById( $GROUP ); } else { my ($name,$type)=split /\s*\|\s*/,$GROUP; return $vars->{isApproved}=0 if ! $name; if ($type) { $GROUP=$this->getNode($name,$type); } else { $GROUP=undef; } $GROUP||=$this->getNode($name,'usergroup'); $GROUP||=$this->getNode($name,'accessrule'); } } return $vars->{isApproved}=0 if ! $GROUP; # We always say that gods are approved, # regardless of their true membership. # Is this really the right thing to do? # It maintains legacy behaviour, but its unclear # if this really is the best idea. (demerphq) return $vars->{ret}=1 if !$notgod && $this->isGod($USER); my $user_id = $this->getId($USER); #You're always approved if it's yourself... return $vars->{ret} = 1 if $user_id == $this->getId($GROUP); foreach my $node ( @{ $this->selectNodegroupFlat($GROUP) } ) { return $vars->{ret} = 1 if $user_id == $this->getId($node); if( $node->{type}{title} =~ /accessrule$/i ) { my $RULE= $node; my $res= do { package Everything::HTML; eval $node->{code}; }; if ($@ or (!$res and defined $res)) { $vars->{isApproved}{rule} = $node; } if( $@ ) { Everything::printLog( "Access Rule eval error in $node->{node_id}\n" . "$@\n$node->{code}" ); $vars->{error}=$@; return $vars->{ret} = 0; } return $vars->{ret}= $res if defined $res; } } return $vars->{ret} = 0; } ###################################################################### +####### # Sub # isGod # # Purpose # Checks to see if a user is in the gods group. This includes r +oot # and '-1' as gods. This also checks sub groups so you can have # other usergroups in the gods group. # # Parameters # $USER - an id or HASH ref to a user node. # # Returns # 1 if the user is a god, 0 otherwise # # Uses %Everything::HTML::HTMLVARS as a per-session cache # sub isGod { my ($this, $USER) = @_; my $user_id; my $usergroup; my $GODS; my $godsgroup; my $god; # he's my god too... return 1 if($USER == -1); $this->getRef($USER); my $vars=\%Everything::HTML::HTMLVARS; $vars->{isGod_calls}++; if (defined $vars->{'isGod.Lookup'}{$USER->{node_id}}) { $vars->{isGod_calls_cachedret}++; return $vars->{'isGod.Lookup'}{$USER->{node_id}} } $user_id = $this->getId($USER); $usergroup = $this->getType("usergroup"); ($GODS) = $this->getNode("gods", $usergroup); $godsgroup = $$GODS{group}; #$this->selectNodegroupFlat($GODS); my $is_god=0; foreach $god (@$godsgroup) { if ($user_id == $this->getId($god)) { $is_god=1; last; } } $vars->{'isGod.Lookup'}{$USER->{node_id}}=$is_god; return $is_god; }
---
$world=~s/war/peace/g

  • Comment on Proposed new behaviour of isGods and isApproved (per session cache)
  • Download Code

Replies are listed 'Best First'.
Re: Proposed new behaviour of isGods and isApproved (per session cache)
by demerphq (Chancellor) on Aug 27, 2005 at 14:01 UTC

    This is the full patch between dev and pm right now, with the exception that the instrumentation code is commented out in this patch.

    ---
    $world=~s/war/peace/g

      Since we currently have no accessrules that use $NODE, I'd leave $NODE out of the key. If we come up with accessrules that use $NODE, I'd have them set a flag saying to not cache the results.

      That should make the cache both smaller and more effective. A hybrid approach would probably be possible (but, since the only specific use for $NODE that I recall hearing isn't needed, I'm expecting uses of $NODE to be exception and probably not much worth caching, so plans to use $NODE extensively would change that).

      But more numbers would be interesting.

      With the cache only per-page-hit, the size should usually remain small... But I worry that the-thread-not-to-linked-to or some other untested case could balloon things up.

      Update: I got the modified patch to apply. So I've got new NodeBase.pm and HTML.pm queued up to be deployed but it's past bed-time so the deploy will wait 'til I can stick around.

      - tye        

        Well, id like to see all of the isJanitorable and CanSdcEdit and nodes of that sort change into accessrules. And in that case I'd expect to see dupe checks. For instance In the SDC nodelet youd do the check once, and then if you were on an SDC page you'd do it again. Im doubtful if we would see multiple $NODE's for the same $USER/$GROUP in a single fetch, but id be unsurprised if we saw multiple fetch for the same USER/GROUP/NODE combination. So from my POV we lose very little by including NODE in the cache and will most likely save ourself a dupe check or two.

        Anyway, to explain what i have in mind ill say that id like to be able to do stuff like:

        if (isApproved($USER,'sitedocclan',$NODE) { ... }

        Which would check that the user is a member of sitedocclan and that $NODE is a type editable by SiteDocClan. This would be done by putting an accessrule as the first member of the group whose code only kicked in if the $NODE parameter was defined.

        Anyway, I guess my point is that id rather cache it all and see if there are issues with the cache than not cache it and later on repatch the code to return it to the current state.

        Also id be very surprised if nodes like the one you mention even excersize this aspect of the design. I mean their access profile is quite easy to manage.

        ---
        $world=~s/war/peace/g

Re: Proposed new behaviour of isGods and isApproved (per session cache) (updated patch)
by demerphq (Chancellor) on Sep 11, 2005 at 17:10 UTC

    The following is an updated version of the isApproved/isGods caching code. It no longer caches on the third parameter. It also includes improvements in how the accessrules are handled, with a couple of things tweaked to make things easier, including having Everything::Experience be required, and making sure $USER is actually a user. Ive used it to create CanConsider as it should be written, on the pmdev server.

    Updated non longer caches accesrule lookups.

    ---
    $world=~s/war/peace/g

      This is now merged into my changes and is live on 'production'. Please grab a copy (Everything/NodeBase.pm) for the test server and let me know if you hate any of my changes.

      I used the %HTMLVARS differently so all isApproved stuff is under the {isApproved} key so custom failure report code should use $HTMLVARS{isApproved}{last}. I only put node IDs not node hashes in, so dumping it is easy to read.

      Thanks.

      - tye        

        It looks ok. The only problem is when you simplified the isGod() code you used '=' and not '||=' in line 2191 so its resetting its state every time. I changed it from

        $vars= $vars->{isGod}= {};

        to

        $vars= ( $vars->{isGod} ||= {} );

        and it seems to be fine. The isApproved stuff OTOH appears to be problem free.

        Thanks for applying the patches. :-)

        ---
        $world=~s/war/peace/g

Re: Proposed new behaviour of isGods and isApproved (per session cache)
by ysth (Canon) on Sep 02, 2005 at 10:01 UTC
     $NODE  - optional reference to the item being tested against.
    I don't understand what this is. My concept of what isApproved is for only requires the existing two parameters. Can you demonstrate a use that wouldn't be better served by a separate subroutine?

      Can you demonstrate a use that wouldn't be better served by a separate subroutine?

      Well thats a good question, and maybe is actually the right way to go.

      The idea is that isApproved can handle all of our access control requirements. For instance a typical scenario is something like

      if ($validtype{$NODE->{type}{title}} and $DB->isApproved($USER,getnode('sitedocclan','usergroup')) ) { .... }

      Which to me should be simply a call like

      if ($DB->isApproved($USER,'sitedoclan_editable',$NODE)) { ... }

      where 'sitedoclan_editable' would be a user group containing an accessrule and sitedocclan. The accessrule would enforce the node level logic, by returning 0 is the $NODE's type was not appropriate, and by returning undef if the node was of the appropriate type it would allow the usergroup logic to make the final decision.

      Now you might be thinking ok, well then why dont we have a routine that does this without changing the behaviour of isApproved. The answer I would offer would be that to provide the flexibility and power available from the proposed isApproved() behaviour you would essentially have to recreate the same scheme anyway, so why not just unify it in the first place.

      The trick is to stop thinking that isApproved is about doing membership checks, but rather to think of isApproved as being an access control mechanism, ie SDC not being able to edit superdocs is basically the same thing as a non SDCer not being able to edit sitedoclets.

      The other nice advantage of knowing the $NODE is that it makes it possible to come up with a framework for automatically providing the appropriate code for rejecting a user.

      ---
      $world=~s/war/peace/g