1use warnings; 2use strict; 3 4package Jifty::API; 5 6=head1 NAME 7 8Jifty::API - Manages and allow reflection on the Jifty::Actions that 9make up a Jifty application's API 10 11=head1 SYNOPSIS 12 13 # Find the full name of an action 14 my $class = Jifty->api->qualify('SomeAction'); 15 16 # New users cannot run some actions 17 if (Jifty->web->current_user->age < 18) { 18 Jifty->api->deny(qr/Vote|PurchaseTobacco/); 19 } 20 21 # Some users cannot even see some actions 22 if (Jifty->web->current_user->id > 10) { 23 Jifty->api->hide('Foo'); 24 Jifty->api->show('FooBar'); 25 Jifty->api->hide('FooBarDeleteTheWorld'); 26 } 27 28 # Fetch the class names of all actions 29 my @actions = Jifty->api->all_actions; 30 31 # Fetch the class names of all the allowed actions 32 my @allowed = Jifty->api->actions; 33 34 # Fetch all of the visible actions (some of which may not be allowed) 35 my @visible = Jifty->api->visible_actions; 36 37 # Check to see if an action is allowed 38 if (Jifty->api->is_allowed('TrueFooBar')) { 39 # do something... 40 } 41 42 # Check to see if an action is visible 43 if (Jifty->api->is_visible('SpamOurUsers')) { 44 SpamBot->be_annoying; 45 } 46 47 # Undo all allow/deny/restrict/hide calls 48 Jifty->api->reset; 49 50=head1 DESCRIPTION 51 52You can fetch an instance of this class by calling L<Jifty/api> in 53your application. This object can be used to examine the actions 54available within your application and manage access to those actions. 55 56=cut 57 58 59use base qw/Class::Accessor::Fast Jifty::Object/; 60 61 62__PACKAGE__->mk_accessors(qw(action_limits)); 63 64=head1 METHODS 65 66=head2 new 67 68Creates a new C<Jifty::API> object. 69 70Don't use this, see L<Jifty/api> to access a reference to 71C<Jifty::API> in your application. 72 73=cut 74 75sub new { 76 my $class = shift; 77 my $self = bless {}, $class; 78 79 # Setup the basic allow/deny rules 80 $self->reset; 81 82 # Find all the actions for the API reference (available at __actions) 83 Jifty::Module::Pluggable->import( 84 search_path => [ 85 Jifty->app_class("Action"), 86 "Jifty::Action", 87 map {ref($_)."::Action"} Jifty->plugins, 88 ], 89 except => qr/\.#/, 90 sub_name => "__actions" 91 ); 92 93 return ($self); 94} 95 96=head2 qualify ACTIONNAME 97 98Returns the fully qualified package name for the given provided 99action. If the C<ACTIONNAME> starts with C<Jifty::> or 100C<ApplicationClass::Action>, simply returns the given name; otherwise, 101it prefixes it with the C<ApplicationClass::Action>. 102 103=cut 104 105sub qualify { 106 my $self = shift; 107 my $action = shift; 108 109 # Get the application class name 110 my $base_path = Jifty->config->framework('ApplicationClass'); 111 112 # Return the class now if it's already fully qualified 113 return $action 114 if ($action =~ /^Jifty::/ 115 or $action =~ /^\Q$base_path\E::/); 116 117 # Otherwise qualify it 118 return $base_path . "::Action::" . $action; 119} 120 121=head2 reset 122 123Resets which actions are allowed to the defaults; that is, all of the 124application's actions, L<Jifty::Action::AboutMe>, 125L<Jifty::Action::Autocomplete>, and L<Jifty::Action::Redirect> are allowed and 126visible; everything else is denied and hidden. See L</restrict> for the details 127of how limits are processed. 128 129=cut 130 131sub reset { 132 my $self = shift; 133 134 # Set up defaults 135 my $app_actions = Jifty->app_class("Action"); 136 137 # These are the default action limits 138 $self->action_limits( 139 [ 140 { deny => 1, hide => 1, restriction => qr/.*/ }, 141 { allow => 1, show => 1, restriction => qr/^\Q$app_actions\E/ }, 142 { deny => 1, hide => 1, restriction => qr/^\Q$app_actions\E::Record::(Create|Delete|Execute|Search|Update)$/ }, 143 { allow => 1, show => 1, restriction => 'Jifty::Action::AboutMe' }, 144 { allow => 1, show => 1, restriction => 'Jifty::Action::Autocomplete' }, 145 { allow => 1, show => 1, restriction => 'Jifty::Action::Redirect' }, 146 ] 147 ); 148} 149 150=head2 deny_for_get 151 152Denies all actions except L<Jifty::Action::AboutMe>, 153L<Jifty::Action::Autocomplete> and L<Jifty::Action::Redirect>. This is to 154protect against a common cross-site scripting hole. In your C<before> 155dispatcher rules, you can whitelist actions that are known to be read-only. 156 157This is called automatically during any C<GET> request. 158 159=cut 160 161sub deny_for_get { 162 my $self = shift; 163 $self->deny(qr/.*/); 164 $self->allow("Jifty::Action::AboutMe"); 165 $self->allow("Jifty::Action::Autocomplete"); 166 $self->allow("Jifty::Action::Redirect"); 167} 168 169=head2 allow RESTRICTIONS 170 171Takes a list of strings or regular expressions, and adds them in order 172to the list of limits for the purposes of L</is_allowed>. See 173L</restrict> for the details of how limits are processed. 174 175Allowing actions also L</show> them. 176 177=cut 178 179sub allow { 180 my $self = shift; 181 $self->restrict( allow => @_ ); 182} 183 184=head2 deny RESTRICTIONS 185 186Takes a list of strings or regular expressions, and adds them in order 187to the list of limits for the purposes of L</is_allowed>. See 188L</restrict> for the details of how limits are processed. 189 190=cut 191 192sub deny { 193 my $self = shift; 194 $self->restrict( deny => @_ ); 195} 196 197=head2 hide RESTRICTIONS 198 199Takes a list of strings or regular expressions, and adds them in order 200to the list of limits for the purposes of L</is_visible>. See 201L</restrict> for the details of how limits are processed. 202 203Hiding actions also L</deny> them. 204 205=cut 206 207sub hide { 208 my $self = shift; 209 $self->restrict( hide => @_ ); 210} 211 212=head2 show RESTRICTIONS 213 214Takes a list of strings or regular expressions, and adds them in order 215to the list of limits for the purposes of L</is_visible>. See 216L</restrict> for the details of how limits are processed. 217 218=cut 219 220sub show { 221 my $self = shift; 222 $self->restrict( show => @_ ); 223} 224 225=head2 restrict POLARITY RESTRICTIONS 226 227Method that L</allow>, L</deny>, L</hide>, and L</show> call internally; 228I<POLARITY> is one of C<allow>, C<deny>, C<hide>, or C<show>. Limits are 229evaluated in the order they're called. The last limit that applies will be the 230one which takes effect. Regexes are matched against the class; strings are 231fully L<qualified|/qualify> and used as an exact match against the class name. 232The base set of restrictions (which is reset every request) is set in 233L</reset>, and usually modified by the application's L<Jifty::Dispatcher> if 234need be. 235 236If you call: 237 238 Jifty->api->deny ( qr'Foo' ); 239 Jifty->api->allow ( qr'FooBar' ); 240 Jifty->api->deny ( qr'FooBarDeleteTheWorld' ); 241 242..then: 243 244 calls to MyApp::Action::Baz will succeed. 245 calls to MyApp::Action::Foo will fail. 246 calls to MyApp::Action::FooBar will pass. 247 calls to MyApp::Action::TrueFoo will fail. 248 calls to MyApp::Action::TrueFooBar will pass. 249 calls to MyApp::Action::TrueFooBarDeleteTheWorld will fail. 250 calls to MyApp::Action::FooBarDeleteTheWorld will fail. 251 252=cut 253 254my %valid_polarity = map { $_ => 1 } qw/allow deny hide show/; 255 256sub restrict { 257 my $self = shift; 258 my $polarity = shift; 259 my @restrictions = @_; 260 261 my(undef, $file, $line) = (caller(1)); 262 263 # Check the sanity of the polarity 264 die "Polarity must be one of: " . join(', ', sort keys %valid_polarity) 265 unless $valid_polarity{$polarity}; 266 267 for my $restriction (@restrictions) { 268 269 # Don't let the user "allow .*" 270 die "For security reasons, Jifty won't let you allow all actions" 271 if $polarity eq "allow" 272 and ref $restriction 273 and $restriction =~ /^\(\?\^?[-xismadlu]*:\^?\.\*\$?\)$/; 274 275 # Fully qualify it if it's a string 276 $restriction = $self->qualify($restriction) 277 unless ref $restriction; 278 279 280 if ($polarity eq 'hide') { 281 # Hiding an action also denies it 282 push @{ $self->action_limits }, 283 { deny => 1, hide => 1, restriction => $restriction, from => "$file:$line" }; 284 } elsif ($polarity eq 'allow') { 285 # Allowing an action also shows it 286 push @{ $self->action_limits }, 287 { allow => 1, show => 1, restriction => $restriction, from => "$file:$line" }; 288 } else { 289 # Otherwise, add to list of restrictions unmodified 290 push @{ $self->action_limits }, 291 { $polarity => 1, restriction => $restriction, from => "$file:$line" }; 292 } 293 } 294} 295 296=head2 is_allowed CLASS 297 298Returns true if the I<CLASS> name (which is fully qualified if it is 299not already) is allowed to be executed. See L</restrict> above for 300the rules that the class name must pass. 301 302=cut 303 304sub is_allowed { 305 my $self = shift; 306 my $action = shift; 307 308 $self->decide_action_polarity($action, 'allow', 'deny'); 309} 310 311=head2 is_visible CLASS 312 313Returns true if the I<CLASS> name (which is fully qualified if it is 314not already) is allowed to be seen. See L</restrict> above for 315the rules that the class name must pass. 316 317=cut 318 319sub is_visible { 320 my $self = shift; 321 my $action = shift; 322 323 $self->decide_action_polarity($action, 'show', 'hide'); 324} 325 326=head2 decide_action_polarity CLASS, ALLOW, DENY 327 328Returns true if the I<CLASS> name it has the ALLOW restriction, false if it has 329the DENY restriction. This is a helper method used by L</is_allowed> and 330L</is_visible>. 331 332If no restrictions apply to this action, then false will be returned. 333 334=cut 335 336sub decide_action_polarity { 337 my $self = shift; 338 my $class = shift; 339 my $allow = shift; 340 my $deny = shift; 341 342 # Qualify the action 343 $class = $self->qualify($class); 344 345 # Assume that it doesn't pass; however, the real fallbacks are 346 # controlled by L</reset>, above. 347 my $valid = 0; 348 349 # Walk all of the limits 350 for my $limit ( @{ $self->action_limits } ) { 351 352 # Regexes are =~ matches, strigns are eq matches 353 if ( ( ref $limit->{restriction} and $class =~ $limit->{restriction} ) 354 or ( $class eq $limit->{restriction} ) ) 355 { 356 357 # If the restriction passes, set the current $allow/$deny 358 # bit according to if this was a positive or negative 359 # limit 360 if ($limit->{$allow}) { 361 $valid = 1; 362 } 363 if ($limit->{$deny}) { 364 $valid = 0; 365 } 366 } 367 } 368 369 return $valid; 370} 371 372=head2 explain CLASS 373 374Returns a string describing what allow, deny, show, and hide rules 375apply to the class name. 376 377=cut 378 379sub explain { 380 my $self = shift; 381 my $class = shift; 382 383 $class = $self->qualify($class); 384 385 my $str = ""; 386 for my $limit ( @{$self->action_limits} ) { 387 next unless $limit->{from}; 388 if ( ( ref $limit->{restriction} and $class =~ $limit->{restriction} ) 389 or ( $class eq $limit->{restriction} ) ) 390 { 391 for my $type (qw/allow deny show hide/) { 392 $str .= ucfirst($type)." at ".$limit->{from}.", matches ".$limit->{restriction}."\n" 393 if $limit->{$type}; 394 } 395 } 396 } 397 return $str; 398} 399 400=head2 all_actions 401 402Lists the class names of all actions for this Jifty application, 403regardless of which are allowed or hidden. See also L</actions> and 404L</visible_actions>. 405 406=cut 407 408# Plugin actions under Jifty::Plugin::*::Action are mirrored under 409# AppName::Action by Jifty::ClassLoader; this code makes all_actions 410# reflect this mirroring. 411sub all_actions { 412 my $self = shift; 413 unless ( $self->{all_actions} ) { 414 my @actions = $self->__actions; 415 my %seen; 416 $seen{$_}++ for @actions; 417 for (@actions) { 418 if (/^Jifty::Plugin::(.*)::Action::(.*)$/) { 419 my $classname = Jifty->app_class( Action => $2 ); 420 push @actions, $classname unless $seen{$classname}; 421 } 422 } 423 $self->{all_actions} = \@actions; 424 } 425 return @{ $self->{all_actions} }; 426} 427 428=head2 actions 429 430Lists the class names of all of the B<allowed> actions for this Jifty 431application; this may include actions under the C<Jifty::Action::> 432namespace, in addition to your application's actions. See also 433L</all_actions> and L</visible_actions>. 434 435=cut 436 437sub actions { 438 my $self = shift; 439 return sort grep { $self->is_allowed($_) } $self->all_actions; 440} 441 442=head2 visible_actions 443 444Lists the class names of all of the B<visible> actions for this Jifty 445application; this may include actions under the C<Jifty::Action::> 446namespace, in addition to your application's actions. See also 447L</all_actions> and L</actions>. 448 449=cut 450 451sub visible_actions { 452 my $self = shift; 453 return sort grep { $self->is_visible($_) } $self->all_actions; 454} 455 456=head1 SEE ALSO 457 458L<Jifty>, L<Jifty::Web>, L<Jifty::Action> 459 460=head1 LICENSE 461 462Jifty is Copyright 2005-2010 Best Practical Solutions, LLC. 463Jifty is distributed under the same terms as Perl itself. 464 465=cut 466 4671; 468