1## OpenXPKI::Workflow::Factory 2## 3## Written 2007 by Alexander Klink for the OpenXPKI project 4## (C) Copyright 2007 by The OpenXPKI Project 5package OpenXPKI::Workflow::Factory; 6 7use strict; 8use warnings; 9 10use Workflow 1.36; 11use base qw( Workflow::Factory ); 12use English; 13use OpenXPKI::Exception; 14use OpenXPKI::Debug; 15use OpenXPKI::Server::Context qw( CTX ); 16use OpenXPKI::Server::Workflow; 17use OpenXPKI::Workflow::Context; 18use OpenXPKI::MooseParams; 19use Workflow::Exception qw( configuration_error workflow_error ); 20use Data::Dumper; 21 22sub new { 23 my $class = ref $_[0] || $_[0]; 24 return bless( {} => $class ); 25} 26 27sub instance { 28 # To stay compatible to the Workflow Module, accept instance on exisiting instances 29 my $self = shift; 30 return $self if (ref $self); 31 32 # use "new", not instance to create a new one 33 OpenXPKI::Exception->throw (message => "I18N_OPENXPKI_WORKFLOW_FACTORY_INSTANCE_METHOD_NOT_SUPPORTED"); 34} 35 36sub create_workflow{ 37 my ( $self, $wf_type, $context ) = @_; 38 ##! 1: 'start' 39 40 OpenXPKI::Exception->throw ( 41 message => 'I18N_OPENXPKI_UI_WORKFLOW_CREATE_NOT_ALLOWED', 42 params => { type => $wf_type } 43 ) unless($self->can_create_workflow($wf_type)); 44 45 if (!$context) { 46 $context = OpenXPKI::Workflow::Context->new(); 47 } 48 49 return $self->SUPER::create_workflow( $wf_type, $context, 'OpenXPKI::Server::Workflow' ); 50} 51 52sub create_workflow_as_system { 53 my ( $self, $wf_type, $context, ) = @_; 54 ##! 1: 'start' 55 56 OpenXPKI::Exception->throw ( 57 message => 'I18N_OPENXPKI_UI_WORKFLOW_CREATE_NOT_ALLOWED', 58 params => { type => $wf_type } 59 ) unless($self->can_create_workflow($wf_type, 'System')); 60 61 if (!$context) { 62 $context = OpenXPKI::Workflow::Context->new(); 63 } 64 65 return $self->SUPER::create_workflow( $wf_type, $context, 'OpenXPKI::Server::Workflow' ); 66} 67 68sub fetch_workflow { 69 70 my ( $self, $wf_type, $wf_id ) = @_; 71 ##! 1: 'start' 72 73 ##! 2: 'calling Workflow::Factory::fetch_workflow()' 74 my $wf = $self->SUPER::fetch_workflow($wf_type, $wf_id, undef, 'OpenXPKI::Server::Workflow' ) 75 or OpenXPKI::Exception->throw( 76 message => 'I18N_OPENXPKI_UI_WORKFLOW_HANDLER_ID_NOT_FOUND', 77 params => { 78 WORKFLOW_TYPE => $wf_type, 79 WORKFLOW_ID => $wf_id, 80 }, 81 ); 82 83 $self->can_access_workflow({ 84 type => $wf->type, 85 creator => $wf->attrib('creator'), 86 tenant => $wf->attrib('tenant') 87 }) 88 or OpenXPKI::Exception->throw( 89 message => 'I18N_OPENXPKI_UI_WORKFLOW_ACCESS_NOT_ALLOWED_FOR_USER', 90 params => { type => $wf->type, id => $wf->id }, 91 ); 92 93 $wf->context()->reset_updated(); 94 95 return $wf; 96 97} 98 99sub list_workflow_titles { 100 my $self = shift; 101 ##! 1: 'start' 102 103 my $result = {}; 104 # Nothing initialised 105 if (ref $self->{_workflow_config} ne 'HASH') { 106 return $result; 107 } 108 109 foreach my $item (keys %{$self->{_workflow_config}}) { 110 my $type = $self->{_workflow_config}->{$item}->{type}; 111 my $desc = $self->{_workflow_config}->{$item}->{description}; 112 $result->{$type} = { label => $type, description => $desc || $type } 113 } 114 return $result; 115} 116 117=head2 get_action_info 118 119Return the UI info for the named action. 120 121Todo: Some of this code is duplicated in the OpenXPKI::Workflow::Config - might 122be useful to merge this into a helper. Might be useful in the API. 123 124=cut 125sub get_action_info { 126 my $self = shift; 127 my $action_name = shift; 128 my $wf_name = shift; # this can be replaced after creating a lookup map for prefix -> workflow 129 ##! 1: 'start' 130 131 my $conn = CTX('config'); 132 133 # Check if it is a global or local action 134 my ($prefix, $name) = ($action_name =~ m{ \A (\w+?)_(\w+) \z }xs); 135 136 my @path; 137 if ($prefix eq 'global') { 138 @path = ('workflow','global','action',$name); 139 } else { 140 @path = ('workflow','def', $wf_name, 'action' , $name); 141 } 142 143 my $action = { name => $action_name, label => $action_name }; 144 foreach my $key (qw(label tooltip description template abort resume uihandle button)) { 145 my $val = $conn->get([ @path, $key]); 146 if (defined $val) { 147 $action->{$key} = $val; 148 } 149 } 150 151 my @input = $conn->get_scalar_as_list([ @path, 'input' ]); 152 my @fields; 153 foreach my $field_name (@input) { 154 ##! 64: 'Field info ' . Dumper $field 155 156 my $field = $self->get_field_info( $field_name, $wf_name ); 157 158 $field->{type} = 'text' unless ($field->{type}); 159 $field->{clonable} = (defined $field->{min} || $field->{max}) || 0; 160 161 push @fields, $field; 162 } 163 164 $action->{field} = \@fields if (scalar @fields); 165 166 return $action; 167} 168 169sub get_field_info { 170 my $self = shift; 171 my $field_name = shift; 172 my $wf_name = shift; 173 ##! 1: 'start' 174 175 my $conn = CTX('config'); 176 177 my @field_path; 178 # Fields can be defined local or global (only actions inside workflow) 179 if ($wf_name) { 180 @field_path = ( 'workflow', 'def', $wf_name, 'field', $field_name ); 181 if (!$conn->exists( \@field_path )) { 182 @field_path = ( 'workflow', 'global', 'field', $field_name ); 183 } 184 } else { 185 @field_path = ( 'workflow', 'global', 'field', $field_name ); 186 } 187 188 my $field = $conn->get_hash( \@field_path ); 189 190 # set field's context key to the field name 191 $field->{name} //= $field_name; 192 193 # Check for option tag and do explicit calls to ensure recursive resolving. 194 # This code is duplicated in OpenXPKI::Server::API2::Plugin::Profile::Util 195 # as we need the same syntax in the profiles - TODO move to common API 196 if ($field->{option}) { 197 198 my $mode = $conn->get( [ @field_path, 'option', 'mode' ] ) || 'list'; 199 my @option; 200 if ($mode eq 'keyvalue') { 201 @option = $conn->get_list( [ @field_path, 'option', 'item' ] ); 202 if (my $label = $conn->get( [ @field_path, 'option', 'label' ] )) { 203 @option = map { { label => sprintf($label, $_->{label}, $_->{value}), value => $_->{value} } } @option; 204 } 205 } else { 206 my @item; 207 if ($mode eq 'keys' || $mode eq 'map') { 208 @item = sort $conn->get_keys( [ @field_path, 'option', 'item' ] ); 209 } else { 210 # option.item holds the items as list, this is mandatory 211 @item = $conn->get_list( [ @field_path, 'option', 'item' ] ); 212 } 213 214 if ($mode eq 'map') { 215 # expects that item is a link to a deeper hash structure 216 # where the each hash item has a key "label" set 217 # will hide items with an empty label 218 foreach my $key (@item) { 219 my $label = $conn->get( [ @field_path, 'option', 'item', $key, 'label' ] ); 220 next unless ($label); 221 push @option, { value => $key, label => $label }; 222 } 223 } elsif (my $label = $conn->get( [ @field_path, 'option', 'label' ] )) { 224 # if set, we generate the values from option.label + key 225 @option = map { { value => $_, label => $label.'_'.uc($_) } } @item; 226 227 } else { 228 # the minimum default - use keys as labels 229 @option = map { { value => $_, label => $_ } } @item; 230 } 231 } 232 $field->{option} = \@option; 233 234 } 235 236 return $field; 237 238} 239 240# Returns a HashRef with configuration details (actions, states) of the given 241# workflow type and state. 242sub get_action_and_state_info { 243 my ($self, $type, $state, $actions, $context) = positional_args(\@_, # OpenXPKI::MooseParams 244 { isa => 'Str', }, 245 { isa => 'Str', }, 246 { isa => 'ArrayRef', }, 247 { isa => 'HashRef|Undef', optional => 1, default => sub { {} } }, 248 ); 249 ##! 4: 'start' 250 251 my $head = CTX('config')->get_hash([ 'workflow', 'def', $type, 'head' ]); 252 my $wf_prefix = $head->{prefix}; 253 254 # 255 # add activities (= actions) 256 # 257 my $action_info = {}; 258 259 OpenXPKI::Connector::WorkflowContext::set_context($context) if $context; 260 for my $action (@{ $actions }) { 261 $action_info->{$action} = $self->get_action_info($action, $type); 262 } 263 OpenXPKI::Connector::WorkflowContext::set_context() if $context; 264 265 # 266 # add state UI info 267 # 268 my $state_info = CTX('config')->get_hash([ 'workflow', 'def', $type, 'state', $state ]); 269 270 # replace hash key "output" with detailed field informations 271 if ($state_info->{output}) { 272 my @output_fields = ref $state_info->{output} eq 'ARRAY' 273 ? @{ $state_info->{output} } 274 : CTX('config')->get_list([ 'workflow', 'def', $type, 'state', $state, 'output' ]); 275 276 # query detailed field informations 277 $state_info->{output} = [ map { $self->get_field_info($_, $type) } @output_fields ]; 278 } 279 280 # add button info 281 my $button = $state_info->{button}; 282 $state_info->{button} = {}; 283 284 # possible actions (options / activity names) in the right order 285 delete $state_info->{action}; 286 my @options = CTX('config')->get_scalar_as_list([ 'workflow', 'def', $type, 'state', $state, 'action' ]); 287 288 # check defined actions and only list the possible ones 289 # (non global actions are prefixed) 290 ##! 64: 'Available actions ' . Dumper keys %{ $action_info->{$action} } 291 $state_info->{option} = []; 292 if ($state_info->{autoselect} && $state_info->{autoselect} !~ m{\Aglobal_}) { 293 $state_info->{autoselect} = $wf_prefix.'_'.$state_info->{autoselect}; 294 } 295 for my $option (@options) { 296 $option =~ m{ \A (\W?)((global_)?([^\s>]+))}xs; 297 $option = $2; 298 299 my $action_prefix = $1; 300 my $full = $2; 301 my $global = $3; 302 my $option_base = $4; 303 304 # evaluate action prefix 305 my $auto = 0; 306 if ($action_prefix eq '~') { 307 $auto = 1; 308 } 309 elsif (not defined $action_prefix or $action_prefix eq '') { 310 # ok 311 } 312 else { 313 OpenXPKI::Exception->throw( 314 message => 'Action contains unknown prefix. Currently supported: "~" (autoselect action)', 315 params => { 316 action => $option, 317 prefix => $action_prefix, 318 }, 319 ); 320 } 321 322 my $action = sprintf("%s_%s", $global ? "global" : $wf_prefix, $option_base); 323 ##! 16: 'Activity ' . $action 324 next unless($action_info->{$action}); 325 326 push @{$state_info->{option}}, $action; 327 if ($auto && !$state_info->{autoselect}) { 328 $state_info->{autoselect} = $action; 329 } 330 331 # Add button config if available 332 $state_info->{button}->{$action} = $button->{$option} if $button->{$option}; 333 } 334 335 # add button markup (head) 336 $state_info->{button}->{_head} = $button->{_head} if $button->{_head}; 337 338 return { 339 activity => $action_info, 340 state => $state_info, 341 }; 342} 343 344 345=head2 can_create_workflow (type, role) 346 347Check if the given role (default is the session role) is allowed to 348create workflows of the given type. Returns true/false and throws an 349exception if the workflow type is unknown or missing. 350 351=cut 352 353sub can_create_workflow { 354 355 my $self = shift; 356 my $type = shift; 357 my $role = shift || CTX('session')->data->role || 'Anonymous'; 358 ##! 1: 'start' 359 360 OpenXPKI::Exception->throw( 361 message => 'No type was given' 362 ) unless($type); 363 364 my $conn = CTX('config'); 365 366 OpenXPKI::Exception->throw( 367 message => 'I18N_OPENXPKI_UI_WORKFLOW_CREATE_UNKNOWN_TYPE' 368 ) unless($conn->exists([ 'workflow', 'def', $type])); 369 370 # if creator is set then access is allowed 371 return ($conn->exists([ 'workflow', 'def', $type, 'acl', $role, 'creator' ])); 372 373} 374 375=head2 can_access_workflow 376 377Helper method to evaluate the acl given in the workflow config against 378against a concrete instance. Expects two hashes to be passed, the first 379hash represents the workflow instance, the second the entity that 380requests access. 381 382The first hash must include type and creator, tenant must be set to 383evaluate tenant based rules. 384 385The second hash is optional, if present it must have the keys user, 386role and tenant (if enabled). If omited user/role is read from the 387session and tenant is checked against the current users tenant list. 388 389Returns 1 if the user can access the workflow. Return undef if no acl 390is defined for the current role and 0 if an acl was found but does not 391authorize the current user. 392 393Will thrown an exception if a mandatory parameter was not passed. 394 395=cut 396 397sub can_access_workflow { 398 399 ##! 1: 'start' 400 my ($self, $instance, $user) = @_; 401 402 OpenXPKI::Exception->throw( 403 message => 'No type given to Workflow::Factory::can_access_workflow', 404 ) unless($instance->{type}); 405 406 OpenXPKI::Exception->throw( 407 message => 'No creator given to Workflow::Factory::can_access_workflow', 408 ) unless($instance->{creator}); 409 410 ##! 64: $instance 411 $user //= { 412 user => CTX('session')->data->user, 413 role => (CTX('session')->data->has_role ? CTX('session')->data->role : 'Anonymous'), 414 tenant => '' 415 }; 416 ##! 64: $user 417 418 my @allowed_creator = CTX('config')->get_scalar_as_list([ 'workflow', 'def', $instance->{type}, 'acl', $user->{role}, 'creator' ]); 419 ##! 32: 'Rules ' . Dumper \@allowed_creator 420 return unless (@allowed_creator); 421 422 my $is_allowed = 0; 423 foreach my $allowed_creator_re (@allowed_creator) { 424 ##! 32: "Checking $allowed_creator_re" 425 # Access only to own workflows - check session user against creator 426 if ($allowed_creator_re eq 'self') { 427 $is_allowed = ($instance->{creator} eq $user->{user}); 428 429 # No access to own workflows 430 } elsif ($allowed_creator_re eq 'others') { 431 $is_allowed = ($instance->{creator} ne $user->{user}); 432 433 # access by tenant 434 } elsif ($allowed_creator_re eq 'tenant') { 435 436 # useless check if the workflow has no tenant 437 next unless(defined $instance->{tenant}); 438 439 # tenant is given, we check for an exact match 440 if ($user->{tenant}) { 441 $is_allowed = ($instance->{tenant} eq $user->{tenant}); 442 443 # tenant is DEFINED = use tenant handler to check 444 } elsif (defined $user->{tenant}) { 445 $is_allowed = CTX('api2')->can_access_tenant( tenant => $instance->{tenant} ); 446 } 447 # no tenant check if tenant is not set - this avoids using 448 # the session tenant handler with a explicit user/role given 449 450 # access to any workflow 451 } elsif ($allowed_creator_re eq 'any') { 452 $is_allowed = 1; 453 454 # Access by Regex - check 455 } else { 456 $is_allowed = ($instance->{creator} =~ qr/$allowed_creator_re/); 457 } 458 ##! 64: "$allowed_creator_re / " . ($is_allowed ? '1' : '0') 459 last if $is_allowed; 460 } 461 ##! 16: "Final result: $is_allowed" 462 return $is_allowed; 463} 464 465=head2 can_access_handle 466 467Check if a user/role can access a certain property (history, techlog) 468or handle (resume, wakeup) for a given workflow type. 469 470Returns boolean true/false or undef if no permissions are set. 471 472=cut 473 474sub can_access_handle { 475 476 my ( $self, $type, $action, $role ) = @_; 477 478 OpenXPKI::Exception->throw( 479 message => 'Unknown handle/property given to can_access_handle', 480 params => { 'action' => $action } 481 ) unless ($action =~ m{\A(fail|resume|reset|wakeup|history|techlog|attribute|context|archive|delete)\z}); 482 483 OpenXPKI::Exception->throw( 484 message => 'None or unknown workflow type was given', 485 params => { type => ($type // '<undef>')} 486 ) unless($type && CTX('config')->exists([ 'workflow', 'def', $type])); 487 488 $role //= (CTX('session')->data->role || 'Anonymous'); 489 return (CTX('config')->get([ 'workflow', 'def', $type, 'acl', $role, $action ])); 490 491} 492 493=head2 update_proc_state($wf, $old_state, $new_state) 494 495Tries to update the C<proc_state> in the database to C<$new_state>. 496 497Returns 1 on success and 0 if e.g. another parallel process already changed the 498given C<$old_state>. 499 500=cut 501sub update_proc_state { 502 my ($self, $wf, $old_state, $new_state) = @_; 503 504 my $wf_config = $self->_get_workflow_config( $wf->type ); 505 my $persister = $self->get_persister( $wf_config->{persister} ); 506 return $persister->update_proc_state($wf->id, $old_state, $new_state); 507} 508 5091; 510__END__ 511 512=head1 Name 513 514OpenXPKI::Workflow::Factory - OpenXPKI specific workflow factory 515 516=head1 Description 517 518This is the OpenXPKI specific subclass of Workflow::Factory. 519We need an OpenXPKI specific subclass because Workflow currently 520enforces that a Factory is a singleton. In OpenXPKI, we want to have 521several factory objects (one for each version and each PKI realm). 522The most important difference between Workflow::Factory and 523OpenXPKI::Workflow::Factory is in the instance() class method, which 524creates only one global instance in the original and a new one for 525each call in the OpenXPKI version. 526 527In addition, the fetch_workflow() method has been modified to do ACL 528checks before returning the workflow to the caller. 529 530All methods return an object of class OpenXPKI::Server::Workflow, which is derived 531from Workflow base class and implements the pause/resume-features. see there for details. 532