Skip to content

Commit 526003f

Browse files
committed
Added a brief REST API info page. In preparation for signing http notifications using RFC 7515, added information about API signing public/private keypair. Refactored api views to reside in api/views.py. Added jwcrypto to requirements.
- Legacy-Id: 14294
1 parent e899ed6 commit 526003f

6 files changed

Lines changed: 254 additions & 34 deletions

File tree

ietf/api/__init__.py

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
from urllib import urlencode
55

66
from django.conf import settings
7-
from django.http import HttpResponse
87
from django.core.exceptions import ObjectDoesNotExist
9-
from django.urls import reverse
108
from django.utils.encoding import force_text
119

1210
import debug # pyflakes:ignore
@@ -16,13 +14,9 @@
1614
from tastypie.api import Api
1715
from tastypie.bundle import Bundle
1816
from tastypie.serializers import Serializer as BaseSerializer
19-
from tastypie.exceptions import BadRequest, ApiFieldError
20-
from tastypie.utils.mime import determine_format, build_content_type
21-
from tastypie.utils import is_valid_jsonp_callback_value
17+
from tastypie.exceptions import ApiFieldError
2218
from tastypie.fields import ApiField
2319

24-
import debug # pyflakes:ignore
25-
2620
_api_list = []
2721

2822
class ModelResource(tastypie.resources.ModelResource):
@@ -99,32 +93,6 @@ def to_simple_html(self, data, options):
9993
_module_dict[_name] = _api
10094
_api_list.append((_name, _api))
10195

102-
def top_level(request):
103-
available_resources = {}
104-
105-
apitop = reverse('ietf.api.top_level')
106-
107-
for name in sorted([ name for name, api in _api_list if len(api._registry) > 0 ]):
108-
available_resources[name] = {
109-
'list_endpoint': '%s/%s/' % (apitop, name),
110-
}
111-
112-
serializer = Serializer()
113-
desired_format = determine_format(request, serializer)
114-
115-
options = {}
116-
117-
if 'text/javascript' in desired_format:
118-
callback = request.GET.get('callback', 'callback')
119-
120-
if not is_valid_jsonp_callback_value(callback):
121-
raise BadRequest('JSONP callback name is invalid.')
122-
123-
options['callback'] = callback
124-
125-
serialized = serializer.serialize(available_resources, desired_format, options)
126-
return HttpResponse(content=serialized, content_type=build_content_type(desired_format))
127-
12896
def autodiscover():
12997
"""
13098
Auto-discover INSTALLED_APPS resources.py modules and fail silently when

ietf/api/urls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.conf.urls import include
44

55
from ietf import api
6+
from ietf.api import views as api_views
67
from ietf.meeting import views as meeting_views
78
from ietf.submit import views as submit_views
89
from ietf.utils.urls import url
@@ -11,11 +12,13 @@
1112

1213
urlpatterns = [
1314
# Top endpoint for Tastypie's REST API (this isn't standard Tastypie):
14-
url(r'^v1/?$', api.top_level),
15+
url(r'^$', api_views.api_help),
16+
url(r'^v1/?$', api_views.top_level),
1517
# Custom API endpoints
1618
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
1719
url(r'^submit/?$', submit_views.api_submit),
1820
]
21+
1922
# Additional (standard) Tastypie endpoints
2023
for n,a in api._api_list:
2124
urlpatterns += [

ietf/api/views.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright The IETF Trust 2017, All Rights Reserved
2+
# -*- coding: utf-8 -*-
3+
4+
from __future__ import unicode_literals
5+
6+
from jwcrypto.jwk import JWK
7+
8+
from django.conf import settings
9+
from django.http import HttpResponse
10+
from django.shortcuts import render
11+
from django.urls import reverse
12+
13+
from tastypie.exceptions import BadRequest
14+
from tastypie.utils.mime import determine_format, build_content_type
15+
from tastypie.utils import is_valid_jsonp_callback_value
16+
17+
import debug # pyflakes:ignore
18+
19+
from ietf.api import Serializer, _api_list
20+
21+
def top_level(request):
22+
available_resources = {}
23+
24+
apitop = reverse('ietf.api.views.top_level')
25+
26+
for name in sorted([ name for name, api in _api_list if len(api._registry) > 0 ]):
27+
available_resources[name] = {
28+
'list_endpoint': '%s/%s/' % (apitop, name),
29+
}
30+
31+
serializer = Serializer()
32+
desired_format = determine_format(request, serializer)
33+
34+
options = {}
35+
36+
if 'text/javascript' in desired_format:
37+
callback = request.GET.get('callback', 'callback')
38+
39+
if not is_valid_jsonp_callback_value(callback):
40+
raise BadRequest('JSONP callback name is invalid.')
41+
42+
options['callback'] = callback
43+
44+
serialized = serializer.serialize(available_resources, desired_format, options)
45+
return HttpResponse(content=serialized, content_type=build_content_type(desired_format))
46+
47+
def api_help(request):
48+
key = JWK()
49+
# import just public part here, for display in info page
50+
key.import_from_pem(settings.API_PUBLIC_KEY_PEM)
51+
return render(request, "api/index.html", {'key': key, })
52+

ietf/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,9 @@ def skip_unreadable_post(record):
933933

934934
UTILS_TEST_RANDOM_STATE_FILE = '.factoryboy_random_state'
935935

936+
API_PUBLIC_KEY_PEM = "Set this in settings_local.py"
937+
API_PRIVATE_KEY_PEM = "Set this in settings_local.py"
938+
936939

937940
# Put the production SECRET_KEY in settings_local.py, and also any other
938941
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn.

ietf/templates/api/index.html

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
{# Copyright The IETF Trust 2007, All Rights Reserved #}
2+
{% extends "base.html" %}
3+
{% load staticfiles %}
4+
{% block title %}API Notes{% endblock %}
5+
{% block content %}
6+
7+
<h2>Datatracker API Notes</h2>
8+
<div class="col-md-1"></div>
9+
<div class="col-md-11 bio-text">
10+
<h3>Framework</h3>
11+
<p>
12+
The datatracker API uses <a href="https://django-tastypie.readthedocs.org/">tastypie</a>
13+
to generate an API which mirrors the Django ORM (Object Relational Mapping)
14+
for the database. Each Django model class maps down to the SQL database
15+
tables and up to the API. The Django models classes are defined in the
16+
models.py files of the datatracker:
17+
</p>
18+
19+
<p>
20+
<a href="http://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/doc/models.py">https://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/doc/models.py</a>
21+
<br>
22+
<a href="http://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/group/models.py">https://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/group/models.py</a>
23+
<br>
24+
<a href="http://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/iesg/models.py">http://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/iesg/models.py</a>
25+
<br>
26+
&hellip;
27+
28+
</p>
29+
<p>
30+
31+
The API top endpoint is at <a href="https://datatracker.ietf.org/api/v1/">https://datatracker.ietf.org/api/v1/</a>. The top
32+
endpoint lists inferior endpoints, and thus permits some autodiscovery,
33+
but there's really no substitute for looking at the actual ORM model classes.
34+
Comparing a class in models.py with the equivalent endpoint may give
35+
some clue (note that in the case of Group, it's a subclass of GroupInfo):
36+
37+
</p>
38+
<p>
39+
40+
<a href="https://datatracker.ietf.org/api/v1/group/group/">https://datatracker.ietf.org/api/v1/group/group/</a>
41+
<br>
42+
<a href="https://trac.tools.ietf.org/tools/ietfdb/browser/trunk/ietf/group/models.py">https://trac.tools.ietf.org/tools/ietfdb/browser/trunk/ietf/group/models.py</a>
43+
44+
</p>
45+
<p>
46+
47+
Data is currently provided in JSON and XML format. Adding new formats is
48+
fairly easy, if it should be found desriable.
49+
50+
</p>
51+
52+
<h3>Documents</h3>
53+
54+
<p>
55+
56+
Documents are listed at
57+
<a href="https://datatracker.ietf.org/api/v1/doc/document/">/api/v1/doc/document/</a>.
58+
59+
</p>
60+
<p>
61+
62+
In general, individual database objects are represented in the api with a path
63+
composed of the model collection, the object name, and the object key. Most
64+
objects have simple numerical keys, but documents have the document name as
65+
key. Take draft-ietf-eppext-keyrelay. Documents have a model 'Document' which
66+
is described in the 'doc' models.py file. Assembling the path components
67+
'doc', 'document' (lowercase!) and 'draft-ietf-eppext-keyrelay', we get the
68+
URL:
69+
70+
</p>
71+
<p>
72+
73+
74+
<a href="https://datatracker.ietf.org/api/v1/doc/document/draft-ietf-eppext-keyrelay/">
75+
https://datatracker.ietf.org/api/v1/doc/document/draft-ietf-eppext-keyrelay/
76+
</a>
77+
78+
</p>
79+
<p>
80+
81+
If you instead do a search for this document, you will get a machine-readable
82+
search result, which is composed of some meta-information about the search,
83+
and a list with one element:
84+
85+
</p>
86+
<p>
87+
88+
<a href="https://datatracker.ietf.org/api/v1/doc/document/?name=draft-ietf-eppext-keyrelay">
89+
api/v1/doc/document/?name=draft-ietf-eppext-keyrelay
90+
</a>
91+
92+
</p>
93+
<p>
94+
95+
To search for documents based on state, you need to know that documents have
96+
multiple orthogonal states:
97+
98+
</p>
99+
<ul>
100+
<li>
101+
If a document has an rfc-editor state, you can select for it by asking for e.g.
102+
<a href="https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-&states__type__slug__in=draft-rfceditor">
103+
v6ops documents which match 'states__type__slug__in=draft-rfceditor'
104+
</a>
105+
</li>
106+
107+
<li>
108+
If a document has an IESG state, you can select for it by asking for e.g.
109+
<a href="https://datatracker.ietf.org/api/v1/doc/document/?name__contains=-v6ops&states__type__slug__in=draft-iesg">
110+
v6ops documents which match 'states__type__slug__in=draft-iesg'
111+
</a>
112+
</li>
113+
114+
<li>
115+
If a document has a WG state, you can select for it by asking for
116+
documents which match 'states__type__slug__in=draft-stream-ietf'
117+
(but without additional filters, that's going to be a lot of documents)
118+
</li>
119+
120+
<li>
121+
States which match 'states__type__slug__in=draft' describe the basic
122+
Active/Expired/Dead whatever state of the draft.
123+
</li>
124+
</ul>
125+
126+
<p>
127+
You could use this in at least two alternative ways:
128+
</p>
129+
130+
<p>
131+
You could either fetch and remember the different state groups of interest to you
132+
with queries like
133+
134+
<pre>
135+
$ curl 'https://datatracker.ietf.org/api/v1/doc/state/?format=json&limit=0&type__slug__in=draft-rfceditor'
136+
$ curl 'https://datatracker.ietf.org/api/v1/doc/state/?format=json&limit=0&type__slug__in=draft-iesg'
137+
$ curl 'https://datatracker.ietf.org/api/v1/doc/state/?format=json&limit=0&type__slug__in=draft-stream-ietf'
138+
</pre>
139+
</p>
140+
141+
<p>
142+
and then match the listed "resource_uri" of the results to the states listed for each
143+
document when you ask for
144+
<pre>
145+
$ curl 'https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-'
146+
</pre>
147+
</p>
148+
149+
<p>
150+
Or alternatively you could do a series of queries asking for matches to the RFC Editor
151+
state first, then the IESG state, then the Stream state, and exclude earlier hits:
152+
<pre>
153+
$ curl 'https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-&states__type__slug__in=draft-rfceditor' ...
154+
$ curl 'https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-&states__type__slug__in=draft-iesg' ...
155+
</pre>
156+
</p>
157+
158+
<p>
159+
etc.
160+
</p>
161+
162+
<h3>API Keys</h3>
163+
164+
<p>
165+
166+
The datatracker does not use any form of API keys currently (03 Nov
167+
2017), but may do so in the future. If so, personal API keys will be
168+
available from your <a href={% url 'ietf.ietfauth.views.profile'
169+
%}>Account Profile</a> page when you are logged in, and document keys
170+
will be visible to document authors on the document status page when
171+
logged in.
172+
173+
</p>
174+
175+
<p>
176+
177+
When sending notifications to other APIs, the datatracker may sign
178+
information with a <a href="https://tools.ietf.org/html/rfc7515">RFC
179+
7515: JSON Web Signature (JWS)</a>, using a public/private keypair with
180+
this public key:
181+
182+
</p>
183+
<p><code>{{key.export_public}}</code></p>
184+
185+
<p>or alternatively:</p>
186+
187+
<pre>{{key.export_to_pem}}</pre>
188+
189+
</div>
190+
191+
192+
{% endblock %}
193+

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ hashids>=1.1.0
2626
html5lib>=0.90,<0.99999999 # ietf.utils.html needs a rewrite for html5lib 1.x -- major code changes in sanitizer
2727
httplib2>=0.10.3
2828
jsonfield>=1.0.3 # for SubmissionCheck. This is https://github.com/bradjasper/django-jsonfield/.
29+
jwcrypto>=0.4.0 # for signed notifications
2930
#lxml>=3.4.0 # from PyQuery;
3031
mimeparse>=0.1.3 # from TastyPie
3132
mock>=2.0.0

0 commit comments

Comments
 (0)