diff --git a/Dockerfile b/Dockerfile index a3222fc..4da04f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,7 @@ ENV JWT_SECRET "Change-this-secret-phrase" ENV JWT_ALGORITHM "HS256" ENV JWT_ACCESS_TOKEN_EXPIRES "900" +ENV API_SECURITY "None" ENV API_USERNAME "admin" ENV API_PASSWORD "admin" @@ -35,7 +36,7 @@ RUN apt-get update \ gammu=1.40.0-1 \ gammu-smsd=1.40.0-1 \ libgammu-dev=1.40.0-1 \ - libmariadb-dev=1:10.3.22-0+deb10u1 \ + libmariadb-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/Readme.md b/Readme.md index c1d50d4..ddcb319 100644 --- a/Readme.md +++ b/Readme.md @@ -4,7 +4,7 @@ This REST API allow you to send and receive SMS using gammu-smsd. A 2.0 swagger documentation is provided at the root URL. -All routes are protected by a Bearer authentication. +All routes can be protected by an authentication methode (Basic, Bearer). # Table of Contents @@ -129,6 +129,11 @@ $ docker-compose up -d default value : "900" description : How long (in ms) an access token should live before it expires. Can be set to 0 to disable expiration. +#### API_SECURITY + default value : "None" + allowed values : "None" | "Bearer" | "Basic" + description : Select the authentication methode for the routes of the Rest API + #### API_USERNAME default value : "admin" description : User name used for connection to the rest API @@ -150,4 +155,4 @@ $ docker-compose up -d # Screenshots -![Swagger](screenshots/swagger.png) \ No newline at end of file +![Swagger](screenshots/swagger.png) diff --git a/docker-compose.yml b/docker-compose.yml index b54d770..82e55d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.3' services: backend: - image: fizcko/sms-gateway + image: fizcko/sms-gateway:latest restart: always ports: - 5000:5000 diff --git a/src/environment/instance.py b/src/environment/instance.py index c3a4674..b091fda 100644 --- a/src/environment/instance.py +++ b/src/environment/instance.py @@ -3,11 +3,26 @@ # Environment config port = os.environ.get("SERVER_PORT", "5000") ip = os.environ.get("SERVER_IP", "0.0.0.0") +security = os.environ.get("API_SECURITY", "None") + +if security == 'Bearer': + require_bearer = True + require_basic = False +elif security == 'Basic': + require_bearer = False + require_basic = True +else: + security = [] + require_bearer = False + require_basic = False environment_config = { "ip": ip, "port": port, - "swagger-url": "/" + "swagger-url": "/", + "security": security, + "require_bearer": require_bearer, + "require_basic": require_basic } # JWT config @@ -39,4 +54,4 @@ gammu_smsd_config = { "conf": gammu_smsd_conf -} \ No newline at end of file +} diff --git a/src/models/auth.py b/src/models/auth.py index f484fa2..c9f285b 100644 --- a/src/models/auth.py +++ b/src/models/auth.py @@ -1,4 +1,4 @@ -from flask_restplus import fields +from flask_restx import fields from server.instance import server login_post = server.api.model('Login Payload', { diff --git a/src/models/inbox.py b/src/models/inbox.py index 02f2a30..4ae19b8 100644 --- a/src/models/inbox.py +++ b/src/models/inbox.py @@ -1,4 +1,4 @@ -from flask_restplus import fields +from flask_restx import fields from server.instance import server inbox_item = server.api.model('inbox item', { @@ -53,4 +53,4 @@ 'results': fields.List( fields.Nested(inbox_item) ) -}) \ No newline at end of file +}) diff --git a/src/models/outbox.py b/src/models/outbox.py index 2863de5..90e3281 100644 --- a/src/models/outbox.py +++ b/src/models/outbox.py @@ -1,4 +1,4 @@ -from flask_restplus import fields +from flask_restx import fields from server.instance import server from datetime import time @@ -85,4 +85,4 @@ def format(self, value): 'results': fields.List( fields.Nested(outbox_item) ) -}) \ No newline at end of file +}) diff --git a/src/models/send.py b/src/models/send.py index e159bab..f812a01 100644 --- a/src/models/send.py +++ b/src/models/send.py @@ -1,4 +1,4 @@ -from flask_restplus import fields +from flask_restx import fields from server.instance import server sms_post = server.api.model('Send SMS Payload', { @@ -40,4 +40,4 @@ ] ) -}) \ No newline at end of file +}) diff --git a/src/models/senditems.py b/src/models/senditems.py index c2d0e88..6a0d1cc 100644 --- a/src/models/senditems.py +++ b/src/models/senditems.py @@ -1,4 +1,4 @@ -from flask_restplus import fields +from flask_restx import fields from server.instance import server senditems_item = server.api.model('senditems item', { @@ -74,4 +74,4 @@ 'results': fields.List( fields.Nested(senditems_item) ) -}) \ No newline at end of file +}) diff --git a/src/requirements.txt b/src/requirements.txt index 1070614..31ed046 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,11 +1,9 @@ -waitress==1.4.3 -flask==1.1.2 -flask-restplus==0.13.0 -flask-jwt-extended==3.24.1 +waitress==2.0.0 +flask==2.0.1 +flask-restx==0.5.1 +flask-jwt-extended==4.3.0 +flask-basicauth==0.2.0 SQLAlchemy==1.3.17 SQLAlchemy-Utils==0.36.6 python-gammu==2.12 mysqlclient==1.4.6 - -# Fix bug in flask -werkzeug==0.16.1 diff --git a/src/resources/decorators.py b/src/resources/decorators.py new file mode 100644 index 0000000..dd40a78 --- /dev/null +++ b/src/resources/decorators.py @@ -0,0 +1,25 @@ +from server.instance import server +from flask_jwt_extended import ( verify_jwt_in_request ) +from flask_basicauth import BasicAuth + +app = server.app +basic_auth = BasicAuth(app) + +def required_bearerAuth(bearerAuth = True): + def decorator(fn): + def decorated(*args,**kwargs): + if bearerAuth: + verify_jwt_in_request() + return fn(*args,**kwargs) + return decorated + return decorator + +def required_basicAuth(basicAuth = True): + def decorator(fn): + def decorated(*args,**kwargs): + if basicAuth: + if not basic_auth.authenticate(): + return {'message': 'Basic authenfication fail', 'details': 'Wrong username or password.'}, 401 + return fn(*args,**kwargs) + return decorated + return decorator diff --git a/src/resources/v1/auth.py b/src/resources/v1/auth.py index 702551c..7a3d8a8 100644 --- a/src/resources/v1/auth.py +++ b/src/resources/v1/auth.py @@ -1,6 +1,9 @@ from flask import request -from flask_restplus import Resource +from flask_restx import Resource from flask_jwt_extended import (create_access_token, get_jwt_identity, jwt_required) +from resources.decorators import ( required_bearerAuth ) +from environment.instance import environment_config + import os from server.instance import server @@ -13,8 +16,9 @@ class login(Resource): @ns.expect(login_post, validate=True) - @ns.doc(description='Get a token for requests') + @ns.doc(description='Get a bearer token for requests protected by a bearer Authentication') @api.response(200, 'Success', token_response) + @api.doc(security=[]) def post(self): ''' Get a token for requests''' json_data = request.json @@ -33,10 +37,9 @@ def post(self): @ns.route('/v1/refresh') class refresh(Resource): - @ns.doc(description='Get a new token for requests') + @ns.doc(description='Get a new bearer token for requests protected by a bearer Authentication') @api.response(200, 'Success', token_response) - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) def get(self): ''' Get a token for requests''' current_user = get_jwt_identity() diff --git a/src/resources/v1/inbox.py b/src/resources/v1/inbox.py index e6b4600..a0467f4 100644 --- a/src/resources/v1/inbox.py +++ b/src/resources/v1/inbox.py @@ -1,11 +1,13 @@ from flask import request -from flask_restplus import Resource, reqparse +from flask_restx import Resource, reqparse from flask_jwt_extended import ( jwt_required ) from database.models import inbox from database.instance import db_session from server.instance import server from math import ceil from models.inbox import inbox_item, inbox_items +from resources.decorators import ( required_bearerAuth, required_basicAuth ) +from environment.instance import environment_config api = server.api ns = api.namespace('Inbox', description='Inbox operations', path='/') @@ -22,8 +24,8 @@ class Inbox(Resource): @api.doc(params={'after': {'description': 'Filter SMS received after a date', 'in': 'query', 'type': 'date'}}) @api.doc(params={'sender': {'description': 'Filter SMS received from a number', 'in': 'query', 'type': 'string'}}) @api.expect(pagination_arguments, validate=True) - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) @api.response(200, 'Success', inbox_items) def get(self): ''' Get all SMS located in the inbox''' @@ -66,8 +68,8 @@ class InboxID(Resource): @ns.doc(params={'id': 'ID of a SMS to get'}) @ns.doc(description='Get a SMS located in the inbox by his ID') - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) @api.response(200, 'Success', inbox_item) def get(self, id: int): ''' Get a SMS located in the inbox by his ID''' @@ -80,12 +82,12 @@ def get(self, id: int): @ns.doc(params={'id': 'ID of a SMS to delete'}) @ns.doc(description='Delete a SMS located in the inbox') @ns.response(204, 'Success') - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) def delete(self, id: int): ''' Delete a SMS located in the inbox''' sms = inbox.query.filter_by(ID=id).first() if(sms): db_session.delete(sms) db_session.commit() - return {'results': 'ok'}, 204 \ No newline at end of file + return {'results': 'ok'}, 204 diff --git a/src/resources/v1/outbox.py b/src/resources/v1/outbox.py index 20f4536..64f610a 100644 --- a/src/resources/v1/outbox.py +++ b/src/resources/v1/outbox.py @@ -1,11 +1,13 @@ from flask import request -from flask_restplus import Resource, reqparse +from flask_restx import Resource, reqparse from flask_jwt_extended import ( jwt_required ) from database.models import outbox from database.instance import db_session from server.instance import server from math import ceil from models.outbox import outbox_item, outbox_items +from resources.decorators import ( required_bearerAuth, required_basicAuth ) +from environment.instance import environment_config api = server.api ns = api.namespace('Outbox', description='Outbox operations', path='/') @@ -22,8 +24,8 @@ class Outbox(Resource): @api.doc(params={'after': {'description': 'Filter SMS send after a date', 'in': 'query', 'type': 'date'}}) @api.doc(params={'destination': {'description': 'Filter SMS send to a number', 'in': 'query', 'type': 'string'}}) @api.expect(pagination_arguments, validate=True) - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) @api.response(200, 'Success', outbox_items) def get(self): ''' Get all SMS located in the outbox''' @@ -66,8 +68,8 @@ class OutboxID(Resource): @ns.doc(params={'id': 'ID of a SMS to get'}) @ns.doc(description='Get a SMS located in the outbox by his ID') - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) @api.response(200, 'Success', outbox_item) def get(self, id: int): ''' Get a SMS located in the outbox by his ID''' @@ -80,8 +82,8 @@ def get(self, id: int): @ns.doc(params={'id': 'ID of a SMS to delete'}) @ns.doc(description='Delete a SMS located in the outbox') @ns.response(204, 'Success') - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) def delete(self, id: int): ''' Delete a SMS located in the outbox''' sms = outbox.query.filter_by(ID=id).first() diff --git a/src/resources/v1/send.py b/src/resources/v1/send.py index 689ba37..8f6f3f2 100644 --- a/src/resources/v1/send.py +++ b/src/resources/v1/send.py @@ -1,9 +1,11 @@ from flask import request -from flask_restplus import Resource +from flask_restx import Resource from flask_jwt_extended import ( jwt_required ) from server.instance import server from environment.instance import gammu_smsd_config from models.send import sms_post, send_result +from resources.decorators import ( required_bearerAuth, required_basicAuth ) +from environment.instance import environment_config import gammu import gammu.smsd @@ -18,8 +20,8 @@ class SendSMS(Resource): @ns.expect(sms_post, validate=True) @ns.doc(description='Send a SMS') - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) @api.response(200, 'Success', send_result) def post(self): ''' Send a SMS''' diff --git a/src/resources/v1/sentitems.py b/src/resources/v1/sentitems.py index 45f1ec5..91f4a94 100644 --- a/src/resources/v1/sentitems.py +++ b/src/resources/v1/sentitems.py @@ -1,11 +1,13 @@ from flask import request -from flask_restplus import Resource, reqparse +from flask_restx import Resource, reqparse from flask_jwt_extended import ( jwt_required ) from database.models import sentitems from database.instance import db_session from server.instance import server from math import ceil from models.senditems import senditems_item, senditems_items +from resources.decorators import ( required_bearerAuth, required_basicAuth ) +from environment.instance import environment_config api = server.api ns = api.namespace('Sent Items', description='Sent Items operations', path='/') @@ -22,8 +24,8 @@ class SendItems(Resource): @api.doc(params={'after': {'description': 'Filter SMS send after a date', 'in': 'query', 'type': 'date'}}) @api.doc(params={'destination': {'description': 'Filter SMS send from a number', 'in': 'query', 'type': 'string'}}) @api.expect(pagination_arguments, validate=True) - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) @api.response(200, 'Success', senditems_items) def get(self): ''' Get all SMS located in the send items''' @@ -66,8 +68,8 @@ class SendItemsID(Resource): @ns.doc(params={'id': 'ID of a SMS to get'}) @ns.doc(description='Get a SMS located in the send items by his ID') - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) @api.response(200, 'Success', senditems_item) def get(self, id: int): ''' Get a SMS located in the send items by his ID''' @@ -80,12 +82,12 @@ def get(self, id: int): @ns.doc(params={'id': 'ID of a SMS to delete'}) @ns.doc(description='Delete a SMS located in the send items') @ns.response(204, 'Success') - @api.doc(security='Bearer') - @jwt_required + @required_bearerAuth(environment_config["require_bearer"]) + @required_basicAuth(environment_config["require_basic"]) def delete(self, id: int): ''' Delete a SMS located in the send items''' sms = sentitems.query.filter_by(ID=id).first() if(sms): db_session.delete(sms) db_session.commit() - return {'results': 'ok'}, 204 \ No newline at end of file + return {'results': 'ok'}, 204 diff --git a/src/server/instance.py b/src/server/instance.py index 52ed9fd..2a8960b 100644 --- a/src/server/instance.py +++ b/src/server/instance.py @@ -1,8 +1,10 @@ from waitress import serve from flask import Flask -from flask_restplus import Api, Resource, fields +from flask_restx import Api, Resource, fields from flask_jwt_extended import (JWTManager) +from flask_basicauth import BasicAuth +import os import logging import datetime @@ -32,21 +34,31 @@ def __init__(self): # The key of the error message in a JSON error response self.app.config['JWT_ERROR_MESSAGE_KEY'] = "message" + # flask_basicauth + self.app.config['BASIC_AUTH_USERNAME'] = os.environ.get("API_USERNAME", "admin") + self.app.config['BASIC_AUTH_PASSWORD'] = os.environ.get("API_PASSWORD", "admin") + authorizations = { 'Bearer': { 'type': 'apiKey', 'in': 'header', 'name': 'Authorization', 'description': "Type in the *'Value'* input box below: **'Bearer XXX'**, where XXX is the token" + }, + 'Basic': { + 'type': 'basic', + 'in': 'header', + 'name': 'Authorization' } } self.api = Api(self.app, - version='1.0', + version='1.0.9', title='SMS Gateway', description='This REST API allow you to send and receive SMS', doc = environment_config["swagger-url"], - authorizations=authorizations + authorizations=authorizations, + security=environment_config["security"] ) self.jwt = JWTManager(self.app)