refactor (review): move grid-manager certificate loading code out of cli

This commit is contained in:
meejah 2020-11-17 17:46:26 -07:00
parent 649cb9380e
commit 938cc5616e
3 changed files with 97 additions and 37 deletions

View File

@ -152,23 +152,16 @@ def list(ctx):
""" """
List all storage-servers known to a Grid Manager List all storage-servers known to a Grid Manager
""" """
fp = _config_path_from_option(ctx.parent.params["config"])
for name in sorted(ctx.obj.grid_manager.storage_servers.keys()): for name in sorted(ctx.obj.grid_manager.storage_servers.keys()):
blank_name = " " * len(name) blank_name = " " * len(name)
click.echo("{}: {}".format(name, ctx.obj.grid_manager.storage_servers[name].public_key())) click.echo("{}: {}".format(name, ctx.obj.grid_manager.storage_servers[name].public_key_string()))
if fp: for cert in ctx.obj.grid_manager.storage_servers[name].certificates:
cert_count = 0 delta = datetime.utcnow() - cert.expires
while fp.child('{}.cert.{}'.format(name, cert_count)).exists(): click.echo("{} cert {}: ".format(blank_name, cert.index), nl=False)
container = json.load(fp.child('{}.cert.{}'.format(name, cert_count)).open('r')) if delta.total_seconds() < 0:
cert_data = json.loads(container['certificate']) click.echo("valid until {} ({})".format(cert.expires, abbreviate_time(delta)))
expires = datetime.utcfromtimestamp(cert_data['expires']) else:
delta = datetime.utcnow() - expires click.echo("expired {} ({})".format(cert.expires, abbreviate_time(delta)))
click.echo("{} cert {}: ".format(blank_name, cert_count), nl=False)
if delta.total_seconds() < 0:
click.echo("valid until {} ({})".format(expires, abbreviate_time(delta)))
else:
click.echo("expired {} ({})".format(expires, abbreviate_time(delta)))
cert_count += 1
@grid_manager.command() @grid_manager.command()
@ -196,15 +189,20 @@ def sign(ctx, name, expiry_days):
click.echo(certificate_data) click.echo(certificate_data)
if fp is not None: if fp is not None:
next_serial = 0 next_serial = 0
while fp.child("{}.cert.{}".format(name, next_serial)).exists(): f = None
while f is None:
try:
f = fp.child("{}.cert.{}".format(name, next_serial)).create()
except Exception:
f = None
next_serial += 1 next_serial += 1
with fp.child('{}.cert.{}'.format(name, next_serial)).open('w') as f: with f:
f.write(certificate_data) f.write(certificate_data)
def _config_path_from_option(config): def _config_path_from_option(config):
""" """
:param string config: a path or - :param str config: a path or -
:returns: a FilePath instance or None :returns: a FilePath instance or None
""" """
if config == "-": if config == "-":

View File

@ -1,3 +1,4 @@
import sys import sys
import json import json
from datetime import ( from datetime import (
@ -13,29 +14,46 @@ from allmydata.util import (
base32, base32,
) )
import attr
@attr.s
class _GridManagerStorageServer(object): class _GridManagerStorageServer(object):
""" """
A Grid Manager's notion of a storage server A Grid Manager's notion of a storage server
""" """
def __init__(self, name, public_key, certificates): name = attr.ib()
self.name = name public_key = attr.ib(validator=attr.validators.instance_of(ed25519.Ed25519PublicKey))
self._public_key = public_key certificates = attr.ib(
self._certificates = [] if certificates is None else certificates default=attr.Factory(list),
validator=attr.validators.instance_of(list),
)
def add_certificate(self, certificate): def add_certificate(self, certificate):
self._certificates.append(certificate) self.certificates.append(certificate)
def public_key(self): def public_key_string(self):
return ed25519.string_from_verifying_key(self._public_key) return ed25519.string_from_verifying_key(self.public_key)
def marshal(self): def marshal(self):
return { return {
u"public_key": self.public_key(), u"public_key": self.public_key_string(),
} }
@attr.s
class _GridManagerCertificate(object):
"""
Represents a single certificate for a single storage-server
"""
filename = attr.ib()
index = attr.ib(validator=attr.validators.instance_of(int))
expires = attr.ib(validator=attr.validators.instance_of(datetime))
public_key = attr.ib(validator=attr.validators.instance_of(ed25519.Ed25519PublicKey))
def create_grid_manager(): def create_grid_manager():
""" """
Create a new Grid Manager with a fresh keypair Create a new Grid Manager with a fresh keypair
@ -47,6 +65,52 @@ def create_grid_manager():
) )
def _load_certificates_for(config_path, name, gm_key=None):
"""
Load any existing certificates for the given storage-server.
:param FilePath config_path: the configuration location (or None for
stdin)
:param str name: the name of an existing storage-server
:param ed25519.VerifyingKey gm_key: an optional Grid Manager
public key. If provided, certificates will be verified against it.
:returns: list containing any known certificates (may be empty)
:raises: ed25519.BadSignature if any certificate signature fails to verify
"""
if config_path is None:
return []
cert_index = 0
cert_path = config_path.child('{}.cert.{}'.format(name, cert_index))
certificates = []
while cert_path.exists():
container = json.load(cert_path.open('r'))
if gm_key is not None:
validate_grid_manager_certificate(gm_key, container)
cert_data = json.loads(container['certificate'])
if cert_data['version'] != 1:
raise ValueError(
"Unknown certificate version '{}' in '{}'".format(
cert_data['version'],
cert_path.path,
)
)
certificates.append(
_GridManagerCertificate(
filename=cert_path.path,
index=cert_index,
expires=datetime.utcfromtimestamp(cert_data['expires']),
public_key=ed25519.verifying_key_from_string(cert_data['public_key'].encode('ascii')),
)
)
cert_index += 1
cert_path = config_path.child('{}.cert.{}'.format(name, cert_index))
return certificates
def load_grid_manager(config_path): def load_grid_manager(config_path):
""" """
Load a Grid Manager from existing configuration. Load a Grid Manager from existing configuration.
@ -56,17 +120,15 @@ def load_grid_manager(config_path):
:returns: a GridManager instance :returns: a GridManager instance
:raises: ValueError if the confguration is invalid :raises: ValueError if the confguration is invalid or IOError if
expected files can't be opened.
""" """
if config_path is None: if config_path is None:
config_file = sys.stdin config_file = sys.stdin
else: else:
try: # this might raise IOError or similar but caller must handle it
config_file = config_path.child("config.json").open("r") config_file = config_path.child("config.json").open("r")
except IOError:
raise ValueError(
"'{}' is not a Grid Manager config-directory".format(config_path)
)
with config_file: with config_file:
config = json.load(config_file) config = json.load(config_file)
@ -99,7 +161,7 @@ def load_grid_manager(config_path):
storage_servers[name] = _GridManagerStorageServer( storage_servers[name] = _GridManagerStorageServer(
name, name,
ed25519.verifying_key_from_string(srv_config['public_key'].encode('ascii')), ed25519.verifying_key_from_string(srv_config['public_key'].encode('ascii')),
None, _load_certificates_for(config_path, name, public_key),
) )
return _GridManager(private_key_bytes, storage_servers) return _GridManager(private_key_bytes, storage_servers)
@ -134,7 +196,7 @@ class _GridManager(object):
epoch_offset = (expiration - datetime(1970, 1, 1)).total_seconds() epoch_offset = (expiration - datetime(1970, 1, 1)).total_seconds()
cert_info = { cert_info = {
"expires": epoch_offset, "expires": epoch_offset,
"public_key": srv.public_key(), "public_key": srv.public_key_string(),
"version": 1, "version": 1,
} }
cert_data = json.dumps(cert_info, separators=(',',':'), sort_keys=True).encode('utf8') cert_data = json.dumps(cert_info, separators=(',',':'), sort_keys=True).encode('utf8')
@ -161,7 +223,7 @@ class _GridManager(object):
raise KeyError( raise KeyError(
"Already have a storage server called '{}'".format(name) "Already have a storage server called '{}'".format(name)
) )
ss = _GridManagerStorageServer(name, public_key, None) ss = _GridManagerStorageServer(name, public_key, [])
self._storage_servers[name] = ss self._storage_servers[name] = ss
return ss return ss

View File

@ -151,7 +151,7 @@ class GridManagerVerifier(SyncTestCase):
for name, ss0 in self.gm.storage_servers.items(): for name, ss0 in self.gm.storage_servers.items():
ss1 = gm2.storage_servers[name] ss1 = gm2.storage_servers[name]
self.assertEqual(ss0.name, ss1.name) self.assertEqual(ss0.name, ss1.name)
self.assertEqual(ss0.public_key(), ss1.public_key()) self.assertEqual(ss0.public_key_string(), ss1.public_key_string())
self.assertEqual(self.gm.marshal(), gm2.marshal()) self.assertEqual(self.gm.marshal(), gm2.marshal())
def test_invalid_no_version(self): def test_invalid_no_version(self):