diff --git a/src/keria/app/agenting.py b/src/keria/app/agenting.py index 588aeebe..9594709d 100644 --- a/src/keria/app/agenting.py +++ b/src/keria/app/agenting.py @@ -12,7 +12,6 @@ from keri import kering from keri.app.notifying import Notifier from keri.app.storing import Mailboxer -from ordered_set import OrderedSet as oset import falcon from falcon import media @@ -20,7 +19,7 @@ from hio.core import http from hio.help import decking from keri.app import configing, keeping, habbing, storing, signaling, oobiing, agenting, delegating, \ - forwarding, querying, connecting + forwarding, querying, connecting, grouping from keri.app.grouping import Counselor from keri.app.keeping import Algos from keri.core import coring, parsing, eventing, routing @@ -37,6 +36,8 @@ from keri.app import challenging from . import aiding, notifying, indirecting, credentialing, presenting +from . import grouping as keriagrouping +from ..peer import exchanging as keriaexchanging from .specing import AgentSpecResource from ..core import authing, longrunning, httping from ..core.authing import Authenticater @@ -81,6 +82,8 @@ def setup(name, bran, adminPort, bootPort, base='', httpPort=None, configFile=No credentialing.loadEnds(app=app, identifierResource=aidEnd) presenting.loadEnds(app=app) notifying.loadEnds(app=app) + keriagrouping.loadEnds(app=app) + keriaexchanging.loadEnds(app=app) if httpPort: happ = falcon.App(middleware=falcon.CORSMiddleware( @@ -274,11 +277,13 @@ def __init__(self, hby, rgy, agentHab, agency, caid, **opts): signaler = signaling.Signaler() self.notifier = Notifier(hby=hby, signaler=signaler) + self.mux = grouping.Multiplexor(hby=hby, notifier=self.notifier) # Initialize all the credential processors self.verifier = verifying.Verifier(hby=hby, reger=rgy.reger) - self.registrar = credentialing.Registrar(agentHab=agentHab, hby=hby, rgy=rgy, counselor=self.counselor, witPub=self.witPub, - witDoer=self.witDoer, postman=self.postman, verifier=self.verifier) + self.registrar = credentialing.Registrar(agentHab=agentHab, hby=hby, rgy=rgy, counselor=self.counselor, + witPub=self.witPub, witDoer=self.witDoer, postman=self.postman, + verifier=self.verifier) self.credentialer = credentialing.Credentialer(agentHab=agentHab, hby=self.hby, rgy=self.rgy, postman=self.postman, registrar=self.registrar, verifier=self.verifier, notifier=self.notifier) @@ -296,7 +301,8 @@ def __init__(self, hby, rgy, agentHab, agency, caid, **opts): challengeHandler = challenging.ChallengeHandler(db=hby.db, signaler=signaler) handlers = [issueHandler, requestHandler, proofHandler, applyHandler, challengeHandler] - self.exc = exchanging.Exchanger(db=hby.db, handlers=handlers) + self.exc = exchanging.Exchanger(hby=hby, handlers=handlers) + grouping.loadHandlers(hby=hby, exc=self.exc, mux=self.mux) self.rvy = routing.Revery(db=hby.db, cues=self.cues) self.kvy = eventing.Kevery(db=hby.db, @@ -470,28 +476,11 @@ def recur(self, tyme): sigers = msg["sigers"] ghab = self.hby.habs[serder.pre] - if "smids" in msg: - smids = msg['smids'] - else: - smids = ghab.db.signingMembers(pre=ghab.pre) - - if "rmids" in msg: - rmids = msg['rmids'] - else: - rmids = ghab.db.rotationMembers(pre=ghab.pre) - atc = bytearray() # attachment atc.extend(coring.Counter(code=coring.CtrDex.ControllerIdxSigs, count=len(sigers)).qb64b) for siger in sigers: atc.extend(siger.qb64b) - others = list(oset(smids + (rmids or []))) - others.remove(ghab.mhab.pre) # don't send to self - print(f"Sending multisig event to {len(others)} other participants") - for recpt in others: - self.postman.send(hab=self.agentHab, dest=recpt, topic="multisig", serder=serder, - attachment=atc) - prefixer = coring.Prefixer(qb64=serder.pre) seqner = coring.Seqner(sn=serder.sn) saider = coring.Saider(qb64=serder.said) @@ -924,7 +913,6 @@ def on_get(req, rep, alias): rep.status = falcon.HTTP_404 return - rep.status = falcon.HTTP_200 rep.content_type = "application/json" rep.data = json.dumps(res).encode("utf-8") diff --git a/src/keria/app/credentialing.py b/src/keria/app/credentialing.py index f6285fae..00cea29f 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -151,6 +151,9 @@ def on_post(self, req, rep, name): ked = httping.getRequiredParam(body, "vcp") vcp = coring.Serder(ked=ked) + ked = httping.getRequiredParam(body, "ixn") + ixn = coring.Serder(ked=ked) + hab = agent.hby.habByName(name) if hab is None: raise falcon.HTTPNotFound(description="alias is not a valid reference to an identfier") @@ -164,7 +167,9 @@ def on_post(self, req, rep, name): anchor = dict(i=registry.regk, s="0", d=registry.regk) # Create registry long running OP that embeds the above received OP or Serder. - agent.registrar.incept(hab, registry) + seqner = coring.Seqner(sn=ixn.sn) + prefixer = coring.Prefixer(qb64=ixn.pre) + agent.registrar.incept(hab, registry, prefixer=prefixer, seqner=seqner, saider=ixn.saider) op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.registry, metadata=dict(anchor=anchor, depends=op)) @@ -664,8 +669,8 @@ def incept(self, hab, registry, prefixer=None, seqner=None, saider=None): hab (Hab): human readable name for the registry registry (SignifyRegistry): qb64 identifier prefix of issuing identifier in control of this registry prefixer (Prefixer): - seqner (Seqner): - saider (Saider): + seqner (Seqner): sequence number class of anchoring event + saider (Saider): SAID class of anchoring event Returns: Registry: created registry diff --git a/src/keria/app/grouping.py b/src/keria/app/grouping.py new file mode 100644 index 00000000..368732fe --- /dev/null +++ b/src/keria/app/grouping.py @@ -0,0 +1,137 @@ +# -*- encoding: utf-8 -*- +""" +KERIA +keria.app.grouping module + +""" +import json + +import falcon +from keri.app import habbing +from keri.core import coring, eventing + +from keria.core import httping + + +def loadEnds(app): + msrCol = MultisigRequestCollectionEnd() + app.add_route("/identifiers/{name}/multisig/request", msrCol) + msrRes = MultisigRequestResourceEnd() + app.add_route("/multisig/request/{said}", msrRes) + + +class MultisigRequestCollectionEnd: + """ Collection endpoint class for creating mulisig exn requests from """ + + @staticmethod + def on_post(req, rep, name): + """ POST method for multisig request collection + + Parameters: + req (falcon.Request): HTTP request object + rep (falcon.Response): HTTP response object + name (str): AID of Hab to load credentials for + + """ + agent = req.context.agent + + body = req.get_media() + + # Get the hab + hab = agent.hby.habByName(name) + if hab is None: + raise falcon.HTTPNotFound(description=f"alias={name} is not a valid reference to an identfier") + + # ...and make sure we're a Group + if not isinstance(hab, habbing.SignifyGroupHab): + raise falcon.HTTPBadRequest(description=f"hab for alias {name} is not a multisig") + + # grab all of the required parameters + ked = httping.getRequiredParam(body, "exn") + serder = coring.Serder(ked=ked) + sigs = httping.getRequiredParam(body, "sigs") + atc = httping.getRequiredParam(body, "atc") + + # create sigers from the edge signatures so we can messagize the whole thing + sigers = [coring.Siger(qb64=sig) for sig in sigs] + + # create seal for the proper location to find the signatures + kever = hab.mhab.kever + seal = eventing.SealEvent(i=hab.mhab.pre, s=hex(kever.lastEst.s), d=kever.lastEst.d) + + ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) + ims.extend(atc.encode("utf-8")) # add the pathed attachments + # make a copy and parse + agent.hby.psr.parseOne(ims=bytearray(ims)) + # now get rid of the event so we can pass it as atc to send + del ims[:serder.size] + + smids = hab.db.signingMembers(pre=hab.pre) + smids.remove(hab.mhab.pre) + + for recp in smids: # this goes to other participants + agent.postman.send(hab=agent.agentHab, + dest=recp, + topic="multisig", + serder=serder, + attachment=ims) + + rep.status = falcon.HTTP_200 + rep.data = json.dumps(serder.ked).encode("utf-8") + + +class MultisigRequestResourceEnd: + """ Resource endpoint class for getting full data for a mulisig exn request from a notification """ + + @staticmethod + def on_get(req, rep, said): + """ GET method for multisig resources + + Parameters: + req (falcon.Request): HTTP request object + rep (falcon.Response): HTTP response object + said (str): qb64 SAID of EXN multisig message. + + """ + agent = req.context.agent + exn = agent.hby.db.exns.get(keys=(said,)) + if exn is None: + raise falcon.HTTPNotFound(f"no multisig request with said={said} found") + + route = exn.ked['r'] + if not route.startswith("/multisig"): + raise falcon.HTTPBadRequest(f"invalid mutlsig conversation with said={said}") + + payload = exn.ked['a'] + match route.split("/"): + case ["", "multisig", "icp"]: + pass + case ["", "multisig", *_]: + gid = payload["gid"] + if gid not in agent.hby.habs: + raise falcon.HTTPBadRequest(f"multisig request for non-local group pre={gid}") + + esaid = exn.ked['e']['d'] + exns = agent.mux.get(esaid=esaid) + + for d in exns: + exn = d['exn'] + serder = coring.Serder(ked=exn) + + route = serder.ked['r'] + payload = serder.ked['a'] + match route.split("/"): + case ["", "multisig", "icp"]: + pass + case ["", "multisig", "vcp"]: + gid = payload["gid"] + ghab = agent.hby.habs[gid] + d['groupName'] = ghab.name + d['memberName'] = ghab.mhab.name + + sender = serder.ked['i'] + if (c := agent.org.get(sender)) is not None: + d['sender'] = c['alias'] + + rep.status = falcon.HTTP_200 + rep.data = json.dumps(exns).encode("utf-8") diff --git a/src/keria/app/notifying.py b/src/keria/app/notifying.py index 480ae486..51e1f3c6 100644 --- a/src/keria/app/notifying.py +++ b/src/keria/app/notifying.py @@ -58,7 +58,9 @@ def on_get(req, rep): count = agent.notifier.getNoteCnt() notes = agent.notifier.getNotes(start=start, end=end) - out = [note.pad for note in notes] + out = [] + for note in notes: + out.append(note.pad) end = start + (len(out) - 1) if len(out) > 0 else 0 rep.set_header("Accept-Ranges", "notes") diff --git a/src/keria/end/ending.py b/src/keria/end/ending.py index 22699f83..cbbfc37f 100644 --- a/src/keria/end/ending.py +++ b/src/keria/end/ending.py @@ -51,32 +51,22 @@ def on_get(self, _, rep, aid=None, role=None, eid=None): """ if aid is None: if self.default is None: - rep.status = falcon.HTTP_NOT_FOUND - rep.text = "no blind oobi for this node" - return + raise falcon.HTTPNotFound(description="no blind oobi for this node") aid = self.default agent = self.agency.lookup(pre=aid) if agent is None: - rep.status = falcon.HTTP_NOT_FOUND - rep.text = "AID not found for this OOBI" - return - - if aid not in agent.hby.kevers: - rep.status = falcon.HTTP_NOT_FOUND - return + raise falcon.HTTPNotFound(description="AID not found for this OOBI") kever = agent.hby.kevers[aid] if not agent.hby.db.fullyWitnessed(kever.serder): - rep.status = falcon.HTTP_NOT_FOUND - return + raise falcon.HTTPNotFound(description=f"{aid} not available") if kever.prefixer.qb64 in agent.hby.prefixes: # One of our identifiers hab = agent.hby.habs[kever.prefixer.qb64] else: # Not allowed to respond - rep.status = falcon.HTTP_NOT_ACCEPTABLE - return + raise falcon.HTTPNotAcceptable(description=f"{aid} is not a local identifier") eids = [] if eid: diff --git a/src/keria/peer/__init__.py b/src/keria/peer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/keria/peer/exchanging.py b/src/keria/peer/exchanging.py new file mode 100644 index 00000000..348a2889 --- /dev/null +++ b/src/keria/peer/exchanging.py @@ -0,0 +1,74 @@ +# -*- encoding: utf-8 -*- +""" +KERIA +keria.app.exchanging module + +""" +import json + +import falcon +from keri.core import coring, eventing + +from keria.core import httping + + +def loadEnds(app): + exnColEnd = ExchangeCollectionEnd() + app.add_route("/identifiers/{name}/exchanges", exnColEnd) + + +class ExchangeCollectionEnd: + + @staticmethod + def on_post(req, rep, name): + """ POST endpoint for exchange message collection """ + agent = req.context.agent + + body = req.get_media() + + # Get the hab + hab = agent.hby.habByName(name) + if hab is None: + raise falcon.HTTPNotFound(description=f"alias={name} is not a valid reference to an identfier") + + # Get the exn, sigs, additional attachments and recipients from the request + ked = httping.getRequiredParam(body, "exn") + sigs = httping.getRequiredParam(body, "sigs") + atc = httping.getRequiredParam(body, "atc") + rec = httping.getRequiredParam(body, "rec") + topic = httping.getRequiredParam(body, "tpc") + + for recp in rec: # Have to verify we already know all the recipients. + if recp not in agent.hby.kevers: + raise falcon.HTTPBadRequest(f"attempt to send to unknown AID={recp}") + + # use that data to create th Serder and Sigers for the exn + serder = coring.Serder(ked=ked) + sigers = [coring.Siger(qb64=sig) for sig in sigs] + + # Now create the stream to send, need the signer seal + kever = hab.kever + seal = eventing.SealEvent(i=hab.pre, s=hex(kever.lastEst.s), d=kever.lastEst.d) + + ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) + + # Have to add the atc to the end... this will be Pathed signatures for embeds + ims.extend(atc.encode("utf-8")) # add the pathed attachments + + # make a copy and parse + agent.hby.psr.parseOne(ims=bytearray(ims)) + + # now get rid of the event so we can pass it as atc to send + del ims[:serder.size] + + for recp in rec: # now let's send it off the all the recipients + agent.postman.send(hab=agent.agentHab, + dest=recp, + topic=topic, + serder=serder, + attachment=ims) + + rep.status = falcon.HTTP_200 + rep.data = json.dumps(serder.ked).encode("utf-8") + + diff --git a/tests/app/test_grouping.py b/tests/app/test_grouping.py new file mode 100644 index 00000000..996e5805 --- /dev/null +++ b/tests/app/test_grouping.py @@ -0,0 +1,80 @@ +# -*- encoding: utf-8 -*- +""" +KERIA +keria.app.grouping module + +Testing the Mark II Agent Grouping endpoints + +""" +from keria.app import grouping, aiding + + +def test_load_ends(helpers): + with helpers.openKeria() as (agency, agent, app, client): + grouping.loadEnds(app=app) + assert app._router is not None + + res = app._router.find("/test") + assert res is None + + (end, *_) = app._router.find("/identifiers/NAME/multisig/request") + assert isinstance(end, grouping.MultisigRequestCollectionEnd) + (end, *_) = app._router.find("/multisig/request/SAID") + assert isinstance(end, grouping.MultisigRequestResourceEnd) + + +def test_multisig_request_ends(helpers): + with helpers.openKeria() as (agency, agent, app, client): + grouping.loadEnds(app=app) + + end = aiding.IdentifierCollectionEnd() + app.add_route("/identifiers", end) + + # First create participants (aid1, aid2) in a multisig AID + salt0 = b'0123456789abcdef' + op = helpers.createAid(client, "aid1", salt0) + aid = op["response"] + pre = aid['i'] + assert pre == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" + + icp = { + "v": "KERI10JSON0002c7_", + "t": "dip", + "d": "EAbkBt1AkiKskBb-SBACC07ioQ1sx9Q44SpKRZwKjMaU", + "i": "EAbkBt1AkiKskBb-SBACC07ioQ1sx9Q44SpKRZwKjMaU", + "s": "0", + "kt": [ + "1/3", + "1/3", + "1/3" + ], + "k": [ + "DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9", + "DM1XbVrBOpVRyXDCKlvMWNE_qkkGU4rVq-7_bHP7za8W", + "DNwaetMMIMbt708EPsdCGHZpMe3gf1OZV-R7LTcJBLnK" + ], + "nt": [ + "1/3", + "1/3", + "1/3" + ], + "n": [ + "EAORnRtObOgNiOlMolji-KijC_isa3lRDpHCsol79cOc", + "EPJy-TM6OHBJeAFTpb31YVrDSPXQTYmhvDc7DioakK8h", + "EHUXA8lpGe1MObjP8RZs9WijzNsjKdoSql_zajgJGLQ6" + ], + "bt": "2", + "b": [ + "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", + "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM", + "BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX" + ], + "c": [], + "a": [], + "di": "EHpD0-CDWOdu5RJ8jHBSUkOqBZ3cXeDVHWNb_Ul89VI7" + } + + + + client.simulate_post() + diff --git a/tests/app/test_notifying.py b/tests/app/test_notifying.py index 4714f6f4..97b774f1 100644 --- a/tests/app/test_notifying.py +++ b/tests/app/test_notifying.py @@ -1,16 +1,15 @@ # -*- encoding: utf-8 -*- """ KERIA -keria.app.agenting module +keria.app.notifying module -Testing the Mark II Agent +Testing the Mark II Agent notification endpoint """ -import datetime from builtins import isinstance from keri.core.coring import randomNonce -from keria.app import notifying +from keria.app import notifying, grouping def test_load_ends(helpers): @@ -33,7 +32,6 @@ def test_notifications(helpers): assert agent.notifier.add(attrs=dict(a=1, b=2, c=3)) is True - dt = datetime.datetime.now() assert agent.notifier.add(attrs=dict(a=1)) is True assert agent.notifier.add(attrs=dict(a=2)) is True assert agent.notifier.add(attrs=dict(a=3)) is True @@ -93,5 +91,3 @@ def test_notifications(helpers): assert notes[0]['r'] is False assert notes[1]['r'] is True assert notes[2]['r'] is not True # just for fun - - diff --git a/tests/end/__init__.py b/tests/end/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/end/test_ending.py b/tests/end/test_ending.py new file mode 100644 index 00000000..ce15e20d --- /dev/null +++ b/tests/end/test_ending.py @@ -0,0 +1,77 @@ +# -*- encoding: utf-8 -*- +""" +KERIA +keria.app.grouping module + +Testing the Mark II Agent Grouping endpoints + +""" +from keri.core import coring + +from keria.app import aiding +from keria.end import ending + + +def test_load_ends(helpers): + with helpers.openKeria() as (agency, agent, app, client): + ending.loadEnds(app=app, agency=agency) + assert app._router is not None + + res = app._router.find("/test") + assert res is None + + (end, *_) = app._router.find("/oobi") + assert isinstance(end, ending.OOBIEnd) + (end, *_) = app._router.find("/oobi/AID") + assert isinstance(end, ending.OOBIEnd) + (end, *_) = app._router.find("/oobi/AID/ROLE") + assert isinstance(end, ending.OOBIEnd) + (end, *_) = app._router.find("/oobi/AID/ROLE/EID") + assert isinstance(end, ending.OOBIEnd) + + +def test_oobi_end(helpers): + with helpers.openKeria() as (agency, agent, app, client): + ending.loadEnds(app=app, agency=agency) + + end = aiding.IdentifierCollectionEnd() + app.add_route("/identifiers", end) + endRolesEnd = aiding.EndRoleCollectionEnd() + app.add_route("/identifiers/{name}/endroles", endRolesEnd) + + # First create participants (aid1, aid2) in a multisig AID + salt = b'0123456789abcdef' + op = helpers.createAid(client, "aid1", salt) + aid = op["response"] + pre = aid['i'] + assert pre == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" + + recp = aid['i'] + rpy = helpers.endrole(recp, agent.agentHab.pre) + sigs = helpers.sign(salt, 0, 0, rpy.raw) + body = dict(rpy=rpy.ked, sigs=sigs) + + res = client.simulate_post(path=f"/identifiers/aid1/endroles", json=body) + op = res.json + ked = op["response"] + serder = coring.Serder(ked=ked) + assert serder.raw == rpy.raw + + res = client.simulate_get(path=f"/oobi") + assert res.status_code == 404 + assert res.json == {'description': 'no blind oobi for this node', 'title': '404 Not Found'} + + # Use a bad AID + res = client.simulate_get(path=f"/oobi/EHXXXXXT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSys") + assert res.status_code == 404 + assert res.json == {'description': 'AID not found for this OOBI', 'title': '404 Not Found'} + + # Use valid AID + res = client.simulate_get(path=f"/oobi/{pre}") + assert res.status_code == 200 + assert res.headers['Content-Type'] == "application/json+cesr" + + # Use valid AID, role and EID + res = client.simulate_get(path=f"/oobi/{pre}/agent/{agent.agentHab.pre}") + assert res.status_code == 200 + assert res.headers['Content-Type'] == "application/json+cesr"