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