Mercurial > libervia-pubsub
comparison sat_pubsub/backend.py @ 330:82d1259b3e36
backend, pgsql storage: better items/notification handling, various fixes:
- replaced const.VAL_AMODEL_ROSTER by const.VAL_AMODEL_PUBLISHER_ROSTER to follow change in pgsql schema
- implemented whitelist access model
- fixed bad access check during items retrieval (access was checked on recipient instead of requestor/sender)
- getItemsData and notification filtering now use inline callbacks: this make these complexe workflows far mor easy to read, and clarity is imperative in these security critical sections.
- publisher-roster access model now need to have only one owner, else it will fail. The idea is to use this model only when owner=publisher, else there is ambiguity on the roster to use to check access
- replaced getNodeOwner by node.getOwners, as a node can have several owners
- notifications filtering has been fixed in a similar way
- psql: simplified withPEP method, pep_table argument is actually not needed
- removed error.NotInRoster: error.Forbidden is used instead
- notifications now notify all the owners, not only the first one
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 26 Mar 2017 20:52:32 +0200 |
parents | 29c2553ef863 |
children | e93a9fd329d9 |
comparison
equal
deleted
inserted
replaced
329:98409ef42c94 | 330:82d1259b3e36 |
---|---|
122 const.OPT_ACCESS_MODEL: | 122 const.OPT_ACCESS_MODEL: |
123 {"type": "list-single", | 123 {"type": "list-single", |
124 "label": "Who can subscribe to this node", | 124 "label": "Who can subscribe to this node", |
125 "options": { | 125 "options": { |
126 const.VAL_AMODEL_OPEN: "Public node", | 126 const.VAL_AMODEL_OPEN: "Public node", |
127 const.VAL_AMODEL_ROSTER: "Node restricted to some roster groups", | 127 const.VAL_AMODEL_PUBLISHER_ROSTER: "Node restricted to some groups of publisher's roster", |
128 const.VAL_AMODEL_JID: "Node restricted to some jids", | 128 const.VAL_AMODEL_WHITELIST: "Node restricted to some jids", |
129 } | 129 } |
130 }, | 130 }, |
131 const.OPT_ROSTER_GROUPS_ALLOWED: | 131 const.OPT_ROSTER_GROUPS_ALLOWED: |
132 {"type": "list-multi", | 132 {"type": "list-multi", |
133 "label": "Groups of the roster allowed to access the node", | 133 "label": "Groups of the roster allowed to access the node", |
557 | 557 |
558 def getAffiliations(self, entity): | 558 def getAffiliations(self, entity): |
559 return self.storage.getAffiliations(entity) | 559 return self.storage.getAffiliations(entity) |
560 | 560 |
561 | 561 |
562 def getItems(self, nodeIdentifier, recipient, maxItems=None, | 562 def getItems(self, nodeIdentifier, requestor, recipient, maxItems=None, |
563 itemIdentifiers=None, ext_data=None): | 563 itemIdentifiers=None, ext_data=None): |
564 d = self.getItemsData(nodeIdentifier, recipient, maxItems, itemIdentifiers, ext_data) | 564 d = self.getItemsData(nodeIdentifier, requestor, recipient, maxItems, itemIdentifiers, ext_data) |
565 d.addCallback(lambda items_data: [item_data.item for item_data in items_data]) | 565 d.addCallback(lambda items_data: [item_data.item for item_data in items_data]) |
566 return d | 566 return d |
567 | 567 |
568 def getItemsData(self, nodeIdentifier, recipient, maxItems=None, | 568 @defer.inlineCallbacks |
569 def getOwnerRoster(self, node, owners=None): | |
570 if owners is None: | |
571 owners = yield node.getOwners() | |
572 | |
573 if len(owners) != 1: | |
574 log.msg('publisher-roster access is not allowed with more than 1 owner') | |
575 return | |
576 | |
577 owner_jid = owners[0] | |
578 | |
579 try: | |
580 roster = yield self.privilege.getRoster(owner_jid) | |
581 except Exception as e: | |
582 log.msg("Error while getting roster of {owner_jid}: {msg}".format( | |
583 owner_jid = owner_jid.full(), | |
584 msg = e)) | |
585 return | |
586 defer.returnValue(roster) | |
587 | |
588 @defer.inlineCallbacks | |
589 def getItemsData(self, nodeIdentifier, requestor, recipient, maxItems=None, | |
569 itemIdentifiers=None, ext_data=None): | 590 itemIdentifiers=None, ext_data=None): |
570 """like getItems but return the whole ItemData""" | 591 """like getItems but return the whole ItemData""" |
592 if maxItems == 0: | |
593 log.msg("WARNING: maxItems=0 on items retrieval") | |
594 defer.returnValue([]) | |
595 | |
571 if ext_data is None: | 596 if ext_data is None: |
572 ext_data = {} | 597 ext_data = {} |
573 d = self.storage.getNode(nodeIdentifier, ext_data.get('pep', False), recipient) | 598 node = yield self.storage.getNode(nodeIdentifier, ext_data.get('pep', False), recipient) |
574 d.addCallback(_getAffiliation, recipient) | 599 node, affiliation = yield _getAffiliation(node, requestor) |
575 d.addCallback(self._doGetItems, recipient, maxItems, itemIdentifiers, | 600 |
576 ext_data) | 601 if not iidavoll.ILeafNode.providedBy(node): |
577 return d | 602 defer.returnValue([]) |
578 | 603 |
579 def checkGroup(self, roster_groups, entity): | 604 if affiliation == 'outcast': |
580 """Check that entity is authorized and in roster | 605 raise error.Forbidden() |
581 @param roster_group: tuple which 2 items: | 606 |
582 - roster: mapping of jid to RosterItem as given by self.privilege.getRoster | 607 |
583 - groups: list of authorized groups | 608 # node access check |
584 @param entity: entity which must be in group | 609 owner = affiliation == 'owner' |
585 @return: (True, roster) if entity is in roster and authorized | 610 access_model = node.getAccessModel() |
586 (False, roster) if entity is in roster but not authorized | 611 roster = None |
587 @raise: error.NotInRoster if entity is not in roster""" | 612 |
588 roster, authorized_groups = roster_groups | 613 if access_model == const.VAL_AMODEL_OPEN or owner: |
589 _entity = entity.userhostJID() | 614 pass |
590 | 615 elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: |
591 if not _entity in roster: | 616 roster = yield self.getOwnerRoster(node) |
592 raise error.NotInRoster | 617 |
593 return (roster[_entity].groups.intersection(authorized_groups), roster) | 618 if roster is None: |
594 | 619 raise error.Forbidden() |
595 def _getNodeGroups(self, roster, nodeIdentifier, pep): | 620 |
596 d = self.storage.getNodeGroups(nodeIdentifier, pep) | 621 if requestor not in roster: |
597 d.addCallback(lambda groups: (roster, groups)) | 622 raise error.Forbidden() |
598 return d | 623 |
599 | 624 authorized_groups = yield node.getAuthorizedGroups() |
600 def _rosterEb(self, failure, owner_jid): | 625 |
601 log.msg("Error while getting roster of {}: {}".format(unicode(owner_jid), failure.value)) | 626 if not roster[requestor].groups.intersection(authorized_groups): |
602 return {} | 627 # requestor is in roster but not in one of the allowed groups |
603 | 628 raise error.Forbidden() |
604 def _doGetItems(self, result, requestor, maxItems, itemIdentifiers, | 629 elif access_model == const.VAL_AMODEL_WHITELIST: |
605 ext_data): | 630 affiliations = yield node.getAffiliations() |
606 node, affiliation = result | 631 try: |
607 if maxItems == 0: | 632 affiliation = affiliations[requestor.userhostJID()] |
608 log.msg("WARNING: maxItems=0 on items retrieval") | 633 except KeyError: |
609 return [] | 634 raise error.Forbidden() |
610 | 635 else: |
611 def append_item_config(items_data): | 636 if affiliation not in ('owner', 'publisher', 'member'): |
612 """Add item config data form to items with roster access model""" | 637 raise error.Forbidden() |
638 else: | |
639 raise Exception(u"Unknown access_model") | |
640 | |
641 # at this point node access is checked | |
642 | |
643 if owner: | |
644 # requestor_groups is only used in restricted access | |
645 requestor_groups = None | |
646 else: | |
647 if roster is None: | |
648 roster = yield self.getOwnerRoster(node) | |
649 if roster is None: | |
650 roster = {} | |
651 roster_item = roster.get(requestor.userhostJID()) | |
652 requestor_groups = tuple(roster_item.groups) if roster_item else tuple() | |
653 | |
654 if itemIdentifiers: | |
655 items_data = yield node.getItemsById(requestor_groups, owner, itemIdentifiers) | |
656 else: | |
657 items_data = yield node.getItems(requestor_groups, owner, maxItems, ext_data) | |
658 | |
659 if owner: | |
660 # Add item config data form to items with roster access model | |
613 for item_data in items_data: | 661 for item_data in items_data: |
614 if item_data.access_model == const.VAL_AMODEL_OPEN: | 662 if item_data.access_model == const.VAL_AMODEL_OPEN: |
615 pass | 663 pass |
616 elif item_data.access_model == const.VAL_AMODEL_ROSTER: | 664 elif item_data.access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: |
617 form = data_form.Form('submit', formNamespace=const.NS_ITEM_CONFIG) | 665 form = data_form.Form('submit', formNamespace=const.NS_ITEM_CONFIG) |
618 access = data_form.Field(None, const.OPT_ACCESS_MODEL, value=const.VAL_AMODEL_ROSTER) | 666 access = data_form.Field(None, const.OPT_ACCESS_MODEL, value=const.VAL_AMODEL_PUBLISHER_ROSTER) |
619 allowed = data_form.Field(None, const.OPT_ROSTER_GROUPS_ALLOWED, values=item_data.config[const.OPT_ROSTER_GROUPS_ALLOWED]) | 667 allowed = data_form.Field(None, const.OPT_ROSTER_GROUPS_ALLOWED, values=item_data.config[const.OPT_ROSTER_GROUPS_ALLOWED]) |
620 form.addField(access) | 668 form.addField(access) |
621 form.addField(allowed) | 669 form.addField(allowed) |
622 item_data.item.addChild(form.toElement()) | 670 item_data.item.addChild(form.toElement()) |
623 elif access_model == const.VAL_AMODEL_JID: | 671 elif access_model == const.VAL_AMODEL_WHITELIST: |
624 #FIXME: manage jid | 672 #FIXME |
625 raise NotImplementedError | 673 raise NotImplementedError |
626 else: | 674 else: |
627 raise error.BadAccessTypeError(access_model) | 675 raise error.BadAccessTypeError(access_model) |
628 return items_data | 676 |
629 | 677 yield self._items_rsm(items_data, node, requestor_groups, owner, itemIdentifiers, ext_data) |
630 def access_checked(access_data): | 678 defer.returnValue(items_data) |
631 authorized, roster = access_data | |
632 if not authorized: | |
633 raise error.NotAuthorized() | |
634 | |
635 roster_item = roster.get(requestor.userhostJID()) | |
636 authorized_groups = tuple(roster_item.groups) if roster_item else tuple() | |
637 owner = affiliation == 'owner' | |
638 | |
639 if itemIdentifiers: | |
640 d = node.getItemsById(authorized_groups, owner, itemIdentifiers) | |
641 else: | |
642 d = node.getItems(authorized_groups, owner, maxItems, ext_data) | |
643 if owner: | |
644 d.addCallback(append_item_config) | |
645 | |
646 d.addCallback(self._items_rsm, node, authorized_groups, | |
647 owner, itemIdentifiers, | |
648 ext_data) | |
649 return d | |
650 | |
651 if not iidavoll.ILeafNode.providedBy(node): | |
652 return [] | |
653 | |
654 if affiliation == 'outcast': | |
655 raise error.Forbidden() | |
656 | |
657 access_model = node.getAccessModel() | |
658 d = node.getNodeOwner() | |
659 | |
660 def gotOwner(owner_jid): | |
661 d_roster = self.privilege.getRoster(owner_jid) | |
662 d_roster.addErrback(self._rosterEb, owner_jid) | |
663 return d_roster | |
664 | |
665 d.addCallback(gotOwner) | |
666 | |
667 if access_model == const.VAL_AMODEL_OPEN or affiliation == 'owner': | |
668 d.addCallback(lambda roster: (True, roster)) | |
669 d.addCallback(access_checked) | |
670 elif access_model == const.VAL_AMODEL_ROSTER: | |
671 d.addCallback(self._getNodeGroups, node.nodeIdentifier, ext_data.get('pep', False)) | |
672 d.addCallback(self.checkGroup, requestor) | |
673 d.addCallback(access_checked) | |
674 | |
675 return d | |
676 | 679 |
677 def _setCount(self, value, response): | 680 def _setCount(self, value, response): |
678 response.count = value | 681 response.count = value |
679 | 682 |
680 def _setIndex(self, value, response, adjust): | 683 def _setIndex(self, value, response, adjust): |
904 _errorMap = { | 907 _errorMap = { |
905 error.NodeNotFound: ('item-not-found', None, None), | 908 error.NodeNotFound: ('item-not-found', None, None), |
906 error.NodeExists: ('conflict', None, None), | 909 error.NodeExists: ('conflict', None, None), |
907 error.Forbidden: ('forbidden', None, None), | 910 error.Forbidden: ('forbidden', None, None), |
908 error.NotAuthorized: ('not-authorized', None, None), | 911 error.NotAuthorized: ('not-authorized', None, None), |
909 error.NotInRoster: ('not-authorized', 'not-in-roster-group', None), | |
910 error.ItemNotFound: ('item-not-found', None, None), | 912 error.ItemNotFound: ('item-not-found', None, None), |
911 error.ItemForbidden: ('bad-request', 'item-forbidden', None), | 913 error.ItemForbidden: ('bad-request', 'item-forbidden', None), |
912 error.ItemRequired: ('bad-request', 'item-required', None), | 914 error.ItemRequired: ('bad-request', 'item-required', None), |
913 error.NoInstantNodes: ('not-acceptable', | 915 error.NoInstantNodes: ('not-acceptable', |
914 'unsupported', | 916 'unsupported', |
969 node = data['node'] | 971 node = data['node'] |
970 pep = data['pep'] | 972 pep = data['pep'] |
971 recipient = data['recipient'] | 973 recipient = data['recipient'] |
972 | 974 |
973 def afterPrepare(result): | 975 def afterPrepare(result): |
974 owner_jid, notifications_filtered = result | 976 owners, notifications_filtered = result |
975 #we notify the owner | 977 #we notify the owners |
976 #FIXME: check if this comply with XEP-0060 (option needed ?) | 978 #FIXME: check if this comply with XEP-0060 (option needed ?) |
977 #TODO: item's access model have to be sent back to owner | 979 #TODO: item's access model have to be sent back to owner |
978 #TODO: same thing for getItems | 980 #TODO: same thing for getItems |
979 | 981 |
980 def getFullItem(item_data): | 982 def getFullItem(item_data): |
987 new_item = deepcopy(item) | 989 new_item = deepcopy(item) |
988 if item_config: | 990 if item_config: |
989 new_item.addChild(item_config.toElement()) | 991 new_item.addChild(item_config.toElement()) |
990 return new_item | 992 return new_item |
991 | 993 |
992 notifications_filtered.append((owner_jid, | 994 for owner_jid in owners: |
993 set([pubsub.Subscription(node.nodeIdentifier, | 995 notifications_filtered.append( |
994 owner_jid, | 996 (owner_jid, |
995 'subscribed')]), | 997 set([pubsub.Subscription(node.nodeIdentifier, |
996 [getFullItem(item_data) for item_data in items_data])) | 998 owner_jid, |
999 'subscribed')]), | |
1000 [getFullItem(item_data) for item_data in items_data])) | |
997 | 1001 |
998 if pep: | 1002 if pep: |
999 return self.backend.privilege.notifyPublish( | 1003 return self.backend.privilege.notifyPublish( |
1000 recipient, | 1004 recipient, |
1001 node.nodeIdentifier, | 1005 node.nodeIdentifier, |
1016 node = data['node'] | 1020 node = data['node'] |
1017 pep = data['pep'] | 1021 pep = data['pep'] |
1018 recipient = data['recipient'] | 1022 recipient = data['recipient'] |
1019 | 1023 |
1020 def afterPrepare(result): | 1024 def afterPrepare(result): |
1021 owner_jid, notifications_filtered = result | 1025 owners, notifications_filtered = result |
1022 #we add the owner | 1026 #we add the owners |
1023 | 1027 |
1024 notifications_filtered.append((owner_jid, | 1028 for owner_jid in owners: |
1025 set([pubsub.Subscription(node.nodeIdentifier, | 1029 notifications_filtered.append( |
1026 owner_jid, | 1030 (owner_jid, |
1027 'subscribed')]), | 1031 set([pubsub.Subscription(node.nodeIdentifier, |
1028 [item_data.item for item_data in items_data])) | 1032 owner_jid, |
1033 'subscribed')]), | |
1034 [item_data.item for item_data in items_data])) | |
1029 | 1035 |
1030 if pep: | 1036 if pep: |
1031 return self.backend.privilege.notifyRetract( | 1037 return self.backend.privilege.notifyRetract( |
1032 recipient, | 1038 recipient, |
1033 node.nodeIdentifier, | 1039 node.nodeIdentifier, |
1042 d = self._prepareNotify(items_data, node, data.get('subscription')) | 1048 d = self._prepareNotify(items_data, node, data.get('subscription')) |
1043 d.addCallback(afterPrepare) | 1049 d.addCallback(afterPrepare) |
1044 return d | 1050 return d |
1045 | 1051 |
1046 | 1052 |
1053 @defer.inlineCallbacks | |
1047 def _prepareNotify(self, items_data, node, subscription=None): | 1054 def _prepareNotify(self, items_data, node, subscription=None): |
1048 """Do a bunch of permissions check and filter notifications | 1055 """Do a bunch of permissions check and filter notifications |
1049 | 1056 |
1050 The owner is not added to these notifications, | 1057 The owner is not added to these notifications, |
1051 it must be called by the calling method | 1058 it must be called by the calling method |
1060 - notifications_filtered | 1067 - notifications_filtered |
1061 - node_owner_jid | 1068 - node_owner_jid |
1062 - items_data | 1069 - items_data |
1063 """ | 1070 """ |
1064 | 1071 |
1065 def filterNotifications(result): | 1072 |
1066 """Check access of subscriber for each item, and keep only allowed ones""" | 1073 if subscription is None: |
1067 notifications, (owner_jid,roster) = result | 1074 notifications = yield self.backend.getNotifications(node.nodeDbId, items_data) |
1068 | 1075 else: |
1069 #we filter items not allowed for the subscribers | 1076 notifications = [(subscription.subscriber, [subscription], items_data)] |
1070 notifications_filtered = [] | 1077 |
1071 | 1078 owners = node.getOwners() |
1072 for subscriber, subscriptions, items_data in notifications: | 1079 owner_roster = None |
1073 if subscriber == owner_jid: | 1080 |
1074 # as notification is always sent to owner, | 1081 # now we check access of subscriber for each item, and keep only allowed ones |
1075 # we ignore owner if he is here | 1082 |
1076 continue | 1083 #we filter items not allowed for the subscribers |
1077 allowed_items = [] #we keep only item which subscriber can access | 1084 notifications_filtered = [] |
1078 | 1085 |
1079 for item_data in items_data: | 1086 for subscriber, subscriptions, items_data in notifications: |
1080 item, access_model = item_data.item, item_data.access_model | 1087 subscriber_bare = subscriber.userhostJID() |
1081 access_list = item_data.config | 1088 if subscriber_bare in owners: |
1082 if access_model == const.VAL_AMODEL_OPEN: | 1089 # as notification is always sent to owner, |
1090 # we ignore owner if he is here | |
1091 continue | |
1092 allowed_items = [] #we keep only item which subscriber can access | |
1093 | |
1094 for item_data in items_data: | |
1095 item, access_model = item_data.item, item_data.access_model | |
1096 access_list = item_data.config | |
1097 if access_model == const.VAL_AMODEL_OPEN: | |
1098 allowed_items.append(item) | |
1099 elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: | |
1100 if owner_roster is None: | |
1101 owner_roster= yield self.getOwnerRoster(node, owners) | |
1102 if owner_roster is None: | |
1103 owner_roster = {} | |
1104 if not subscriber_bare in owner_roster: | |
1105 continue | |
1106 #the subscriber is known, is he in the right group ? | |
1107 authorized_groups = access_list[const.OPT_ROSTER_GROUPS_ALLOWED] | |
1108 if owner_roster[subscriber_bare].groups.intersection(authorized_groups): | |
1083 allowed_items.append(item) | 1109 allowed_items.append(item) |
1084 elif access_model == const.VAL_AMODEL_ROSTER: | 1110 else: #unknown access_model |
1085 _subscriber = subscriber.userhostJID() | 1111 # TODO: white list access |
1086 if not _subscriber in roster: | 1112 raise NotImplementedError |
1087 continue | 1113 |
1088 #the subscriber is known, is he in the right group ? | 1114 if allowed_items: |
1089 authorized_groups = access_list[const.OPT_ROSTER_GROUPS_ALLOWED] | 1115 notifications_filtered.append((subscriber, subscriptions, allowed_items)) |
1090 if roster[_subscriber].groups.intersection(authorized_groups): | 1116 |
1091 allowed_items.append(item) | 1117 defer.returnValue((owners, notifications_filtered)) |
1092 | |
1093 else: #unknown access_model | |
1094 raise NotImplementedError | |
1095 | |
1096 if allowed_items: | |
1097 notifications_filtered.append((subscriber, subscriptions, allowed_items)) | |
1098 return (owner_jid, notifications_filtered) | |
1099 | |
1100 | |
1101 if subscription is None: | |
1102 d1 = self.backend.getNotifications(node.nodeDbId, items_data) | |
1103 else: | |
1104 d1 = defer.succeed([(subscription.subscriber, [subscription], | |
1105 items_data)]) | |
1106 | |
1107 def _got_owner(owner_jid): | |
1108 #return a tuple with owner_jid and roster | |
1109 def rosterEb(failure): | |
1110 log.msg("Error while getting roster of {}: {}".format(unicode(owner_jid), failure.value)) | |
1111 return (owner_jid, {}) | |
1112 | |
1113 d = self.backend.privilege.getRoster(owner_jid) | |
1114 d.addErrback(rosterEb) | |
1115 d.addCallback(lambda roster: (owner_jid,roster)) | |
1116 return d | |
1117 | |
1118 d2 = node.getNodeOwner() | |
1119 d2.addCallback(_got_owner) | |
1120 d = defer.gatherResults([d1, d2]) | |
1121 d.addCallback(filterNotifications) | |
1122 return d | |
1123 | 1118 |
1124 def _preDelete(self, data, pep, recipient): | 1119 def _preDelete(self, data, pep, recipient): |
1125 nodeIdentifier = data['node'].nodeIdentifier | 1120 nodeIdentifier = data['node'].nodeIdentifier |
1126 redirectURI = data.get('redirectURI', None) | 1121 redirectURI = data.get('redirectURI', None) |
1127 d = self.backend.getSubscribers(nodeIdentifier, pep, recipient) | 1122 d = self.backend.getSubscribers(nodeIdentifier, pep, recipient) |
1129 self.serviceJID, | 1124 self.serviceJID, |
1130 nodeIdentifier, | 1125 nodeIdentifier, |
1131 subscribers, | 1126 subscribers, |
1132 redirectURI)) | 1127 redirectURI)) |
1133 return d | 1128 return d |
1134 | |
1135 | 1129 |
1136 def _mapErrors(self, failure): | 1130 def _mapErrors(self, failure): |
1137 e = failure.trap(*self._errorMap.keys()) | 1131 e = failure.trap(*self._errorMap.keys()) |
1138 | 1132 |
1139 condition, pubsubCondition, feature = self._errorMap[e] | 1133 condition, pubsubCondition, feature = self._errorMap[e] |
1289 try: | 1283 try: |
1290 ext_data['pep'] = request.delegated | 1284 ext_data['pep'] = request.delegated |
1291 except AttributeError: | 1285 except AttributeError: |
1292 pass | 1286 pass |
1293 d = self.backend.getItems(request.nodeIdentifier, | 1287 d = self.backend.getItems(request.nodeIdentifier, |
1288 request.sender, | |
1294 request.recipient, | 1289 request.recipient, |
1295 request.maxItems, | 1290 request.maxItems, |
1296 request.itemIdentifiers, | 1291 request.itemIdentifiers, |
1297 ext_data) | 1292 ext_data) |
1298 return d.addErrback(self._mapErrors) | 1293 return d.addErrback(self._mapErrors) |