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