-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added annotations to auto-manage the sign/unsign input parameters and…
… return (#2457) * Adding vscode ruff extension and removing snyk * Removing x-ray extra noise * Fixed typo * Expanded test coverage and fixed a few bugs in the sign/unsign annotations * Fixed comment of the unsign_params annotation * Forgot to add latest tests :X Removed unused imports * Apply black formatter formatting on misformatted files * Fixed import ordering
- Loading branch information
1 parent
5bb217c
commit bc5ad54
Showing
8 changed files
with
215 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
from functools import wraps | ||
|
||
# from flask import current_app | ||
from inspect import signature | ||
|
||
from app import signer_notification | ||
from app.encryption import SignedNotification, SignedNotifications | ||
|
||
|
||
def unsign_params(func): | ||
""" | ||
A decorator that verifies the SignedNotification|SignedNotifications typed | ||
arguments of the decorated function using `CryptoSigner().verify`. | ||
Args: | ||
func (callable): The function to be decorated. | ||
Returns: | ||
callable: The wrapped function with verification, un-signing decorated | ||
parameters typed with SignedNotification[s]. | ||
The decorated function should expect the first argument to be a signed string. | ||
The decorator will verify this signed string before calling the decorated function. | ||
""" | ||
|
||
@wraps(func) | ||
def wrapper(*args, **kwargs): | ||
sig = signature(func) | ||
|
||
# Find the parameter annotated with VerifyAndSign | ||
bound_args = sig.bind(*args, **kwargs) | ||
bound_args.apply_defaults() | ||
|
||
for param_name, param in sig.parameters.items(): | ||
if param.annotation in (SignedNotification, SignedNotifications): | ||
signed = bound_args.arguments[param_name] | ||
|
||
# Verify the signed string or list of signed strings | ||
if param.annotation is SignedNotification: | ||
verified_value = signer_notification.verify(signed) | ||
elif param.annotation is SignedNotifications: | ||
verified_value = [signer_notification.verify(item) for item in signed] | ||
|
||
# Replace the signed value with the verified value | ||
bound_args.arguments[param_name] = verified_value | ||
|
||
# Call the decorated function with the verified value | ||
result = func(*bound_args.args, **bound_args.kwargs) | ||
return result | ||
|
||
return wrapper | ||
|
||
|
||
def sign_return(func): | ||
""" | ||
A decorator that signs the result of the decorated function using CryptoSigner. | ||
Args: | ||
func (callable): The function to be decorated. | ||
Returns: | ||
callable: The wrapped function that returns a signed result. | ||
""" | ||
|
||
@wraps(func) | ||
def wrapper(*args, **kwargs): | ||
# Call the decorated function with the verified value | ||
result = func(*args, **kwargs) | ||
|
||
if isinstance(result, str): | ||
# Sign the str result of the decorated function | ||
signed_result = signer_notification.sign(result) | ||
elif isinstance(result, list): | ||
# Sign the list result of the decorated function | ||
signed_result = [signer_notification.sign(item) for item in result] | ||
else: | ||
signed_result = result | ||
|
||
return signed_result | ||
|
||
return wrapper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import pytest | ||
from itsdangerous.exc import BadSignature | ||
|
||
from app import signer_notification | ||
from app.annotations import sign_return, unsign_params | ||
from app.encryption import CryptoSigner, SignedNotification, SignedNotifications | ||
|
||
|
||
class TestUnsignParamsAnnotation: | ||
@pytest.fixture(scope="class", autouse=True) | ||
def setup_class(self, notify_api): | ||
# We just want to setup the notify_api flask app for tests within the class. | ||
pass | ||
|
||
def test_unsign_with_bad_signature_notification(self, notify_api): | ||
@unsign_params | ||
def annotated_unsigned_function( | ||
signed_notification: SignedNotification, | ||
) -> str: | ||
return signed_notification | ||
|
||
custom_signer = CryptoSigner() | ||
custom_signer.init_app(notify_api, "shhhhh", "salty") | ||
|
||
signed = custom_signer.sign("raw notification") | ||
with pytest.raises(BadSignature): | ||
annotated_unsigned_function(signed) | ||
|
||
def test_unsign_with_one_signed_notification(self): | ||
@unsign_params | ||
def func_with_one_signed_notification( | ||
signed_notification: SignedNotification, | ||
) -> str: | ||
return signed_notification | ||
|
||
signed = signer_notification.sign("raw notification") | ||
unsigned = func_with_one_signed_notification(signed) | ||
assert unsigned == "raw notification" | ||
|
||
def test_unsign_with_non_SignedNotification_parameter(self): | ||
def func_with_one_signed_notification(signed_notification: str): | ||
return signed_notification | ||
|
||
signed = "raw notification" | ||
unsigned = func_with_one_signed_notification(signed) | ||
assert unsigned == "raw notification" | ||
|
||
def test_unsign_with_list_of_signed_notifications(self): | ||
@unsign_params | ||
def func_with_list_of_signed_notifications( | ||
signed_notifications: SignedNotifications, | ||
): | ||
return signed_notifications | ||
|
||
signed = [signer_notification.sign(notification) for notification in ["raw notification 1", "raw notification 2"]] | ||
unsigned = func_with_list_of_signed_notifications(signed) | ||
assert unsigned == ["raw notification 1", "raw notification 2"] | ||
|
||
def test_unsign_with_empty_list_of_signed_notifications(self): | ||
@unsign_params | ||
def func_with_list_of_signed_notifications( | ||
signed_notifications: SignedNotifications, | ||
): | ||
return signed_notifications | ||
|
||
signed = [] | ||
unsigned = func_with_list_of_signed_notifications(signed) | ||
assert unsigned == [] | ||
|
||
def test_sign_return(self): | ||
@sign_return | ||
def func_to_sign_return(): | ||
return "raw notification" | ||
|
||
signed = func_to_sign_return() | ||
assert signer_notification.verify(signed) == "raw notification" | ||
|
||
def test_sign_return_with_list(self): | ||
@sign_return | ||
def func_to_sign_return(): | ||
return ["raw notification 1", "raw notification 2"] | ||
|
||
signed = func_to_sign_return() | ||
assert [signer_notification.verify(notification) for notification in signed] == [ | ||
"raw notification 1", | ||
"raw notification 2", | ||
] | ||
|
||
def test_sign_return_with_empty_list(self): | ||
@sign_return | ||
def func_to_sign_return(): | ||
return [] | ||
|
||
signed = func_to_sign_return() | ||
assert signed == [] | ||
|
||
def test_sign_return_with_non_string_return(self): | ||
@sign_return | ||
def func_to_sign_return(): | ||
return 1 | ||
|
||
signed = func_to_sign_return() | ||
assert signed == 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters