Skip to content
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

fix: refactor api_new_meeting_registration. Fixes #7608 #7724

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

rpcross
Copy link
Collaborator

@rpcross rpcross commented Jul 21, 2024

  • change to use JSON payload
  • handle multiple records in one request

@rpcross
Copy link
Collaborator Author

rpcross commented Jul 25, 2024

I am working on tests for this. Using a JSON payload means the existing require_api_key decorator won't work. Assuming JSON payload is the way to go, are there other precedents for checking API key? Function can do it's own validation, or can update decorator.

@rpcross rpcross requested a review from rjsparks July 25, 2024 19:00
@jennifer-richards
Copy link
Member

are there other precedents for checking API key?

def requires_api_token(func_or_endpoint: Optional[Union[Callable, str]] = None):
"""Validate API token before executing the wrapped method
Usage:
* Basic: endpoint defaults to the qualified name of the wrapped method. E.g., in ietf.api.views,
@requires_api_token
def my_view(request):
...
will require a token for "ietf.api.views.my_view"
* Custom endpoint: specify the endpoint explicitly
@requires_api_token("ietf.api.views.some_other_thing")
def my_view(request):
...
will require a token for "ietf.api.views.some_other_thing"
"""
is what we've started using for datatracker. This uses the X-Api-Key header to store a shared secret and keeps the auth concerns out of the API itself.

@rjsparks
Copy link
Member

rjsparks commented Sep 4, 2024

@rpcross This may have fallen into the cracks because of 120?

@rjsparks
Copy link
Member

@rpcross - lets discuss this soon?

Copy link

codecov bot commented Nov 25, 2024

Codecov Report

Attention: Patch coverage is 68.11594% with 22 lines in your changes missing coverage. Please review.

Project coverage is 88.69%. Comparing base (7ecf23e) to head (4ae7306).
Report is 19 commits behind head on main.

Files with missing lines Patch % Lines
ietf/api/views.py 68.11% 22 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7724      +/-   ##
==========================================
- Coverage   88.95%   88.69%   -0.27%     
==========================================
  Files         303      310       +7     
  Lines       41273    40951     -322     
==========================================
- Hits        36713    36320     -393     
- Misses       4560     4631      +71     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@rpcross rpcross marked this pull request as ready for review December 5, 2024 19:05
Copy link
Member

@jennifer-richards jennifer-richards left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few comments inline, but I have reservations about the strategy used for removing/updating/replacing MeetingRegistration records. As it's done, I'm pretty sure there are race conditions that can lead to corrupt registrations. E.g., if two identical requests to register a user are at nearly the same time, two identical registrations may be created. This will happen if the later-running request handler executes line 334 (regs = MeetingRegistration.objects.filter(meeting__number=number, email=email)) before the earlier-running handler executes line 357 (new_reg.save()).

At minimum, we ought to put a uniqueness constraint on MeetingRegistration to guard against this. Not sure, but I think it'd need to be over email, meeting, and reg_type.

I think that constraint should be enough to avoid data corruption, but it would result in some API errors that were unnecessary. I.e., the two simultaneous identical requests would both "succeed" in that their work would be done as requested, but the later one would get back an error. It'd be very cool to rework the API using get_or_create (plus probably some additional subtlety handling) to avoid this, but I'm not sure it's necessary.

ietf/api/tests.py Outdated Show resolved Hide resolved
ietf/api/tests.py Outdated Show resolved Hide resolved
ietf/api/views.py Outdated Show resolved Hide resolved
# get person
person = Person.objects.filter(email__address=email).first()
if not person:
log.log(f"api_new_meeting_registration_v2 no Person found for {email}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want the registration system to know that the registration it just sent doesn't match a person? Think about whether telling the person that just registered "Oh no - something went wrong with your registration" might move some of the person-fixup work we do at meetings to the period before the meeting.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this in for completeness, but do we expect this to happen? Since Registration now requires the participant to be logged in via the Datatracker, the existence of Person should be largely assured. My feeling is that all auth issues at the last few meetings have been either 1) someone is trying to access meetecho without having registered, 2) duplicate person issues.

Comment on lines +333 to +356
# get existing records if any
regs = MeetingRegistration.objects.filter(meeting__number=number, email=email)
pks = [r.pk for r in regs]

for registration in payload:
new_reg = MeetingRegistration(
meeting=meeting,
first_name=registration['first_name'],
last_name=registration['last_name'],
affiliation=registration['affiliation'],
country_code=registration['country_code'],
person=person,
email=email,
reg_type=registration['reg_type'],
ticket_type=registration['ticket_type'],
checkedin=registration['checkedin'])

# update any existing records if there are any
new_reg.pk = _safe_pop(pks)
if new_reg.pk:
log.log(f"Updating MeetingRegistration record for meeting:{meeting} email:{email}")
else:
log.log(f"New MeetingRegistration record for meeting:{meeting} email:{email}")
new_reg.save()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me, and looks error-prone - it could effectively swap pks on records depending on the order L334 returns objects. Why isn't this using get_or_create with defaults and using the created flag instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to prefer the current model where a single person's registration is represented as several MeetingRegistration instances? The gymnastics in checking that the payload pertains only to a single email seems to be begging for a model that represents that as a single registration per email with fields representing the different options.

I think that'll greatly simplify the API implementation. It'll mean a migration, but I don't think that'll be terribly difficult.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, as far as I can tell there are currently no models with foreign keys into MeetingRegistration so it's not an going to break things now if PKs get swapped, but it'll do something in between cramping our style and causing serious bugs later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me, and looks error-prone - it could effectively swap pks on records depending on the order L334 returns objects. Why isn't this using get_or_create with defaults and using the created flag instead?

It certainly could swap pks. It is a brute force approach I chose for it's definitiveness. We can’t use get_or_create because there may be more than one existing registration record even for the same reg_type (two onsite one-day regs for example) AND the reg_type can change, switch from onsite to remote, so we’d be updating a record with a different reg_type. The simplest approach is to delete and re-create, or fully overwrite as I have done. Other approaches require more complexity. The following might work. Assuming that any notification will include all existing registrations for a given participant, and that a single registration save triggers the notification than it should be true that only one registration is added or was changed. If payload == 1 we can use get_or_create. (Except if payload == 1 and 2 already exist it means we missed a cancel somewhere). If payload > 1 we know that only one is new or updated and the rest should match existing records. We can do the work to determine which record doesn't match existing. This is a more complex procedure but would result in preserving the record PKs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to prefer the current model where a single person's registration is represented as several MeetingRegistration instances? The gymnastics in checking that the payload pertains only to a single email seems to be begging for a model that represents that as a single registration per email with fields representing the different options.

I think that'll greatly simplify the API implementation. It'll mean a migration, but I don't think that'll be terribly difficult.

We actually started with one MeetingRegistration record per person and had a reg_type field that was text and contained comma separated types, "onsite, hackathon_onsite", which proved problematic to deal with so we change the model to match registration, which could be multiple registration records per person. The current model seems appropriate.

              <- meeting 120 onsite week pass

person A <- meeting 121 onsite one day pass
<- meeting 121 remote week pass

Or maybe breakout another table?

person A <- meeting 121 <- onsite one-day
<- remote week pass

Comment on lines +376 to +378
# delete any remaining records
if pks:
MeetingRegistration.objects.filter(pk__in=pks).delete()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If L333-L356 change, this will have to be adjusted to match

# handle nomcom volunteer
if registration['is_nomcom_volunteer'] and person:
try:
nomcom = NomCom.objects.get(is_accepting_volunteers=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the registration system check to see if a nomcom is accepting volunteers before offering the choice to someone?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not. It's another level of integration that could be added. My understanding was that AMS would coordinate with the nomcom chair. When prepping to open registration if nomcom is accepting volunteers I would ensure the volunteer question appears on the reg form.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants