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:
parent
e899ed6e4d
commit
526003fd26
|
@ -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
|
||||||
|
|
|
@ -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
52
ietf/api/views.py
Normal 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, })
|
||||||
|
|
|
@ -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.
|
||||||
|
|
193
ietf/templates/api/index.html
Normal file
193
ietf/templates/api/index.html
Normal 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>
|
||||||
|
…
|
||||||
|
|
||||||
|
</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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue