Merge pull request #1034 from alphagov/push-inbound-sms

Push inbound sms
This commit is contained in:
Rebecca Law
2017-06-19 15:15:07 +01:00
committed by GitHub
21 changed files with 519 additions and 64 deletions

View File

@@ -73,7 +73,7 @@ def requires_auth():
for api_key in service.api_keys:
try:
get_decode_errors(auth_token, api_key.unsigned_secret)
get_decode_errors(auth_token, api_key.secret)
except TokenDecodeError:
continue

View File

@@ -1,12 +0,0 @@
from flask import current_app
from itsdangerous import URLSafeSerializer
def get_secret(secret):
serializer = URLSafeSerializer(current_app.config.get('SECRET_KEY'))
return serializer.loads(secret, salt=current_app.config.get('DANGEROUS_SALT'))
def generate_secret(token):
serializer = URLSafeSerializer(current_app.config.get('SECRET_KEY'))
return serializer.dumps(str(token), current_app.config.get('DANGEROUS_SALT'))

View File

@@ -1,14 +1,13 @@
import uuid
from datetime import datetime
from app import db
from app import db, encryption
from app.models import ApiKey
from app.dao.dao_utils import (
transactional,
version_class
)
from app.authentication.utils import generate_secret
@transactional
@@ -16,7 +15,7 @@ from app.authentication.utils import generate_secret
def save_model_api_key(api_key):
if not api_key.id:
api_key.id = uuid.uuid4() # must be set now so version history model can use same id
api_key.secret = generate_secret(uuid.uuid4())
api_key.secret = uuid.uuid4()
db.session.add(api_key)
@@ -39,7 +38,7 @@ def get_unsigned_secrets(service_id):
This method can only be exposed to the Authentication of the api calls.
"""
api_keys = ApiKey.query.filter_by(service_id=service_id, expiry_date=None).all()
keys = [x.unsigned_secret for x in api_keys]
keys = [x.secret for x in api_keys]
return keys
@@ -48,4 +47,4 @@ def get_unsigned_secret(key_id):
This method can only be exposed to the Authentication of the api calls.
"""
api_key = ApiKey.query.filter_by(id=key_id, expiry_date=None).one()
return api_key.unsigned_secret
return api_key.secret

View File

@@ -0,0 +1,32 @@
from datetime import datetime
from app import db, create_uuid
from app.dao.dao_utils import transactional, version_class
from app.models import ServiceInboundApi
@transactional
@version_class(ServiceInboundApi)
def save_service_inbound_api(service_inbound_api):
service_inbound_api.id = create_uuid()
service_inbound_api.created_at == datetime.utcnow()
service_inbound_api.bearer_token = service_inbound_api.bearer_token
db.session.add(service_inbound_api)
@transactional
@version_class(ServiceInboundApi)
def reset_service_inbound_api(service_inbound_api, updated_by_id, url=None, bearer_token=None):
if url:
service_inbound_api.url = url
if bearer_token:
service_inbound_api.bearer_token = bearer_token
service_inbound_api.updated_by_id = updated_by_id
service_inbound_api.updated_at = datetime.utcnow()
db.session.add(service_inbound_api)
def get_service_inbound_api(service_inbound_api_id, service_id):
return ServiceInboundApi.query.filter_by(id=service_inbound_api_id,
service_id=service_id).first()

View File

@@ -22,7 +22,6 @@ from app.encryption import (
hashpw,
check_hash
)
from app.authentication.utils import get_secret
from app import (
db,
encryption,
@@ -295,12 +294,46 @@ class ServiceWhitelist(db.Model):
return 'Recipient {} of type: {}'.format(self.recipient, self.recipient_type)
class ServiceInboundApi(db.Model, Versioned):
__tablename__ = 'service_inbound_api'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False, unique=True)
service = db.relationship('Service', backref='inbound_api')
url = db.Column(db.String(), nullable=False)
_bearer_token = db.Column("bearer_token", db.String(), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, nullable=True)
updated_by = db.relationship('User')
updated_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), index=True, nullable=False)
@property
def bearer_token(self):
if self._bearer_token:
return encryption.decrypt(self._bearer_token)
return None
@bearer_token.setter
def bearer_token(self, bearer_token):
if bearer_token:
self._bearer_token = encryption.encrypt(str(bearer_token))
def serialize(self):
return {
"id": str(self.id),
"service_id": str(self.service_id),
"url": self.url,
"updated_by_id": str(self.updated_by_id),
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None
}
class ApiKey(db.Model, Versioned):
__tablename__ = 'api_keys'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = db.Column(db.String(255), nullable=False)
secret = db.Column(db.String(255), unique=True, nullable=False)
_secret = db.Column("secret", db.String(255), unique=True, nullable=False)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False)
service = db.relationship('Service', backref='api_keys')
key_type = db.Column(db.String(255), db.ForeignKey('key_types.name'), index=True, nullable=False)
@@ -325,8 +358,15 @@ class ApiKey(db.Model, Versioned):
)
@property
def unsigned_secret(self):
return get_secret(self.secret)
def secret(self):
if self._secret:
return encryption.decrypt(self._secret)
return None
@secret.setter
def secret(self, secret):
if secret:
self._secret = encryption.encrypt(str(secret))
KEY_TYPE_NORMAL = 'normal'

View File

@@ -53,12 +53,20 @@ def build_error_message(errors):
fields.append({"error": "ValidationError", "message": field})
message = {
"status_code": 400,
"errors": fields
"errors": unique_errors(fields)
}
return json.dumps(message)
def unique_errors(dups):
unique = []
for x in dups:
if x not in unique:
unique.append(x)
return unique
def __format_message(e):
def get_path(e):
error_path = None

View File

@@ -18,3 +18,13 @@ personalisation = {
"code": "1001", # yet to be implemented
"link": "link to our error documentation not yet implemented"
}
https_url = {
"type": "string",
"format": "uri",
"pattern": "^https.*",
"validationMessage": "is not a valid https url",
"code": "1001", # yet to be implemented
"link": "link to our error documentation not yet implemented"
}

View File

@@ -338,7 +338,7 @@ class ApiKeySchema(BaseSchema):
class Meta:
model = models.ApiKey
exclude = ("service", "secret")
exclude = ("service", "_secret")
strict = True

View File

@@ -8,6 +8,7 @@ from flask import (
current_app,
Blueprint
)
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm.exc import NoResultFound
from app import redis_store
@@ -20,6 +21,11 @@ from app.dao.api_key_dao import (
expire_api_key)
from app.dao.date_util import get_financial_year
from app.dao.notification_usage_dao import get_total_billable_units_for_sent_sms_notifications_in_date_range
from app.dao.service_inbound_api_dao import (
save_service_inbound_api,
reset_service_inbound_api,
get_service_inbound_api
)
from app.dao.services_dao import (
dao_fetch_service_by_id,
dao_fetch_all_services,
@@ -49,8 +55,10 @@ from app.errors import (
InvalidRequest,
register_errors
)
from app.models import Service
from app.models import Service, ServiceInboundApi
from app.schema_validation import validate
from app.service import statistics
from app.service.service_inbound_api_schema import service_inbound_api, update_service_inbound_api_schema
from app.service.utils import get_whitelist_objects
from app.service.sender import send_notification_to_service_users
from app.schemas import (
@@ -531,3 +539,55 @@ def get_yearly_monthly_usage(service_id):
return json.dumps(json_results)
except TypeError:
return jsonify(result='error', message='No valid year provided'), 400
@service_blueprint.route('/<uuid:service_id>/inbound-api', methods=['POST'])
def create_service_inbound_api(service_id):
data = request.get_json()
validate(data, service_inbound_api)
data["service_id"] = service_id
inbound_api = ServiceInboundApi(**data)
try:
save_service_inbound_api(inbound_api)
except SQLAlchemyError as e:
return handle_sql_errror(e)
return jsonify(data=inbound_api.serialize()), 201
@service_blueprint.route('/<uuid:service_id>/inbound-api/<uuid:inbound_api_id>', methods=['POST'])
def update_service_inbound_api(service_id, inbound_api_id):
data = request.get_json()
validate(data, update_service_inbound_api_schema)
to_update = get_service_inbound_api(inbound_api_id, service_id)
reset_service_inbound_api(service_inbound_api=to_update,
updated_by_id=data["updated_by_id"],
url=data.get("url", None),
bearer_token=data.get("bearer_token", None))
return jsonify(data=to_update.serialize()), 200
@service_blueprint.route('/<uuid:service_id>/inbound-api/<uuid:inbound_api_id>', methods=["GET"])
def fetch_service_inbound_api(service_id, inbound_api_id):
inbound_api = get_service_inbound_api(inbound_api_id, service_id)
return jsonify(data=inbound_api.serialize()), 200
def handle_sql_errror(e):
if hasattr(e, 'orig') and hasattr(e.orig, 'pgerror') and e.orig.pgerror \
and ('duplicate key value violates unique constraint "ix_service_inbound_api_service_id"'
in e.orig.pgerror):
return jsonify(
result='error',
message={'name': ["You can only have one URL and bearer token for your service."]}
), 400
elif hasattr(e, 'orig') and hasattr(e.orig, 'pgerror') and e.orig.pgerror \
and ('insert or update on table "service_inbound_api" violates '
'foreign key constraint "service_inbound_api_service_id_fkey"'
in e.orig.pgerror):
return jsonify(result='error', message="No result found"), 404
else:
raise e

View File

@@ -0,0 +1,27 @@
from app.schema_validation.definitions import uuid, https_url
service_inbound_api = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST service inbound api schema",
"type": "object",
"title": "Create service inbound api",
"properties": {
"url": https_url,
"bearer_token": {"type": "string", "minLength": 10},
"updated_by_id": uuid
},
"required": ["url", "bearer_token", "updated_by_id"]
}
update_service_inbound_api_schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST service inbound api schema",
"type": "object",
"title": "Create service inbound api",
"properties": {
"url": https_url,
"bearer_token": {"type": "string", "minLength": 10},
"updated_by_id": uuid
},
"required": ["updated_by_id"]
}