api: add triggerable global actions to card data api (#88875)
This commit is contained in:
parent
ab9adecf55
commit
8779eea796
|
@ -344,6 +344,17 @@ l’adresse.
|
|||
contenu des champs de type « Fichier » n’est pas exporté.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Un paramètre <code>include-actions</code> permet d’inclure (<code>on</code>) ou
|
||||
non (<code>off</code>) la liste des actions globales et des déclencheurs de
|
||||
sauts automatiques actuellement accessible via l'API à l'utilisateur qui
|
||||
effectue la requête.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?include-actions=on</input>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
|
|
@ -10,6 +10,7 @@ from wcs.api_access import ApiAccess
|
|||
from wcs.carddef import CardDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.upload_storage import PicklableUpload
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app
|
||||
from .utils import sign_uri
|
||||
|
@ -175,6 +176,8 @@ def test_carddata_include_params(pub, local_user, auth):
|
|||
resp = get_url('/api/cards/test/list?include-workflow-data=on')
|
||||
assert 'workflow' in resp.json['data'][0]
|
||||
assert 'data' in resp.json['data'][0]['workflow']
|
||||
resp = get_url('/api/cards/test/list?include-actions=on')
|
||||
assert 'actions' in resp.json['data'][0]
|
||||
|
||||
resp = get_url('/api/cards/test/%s/?include-fields=off' % carddata.id)
|
||||
assert 'fields' not in resp.json
|
||||
|
@ -192,6 +195,8 @@ def test_carddata_include_params(pub, local_user, auth):
|
|||
assert 'data' not in resp.json['workflow']
|
||||
resp = get_url('/api/cards/test/%s/?include-workflow=off&include-workflow-data=off' % carddata.id)
|
||||
assert 'workflow' not in resp.json
|
||||
resp = get_url('/api/cards/test/%s/?include-actions=off' % carddata.id)
|
||||
assert 'actions' not in resp.json
|
||||
|
||||
resp = get_url('/api/cards/test/list')
|
||||
assert len(resp.json['data']) == 1
|
||||
|
@ -499,3 +504,86 @@ def test_api_card_list_custom_id_filter_identifier(pub):
|
|||
assert len(resp.json['data']) == 1
|
||||
assert resp.json['data'][0]['id'] == 'bar'
|
||||
assert resp.json['data'][0]['internal_id'] == str(card.id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('auth', ['signature', 'http-basic'])
|
||||
def test_carddata_global_actions(auth, pub, local_user):
|
||||
CardDef.wipe()
|
||||
Workflow.wipe()
|
||||
ApiAccess.wipe()
|
||||
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='allowed-action-role')
|
||||
role.store()
|
||||
|
||||
local_user.roles = [role.id]
|
||||
local_user.store()
|
||||
|
||||
access = ApiAccess()
|
||||
access.name = 'test'
|
||||
access.access_identifier = 'test'
|
||||
access.access_key = '12345'
|
||||
access.store()
|
||||
|
||||
app = get_app(pub)
|
||||
|
||||
if auth == 'http-basic':
|
||||
access.roles = [role]
|
||||
access.store()
|
||||
|
||||
def get_url(url, **kwargs):
|
||||
app.set_authorization(('Basic', ('test', '12345')))
|
||||
return app.get(url, **kwargs)
|
||||
|
||||
else:
|
||||
|
||||
def get_url(url, **kwargs):
|
||||
return app.get(
|
||||
sign_uri(
|
||||
url,
|
||||
user=local_user,
|
||||
orig=access.access_identifier,
|
||||
key=access.access_key,
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
workflow = Workflow(name='test-workflow')
|
||||
workflow.add_status('test-status')
|
||||
|
||||
action = workflow.add_global_action('Global Action')
|
||||
trigger = action.append_trigger('webservice')
|
||||
trigger.identifier = 'test-trigger'
|
||||
trigger.roles = [role.id]
|
||||
workflow.store()
|
||||
|
||||
CardDef.wipe()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'test-carddef'
|
||||
carddef.workflow_id = workflow.id
|
||||
carddef.workflow_roles = {'_receiver': role.id}
|
||||
carddef.store()
|
||||
|
||||
carddata = carddef.data_class()()
|
||||
carddata.just_created()
|
||||
carddata.jump_status('workflow-status')
|
||||
carddata.store()
|
||||
|
||||
resp = get_url('/api/cards/test-carddef/%s/?include-actions=on' % carddata.id)
|
||||
assert resp.json['actions'] == {
|
||||
'global-action:test-trigger': f'{carddata.get_api_url()}hooks/test-trigger/'
|
||||
}
|
||||
|
||||
trigger.identifier = None
|
||||
workflow.store()
|
||||
|
||||
resp = get_url('/api/cards/test-carddef/%s/?include-actions=on' % carddata.id)
|
||||
assert resp.json['actions'] == {}
|
||||
|
||||
trigger.identifier = 'test-trigger'
|
||||
trigger.roles = ['_unhautorized']
|
||||
workflow.store()
|
||||
|
||||
resp = get_url('/api/cards/test-carddef/%s/?include-actions=on' % carddata.id)
|
||||
assert resp.json['actions'] == {}
|
||||
|
||||
|
|
|
@ -2618,6 +2618,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
include_submission = get_query_flag('include-submission') or full
|
||||
include_workflow = get_query_flag('include-workflow') or full
|
||||
include_workflow_data = get_query_flag('include-workflow-data') or full
|
||||
include_actions = get_query_flag('include-actions') or full
|
||||
if include_evolution or include_workflow:
|
||||
self.formdef.data_class().load_all_evolutions(items)
|
||||
# noqa pylint: disable=too-many-boolean-expressions
|
||||
|
@ -2628,6 +2629,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
or include_submission
|
||||
or include_workflow
|
||||
or include_workflow_data
|
||||
or include_actions
|
||||
):
|
||||
job = JsonFileExportAfterJob(self.formdef)
|
||||
output = job.create_json_export(
|
||||
|
@ -2643,6 +2645,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
include_unnamed_fields=False,
|
||||
include_workflow=include_workflow,
|
||||
include_workflow_data=include_workflow_data,
|
||||
include_actions=include_actions,
|
||||
values_at=get_request().form.get('at'),
|
||||
)
|
||||
if job.id:
|
||||
|
@ -4467,6 +4470,7 @@ class JsonFileExportAfterJob(CsvExportAfterJob):
|
|||
include_unnamed_fields,
|
||||
include_workflow,
|
||||
include_workflow_data,
|
||||
include_actions,
|
||||
values_at=None,
|
||||
):
|
||||
# noqa pylint: disable=too-many-arguments
|
||||
|
@ -4528,6 +4532,7 @@ class JsonFileExportAfterJob(CsvExportAfterJob):
|
|||
include_unnamed_fields=include_unnamed_fields,
|
||||
include_workflow=include_workflow,
|
||||
include_workflow_data=include_workflow_data,
|
||||
include_actions=include_actions,
|
||||
values_at=values_at,
|
||||
)
|
||||
except NoContentSnapshotAt:
|
||||
|
@ -4555,6 +4560,7 @@ class JsonFileExportAfterJob(CsvExportAfterJob):
|
|||
include_unnamed_fields=True,
|
||||
include_workflow=True,
|
||||
include_workflow_data=True,
|
||||
include_actions=False,
|
||||
)
|
||||
},
|
||||
indent=2,
|
||||
|
|
|
@ -1547,6 +1547,7 @@ class FormData(StorableObject):
|
|||
include_unnamed_fields=False,
|
||||
include_workflow=True,
|
||||
include_workflow_data=True,
|
||||
include_actions=True,
|
||||
values_at=None,
|
||||
):
|
||||
# noqa pylint: disable=too-many-arguments
|
||||
|
@ -1647,6 +1648,21 @@ class FormData(StorableObject):
|
|||
data['workflow'] = {}
|
||||
data['workflow']['data'] = self.workflow_data
|
||||
|
||||
# include actions
|
||||
if include_actions:
|
||||
actions = {}
|
||||
data['actions'] = actions
|
||||
|
||||
for trigger in self.formdef.workflow.get_all_global_action_triggers():
|
||||
if (
|
||||
trigger.key == 'webservice'
|
||||
and trigger.identifier
|
||||
and trigger.check_executable(self, user)
|
||||
):
|
||||
actions[
|
||||
f'global-action:{trigger.identifier}'
|
||||
] = f'{self.get_api_url()}hooks/{trigger.identifier}/'
|
||||
|
||||
if include_roles:
|
||||
# add a roles dictionary, with workflow functions and two special
|
||||
# entries for concerned/actions roles.
|
||||
|
@ -1720,6 +1736,7 @@ class FormData(StorableObject):
|
|||
def export_to_json(
|
||||
self,
|
||||
anonymise=False,
|
||||
user=None,
|
||||
include_evolution=True,
|
||||
include_files=True,
|
||||
include_roles=True,
|
||||
|
@ -1728,11 +1745,13 @@ class FormData(StorableObject):
|
|||
include_unnamed_fields=False,
|
||||
include_workflow=True,
|
||||
include_workflow_data=True,
|
||||
include_actions=True,
|
||||
values_at=None,
|
||||
):
|
||||
# noqa pylint: disable=too-many-arguments
|
||||
data = self.get_json_export_dict(
|
||||
anonymise=anonymise,
|
||||
user=user,
|
||||
include_evolution=include_evolution,
|
||||
include_files=include_files,
|
||||
include_roles=include_roles,
|
||||
|
@ -1741,6 +1760,7 @@ class FormData(StorableObject):
|
|||
include_unnamed_fields=include_unnamed_fields,
|
||||
include_workflow=include_workflow,
|
||||
include_workflow_data=include_workflow_data,
|
||||
include_actions=include_actions,
|
||||
values_at=values_at,
|
||||
)
|
||||
return json.dumps(data, cls=misc.JSONEncoder)
|
||||
|
|
|
@ -245,6 +245,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
include_unnamed_fields=False,
|
||||
include_workflow=get_query_flag('include-workflow', default=True),
|
||||
include_workflow_data=get_query_flag('include-workflow-data', default=True),
|
||||
include_actions=get_query_flag('include-actions', default=False),
|
||||
values_at=values_at,
|
||||
)
|
||||
|
||||
|
@ -479,12 +480,15 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
include_unnamed_fields=False,
|
||||
include_workflow=True,
|
||||
include_workflow_data=True,
|
||||
include_actions=True,
|
||||
values_at=None,
|
||||
):
|
||||
# noqa pylint: disable=too-many-arguments
|
||||
get_response().set_content_type('application/json')
|
||||
user = get_user_from_api_query_string() or get_request().user
|
||||
return self.filled.export_to_json(
|
||||
anonymise=anonymise,
|
||||
user=user,
|
||||
include_evolution=include_evolution,
|
||||
include_files=include_files,
|
||||
include_roles=include_roles,
|
||||
|
@ -493,6 +497,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
include_unnamed_fields=include_unnamed_fields,
|
||||
include_workflow=include_workflow,
|
||||
include_workflow_data=include_workflow_data,
|
||||
include_actions=include_actions,
|
||||
values_at=values_at,
|
||||
)
|
||||
|
||||
|
|
|
@ -19,8 +19,7 @@ import json
|
|||
from quixote import get_request, get_response
|
||||
from quixote.directory import Directory
|
||||
|
||||
from wcs.api import get_user_from_api_query_string, is_url_signed
|
||||
from wcs.roles import logged_users_role
|
||||
from wcs.api import get_user_from_api_query_string
|
||||
from wcs.wf.jump import WorkflowTriggeredEvolutionPart
|
||||
from wcs.workflows import WorkflowGlobalActionWebserviceTrigger, perform_items, push_perform_workflow
|
||||
|
||||
|
@ -42,19 +41,8 @@ class HookDirectory(Directory):
|
|||
raise errors.AccessForbiddenError('must be POST')
|
||||
|
||||
user = get_user_from_api_query_string() or get_request().user
|
||||
if self.trigger.roles:
|
||||
for role in self.trigger.roles:
|
||||
if role == logged_users_role().id and (user or is_url_signed()):
|
||||
break
|
||||
if role == '_submitter' and self.formdata.is_submitter(user):
|
||||
break
|
||||
if not user:
|
||||
continue
|
||||
if self.formdata.get_function_roles(role).intersection(user.get_roles()):
|
||||
break
|
||||
else:
|
||||
if not ('_signed_calls' in self.trigger.roles and is_url_signed()):
|
||||
raise errors.AccessForbiddenError('insufficient roles')
|
||||
if not self.trigger.check_executable(self.formdata, user):
|
||||
raise errors.AccessForbiddenError('insufficient roles')
|
||||
|
||||
workflow_data = get_request().json if hasattr(get_request(), '_json') else None
|
||||
self.formdata.evolution[-1].add_part(
|
||||
|
|
|
@ -35,6 +35,7 @@ from quixote import get_publisher, get_request, get_response, get_session
|
|||
from quixote.html import TemplateIO, htmlescape, htmltext
|
||||
|
||||
import wcs.qommon.storage as st
|
||||
from wcs.api_utils import is_url_signed
|
||||
from wcs.qommon.storage import StorableObject, atomic_write
|
||||
from wcs.sql_criterias import Contains, LessOrEqual, Null, StatusReachedTimeoutCriteria, StrictNotEqual
|
||||
|
||||
|
@ -2424,6 +2425,25 @@ class WorkflowGlobalActionWebserviceTrigger(WorkflowGlobalActionManualTrigger):
|
|||
def get_dependencies(self):
|
||||
return []
|
||||
|
||||
def check_executable(self, formdata, user):
|
||||
if self.roles is None:
|
||||
return True
|
||||
|
||||
for role in self.roles: # noqa pylint: disable=not-an-iterable
|
||||
if role == logged_users_role().id and (user or is_url_signed()):
|
||||
return True
|
||||
if role == '_submitter' and formdata.is_submitter(user):
|
||||
return True
|
||||
if not user:
|
||||
continue
|
||||
if formdata.get_function_roles(role).intersection(user.get_roles()):
|
||||
return True
|
||||
|
||||
if '_signed_calls' in self.roles and is_url_signed():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class SerieOfActionsMixin:
|
||||
items = None
|
||||
|
|
Loading…
Reference in New Issue