fix: add recording-name api key endpoint; appauth url fix (#8081)

* fix: add endpoint option for recording-name

* chore: migration

* test: validate PersonalApiKey when used in tests

* fix: limit /api/appauth URLs as intended

* test: fix tests

* chore: fix lint

* test: PersonalApiKey create -> factory

* chore: remove unused import
This commit is contained in:
Jennifer Richards 2024-10-25 10:52:09 -03:00 committed by GitHub
parent 0b4b26f9c3
commit 3130ecd9f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 118 additions and 59 deletions

View file

@ -33,9 +33,8 @@ from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.meeting.models import Session
from ietf.nomcom.models import Volunteer
from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year
from ietf.person.factories import PersonFactory, random_faker, EmailFactory
from ietf.person.factories import PersonFactory, random_faker, EmailFactory, PersonalApiKeyFactory
from ietf.person.models import Email, User
from ietf.person.models import PersonalApiKey
from ietf.stats.models import MeetingRegistration
from ietf.utils.mail import empty_outbox, outbox, get_payload_text
from ietf.utils.models import DumpInfo
@ -71,7 +70,7 @@ class CustomApiTests(TestCase):
meeting = MeetingFactory(type_id='ietf')
session = SessionFactory(group__type_id='wg', meeting=meeting)
group = session.group
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
apikey = PersonalApiKeyFactory(endpoint=url, person=recman)
video = 'https://foo.example.com/bar/beer/'
# error cases
@ -79,7 +78,7 @@ class CustomApiTests(TestCase):
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {'apikey': badapikey.hash()} )
@ -151,7 +150,7 @@ class CustomApiTests(TestCase):
recman = recmanrole.person
meeting = MeetingFactory(type_id="ietf")
session = SessionFactory(group__type_id="wg", meeting=meeting)
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
apikey = PersonalApiKeyFactory(endpoint=url, person=recman)
video = "https://foo.example.com/bar/beer/"
# error cases
@ -159,7 +158,7 @@ class CustomApiTests(TestCase):
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id="ietf", name_id="ad")
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {"apikey": badapikey.hash()})
@ -228,7 +227,7 @@ class CustomApiTests(TestCase):
recman = recmanrole.person
meeting = MeetingFactory(type_id="ietf")
session = SessionFactory(group__type_id="wg", meeting=meeting)
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
apikey = PersonalApiKeyFactory(endpoint=url, person=recman)
name = "testname"
# error cases
@ -236,7 +235,7 @@ class CustomApiTests(TestCase):
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id="ietf", name_id="ad")
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {"apikey": badapikey.hash()})
@ -295,10 +294,10 @@ class CustomApiTests(TestCase):
recman = recmanrole.person
meeting = MeetingFactory(type_id='ietf')
session = SessionFactory(group__type_id='wg', meeting=meeting)
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
apikey = PersonalApiKeyFactory(endpoint=url, person=recman)
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
@ -361,10 +360,10 @@ class CustomApiTests(TestCase):
recman = recmanrole.person
meeting = MeetingFactory(type_id="ietf")
session = SessionFactory(group__type_id="wg", meeting=meeting)
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
apikey = PersonalApiKeyFactory(endpoint=url, person=recman)
badrole = RoleFactory(group__type_id="ietf", name_id="ad")
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
@ -517,8 +516,8 @@ class CustomApiTests(TestCase):
),
):
url = urlreverse(f"ietf.meeting.views.api_upload_{type_id}")
apikey = PersonalApiKey.objects.create(endpoint=url, person=recmanrole.person)
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
apikey = PersonalApiKeyFactory(endpoint=url, person=recmanrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
r = self.client.post(url, {})
self.assertContains(r, "Missing apikey parameter", status_code=400)
@ -562,7 +561,7 @@ class CustomApiTests(TestCase):
meeting = MeetingFactory(type_id='ietf')
session = SessionFactory(group__type_id='wg', meeting=meeting)
group = session.group
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
apikey = PersonalApiKeyFactory(endpoint=url, person=recman)
people = [
{"name": "Andrea Andreotti", "affiliation": "Azienda"},
@ -579,7 +578,7 @@ class CustomApiTests(TestCase):
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {'apikey': badapikey.hash()})
@ -654,7 +653,7 @@ class CustomApiTests(TestCase):
meeting = MeetingFactory(type_id="ietf")
session = SessionFactory(group__type_id="wg", meeting=meeting)
group = session.group
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
apikey = PersonalApiKeyFactory(endpoint=url, person=recman)
people = [
{"name": "Andrea Andreotti", "affiliation": "Azienda"},
@ -671,7 +670,7 @@ class CustomApiTests(TestCase):
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id="ietf", name_id="ad")
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {"apikey": badapikey.hash()})
@ -781,14 +780,14 @@ class CustomApiTests(TestCase):
url = urlreverse('ietf.api.views.ApiV2PersonExportView')
robot = PersonFactory(user__is_staff=True)
RoleFactory(name_id='robot', person=robot, email=robot.email(), group__acronym='secretariat')
apikey = PersonalApiKey.objects.create(endpoint=url, person=robot)
apikey = PersonalApiKeyFactory(endpoint=url, person=robot)
# error cases
r = self.client.post(url, {})
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {'apikey': badapikey.hash()})
@ -827,7 +826,7 @@ class CustomApiTests(TestCase):
oidcp = PersonFactory(user__is_staff=True)
# Make sure 'oidcp' has an acceptable role
RoleFactory(name_id='robot', person=oidcp, email=oidcp.email(), group__acronym='secretariat')
key = PersonalApiKey.objects.create(person=oidcp, endpoint=url)
key = PersonalApiKeyFactory(person=oidcp, endpoint=url)
reg['apikey'] = key.hash()
#
# Test valid POST
@ -911,7 +910,7 @@ class CustomApiTests(TestCase):
oidcp = PersonFactory(user__is_staff=True)
# Make sure 'oidcp' has an acceptable role
RoleFactory(name_id='robot', person=oidcp, email=oidcp.email(), group__acronym='secretariat')
key = PersonalApiKey.objects.create(person=oidcp, endpoint=url)
key = PersonalApiKeyFactory(person=oidcp, endpoint=url)
reg['apikey'] = key.hash()
# first test is_nomcom_volunteer False
@ -945,28 +944,30 @@ class CustomApiTests(TestCase):
def test_api_appauth(self):
url = urlreverse('ietf.api.views.app_auth')
person = PersonFactory()
apikey = PersonalApiKey.objects.create(endpoint=url, person=person)
self.client.login(username=person.user.username,password=f'{person.user.username}+password')
self.client.logout()
# error cases
# missing apikey
r = self.client.post(url, {})
self.assertContains(r, 'Missing apikey parameter', status_code=400)
# invalid apikey
r = self.client.post(url, {'apikey': 'foobar'})
self.assertContains(r, 'Invalid apikey', status_code=403)
# working case
r = self.client.post(url, {'apikey': apikey.hash()})
self.assertEqual(r.status_code, 200)
jsondata = r.json()
self.assertEqual(jsondata['success'], True)
for app in ["authortools", "bibxml"]:
url = urlreverse('ietf.api.views.app_auth', kwargs={"app": app})
person = PersonFactory()
apikey = PersonalApiKeyFactory(endpoint=url, person=person)
self.client.login(username=person.user.username,password=f'{person.user.username}+password')
self.client.logout()
# error cases
# missing apikey
r = self.client.post(url, {})
self.assertContains(r, 'Missing apikey parameter', status_code=400)
# invalid apikey
r = self.client.post(url, {'apikey': 'foobar'})
self.assertContains(r, 'Invalid apikey', status_code=403)
# working case
r = self.client.post(url, {'apikey': apikey.hash()})
self.assertEqual(r.status_code, 200)
jsondata = r.json()
self.assertEqual(jsondata['success'], True)
self.client.logout()
def test_api_get_session_matherials_no_agenda_meeting_url(self):
meeting = MeetingFactory(type_id='ietf')
session = SessionFactory(meeting=meeting)

View file

@ -69,7 +69,7 @@ urlpatterns = [
# Datatracker version
url(r'^version/?$', api_views.version),
# Application authentication API key
url(r'^appauth/[authortools|bibxml]', api_views.app_auth),
url(r'^appauth/(?P<app>authortools|bibxml)$', api_views.app_auth),
# latest versions
url(r'^rfcdiff-latest-json/%(name)s(?:-%(rev)s)?(\.txt|\.html)?/?$' % settings.URL_REGEXPS, api_views.rfcdiff_latest_json),
url(r'^rfcdiff-latest-json/(?P<name>[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', api_views.rfcdiff_latest_json),

View file

@ -30,7 +30,7 @@ from tastypie.utils import is_valid_jsonp_callback_value
from tastypie.utils.mime import determine_format, build_content_type
from textwrap import dedent
from traceback import format_exception, extract_tb
from typing import Iterable, Optional
from typing import Iterable, Optional, Literal
import ietf
from ietf.api import _api_list
@ -251,7 +251,7 @@ def version(request):
@require_api_key
@csrf_exempt
def app_auth(request):
def app_auth(request, app: Literal["authortools", "bibxml"]):
return HttpResponse(
json.dumps({'success': True}),
content_type='application/json')

View file

@ -27,8 +27,8 @@ from ietf.group.factories import GroupFactory, RoleFactory, ReviewTeamFactory
from ietf.ipr.factories import HolderIprDisclosureFactory
from ietf.name.models import BallotPositionName
from ietf.iesg.models import TelechatDate
from ietf.person.models import Person, PersonalApiKey
from ietf.person.factories import PersonFactory
from ietf.person.models import Person
from ietf.person.factories import PersonFactory, PersonalApiKeyFactory
from ietf.person.utils import get_active_ads
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
@ -111,7 +111,7 @@ class EditPositionTests(TestCase):
create_ballot_if_not_open(None, draft, ad, 'approve')
ad.user.last_login = timezone.now()
ad.user.save()
apikey = PersonalApiKey.objects.create(endpoint=url, person=ad)
apikey = PersonalApiKeyFactory(endpoint=url, person=ad)
# vote
events_before = draft.docevent_set.count()

View file

@ -35,7 +35,7 @@ from ietf.ietfauth.utils import has_role
from ietf.meeting.factories import MeetingFactory
from ietf.nomcom.factories import NomComFactory
from ietf.person.factories import PersonFactory, EmailFactory, UserFactory, PersonalApiKeyFactory
from ietf.person.models import Person, Email, PersonalApiKey
from ietf.person.models import Person, Email
from ietf.person.tasks import send_apikey_usage_emails_task
from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory
from ietf.review.models import ReviewWish, UnavailablePeriod
@ -788,9 +788,8 @@ class IetfAuthTests(TestCase):
self.assertContains(r, 'Invalid apikey', status_code=403)
# invalid apikey (invalidated api key)
unauthorized_url = urlreverse('ietf.api.views.app_auth')
invalidated_apikey = PersonalApiKey.objects.create(
endpoint=unauthorized_url, person=person, valid=False)
unauthorized_url = urlreverse('ietf.api.views.app_auth', kwargs={'app': 'authortools'})
invalidated_apikey = PersonalApiKeyFactory(endpoint=unauthorized_url, person=person, valid=False)
r = self.client.post(unauthorized_url, {'apikey': invalidated_apikey.hash()})
self.assertContains(r, 'Invalid apikey', status_code=403)
@ -803,7 +802,11 @@ class IetfAuthTests(TestCase):
person.user.save()
# endpoint mismatch
key2 = PersonalApiKey.objects.create(person=person, endpoint='/')
key2 = PersonalApiKeyFactory(
person=person,
endpoint='/',
validate_model=False, # allow invalid endpoint
)
r = self.client.post(key.endpoint, {'apikey':key2.hash(), 'dummy':'dummy',})
self.assertContains(r, 'Apikey endpoint mismatch', status_code=400)
key2.delete()

View file

@ -38,7 +38,7 @@ import debug # pyflakes:ignore
from ietf.doc.models import Document, NewRevisionDocEvent
from ietf.group.models import Group, Role, GroupFeatures
from ietf.group.utils import can_manage_group
from ietf.person.models import Person, PersonalApiKey
from ietf.person.models import Person
from ietf.meeting.helpers import can_approve_interim_request, can_request_interim_meeting, can_view_interim_request, preprocess_assignments_for_agenda
from ietf.meeting.helpers import send_interim_approval_request, AgendaKeywordTagger
from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_interim_session_cancellation_notice
@ -56,7 +56,7 @@ from ietf.utils.mail import outbox, empty_outbox, get_payload_text
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
from ietf.utils.timezone import date_today, time_now
from ietf.person.factories import PersonFactory
from ietf.person.factories import PersonFactory, PersonalApiKeyFactory
from ietf.group.factories import GroupFactory, GroupEventFactory, RoleFactory
from ietf.meeting.factories import (SessionFactory, ScheduleFactory,
SessionPresentationFactory, MeetingFactory, FloorPlanFactory,
@ -8743,7 +8743,7 @@ class ProceedingsTests(BaseMeetingTestCase):
add_attendees_url = urlreverse('ietf.meeting.views.api_add_session_attendees')
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman', person__user__last_login=timezone.now())
recman = recmanrole.person
apikey = PersonalApiKey.objects.create(endpoint=add_attendees_url, person=recman)
apikey = PersonalApiKeyFactory(endpoint=add_attendees_url, person=recman)
attendees = [person.user.pk for person in persons]
self.client.login(username='recman', password='recman+password')
r = self.client.post(add_attendees_url, {'apikey':apikey.hash(), 'attended':f'{{"session_id":{session.pk},"attendees":{attendees}}}'})

View file

@ -158,10 +158,22 @@ class EmailFactory(factory.django.DjangoModelFactory):
class PersonalApiKeyFactory(factory.django.DjangoModelFactory):
person = factory.SubFactory(PersonFactory)
endpoint = FuzzyChoice(PERSON_API_KEY_ENDPOINTS)
endpoint = FuzzyChoice(v for v, n in PERSON_API_KEY_ENDPOINTS)
class Meta:
model = PersonalApiKey
skip_postgeneration_save = True
@factory.post_generation
def validate_model(obj, create, extracted, **kwargs):
"""Validate the model after creation
Passing validate_model=False will disable the validation.
"""
do_clean = True if extracted is None else extracted
if do_clean:
obj.full_clean()
class PersonApiKeyEventFactory(factory.django.DjangoModelFactory):
key = factory.SubFactory(PersonalApiKeyFactory)

View file

@ -0,0 +1,42 @@
# Generated by Django 4.2.16 on 2024-10-24 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("person", "0002_alter_historicalperson_ascii_and_more"),
]
operations = [
migrations.AlterField(
model_name="personalapikey",
name="endpoint",
field=models.CharField(
choices=[
("/api/appauth/authortools", "/api/appauth/authortools"),
("/api/appauth/bibxml", "/api/appauth/bibxml"),
("/api/iesg/position", "/api/iesg/position"),
(
"/api/meeting/session/recording-name",
"/api/meeting/session/recording-name",
),
(
"/api/meeting/session/video/url",
"/api/meeting/session/video/url",
),
("/api/notify/meeting/bluesheet", "/api/notify/meeting/bluesheet"),
(
"/api/notify/meeting/registration",
"/api/notify/meeting/registration",
),
("/api/notify/session/attendees", "/api/notify/session/attendees"),
("/api/notify/session/chatlog", "/api/notify/session/chatlog"),
("/api/notify/session/polls", "/api/notify/session/polls"),
("/api/v2/person/person", "/api/v2/person/person"),
],
max_length=128,
),
),
]

View file

@ -376,6 +376,7 @@ PERSON_API_KEY_VALUES = [
("/api/iesg/position", "/api/iesg/position", "Area Director"),
("/api/v2/person/person", "/api/v2/person/person", "Robot"),
("/api/meeting/session/video/url", "/api/meeting/session/video/url", "Recording Manager"),
("/api/meeting/session/recording-name", "/api/meeting/session/recording-name", "Recording Manager"),
("/api/notify/meeting/registration", "/api/notify/meeting/registration", "Robot"),
("/api/notify/meeting/bluesheet", "/api/notify/meeting/bluesheet", "Recording Manager"),
("/api/notify/session/attendees", "/api/notify/session/attendees", "Recording Manager"),