-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Delayed signing of notification from processing to the redis saving #2463
base: main
Are you sure you want to change the base?
Conversation
Refactored many types as well to reflect the notification's lifecycle, as well as removed unnecessary fields.
SignedNotifications = NewType("SignedNotifications", List[SignedNotification]) | ||
|
||
|
||
class NotificationDictToSign(TypedDict): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was moved into the types
module and became PendingNotification
to represent the early lifecycle stage of the notification processing (when it hits the API and prior to be saved into the DB).
from typing_extensions import NotRequired # type: ignore | ||
|
||
SignedNotification = NewType("SignedNotification", str) | ||
SignedNotifications = NewType("SignedNotifications", List[SignedNotification]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was moved into the types
module.
@wraps(func) | ||
def wrapper(*args, **kwargs): | ||
from app import signer_notification | ||
from app.queue import QueueMessage |
Check notice
Code scanning / CodeQL
Cyclic import Note
app.queue
@@ -22,6 +57,9 @@ | |||
|
|||
@wraps(func) | |||
def wrapper(*args, **kwargs): | |||
from app import signer_notification | |||
from app.types import SignedNotification, SignedNotifications |
Check notice
Code scanning / CodeQL
Cyclic import Note
app.types
@@ -8,6 +8,7 @@ | |||
from flask import current_app | |||
from redis import Redis | |||
|
|||
from app.annotations import sign_param |
Check notice
Code scanning / CodeQL
Cyclic import Note
app.annotations
from app.models import Job, NotificationType, Service | ||
from app.queue import QueueMessage |
Check notice
Code scanning / CodeQL
Cyclic import Note
app.queue
@@ -101,3 +134,18 @@ | |||
|
|||
signed = func_to_sign_return() | |||
assert signed == 1 | |||
|
|||
def test_unsign_and_return(self, mocker): | |||
from tests.app import test_annotations |
Check notice
Code scanning / CodeQL
Module imports itself Note test
@@ -42,22 +18,22 @@ def init_app(self, app: Any, secret_key: str | List[str], salt: str) -> None: | |||
self.serializer = URLSafeSerializer(secret_key) | |||
self.salt = salt | |||
|
|||
def sign(self, to_sign: str | NotificationDictToSign) -> str | bytes: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uncoupling the notification model from the CryptoSigner
. It will only have a str
to sign when that is called.
template_id=notification.get("template_id"), | ||
template_version=notification.get("template_version"), | ||
template_id=verifiedNotification.template_id, | ||
template_version=verifiedNotification.template_version, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now a data class so it has proper fields instead of dictionary entries.
# created_by_id: this is the user who created the notification and will be set on sending time, used by one off or admin UI uploads | ||
# reference=verifiedNotification.reference, # type: ignore | ||
# created_by_id=verifiedNotification.created_by_id, # type: ignore | ||
# billable_units=verifiedNotification.billable_units, # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove these:
reference
: a notification will only have a reference once SES or Pinpoint is called. The exception is letters where a random reference will be generated but this function does not handle these, not GCNotify at large.created_by_id
: Not set when received by the API AFAIK. Other code paths are used by the one-off send or bulk send, where thecreated_by_id
might be set.billable_units
: most likely not set at this stage yet.
# reference=verifiedNotification.reference, # type: ignore | ||
# created_by_id=verifiedNotification.created_by_id, # type: ignore | ||
# billable_units=verifiedNotification.billable_units, # type: ignore | ||
status=NOTIFICATION_CREATED, # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not receive a status from the notification, and should we? We can safely say the notification is created at this point, prior to save it into the database. I checked the other code paths for the persist_notification
function (the one off save -- this one is for multiple) and it sets the notification to created status by default.
# created_by_id=verifiedNotification.created_by_id, # type: ignore | ||
# billable_units=verifiedNotification.billable_units, # type: ignore | ||
status=NOTIFICATION_CREATED, # type: ignore | ||
reply_to_text=verifiedNotification.reply_to_text, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reply_to_text
field can be set via the API by providing the email_reply_to_id
JSON field in the POST request. So that should be present in the PendingNotification | VerifiedNotification
type and passed down the stream to the database.
case models.EMAIL_TYPE: | ||
notification_obj.normalised_to = format_email_address(notification_recipient) | ||
# case models.LETTER_TYPE: | ||
# notification_obj.postage = verifiedNotification.postage # or verifiedNotification.template_postage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah.. I thought this is so useless and annoying..
api_key: str | ||
key_type: str # should be ApiKeyType but can't import that here | ||
client_reference: Optional[str] | ||
reply_to_text: Optional[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reply_to_text
field can be set via the API by providing the email_reply_to_id
JSON field in the POST request. So that should be present in the PendingNotification | VerifiedNotification
type and passed down the stream to the database.
class VerifiedNotification(NotificationDictToSign): | ||
|
||
@dataclass | ||
class PendingNotification(QueueMessage): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously known as NotificationDictToSign
.
|
||
signed_notification_data = signer_notification.sign(_notification) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This layer does not signed the notification anymore: we want to delay the signing in the Redis queue because we need to add extra metadata in there, some coupled to queue management. Hence we removed the signing and Redis will do it itself, via the new signing annotations.
@@ -193,8 +219,9 @@ def acknowledge(self, receipt: UUID) -> bool: | |||
put_batch_saving_inflight_processed(self.__metrics_logger, self, 1) | |||
return True | |||
|
|||
def publish(self, message: str): | |||
self._redis_client.rpush(self._inbox, message) | |||
@sign_param |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will sign the parameter automatically, using the app.signer_notification
object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like these changes so far!
"job_id": _notification.get("job", None), | ||
"job_row_number": _notification.get("row_number", None), | ||
} | ||
notification = VerifiedNotification.from_dict( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! having a from_dict
method is a much better way to do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we rename notification -> verified_notification to make this consistent with the code below?
if redis_store.get(redis.daily_limit_cache_key(service_id)): | ||
redis_store.incr(redis.daily_limit_cache_key(service_id)) | ||
|
||
current_app.logger.info( | ||
"{} {} created at {}".format( | ||
notification.get("notification_type"), | ||
notification.get("notification_id"), | ||
notification.get("notification_created_at"), # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this just a bug? I don't think this key was actually there.
@classmethod | ||
def from_dict(cls, data: dict): | ||
"""Create an object from a dictionary.""" | ||
return cls(**data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the doc for dataclasses: https://docs.python.org/3/library/dataclasses.html
It looks like that decorater will add an __init__
method where you can pass in all the data you have defined above. So you could create an instance like this:
pending_notification = PendingNotification(id=x.id, template=x.template, etc )
instead of:
pending_notification = PendingNotification.from_dict({"id": x.id, "template": x.template, etc })
if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But we do often pass in a dict that looks like {**notification, "id": new_id}
so having a from_dict
method is probably best
"client_reference": form.get("reference", None), | ||
"reply_to_text": reply_to_text, | ||
} | ||
_notification = PendingNotification.from_dict( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could rename to
_notification = PendingNotification.from_dict( | |
pending_notification = PendingNotification.from_dict( |
from app import signer_notification | ||
from app.encryption import SignedNotification, SignedNotifications | ||
|
||
def sign_param(func): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a great idea but I can't tell if it works just by looking at the code. Could you add some instructions for how to test if this is working?
Summary | Résumé
Related Issues | Cartes liées
Test instructions | Instructions pour tester la modification
TODO: Fill in test instructions for the reviewer.
Release Instructions | Instructions pour le déploiement
None.
Reviewer checklist | Liste de vérification du réviseur