mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-07 09:18:24 -04:00
Merge pull request #96 from alphagov/api-keys-flow
Add pages for create/view/revoke API keys
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import dateutil
|
||||||
from flask import Flask, session, Markup, escape, render_template
|
from flask import Flask, session, Markup, escape, render_template
|
||||||
from flask._compat import string_types
|
from flask._compat import string_types
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
@@ -8,6 +9,7 @@ from flask_login import LoginManager
|
|||||||
from flask_wtf import CsrfProtect
|
from flask_wtf import CsrfProtect
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
from app.notify_client.api_client import NotificationsAdminAPIClient
|
from app.notify_client.api_client import NotificationsAdminAPIClient
|
||||||
|
from app.notify_client.api_key_api_client import ApiKeyApiClient
|
||||||
from app.notify_client.user_api_client import UserApiClient
|
from app.notify_client.user_api_client import UserApiClient
|
||||||
from app.its_dangerous_session import ItsdangerousSessionInterface
|
from app.its_dangerous_session import ItsdangerousSessionInterface
|
||||||
import app.proxy_fix
|
import app.proxy_fix
|
||||||
@@ -20,6 +22,7 @@ csrf = CsrfProtect()
|
|||||||
|
|
||||||
notifications_api_client = NotificationsAdminAPIClient()
|
notifications_api_client = NotificationsAdminAPIClient()
|
||||||
user_api_client = UserApiClient()
|
user_api_client = UserApiClient()
|
||||||
|
api_key_api_client = ApiKeyApiClient()
|
||||||
|
|
||||||
|
|
||||||
def create_app(config_name, config_overrides=None):
|
def create_app(config_name, config_overrides=None):
|
||||||
@@ -34,6 +37,7 @@ def create_app(config_name, config_overrides=None):
|
|||||||
|
|
||||||
notifications_api_client.init_app(application)
|
notifications_api_client.init_app(application)
|
||||||
user_api_client.init_app(application)
|
user_api_client.init_app(application)
|
||||||
|
api_key_api_client.init_app(application)
|
||||||
|
|
||||||
login_manager.init_app(application)
|
login_manager.init_app(application)
|
||||||
login_manager.login_view = 'main.sign_in'
|
login_manager.login_view = 'main.sign_in'
|
||||||
@@ -51,6 +55,7 @@ def create_app(config_name, config_overrides=None):
|
|||||||
application.add_template_filter(placeholders)
|
application.add_template_filter(placeholders)
|
||||||
application.add_template_filter(replace_placeholders)
|
application.add_template_filter(replace_placeholders)
|
||||||
application.add_template_filter(nl2br)
|
application.add_template_filter(nl2br)
|
||||||
|
application.add_template_filter(format_datetime)
|
||||||
|
|
||||||
application.after_request(useful_headers_after_request)
|
application.after_request(useful_headers_after_request)
|
||||||
register_errorhandlers(application)
|
register_errorhandlers(application)
|
||||||
@@ -131,6 +136,12 @@ def replace_placeholders(template, values):
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def format_datetime(date):
|
||||||
|
date = dateutil.parser.parse(date)
|
||||||
|
native = date.replace(tzinfo=None)
|
||||||
|
return native.strftime('%A %d %B %Y at %H:%M')
|
||||||
|
|
||||||
|
|
||||||
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
||||||
def useful_headers_after_request(response):
|
def useful_headers_after_request(response):
|
||||||
response.headers.add('X-Frame-Options', 'deny')
|
response.headers.add('X-Frame-Options', 'deny')
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
(function(Modules) {
|
(function(Modules) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
if (!document.queryCommandSupported('copy')) return;
|
||||||
|
|
||||||
Modules.ApiKey = function() {
|
Modules.ApiKey = function() {
|
||||||
|
|
||||||
const states = {
|
const states = {
|
||||||
'initial': `
|
|
||||||
<input type='button' class='api-key-button-show' value='Show API key' />
|
|
||||||
`,
|
|
||||||
'keyVisibleBasic': key => `
|
|
||||||
<span class="api-key-key">${key}</span>
|
|
||||||
`,
|
|
||||||
'keyVisible': key => `
|
'keyVisible': key => `
|
||||||
<span class="api-key-key">${key}</span>
|
<span class="api-key-key">${key}</span>
|
||||||
<input type='button' class='api-key-button-copy' value='Copy API key to clipboard' />
|
<input type='button' class='api-key-button-copy' value='Copy API key to clipboard' />
|
||||||
@@ -33,23 +29,22 @@
|
|||||||
|
|
||||||
this.start = function(component) {
|
this.start = function(component) {
|
||||||
|
|
||||||
const $component = $(component).html(states.initial).attr('aria-live', 'polite'),
|
const $component = $(component),
|
||||||
key = $component.data('key');
|
key = $component.data('key');
|
||||||
|
|
||||||
$component
|
$component
|
||||||
.on(
|
.html(states.keyVisible(key))
|
||||||
'click', '.api-key-button-show', () =>
|
.attr('aria-live', 'polite')
|
||||||
$component.html(
|
|
||||||
document.queryCommandSupported('copy') ?
|
|
||||||
states.keyVisible(key) : states.keyVisibleBasic(key)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.on(
|
.on(
|
||||||
'click', '.api-key-button-copy', () =>
|
'click', '.api-key-button-copy', () =>
|
||||||
this.copyKey(
|
this.copyKey(
|
||||||
$('.api-key-key', component)[0], () =>
|
$('.api-key-key', component)[0], () =>
|
||||||
$component.html(states.keyCopied)
|
$component.html(states.keyCopied)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.on(
|
||||||
|
'click', '.api-key-button-show', () =>
|
||||||
|
$component.html(states.keyVisible(key))
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
.api-key {
|
.api-key {
|
||||||
|
|
||||||
|
&-name {
|
||||||
|
@include bold-19;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
&-key {
|
&-key {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.table {
|
||||||
|
margin-bottom: $gutter;
|
||||||
|
}
|
||||||
|
|
||||||
.table-heading {
|
.table-heading {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -16,8 +20,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-error {
|
&-error {
|
||||||
|
|
||||||
color: $error-colour;
|
color: $error-colour;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
|
a:link,
|
||||||
|
a:visited {
|
||||||
|
color: $error-colour;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,3 +235,17 @@ class ChangeMobileNumberForm(Form):
|
|||||||
|
|
||||||
class ConfirmMobileNumberForm(Form):
|
class ConfirmMobileNumberForm(Form):
|
||||||
sms_code = sms_code()
|
sms_code = sms_code()
|
||||||
|
|
||||||
|
|
||||||
|
class CreateKeyForm(Form):
|
||||||
|
def __init__(self, existing_key_names=[], *args, **kwargs):
|
||||||
|
self.existing_key_names = [x.lower() for x in existing_key_names]
|
||||||
|
super(CreateKeyForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
key_name = StringField(u'Description of key', validators=[
|
||||||
|
DataRequired(message='You need to give the key a name')
|
||||||
|
])
|
||||||
|
|
||||||
|
def validate_key_name(self, key_name):
|
||||||
|
if key_name.data.lower() in self.existing_key_names:
|
||||||
|
raise ValidationError('A key with this name already exists')
|
||||||
|
|||||||
@@ -1,9 +1,55 @@
|
|||||||
from flask import render_template
|
from flask import request, render_template, redirect, url_for, flash
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from app.main import main
|
from app.main import main
|
||||||
|
from app.main.forms import CreateKeyForm
|
||||||
|
from app import api_key_api_client
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<int:service_id>/documentation")
|
||||||
|
@login_required
|
||||||
|
def documentation(service_id):
|
||||||
|
return render_template('views/documentation.html', service_id=service_id)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<int:service_id>/api-keys")
|
@main.route("/services/<int:service_id>/api-keys")
|
||||||
@login_required
|
@login_required
|
||||||
def api_keys(service_id):
|
def api_keys(service_id):
|
||||||
return render_template('views/api-keys.html', service_id=service_id)
|
return render_template(
|
||||||
|
'views/api-keys.html',
|
||||||
|
service_id=service_id,
|
||||||
|
keys=api_key_api_client.get_api_keys(service_id=service_id)['apiKeys']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<int:service_id>/api-keys/create", methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def create_api_key(service_id):
|
||||||
|
key_names = [
|
||||||
|
key['name'] for key in api_key_api_client.get_api_keys(service_id=service_id)['apiKeys']
|
||||||
|
]
|
||||||
|
form = CreateKeyForm(key_names)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
secret = api_key_api_client.create_api_key(service_id=service_id, key_name=form.key_name.data)
|
||||||
|
return render_template('views/api-keys/show.html', service_id=service_id, secret=secret,
|
||||||
|
key_name=form.key_name.data)
|
||||||
|
return render_template(
|
||||||
|
'views/api-keys/create.html',
|
||||||
|
service_id=service_id,
|
||||||
|
key_name=form.key_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<int:service_id>/api-keys/revoke/<int:key_id>", methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def revoke_api_key(service_id, key_id):
|
||||||
|
key_name = api_key_api_client.get_api_keys(service_id=service_id, key_id=key_id)['apiKeys'][0]['name']
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template(
|
||||||
|
'views/api-keys/revoke.html',
|
||||||
|
service_id=service_id,
|
||||||
|
key_name=key_name
|
||||||
|
)
|
||||||
|
elif request.method == 'POST':
|
||||||
|
api_key_api_client.revoke_api_key(service_id=service_id, key_id=key_id)
|
||||||
|
flash('‘{}’ was revoked'.format(key_name))
|
||||||
|
return redirect(url_for('.api_keys', service_id=service_id))
|
||||||
|
|||||||
27
app/notify_client/api_key_api_client.py
Normal file
27
app/notify_client/api_key_api_client.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from client.base import BaseAPIClient
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyApiClient(BaseAPIClient):
|
||||||
|
def __init__(self, base_url=None, client_id=None, secret=None):
|
||||||
|
super(self.__class__, self).__init__(base_url=base_url or 'base_url',
|
||||||
|
client_id=client_id or 'client_id',
|
||||||
|
secret=secret or 'secret')
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
self.base_url = app.config['API_HOST_NAME']
|
||||||
|
self.client_id = app.config['ADMIN_CLIENT_USER_NAME']
|
||||||
|
self.secret = app.config['ADMIN_CLIENT_SECRET']
|
||||||
|
|
||||||
|
def get_api_keys(self, service_id, key_id=None, *params):
|
||||||
|
if key_id:
|
||||||
|
return self.get(url='/service/{}/api-keys/{}'.format(service_id, key_id))
|
||||||
|
else:
|
||||||
|
return self.get(url='/service/{}/api-keys'.format(service_id))
|
||||||
|
|
||||||
|
def create_api_key(self, service_id, key_name, *params):
|
||||||
|
data = {"name": key_name}
|
||||||
|
key = self.post(url='/service/{}/api-key'.format(service_id), data=data)
|
||||||
|
return key['data']
|
||||||
|
|
||||||
|
def revoke_api_key(self, service_id, key_id, *params):
|
||||||
|
return self.post(url='/service/{0}/api-key/revoke/{1}'.format(service_id, key_id), data=None)
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
{% macro api_key(key) %}
|
{% macro api_key(key, name) %}
|
||||||
|
<h2 class="api-key-name">
|
||||||
|
{{ name }}
|
||||||
|
</h2>
|
||||||
<div data-module="api-key" data-key="{{ key }}">
|
<div data-module="api-key" data-key="{{ key }}">
|
||||||
<span class="api-key-key">{{ key }}</span>
|
<span class="api-key-key">{{ key }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,3 +55,7 @@
|
|||||||
{% macro right_aligned_field_heading(text) %}
|
{% macro right_aligned_field_heading(text) %}
|
||||||
<span class="table-field-heading-right-aligned">{{ text }}</span>
|
<span class="table-field-heading-right-aligned">{{ text }}</span>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro hidden_field_heading(text) %}
|
||||||
|
<span class="visuallyhidden">{{ text }}</span>
|
||||||
|
{%- endmacro %}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
<li><a href="{{ url_for('.manage_service_templates', service_id=service_id) }}">Templates</a></li>
|
<li><a href="{{ url_for('.manage_service_templates', service_id=service_id) }}">Templates</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ url_for('.api_keys', service_id=service_id) }}">API keys and documentation</a></li>
|
<li><a href="{{ url_for('.api_keys', service_id=123) }}">API keys</a></li>
|
||||||
|
<li><a href="{{ url_for('.documentation', service_id=123) }}">Developer documentation</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ url_for('.manage_users', service_id=service_id) }}">Manage users</a></li>
|
<li><a href="{{ url_for('.manage_users', service_id=service_id) }}">Manage users</a></li>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{% extends "withnav_template.html" %}
|
{% extends "withnav_template.html" %}
|
||||||
{% from "components/page-footer.html" import page_footer %}
|
{% from "components/table.html" import list_table, field, hidden_field_heading %}
|
||||||
{% from "components/api-key.html" import api_key %}
|
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
GOV.UK Notify | API keys and documentation
|
GOV.UK Notify | API keys and documentation
|
||||||
@@ -8,49 +7,51 @@
|
|||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
<div class="grid-row">
|
<h1 class="heading-xlarge">
|
||||||
<div class="column-two-thirds">
|
API keys
|
||||||
|
</h1>
|
||||||
|
|
||||||
<h1 class="heading-xlarge">
|
<p>
|
||||||
API keys and documentation
|
To connect to the API you will need to send your service ID, encrypted with
|
||||||
</h1>
|
an API key. The API key stays secret.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="heading-medium">
|
<p>
|
||||||
How to integrate GOV.UK Notify into your service
|
There are client libraries available which can do this for you. See
|
||||||
</h2>
|
<a href="{{ url_for(".documentation", service_id=service_id) }}">the
|
||||||
|
developer documentation</a> for more information.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<h2 class="api-key-name">
|
||||||
blah blah blah this is where we tell you how the API works
|
Service ID
|
||||||
</p>
|
</h2>
|
||||||
|
<p class="api-key-key">
|
||||||
|
{{ service_id }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="heading-medium">Repositories</h2>
|
{% call(item) list_table(
|
||||||
|
keys,
|
||||||
|
empty_message="You haven’t created any API keys yet",
|
||||||
|
caption="API keys",
|
||||||
|
caption_visible=False,
|
||||||
|
field_headings=['Key name', hidden_field_heading('Action')]
|
||||||
|
) %}
|
||||||
|
{% call field() %}
|
||||||
|
{{ item.name }}
|
||||||
|
{% endcall %}
|
||||||
|
{% if item.expiry_date %}
|
||||||
|
{% call field(align='right', status='default') %}
|
||||||
|
Revoked {{ item.expiry_date|format_datetime }}
|
||||||
|
{% endcall %}
|
||||||
|
{% else %}
|
||||||
|
{% call field(align='right', status='error') %}
|
||||||
|
<a href='{{ url_for('.revoke_api_key', service_id=service_id, key_id=item.id) }}'>Revoke</a>
|
||||||
|
{% endcall %}
|
||||||
|
{% endif %}
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{ url_for('.create_api_key', service_id=service_id) }}">Create a new API key</a>
|
||||||
<p>
|
</p>
|
||||||
<a href="https://github.com/alphagov/notifications-api">GOV.UK Notify API</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="https://github.com/alphagov/notify-api-client">GOV.UK Notify Python client</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="heading-medium">API key for [service name]</h2>
|
|
||||||
|
|
||||||
{{ api_key('d30512af92e1386d63b90e5973b49a10') }}
|
|
||||||
|
|
||||||
<h2 class="heading-medium">API endpoint</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
https://www.notify.works/api/endpoint
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ page_footer(
|
|
||||||
back_link=url_for('.service_dashboard', service_id=service_id),
|
|
||||||
back_link_text='Back to dashboard'
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
24
app/templates/views/api-keys/create.html
Normal file
24
app/templates/views/api-keys/create.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "withnav_template.html" %}
|
||||||
|
{% from "components/page-footer.html" import page_footer %}
|
||||||
|
{% from "components/textbox.html" import textbox %}
|
||||||
|
|
||||||
|
{% block page_title %}
|
||||||
|
GOV.UK Notify | API keys and documentation
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
|
<h1 class="heading-xlarge">
|
||||||
|
Add a new API key
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{{ textbox(key_name, hint='eg CRM application') }}
|
||||||
|
{{ page_footer(
|
||||||
|
'Continue',
|
||||||
|
back_link=url_for('.api_keys', service_id=service_id),
|
||||||
|
back_link_text='Back to API keys'
|
||||||
|
) }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
37
app/templates/views/api-keys/revoke.html
Normal file
37
app/templates/views/api-keys/revoke.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "withnav_template.html" %}
|
||||||
|
{% from "components/page-footer.html" import page_footer %}
|
||||||
|
{% from "components/api-key.html" import api_key %}
|
||||||
|
|
||||||
|
{% block page_title %}
|
||||||
|
GOV.UK Notify | API keys and documentation
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="column-two-thirds">
|
||||||
|
|
||||||
|
<h1 class="heading-xlarge">
|
||||||
|
Revoke API key
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
‘{{ key_name }}’ will no longer let you connect to GOV.UK Notify.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can’t undo this.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{{ page_footer(
|
||||||
|
'Revoke this API key',
|
||||||
|
back_link=url_for('.api_keys', service_id=service_id),
|
||||||
|
back_link_text='Back to API keys',
|
||||||
|
destructive=True
|
||||||
|
) }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
33
app/templates/views/api-keys/show.html
Normal file
33
app/templates/views/api-keys/show.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends "withnav_template.html" %}
|
||||||
|
{% from "components/page-footer.html" import page_footer %}
|
||||||
|
{% from "components/api-key.html" import api_key %}
|
||||||
|
|
||||||
|
{% block page_title %}
|
||||||
|
GOV.UK Notify | API keys and documentation
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="column-two-thirds">
|
||||||
|
|
||||||
|
<h1 class="heading-xlarge">
|
||||||
|
New API key
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Copy your key to somewhere safe. You won’t be able to see it again
|
||||||
|
once you leave this page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ api_key(secret, key_name) }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ page_footer(
|
||||||
|
back_link=url_for('.api_keys', service_id=service_id),
|
||||||
|
back_link_text='Back to API keys'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
49
app/templates/views/documentation.html
Normal file
49
app/templates/views/documentation.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends "withnav_template.html" %}
|
||||||
|
{% from "components/page-footer.html" import page_footer %}
|
||||||
|
{% from "components/api-key.html" import api_key %}
|
||||||
|
|
||||||
|
{% block page_title %}
|
||||||
|
GOV.UK Notify | API keys and documentation
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="column-two-thirds">
|
||||||
|
|
||||||
|
<h1 class="heading-xlarge">
|
||||||
|
Developer documentation
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h2 class="heading-medium">
|
||||||
|
How to integrate GOV.UK Notify into your service
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
blah blah blah this is where we tell you how the API works
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 class="heading-medium">Repositories</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/alphagov/notifications-api">GOV.UK Notify API</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/alphagov/notify-api-client">GOV.UK Notify Python client</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 class="heading-medium">API endpoint</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
https://www.notify.works/api/endpoint
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{ url_for('.api_keys', service_id=service_id) }}">API keys for your service</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -39,6 +39,12 @@ def template_json(id_, name, type_, content, service_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def api_key_json(id_, name, expiry_date=None):
|
||||||
|
return {'id': id_,
|
||||||
|
'name': name,
|
||||||
|
'expiry_date': expiry_date
|
||||||
|
}
|
||||||
|
|
||||||
TEST_USER_EMAIL = 'test@user.gov.uk'
|
TEST_USER_EMAIL = 'test@user.gov.uk'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
16
tests/app/main/test_create_api_key_form.py
Normal file
16
tests/app/main/test_create_api_key_form.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from werkzeug.datastructures import MultiDict
|
||||||
|
|
||||||
|
from app.main.forms import CreateKeyForm
|
||||||
|
|
||||||
|
|
||||||
|
def test_return_validation_error_when_key_name_exists(app_,
|
||||||
|
db_,
|
||||||
|
db_session):
|
||||||
|
def _get_names():
|
||||||
|
return ['some key', 'another key']
|
||||||
|
|
||||||
|
with app_.test_request_context():
|
||||||
|
form = CreateKeyForm(_get_names(),
|
||||||
|
formdata=MultiDict([('key_name', 'Some key')]))
|
||||||
|
form.validate()
|
||||||
|
assert {'key_name': ['A key with this name already exists']} == form.errors
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from datetime import date
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
|
|
||||||
@@ -7,9 +8,116 @@ def test_should_show_api_keys_and_documentation_page(app_,
|
|||||||
mock_api_user,
|
mock_api_user,
|
||||||
mock_user_loader,
|
mock_user_loader,
|
||||||
mock_user_dao_get_by_email):
|
mock_user_dao_get_by_email):
|
||||||
|
with app_.test_request_context():
|
||||||
|
with app_.test_client() as client:
|
||||||
|
client.login(mock_api_user)
|
||||||
|
response = client.get(url_for('main.documentation', service_id=123))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_show_empty_api_keys_page(app_,
|
||||||
|
db_,
|
||||||
|
db_session,
|
||||||
|
mock_api_user,
|
||||||
|
mock_user_loader,
|
||||||
|
mock_user_dao_get_by_email,
|
||||||
|
mock_get_no_api_keys):
|
||||||
with app_.test_request_context():
|
with app_.test_request_context():
|
||||||
with app_.test_client() as client:
|
with app_.test_client() as client:
|
||||||
client.login(mock_api_user)
|
client.login(mock_api_user)
|
||||||
response = client.get(url_for('main.api_keys', service_id=123))
|
response = client.get(url_for('main.api_keys', service_id=123))
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
assert 'You haven’t created any API keys yet' in response.get_data(as_text=True)
|
||||||
|
assert 'Create a new API key' in response.get_data(as_text=True)
|
||||||
|
mock_get_no_api_keys.assert_called_once_with(service_id=123)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_show_api_keys_page(app_,
|
||||||
|
db_,
|
||||||
|
db_session,
|
||||||
|
mock_api_user,
|
||||||
|
mock_user_loader,
|
||||||
|
mock_user_dao_get_by_email,
|
||||||
|
mock_get_api_keys):
|
||||||
|
with app_.test_request_context():
|
||||||
|
with app_.test_client() as client:
|
||||||
|
client.login(mock_api_user)
|
||||||
|
response = client.get(url_for('main.api_keys', service_id=123))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert 'some key name' in response.get_data(as_text=True)
|
||||||
|
assert 'another key name' in response.get_data(as_text=True)
|
||||||
|
assert 'Revoked Thursday 01 January 1970 at 00:00' in response.get_data(as_text=True)
|
||||||
|
mock_get_api_keys.assert_called_once_with(service_id=123)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_show_name_api_key_page(app_,
|
||||||
|
db_,
|
||||||
|
db_session,
|
||||||
|
mock_api_user,
|
||||||
|
mock_user_loader,
|
||||||
|
mock_user_dao_get_by_email,
|
||||||
|
mock_get_api_keys):
|
||||||
|
with app_.test_request_context():
|
||||||
|
with app_.test_client() as client:
|
||||||
|
client.login(mock_api_user)
|
||||||
|
response = client.get(url_for('main.create_api_key', service_id=123))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_render_show_api_key(app_,
|
||||||
|
db_,
|
||||||
|
db_session,
|
||||||
|
mock_api_user,
|
||||||
|
mock_user_loader,
|
||||||
|
mock_user_dao_get_by_email,
|
||||||
|
mock_create_api_key,
|
||||||
|
mock_get_api_keys):
|
||||||
|
with app_.test_request_context():
|
||||||
|
with app_.test_client() as client:
|
||||||
|
client.login(mock_api_user)
|
||||||
|
response = client.post(url_for('main.create_api_key', service_id=123),
|
||||||
|
data={'key_name': 'some default key name'})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert 'some default key name' in response.get_data(as_text=True)
|
||||||
|
mock_create_api_key.assert_called_once_with(service_id=123, key_name='some default key name')
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_show_confirm_revoke_api_key(app_,
|
||||||
|
db_,
|
||||||
|
db_session,
|
||||||
|
mock_api_user,
|
||||||
|
mock_user_loader,
|
||||||
|
mock_user_dao_get_by_email,
|
||||||
|
mock_get_api_keys):
|
||||||
|
with app_.test_request_context():
|
||||||
|
with app_.test_client() as client:
|
||||||
|
client.login(mock_api_user)
|
||||||
|
response = client.get(url_for('main.revoke_api_key', service_id=123, key_id=321))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert 'some key name' in response.get_data(as_text=True)
|
||||||
|
mock_get_api_keys.assert_called_once_with(service_id=123, key_id=321)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_redirect_after_revoking_api_key(app_,
|
||||||
|
db_,
|
||||||
|
db_session,
|
||||||
|
mock_api_user,
|
||||||
|
mock_user_loader,
|
||||||
|
mock_user_dao_get_by_email,
|
||||||
|
mock_revoke_api_key,
|
||||||
|
mock_get_api_keys):
|
||||||
|
with app_.test_request_context():
|
||||||
|
with app_.test_client() as client:
|
||||||
|
client.login(mock_api_user)
|
||||||
|
response = client.post(url_for('main.revoke_api_key', service_id=123, key_id=321))
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.location == url_for('.api_keys', service_id=123, _external=True)
|
||||||
|
mock_revoke_api_key.assert_called_once_with(service_id=123, key_id=321)
|
||||||
|
mock_get_api_keys.assert_called_once_with(service_id=123, key_id=321)
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from alembic.command import upgrade
|
from alembic.command import upgrade
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
from flask.ext.migrate import Migrate, MigrateCommand
|
from flask.ext.migrate import Migrate, MigrateCommand
|
||||||
from flask.ext.script import Manager
|
from flask.ext.script import Manager
|
||||||
from sqlalchemy.schema import MetaData
|
from sqlalchemy.schema import MetaData
|
||||||
from . import (
|
|
||||||
create_test_user, create_another_test_user, service_json, TestClient,
|
|
||||||
get_test_user, template_json)
|
|
||||||
from app import create_app, db
|
from app import create_app, db
|
||||||
|
from . import (
|
||||||
|
create_test_user, service_json, TestClient,
|
||||||
|
get_test_user, template_json, api_key_json)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
@@ -89,6 +92,7 @@ def mock_get_service(mocker, mock_api_user):
|
|||||||
service_id, "Test Service", [mock_api_user.id], limit=1000,
|
service_id, "Test Service", [mock_api_user.id], limit=1000,
|
||||||
active=False, restricted=True)
|
active=False, restricted=True)
|
||||||
return {'data': service, 'token': 1}
|
return {'data': service, 'token': 1}
|
||||||
|
|
||||||
return mocker.patch('app.notifications_api_client.get_service', side_effect=_create)
|
return mocker.patch('app.notifications_api_client.get_service', side_effect=_create)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,6 +103,7 @@ def mock_create_service(mocker):
|
|||||||
101, service_name, [user_id], limit=limit,
|
101, service_name, [user_id], limit=limit,
|
||||||
active=active, restricted=restricted)
|
active=active, restricted=restricted)
|
||||||
return {'data': service}
|
return {'data': service}
|
||||||
|
|
||||||
mock_class = mocker.patch(
|
mock_class = mocker.patch(
|
||||||
'app.notifications_api_client.create_service', side_effect=_create)
|
'app.notifications_api_client.create_service', side_effect=_create)
|
||||||
return mock_class
|
return mock_class
|
||||||
@@ -116,6 +121,7 @@ def mock_update_service(mocker):
|
|||||||
service_id, service_name, users, limit=limit,
|
service_id, service_name, users, limit=limit,
|
||||||
active=active, restricted=restricted)
|
active=active, restricted=restricted)
|
||||||
return {'data': service}
|
return {'data': service}
|
||||||
|
|
||||||
mock_class = mocker.patch(
|
mock_class = mocker.patch(
|
||||||
'app.notifications_api_client.update_service', side_effect=_update)
|
'app.notifications_api_client.update_service', side_effect=_update)
|
||||||
return mock_class
|
return mock_class
|
||||||
@@ -129,6 +135,7 @@ def mock_get_services(mocker, mock_api_user):
|
|||||||
service_two = service_json(
|
service_two = service_json(
|
||||||
2, "service_two", [mock_api_user.id], 1000, True, False)
|
2, "service_two", [mock_api_user.id], 1000, True, False)
|
||||||
return {'data': [service_one, service_two]}
|
return {'data': [service_one, service_two]}
|
||||||
|
|
||||||
mock_class = mocker.patch(
|
mock_class = mocker.patch(
|
||||||
'app.notifications_api_client.get_services', side_effect=_create)
|
'app.notifications_api_client.get_services', side_effect=_create)
|
||||||
return mock_class
|
return mock_class
|
||||||
@@ -138,6 +145,7 @@ def mock_get_services(mocker, mock_api_user):
|
|||||||
def mock_delete_service(mocker, mock_get_service):
|
def mock_delete_service(mocker, mock_get_service):
|
||||||
def _delete(service_id):
|
def _delete(service_id):
|
||||||
return mock_get_service.side_effect(service_id)
|
return mock_get_service.side_effect(service_id)
|
||||||
|
|
||||||
mock_class = mocker.patch(
|
mock_class = mocker.patch(
|
||||||
'app.notifications_api_client.delete_service', side_effect=_delete)
|
'app.notifications_api_client.delete_service', side_effect=_delete)
|
||||||
return mock_class
|
return mock_class
|
||||||
@@ -149,6 +157,7 @@ def mock_get_service_template(mocker):
|
|||||||
template = template_json(
|
template = template_json(
|
||||||
template_id, "Template Name", "sms", "template content", service_id)
|
template_id, "Template Name", "sms", "template content", service_id)
|
||||||
return {'data': template}
|
return {'data': template}
|
||||||
|
|
||||||
return mocker.patch(
|
return mocker.patch(
|
||||||
'app.notifications_api_client.get_service_template',
|
'app.notifications_api_client.get_service_template',
|
||||||
side_effect=_create)
|
side_effect=_create)
|
||||||
@@ -160,6 +169,7 @@ def mock_create_service_template(mocker):
|
|||||||
template = template_json(
|
template = template_json(
|
||||||
101, name, type_, content, service)
|
101, name, type_, content, service)
|
||||||
return {'data': template}
|
return {'data': template}
|
||||||
|
|
||||||
mock_class = mocker.patch(
|
mock_class = mocker.patch(
|
||||||
'app.notifications_api_client.create_service_template',
|
'app.notifications_api_client.create_service_template',
|
||||||
side_effect=_create)
|
side_effect=_create)
|
||||||
@@ -172,6 +182,7 @@ def mock_update_service_template(mocker):
|
|||||||
template = template_json(
|
template = template_json(
|
||||||
id_, name, type_, content, service)
|
id_, name, type_, content, service)
|
||||||
return {'data': template}
|
return {'data': template}
|
||||||
|
|
||||||
mock_class = mocker.patch(
|
mock_class = mocker.patch(
|
||||||
'app.notifications_api_client.update_service_template',
|
'app.notifications_api_client.update_service_template',
|
||||||
side_effect=_update)
|
side_effect=_update)
|
||||||
@@ -186,6 +197,7 @@ def mock_get_service_templates(mocker):
|
|||||||
template_two = template_json(
|
template_two = template_json(
|
||||||
2, "template_two", "sms", "template two content", service_id)
|
2, "template_two", "sms", "template two content", service_id)
|
||||||
return {'data': [template_one, template_two]}
|
return {'data': [template_one, template_two]}
|
||||||
|
|
||||||
mock_class = mocker.patch(
|
mock_class = mocker.patch(
|
||||||
'app.notifications_api_client.get_service_templates',
|
'app.notifications_api_client.get_service_templates',
|
||||||
side_effect=_create)
|
side_effect=_create)
|
||||||
@@ -199,6 +211,7 @@ def mock_delete_service_template(mocker):
|
|||||||
template_id, "Template to delete",
|
template_id, "Template to delete",
|
||||||
"sms", "content to be deleted", service_id)
|
"sms", "content to be deleted", service_id)
|
||||||
return {'data': template}
|
return {'data': template}
|
||||||
|
|
||||||
return mocker.patch(
|
return mocker.patch(
|
||||||
'app.notifications_api_client.delete_service_template', side_effect=_delete)
|
'app.notifications_api_client.delete_service_template', side_effect=_delete)
|
||||||
|
|
||||||
@@ -314,3 +327,43 @@ def mock_user_dao_get_new_password(mocker, mock_api_user):
|
|||||||
mock_class = mocker.patch('app.main.dao.users_dao.get_user_by_email')
|
mock_class = mocker.patch('app.main.dao.users_dao.get_user_by_email')
|
||||||
mock_class.return_value = mock_api_user
|
mock_class.return_value = mock_api_user
|
||||||
return mock_class
|
return mock_class
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def mock_create_api_key(mocker):
|
||||||
|
def _create(service_id, key_name):
|
||||||
|
import uuid
|
||||||
|
return {'data': str(uuid.uuid4())}
|
||||||
|
|
||||||
|
mock_class = mocker.patch('app.api_key_api_client.create_api_key', side_effect=_create)
|
||||||
|
return mock_class
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def mock_revoke_api_key(mocker):
|
||||||
|
def _revoke(service_id, key_id):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
mock_class = mocker.patch('app.api_key_api_client.revoke_api_key', side_effect=_revoke)
|
||||||
|
return mock_class
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def mock_get_api_keys(mocker):
|
||||||
|
def _get_keys(service_id, key_id=None):
|
||||||
|
keys = {'apiKeys': [api_key_json(1, 'some key name'),
|
||||||
|
api_key_json(2, 'another key name', expiry_date=str(date.fromtimestamp(0)))]}
|
||||||
|
return keys
|
||||||
|
|
||||||
|
mock_class = mocker.patch('app.api_key_api_client.get_api_keys', side_effect=_get_keys)
|
||||||
|
return mock_class
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def mock_get_no_api_keys(mocker):
|
||||||
|
def _get_keys(service_id):
|
||||||
|
keys = {'apiKeys': []}
|
||||||
|
return keys
|
||||||
|
|
||||||
|
mock_class = mocker.patch('app.api_key_api_client.get_api_keys', side_effect=_get_keys)
|
||||||
|
return mock_class
|
||||||
|
|||||||
Reference in New Issue
Block a user