From 30143ea1222b603d8edc6de749fecfac7778cd83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 8 Aug 2022 18:20:52 +0200
Subject: [PATCH] api: include roles in bundle export (#68036)

---
 tests/api/test_export_import.py | 29 +++++++++++++++++++++++------
 wcs/api_export_import.py        | 19 ++++++++++++++++---
 wcs/roles.py                    |  4 ++++
 wcs/wf/dispatch.py              | 12 +++++++++++-
 wcs/wf/form.py                  |  1 +
 wcs/wf/roles.py                 |  5 ++++-
 wcs/wf/sendmail.py              |  1 +
 wcs/workflows.py                | 30 +++++++++++++++++++++++++++++-
 8 files changed, 89 insertions(+), 12 deletions(-)

diff --git a/tests/api/test_export_import.py b/tests/api/test_export_import.py
index 450ae3c9b..6c606d044 100644
--- a/tests/api/test_export_import.py
+++ b/tests/api/test_export_import.py
@@ -91,6 +91,9 @@ def test_export_import_form_dependencies_404(pub):
 
 
 def test_export_import_dependencies(pub):
+    role = pub.role_class(name='Test role')
+    role.store()
+
     formdef = FormDef()
     formdef.name = 'Test'
     formdef.store()
@@ -111,7 +114,8 @@ def test_export_import_dependencies(pub):
     workflow.variables_formdef.fields = [StringField(label='Test', id='1')]
 
     status = workflow.add_status('New')
-    status.add_action('form')
+    action = status.add_action('form')
+    action.by = [role.id]
 
     data_source = NamedDataSource(name='foobar')
     data_source.store()
@@ -121,6 +125,7 @@ def test_export_import_dependencies(pub):
     display_form.formdef.fields.append(StringField(label='Test', data_source={'type': 'foobar'}))
 
     send_mail = status.add_action('sendmail')
+    send_mail.to = [role.id]
 
     workflow.store()
 
@@ -132,16 +137,24 @@ def test_export_import_dependencies(pub):
 
     resp = get_app(pub).get(sign_uri('/api/export-import/forms/'))
     resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
-    assert resp.json['data']
     assert {(x['id'], x['type']) for x in resp.json['data']} == {('test', 'workflows'), ('test', 'blocks')}
+    for dependency in resp.json['data']:
+        get_app(pub).get(sign_uri(dependency['urls']['export']))
 
     resp = get_app(pub).get(sign_uri('/api/export-import/workflows/'))
     resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
-    assert resp.json['data']
     assert {(x['id'], x['type']) for x in resp.json['data']} == {
         ('foobar', 'data-sources'),
         ('test', 'blocks'),
+        ('test-role', 'roles'),
     }
+    for dependency in resp.json['data']:
+        resp = get_app(pub).get(sign_uri(dependency['urls']['export']))
+        if 'test-role' in dependency['urls']['export']:
+            assert resp.json == {'name': 'Test role', 'slug': 'test-role', 'uuid': None}
+            assert resp.content_type == 'application/json'
+        else:
+            assert resp.content_type == 'text/xml'
 
     mail_template = MailTemplate(name='test mail template')
     mail_template.store()
@@ -149,12 +162,14 @@ def test_export_import_dependencies(pub):
     workflow.store()
     resp = get_app(pub).get(sign_uri('/api/export-import/workflows/'))
     resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
-    assert resp.json['data']
     assert {(x['id'], x['type']) for x in resp.json['data']} == {
         ('foobar', 'data-sources'),
         ('test', 'blocks'),
         ('test-mail-template', 'mail-templates'),
+        ('test-role', 'roles'),
     }
+    for dependency in resp.json['data']:
+        get_app(pub).get(sign_uri(dependency['urls']['export']))
     resp = get_app(pub).get(sign_uri(resp.json['data'][-1]['urls']['dependencies']))
     assert resp.json['data'] == []
 
@@ -165,8 +180,9 @@ def test_export_import_dependencies(pub):
     resp = get_app(pub).get(sign_uri('/api/export-import/workflows/'))
     resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
     resp = get_app(pub).get(sign_uri(resp.json['data'][-1]['urls']['dependencies']))
-    assert resp.json['data']
     assert {(x['id'], x['type']) for x in resp.json['data']} == {('cat', 'mail-templates-categories')}
+    for dependency in resp.json['data']:
+        get_app(pub).get(sign_uri(dependency['urls']['export']))
 
     resp = get_app(pub).get(sign_uri('/api/export-import/workflows/'))
     resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
@@ -179,7 +195,8 @@ def test_export_import_dependencies(pub):
     data_source.store()
     resp = get_app(pub).get(sign_uri('/api/export-import/workflows/'))
     resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
-    resp = get_app(pub).get(sign_uri(resp.json['data'][1]['urls']['dependencies']))
+    data_sources_entry = [x for x in resp.json['data'] if x['type'] == 'data-sources'][0]
+    resp = get_app(pub).get(sign_uri(data_sources_entry['urls']['dependencies']))
     assert {(x['id'], x['type']) for x in resp.json['data']} == {('cat', 'data-sources-categories')}
 
 
diff --git a/wcs/api_export_import.py b/wcs/api_export_import.py
index 3a0ebe394..ba5f454a4 100644
--- a/wcs/api_export_import.py
+++ b/wcs/api_export_import.py
@@ -36,6 +36,7 @@ from wcs.categories import (
 from wcs.data_sources import NamedDataSource, StubNamedDataSource
 from wcs.formdef import FormDef
 from wcs.mail_templates import MailTemplate
+from wcs.sql import Role
 from wcs.workflows import Workflow
 from wcs.wscalls import NamedWsCall
 
@@ -52,6 +53,7 @@ klasses = {
     'data-sources-categories': DataSourceCategory,
     'forms-categories': Category,
     'forms': FormDef,
+    'roles': Role,
     'mail-templates-categories': MailTemplateCategory,
     'mail-templates': MailTemplate,
     'workflows-categories': WorkflowCategory,
@@ -117,6 +119,12 @@ def index(request):
             'singular': _('Category (data Sources)'),
             'minor': True,
         },
+        {
+            'id': 'roles',
+            'text': _('Roles'),
+            'singular': _('Role'),
+            'minor': True,
+        },
     ]
     for obj in response:
         obj['urls'] = {
@@ -166,9 +174,14 @@ def object_export(request, objects, slug):
     obj = get_object(objects, slug)
     if obj is None:
         raise Http404()
-    etree = obj.export_to_xml(include_id=True)
-    indent_xml(etree)
-    return HttpResponse(ET.tostring(etree), content_type='text/xml')
+    if hasattr(obj, 'export_for_application'):
+        content, content_type = obj.export_for_application()
+    else:
+        etree = obj.export_to_xml(include_id=True)
+        indent_xml(etree)
+        content = ET.tostring(etree)
+        content_type = 'text/xml'
+    return HttpResponse(content, content_type=content_type)
 
 
 @signature_required
diff --git a/wcs/roles.py b/wcs/roles.py
index 29b346631..5ea555041 100644
--- a/wcs/roles.py
+++ b/wcs/roles.py
@@ -14,6 +14,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+import json
 import urllib.parse
 import xml.etree.ElementTree as ET
 
@@ -114,6 +115,9 @@ class Role(StorableObject):
             ET.SubElement(root, boolean_attribute).text = value
         return root
 
+    def export_for_application(self):
+        return (json.dumps({'name': self.name, 'slug': self.slug, 'uuid': self.uuid}), 'application/json')
+
     @classmethod
     def import_from_xml(cls, fd, charset=None, include_id=False):
         try:
diff --git a/wcs/wf/dispatch.py b/wcs/wf/dispatch.py
index bcd684017..a8ec03749 100644
--- a/wcs/wf/dispatch.py
+++ b/wcs/wf/dispatch.py
@@ -21,7 +21,13 @@ from quixote import get_publisher
 from quixote.html import htmltext
 
 from wcs.roles import get_user_roles
-from wcs.workflows import WorkflowStatusItem, XmlSerialisable, get_role_name_and_slug, register_item_class
+from wcs.workflows import (
+    WorkflowStatusItem,
+    XmlSerialisable,
+    get_role_dependencies,
+    get_role_name_and_slug,
+    register_item_class,
+)
 
 from ..qommon import _
 from ..qommon.form import (
@@ -136,6 +142,10 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem):
         if rules:
             self.rules = rules
 
+    def get_dependencies(self):
+        yield from get_role_dependencies([self.role_id])
+        yield from get_role_dependencies([x.get('role_id') for x in self.rules])
+
     def get_line_details(self):
         if self.role_key:
             function_label = self.parent.parent.roles.get(self.role_key, '?')
diff --git a/wcs/wf/form.py b/wcs/wf/form.py
index 11fb2ae28..67d6459a9 100644
--- a/wcs/wf/form.py
+++ b/wcs/wf/form.py
@@ -193,6 +193,7 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
         return changed
 
     def get_dependencies(self):
+        yield from super().get_dependencies()
         if self.formdef and self.formdef.fields:
             for field in self.formdef.fields:
                 yield from field.get_dependencies()
diff --git a/wcs/wf/roles.py b/wcs/wf/roles.py
index a4c5eca8e..afd31a6a3 100644
--- a/wcs/wf/roles.py
+++ b/wcs/wf/roles.py
@@ -20,7 +20,7 @@ from quixote import get_publisher, get_request, get_response
 
 from wcs.api_utils import MissingSecret, get_secret_and_orig, sign_url
 from wcs.roles import get_user_roles
-from wcs.workflows import WorkflowStatusItem, register_item_class
+from wcs.workflows import WorkflowStatusItem, get_role_dependencies, register_item_class
 
 from ..qommon import _
 from ..qommon.form import SingleSelectWidgetWithOther
@@ -57,6 +57,9 @@ class RoleMixin:
             return role.name
         return _('unknown - %s') % self.role_id
 
+    def get_dependencies(self):
+        yield from get_role_dependencies([self.role_id])
+
 
 class AddRoleWorkflowStatusItem(RoleMixin, WorkflowStatusItem):
     description = _('Role Addition')
diff --git a/wcs/wf/sendmail.py b/wcs/wf/sendmail.py
index ce6a458c3..f93b85442 100644
--- a/wcs/wf/sendmail.py
+++ b/wcs/wf/sendmail.py
@@ -70,6 +70,7 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
     comment = None
 
     def get_dependencies(self):
+        yield from super().get_dependencies()
         yield MailTemplate.get_by_slug(self.mail_template)
 
     def _get_role_id_from_xml(self, elem, charset, include_id=False, snapshot=False):
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 274efaad2..400087172 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -142,6 +142,16 @@ class RedisplayFormException(Exception):
     pass
 
 
+def get_role_dependencies(roles):
+    for role_id in roles or []:
+        if not role_id:
+            continue
+        role_id = str(role_id)
+        if role_id.startswith('_') or role_id == 'logged-users':
+            continue
+        yield get_publisher().role_class.get(role_id, ignore_errors=True)
+
+
 class AttachmentSubstitutionProxy:
     def __init__(self, formdata, attachment_evolution_part):
         self.formdata = formdata
@@ -638,6 +648,9 @@ class Workflow(StorableObject):
         if self.possible_status:
             for status in self.possible_status:
                 yield from status.get_dependencies()
+        if self.global_actions:
+            for action in self.global_actions:
+                yield from action.get_dependencies()
 
     @classmethod
     def get(cls, id, ignore_errors=False, ignore_migration=False):
@@ -1329,6 +1342,9 @@ class WorkflowGlobalActionManualTrigger(WorkflowGlobalActionTrigger):
     def roles_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self._roles_init_with_xml('roles', elem, charset, include_id=include_id, snapshot=snapshot)
 
+    def get_dependencies(self):
+        yield from get_role_dependencies(self.roles)
+
 
 class WorkflowGlobalActionTimeoutTriggerMarker(EvolutionPart):
     def __init__(self, timeout_id):
@@ -1637,6 +1653,9 @@ class WorkflowGlobalActionTimeoutTrigger(WorkflowGlobalActionTrigger):
                         )
                         break
 
+    def get_dependencies(self):
+        return []
+
 
 class WorkflowGlobalActionWebserviceTrigger(WorkflowGlobalActionManualTrigger):
     key = 'webservice'
@@ -1673,6 +1692,9 @@ class WorkflowGlobalActionWebserviceTrigger(WorkflowGlobalActionManualTrigger):
 
         return [('hooks', WorkflowGlobalActionWebserviceHooksDirectory(formdata))]
 
+    def get_dependencies(self):
+        return []
+
 
 class SerieOfActionsMixin:
     items = None
@@ -1782,6 +1804,11 @@ class WorkflowGlobalAction(SerieOfActionsMixin):
             trigger_o.parent = self
             trigger_o.init_with_xml(trigger, charset, include_id=include_id, snapshot=snapshot)
 
+    def get_dependencies(self):
+        yield from super().get_dependencies()
+        for trigger in self.triggers or []:
+            yield from trigger.get_dependencies()
+
 
 class WorkflowCriticalityLevel:
     id = None
@@ -2194,7 +2221,8 @@ class WorkflowStatusItem(XmlSerialisable):
         return self.parent.parent.get_add_role_label()
 
     def get_dependencies(self):
-        return []
+        yield from get_role_dependencies(getattr(self, 'by', None))
+        yield from get_role_dependencies(getattr(self, 'to', None))
 
     @noop_mark
     def perform(self, formdata):
-- 
2.36.1

