From 526003fd26038591c6fac9979bb3519048c79f14 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Fri, 3 Nov 2017 16:17:00 +0000 Subject: [PATCH] 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 --- ietf/api/__init__.py | 34 +----- ietf/api/urls.py | 5 +- ietf/api/views.py | 52 +++++++++ ietf/settings.py | 3 + ietf/templates/api/index.html | 193 ++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 6 files changed, 254 insertions(+), 34 deletions(-) create mode 100644 ietf/api/views.py create mode 100644 ietf/templates/api/index.html diff --git a/ietf/api/__init__.py b/ietf/api/__init__.py index ee0d27bb8..7506b6480 100644 --- a/ietf/api/__init__.py +++ b/ietf/api/__init__.py @@ -4,9 +4,7 @@ import datetime from urllib import urlencode from django.conf import settings -from django.http import HttpResponse from django.core.exceptions import ObjectDoesNotExist -from django.urls import reverse from django.utils.encoding import force_text import debug # pyflakes:ignore @@ -16,13 +14,9 @@ import tastypie.resources from tastypie.api import Api from tastypie.bundle import Bundle from tastypie.serializers import Serializer as BaseSerializer -from tastypie.exceptions import BadRequest, ApiFieldError -from tastypie.utils.mime import determine_format, build_content_type -from tastypie.utils import is_valid_jsonp_callback_value +from tastypie.exceptions import ApiFieldError from tastypie.fields import ApiField -import debug # pyflakes:ignore - _api_list = [] class ModelResource(tastypie.resources.ModelResource): @@ -99,32 +93,6 @@ for _app in settings.INSTALLED_APPS: _module_dict[_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(): """ Auto-discover INSTALLED_APPS resources.py modules and fail silently when diff --git a/ietf/api/urls.py b/ietf/api/urls.py index 7cb85e8f4..365857333 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -3,6 +3,7 @@ from django.conf.urls import include from ietf import api +from ietf.api import views as api_views from ietf.meeting import views as meeting_views from ietf.submit import views as submit_views from ietf.utils.urls import url @@ -11,11 +12,13 @@ api.autodiscover() urlpatterns = [ # 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 url(r'^notify/meeting/import_recordings/(?P[a-z0-9-]+)/?$', meeting_views.api_import_recordings), url(r'^submit/?$', submit_views.api_submit), ] + # Additional (standard) Tastypie endpoints for n,a in api._api_list: urlpatterns += [ diff --git a/ietf/api/views.py b/ietf/api/views.py new file mode 100644 index 000000000..4ffff0a54 --- /dev/null +++ b/ietf/api/views.py @@ -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, }) + diff --git a/ietf/settings.py b/ietf/settings.py index 80a583b6a..4695a0268 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -933,6 +933,9 @@ STATS_NAMES_LIMIT = 25 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 # sensitive or site-specific changes. DO NOT commit settings_local.py to svn. diff --git a/ietf/templates/api/index.html b/ietf/templates/api/index.html new file mode 100644 index 000000000..431bdcf2c --- /dev/null +++ b/ietf/templates/api/index.html @@ -0,0 +1,193 @@ +{# Copyright The IETF Trust 2007, All Rights Reserved #} +{% extends "base.html" %} +{% load staticfiles %} +{% block title %}API Notes{% endblock %} +{% block content %} + +

Datatracker API Notes

+
+
+

Framework

+

+ The datatracker API uses tastypie + 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: +

+ +

+ https://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/doc/models.py +
+ https://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/group/models.py +
+ http://svn.tools.ietf.org/svn/tools/ietfdb/trunk/ietf/iesg/models.py +
+ … + +

+

+ + The API top endpoint is at https://datatracker.ietf.org/api/v1/. 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): + +

+

+ + https://datatracker.ietf.org/api/v1/group/group/ +
+ https://trac.tools.ietf.org/tools/ietfdb/browser/trunk/ietf/group/models.py + +

+

+ + Data is currently provided in JSON and XML format. Adding new formats is + fairly easy, if it should be found desriable. + +

+ +

Documents

+ +

+ + Documents are listed at + /api/v1/doc/document/. + +

+

+ + 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: + +

+

+ + + + https://datatracker.ietf.org/api/v1/doc/document/draft-ietf-eppext-keyrelay/ + + +

+

+ + 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: + +

+

+ + + api/v1/doc/document/?name=draft-ietf-eppext-keyrelay + + +

+

+ + To search for documents based on state, you need to know that documents have + multiple orthogonal states: + +

+ + +

+ You could use this in at least two alternative ways: +

+ +

+ You could either fetch and remember the different state groups of interest to you + with queries like + +

+      $ 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'
+      
+

+ +

+ and then match the listed "resource_uri" of the results to the states listed for each + document when you ask for +

+      $ curl 'https://datatracker.ietf.org/api/v1/doc/document/?limit=0&name__contains=-v6ops-'
+      
+

+ +

+ 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: +

+      $ 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' ...
+    
+

+ +

+ etc. +

+ +

API Keys

+ +

+ + 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 Account Profile page when you are logged in, and document keys + will be visible to document authors on the document status page when + logged in. + +

+ +

+ + When sending notifications to other APIs, the datatracker may sign + information with a RFC + 7515: JSON Web Signature (JWS), using a public/private keypair with + this public key: + +

+

{{key.export_public}}

+ +

or alternatively:

+ +
{{key.export_to_pem}}
+ +
+ + +{% endblock %} + diff --git a/requirements.txt b/requirements.txt index 3c6bc9894..d926f1e57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 httplib2>=0.10.3 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; mimeparse>=0.1.3 # from TastyPie mock>=2.0.0