mirror of https://github.com/nucypher/nucypher.git
Update server logic to properly handle errors and unsatisfied conditions.
parent
806f08b1be
commit
6a3500dffe
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue