Merge pull request #126 from GSA/open-api

Add OpenAPI spec for selected api endpoints
This commit is contained in:
Ryan Ahearn
2022-11-30 13:30:26 -05:00
committed by GitHub
11 changed files with 626 additions and 10 deletions

View File

@@ -107,7 +107,7 @@ jobs:
uses: zaproxy/action-api-scan@v0.1.1
with:
docker_name: 'owasp/zap2docker-stable'
target: 'http://localhost:6011/_status'
target: 'http://localhost:6011/docs/openapi.yml'
fail_action: true
allow_issue_writing: false
rules_file_name: 'zap.conf'

View File

@@ -76,7 +76,7 @@ jobs:
uses: zaproxy/action-api-scan@v0.1.1
with:
docker_name: 'owasp/zap2docker-weekly'
target: 'http://localhost:6011/_status'
target: 'http://localhost:6011/docs/openapi.yml'
fail_action: true
allow_issue_writing: false
rules_file_name: 'zap.conf'

View File

@@ -134,6 +134,7 @@ def register_blueprint(application):
)
from app.billing.rest import billing_blueprint
from app.complaint.complaint_rest import complaint_blueprint
from app.docs import docs as docs_blueprint
from app.email_branding.rest import email_branding_blueprint
from app.events.rest import events as events_blueprint
from app.inbound_number.rest import inbound_number_blueprint
@@ -193,6 +194,9 @@ def register_blueprint(application):
status_blueprint.before_request(requires_no_auth)
application.register_blueprint(status_blueprint)
docs_blueprint.before_request(requires_no_auth)
application.register_blueprint(docs_blueprint)
# delivery receipts
ses_callback_blueprint.before_request(requires_no_auth)
application.register_blueprint(ses_callback_blueprint)

View File

@@ -132,8 +132,9 @@ def _decode_jwt_token(auth_token, api_keys, service_id=None):
try:
decode_jwt_token(auth_token, api_key.secret)
except TokenExpiredError:
err_msg = "Error: Your system clock must be accurate to within 30 seconds"
raise AuthError(err_msg, 403, service_id=service_id, api_key_id=api_key.id)
if not current_app.config.get('ALLOW_EXPIRED_API_TOKEN', False):
err_msg = "Error: Your system clock must be accurate to within 30 seconds"
raise AuthError(err_msg, 403, service_id=service_id, api_key_id=api_key.id)
except TokenAlgorithmError:
err_msg = "Invalid token: algorithm used is not HS256"
raise AuthError(err_msg, 403, service_id=service_id, api_key_id=api_key.id)

View File

@@ -1,14 +1,15 @@
import csv
import functools
import itertools
import os
import uuid
from datetime import datetime, timedelta
from os import getenv
import click
import flask
from click_datetime import Datetime as click_dt
from flask import current_app, json
from notifications_python_client.authentication import create_jwt_token
from notifications_utils.recipients import RecipientCSV
from notifications_utils.statsd_decorators import statsd
from notifications_utils.template import SMSMessageTemplate
@@ -86,7 +87,7 @@ class notify_command:
# in the test environment the app context is already provided and having
# another will lead to the test db connection being closed prematurely
if os.getenv('NOTIFY_ENVIRONMENT', '') != 'test':
if getenv('NOTIFY_ENVIRONMENT', '') != 'test':
# with_appcontext ensures the config is loaded, db connected, etc.
decorators.insert(0, flask.cli.with_appcontext)
@@ -111,7 +112,7 @@ def purge_functional_test_data(user_email_prefix):
users, services, etc. Give an email prefix. Probably "notify-tests-preview".
"""
if os.getenv('NOTIFY_ENVIRONMENT', '') not in ['development', 'test']:
if getenv('NOTIFY_ENVIRONMENT', '') not in ['development', 'test']:
current_app.logger.error('Can only be run in development')
return
@@ -726,7 +727,7 @@ def validate_mobile(ctx, param, value):
@click.option('-s', '--state', default="active")
@click.option('-d', '--admin', default=False, type=bool)
def create_test_user(name, email, mobile_number, password, auth_type, state, admin):
if os.getenv('NOTIFY_ENVIRONMENT', '') not in ['development', 'test']:
if getenv('NOTIFY_ENVIRONMENT', '') not in ['development', 'test']:
current_app.logger.error('Can only be run in development')
return
@@ -746,3 +747,22 @@ def create_test_user(name, email, mobile_number, password, auth_type, state, adm
except IntegrityError:
print("duplicate user", user.name)
db.session.rollback()
@notify_command(name='create-admin-jwt')
def create_admin_jwt():
if getenv('NOTIFY_ENVIRONMENT', '') != 'development':
current_app.logger.error('Can only be run in development')
return
print(create_jwt_token(current_app.config['SECRET_KEY'], current_app.config['ADMIN_CLIENT_ID']))
@notify_command(name='create-user-jwt')
@click.option('-t', '--token', required=True, prompt=False)
def create_user_jwt(token):
if getenv('NOTIFY_ENVIRONMENT', '') != 'development':
current_app.logger.error('Can only be run in development')
return
service_id = token[-73:-37]
api_key = token[-36:]
print(create_jwt_token(api_key, service_id))

View File

@@ -80,6 +80,7 @@ class Config(object):
('{"%s":["%s"]}' % (ADMIN_CLIENT_ID, getenv('ADMIN_CLIENT_SECRET')))
)
)
ALLOW_EXPIRED_API_TOKEN = False
# encyption secret/salt
SECRET_KEY = getenv('SECRET_KEY')
DANGEROUS_SALT = getenv('DANGEROUS_SALT')
@@ -367,6 +368,7 @@ class Development(Config):
DANGEROUS_SALT = 'dev-notify-salt'
SECRET_KEY = 'dev-notify-secret-key' # nosec B105 - this is only used in development
INTERNAL_CLIENT_API_KEYS = {Config.ADMIN_CLIENT_ID: ['dev-notify-secret-key']}
ALLOW_EXPIRED_API_TOKEN = getenv('ALLOW_EXPIRED_API_TOKEN', '0') == '1'
class Test(Development):

11
app/docs/__init__.py Normal file
View File

@@ -0,0 +1,11 @@
from os import path
from flask import Blueprint, current_app, send_file
docs = Blueprint('docs', __name__, url_prefix='/docs')
@docs.route('/openapi.yml', methods=['GET'])
def send_openapi():
openapi_schema = path.join(current_app.root_path, '../docs/openapi.yml')
return send_file(openapi_schema, mimetype='text/yaml'), 200

View File

@@ -8,3 +8,31 @@ For a usage example, see [our Python demo](https://github.com/GSA/notify-python-
An API key can be created at https://notifications-admin.app.cloud.gov/services/YOUR_SERVICE_ID/api/keys. However, in order to successfully send messages, you will need to receive a secret header token from the Notify team.
## Using OpenAPI documentation
### Retrieving a jwt-encoded bearer token for use
On a mac, run
#### Admin UI token
```
flask command create-admin-jwt | tail -n 1 | pbcopy
```
#### User token
```
flask command create-user-jwt --token=<USER_API_TOKEN> | tail -n 1 | pbcopy
```
to copy a token to your pasteboard. This token will expire in 30 seconds
### Disable token expiration checking in development
Because jwt tokens expire so quickly, the development server can be set to allow tokens older than 30 seconds:
```
env ALLOW_EXPIRED_API_TOKEN=1 make run-flask
```

535
docs/openapi.yml Normal file
View File

@@ -0,0 +1,535 @@
openapi: '3.0.2'
info:
title: Notify API
version: '1.0'
description: |
The following OpenAPI document lists a subset of the available APIs for US Notify.
We are currently API compatible with GOV.UK Notify. Please visit [their documentation](https://docs.notifications.service.gov.uk/rest-api.html)
for more information.
Authorization uses [a JSON Web Token (JWT) bearer token](https://docs.notifications.service.gov.uk/rest-api.html#authorisation-header). The internal-api
methods use the same scheme, but must use a specific key for the Admin UI.
servers:
- url: https://notify-api.app.cloud.gov
description: Production API endpoint
- url: https://notify-api-staging.app.cloud.gov
description: Staging API endpoint
- url: http://localhost:6011
description: Local development API endpoint
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
uuidPath:
name: uuid
in: path
required: true
schema:
type: string
schemas:
serviceObject:
type: object
properties:
active:
type: boolean
billing_contact_email_addresses:
type: string
billing_contact_names:
type: string
billing_reference:
type: string
consent_to_research:
type: string
contact_link:
type: string
count_as_live:
type: boolean
created_by:
type: string
email_branding:
type: string
email_from:
type: string
go_live_at:
type: string
go_live_user:
type: string
id:
type: string
inbound_api:
type: array
letter_branding:
type: string
message_limit:
type: number
name:
type: string
notes:
type: string
organisation:
type: string
organisation_type:
type: string
enum: ["federal", "state", "other"]
default: "federal"
permissions:
type: array
items:
type: string
prefix_sms:
type: boolean
purchase_order_number:
type: string
rate_limit:
type: number
research_model:
type: boolean
restricted:
type: boolean
service_callback_api:
type: array
volume_email:
type: string
volume_letter:
type: string
volume_sms:
type: string
userObject:
type: object
properties:
auth_type:
type: string
can_use_webauthn:
type: string
current_session_id:
type: string
email_access_validated_at:
type: string
email_address:
type: string
failed_login_count:
type: number
id:
type: string
logged_in_at:
type: string
mobile_number:
type: string
name:
type: string
organisations:
type: array
items:
type: string
password_changed_at:
type: string
permissions:
type: object
properties:
SERVICE_ID:
type: array
items:
type: string
platform_admin:
type: boolean
services:
type: array
items:
type: string
state:
type: string
enum: ["pending", "active", "inactive"]
apiKeyResponse:
type: object
properties:
apiKeys:
type: array
items:
type: object
properties:
created_by:
type: string
created_at:
type: string
expiry_date:
type: string
id:
type: string
key_type:
type: string
name:
type: string
updated_at:
type: string
version:
type: number
paths:
/_status?simple=1:
get:
description: 'Retrieve only an acknowledgement that the server is listening'
tags:
- public
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: ["ok"]
/_status:
get:
description: 'Retrieve information on the status of the Notify API server'
tags:
- public
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
build_time:
type: string
db_version:
type: string
git_commit:
type: string
status:
type: string
enum: ["ok"]
/_status/live-service-and-organisation-counts:
get:
description: 'Retrieve a count of live services and organizations in the Notify system'
tags:
- public
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
services:
type: number
organisations:
type: number
/user:
get:
security:
- bearerAuth: []
description: 'Retrieve list of all users'
tags:
- internal-api
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/userObject"
/user/{uuid}:
get:
security:
- bearerAuth: []
description: 'Retrieve single user details'
tags:
- internal-api
parameters:
- $ref: "#/components/parameters/uuidPath"
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/userObject"
/organisations:
get:
security:
- bearerAuth: []
description: 'Retrieve organization details'
tags:
- internal-api
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
type: object
properties:
active:
type: boolean
count_of_live_services:
type: number
domains:
type: array
id:
type: string
name:
type: string
organisation_type:
type: string
enum: ["federal", "state", "other"]
/service:
get:
security:
- bearerAuth: []
description: 'Retrieve all services'
tags:
- internal-api
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/serviceObject"
/service/find-services-by-name:
get:
security:
- bearerAuth: []
description: 'Find a service by name'
tags:
- internal-api
parameters:
- name: service_name
in: query
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
active:
type: boolean
id:
type: string
name:
type: string
research_mode:
type: boolean
restricted:
type: boolean
/service/live-services-data:
get:
security:
- bearerAuth: []
description: 'Unsure'
tags:
- internal-api
responses:
'200':
description: OK
/service/{uuid}:
get:
security:
- bearerAuth: []
description: 'Retrieve details of a single service'
tags:
- internal-api
parameters:
- $ref: "#/components/parameters/uuidPath"
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/serviceObject"
/service/{uuid}/statistics:
get:
security:
- bearerAuth: []
description: 'Retrieve statistics about messages sent by a service'
tags:
- internal-api
parameters:
- $ref: "#/components/parameters/uuidPath"
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
/service/{uuid}/api-keys:
get:
security:
- bearerAuth: []
description: 'Retrieve api-keys for a service'
tags:
- internal-api
parameters:
- $ref: "#/components/parameters/uuidPath"
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/apiKeyResponse"
/service/{uuid}/api-keys/{key-id}:
get:
security:
- bearerAuth: []
description: 'Retrieve details of a single API key'
tags:
- internal-api
parameters:
- $ref: "#/components/parameters/uuidPath"
- name: key-id
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/apiKeyResponse"
/service/{uuid}/users:
get:
security:
- bearerAuth: []
description: 'Retrieve users associated with this service'
tags:
- internal-api
parameters:
- $ref: "#/components/parameters/uuidPath"
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/userObject"
/v2/notifications/sms:
post:
security:
- bearerAuth: []
description: 'Send an SMS message to a single phone number'
tags:
- external-api
requestBody:
required: true
description: |
The request body is a JSON object giving at least the phone nubmer to
deliver the message to and the template ID to send to that number.
If the template has variables, provide them in the `personalisation`
object with the variable names as the object keys.
content:
application/json:
schema:
type: object
required:
- phone_number
- template_id
properties:
phone_number:
type: string
template_id:
type: string
personalisation:
type: object
reference:
type: string
example:
phone_number: "5558675309"
template_id: "85b58733-7ebf-494e-bee2-a21a4ce17d58"
personalisation:
variable: "value"
responses:
'201':
description: Sent
content:
application/json:
schema:
type: object
properties:
content:
type: object
properties:
body:
type: string
from_number:
type: string
required:
- body
- from_number
additionalProperties: false
id:
type: string
reference:
type: string
scheduled_for:
type: string
template:
type: object
properties:
id:
type: string
uri:
type: string
version:
type: integer
required:
- id
- uri
- version
additionalProperties: false
uri:
type: string
additionalProperties: false
required:
- content
- id
- reference
- template
- uri

View File

@@ -37,5 +37,11 @@ This will run an interactive prompt to create a user, and then mark that user as
2. On your host machine run:
```
docker run -v $(pwd):/zap/wrk/:rw --network="notify-network" -t owasp/zap2docker-weekly zap-api-scan.py -t http://dev:6011/_status -f openapi -c zap.conf
```
docker run -v $(pwd):/zap/wrk/:rw --network="notify-network" -t owasp/zap2docker-weekly zap-api-scan.py -t http://dev:6011/docs/openapi.yml -f openapi -c zap.conf
```
The equivalent command if you are running the API locally:
```
docker run -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-weekly zap-api-scan.py -t http://host.docker.internal:6011/docs/openapi.yml -f openapi -c zap.conf
```

View File

@@ -6,6 +6,15 @@ _Most of the API endpoints in this repo are for internal use. These are all defi
Public APIs are intended for use by services and are all located under `app/v2/` to distinguish them from internal endpoints. Originally we did have a "v1" public API, where we tried to reuse / expose existing internal endpoints. The needs for public APIs are sufficiently different that we decided to separate them out. Any "v1" endpoints that remain are now purely internal and no longer exposed to services.
## Documenting APIs
New and existing APIs should be documented within [openapi.yml](./openapi.yml). Tools to help
with editing this file:
* [OpenAPI Editor for VSCode](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi)
* [OpenAPI specification](https://spec.openapis.org/oas/v3.0.2)
## New APIs
Here are some pointers for how we write public API endpoints.