Projet

Général

Profil

0002-workflows-add-possibility-to-trigger-global-actions-.patch

Frédéric Péters, 11 avril 2019 13:15

Télécharger (14,5 ko)

Voir les différences:

Subject: [PATCH 2/2] workflows: add possibility to trigger global actions with
 a webservice (#32184)

 tests/test_api.py             | 68 ++++++++++++++++++++++++++++++++
 tests/test_workflow_import.py |  9 +++++
 wcs/admin/workflows.py        |  1 +
 wcs/api.py                    | 24 ++++++++----
 wcs/forms/workflows.py        | 74 +++++++++++++++++++++++++++++++++++
 wcs/workflows.py              | 46 +++++++++++++++++++++-
 6 files changed, 213 insertions(+), 9 deletions(-)
 create mode 100644 wcs/forms/workflows.py
tests/test_api.py
28 28
from wcs.data_sources import NamedDataSource
29 29
from wcs.workflows import Workflow, EditableWorkflowStatusItem, WorkflowBackofficeFieldsFormDef
30 30
from wcs.wf.jump import JumpWorkflowStatusItem
31
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
31 32
from wcs import fields, qommon
32 33
from wcs.api_utils import sign_url, get_secret_and_orig, is_url_signed, DEFAULT_DURATION
33 34
from wcs.qommon.errors import AccessForbiddenError
......
2179 2180
    resp = get_app(pub).post(sign_uri(formdata.get_url() + 'jump/trigger/XXX'))
2180 2181
    assert resp.json == {'err': 0, 'url': None}
2181 2182

  
2183
def test_workflow_global_webservice_trigger(pub, local_user):
2184
    workflow = Workflow(name='test')
2185
    st1 = workflow.add_status('Status1', 'st1')
2186

  
2187
    ac1 = workflow.add_global_action('Action', 'ac1')
2188
    trigger = ac1.append_trigger('webservice')
2189
    trigger.identifier = 'plop'
2190

  
2191
    add_to_journal = RegisterCommenterWorkflowStatusItem()
2192
    add_to_journal.id = '_add_to_journal'
2193
    add_to_journal.comment = 'HELLO WORLD'
2194
    ac1.items.append(add_to_journal)
2195
    add_to_journal.parent = ac1
2196

  
2197
    workflow.store()
2198

  
2199
    FormDef.wipe()
2200
    formdef = FormDef()
2201
    formdef.name = 'test'
2202
    formdef.fields = []
2203
    formdef.workflow_id = workflow.id
2204
    formdef.store()
2205

  
2206
    formdef.data_class().wipe()
2207
    formdata = formdef.data_class()()
2208
    formdata.just_created()
2209
    formdata.store()
2210
    assert formdef.data_class().get(formdata.id).status == 'wf-st1'
2211

  
2212
    # call to undefined hook
2213
    resp = get_app(pub).post(sign_uri(formdata.get_url() + 'hooks/XXX/'), status=404)
2214
    resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/XXX/'), status=404)
2215

  
2216
    # anonymous call
2217
    resp = get_app(pub).post(formdata.get_url() + 'hooks/plop/', status=200)
2218
    assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD'
2219

  
2220
    add_to_journal.comment = 'HELLO WORLD 2'
2221
    workflow.store()
2222
    resp = get_app(pub).post(formdata.get_api_url() + 'hooks/plop/', status=200)
2223
    assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 2'
2224

  
2225
    # call requiring user
2226
    add_to_journal.comment = 'HELLO WORLD 3'
2227
    trigger.roles = ['logged-users']
2228
    workflow.store()
2229
    resp = get_app(pub).post(formdata.get_api_url() + 'hooks/plop/', status=403)
2230
    resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/'), status=200)
2231
    assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 3'
2232

  
2233
    # call requiring roles
2234
    add_to_journal.comment = 'HELLO WORLD 4'
2235
    trigger.roles = ['logged-users']
2236
    workflow.store()
2237
    Role.wipe()
2238
    role = Role(name='xxx')
2239
    role.store()
2240
    trigger.roles = [role.id]
2241
    workflow.store()
2242
    resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/'), status=403)
2243
    resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/', user=local_user), status=403)
2244

  
2245
    local_user.roles = [role.id]
2246
    local_user.store()
2247
    resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/', user=local_user), status=200)
2248
    assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 4'
2249

  
2182 2250
def test_tracking_code(pub):
2183 2251
    FormDef.wipe()
2184 2252
    formdef = FormDef()
tests/test_workflow_import.py
593 593
    assert wf2.global_actions[0].triggers[-1].id == trigger.id
594 594
    assert wf2.global_actions[0].triggers[-1].anchor == trigger.anchor
595 595

  
596
def test_global_webservice_trigger(pub):
597
    wf = Workflow(name='global actions')
598
    ac1 = wf.add_global_action('Action', 'ac1')
599
    trigger = ac1.append_trigger('webservice')
600
    trigger.identifier = 'plop'
601

  
602
    wf2 = assert_import_export_works(wf, include_id=True)
603
    assert wf2.global_actions[0].triggers[-1].id == trigger.id
604
    assert wf2.global_actions[0].triggers[-1].identifier == trigger.identifier
596 605

  
597 606
def test_profile_action(pub):
598 607
    wf = Workflow(name='status')
wcs/admin/workflows.py
1272 1272
        available_triggers = [
1273 1273
            ('timeout', _('Automatic')),
1274 1274
            ('manual', _('Manual')),
1275
            ('webservice', _('Webservice')),
1275 1276
        ]
1276 1277
        form.add(SingleSelectWidget, 'type', title=_('Type'),
1277 1278
                required=True, options=available_triggers)
wcs/api.py
147 147
        if not api_user:
148 148
            if get_request().user and get_request().user.is_admin:
149 149
                return # grant access to admins, to ease debug
150
            raise AccessForbiddenError('user not authenticated')
150
            raise AccessForbiddenError('user not authenticated X')
151 151
        if not self.formdef.is_user_allowed_read_status_and_history(api_user, self.filled):
152 152
            raise AccessForbiddenError('unsufficient roles')
153 153

  
......
164 164
    def check_access(self, api_name=None):
165 165
        if 'anonymise' in get_request().form:
166 166
            if not is_url_signed() or (get_request().user and get_request().user.is_admin):
167
                raise AccessForbiddenError('user not authenticated')
167
                raise AccessForbiddenError('user not authenticated Y')
168 168
        else:
169 169
            api_user = get_user_from_api_query_string(api_name=api_name)
170 170
            if not api_user:
171 171
                if get_request().user and get_request().user.is_admin:
172 172
                    return # grant access to admins, to ease debug
173
                raise AccessForbiddenError('user not authenticated')
173
                raise AccessForbiddenError('user not authenticated Z')
174 174
            if not self.formdef.is_of_concern_for_user(api_user):
175 175
                raise AccessForbiddenError('unsufficient roles')
176 176

  
......
178 178
        if component == 'ics':
179 179
            return self.ics()
180 180

  
181
        # check access for all paths, to block access to formdata that would
182
        # otherwise be accessible if the user is the submitter.
183
        self.check_access()
181
        # check access for all paths (except webooks), to block access to
182
        # formdata that would otherwise be accessible if the user is the
183
        # submitter.
184
        if not self.is_webhook:
185
            self.check_access()
184 186
        try:
185 187
            formdata = self.formdef.data_class().get(component)
186 188
        except KeyError:
187 189
            raise TraversalError()
188 190
        return ApiFormdataPage(self.formdef, formdata)
189 191

  
192
    def _q_traverse(self, path):
193
        self.is_webhook = False
194
        if len(path) > 1:
195
            # webhooks have their own access checks, request cannot be blocked
196
            # at this point.
197
            self.is_webhook = bool(path[1] == 'hooks')
198
        return super(ApiFormPage, self)._q_traverse(path)
199

  
190 200

  
191 201
class ApiFormsDirectory(Directory):
192 202
    _q_exports = ['', 'geojson']
......
195 205
        if not is_url_signed():
196 206
            # grant access to admins, to ease debug
197 207
            if not (get_request().user and get_request().user.is_admin):
198
                raise AccessForbiddenError('user not authenticated')
208
                raise AccessForbiddenError('user not authenticated W')
199 209
            if get_request().form.get('ignore-roles') == 'on' and not get_request().user.can_go_in_backoffice():
200 210
                raise AccessForbiddenError('user not allowed to ignore roles')
201 211

  
wcs/forms/workflows.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2019  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
import json
18

  
19
from quixote import get_request, get_response
20
from quixote.directory import Directory
21

  
22
from qommon import errors
23

  
24
from wcs.api import get_user_from_api_query_string, is_url_signed
25
from wcs.roles import logged_users_role
26
from wcs.workflows import (
27
        WorkflowGlobalActionWebserviceTrigger,
28
        get_role_translation,
29
        perform_items)
30

  
31

  
32
class HookDirectory(Directory):
33
    _q_exports = ['']
34

  
35
    def __init__(self, formdata, action, trigger):
36
        self.formdata = formdata
37
        self.action = action
38
        self.trigger = trigger
39

  
40
    def _q_index(self):
41
        get_response().set_content_type('application/json')
42

  
43
        if not get_request().get_method() == 'POST':
44
            raise errors.AccessForbiddenError('must be POST')
45

  
46
        user = get_user_from_api_query_string() or get_request().user
47
        if self.trigger.roles:
48
            for role in self.trigger.roles:
49
                if role == logged_users_role().id and (user or is_url_signed()):
50
                    break
51
                if role == '_submitter' and self.formdata.is_submitter(user):
52
                    break
53
                if not user:
54
                    continue
55
                if get_role_translation(self.formdata, role) in (user.roles or []):
56
                    break
57
            else:
58
                raise errors.AccessForbiddenError('insufficient roles')
59

  
60
        perform_items(self.action.items, self.formdata)
61
        return json.dumps({'err': 0})
62

  
63

  
64
class WorkflowGlobalActionWebserviceHooksDirectory(Directory):
65
    def __init__(self, formdata):
66
        self.formdata = formdata
67

  
68
    def _q_lookup(self, component):
69
        for action in self.formdata.formdef.workflow.global_actions:
70
            for trigger in action.triggers or []:
71
                if isinstance(trigger, WorkflowGlobalActionWebserviceTrigger):
72
                    if trigger.identifier == component:
73
                        return HookDirectory(self.formdata, action, trigger)
74
        raise errors.TraversalError()
wcs/workflows.py
435 435
        wf_status = formdata.get_status()
436 436
        if not wf_status:  # draft
437 437
            return []
438
        return wf_status.get_subdirectories(formdata)
438
        directories = []
439
        for action in self.global_actions:
440
            for trigger in action.triggers or []:
441
                directories.extend(trigger.get_subdirectories(formdata))
442
        directories.extend(wf_status.get_subdirectories(formdata))
443
        return directories
439 444

  
440 445
    def __setstate__(self, dict):
441 446
        self.__dict__.update(dict)
......
950 955
                    value = getattr(self, '%s_parse' % f)(value)
951 956
                setattr(self, f, value)
952 957

  
958
    def get_subdirectories(self, formdata):
959
        return []
960

  
953 961

  
954 962
class WorkflowGlobalActionManualTrigger(WorkflowGlobalActionTrigger):
955 963
    key = 'manual'
......
1159 1167
                        break
1160 1168

  
1161 1169

  
1170
class WorkflowGlobalActionWebserviceTrigger(WorkflowGlobalActionManualTrigger):
1171
    key = 'webservice'
1172
    identifier = None
1173
    roles = None
1174

  
1175
    def get_parameters(self):
1176
        return ('identifier', 'roles')
1177

  
1178
    def render_as_line(self):
1179
        if self.identifier:
1180
            return _('Webservice (%s)') % self.identifier
1181
        else:
1182
            return _('Webservice (not configured)')
1183

  
1184
    def form(self, workflow):
1185
        form = Form(enctype='multipart/form-data')
1186
        form.add(StringWidget, 'identifier', title=_('Identifier'),
1187
                required=True, value=self.identifier)
1188
        options = [(None, '---', None)]
1189
        options += workflow.get_list_of_roles(include_logged_in_users=True)
1190
        form.add(WidgetList, 'roles', title=_('Roles'),
1191
                element_type=SingleSelectWidget,
1192
                value=self.roles,
1193
                add_element_label=_('Add Role'),
1194
                element_kwargs={'render_br': False,
1195
                                'options': options})
1196
        return form
1197

  
1198
    def get_subdirectories(self, formdata):
1199
        from wcs.forms.workflows import WorkflowGlobalActionWebserviceHooksDirectory
1200
        return [('hooks', WorkflowGlobalActionWebserviceHooksDirectory(formdata))]
1201

  
1202

  
1162 1203
class WorkflowGlobalAction(object):
1163 1204
    id = None
1164 1205
    name = None
......
1186 1227
    def append_trigger(self, type):
1187 1228
        trigger_types = {
1188 1229
            'manual': WorkflowGlobalActionManualTrigger,
1189
            'timeout': WorkflowGlobalActionTimeoutTrigger
1230
            'timeout': WorkflowGlobalActionTimeoutTrigger,
1231
            'webservice': WorkflowGlobalActionWebserviceTrigger,
1190 1232
        }
1191 1233
        o = trigger_types.get(type)()
1192 1234
        if not self.triggers:
1193
-