mirror of
https://github.com/GSA/notifications-api.git
synced 2026-07-30 11:18:54 -04:00
Merge pull request #1034 from alphagov/push-inbound-sms
Push inbound sms
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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'))
|
||||
@@ -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
|
||||
|
||||
32
app/dao/service_inbound_api_dao.py
Normal file
32
app/dao/service_inbound_api_dao.py
Normal 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()
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ class ApiKeySchema(BaseSchema):
|
||||
|
||||
class Meta:
|
||||
model = models.ApiKey
|
||||
exclude = ("service", "secret")
|
||||
exclude = ("service", "_secret")
|
||||
strict = True
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
27
app/service/service_inbound_api_schema.py
Normal file
27
app/service/service_inbound_api_schema.py
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user