sally 1.0.5__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.
- sally/__init__.py +35 -0
- sally/app/__init__.py +7 -0
- sally/app/bootstrapping.py +82 -0
- sally/app/cli/__init__.py +7 -0
- sally/app/cli/commands/__init__.py +7 -0
- sally/app/cli/commands/hook/__init__.py +0 -0
- sally/app/cli/commands/hook/demo.py +120 -0
- sally/app/cli/commands/server/__init__.py +0 -0
- sally/app/cli/commands/server/start.py +134 -0
- sally/app/cli/commands/version.py +43 -0
- sally/app/cli/kli.py +33 -0
- sally/app/logs.py +33 -0
- sally/core/__init__.py +0 -0
- sally/core/basing.py +102 -0
- sally/core/credentials.py +59 -0
- sally/core/handling.py +618 -0
- sally/core/httping.py +159 -0
- sally/core/monitoring.py +26 -0
- sally/core/serving.py +232 -0
- sally/core/verifying.py +74 -0
- sally-1.0.5.dist-info/METADATA +433 -0
- sally-1.0.5.dist-info/RECORD +26 -0
- sally-1.0.5.dist-info/WHEEL +5 -0
- sally-1.0.5.dist-info/entry_points.txt +2 -0
- sally-1.0.5.dist-info/licenses/LICENSE +201 -0
- sally-1.0.5.dist-info/top_level.txt +1 -0
sally/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
SALLY
|
|
5
|
+
sally package
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = '1.0.5' # also change in setup.py
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from hio.help import ogling
|
|
14
|
+
|
|
15
|
+
from sally.app.logs import TruncatedFormatter
|
|
16
|
+
|
|
17
|
+
log_name = 'sally' # name of this project that shows up in log messages
|
|
18
|
+
log_format_str = f'%(asctime)s [{log_name}] %(levelname)-8s %(module)s.%(funcName)s-%(lineno)s %(message)s'
|
|
19
|
+
|
|
20
|
+
ogler = ogling.initOgler(prefix=log_name, syslogged=False)
|
|
21
|
+
ogler.level = logging.INFO
|
|
22
|
+
|
|
23
|
+
formatter = TruncatedFormatter(log_format_str)
|
|
24
|
+
formatter.default_msec_format = None
|
|
25
|
+
logHandler = logging.StreamHandler()
|
|
26
|
+
logHandler.setFormatter(formatter)
|
|
27
|
+
ogler.baseFormatter = formatter
|
|
28
|
+
ogler.baseConsoleHandler = logHandler
|
|
29
|
+
ogler.baseConsoleHandler.setFormatter(formatter)
|
|
30
|
+
ogler.reopen(name=log_name, temp=True, clear=True)
|
|
31
|
+
|
|
32
|
+
def set_log_level(loglevel, logger):
|
|
33
|
+
"""Set the log level for the logger."""
|
|
34
|
+
ogler.level = logging.getLevelName(loglevel.upper())
|
|
35
|
+
logger.setLevel(ogler.level)
|
sally/app/__init__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from hio.base import doing
|
|
4
|
+
from keri.app import habbing, oobiing
|
|
5
|
+
|
|
6
|
+
from sally import ogler, log_name
|
|
7
|
+
|
|
8
|
+
logger = ogler.getLogger(log_name)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BootstrapRunner(doing.DoDoer):
|
|
12
|
+
def __init__(self, hby: habbing.Habery, tymth: Callable):
|
|
13
|
+
"""
|
|
14
|
+
Parameters:
|
|
15
|
+
hby (Habery): the hab in which to create the AID, if needed
|
|
16
|
+
tymth (function): the clock time (scheduler tick rate) to use from the parent Doist
|
|
17
|
+
"""
|
|
18
|
+
self.hby = hby
|
|
19
|
+
self.complete = False
|
|
20
|
+
super(BootstrapRunner, self).__init__(tymth=tymth)
|
|
21
|
+
self.extend([OobiResolveBootstrapper(hby, self)])
|
|
22
|
+
|
|
23
|
+
def recur(self, tyme, deeds=None):
|
|
24
|
+
if self.complete:
|
|
25
|
+
return True
|
|
26
|
+
super(BootstrapRunner, self).recur(tyme, deeds)
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def configureAndIncept(self):
|
|
30
|
+
"""Configure a keystore by resolving OOBIs in config"""
|
|
31
|
+
self.extend(OobiResolveBootstrapper(self.hby, self))
|
|
32
|
+
|
|
33
|
+
class OobiResolveBootstrapper(doing.Doer):
|
|
34
|
+
"""
|
|
35
|
+
Bootstraps OOBI Resolutions for a Habery and an AID, resolving all OOBIs in the bootstrap configuration file.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, hby: habbing.Habery, parent: BootstrapRunner, tock=0.0, **kwa):
|
|
39
|
+
self.hby = hby
|
|
40
|
+
self.parent = parent
|
|
41
|
+
super(OobiResolveBootstrapper, self).__init__(tock=tock, **kwa)
|
|
42
|
+
|
|
43
|
+
def configure_and_incept(self):
|
|
44
|
+
oobi_count = self.hby.db.oobis.cntAll()
|
|
45
|
+
if oobi_count:
|
|
46
|
+
obi = oobiing.Oobiery(hby=self.hby)
|
|
47
|
+
self.parent.extend(obi.doers)
|
|
48
|
+
|
|
49
|
+
while oobi_count > self.hby.db.roobi.cntAll():
|
|
50
|
+
yield 0.25
|
|
51
|
+
|
|
52
|
+
for (oobi,), obr in self.hby.db.roobi.getItemIter():
|
|
53
|
+
if obr.state in (oobiing.Result.resolved,):
|
|
54
|
+
logger.info(f"{oobi} succeeded")
|
|
55
|
+
if obr in (oobiing.Result.failed,):
|
|
56
|
+
logger.error(f"{oobi} failed")
|
|
57
|
+
|
|
58
|
+
self.parent.remove(obi.doers)
|
|
59
|
+
|
|
60
|
+
wc = [oobi for (oobi,), _ in self.hby.db.woobi.getItemIter()]
|
|
61
|
+
if len(wc) > 0:
|
|
62
|
+
logger.info(f"\nAuthenticating Well-Knowns...")
|
|
63
|
+
authn = oobiing.Authenticator(hby=self.hby)
|
|
64
|
+
self.parent.extend(authn.doers)
|
|
65
|
+
|
|
66
|
+
while True:
|
|
67
|
+
cap = []
|
|
68
|
+
for (_,), wk in self.hby.db.wkas.getItemIter(keys=b''):
|
|
69
|
+
cap.append(wk.url)
|
|
70
|
+
|
|
71
|
+
if set(wc) & set(cap) == set(wc):
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
yield 0.5
|
|
75
|
+
|
|
76
|
+
self.parent.remove(authn.doers)
|
|
77
|
+
|
|
78
|
+
self.parent.complete = True
|
|
79
|
+
|
|
80
|
+
def recur(self, tock=0.0, **opts):
|
|
81
|
+
yield from self.configure_and_incept()
|
|
82
|
+
return True
|
|
File without changes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
sally.cli.commands module
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import logging
|
|
8
|
+
import pprint
|
|
9
|
+
|
|
10
|
+
import falcon
|
|
11
|
+
from hio.core import http
|
|
12
|
+
from keri import help
|
|
13
|
+
from keri.app import directing
|
|
14
|
+
|
|
15
|
+
from sally import log_name, ogler
|
|
16
|
+
from sally.core import handling, httping
|
|
17
|
+
from sally.core.monitoring import HealthEnd
|
|
18
|
+
|
|
19
|
+
logger = ogler.getLogger(log_name)
|
|
20
|
+
|
|
21
|
+
parser = argparse.ArgumentParser(description='Launch SALLY sample web hook server')
|
|
22
|
+
parser.set_defaults(handler=lambda args: launch(args),
|
|
23
|
+
transferable=True)
|
|
24
|
+
parser.add_argument('-p', '--http',
|
|
25
|
+
action='store',
|
|
26
|
+
default=9923,
|
|
27
|
+
help="Port on which to listen for web hook events. Defaults to 9923")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def launch(args, expire=0.0):
|
|
31
|
+
baseFormatter = logging.Formatter('%(asctime)s [hook] %(levelname)-8s %(message)s')
|
|
32
|
+
baseFormatter.default_msec_format = None
|
|
33
|
+
help.ogler.baseConsoleHandler.setFormatter(baseFormatter)
|
|
34
|
+
help.ogler.level = logging.getLevelName(logging.INFO)
|
|
35
|
+
help.ogler.reopen(name="hook", temp=True, clear=True)
|
|
36
|
+
|
|
37
|
+
httpPort = args.http
|
|
38
|
+
|
|
39
|
+
app = falcon.App(
|
|
40
|
+
middleware=httping.cors_middleware())
|
|
41
|
+
app.add_route("/", WebhookListener())
|
|
42
|
+
app.add_route("/health", HealthEnd())
|
|
43
|
+
|
|
44
|
+
server = http.Server(port=httpPort, app=app)
|
|
45
|
+
httpServerDoer = http.ServerDoer(server=server)
|
|
46
|
+
|
|
47
|
+
logger.info(f"Sally Web Hook Sample listening on {httpPort}")
|
|
48
|
+
directing.runController(doers=[httpServerDoer], expire=expire)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class WebhookListener:
|
|
52
|
+
"""
|
|
53
|
+
Demonstration endpoint for web hook calls that prints events to stdout and stores a simple presentation cache.
|
|
54
|
+
"""
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self.received = dict()
|
|
57
|
+
|
|
58
|
+
def on_post(self, req, resp):
|
|
59
|
+
"""Receives web hook POST events by printing the credential results to stdout and storing presentations them in memory
|
|
60
|
+
Parameters:
|
|
61
|
+
req: falcon.Request HTTP request
|
|
62
|
+
rep: falcon.Response HTTP response
|
|
63
|
+
"""
|
|
64
|
+
logger.info("** HEADERS **")
|
|
65
|
+
logger.info(pprint.pprint(req.headers))
|
|
66
|
+
logger.info("*************")
|
|
67
|
+
|
|
68
|
+
logger.info("**** BODY ****")
|
|
69
|
+
body = req.get_media()
|
|
70
|
+
logger.info(pprint.pprint(body))
|
|
71
|
+
logger.info("**************")
|
|
72
|
+
|
|
73
|
+
data = body.get("data", {})
|
|
74
|
+
if not data:
|
|
75
|
+
logger.error("No data in body")
|
|
76
|
+
resp.media = {"error": "No data in body"}
|
|
77
|
+
resp.status = falcon.HTTP_400
|
|
78
|
+
return
|
|
79
|
+
type = self._resolve_type(data["schema"])
|
|
80
|
+
|
|
81
|
+
holder = data.get("recipient", "")
|
|
82
|
+
presentation = dict(
|
|
83
|
+
credential=data.get("credential", ""),
|
|
84
|
+
type=type,
|
|
85
|
+
issuer=body.get("actor", ""),
|
|
86
|
+
holder=holder,
|
|
87
|
+
LEI=data.get("LEI", ""),
|
|
88
|
+
personLegalName=data.get("personLegalName", ""),
|
|
89
|
+
officialRole=data.get("officialRole", ""),
|
|
90
|
+
)
|
|
91
|
+
self.received[holder] = presentation
|
|
92
|
+
resp.status = falcon.HTTP_202
|
|
93
|
+
|
|
94
|
+
def _resolve_type(self, schema_said):
|
|
95
|
+
"""Return human friendly name for schema type"""
|
|
96
|
+
match schema_said:
|
|
97
|
+
case handling.QVI_SCHEMA:
|
|
98
|
+
return "QVI"
|
|
99
|
+
case handling.LE_SCHEMA:
|
|
100
|
+
return "LE"
|
|
101
|
+
case handling.OOR_AUTH_SCHEMA:
|
|
102
|
+
return "OOR Auth"
|
|
103
|
+
case handling.OOR_SCHEMA:
|
|
104
|
+
return "OOR"
|
|
105
|
+
case _:
|
|
106
|
+
raise ValueError(f"Unknown schema type with SAID: {schema_said}")
|
|
107
|
+
|
|
108
|
+
def on_get(self, req, resp):
|
|
109
|
+
"""
|
|
110
|
+
Tells the presenter if they have presented a credential before.
|
|
111
|
+
Used in testing to determine that a presentation has succeeded.
|
|
112
|
+
"""
|
|
113
|
+
holder = req.get_param("holder", required=True)
|
|
114
|
+
if holder in self.received:
|
|
115
|
+
resp.media = self.received[holder]
|
|
116
|
+
resp.status = falcon.HTTP_200
|
|
117
|
+
else:
|
|
118
|
+
resp.media = {"error": f"No credential presented by {holder}"}
|
|
119
|
+
resp.status = falcon.HTTP_404
|
|
120
|
+
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
sally.cli.commands module
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from hio.base import doing
|
|
10
|
+
from keri.app import keeping, habbing, configing
|
|
11
|
+
from keri.app.cli.common import existing
|
|
12
|
+
|
|
13
|
+
import sally
|
|
14
|
+
from sally import ogler, log_name, set_log_level
|
|
15
|
+
from sally.app.bootstrapping import BootstrapRunner
|
|
16
|
+
from sally.core import serving
|
|
17
|
+
|
|
18
|
+
parser = argparse.ArgumentParser(description='Launch Sally vLEI credential presentation receiver service.')
|
|
19
|
+
parser.set_defaults(handler=lambda args: launch(args),
|
|
20
|
+
transferable=True)
|
|
21
|
+
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-d", "--direct", action="store_true", default=False,
|
|
24
|
+
help="Listen for direct-mode messages on HTTP port. Default is True")
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
'-p', '--http', action='store', default=9723,
|
|
27
|
+
help="Port on which to listen for OOBI requests. Defaults to 9723")
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
'-n', '--name', action='store', default="sally",
|
|
30
|
+
help="Name of controller. Default is sally.")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
'-b', '--base', required=False, default="",
|
|
33
|
+
help='additional optional prefix to file location of KERI keystore')
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-c", "--config-dir", dest="configDir",
|
|
36
|
+
help="directory override for configuration data")
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-f", '--config-file', dest="configFile", action='store', default=None,
|
|
39
|
+
help="Habery (keystore) configuration filename")
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"-i", "--incept-file", dest="inceptFile", action='store', default=None,
|
|
42
|
+
help="inception configuration filename")
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
'-a', '--alias', required=True,
|
|
45
|
+
help='human readable alias for the new identifier prefix')
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
'-s', '--salt', required=False,
|
|
48
|
+
help='qualified base64 salt for creating key pairs')
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
'--passcode', dest="bran", default=None,
|
|
51
|
+
help='21 character encryption passcode for keystore (is not saved)')
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
'-w', '--web-hook', action='store', required=True, default=None,
|
|
54
|
+
help='Webhook address for outbound notifications of credential issuance or revocation')
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
'--auth', action="store", required=True,
|
|
57
|
+
help='AID or alias of authority for OOBIs and QVI credential issuer')
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"-r", "--retry-delay", default=10, type=int, action="store",
|
|
60
|
+
help="retry delay (in seconds) for failed web hook attempts")
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"-e", "--escrow-timeout", default=10, type=int, action="store",
|
|
63
|
+
help="timeout (in minutes) for escrowed events that have not been delivered to the web hook. Defaults to 10")
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"-l", "--loglevel", action="store", required=False, default=os.getenv("SALLY_LOG_LEVEL", "INFO"),
|
|
66
|
+
help="Set log level to DEBUG | INFO | WARNING | ERROR | CRITICAL. Default is CRITICAL")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
logger = ogler.getLogger(log_name)
|
|
70
|
+
|
|
71
|
+
def launch(args, expire=0.0):
|
|
72
|
+
"""Launch Sally vLEI credential presentation receiver service"""
|
|
73
|
+
# Logging config
|
|
74
|
+
set_log_level(args.loglevel, logger)
|
|
75
|
+
|
|
76
|
+
# Parse arguments
|
|
77
|
+
hook = args.web_hook
|
|
78
|
+
name = args.name
|
|
79
|
+
salt = args.salt
|
|
80
|
+
base = args.base
|
|
81
|
+
bran = args.bran
|
|
82
|
+
try:
|
|
83
|
+
http_port = int(args.http)
|
|
84
|
+
except ValueError:
|
|
85
|
+
raise ValueError(f"Invalid port number: {args.http}. Must be an integer.")
|
|
86
|
+
auth = args.auth
|
|
87
|
+
direct = args.direct
|
|
88
|
+
|
|
89
|
+
timeout = args.escrow_timeout
|
|
90
|
+
retry = args.retry_delay
|
|
91
|
+
|
|
92
|
+
alias = args.alias
|
|
93
|
+
config_file = args.configFile
|
|
94
|
+
config_dir = args.configDir
|
|
95
|
+
|
|
96
|
+
# Inception configuration for when Sally bootstraps itself
|
|
97
|
+
incept_file = args.inceptFile
|
|
98
|
+
incept_args = {
|
|
99
|
+
"name": name, "base": base, "alias": alias, "bran": bran,
|
|
100
|
+
"incept_file": incept_file, "config_dir": config_dir
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
hby = init_habery(name=name, base=base, bran=bran, config_file=config_file,
|
|
104
|
+
config_dir=config_dir, salt=salt)
|
|
105
|
+
hab = serving.incept_if_new(hby, alias, incept_args)
|
|
106
|
+
|
|
107
|
+
# setup doers
|
|
108
|
+
doers = serving.setupDoers(hby, hab, alias=alias, http_port=http_port, hook=hook, auth=auth,
|
|
109
|
+
timeout=timeout, retry=retry, direct=direct, incept_args=incept_args)
|
|
110
|
+
logger.info(f"Sally Server v{sally.__version__} listening on {http_port} with DB version {hby.db.version}")
|
|
111
|
+
serving.run_doers(doers)
|
|
112
|
+
|
|
113
|
+
def init_habery(name, base, bran, config_file, config_dir, salt):
|
|
114
|
+
"""Initialize or reopen the Habery (keystore)"""
|
|
115
|
+
ks = keeping.Keeper(name=name, base=base, temp=False, reopen=True)
|
|
116
|
+
aeid = ks.gbls.get('aeid')
|
|
117
|
+
|
|
118
|
+
cf = None
|
|
119
|
+
if aeid is None:
|
|
120
|
+
if config_file is not None:
|
|
121
|
+
cf = configing.Configer(name=config_file, base=base, headDirPath=config_dir,
|
|
122
|
+
temp=False, reopen=True, clear=False)
|
|
123
|
+
habery_cfg = dict()
|
|
124
|
+
habery_cfg["salt"] = salt if salt else None # When None causes Habery to randomize salt
|
|
125
|
+
hby = habbing.Habery(name=name, base=base, bran=bran, cf=cf, ks=ks, **habery_cfg)
|
|
126
|
+
else:
|
|
127
|
+
ks.close() # existing.setupHby reopens, so close here to avoid LMDB conflict.
|
|
128
|
+
hby = existing.setupHby(name=name, base=base, bran=bran)
|
|
129
|
+
|
|
130
|
+
# Resolve OOBIs for the Habery and the new Hab
|
|
131
|
+
bootstrap_doist = doing.Doist(limit=30.0, tock=0.03125, real=True)
|
|
132
|
+
runner = BootstrapRunner(hby=hby, tymth=bootstrap_doist.tymen())
|
|
133
|
+
bootstrap_doist.do(doers=[runner])
|
|
134
|
+
return hby
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
sally.app.cli.commands module
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from hio.base import doing
|
|
8
|
+
from keri.app import directing
|
|
9
|
+
|
|
10
|
+
import sally
|
|
11
|
+
from keri.app.cli.common import existing
|
|
12
|
+
|
|
13
|
+
parser = argparse.ArgumentParser(description='Print version of sally CLI')
|
|
14
|
+
parser.set_defaults(handler=lambda args: handler(args))
|
|
15
|
+
parser.add_argument('--name', '-n', help='keystore name and file location of KERI keystore', required=False,
|
|
16
|
+
default=None)
|
|
17
|
+
parser.add_argument('--base', '-b', help='additional optional prefix to file location of KERI keystore',
|
|
18
|
+
required=False, default="")
|
|
19
|
+
parser.add_argument('--passcode', '-p', help='22 character encryption passcode for keystore (is not saved)',
|
|
20
|
+
dest="bran", default=None) # passcode => bran
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def handler(args):
|
|
24
|
+
kwa = dict(args=args)
|
|
25
|
+
doers = [doing.doify(version, **kwa)]
|
|
26
|
+
directing.runController(doers=doers, expire=0.0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def version(tymth, tock=0.0, **opts):
|
|
30
|
+
""" Command line version handler
|
|
31
|
+
"""
|
|
32
|
+
_ = (yield tock)
|
|
33
|
+
|
|
34
|
+
args = opts["args"]
|
|
35
|
+
name = args.name
|
|
36
|
+
base = args.base
|
|
37
|
+
bran = args.bran
|
|
38
|
+
|
|
39
|
+
print(f"Library version: {sally.__version__}")
|
|
40
|
+
|
|
41
|
+
if name is not None:
|
|
42
|
+
with existing.existingHby(name=name, base=base, bran=bran) as hby:
|
|
43
|
+
print(f"Database version: {hby.db.version}")
|
sally/app/cli/kli.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
sally.app.commands module
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import multicommand
|
|
9
|
+
|
|
10
|
+
from sally.app.cli import commands
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = multicommand.create_parser(commands)
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
|
|
17
|
+
if not hasattr(args, 'handler'):
|
|
18
|
+
parser.print_help()
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
args.handler(args)
|
|
23
|
+
except Exception as ex:
|
|
24
|
+
if os.getenv('DEBUG_KLI'):
|
|
25
|
+
import traceback
|
|
26
|
+
traceback.print_exc()
|
|
27
|
+
else:
|
|
28
|
+
print(f"ERR: {ex}")
|
|
29
|
+
return -1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
main()
|
sally/app/logs.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TruncatedFormatter(logging.Formatter):
|
|
6
|
+
"""
|
|
7
|
+
Formats log records with shorter module and function names and a right justified line number for readability.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
"2025-09-05 13:59:29 [keria] INFO serving .runAgency - 145 The Agency is loaded and waiting for requests..."
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, fmt=None, datefmt=None, style='%'):
|
|
14
|
+
super().__init__(fmt, datefmt, style)
|
|
15
|
+
|
|
16
|
+
def format(self, record):
|
|
17
|
+
"""
|
|
18
|
+
Format the log record with truncated module and function names.
|
|
19
|
+
Ignores exceptions and logs an error if formatting fails.
|
|
20
|
+
"""
|
|
21
|
+
record = copy.copy(record)
|
|
22
|
+
|
|
23
|
+
# Truncate module and funcName to first 'chars' characters
|
|
24
|
+
mod_chars = 10 # number of spaces to truncate to
|
|
25
|
+
fn_chars = 14 # number of spaces to truncate to
|
|
26
|
+
record.module = (record.module[:mod_chars] + ' ' * mod_chars)[:mod_chars]
|
|
27
|
+
record.funcName = (record.funcName[:fn_chars] + ' ' * fn_chars)[:fn_chars]
|
|
28
|
+
record.lineno = str(record.lineno).rjust(5) # Ensure line number is right-aligned
|
|
29
|
+
try:
|
|
30
|
+
return super().format(record)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logging.error(f'Error formatting log record: {e}')
|
|
33
|
+
raise e
|
sally/core/__init__.py
ADDED
|
File without changes
|
sally/core/basing.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
SALLY
|
|
4
|
+
sally.core.basing module
|
|
5
|
+
|
|
6
|
+
Database support
|
|
7
|
+
"""
|
|
8
|
+
from keri.core import coring, serdering
|
|
9
|
+
from keri.db import dbing, subing
|
|
10
|
+
|
|
11
|
+
from sally import log_name, ogler
|
|
12
|
+
|
|
13
|
+
logger = ogler.getLogger(log_name)
|
|
14
|
+
|
|
15
|
+
class CueBaser(dbing.LMDBer):
|
|
16
|
+
"""
|
|
17
|
+
Noter stores Notifications generated by the agent that are
|
|
18
|
+
intended to be read and dismissed by the controller of the agent.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
TailDirPath = "sally/db"
|
|
22
|
+
AltTailDirPath = ".sally/db"
|
|
23
|
+
TempPrefix = "sally_db_"
|
|
24
|
+
|
|
25
|
+
def __init__(self, name="cb", headDirPath=None, reopen=True, **kwa):
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
headDirPath:
|
|
30
|
+
perm:
|
|
31
|
+
reopen:
|
|
32
|
+
kwa:
|
|
33
|
+
"""
|
|
34
|
+
self.snd = None
|
|
35
|
+
|
|
36
|
+
self.iss = None
|
|
37
|
+
self.rev = None
|
|
38
|
+
|
|
39
|
+
self.recv = None
|
|
40
|
+
self.revk = None
|
|
41
|
+
|
|
42
|
+
self.ack = None
|
|
43
|
+
|
|
44
|
+
super(CueBaser, self).__init__(name=name, headDirPath=headDirPath, reopen=reopen, **kwa)
|
|
45
|
+
|
|
46
|
+
def reopen(self, **kwa):
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
:param kwa:
|
|
50
|
+
:return:
|
|
51
|
+
"""
|
|
52
|
+
super(CueBaser, self).reopen(**kwa)
|
|
53
|
+
|
|
54
|
+
# Database of senders of the presentation or revocation messages
|
|
55
|
+
self.snd = subing.CesrSuber(db=self, subkey='snd.', klas=coring.Prefixer)
|
|
56
|
+
|
|
57
|
+
# Presentations waiting for the grant's embedded ACDC and TEL state to
|
|
58
|
+
# finish verification.
|
|
59
|
+
self.iss = subing.CesrSuber(db=self, subkey='iss.', klas=coring.Dater)
|
|
60
|
+
# revocations that are waiting for the TEL event to be received and processed
|
|
61
|
+
self.rev = subing.CesrSuber(db=self, subkey='rev.', klas=coring.Dater)
|
|
62
|
+
|
|
63
|
+
# presentations with resolved credentials that need to be sent to the hook
|
|
64
|
+
self.recv = subing.SerderSuber(db=self, subkey="recv", klas=serdering.SerderACDC)
|
|
65
|
+
# revocations whose TEL rev event has been resolved that need to be sent to the hook
|
|
66
|
+
self.revk = subing.SerderSuber(db=self, subkey="revk", klas=serdering.SerderACDC)
|
|
67
|
+
|
|
68
|
+
# presentations that have been sent to the hook that need to be ack'ed
|
|
69
|
+
self.ack = subing.SerderSuber(db=self, subkey="ack", klas=serdering.SerderACDC)
|
|
70
|
+
|
|
71
|
+
return self.env
|
|
72
|
+
|
|
73
|
+
def clearEscrows(self):
|
|
74
|
+
"""
|
|
75
|
+
Clear all credential escrows. Useful in testing to avoid many unneeded log messages or force reprocessing of presentations.
|
|
76
|
+
"""
|
|
77
|
+
self.iss.trim()
|
|
78
|
+
self.rev.trim()
|
|
79
|
+
self.recv.trim()
|
|
80
|
+
self.revk.trim()
|
|
81
|
+
self.ack.trim()
|
|
82
|
+
logger.info("Cleared iss and rev escrows")
|
|
83
|
+
|
|
84
|
+
def getCounts(self):
|
|
85
|
+
"""
|
|
86
|
+
Get counts of each database for metrics monitoring
|
|
87
|
+
"""
|
|
88
|
+
snd = len([said for (said,), prefixer in self.snd.getItemIter()])
|
|
89
|
+
iss = len([said for (said,), dater in self.iss.getItemIter()])
|
|
90
|
+
rev = len([said for (said,), dater in self.rev.getItemIter()])
|
|
91
|
+
|
|
92
|
+
recv = len([said for (said, dater_qb64), creder in self.recv.getItemIter()])
|
|
93
|
+
revk = len([said for (said, dater_qb64), creder in self.revk.getItemIter()])
|
|
94
|
+
ack = len([said for (said,), creder in self.ack.getItemIter()])
|
|
95
|
+
return {
|
|
96
|
+
'senders': snd,
|
|
97
|
+
'iss': iss,
|
|
98
|
+
'rev': rev,
|
|
99
|
+
'recv': recv,
|
|
100
|
+
'revk': revk,
|
|
101
|
+
'ack': ack
|
|
102
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from hio.base import doing
|
|
2
|
+
from hio.help import decking
|
|
3
|
+
from keri.core import coring
|
|
4
|
+
|
|
5
|
+
from sally import ogler, log_name
|
|
6
|
+
|
|
7
|
+
logger = ogler.getLogger(log_name)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TeveryCuery(doing.Doer):
|
|
11
|
+
"""
|
|
12
|
+
Processes credential revocation cues and records when a given credential was revoked in the local cue database (CueBaser).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, cdb, reger, cues=None, **kwa):
|
|
16
|
+
"""
|
|
17
|
+
Parameters:
|
|
18
|
+
cdb (CueBaser): instance of CueBaser database
|
|
19
|
+
reger (Reger): Stores ACDC / TEL events
|
|
20
|
+
cues (Deck): collection of events (cue) to process
|
|
21
|
+
"""
|
|
22
|
+
self.cdb = cdb
|
|
23
|
+
self.reger = reger
|
|
24
|
+
self.cues = cues if cues is not None else decking.Deck()
|
|
25
|
+
|
|
26
|
+
super(TeveryCuery, self).__init__(**kwa)
|
|
27
|
+
|
|
28
|
+
def do(self, tymth, *, tock=0.0, **opts):
|
|
29
|
+
"""
|
|
30
|
+
Iterates over the cues Deck and processes each ACDC revocation cue to save the ACDC sender and revocation time in the cue database.
|
|
31
|
+
|
|
32
|
+
Inherited Parameters:
|
|
33
|
+
tymth (function): closure for read only injection of cycle time in seconds
|
|
34
|
+
tock (float): cycle time in seconds
|
|
35
|
+
"""
|
|
36
|
+
self.wind(tymth) # Set the clock with cycle time function
|
|
37
|
+
self.tock = tock # Set the cycle time of this doer
|
|
38
|
+
yield self.tock # allow priming of this doer to advance to this line
|
|
39
|
+
|
|
40
|
+
while True:
|
|
41
|
+
while self.cues:
|
|
42
|
+
cue = self.cues.popleft()
|
|
43
|
+
if cue['kin'] == "revoked":
|
|
44
|
+
serder = cue["serder"]
|
|
45
|
+
said = serder.ked["i"]
|
|
46
|
+
creder = self.reger.creds.get(said)
|
|
47
|
+
if creder is None:
|
|
48
|
+
logger.error(f"revocation received for unknown credential {said}")
|
|
49
|
+
|
|
50
|
+
prefixer = coring.Prefixer(qb64=creder.issuer)
|
|
51
|
+
saider = coring.Saider(qb64=said)
|
|
52
|
+
now = coring.Dater()
|
|
53
|
+
|
|
54
|
+
self.cdb.snd.pin(keys=(saider.qb64,), val=prefixer)
|
|
55
|
+
self.cdb.rev.pin(keys=(saider.qb64,), val=now)
|
|
56
|
+
|
|
57
|
+
yield self.tock
|
|
58
|
+
|
|
59
|
+
yield self.tock
|