oldaplib 0.3.30__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
1
  import re
2
+ import textwrap
3
+
2
4
  import jwt
3
5
 
4
6
  from datetime import datetime, timedelta
5
7
  from enum import Flag, auto
6
8
  from functools import partial
7
- from typing import Type, Any, Self, cast
9
+ from typing import Type, Any, Self, cast, Dict
8
10
 
9
11
  from oldaplib.src.datamodel import DataModel
10
12
  from oldaplib.src.enums.action import Action
@@ -18,6 +20,7 @@ from oldaplib.src.helpers.attributechange import AttributeChange
18
20
  from oldaplib.src.helpers.context import Context
19
21
  from oldaplib.src.helpers.convert2datatype import convert2datatype
20
22
  from oldaplib.src.helpers.langstring import LangString
23
+ from oldaplib.src.helpers.observable_dict import ObservableDict
21
24
  from oldaplib.src.helpers.observable_set import ObservableSet
22
25
  from oldaplib.src.helpers.oldaperror import OldapErrorNotFound, OldapErrorValue, OldapErrorInconsistency, \
23
26
  OldapErrorNoPermission, OldapError, OldapErrorUpdateFailed, OldapErrorInUse, OldapErrorAlreadyExists
@@ -34,7 +37,7 @@ from oldaplib.src.xsd.xsd_ncname import Xsd_NCName
34
37
  from oldaplib.src.xsd.xsd_qname import Xsd_QName
35
38
  from oldaplib.src.xsd.xsd_string import Xsd_string
36
39
 
37
- ValueType = LangString | ObservableSet | Xsd
40
+ ValueType = LangString | ObservableSet | Xsd | Dict[Xsd_QName, DataPermission] | ObservableDict
38
41
 
39
42
  class SortBy(Flag):
40
43
  PROPVAL = auto()
@@ -82,12 +85,13 @@ class ResourceInstance:
82
85
  :type changeset: dict[Iri, AttributeChange]
83
86
  """
84
87
  _iri: Iri
85
- _values: dict[Xsd_QName, LangString | ObservableSet]
88
+ _values: dict[Xsd_QName, LangString | ObservableSet | DataPermission]
86
89
  _graph: Xsd_NCName
87
- _changeset: dict[Iri, AttributeChange]
90
+ _changeset: dict[Xsd_QName, AttributeChange]
91
+ _attached_roles: ObservableDict
88
92
 
89
- __slots__ = ['_iri', '_values', '_graph', '_changeset', '_superclass_objs',
90
- '_con', 'project', 'name', 'factory', 'properties', 'superclass']
93
+ __slots__ = ['_iri', '_values', '_graph', '_changeset', '_superclass_objs', '_con', '_attached_roles',
94
+ 'project', 'name', 'factory', 'properties', 'superclass', 'user_default_roles']
91
95
 
92
96
 
93
97
  def __init__(self, *,
@@ -112,10 +116,32 @@ class ResourceInstance:
112
116
  self._values: dict[Xsd_QName, ValueType] = {}
113
117
  self._graph = self.project.projectShortName
114
118
  self._superclass_objs = {}
115
- self._changeset = {}
119
+ self._changeset: dict[Xsd_QName, AttributeChange] = {}
120
+ self._attached_roles = ObservableDict(on_change=self.__attachedToRole_cb)
116
121
 
117
122
  def set_values(propclass: dict[Xsd_QName, HasProperty]):
118
123
  for prop_iri, hasprop in propclass.items():
124
+ if str(prop_iri) == 'oldap:attachedToRole':
125
+ if kwargs.get(str(prop_iri)) or kwargs.get(prop_iri.fragment):
126
+ #
127
+ # we have an attachedToRole property given in the constructor...
128
+ #
129
+ value = kwargs[str(prop_iri)] if kwargs.get(str(prop_iri)) else kwargs[prop_iri.fragment]
130
+ value = {Xsd_QName(role): dperm if isinstance(dperm, DataPermission) else DataPermission.from_string(dperm) for role, dperm in value.items()}
131
+ if not isinstance(value, dict):
132
+ raise OldapErrorValue(f'{self.name}: Property {prop_iri} with attachedToRole must be a dict')
133
+ self._attached_roles = ObservableDict(value, on_change=self.__attachedToRole_cb)
134
+ self._values[prop_iri] = ObservableSet({Xsd_QName(x, validate=True) for x in value.keys()},
135
+ notifier=self.notifier, notify_data=prop_iri)
136
+ else:
137
+ #
138
+ # we take the user default values...
139
+ #
140
+ self._values[prop_iri] = ObservableSet({Xsd_QName(x) for x in self.user_default_roles.keys()},
141
+ notifier=self.notifier, notify_data=prop_iri)
142
+ self._attached_roles = ObservableDict(self.user_default_roles, on_change=self.__attachedToRole_cb)
143
+
144
+ continue
119
145
  if kwargs.get(str(prop_iri)) or kwargs.get(prop_iri.fragment):
120
146
  value = kwargs[str(prop_iri)] if kwargs.get(str(prop_iri)) else kwargs[prop_iri.fragment]
121
147
  if isinstance(value, (list, tuple, set, LangString)): # we may have multiple values...
@@ -133,7 +159,7 @@ class ResourceInstance:
133
159
 
134
160
  for prop_iri, hasprop in propclass.items():
135
161
  #
136
- # Validate
162
+ # Validate cardinalities
137
163
  #
138
164
  if hasprop.get(HasPropertyAttr.MIN_COUNT): # testing for MIN_COUNT conformance
139
165
  if hasprop[HasPropertyAttr.MIN_COUNT] > 0 and not self._values.get(prop_iri):
@@ -203,7 +229,7 @@ class ResourceInstance:
203
229
 
204
230
  def get(self, key: Xsd_QName) -> ValueType | Xsd | None:
205
231
  if self._values.get(key):
206
- return self._values[key]
232
+ return self.__get_value(key)
207
233
  else:
208
234
  return None
209
235
 
@@ -360,6 +386,11 @@ class ResourceInstance:
360
386
 
361
387
  self._changeset[prop_iri] = AttributeChange(None, Action.MODIFY)
362
388
 
389
+ def __attachedToRole_cb(self, old_value: ObservableDict):
390
+ if self._changeset.get(Xsd_QName('oldap:attachedToRole')) is None:
391
+ self._changeset[Xsd_QName('oldap:attachedToRole')] = AttributeChange(old_value, Action.MODIFY)
392
+
393
+
363
394
  def check_for_permissions(self, permission: AdminPermission) -> tuple[bool, str]:
364
395
  #
365
396
  # First we check if the logged-in user ("actor") has the permission to create a user for
@@ -383,14 +414,36 @@ class ResourceInstance:
383
414
  #attr = Xsd_QName(prefix, fragment, validate=False)
384
415
  if not isinstance(attr, Xsd_QName):
385
416
  attr = Xsd_QName(attr)
417
+ if attr == Xsd_QName('oldap:attachedToRole'):
418
+ return self._attached_roles
386
419
  tmp = self._values.get(attr, None)
387
420
  if tmp is not None and str(attr) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModifiedBy', 'oldap:lastModificationDate'}:
421
+ if len(tmp) != 1:
422
+ raise OldapErrorValue(f'{self.name}: Property {attr} should have exactly one value, got {len(tmp)} values.')
388
423
  return next(iter(tmp))
389
424
  return tmp
390
425
 
391
426
  def __set_value(self: Self, value: ValueType | Xsd | None, attr: Xsd_QName | str) -> None:
392
427
  if not isinstance(attr, Xsd_QName):
393
428
  attr = Xsd_QName(attr)
429
+
430
+ if attr == Xsd_QName('oldap:attachedToRole'):
431
+ if value is None:
432
+ self._changeset[attr] = AttributeChange(self._values.get(attr), Action.DELETE)
433
+ del self._values[attr]
434
+ self._attached_roles = ObservableDict(notifier=self.__attachedToRole_cb)
435
+ return
436
+ if not isinstance(value, dict):
437
+ raise OldapErrorValue(f'{self.name}: Property {attr} requires a dict, got {type(value).__name__}.')
438
+ for role_qname in value.keys():
439
+ if not isinstance(role_qname, Xsd_QName):
440
+ raise OldapErrorValue(f'{self.name}: Property {attr} requires keys to be Xsd_QName, got {type(role_qname).__name__}.')
441
+ if not isinstance(value[role_qname], DataPermission):
442
+ raise OldapErrorValue(f'{self.name}: Property {attr} requires values to be DataPermission, got {type(value[role_qname]).__name__}.')
443
+ self._changeset[attr] = AttributeChange(self._values.get(attr), Action.REPLACE)
444
+ self._values[attr] = ObservableSet({value.keys()}, notifier=self.notifier, notify_data=attr)
445
+ self._attached_roles = ObservableDict(value, notifier=self.__attachedToRole_cb)
446
+ return
394
447
  hasprop = self.properties.get(attr)
395
448
 
396
449
  #
@@ -444,6 +497,12 @@ class ResourceInstance:
444
497
  def __del_value(self: Self, attr: Xsd_QName | str) -> None:
445
498
  if not isinstance(attr, Xsd_QName):
446
499
  attr = Xsd_QName(attr)
500
+
501
+ if attr == Xsd_QName('oldap:attachedToRole'):
502
+ self._changeset[attr] = AttributeChange(self._values.get(attr), Action.DELETE)
503
+ del self._values[attr]
504
+ self._attached_roles = ObservableDict(notifier=self.__attachedToRole_cb)
505
+ return
447
506
  hasprop = self.properties.get(attr)
448
507
 
449
508
  if hasprop.get(HasPropertyAttr.MIN_COUNT): # testing for MIN_COUNT conformance
@@ -461,6 +520,10 @@ class ResourceInstance:
461
520
  def changeset(self) -> dict[Xsd_QName, AttributeChange]:
462
521
  return self._changeset
463
522
 
523
+ @property
524
+ def attachedToRoleAnnotation(self) -> ObservableDict:
525
+ return self._attached_roles
526
+
464
527
  def clear_changeset(self) -> None:
465
528
  for item in self._values:
466
529
  if hasattr(self._values[item], 'clear_changeset'):
@@ -468,21 +531,29 @@ class ResourceInstance:
468
531
  self._changeset = {}
469
532
 
470
533
 
471
- def get_data_permission(self, context: Context, permission: DataPermission) -> bool:
534
+ def get_data_permission(self, permission: DataPermission) -> bool:
535
+ context = Context(name=self._con.context_name)
472
536
  permission_query = context.sparql_context
473
- permission_query += f'''
537
+ permission_query += textwrap.dedent(f'''
474
538
  ASK {{
475
- GRAPH {self._graph}:data {{
476
- {self._iri.toRdf} oldap:grantsPermission ?permset .
539
+ {{
540
+ GRAPH {self._graph}:data {{
541
+ {self._iri.toRdf} oldap:createdBy {self._con.userIri.toRdf} .
542
+ }}
477
543
  }}
478
- GRAPH oldap:admin {{
479
- {self._con.userIri.toRdf} oldap:hasPermissions ?permset .
480
- ?permset oldap:givesPermission ?DataPermission .
481
- ?DataPermission oldap:permissionValue ?permval .
544
+ UNION
545
+ {{
546
+ GRAPH oldap:admin {{
547
+ {self._con.userIri.toRdf} oldap:hasRole ?role .
548
+ ?dataperm oldap:permissionValue ?permval .
549
+ FILTER(?permval >= {permission.numeric.toRdf})
550
+ }}
551
+ GRAPH {self._graph}:data {{
552
+ {self._iri.toRdf} oldap:attachedToRole ?role .
553
+ <<{self._iri.toRdf} oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
554
+ }}
482
555
  }}
483
- FILTER(?permval >= {permission.numeric.toRdf})
484
- }}'''
485
-
556
+ }}''')
486
557
  if self._con.in_transaction():
487
558
  result = self._con.transaction_query(permission_query)
488
559
  else:
@@ -539,7 +610,6 @@ class ResourceInstance:
539
610
  sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{'
540
611
 
541
612
  sparql += f'\n{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} a {self.name}'
542
-
543
613
  for prop_iri, values in self._values.items():
544
614
  if self.properties.get(prop_iri) and self.properties[prop_iri].prop.datatype == XsdDatatypes.QName:
545
615
  qnames = {f'"{x}"^^xsd:QName' for x in values}
@@ -547,9 +617,11 @@ class ResourceInstance:
547
617
  sparql += f' ;\n{blank:{(indent + 3) * indent_inc}}{prop_iri.toRdf} {qnames_rdf}'
548
618
  else:
549
619
  sparql += f' ;\n{blank:{(indent + 3) * indent_inc}}{prop_iri.toRdf} {values.toRdf}'
620
+ for role, dperm in self._attached_roles.items():
621
+ sparql += f' .\n{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} oldap:attachedToRole {role.toRdf}>> oldap:hasDataPermission {dperm.toRdf}'
622
+
550
623
  sparql += f' .\n{blank:{(indent + 1) * indent_inc}}}}\n'
551
624
  sparql += f'{blank:{indent * indent_inc}}}}\n'
552
-
553
625
  self._con.transaction_start()
554
626
  try:
555
627
  result = self._con.transaction_query(sparql0)
@@ -584,20 +656,30 @@ class ResourceInstance:
584
656
  graph = cls.project.projectShortName
585
657
  context = Context(name=con.context_name)
586
658
  sparql = context.sparql_context
587
- sparql += f'''
588
- SELECT ?predicate ?value
659
+ sparql += textwrap.dedent(f'''
660
+ SELECT DISTINCT ?predicate ?value
589
661
  WHERE {{
662
+ {{
663
+ GRAPH {graph}:data {{
664
+ {iri.toRdf} oldap:createdBy {con.userIri.toRdf} .
665
+ }}
666
+ }}
667
+ UNION
668
+ {{
669
+ GRAPH oldap:admin {{
670
+ {con.userIri.toRdf} oldap:hasRole ?role .
671
+ ?dataperm oldap:permissionValue ?permval .
672
+ FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})
673
+ }}
674
+ }}
590
675
  GRAPH {graph}:data {{
591
676
  {iri.toRdf} ?predicate ?value .
592
- {iri.toRdf} oldap:grantsPermission ?permset .
677
+ {iri.toRdf} oldap:attachedToRole ?role .
678
+ <<{iri.toRdf} oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
593
679
  }}
594
- GRAPH oldap:admin {{
595
- {con.userIri.toRdf} oldap:hasPermissions ?permset .
596
- ?permset oldap:givesPermission ?DataPermission .
597
- ?DataPermission oldap:permissionValue ?permval .
598
- }}
599
- FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})
600
- }}'''
680
+
681
+ }}
682
+ ''')
601
683
  jsonres = con.query(sparql)
602
684
  res = QueryProcessor(context, jsonres)
603
685
  objtype = None
@@ -622,8 +704,25 @@ class ResourceInstance:
622
704
  kwargs[r['predicate'].as_qname.fragment] = r['value']
623
705
  else:
624
706
  raise OldapErrorInconsistency(f"Expected QName as predicate, got {r['predicate']}")
707
+
625
708
  if objtype is None:
626
709
  raise OldapErrorNotFound(f'Resource with iri <{iri}> not found.')
710
+ sparql = context.sparql_context
711
+ sparql += textwrap.dedent(f'''
712
+ SELECT DISTINCT ?role ?dataperm
713
+ WHERE {{
714
+ GRAPH {graph}:data {{
715
+ << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
716
+ }}
717
+ }}
718
+ ''')
719
+ jsonres = con.query(sparql)
720
+ res = QueryProcessor(context, jsonres)
721
+ roles = {}
722
+ for r in res:
723
+ roles[r['role']] = DataPermission.from_qname(r['dataperm'])
724
+ kwargs['attachedToRole'] = roles
725
+
627
726
  if cls.__name__ != objtype:
628
727
  raise OldapErrorInconsistency(f'Expected class {cls.__name__}, got {objtype} instead.')
629
728
  return cls(iri=iri, **kwargs)
@@ -658,8 +757,9 @@ class ResourceInstance:
658
757
  sparql_list = []
659
758
  required_permission = DataPermission.DATA_EXTEND
660
759
  for field, change in self._changeset.items():
661
- if field == 'oldap:grantsPermission':
760
+ if field == 'oldap:attachedToRole':
662
761
  required_permission = DataPermission.DATA_PERMISSIONS
762
+ continue
663
763
  if change.action == Action.MODIFY:
664
764
  continue # will be processed below!
665
765
  if change.action != Action.CREATE:
@@ -695,6 +795,77 @@ class ResourceInstance:
695
795
  sparql_list.append(sparql)
696
796
 
697
797
  for field, change in self._changeset.items():
798
+ if field == 'oldap:attachedToRole':
799
+ sparql = f'# Processing field "{field}"\n'
800
+ add_sparql = False
801
+ if (change.action == Action.DELETE or change.action == Action.REPLACE) and len(change.old_value) > 0:
802
+ sparql += f'{blank:{indent * indent_inc}}DELETE DATA {{\n'
803
+ sparql += f'{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{\n'
804
+ for role, dperm in change.old_value.items():
805
+ sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
806
+ sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission {dperm.toRdf} .\n'
807
+ sparql += f'{blank:{(indent + 1) * indent_inc}}}}\n'
808
+ sparql += f'{blank:{indent * indent_inc}}}}\n'
809
+ add_sparql = True
810
+ elif (change.action == Action.CREATE or change.action == Action.REPLACE) and len(self._attached_roles) > 0:
811
+ sparql += f'{blank:{indent * indent_inc}}INSERT DATA {{\n'
812
+ sparql += f'{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{\n'
813
+ for role, dperm in self._attached_roles.items():
814
+ sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
815
+ sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission {dperm.toRdf} .\n'
816
+ sparql += f'{blank:{(indent + 1) * indent_inc}}}}\n'
817
+ sparql += f'{blank:{indent * indent_inc}}}}\n'
818
+ add_sparql = True
819
+ elif change.action == Action.MODIFY:
820
+ added = {}
821
+ removed = {}
822
+ changed = {}
823
+ if change.old_value:
824
+ added = {key: self._attached_roles[key] for key in self._attached_roles.keys() - change.old_value.keys()}
825
+ else:
826
+ added = self._attached_roles
827
+ if change.old_value:
828
+ removed = {key: change.old_value[key] for key in
829
+ change.old_value.keys() - self._attached_roles.keys()}
830
+ else:
831
+ removed = {}
832
+ if change.old_value:
833
+ changed = {key: {'old': change.old_value.get(key),
834
+ 'new': self._attached_roles.get(key)}
835
+ for key in change.old_value.keys() & self._attached_roles.keys()
836
+ if self._attached_roles[key] != change.old_value[key]}
837
+ else:
838
+ changed = {}
839
+ if len(removed) > 0 or len(changed) > 0:
840
+ sparql += f'{blank:{indent * indent_inc}}DELETE DATA {{\n'
841
+ sparql += f'{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{\n'
842
+ for role, dperm in removed.items():
843
+ sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
844
+ sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission {dperm.toRdf} .\n'
845
+ for role, data in changed.items():
846
+ sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
847
+ sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission {data['old'].toRdf} .\n'
848
+ sparql += f'{blank:{(indent + 1) * indent_inc}}}}\n'
849
+ sparql += f'{blank:{indent * indent_inc}}}}\n'
850
+ add_sparql = True
851
+ if len(added) > 0 or len(changed) > 0:
852
+ if add_sparql:
853
+ sparql += f'{blank:{indent * indent_inc}}\n;\n'
854
+ sparql += f'{blank:{indent * indent_inc}}INSERT DATA {{\n'
855
+ sparql += f'{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{\n'
856
+ for role, dperm in added.items():
857
+ sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
858
+ sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission {dperm.toRdf} .\n'
859
+ for role, data in changed.items():
860
+ sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
861
+ sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission {data['old'].toRdf} .\n'
862
+ sparql += f'{blank:{(indent + 1) * indent_inc}}}}\n'
863
+ sparql += f'{blank:{indent * indent_inc}}}}\n'
864
+ add_sparql = True
865
+ if add_sparql:
866
+ sparql_list.append(sparql)
867
+ continue
868
+
698
869
  if change.action != Action.MODIFY:
699
870
  continue # has been processed above
700
871
  if self.properties[field].prop.datatype == XsdDatatypes.langString:
@@ -754,7 +925,7 @@ class ResourceInstance:
754
925
  # Test permission for Action.REPLACE
755
926
  #
756
927
  if not admin_resources:
757
- if not self.get_data_permission(context, required_permission):
928
+ if not self.get_data_permission(required_permission):
758
929
  self._con.transaction_abort()
759
930
  raise OldapErrorNoPermission(f'No permission to update resource "{self._iri}"')
760
931
  try:
@@ -810,13 +981,14 @@ class ResourceInstance:
810
981
  DELETE WHERE {{
811
982
  GRAPH {self._graph}:data {{
812
983
  {self._iri.toRdf} ?prop ?val .
984
+ << {self._iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
813
985
  }}
814
986
  }}
815
987
  """
816
988
 
817
989
  self._con.transaction_start()
818
990
  if not admin_resources:
819
- if not self.get_data_permission(context, DataPermission.DATA_DELETE):
991
+ if not self.get_data_permission(DataPermission.DATA_DELETE):
820
992
  self._con.transaction_abort()
821
993
  raise OldapErrorNoPermission(f'No permission to update resource "{self._iri}"')
822
994
  try:
@@ -840,21 +1012,27 @@ class ResourceInstance:
840
1012
  @staticmethod
841
1013
  def read_data(con: IConnection, projectShortName: Xsd_NCName | str, iri: Iri | str) -> dict[Xsd_QName, Any]:
842
1014
  """
843
- Reads and processes data from a given IConnection instance based on a specified
844
- project short name and a resource IRI. This method fetches the data from a graph
845
- in the RDF database, verifies user permissions, and returns selected data in a
846
- dictionary format.
1015
+ Retrieves data from a resource in the specified project with permissions validation.
1016
+ NOTE: It does *NOT* return the attachedToRole data!
847
1017
 
848
- :param con: The connection object used to query the RDF data.
1018
+ This function performs a SPARQL query to fetch data associated with the given
1019
+ IRI within a specific project graph, taking into account user permissions for
1020
+ the resource. If no data is found for the given IRI, or if permission requirements
1021
+ are not met, exceptions are raised.
1022
+
1023
+ :param con: A connection object used to interact with the database.
849
1024
  :type con: IConnection
850
- :param projectShortName: The short name of the project used to identify the graph
851
- in the RDF store. Accepts either Xsd_NCName or a string.
852
- :param iri: The IRI of the resource being queried. Accepts either Iri or a string.
853
- :return: A dictionary mapping predicates to their associated values from the
854
- selected resource.
1025
+ :param projectShortName: The short name of the project graph. Can be an Xsd_NCName
1026
+ or string.
1027
+ :type projectShortName: Xsd_NCName | str
1028
+ :param iri: The IRI of the resource to retrieve data for. Can be an Iri instance
1029
+ or string.
1030
+ :type iri: Iri | str
1031
+ :return: A dictionary mapping predicates (of type Xsd_QName) to their corresponding
1032
+ values.
855
1033
  :rtype: dict[Xsd_QName, Any]
856
- :raises OldapErrorInconsistency: If a non-QName predicate is found in the results.
857
- :raises OldapErrorNotFound: If the resource with the given IRI is not found.
1034
+ :raises OldapErrorInconsistency: If a predicate is not a valid QName.
1035
+ :raises OldapErrorNotFound: If the resource with the specified IRI cannot be found.
858
1036
  """
859
1037
  if not isinstance(iri, Iri):
860
1038
  iri = Iri(iri, validate=True)
@@ -865,26 +1043,37 @@ class ResourceInstance:
865
1043
 
866
1044
  context = Context(name=con.context_name)
867
1045
  sparql = context.sparql_context
868
- sparql += f'''
869
- SELECT ?predicate ?value
1046
+ sparql += textwrap.dedent(f'''
1047
+ SELECT DISTINCT ?predicate ?value
870
1048
  WHERE {{
1049
+ {{
1050
+ GRAPH {graph}:data {{
1051
+ {iri.toRdf} oldap:createdBy {con.userIri.toRdf} .
1052
+ }}
1053
+ }}
1054
+ UNION
1055
+ {{
1056
+ GRAPH oldap:admin {{
1057
+ {con.userIri.toRdf} oldap:hasRole ?role .
1058
+ ?dataperm oldap:permissionValue ?permval .
1059
+ FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})
1060
+ }}
1061
+ }}
871
1062
  GRAPH {graph}:data {{
872
1063
  {iri.toRdf} ?predicate ?value .
873
- {iri.toRdf} oldap:grantsPermission ?permset .
874
- }}
875
- GRAPH oldap:admin {{
876
- {con.userIri.toRdf} oldap:hasPermissions ?permset .
877
- ?permset oldap:givesPermission ?DataPermission .
878
- ?DataPermission oldap:permissionValue ?permval .
1064
+ {iri.toRdf} oldap:attachedToRole ?role .
1065
+ <<{iri.toRdf} oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
879
1066
  }}
880
- FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})
881
1067
  }}
882
- '''
1068
+ ''')
1069
+
883
1070
  jsonres = con.query(sparql)
884
1071
  res = QueryProcessor(context, jsonres)
885
1072
  data = {}
886
1073
  for r in res:
887
1074
  if r['predicate'].is_qname:
1075
+ if r['predicate'].as_qname == Xsd_QName('oldap:attachedToRole'):
1076
+ continue
888
1077
  if not data.get(r['predicate'].as_qname):
889
1078
  data[r['predicate'].as_qname] = []
890
1079
  data[str(r['predicate'].as_qname)].append(str(r['value']))
@@ -892,6 +1081,23 @@ class ResourceInstance:
892
1081
  raise OldapErrorInconsistency(f"Expected QName as predicate, got {r['predicate']}")
893
1082
  if not data.get('rdf:type'):
894
1083
  raise OldapErrorNotFound(f'Resource with iri <{iri}> not found.')
1084
+
1085
+ sparql = context.sparql_context
1086
+ sparql += textwrap.dedent(f'''
1087
+ SELECT DISTINCT ?role ?dataperm
1088
+ WHERE {{
1089
+ GRAPH {graph}:data {{
1090
+ << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
1091
+ }}
1092
+ }}
1093
+ ''')
1094
+ jsonres = con.query(sparql)
1095
+ res = QueryProcessor(context, jsonres)
1096
+ roles = {}
1097
+ for r in res:
1098
+ roles[r['role']] = DataPermission.from_qname(r['dataperm'])
1099
+ data[Xsd_QName('oldap:attachedToRole')] = roles
1100
+
895
1101
  return data
896
1102
 
897
1103
  @staticmethod
@@ -981,6 +1187,11 @@ class ResourceInstance:
981
1187
  if sortBy == SortBy.LASTMOD:
982
1188
  sparql += '?lastModificationDate'
983
1189
  sparql += f'\n{blank:{indent * indent_inc}}WHERE {{'
1190
+ sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH oldap:admin {{'
1191
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}{con.userIri.toRdf} oldap:hasRole ?role .'
1192
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}?DataPermission oldap:permissionValue ?permval .'
1193
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})'
1194
+ sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
984
1195
  sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH {graph}:data {{'
985
1196
  if sortBy:
986
1197
  if sortBy == SortBy.CREATED:
@@ -991,16 +1202,11 @@ class ResourceInstance:
991
1202
  sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s rdf:type ?t .'
992
1203
  if resClass:
993
1204
  sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s rdf:type {resClass} .'
994
- sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:grantsPermission ?permset .'
1205
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:attachedToRole ?role .'
1206
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}<< ?s oldap:attachedToRole ?role >> oldap:hasDataPermission ?DataPermission .'
1207
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}FILTER(isLiteral(?o) && (datatype(?o) = xsd:string || datatype(?o) = rdf:langString || lang(?o) != ""))'
1208
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}FILTER(CONTAINS(LCASE(STR(?o)), "{s}")) # case-insensitive substring match'
995
1209
  sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
996
- sparql += f'\n{blank:{(indent + 1) * indent_inc}}FILTER(isLiteral(?o) && (datatype(?o) = xsd:string || datatype(?o) = rdf:langString || lang(?o) != ""))'
997
- sparql += f'\n{blank:{(indent + 1) * indent_inc}}FILTER(CONTAINS(LCASE(STR(?o)), "{s}")) # case-insensitive substring match'
998
- sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH oldap:admin {{'
999
- sparql += f'\n{blank:{(indent + 2) * indent_inc}}{con.userIri.toRdf} oldap:hasPermissions ?permset .'
1000
- sparql += f'\n{blank:{(indent + 2) * indent_inc}}?permset oldap:givesPermission ?DataPermission .'
1001
- sparql += f'\n{blank:{(indent + 2) * indent_inc}}?DataPermission oldap:permissionValue ?permval .'
1002
- sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
1003
- sparql += f'\n{blank:{(indent + 1) * indent_inc}}FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})'
1004
1210
  sparql += f'\n{blank:{indent * indent_inc}}}}'
1005
1211
 
1006
1212
  if sortBy:
@@ -1015,7 +1221,6 @@ class ResourceInstance:
1015
1221
  sparql += f'\n{blank:{indent * indent_inc}}LIMIT {limit} OFFSET {offset}'
1016
1222
  sparql += '\n'
1017
1223
 
1018
-
1019
1224
  try:
1020
1225
  jsonres = con.query(sparql)
1021
1226
  except OldapError:
@@ -1105,7 +1310,14 @@ class ResourceInstance:
1105
1310
  if sortBy == SortBy.LASTMOD:
1106
1311
  sparql += '?lastModificationDate'
1107
1312
  sparql += f'\n{blank:{indent * indent_inc}}WHERE {{'
1313
+ sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH oldap:admin {{'
1314
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}{con.userIri.toRdf} oldap:hasRole ?role .'
1315
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}?dataperm oldap:permissionValue ?permval .'
1316
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})'
1317
+ sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
1108
1318
  sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH {graph}:data {{'
1319
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:attachedToRole ?role .'
1320
+ sparql += f'\n{blank:{(indent + 2) * indent_inc}}<< ?s oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .'
1109
1321
  if sortBy:
1110
1322
  if sortBy == SortBy.CREATED:
1111
1323
  sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:creationDate ?creationDate .'
@@ -1115,14 +1327,7 @@ class ResourceInstance:
1115
1327
  for index, prop in enumerate(includeProperties):
1116
1328
  sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s {prop} ?o{index} .'
1117
1329
  sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s rdf:type {resClass} .'
1118
- sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:grantsPermission ?permset .'
1119
- sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
1120
- sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH oldap:admin {{'
1121
- sparql += f'\n{blank:{(indent + 2) * indent_inc}}{con.userIri.toRdf} oldap:hasPermissions ?permset .'
1122
- sparql += f'\n{blank:{(indent + 2) * indent_inc}}?permset oldap:givesPermission ?DataPermission .'
1123
- sparql += f'\n{blank:{(indent + 2) * indent_inc}}?DataPermission oldap:permissionValue ?permval .'
1124
1330
  sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
1125
- sparql += f'\n{blank:{(indent + 1) * indent_inc}}FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})'
1126
1331
  sparql += f'\n{blank:{indent * indent_inc}}}}'
1127
1332
 
1128
1333
  if sortBy:
@@ -1187,23 +1392,23 @@ class ResourceInstance:
1187
1392
  context = Context(name=con.context_name)
1188
1393
  sparql = context.sparql_context
1189
1394
 
1190
- sparql += f"""
1395
+ sparql += textwrap.dedent(f"""
1191
1396
  SELECT ?subject ?graph ?path ?prop ?val ?permval
1192
1397
  WHERE {{
1193
1398
  VALUES ?inputImageId {{ {mediaObjectId.toRdf} }}
1194
1399
  ?subject rdf:type shared:MediaObject .
1400
+ GRAPH oldap:admin {{
1401
+ {con.userIri.toRdf} oldap:hasRole ?role .
1402
+ ?dataperm oldap:permissionValue ?permval .
1403
+ }}
1195
1404
  GRAPH ?graph {{
1196
- ?subject oldap:grantsPermission ?permset .
1405
+ ?subject oldap:attachedToRole ?role .
1406
+ <<?subject oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
1197
1407
  ?subject shared:imageId ?inputImageId .
1198
1408
  ?subject ?prop ?val .
1199
1409
  }}
1200
- GRAPH oldap:admin {{
1201
- {con.userIri.toRdf} oldap:hasPermissions ?permset .
1202
- ?permset oldap:givesPermission ?DataPermission .
1203
- ?DataPermission oldap:permissionValue ?permval .
1204
- }}
1205
1410
  }}
1206
- """
1411
+ """)
1207
1412
  try:
1208
1413
  jsonres = con.query(sparql)
1209
1414
  except OldapError:
@@ -1221,7 +1426,8 @@ class ResourceInstance:
1221
1426
  if str(r['prop']) == 'rdf:type':
1222
1427
  continue
1223
1428
  if str(r['prop']) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModifiedBy', 'oldap:lastModificationDate',
1224
- 'shared:imageId', 'shared:originalName', 'shared:originalMimeType', 'shared:serverUrl', 'shared:path', 'shared:protocol'}:
1429
+ 'dcterms:type', 'shared:imageId', 'shared:originalName', 'shared:originalMimeType',
1430
+ 'shared:serverUrl', 'shared:path', 'shared:protocol'}:
1225
1431
  result[str(r['prop'])] = r['val']
1226
1432
  else:
1227
1433
  if result.get(str(r['prop'])) is None:
@@ -1259,16 +1465,15 @@ class ResourceInstance:
1259
1465
  SELECT ?graph ?prop ?val ?permval
1260
1466
  WHERE {{
1261
1467
  {mediaObjectIri.toRdf} rdf:type shared:MediaObject .
1468
+ GRAPH oldap:admin {{
1469
+ {con.userIri.toRdf} oldap:hasRole ?role .
1470
+ ?dataperm oldap:permissionValue ?permval .
1471
+ }}
1262
1472
  GRAPH ?graph {{
1263
- {mediaObjectIri.toRdf} oldap:grantsPermission ?permset .
1264
- {mediaObjectIri.toRdf} shared:imageId ?inputImageId .
1473
+ {mediaObjectIri.toRdf} oldap:attachedToRole ?role .
1474
+ <<{mediaObjectIri.toRdf} oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
1265
1475
  {mediaObjectIri.toRdf} ?prop ?val .
1266
1476
  }}
1267
- GRAPH oldap:admin {{
1268
- {con.userIri.toRdf} oldap:hasPermissions ?permset .
1269
- ?permset oldap:givesPermission ?DataPermission .
1270
- ?DataPermission oldap:permissionValue ?permval .
1271
- }}
1272
1477
  }}
1273
1478
  """
1274
1479
  try:
@@ -1288,7 +1493,8 @@ class ResourceInstance:
1288
1493
  if str(r['prop']) == 'rdf:type':
1289
1494
  continue
1290
1495
  if str(r['prop']) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModifiedBy', 'oldap:lastModificationDate',
1291
- 'shared:imageId', 'shared:originalName', 'shared:originalMimeType', 'shared:serverUrl', 'shared:path', 'shared:protocol'}:
1496
+ 'dcterms:type', 'shared:imageId', 'shared:originalName', 'shared:originalMimeType',
1497
+ 'shared:serverUrl', 'shared:path', 'shared:protocol'}:
1292
1498
  result[str(r['prop'])] = r['val']
1293
1499
  else:
1294
1500
  if result.get(str(r['prop'])) is None:
@@ -1348,6 +1554,7 @@ class ResourceInstanceFactory:
1348
1554
  _sharedProject: Project
1349
1555
  _datamodel: DataModel
1350
1556
  _sharedModel: DataModel
1557
+ _user_default_roles: Dict[Xsd_QName, DataPermission] = {}
1351
1558
 
1352
1559
  def __init__(self,
1353
1560
  con: IConnection,
@@ -1358,6 +1565,7 @@ class ResourceInstanceFactory:
1358
1565
  else:
1359
1566
  self._project = Project.read(self._con, project)
1360
1567
  self._sharedProject = Project.read(self._con, "oldap:SharedProject")
1568
+ self._user_default_roles = {r: DataPermission.from_qname(p) for r, p in self._con._userdata.hasRole.items()}
1361
1569
 
1362
1570
  self._datamodel = DataModel.read(con=self._con, project=self._project)
1363
1571
  self._sharedModel = DataModel.read(con=self._con, project=self._sharedProject)
@@ -1383,7 +1591,9 @@ class ResourceInstanceFactory:
1383
1591
  'name': resclass.owl_class_iri,
1384
1592
  'factory': self,
1385
1593
  'properties': resclass.properties,
1386
- 'superclass': resclass.superclass})
1594
+ 'superclass': resclass.superclass,
1595
+ 'user_default_roles': self._user_default_roles,
1596
+ })
1387
1597
 
1388
1598
 
1389
1599
  def read(self, iri: Iri | str) -> ResourceInstance:
@@ -1392,22 +1602,41 @@ class ResourceInstanceFactory:
1392
1602
  graph = self._project.projectShortName
1393
1603
  context = Context(name=self._con.context_name)
1394
1604
  sparql = context.sparql_context
1395
- sparql += f'''
1396
- SELECT ?predicate ?value
1605
+ sparql += textwrap.dedent(f'''
1606
+ SELECT DISTINCT ?predicate ?value
1397
1607
  WHERE {{
1398
- BIND({iri.toRdf} as ?iri)
1608
+ # 1) compute the max permval for this user + this resource
1609
+ {{
1610
+ SELECT (MAX(?pv) AS ?permval)
1611
+ WHERE {{
1612
+ GRAPH oldap:admin {{
1613
+ {self._con.userIri.toRdf} oldap:hasRole ?role .
1614
+ }}
1615
+ GRAPH {graph}:data {{
1616
+ {iri.toRdf} oldap:attachedToRole ?role .
1617
+ << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
1618
+ }}
1619
+ GRAPH oldap:admin {{
1620
+ ?dataperm oldap:permissionValue ?pv .
1621
+ }}
1622
+ }}
1623
+ }}
1624
+
1625
+ # 2) return the resource triples, but only if there exists a role with that max permval
1399
1626
  GRAPH {graph}:data {{
1400
- ?iri ?predicate ?value .
1401
- ?iri oldap:grantsPermission ?permset .
1627
+ {iri.toRdf} ?predicate ?value .
1402
1628
  }}
1403
- BIND({self._con.userIri.toRdf} as ?user)
1404
- GRAPH oldap:admin {{
1405
- ?user oldap:hasPermissions ?permset .
1406
- ?permset oldap:givesPermission ?DataPermission .
1407
- ?DataPermission oldap:permissionValue ?permval .
1629
+ FILTER EXISTS {{
1630
+ GRAPH oldap:admin {{
1631
+ {self._con.userIri.toRdf} oldap:hasRole ?role .
1632
+ ?dataperm oldap:permissionValue ?permval .
1633
+ }}
1634
+ GRAPH {graph}:data {{
1635
+ {iri.toRdf} oldap:attachedToRole ?role .
1636
+ << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
1637
+ }}
1408
1638
  }}
1409
- FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})
1410
- }}'''
1639
+ }}''')
1411
1640
  jsonres = self._con.query(sparql)
1412
1641
  res = QueryProcessor(context, jsonres)
1413
1642
  objtype = None
@@ -1434,6 +1663,22 @@ class ResourceInstanceFactory:
1434
1663
  raise OldapErrorInconsistency(f"Expected QName as predicate, got {r['predicate']}")
1435
1664
  if objtype is None:
1436
1665
  raise OldapErrorNotFound(f'Resource with iri <{iri}> not found.')
1666
+ sparql = context.sparql_context
1667
+ sparql += textwrap.dedent(f'''
1668
+ SELECT DISTINCT ?role ?dataperm
1669
+ WHERE {{
1670
+ GRAPH {graph}:data {{
1671
+ << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
1672
+ }}
1673
+ }}
1674
+ ''')
1675
+ jsonres = self._con.query(sparql)
1676
+ res = QueryProcessor(context, jsonres)
1677
+ roles = {}
1678
+ for r in res:
1679
+ roles[r['role']] = DataPermission.from_qname(r['dataperm'])
1680
+ kwargs['attachedToRole'] = roles
1681
+
1437
1682
  Instance = self.createObjectInstance(objtype)
1438
1683
  return Instance(iri=iri, **kwargs)
1439
1684