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