Merge pull request #2460 from KPrasch/probation

Complete grant CLI policy interactons and probationary period enforcement
pull/2461/head
K Prasch 2020-12-08 14:08:53 -08:00 committed by GitHub
commit af1c11b0d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 190 additions and 54 deletions

View File

@ -0,0 +1 @@
Introduces a probationary period for policy creation in the network, until 2021-02-28 23:59:59 UTC.

View File

@ -0,0 +1 @@
Complete interactive collection of policy parameters via alice grant CLI.

View File

@ -15,14 +15,14 @@
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import click
from marshmallow import validates_schema
from nucypher.cli import options, types
from nucypher.characters.control.specifications import fields
from nucypher.characters.control.specifications.base import BaseSchema
from nucypher.characters.control.specifications.exceptions import (
InvalidArgumentCombo)
from nucypher.cli import options, types
from nucypher.characters.control.specifications.exceptions import InvalidArgumentCombo
class PolicyBaseSchema(BaseSchema):
@ -52,7 +52,8 @@ class PolicyBaseSchema(BaseSchema):
click=click.option(
'--expiration',
help="Expiration Datetime of a policy",
type=click.STRING))
type=click.DateTime())
)
# optional input
value = fields.Wei(
@ -96,7 +97,7 @@ class GrantPolicy(PolicyBaseSchema):
label = fields.Label(
load_only=True, required=True,
click=options.option_label(required=True))
click=options.option_label(required=False))
# output fields
treasure_map = fields.TreasureMap(dump_only=True)

View File

@ -14,56 +14,68 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import contextlib
import json
from collections import OrderedDict
import maya
import random
import time
from base64 import b64decode, b64encode
from collections import OrderedDict
from datetime import datetime
from functools import partial
from json.decoder import JSONDecodeError
from queue import Queue
from random import shuffle
from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
from bytestring_splitter import (
BytestringKwargifier,
BytestringSplitter,
BytestringSplittingError,
VariableLengthBytestring
)
from constant_sorrow import constants
from constant_sorrow.constants import (
INCLUDED_IN_BYTESTRING,
PUBLIC_ONLY,
STRANGER_ALICE,
UNKNOWN_VERSION,
READY,
INVALIDATED
)
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509 import Certificate, NameOID, load_pem_x509_certificate
from datetime import datetime
from eth_utils import to_checksum_address
from flask import Response, request
from functools import partial
from json.decoder import JSONDecodeError
from queue import Queue
from random import shuffle
from twisted.internet import reactor, stdio, threads
from twisted.internet.task import LoopingCall
from typing import Dict, Iterable, List, Optional, Tuple, Union
from umbral import pre
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
from umbral.pre import UmbralCorrectnessError
from umbral.signing import Signature
import nucypher
from bytestring_splitter import BytestringKwargifier, BytestringSplitter, BytestringSplittingError, \
VariableLengthBytestring
from constant_sorrow import constants
from constant_sorrow.constants import (INCLUDED_IN_BYTESTRING,
PUBLIC_ONLY,
STRANGER_ALICE,
UNKNOWN_VERSION,
READY,
INVALIDATED)
from nucypher.acumen.nicknames import Nickname
from nucypher.acumen.perception import FleetSensor
from nucypher.blockchain.eth.actors import BlockchainPolicyAuthor, Worker
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
from nucypher.blockchain.eth.constants import LENGTH_ECDSA_SIGNATURE_WITH_RECOVERY, ETH_ADDRESS_BYTE_LENGTH
from nucypher.blockchain.eth.constants import ETH_ADDRESS_BYTE_LENGTH
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.token import WorkTracker
from nucypher.characters.banners import ALICE_BANNER, BOB_BANNER, ENRICO_BANNER, URSULA_BANNER
from nucypher.characters.base import Character, Learner
from nucypher.characters.control.controllers import (
WebController
)
from nucypher.characters.control.controllers import WebController
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.characters.control.interfaces import AliceInterface, BobInterface, EnricoInterface
from nucypher.cli.processes import UrsulaCommandProtocol
from nucypher.config.constants import END_OF_POLICIES_PROBATIONARY_PERIOD
from nucypher.config.storages import ForgetfulNodeStorage, NodeStorage
from nucypher.crypto.api import encrypt_and_sign, keccak_digest
from nucypher.crypto.constants import HRAC_LENGTH, PUBLIC_KEY_LENGTH
@ -80,11 +92,6 @@ from nucypher.network.protocols import InterfaceInfo, parse_node_uri
from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app
from nucypher.network.trackers import AvailabilityTracker
from nucypher.utilities.logging import Logger
from umbral import pre
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
from umbral.pre import UmbralCorrectnessError
from umbral.signing import Signature
class Alice(Character, BlockchainPolicyAuthor):
@ -288,7 +295,13 @@ class Alice(Character, BlockchainPolicyAuthor):
self.remember_node(node=handpicked_ursula)
policy = self.create_policy(bob=bob, label=label, **policy_params)
self.log.debug(f"Successfully created {policy} ... ")
# TODO: Remove when the time is right.
if policy.expiration > END_OF_POLICIES_PROBATIONARY_PERIOD:
raise self.ActorError(f"The requested duration for this policy (until {policy.expiration}) exceeds the "
f"probationary period ({END_OF_POLICIES_PROBATIONARY_PERIOD}).")
self.log.debug(f"Generated new policy proposal {policy} ... ")
#
# We'll find n Ursulas by default. It's possible to "play the field" by trying different

View File

@ -65,7 +65,7 @@ def get_nucypher_password(confirm: bool = False, envvar=NUCYPHER_ENVVAR_KEYRING_
def unlock_nucypher_keyring(emitter: StdoutEmitter, password: str, character_configuration: CharacterConfiguration) -> bool:
"""Unlocks a nucypher keyring and attaches it to the supplied configuration if successful."""
emitter.message(DECRYPTING_CHARACTER_KEYRING.format(name=character_configuration.NAME), color='yellow')
emitter.message(DECRYPTING_CHARACTER_KEYRING.format(name=character_configuration.NAME.capitalize()), color='yellow')
# precondition
if character_configuration.dev_mode:

View File

@ -14,7 +14,8 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from typing import Type, Union
from tabulate import tabulate
from typing import Type, Union, Dict
import click
from constant_sorrow.constants import UNKNOWN_DEVELOPMENT_CHAIN_ID
@ -144,3 +145,11 @@ def verify_upgrade_details(blockchain: Union[BlockchainDeployerInterface, Blockc
click.confirm(CONFIRM_VERSIONED_UPGRADE.format(contract_name=deployer.contract_name,
old_version=old_contract.version,
new_version=new_version), abort=True)
def confirm_staged_grant(emitter, grant_request: Dict) -> None:
# TODO: Expand and detail
emitter.echo("Successfully staged grant. Please review the details:\n", color='green')
table = ([field, value] for field, value in grant_request.items())
emitter.echo(tabulate(table, tablefmt="simple"))
click.confirm('\nGrant access and sign transaction?', abort=True)

View File

@ -15,9 +15,13 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import click
import maya
import os
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_PASSWORD
from datetime import timedelta
from web3.main import Web3
from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.characters.control.emitters import StdoutEmitter
@ -25,8 +29,10 @@ from nucypher.characters.control.interfaces import AliceInterface
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password
from nucypher.cli.actions.configure import (
destroy_configuration,
handle_missing_configuration_file, get_or_update_configuration
handle_missing_configuration_file,
get_or_update_configuration
)
from nucypher.cli.actions.confirm import confirm_staged_grant
from nucypher.cli.actions.select import select_client_account, select_config_file
from nucypher.cli.commands.deploy import option_gas_strategy
from nucypher.cli.config import group_general_config
@ -55,12 +61,19 @@ from nucypher.cli.options import (
option_teacher_uri,
option_lonely
)
from nucypher.cli.painting.help import paint_new_installation_help
from nucypher.cli.painting.help import (
paint_new_installation_help,
paint_probationary_period_disclaimer,
enforce_probationary_period
)
from nucypher.cli.processes import get_geth_provider_process
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, GWEI
from nucypher.cli.utils import make_cli_character, setup_emitter
from nucypher.config.characters import AliceConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD, TEMPORARY_DOMAIN
from nucypher.config.constants import (
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
TEMPORARY_DOMAIN,
)
from nucypher.config.keyring import NucypherKeyring
from nucypher.network.middleware import RestMiddleware
@ -428,6 +441,7 @@ def derive_policy_pubkey(general_config, label, character_options, config_file):
@option_config_file
@group_general_config
@group_character_options
@option_force
def grant(general_config,
bob_encrypting_key,
bob_verifying_key,
@ -437,7 +451,8 @@ def grant(general_config,
expiration,
m, n,
character_options,
config_file):
config_file,
force):
"""Create and enact an access policy for some Bob. """
# Setup
@ -447,13 +462,62 @@ def grant(general_config,
# Input validation
if ALICE.federated_only:
if any((value, rate)):
raise click.BadOptionUsage(option_name="--value, --rate",
message="Can't use --value or --rate with a federated Alice.")
message = "Can't use --value or --rate with a federated Alice."
raise click.BadOptionUsage(option_name="--value, --rate", message=message)
elif bool(value) and bool(rate):
raise click.BadOptionUsage(option_name="--rate", message="Can't use --value if using --rate")
elif not (bool(value) or bool(rate)):
rate = ALICE.default_rate # TODO #1709
click.confirm(f"Confirm default rate {rate}?", abort=True)
# Interactive collection follows:
# TODO: Extricate to support modules
# - Disclaimer
# - Label
# - Expiration Date & Time
# - M of N
# - Policy Value (ETH)
# Policy Expiration
# TODO: Remove this line when the time is right.
paint_probationary_period_disclaimer(emitter)
# Label
if not label:
label = click.prompt(f'Enter label to grant Bob {bob_verifying_key[:8]}', type=click.STRING)
if not force and not expiration:
if ALICE.duration_periods:
# TODO: use a default in days or periods?
expiration = maya.now() + timedelta(days=ALICE.duration_periods) # default
if not click.confirm(f'Use default policy duration (expires {expiration})?'):
expiration = click.prompt('Enter policy expiration datetime', type=click.DateTime())
else:
# No policy duration default default available; Go interactive
expiration = click.prompt('Enter policy expiration datetime', type=click.DateTime())
# TODO: Remove this line when the time is right.
enforce_probationary_period(emitter=emitter, expiration=expiration)
# Policy Threshold and Shares
if not n:
n = ALICE.n
if not force and not click.confirm(f'Use default value for N ({n})?', default=True):
n = click.prompt('Enter total number of shares (N)', type=click.INT)
if not m:
m = ALICE.m
if not force and not click.confirm(f'Use default value for M ({m})?', default=True):
m = click.prompt('Enter threshold (M)', type=click.IntRange(1, n))
# Policy Value
policy_value_provided = bool(value) or bool(rate)
if not ALICE.federated_only and not policy_value_provided:
rate = ALICE.default_rate # TODO #1709 - Fine tuning and selection of default rates
if not force:
default_gwei = Web3.fromWei(rate, 'gwei')
prompt = "Confirm rate of {node_rate} gwei ({total_rate} gwei per period)?"
if not click.confirm(prompt.format(node_rate=default_gwei, total_rate=default_gwei*n), default=True):
interactive_rate = click.prompt('Enter rate per period in gwei', type=GWEI)
# TODO: Validate interactively collected rate (#1709)
click.confirm(prompt.format(node_rate=rate, total_rate=rate*n), default=True, abort=True)
rate = Web3.toWei(interactive_rate, 'gwei')
# Request
grant_request = {
@ -468,7 +532,11 @@ def grant(general_config,
if value:
grant_request['value'] = value
elif rate:
grant_request['rate'] = rate
grant_request['rate'] = rate # in wei
if not force and not general_config.json_ipc:
confirm_staged_grant(emitter=emitter, grant_request=grant_request)
emitter.echo(f'Granting Access to {bob_verifying_key[:8]}', color='yellow')
return ALICE.controller.grant(request=grant_request)

View File

@ -454,6 +454,10 @@ def unbond_worker(general_config: GroupGeneralConfig,
blockchain=blockchain,
client_account=client_account,
hw_wallet=transacting_staker_options.hw_wallet)
if not force:
click.confirm("Are you sure you want to unbond your worker?", abort=True)
STAKEHOLDER.assimilate(password=password)
receipt = STAKEHOLDER.unbond_worker()

View File

@ -355,7 +355,7 @@ COLLECT_NUCYPHER_PASSWORD = "Enter NuCypher keyring password"
GENERIC_PASSWORD_PROMPT = "Enter password"
DECRYPTING_CHARACTER_KEYRING = 'Decrypting {name} keyring...'
DECRYPTING_CHARACTER_KEYRING = 'Authenticating {name}'
#

View File

@ -16,10 +16,11 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import click
import maya
from nucypher.blockchain.eth.sol.__conf__ import SOLIDITY_COMPILER_VERSION
from nucypher.characters.banners import NUCYPHER_BANNER
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, USER_LOG_DIR
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, USER_LOG_DIR, END_OF_POLICIES_PROBATIONARY_PERIOD
def echo_version(ctx, param, value):
@ -74,6 +75,38 @@ def paint_new_installation_help(emitter, new_configuration):
character_name_starts_with_vowel = character_name[0].lower() in vowels
adjective = 'an' if character_name_starts_with_vowel else 'a'
suggested_command = f'nucypher {character_name} run'
how_to_run_message = f"\nTo run {adjective} {character_name.capitalize()} node from the default configuration filepath run: \n\n'{suggested_command}'\n"
how_to_run_message = f"\nTo run {adjective} {character_name.capitalize()} node from the default configuration " \
f"filepath run: \n\n'{suggested_command}'\n"
emitter.echo(how_to_run_message.format(suggested_command), color='green')
def paint_probationary_period_disclaimer(emitter):
width = 60
import textwrap
disclaimer_title = " DISCLAIMER ".center(width, "=")
paragraph = f"""
Some areas of the NuCypher network are still under active development;
as a consequence, we have established a probationary period for policies in the network.
Currently the creation of sharing policies with durations beyond {END_OF_POLICIES_PROBATIONARY_PERIOD} are prevented.
After this date the probationary period will be over, and you will be able to create policies with any duration
as supported by nodes on the network.
"""
text = (
"\n",
disclaimer_title,
*[line.center(width) for line in textwrap.wrap(paragraph, width - 2)],
"=" * len(disclaimer_title),
"\n"
)
for sentence in text:
emitter.echo(sentence, color='yellow')
def enforce_probationary_period(emitter, expiration):
"""Used during CLI grant to prevent publication of a policy outside the probationary period."""
if maya.MayaDT.from_datetime(expiration) > END_OF_POLICIES_PROBATIONARY_PERIOD:
emitter.echo(f"The requested duration for this policy (until {expiration}) exceeds the probationary period"
f" ({END_OF_POLICIES_PROBATIONARY_PERIOD}).", color="red")
raise click.Abort()

View File

@ -22,6 +22,8 @@ from pathlib import Path
import os
from appdirs import AppDirs
from maya import MayaDT
import nucypher
# Environment variables
@ -69,3 +71,6 @@ TEMPORARY_DOMAIN = ":temporary-domain:" # for use with `--dev` node runtimes
# Event Blocks Throttling
NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS = 'NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS'
# Probationary period (see #2353)
END_OF_POLICIES_PROBATIONARY_PERIOD = MayaDT.from_iso8601('2021-02-28T23:59:59.0Z')

View File

@ -294,6 +294,7 @@ def run_entire_cli_lifecycle(click_runner,
bob_keys = side_channel.fetch_bob_public_keys()
bob_encrypting_key = bob_keys.bob_encrypting_key
bob_verifying_key = bob_keys.bob_verifying_key
expiration = (maya.now() + datetime.timedelta(days=3)).datetime().strftime("%Y-%m-%d %H:%M:%S")
grant_args = ('alice', 'grant',
'--mock-networking',
@ -303,7 +304,7 @@ def run_entire_cli_lifecycle(click_runner,
'--config-file', alice_configuration_file_location,
'--m', 2,
'--n', 3,
'--expiration', (maya.now() + datetime.timedelta(days=3)).iso8601(),
'--expiration', expiration,
'--label', random_label,
'--bob-encrypting-key', bob_encrypting_key,
'--bob-verifying-key', bob_verifying_key)
@ -314,9 +315,8 @@ def run_entire_cli_lifecycle(click_runner,
grant_args += ('--provider', TEST_PROVIDER_URI,
'--rate', Web3.toWei(9, 'gwei'))
# TODO: Stop.
grant_result = click_runner.invoke(nucypher_cli, grant_args, catch_exceptions=False, env=envvars)
assert grant_result.exit_code == 0
assert grant_result.exit_code == 0, grant_result.output
grant_result = json.loads(grant_result.output)

View File

@ -755,6 +755,7 @@ def test_stake_unbond_worker(click_runner,
init_args = ('stake', 'unbond-worker',
'--config-file', stakeholder_configuration_file_location,
'--staking-address', manual_staker,
'--force'
)
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}'

View File

@ -118,7 +118,7 @@ def test_unlock_nucypher_keyring_invalid_password(mocker, test_emitter, alice_bl
keyring_attach_spy.assert_called_once()
captured = capsys.readouterr()
assert DECRYPTING_CHARACTER_KEYRING.format(name='alice') in captured.out
assert DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize()) in captured.out
def test_unlock_nucypher_keyring_dev_mode(mocker, test_emitter, capsys, alice_blockchain_test_config):
@ -137,7 +137,7 @@ def test_unlock_nucypher_keyring_dev_mode(mocker, test_emitter, capsys, alice_bl
assert result
output = capsys.readouterr().out
message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME)
message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize())
assert message in output
unlock_spy.assert_not_called()
@ -166,7 +166,7 @@ def test_unlock_nucypher_keyring(mocker,
assert result
captured = capsys.readouterr()
message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME)
message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize())
assert message in captured.out
unlock_spy.assert_called_once_with(password=INSECURE_DEVELOPMENT_PASSWORD)