Update server logic to properly handle errors and unsatisfied conditions.

pull/3007/head
derekpierre 2022-11-09 17:04:45 -05:00
parent 806f08b1be
commit 6a3500dffe
4 changed files with 65 additions and 39 deletions

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
"""
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

View File

@ -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)

View File

@ -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, <Operator>, 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')