0002-workflows-add-possibility-to-trigger-global-actions-.patch
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 |
- |