diff --git a/nucypher/network/server.py b/nucypher/network/server.py index b51cbeaa1..4dfa8b8ad 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -217,7 +217,17 @@ def _make_rest_app(this_node, log: Logger) -> Flask: capsules_to_process = list() for capsule, lingo in packets: # raises an exception or continues - evaluate_conditions_for_ursula(lingo=lingo, providers=providers, context=context) + result, error = evaluate_conditions_for_ursula( + lingo=lingo, providers=providers, context=context + ) + if error: + # error cases + return Response(*error) + elif not result: + # explicit condition failure + return Response( + "Decryption conditions not satisfied", HTTPStatus.FORBIDDEN + ) capsules_to_process.append((lingo, capsule)) # Strip away conditions that have already been evaluated diff --git a/nucypher/policy/conditions/_utils.py b/nucypher/policy/conditions/_utils.py index 9d15fe4c1..f25ceb373 100644 --- a/nucypher/policy/conditions/_utils.py +++ b/nucypher/policy/conditions/_utils.py @@ -1,8 +1,24 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + 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 . +""" + import json from http import HTTPStatus -from typing import Union, Type, Dict, Optional +from typing import Dict, Optional, Tuple, Type, Union -from flask import Response from marshmallow import Schema, post_dump from web3.providers import BaseProvider @@ -12,6 +28,7 @@ from nucypher.policy.conditions.context import ( InvalidContextVariableData, RequiredContextVariable, ) +from nucypher.policy.conditions.evm import RPCCondition from nucypher.utilities.logging import Logger _ETH = 'eth_' @@ -48,10 +65,9 @@ def _resolve_condition_lingo(json_data) -> Union[Type['Operator'], Type['Reencry conditions expression framework. """ # TODO: This is ugly but avoids circular imports :-| - from nucypher.policy.conditions.time import TimeCondition - from nucypher.policy.conditions.evm import ContractCondition - from nucypher.policy.conditions.evm import RPCCondition + from nucypher.policy.conditions.evm import ContractCondition, RPCCondition from nucypher.policy.conditions.lingo import Operator + from nucypher.policy.conditions.time import TimeCondition # Inspect method = json_data.get('method') @@ -81,57 +97,49 @@ def _deserialize_condition_lingo(data: Union[str, Dict[str, str]]) -> Union['Ope return instance -def evaluate_conditions_for_ursula(lingo: 'ConditionLingo', - providers: Optional[Dict[str, BaseProvider]] = None, - context: Optional[Dict[Union[str, int], Union[str, int]]] = None, - log: Logger = __LOGGER, - ) -> Response: +def evaluate_conditions_for_ursula( + lingo: "ConditionLingo", + providers: Optional[Dict[str, BaseProvider]] = None, + context: Optional[Dict[Union[str, int], Union[str, int]]] = None, + log: Logger = __LOGGER, +) -> Tuple[bool, Optional[Tuple[str, HTTPStatus]]]: # avoid using a mutable defaults and support federated mode context = context or dict() providers = providers or dict() - + result, error = False, None if lingo is not None: # TODO: Evaluate all conditions even if one fails and report the result try: log.info(f'Evaluating access conditions {lingo.id}') - _results = lingo.eval(providers=providers, **context) + result = lingo.eval(providers=providers, **context) except ReencryptionCondition.InvalidCondition as e: message = f"Incorrect value provided for condition: {e}" error = (message, HTTPStatus.BAD_REQUEST) - log.info(message) - return Response(message, status=error[1]) except RequiredContextVariable as e: message = f"Missing required inputs: {e}" # TODO: be more specific and name the missing inputs, etc error = (message, HTTPStatus.BAD_REQUEST) - log.info(message) - return Response(message, status=error[1]) except InvalidContextVariableData as e: message = f"Invalid data provided for context variable: {e}" error = (message, HTTPStatus.BAD_REQUEST) - log.info(message) - return Response(message, status=error[1]) except ContextVariableVerificationFailed as e: message = f"Context variable data could not be verified: {e}" error = (message, HTTPStatus.FORBIDDEN) - log.info(message) - return Response(message, status=error[1]) + except RPCCondition.NoConnectionToChain as e: + message = f"Node does not have a connection to chain ID {e.chain}: {e}" + error = (message, HTTPStatus.NOT_IMPLEMENTED) except ReencryptionCondition.ConditionEvaluationFailed as e: message = f"Decryption condition not evaluated: {e}" error = (message, HTTPStatus.BAD_REQUEST) - log.info(message) - return Response(message, status=error[1]) - except lingo.Failed as e: - # TODO: Better error reporting - message = f"Decryption conditions not satisfied: {e}" - error = (message, HTTPStatus.FORBIDDEN) - log.info(message) - return Response(message, status=error[1]) except Exception as e: # TODO: Unsure why we ended up here message = f"Unexpected exception while evaluating " \ f"decryption condition ({e.__class__.__name__}): {e}" error = (message, HTTPStatus.INTERNAL_SERVER_ERROR) log.warn(message) - return Response(message, status=error[1]) + + if error: + log.info(error[0]) + + return result, error diff --git a/nucypher/policy/conditions/evm.py b/nucypher/policy/conditions/evm.py index 1d9c271da..d4e9b349a 100644 --- a/nucypher/policy/conditions/evm.py +++ b/nucypher/policy/conditions/evm.py @@ -16,7 +16,7 @@ """ import re -from typing import Any, List, Optional, Tuple, Dict +from typing import Any, Dict, List, Optional, Tuple from eth_typing import ChecksumAddress from eth_utils import to_checksum_address @@ -31,7 +31,6 @@ from nucypher.policy.conditions.base import ReencryptionCondition from nucypher.policy.conditions.context import get_context_value, is_context_variable from nucypher.policy.conditions.lingo import ReturnValueTest - # Permitted blockchains for condition evaluation _CONDITION_CHAINS = ( 1, # ethereum/mainnet @@ -113,6 +112,13 @@ class RPCCondition(ReencryptionCondition): class RPCExecutionFailed(ReencryptionCondition.ConditionEvaluationFailed): """Raised when an exception is raised from an RPC call.""" + class NoConnectionToChain(RuntimeError): + """Raised when a node does not have an associated provider for a chain.""" + + def __init__(self, chain: int, *args, **kwargs): + self.chain = chain + super().__init__(*args, **kwargs) + class Schema(CamelCaseSchema): name = fields.Str(required=False) chain = fields.Int(required=True) @@ -163,9 +169,10 @@ class RPCCondition(ReencryptionCondition): try: provider = providers[self.chain] except KeyError: - # TODO Use a custom exception class, and catch bubble it up to include info about the node - # QUESTION Are nodes required to provide connections to all providers? - raise Exception(f'This node does not have a connection to chain {self.chain}') + raise self.NoConnectionToChain( + chain=self.chain, + message=f"This node does not have a connection to chain ID {self.chain}", + ) # Instantiate a local web3 instance self.w3 = Web3(provider) diff --git a/nucypher/policy/conditions/lingo.py b/nucypher/policy/conditions/lingo.py index 5b05aac64..aeac49d29 100644 --- a/nucypher/policy/conditions/lingo.py +++ b/nucypher/policy/conditions/lingo.py @@ -21,7 +21,7 @@ import base64 import json import operator as pyoperator from hashlib import md5 -from typing import Any, Dict, List, Union, Iterator +from typing import Any, Dict, Iterator, List, Union from marshmallow import fields, post_load @@ -191,6 +191,9 @@ class ConditionLingo: data = self.to_json().encode() return data + def __repr__(self): + return f"{self.__class__.__name__} (id={self.id} | size={len(bytes(self))})" + def __eval(self, eval_string: str): # TODO: Additional protection and/or sanitation here result = eval(eval_string) @@ -214,9 +217,7 @@ class ConditionLingo: # [True, , False] -> 'True or False' eval_string = ' '.join(str(e) for e in data) result = self.__eval(eval_string=eval_string) - if not result: - raise self.Failed - return True + return result OR = Operator('or')