[PATCH 1 of 1 eac V2] [schema] Backport security settings from the saem_ref cube

Sylvain Thenault sylvain.thenault at logilab.fr
Wed Mar 8 12:10:49 CET 2017


# HG changeset patch
# User Sylvain Thénault <sylvain.thenault at logilab.fr>
# Date 1488958771 -3600
#      Wed Mar 08 08:39:31 2017 +0100
# Node ID 22b591ad3be1570a5104add188131ff489bf2460
# Parent  47724158121da77c11fa61c543d855a7a4842fc6
[schema] Backport security settings from the saem_ref cube

introducing a dependency to the compound cube.

Benefit of doing so is that client cube only have to setup desired permission on
the container (AuthorityRecord), and all the subtree will follow those, which
sounds a good default behaviour.

diff --git a/cubicweb-eac.spec b/cubicweb-eac.spec
--- a/cubicweb-eac.spec
+++ b/cubicweb-eac.spec
@@ -22,10 +22,11 @@ BuildRoot:      %{_tmppath}/%{name}-%{ve
 BuildRequires:  %{python} %{python}-setuptools
 Requires:       cubicweb >= 3.24.0
 Requires:       cubicweb-prov >= 0.4.0
 Requires:       cubicweb-skos
 Requires:       cubicweb-addressbook
+Requires:       cubicweb-compound
 Requires:       %{python}-six >= 1.4.0
 
 %description
 Implementation of Encoded Archival Context for CubicWeb
 
diff --git a/cubicweb_eac/__init__.py b/cubicweb_eac/__init__.py
--- a/cubicweb_eac/__init__.py
+++ b/cubicweb_eac/__init__.py
@@ -1,10 +1,13 @@
 """cubicweb-eac application package
 
 Implementation of Encoded Archival Context for CubicWeb
 """
 
+from functools import partial
+from cubicweb_compound import CompositeGraph
+
 
 # EAC mappings
 
 TYPE_MAPPING = {
     'corporateBody': u'authority',
@@ -21,5 +24,8 @@ MAINTENANCETYPE_MAPPING = {
 ADDRESS_MAPPING = [
     ('StreetName', 'street'),
     ('PostCode', 'postalcode'),
     ('CityName', 'city'),
 ]
+
+
+AuthorityRecordGraph = partial(CompositeGraph, skiprtypes=('generated', 'used'))
diff --git a/cubicweb_eac/__pkginfo__.py b/cubicweb_eac/__pkginfo__.py
--- a/cubicweb_eac/__pkginfo__.py
+++ b/cubicweb_eac/__pkginfo__.py
@@ -17,10 +17,11 @@ web = 'http://www.cubicweb.org/project/%
     'cubicweb': '>= 3.24.0',
     'six': '>= 1.4.0',
     'cubicweb-prov': '>= 0.4.0',
     'cubicweb-skos': None,
     'cubicweb-addressbook': None,
+    'cubicweb-compound': None,
     'python-dateutil': None,
 }
 __recommends__ = {}
 
 classifiers = [
diff --git a/cubicweb_eac/hooks.py b/cubicweb_eac/hooks.py
--- a/cubicweb_eac/hooks.py
+++ b/cubicweb_eac/hooks.py
@@ -41,5 +41,19 @@ class EnsureVocabularySourceOp(hook.Data
     def precommit_event(self):
         cnx = self.cnx
         for eid, concept_eid in self.get_data():
             cnx.execute('SET X vocabulary_source SC WHERE NOT X vocabulary_source SC, '
                         'C in_scheme SC, C eid %(c)s, X eid %(x)s', {'x': eid, 'c': concept_eid})
+
+
+def registration_callback(vreg):
+    vreg.register_all(globals().values(), __name__)
+
+    # Add relations involved in a composite graph with security setup to "on
+    # commit" check step.
+    from cubicweb.server import ON_COMMIT_ADD_RELATIONS
+    from cubicweb_compound.utils import mandatory_rdefs
+    from cubicweb_eac import AuthorityRecordGraph
+
+    graph = AuthorityRecordGraph(vreg.schema)
+    for rdef, __ in mandatory_rdefs(vreg.schema, graph.parent_structure('AuthorityRecord')):
+        ON_COMMIT_ADD_RELATIONS.add(rdef.rtype)
diff --git a/cubicweb_eac/migration/0.5.0_Any.py b/cubicweb_eac/migration/0.5.0_Any.py
new file mode 100644
--- /dev/null
+++ b/cubicweb_eac/migration/0.5.0_Any.py
@@ -0,0 +1,1 @@
+add_cube('compound')
diff --git a/cubicweb_eac/schema.py b/cubicweb_eac/schema.py
--- a/cubicweb_eac/schema.py
+++ b/cubicweb_eac/schema.py
@@ -393,5 +393,15 @@ class equivalent_concept(RelationDefinit
     constraints = [RQLVocabularyConstraint('S vocabulary_source SC, O in_scheme SC')]
     cardinality = '?*'
     # relation with 'ExternalUri' as object can't be inlined because of a limitation of
     # data-import's (massive) store
     inlined = False
+
+
+def post_build_callback(schema):
+    from cubicweb_eac import AuthorityRecordGraph
+    from cubicweb_compound.utils import (graph_set_etypes_update_permissions,
+                                         graph_set_write_rdefs_permissions)
+
+    graph = AuthorityRecordGraph(schema)
+    graph_set_etypes_update_permissions(schema, graph, 'AuthorityRecord')
+    graph_set_write_rdefs_permissions(schema, graph, 'AuthorityRecord')
diff --git a/debian/control b/debian/control
--- a/debian/control
+++ b/debian/control
@@ -15,10 +15,11 @@ Depends:
  python-cubicweb (>= 3.24.0),
  python-six (>= 1.4.0),
  cubicweb-prov (>= 0.4.0),
  cubicweb-skos,
  cubicweb-addressbook,
+ cubicweb-compound,
  ${python:Depends},
  ${misc:Depends},
 Description: Implementation of Encoded Archival Context for CubicWeb
  CubicWeb is a semantic web application framework.
  .
diff --git a/dev-requirements.txt b/dev-requirements.txt
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,3 +1,4 @@
 pytest
 mock
 docutils
+http://hg.logilab.org/review/cubes/compound/archive/tip.tar.bz2#egg=cubicweb-compound
diff --git a/test/test_schema.py b/test/test_schema.py
--- a/test/test_schema.py
+++ b/test/test_schema.py
@@ -18,14 +18,15 @@
 import sqlite3
 import unittest
 from datetime import date
 from contextlib import contextmanager
 
-from cubicweb import ValidationError
+from cubicweb import ValidationError, Unauthorized
 from cubicweb.devtools.testlib import CubicWebTC
 
-from cubicweb_eac import testutils
+from cubicweb_compound.utils import optional_relations, graph_relations
+from cubicweb_eac import testutils, AuthorityRecordGraph
 
 
 @contextmanager
 def assertValidationError(self, cnx):
     with self.assertRaises(ValidationError) as cm:
@@ -77,7 +78,115 @@ class SchemaConstraintsTC(CubicWebTC):
             with self.assertValidationError(cnx) as cm:
                 agent.cw_set(start_date=date(527, 4, 12))
             self.assertIn("must be less than", str(cm.exception))
 
 
+class AuthorityRecordGraphTC(CubicWebTC):
+
+    def test_graph_structure(self):
+        graph = AuthorityRecordGraph(self.schema)
+        expected = {
+            'AgentFunction': {('function_agent', 'subject'): set(['AuthorityRecord'])},
+            'AgentPlace': {('place_agent', 'subject'): set(['AuthorityRecord'])},
+            'Citation': {('has_citation', 'object'): set([
+                'GeneralContext', 'Mandate', 'Occupation', 'AgentFunction',
+                'AgentPlace', 'History', 'LegalStatus',
+            ])},
+            'EACOtherRecordId': {('eac_other_record_id_of', 'subject'):
+                                 set(['AuthorityRecord'])},
+            'EACResourceRelation': {('resource_relation_agent', 'subject'):
+                                    set(['AuthorityRecord'])},
+            'EACSource': {('source_agent', 'subject'): set(['AuthorityRecord'])},
+            'GeneralContext': {('general_context_of', 'subject'): set(['AuthorityRecord'])},
+            'History': {('history_agent', 'subject'): set(['AuthorityRecord'])},
+            'LegalStatus': {('legal_status_agent', 'subject'): set(['AuthorityRecord'])},
+            'Mandate': {('mandate_agent', 'subject'): set(['AuthorityRecord'])},
+            'NameEntry': {('name_entry_for', 'subject'): set(['AuthorityRecord'])},
+            'Occupation': {('occupation_agent', 'subject'): set(['AuthorityRecord'])},
+            'PostalAddress': {('place_address', 'object'): set(['AgentPlace'])},
+            'Structure': {('structure_agent', 'subject'): set(['AuthorityRecord'])},
+        }
+        struct = dict(
+            (k, dict((rel, set(targets)) for rel, targets in v.items()))
+            for k, v in graph.parent_structure('AuthorityRecord').items())
+        self.assertEqual(struct, expected)
+
+    def test_optional_relations(self):
+        graph = AuthorityRecordGraph(self.schema)
+        structure = graph.parent_structure('AuthorityRecord')
+        opts = optional_relations(self.schema, structure)
+        expected = {}
+        self.assertEqual(opts, expected)
+
+    def test_relations_consistency(self):
+        graph = AuthorityRecordGraph(self.schema)
+        structure = graph.parent_structure('AuthorityRecord')
+        structurals, optionals, mandatories = graph_relations(
+            self.schema, structure)
+        self.assertEqual(structurals - optionals, mandatories)
+
+
+class SecurityTC(CubicWebTC):
+    """Test case for permissions set in the schema"""
+
+    @contextmanager
+    def assertUnauthorized(self, cnx):
+        with self.assertRaises(Unauthorized) as cm:
+            yield cm
+            cnx.commit()
+        cnx.rollback()
+
+    def test_agentkind_type(self):
+        with self.admin_access.cnx() as cnx:
+            kind = cnx.find('AgentKind', name=u'person').one()
+            # no one can update nor delete a kind
+            with self.assertUnauthorized(cnx):
+                kind.cw_set(name=u'gloups')
+            with self.assertUnauthorized(cnx):
+                kind.cw_delete()
+
+        with self.admin_access.cnx() as cnx:
+            self.create_user(cnx, login=u'toto', groups=('users', 'guests'))
+            cnx.commit()
+        with self.new_access('toto').cnx() as cnx:
+            cnx.create_entity('AgentKind', name=u'new')
+
+    def test_agent_kind_relation(self):
+        """Test we can only change kind from unknown to another."""
+        with self.admin_access.cnx() as cnx:
+            record = testutils.authority_record(cnx, u'bob', kind=u'unknown-agent-kind')
+            cnx.commit()
+            record.cw_set(agent_kind=cnx.find('AgentKind', name=u'person').one())
+            cnx.commit()
+            with self.assertRaises(Unauthorized):
+                record.cw_set(agent_kind=cnx.find('AgentKind', name=u'authority').one())
+
+    def test_authority_record_base(self):
+        with self.admin_access.cnx() as cnx:
+            self.create_user(cnx, login=u'toto', groups=('users', 'guests'))
+            function = cnx.create_entity('AgentFunction', name=u'director')
+            testutils.authority_record(cnx, u'admin record', reverse_function_agent=function)
+            cnx.commit()
+
+        with self.new_access('toto').cnx() as cnx:
+            # user can create and modify its own records
+            function = cnx.create_entity('AgentFunction', name=u'grouillot')
+            record = testutils.authority_record(cnx, u'bob', reverse_function_agent=function)
+            cnx.commit()
+            function.cw_set(name=u'grouyo')
+            cnx.commit()
+            cnx.create_entity('GeneralContext', content=u'plop',
+                              general_context_of=record)
+            cnx.commit()
+            # but not modify other's
+            record = cnx.find('AuthorityRecord', has_text=u'admin record').one()
+            with self.assertUnauthorized(cnx):
+                record.cw_set(record_id=u'bobby')
+            with self.assertUnauthorized(cnx):
+                record.reverse_function_agent[0].cw_set(name=u'grouillot')
+            with self.assertUnauthorized(cnx):
+                function = cnx.create_entity('AgentFunction', name=u'grouillot')
+                record.cw_set(reverse_function_agent=function)
+
+
 if __name__ == '__main__':
     unittest.main()


More information about the saem-devel mailing list