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
This commit is contained in:
Henrik Levkowetz 2017-11-03 16:17:00 +00:00
parent e899ed6e4d
commit 526003fd26
6 changed files with 254 additions and 34 deletions

View file

@ -4,9 +4,7 @@ import datetime
from urllib import urlencode from urllib import urlencode
from django.conf import settings from django.conf import settings
from django.http import HttpResponse
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from django.utils.encoding import force_text from django.utils.encoding import force_text
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -16,13 +14,9 @@ import tastypie.resources
from tastypie.api import Api from tastypie.api import Api
from tastypie.bundle import Bundle from tastypie.bundle import Bundle
from tastypie.serializers import Serializer as BaseSerializer from tastypie.serializers import Serializer as BaseSerializer
from tastypie.exceptions import BadRequest, ApiFieldError from tastypie.exceptions import ApiFieldError
from tastypie.utils.mime import determine_format, build_content_type
from tastypie.utils import is_valid_jsonp_callback_value
from tastypie.fields import ApiField from tastypie.fields import ApiField
import debug # pyflakes:ignore
_api_list = [] _api_list = []
class ModelResource(tastypie.resources.ModelResource): class ModelResource(tastypie.resources.ModelResource):
@ -99,32 +93,6 @@ for _app in settings.INSTALLED_APPS:
_module_dict[_name] = _api _module_dict[_name] = _api
_api_list.append((_name, _api)) _api_list.append((_name, _api))
def top_level(request):
available_resources = {}
apitop = reverse('ietf.api.top_level')
for name in sorted([ name for name, api in _api_list if len(api._registry) > 0 ]):
available_resources[name] = {
'list_endpoint': '%s/%s/' % (apitop, name),
}
serializer = Serializer()
desired_format = determine_format(request, serializer)
options = {}
if 'text/javascript' in desired_format:
callback = request.GET.get('callback', 'callback')
if not is_valid_jsonp_callback_value(callback):
raise BadRequest('JSONP callback name is invalid.')
options['callback'] = callback
serialized = serializer.serialize(available_resources, desired_format, options)
return HttpResponse(content=serialized, content_type=build_content_type(desired_format))
def autodiscover(): def autodiscover():
""" """
Auto-discover INSTALLED_APPS resources.py modules and fail silently when Auto-discover INSTALLED_APPS resources.py modules and fail silently when

View file

@ -3,6 +3,7 @@
from django.conf.urls import include from django.conf.urls import include
from ietf import api from ietf import api
from ietf.api import views as api_views
from ietf.meeting import views as meeting_views from ietf.meeting import views as meeting_views
from ietf.submit import views as submit_views from ietf.submit import views as submit_views
from ietf.utils.urls import url from ietf.utils.urls import url
@ -11,11 +12,13 @@ api.autodiscover()
urlpatterns = [ urlpatterns = [
# Top endpoint for Tastypie's REST API (this isn't standard Tastypie): # Top endpoint for Tastypie's REST API (this isn't standard Tastypie):
url(r'^v1/?$', api.top_level), url(r'^$', api_views.api_help),
url(r'^v1/?$', api_views.top_level),
# Custom API endpoints # Custom API endpoints
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings), url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
url(r'^submit/?$', submit_views.api_submit), url(r'^submit/?$', submit_views.api_submit),
] ]
# Additional (standard) Tastypie endpoints # Additional (standard) Tastypie endpoints
for n,a in api._api_list: for n,a in api._api_list:
urlpatterns += [ urlpatterns += [

52
ietf/api/views.py Normal file
View file

@ -0,0 +1,52 @@
# Copyright The IETF Trust 2017, All Rights Reserved
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from jwcrypto.jwk import JWK
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse
from tastypie.exceptions import BadRequest
from tastypie.utils.mime import determine_format, build_content_type
from tastypie.utils import is_valid_jsonp_callback_value
import debug # pyflakes:ignore
from ietf.api import Serializer, _api_list
def top_level(request):
available_resources = {}
apitop = reverse('ietf.api.views.top_level')
for name in sorted([ name for name, api in _api_list if len(api._registry) > 0 ]):
available_resources[name] = {
'list_endpoint': '%s/%s/' % (apitop, name),
}
serializer = Serializer()
desired_format = determine_format(request, serializer)
options = {}
if 'text/javascript' in desired_format:
callback = request.GET.get('callback', 'callback')
if not is_valid_jsonp_callback_value(callback):
raise BadRequest('JSONP callback name is invalid.')
options['callback'] = callback
serialized = serializer.serialize(available_resources, desired_format, options)
return HttpResponse(content=serialized, content_type=build_content_type(desired_format))
def api_help(request):
key = JWK()
# import just public part here, for display in info page
key.import_from_pem(settings.API_PUBLIC_KEY_PEM)
return render(request, "api/index.html", {'key': key, })

View file

@ -933,6 +933,9 @@ STATS_NAMES_LIMIT = 25
UTILS_TEST_RANDOM_STATE_FILE = '.factoryboy_random_state' UTILS_TEST_RANDOM_STATE_FILE = '.factoryboy_random_state'
API_PUBLIC_KEY_PEM = "Set this in settings_local.py"
API_PRIVATE_KEY_PEM = "Set this in settings_local.py"
# Put the production SECRET_KEY in settings_local.py, and also any other # Put the production SECRET_KEY in settings_local.py, and also any other
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn. # sensitive or site-specific changes. DO NOT commit settings_local.py to svn.

View file

@ -0,0 +1,193 @@
{# Copyright The IETF Trust 2007, All Rights Reserved #}
{% extends "base.html" %}
{% load staticfiles %}
{% block title %}API Notes{% endblock %}
{% block content %}
<h2>Datatracker API Notes</h2>
<div class="col-md-1"></div>
<div class="col-md-11 bio-text">
<h3>Framework</h3>
<p>
The datatracker API uses <a href="https://django-tastypie.readthedocs.org/">tastypie</a>
to generate an API which mirrors the Django ORM (Object Relational Mapping)
for the database. Each Django model class maps down to the SQL database
tables and up to the API. The Django models classes are defined in the
models.py files of the datatracker:
</p>
<p>
<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>
<br>
<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>
<br>
<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>
<br>
&hellip;
</p>
<p>
The API top endpoint is at <a href="https://datatracker.ietf.org/api/v1/">https://datatracker.ietf.org/api/v1/</a>. The top
endpoint lists inferior endpoints, and thus permits some autodiscovery,
but there's really no substitute for looking at the actual ORM model classes.
Comparing a class in models.py with the equivalent endpoint may give
some clue (note that in the case of Group, it's a subclass of GroupInfo):
</p>
<p>
<a href="https://datatracker.ietf.org/api/v1/group/group/">https://datatracker.ietf.org/api/v1/group/group/</a>
<br>
<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>
</p>
<p>
Data is currently provided in JSON and XML format. Adding new formats is
fairly easy, if it should be found desriable.
</p>
<h3>Documents</h3>
<p>
Documents are listed at
<a href="https://datatracker.ietf.org/api/v1/doc/document/">/api/v1/doc/document/</a>.
</p>
<p>
In general, individual database objects are represented in the api with a path
composed of the model collection, the object name, and the object key. Most
objects have simple numerical keys, but documents have the document name as
key. Take draft-ietf-eppext-keyrelay. Documents have a model 'Document' which
is described in the 'doc' models.py file. Assembling the path components
'doc', 'document' (lowercase!) and 'draft-ietf-eppext-keyrelay', we get the
URL:
</p>
<p>
<a href="https://datatracker.ietf.org/api/v1/doc/document/draft-ietf-eppext-keyrelay/">
https://datatracker.ietf.org/api/v1/doc/document/draft-ietf-eppext-keyrelay/
</a>
</p>
<p>
If you instead do a search for this document, you will get a machine-readable
search result, which is composed of some meta-information about the search,
and a list with one element:
</p>
<p>
<a href="https://datatracker.ietf.org/api/v1/doc/document/?name=draft-ietf-eppext-keyrelay">
api/v1/doc/document/?name=draft-ietf-eppext-keyrelay
</a>
</p>
<p>
To search for documents based on state, you need to know that documents have
multiple orthogonal states:
</p>
<ul>
<li>
If a document has an rfc-editor state, you can select for it by asking for e.g.
<a href="https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-&states__type__slug__in=draft-rfceditor">
v6ops documents which match 'states__type__slug__in=draft-rfceditor'
</a>
</li>
<li>
If a document has an IESG state, you can select for it by asking for e.g.
<a href="https://datatracker.ietf.org/api/v1/doc/document/?name__contains=-v6ops&states__type__slug__in=draft-iesg">
v6ops documents which match 'states__type__slug__in=draft-iesg'
</a>
</li>
<li>
If a document has a WG state, you can select for it by asking for
documents which match 'states__type__slug__in=draft-stream-ietf'
(but without additional filters, that's going to be a lot of documents)
</li>
<li>
States which match 'states__type__slug__in=draft' describe the basic
Active/Expired/Dead whatever state of the draft.
</li>
</ul>
<p>
You could use this in at least two alternative ways:
</p>
<p>
You could either fetch and remember the different state groups of interest to you
with queries like
<pre>
$ curl 'https://datatracker.ietf.org/api/v1/doc/state/?format=json&limit=0&type__slug__in=draft-rfceditor'
$ curl 'https://datatracker.ietf.org/api/v1/doc/state/?format=json&limit=0&type__slug__in=draft-iesg'
$ curl 'https://datatracker.ietf.org/api/v1/doc/state/?format=json&limit=0&type__slug__in=draft-stream-ietf'
</pre>
</p>
<p>
and then match the listed "resource_uri" of the results to the states listed for each
document when you ask for
<pre>
$ curl 'https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-'
</pre>
</p>
<p>
Or alternatively you could do a series of queries asking for matches to the RFC Editor
state first, then the IESG state, then the Stream state, and exclude earlier hits:
<pre>
$ curl 'https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-&states__type__slug__in=draft-rfceditor' ...
$ curl 'https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-&states__type__slug__in=draft-iesg' ...
</pre>
</p>
<p>
etc.
</p>
<h3>API Keys</h3>
<p>
The datatracker does not use any form of API keys currently (03 Nov
2017), but may do so in the future. If so, personal API keys will be
available from your <a href={% url 'ietf.ietfauth.views.profile'
%}>Account Profile</a> page when you are logged in, and document keys
will be visible to document authors on the document status page when
logged in.
</p>
<p>
When sending notifications to other APIs, the datatracker may sign
information with a <a href="https://tools.ietf.org/html/rfc7515">RFC
7515: JSON Web Signature (JWS)</a>, using a public/private keypair with
this public key:
</p>
<p><code>{{key.export_public}}</code></p>
<p>or alternatively:</p>
<pre>{{key.export_to_pem}}</pre>
</div>
{% endblock %}

View file

@ -26,6 +26,7 @@ hashids>=1.1.0
html5lib>=0.90,<0.99999999 # ietf.utils.html needs a rewrite for html5lib 1.x -- major code changes in sanitizer html5lib>=0.90,<0.99999999 # ietf.utils.html needs a rewrite for html5lib 1.x -- major code changes in sanitizer
httplib2>=0.10.3 httplib2>=0.10.3
jsonfield>=1.0.3 # for SubmissionCheck. This is https://github.com/bradjasper/django-jsonfield/. jsonfield>=1.0.3 # for SubmissionCheck. This is https://github.com/bradjasper/django-jsonfield/.
jwcrypto>=0.4.0 # for signed notifications
#lxml>=3.4.0 # from PyQuery; #lxml>=3.4.0 # from PyQuery;
mimeparse>=0.1.3 # from TastyPie mimeparse>=0.1.3 # from TastyPie
mock>=2.0.0 mock>=2.0.0