From f3a57cb2cd95ee617bc291415778573f28b8735f Mon Sep 17 00:00:00 2001 From: esensar Date: Sat, 6 Oct 2018 13:38:27 +0200 Subject: [PATCH 1/7] Remove code from accounts __init__ to separate modules --- app/accounts/__init__.py | 117 ------------------ app/accounts/api.py | 114 +++++++++++++++++ app/accounts/blueprint.py | 3 + app/api/__init__.py | 2 +- app/api/resources/account.py | 2 +- app/api/resources/token.py | 2 +- app/core.py | 4 +- .../{celery.py => celery_configurator.py} | 0 config.py | 2 +- 9 files changed, 123 insertions(+), 123 deletions(-) create mode 100644 app/accounts/api.py create mode 100644 app/accounts/blueprint.py rename app/tasks/{celery.py => celery_configurator.py} (100%) diff --git a/app/accounts/__init__.py b/app/accounts/__init__.py index 81afe41..e69de29 100644 --- a/app/accounts/__init__.py +++ b/app/accounts/__init__.py @@ -1,117 +0,0 @@ -from app.core import bcrypt -from flask import Blueprint -from .models import Account, Role - -accounts_bp = Blueprint('accounts', __name__) - - -def create_account(username, email, password): - """ - Tries to create account with given parameters. Raises error on failure - - :param username: Desired username for Account - :param email: Desired email for Account - :param password: Desired password for Account - :type username: string - :type email: string - :type password: string - :returns: True if account is successfully created - :rtype: Boolean - :raises: ValueError if account already exists - """ - if not Account.exists_with_any_of(username=username, email=email): - pw_hash = bcrypt.generate_password_hash(password).decode('utf-8') - account = Account(username, pw_hash, email) - account.save() - return True - - raise ValueError("Account with given parameters already exists") - - -def update_account_role(account_id, role_id): - """ - Tries to update account role - - :param account_id: Target account id - :param role_id: New role role_id - :type account_id: int - :type role_id: int - :returns: True if role is updated successfully - :rtype: Boolean - """ - acc = Account.get(id=account_id) - acc.role_id = role_id - acc.save() - - -def create_role(display_name, permissions): - """ - Tries to create role - - :param display_name: Name of role - display only - :param permissions: List of strings - permissions that this role has - :type display_name: String - :type permissions: List of String - :returns: True if role is successfully created - :rtype: Boolean - :raises: ValueError if role already exists - """ - role = Role(display_name, permissions) - role.save() - - -def get_role(role_id): - """ - Tries to get role - - :param role_id: Id of role - :type role_id: int - :returns: Role if found - :rtype: Role - """ - return Role.get(role_id) - - -def get_all_roles(): - """ - Gets all roles - - :returns: Role list if found - :rtype: List of Roles - """ - return Role.get_all() - - -def create_token(username, password): - """ - Tries to create token for account with given parameters. - Raises error on failure - - :param username: username of Account - :param password: password of Account - :type username: string - :type password: string - :returns: created token - :rtype: string - :raises: ValueError if credentials are invalid or account does not exist - """ - if not Account.exists(username=username): - raise ValueError("Invalid credentials") - - account = Account.get(username=username) - if not bcrypt.check_password_hash(account.password, password): - raise ValueError("Invalid credentials") - - return account.create_auth_token() - - -def validate_token(token): - """ - Validates token and returns associated account - - :param token: auth token to validate - :type token: string - :returns: created token - :rtype: Account - """ - return Account.validate_token(token) diff --git a/app/accounts/api.py b/app/accounts/api.py new file mode 100644 index 0000000..4a2d9d8 --- /dev/null +++ b/app/accounts/api.py @@ -0,0 +1,114 @@ +from app.core import bcrypt +from .models import Account, Role + + +def create_account(username, email, password): + """ + Tries to create account with given parameters. Raises error on failure + + :param username: Desired username for Account + :param email: Desired email for Account + :param password: Desired password for Account + :type username: string + :type email: string + :type password: string + :returns: True if account is successfully created + :rtype: Boolean + :raises: ValueError if account already exists + """ + if not Account.exists_with_any_of(username=username, email=email): + pw_hash = bcrypt.generate_password_hash(password).decode('utf-8') + account = Account(username, pw_hash, email) + account.save() + return True + + raise ValueError("Account with given parameters already exists") + + +def update_account_role(account_id, role_id): + """ + Tries to update account role + + :param account_id: Target account id + :param role_id: New role role_id + :type account_id: int + :type role_id: int + :returns: True if role is updated successfully + :rtype: Boolean + """ + acc = Account.get(id=account_id) + acc.role_id = role_id + acc.save() + + +def create_role(display_name, permissions): + """ + Tries to create role + + :param display_name: Name of role - display only + :param permissions: List of strings - permissions that this role has + :type display_name: String + :type permissions: List of String + :returns: True if role is successfully created + :rtype: Boolean + :raises: ValueError if role already exists + """ + role = Role(display_name, permissions) + role.save() + + +def get_role(role_id): + """ + Tries to get role + + :param role_id: Id of role + :type role_id: int + :returns: Role if found + :rtype: Role + """ + return Role.get(role_id) + + +def get_all_roles(): + """ + Gets all roles + + :returns: Role list if found + :rtype: List of Roles + """ + return Role.get_all() + + +def create_token(username, password): + """ + Tries to create token for account with given parameters. + Raises error on failure + + :param username: username of Account + :param password: password of Account + :type username: string + :type password: string + :returns: created token + :rtype: string + :raises: ValueError if credentials are invalid or account does not exist + """ + if not Account.exists(username=username): + raise ValueError("Invalid credentials") + + account = Account.get(username=username) + if not bcrypt.check_password_hash(account.password, password): + raise ValueError("Invalid credentials") + + return account.create_auth_token() + + +def validate_token(token): + """ + Validates token and returns associated account + + :param token: auth token to validate + :type token: string + :returns: created token + :rtype: Account + """ + return Account.validate_token(token) diff --git a/app/accounts/blueprint.py b/app/accounts/blueprint.py new file mode 100644 index 0000000..3ba41b8 --- /dev/null +++ b/app/accounts/blueprint.py @@ -0,0 +1,3 @@ +from flask import Blueprint + +accounts_bp = Blueprint('accounts', __name__) diff --git a/app/api/__init__.py b/app/api/__init__.py index c862280..0b077dc 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -3,7 +3,7 @@ from flask import Blueprint, request, g from flask_restful import Api, Resource, abort from functools import wraps from marshmallow import ValidationError -from app.accounts import validate_token +from app.accounts.api import validate_token api_bp = Blueprint('api', __name__) diff --git a/app/api/resources/account.py b/app/api/resources/account.py index 382f46b..8fbe961 100644 --- a/app/api/resources/account.py +++ b/app/api/resources/account.py @@ -3,7 +3,7 @@ from flask import g from marshmallow import Schema, fields from webargs.flaskparser import use_args from flasgger import swag_from -import app.accounts as accounts +import app.accounts.api as accounts from app.api import ProtectedResource, requires_permission diff --git a/app/api/resources/token.py b/app/api/resources/token.py index d98f2b4..26d80bf 100644 --- a/app/api/resources/token.py +++ b/app/api/resources/token.py @@ -3,7 +3,7 @@ from webargs import fields from webargs.flaskparser import use_args from flasgger import swag_from from app.api import ProtectedResource -import app.accounts as accounts +import app.accounts.api as accounts class TokenResource(Resource): diff --git a/app/core.py b/app/core.py index ef5a5bc..c4ca7ea 100644 --- a/app/core.py +++ b/app/core.py @@ -4,7 +4,7 @@ from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flasgger import Swagger from flask_cors import CORS -from .tasks import celery as celery_configurator +from .tasks import celery_configurator app = FlaskAPI(__name__, instance_relative_config=True) app.config.from_object('config') @@ -27,7 +27,7 @@ def setup_blueprints(app): They are exposed as blueprints just for consistency, otherwise they are just simple python packages/modules """ - from .accounts import accounts_bp + from .accounts.blueprint import accounts_bp from .devices import devices_bp from .dashboard import dashboard_bp from .api import api_bp diff --git a/app/tasks/celery.py b/app/tasks/celery_configurator.py similarity index 100% rename from app/tasks/celery.py rename to app/tasks/celery_configurator.py diff --git a/config.py b/config.py index f9e0237..55e4c11 100644 --- a/config.py +++ b/config.py @@ -28,7 +28,7 @@ CSRF_SESSION_KEY = "secret" SECRET_KEY = "?['Z(Z\x83Y \x06T\x12\x96<\xff\x12\xe0\x1b\xd1J\xe0\xd9ld" # MQTT configuration -MQTT_CLIENT_ID = 'final-iot-backend-server' +MQTT_CLIENT_ID = 'final-iot-backend-server-local2' MQTT_BROKER_URL = 'broker.hivemq.com' MQTT_BROKER_PORT = 1883 MQTT_USERNAME = 'user' From 723d1377a1742b48cc14671d31fb619502e8cc80 Mon Sep 17 00:00:00 2001 From: esensar Date: Sat, 6 Oct 2018 13:46:32 +0200 Subject: [PATCH 2/7] Remove code from dashboards __init__.py to separate modules --- app/api/resources/dashboard.py | 2 +- app/core.py | 2 +- app/dashboards/__init__.py | 0 app/{dashboard/__init__.py => dashboards/api.py} | 3 --- app/dashboards/blueprint.py | 3 +++ app/{dashboard => dashboards}/models.py | 0 6 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 app/dashboards/__init__.py rename app/{dashboard/__init__.py => dashboards/api.py} (94%) create mode 100644 app/dashboards/blueprint.py rename app/{dashboard => dashboards}/models.py (100%) diff --git a/app/api/resources/dashboard.py b/app/api/resources/dashboard.py index f2ee90c..a046fd2 100644 --- a/app/api/resources/dashboard.py +++ b/app/api/resources/dashboard.py @@ -3,7 +3,7 @@ from flask_restful import abort from marshmallow import Schema, fields from webargs.flaskparser import use_args from flasgger import swag_from -import app.dashboard as dashboard +import app.dashboards.api as dashboard from app.api import ProtectedResource diff --git a/app/core.py b/app/core.py index c4ca7ea..d1ab8a5 100644 --- a/app/core.py +++ b/app/core.py @@ -29,7 +29,7 @@ def setup_blueprints(app): """ from .accounts.blueprint import accounts_bp from .devices import devices_bp - from .dashboard import dashboard_bp + from .dashboards.blueprint import dashboard_bp from .api import api_bp from .mqtt import mqtt_bp diff --git a/app/dashboards/__init__.py b/app/dashboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/dashboard/__init__.py b/app/dashboards/api.py similarity index 94% rename from app/dashboard/__init__.py rename to app/dashboards/api.py index 90f4499..e35e809 100644 --- a/app/dashboard/__init__.py +++ b/app/dashboards/api.py @@ -1,8 +1,5 @@ -from flask import Blueprint from .models import Dashboard -dashboard_bp = Blueprint('dashboard', __name__) - # Public interface def create_dashboard(dashboard_data, account_id): diff --git a/app/dashboards/blueprint.py b/app/dashboards/blueprint.py new file mode 100644 index 0000000..bf4b371 --- /dev/null +++ b/app/dashboards/blueprint.py @@ -0,0 +1,3 @@ +from flask import Blueprint + +dashboard_bp = Blueprint('dashboard', __name__) diff --git a/app/dashboard/models.py b/app/dashboards/models.py similarity index 100% rename from app/dashboard/models.py rename to app/dashboards/models.py From df874fee6c6c980abec83008a782e21590072bfd Mon Sep 17 00:00:00 2001 From: esensar Date: Sat, 6 Oct 2018 13:50:53 +0200 Subject: [PATCH 3/7] Move code from devices __init__.py into separate modules --- app/api/resources/device.py | 2 +- app/core.py | 2 +- app/devices/__init__.py | 206 ------------------------------------ app/devices/api.py | 203 +++++++++++++++++++++++++++++++++++ app/devices/blueprint.py | 3 + app/mqtt/mqtt_client.py | 2 +- 6 files changed, 209 insertions(+), 209 deletions(-) create mode 100644 app/devices/api.py create mode 100644 app/devices/blueprint.py diff --git a/app/api/resources/device.py b/app/api/resources/device.py index ce123f5..30a3fbe 100644 --- a/app/api/resources/device.py +++ b/app/api/resources/device.py @@ -3,7 +3,7 @@ from marshmallow import Schema, fields from webargs.flaskparser import use_args from flasgger import swag_from from flask import g, request -import app.devices as devices +import app.devices.api as devices from app.api import ProtectedResource diff --git a/app/core.py b/app/core.py index d1ab8a5..df02b8e 100644 --- a/app/core.py +++ b/app/core.py @@ -28,7 +28,7 @@ def setup_blueprints(app): they are just simple python packages/modules """ from .accounts.blueprint import accounts_bp - from .devices import devices_bp + from .devices.blueprint import devices_bp from .dashboards.blueprint import dashboard_bp from .api import api_bp from .mqtt import mqtt_bp diff --git a/app/devices/__init__.py b/app/devices/__init__.py index e939f9f..e69de29 100644 --- a/app/devices/__init__.py +++ b/app/devices/__init__.py @@ -1,206 +0,0 @@ -import sys -from flask import Blueprint -from .models import Device, Recording, DeviceAssociation, DeviceType -from app.core import app - -devices_bp = Blueprint('devices', __name__) - - -# Public interface -def create_device(name, account_id, device_type=1): - """ - Tries to create device with given parameters - - :param name: Desired device name - :param device_type: Id of desired device type. - By default it is 1 (STANDARD) - :type name: string - :type device_type: int - :returns: True if device is successfully created - :rtype: Boolean - """ - device = Device(name, None, device_type) - device.save() - device_association = DeviceAssociation(device.id, account_id) - device_association.save() - - -def create_device_type(name): - """ - Tries to create device type with given parameters - - :param name: Desired device type name - :type name: string - :returns: True if device type is successfully created - :rtype: Boolean - """ - device_type = DeviceType(name) - device_type.save() - - -def set_device_configuration(device_id, configuration_json): - """ - Tries to update configuration of device with given id - - :param device_id: Id of device to change configuration - :param configuration_json: New configuration - :type device_id: int - :type configuration_json: JSON - :rtype: Boolean - """ - from app.celery_builder import send_config - device = Device.get(id=device_id) - device.configuration = configuration_json - device.save() - send_config.delay(device_id, str(configuration_json)) - - -def get_device_configuration(device_id): - """ - Tries to get configuration for device with given parameters. - - :param device_id: Id of device - :type device_id: int - :returns: Configuration of given device - :rtype: JSON Configuration - """ - return Device.get(id=device_id).configuration - - -def get_device_recordings(device_id): - """ - Tries to get device recording for device with given parameters. Raises - error on failure - - :param device_id: Id of device - :type device_id: int - :returns: List of Recordings for given device - :rtype: List of Recording - :raises: ValueError if device does not exist - """ - if not Device.exists(id=device_id): - raise ValueError("Device with id %s does not exist" % device_id) - - return Recording.get_many(device_id=device_id) - - -def get_device_recordings_filtered(device_id, record_type=None, - start_date=None, end_date=None): - """ - Tries to get device recording for device with given parameters. Raises - error on failure - - :param device_id: Id of device - :param record_type: Type of recording - :param start_date: Lower date limit - :param end_date: Upper date limit - :type device_id: int - :type record_type: int - :type start_date: Date (string: %d-%m-%Y) - :type end_date: Date (string: %d-%m-%Y) - :returns: List of Recordings for given filters - :rtype: List of Recording - :raises: ValueError if device does not exist - """ - if not Device.exists(id=device_id): - raise ValueError("Device with id %s does not exist" % device_id) - - return Recording.get_many_filtered(device_id, record_type, - start_date, end_date) - - -def get_device(device_id): - """ - Tries to get device with given parameters. Raises error on failure - - :param device_id: Id of device - :type device_id: int - :returns: Requested device - :rtype: Device - """ - return Device.get(id=device_id) - - -def can_user_access_device(account_id, device_id): - """ - Checks if user with given account_id can access device with given device_id - - :param account_id: Id of account - :param device_id: Id of device - :type account_id: int - :type device_id: int - :returns: true if device is accessible by this account, false otherwise - :rtype: Boolean - """ - return len(DeviceAssociation.get_many(account_id=account_id, - device_id=device_id)) > 0 - - -def get_device_type(device_type_id): - """ - Tries to get device type with given parameters. Raises error on failure - - :param device_type_id: Id of device type - :type device_type_id: int - :returns: Requested device type - :rtype: DeviceType - """ - return DeviceType.get(id=device_type_id) - - -def delete_device(device_id): - """ - Tries to delete device with given parameters. Does not raise errors - """ - Device.get(id=device_id).delete() - - -def get_devices(account_id): - """ - Tries to get all devices associated to account. Raises error on - failure - - :returns: List of Devices associated to this account - :rtype: List of Devices - """ - return Device.get_many_for_user(account_id) - - -def get_device_types(): - """ - Tries to get all device types. Raises error on failure - - :returns: List of device types - :rtype: List of DeviceTypes - """ - return DeviceType.get_many() - - -def create_recording(device_id, raw_json): - """ - Tries to create recording with given parameters. Raises error on failure - - :param device_id: Id of device - :type device_id: int - :param raw_json: Raw json received - :type raw_json: json - :raises: ValueError if parsing fails or device does not exist - """ - def parse_raw_json_recording(device_id, json_msg): - try: - return Recording(device_id=device_id, - record_type=json_msg["record_type"], - record_value=json_msg["record_value"], - recorded_at=json_msg["recorded_at"], - raw_json=json_msg) - except KeyError: - error_type, error_instance, traceback = sys.exc_info() - raise ValueError("JSON parsing failed! Key error: " - + str(error_instance)) - - if not Device.exists(id=device_id): - raise ValueError("Device does not exist!") - - recording = parse_raw_json_recording(device_id, raw_json) - with app.app_context(): - recording.save() diff --git a/app/devices/api.py b/app/devices/api.py new file mode 100644 index 0000000..0e05819 --- /dev/null +++ b/app/devices/api.py @@ -0,0 +1,203 @@ +import sys +from .models import Device, Recording, DeviceAssociation, DeviceType +from app.core import app + + +# Public interface +def create_device(name, account_id, device_type=1): + """ + Tries to create device with given parameters + + :param name: Desired device name + :param device_type: Id of desired device type. + By default it is 1 (STANDARD) + :type name: string + :type device_type: int + :returns: True if device is successfully created + :rtype: Boolean + """ + device = Device(name, None, device_type) + device.save() + device_association = DeviceAssociation(device.id, account_id) + device_association.save() + + +def create_device_type(name): + """ + Tries to create device type with given parameters + + :param name: Desired device type name + :type name: string + :returns: True if device type is successfully created + :rtype: Boolean + """ + device_type = DeviceType(name) + device_type.save() + + +def set_device_configuration(device_id, configuration_json): + """ + Tries to update configuration of device with given id + + :param device_id: Id of device to change configuration + :param configuration_json: New configuration + :type device_id: int + :type configuration_json: JSON + :rtype: Boolean + """ + from app.celery_builder import send_config + device = Device.get(id=device_id) + device.configuration = configuration_json + device.save() + send_config.delay(device_id, str(configuration_json)) + + +def get_device_configuration(device_id): + """ + Tries to get configuration for device with given parameters. + + :param device_id: Id of device + :type device_id: int + :returns: Configuration of given device + :rtype: JSON Configuration + """ + return Device.get(id=device_id).configuration + + +def get_device_recordings(device_id): + """ + Tries to get device recording for device with given parameters. Raises + error on failure + + :param device_id: Id of device + :type device_id: int + :returns: List of Recordings for given device + :rtype: List of Recording + :raises: ValueError if device does not exist + """ + if not Device.exists(id=device_id): + raise ValueError("Device with id %s does not exist" % device_id) + + return Recording.get_many(device_id=device_id) + + +def get_device_recordings_filtered(device_id, record_type=None, + start_date=None, end_date=None): + """ + Tries to get device recording for device with given parameters. Raises + error on failure + + :param device_id: Id of device + :param record_type: Type of recording + :param start_date: Lower date limit + :param end_date: Upper date limit + :type device_id: int + :type record_type: int + :type start_date: Date (string: %d-%m-%Y) + :type end_date: Date (string: %d-%m-%Y) + :returns: List of Recordings for given filters + :rtype: List of Recording + :raises: ValueError if device does not exist + """ + if not Device.exists(id=device_id): + raise ValueError("Device with id %s does not exist" % device_id) + + return Recording.get_many_filtered(device_id, record_type, + start_date, end_date) + + +def get_device(device_id): + """ + Tries to get device with given parameters. Raises error on failure + + :param device_id: Id of device + :type device_id: int + :returns: Requested device + :rtype: Device + """ + return Device.get(id=device_id) + + +def can_user_access_device(account_id, device_id): + """ + Checks if user with given account_id can access device with given device_id + + :param account_id: Id of account + :param device_id: Id of device + :type account_id: int + :type device_id: int + :returns: true if device is accessible by this account, false otherwise + :rtype: Boolean + """ + return len(DeviceAssociation.get_many(account_id=account_id, + device_id=device_id)) > 0 + + +def get_device_type(device_type_id): + """ + Tries to get device type with given parameters. Raises error on failure + + :param device_type_id: Id of device type + :type device_type_id: int + :returns: Requested device type + :rtype: DeviceType + """ + return DeviceType.get(id=device_type_id) + + +def delete_device(device_id): + """ + Tries to delete device with given parameters. Does not raise errors + """ + Device.get(id=device_id).delete() + + +def get_devices(account_id): + """ + Tries to get all devices associated to account. Raises error on + failure + + :returns: List of Devices associated to this account + :rtype: List of Devices + """ + return Device.get_many_for_user(account_id) + + +def get_device_types(): + """ + Tries to get all device types. Raises error on failure + + :returns: List of device types + :rtype: List of DeviceTypes + """ + return DeviceType.get_many() + + +def create_recording(device_id, raw_json): + """ + Tries to create recording with given parameters. Raises error on failure + + :param device_id: Id of device + :type device_id: int + :param raw_json: Raw json received + :type raw_json: json + :raises: ValueError if parsing fails or device does not exist + """ + def parse_raw_json_recording(device_id, json_msg): + try: + return Recording(device_id=device_id, + record_type=json_msg["record_type"], + record_value=json_msg["record_value"], + recorded_at=json_msg["recorded_at"], + raw_json=json_msg) + except KeyError: + error_type, error_instance, traceback = sys.exc_info() + raise ValueError("JSON parsing failed! Key error: " + + str(error_instance)) + + if not Device.exists(id=device_id): + raise ValueError("Device does not exist!") + + recording = parse_raw_json_recording(device_id, raw_json) + with app.app_context(): + recording.save() diff --git a/app/devices/blueprint.py b/app/devices/blueprint.py new file mode 100644 index 0000000..52e89e4 --- /dev/null +++ b/app/devices/blueprint.py @@ -0,0 +1,3 @@ +from flask import Blueprint + +devices_bp = Blueprint('devices', __name__) diff --git a/app/mqtt/mqtt_client.py b/app/mqtt/mqtt_client.py index 283e01a..697d3b6 100644 --- a/app/mqtt/mqtt_client.py +++ b/app/mqtt/mqtt_client.py @@ -1,7 +1,7 @@ import sys import json from flask_mqtt import Mqtt -import app.devices as devices +import app.devices.api as devices class MqttClient: From b83a6cf70e7e67bfed7192fba9c854e047c541c3 Mon Sep 17 00:00:00 2001 From: esensar Date: Sat, 6 Oct 2018 13:54:49 +0200 Subject: [PATCH 4/7] Move code from mqtt __init__.py into separate modules --- app/core.py | 2 +- app/mqtt/__init__.py | 19 ------------------- app/mqtt/blueprint.py | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 app/mqtt/blueprint.py diff --git a/app/core.py b/app/core.py index df02b8e..03a41bb 100644 --- a/app/core.py +++ b/app/core.py @@ -31,7 +31,7 @@ def setup_blueprints(app): from .devices.blueprint import devices_bp from .dashboards.blueprint import dashboard_bp from .api import api_bp - from .mqtt import mqtt_bp + from .mqtt.blueprint import mqtt_bp app.register_blueprint(accounts_bp) app.register_blueprint(devices_bp) diff --git a/app/mqtt/__init__.py b/app/mqtt/__init__.py index f881c9b..e69de29 100644 --- a/app/mqtt/__init__.py +++ b/app/mqtt/__init__.py @@ -1,19 +0,0 @@ -import atexit -from flask import Blueprint -from .mqtt_client import MqttClient - -mqtt_bp = Blueprint('mqtt', __name__) - - -# Setup -@mqtt_bp.record -def __on_blueprint_setup(setup_state): - MqttClient.setup(setup_state.app) - - -# When app dies, stop mqtt connection -def on_stop(): - MqttClient.tear_down() - - -atexit.register(on_stop) diff --git a/app/mqtt/blueprint.py b/app/mqtt/blueprint.py new file mode 100644 index 0000000..f881c9b --- /dev/null +++ b/app/mqtt/blueprint.py @@ -0,0 +1,19 @@ +import atexit +from flask import Blueprint +from .mqtt_client import MqttClient + +mqtt_bp = Blueprint('mqtt', __name__) + + +# Setup +@mqtt_bp.record +def __on_blueprint_setup(setup_state): + MqttClient.setup(setup_state.app) + + +# When app dies, stop mqtt connection +def on_stop(): + MqttClient.tear_down() + + +atexit.register(on_stop) From f63137cb39f82ff3f7d5b5b94c5a21879f74a85b Mon Sep 17 00:00:00 2001 From: esensar Date: Sat, 6 Oct 2018 14:07:40 +0200 Subject: [PATCH 5/7] Remove code from api __init__.py into separate modules --- app/api/__init__.py | 106 ------------------------------- app/api/auth_protection.py | 32 ++++++++++ app/api/blueprint.py | 60 +++++++++++++++++ app/api/permission_protection.py | 19 ++++++ app/api/resources/account.py | 3 +- app/api/resources/dashboard.py | 2 +- app/api/resources/device.py | 2 +- app/api/resources/token.py | 2 +- app/core.py | 2 +- 9 files changed, 117 insertions(+), 111 deletions(-) create mode 100644 app/api/auth_protection.py create mode 100644 app/api/blueprint.py create mode 100644 app/api/permission_protection.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 0b077dc..e69de29 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -1,106 +0,0 @@ -import sys -from flask import Blueprint, request, g -from flask_restful import Api, Resource, abort -from functools import wraps -from marshmallow import ValidationError -from app.accounts.api import validate_token - - -api_bp = Blueprint('api', __name__) -api = Api(api_bp) - - -def protected(func): - @wraps(func) - def protected_function(*args, **kwargs): - try: - token = request.headers['Authorization'] or None - - if not token: - abort(401, message='Unauthorized', status='error') - - g.current_account = validate_token(token.replace("Bearer ", "")) - if not g.current_account: - abort(401, message='Unauthorized', status='error') - except Exception: - error_type, error_instance, traceback = sys.exc_info() - print(str(error_type)) - print(str(error_instance)) - abort(401, message='Unauthorized', status='error') - - return func(*args, **kwargs) - - return protected_function - - -def requires_permission(permission, action_name='Action'): - def requires_permission_decorator(func): - @wraps(func) - def permission_protected_function(*args, **kwargs): - if permission not in g.current_account.role.permissions: - abort(403, - message=(action_name+' is not allowed'), - status='error') - - return func(*args, **kwargs) - - return permission_protected_function - - return requires_permission_decorator - - -class ProtectedResource(Resource): - method_decorators = [protected] - - -def add_resources(): - from .resources.account import (AccountResource, - AccountListResource, - AccountRoleResource, - RoleResource, - RolesResource) - from .resources.token import TokenResource, ValidateTokenResource - from .resources.device import (DeviceResource, - DeviceRecordingResource, - DeviceListResource, - DeviceTypeResource, - DeviceTypeListResource, - DeviceConfigurationResource) - from .resources.dashboard import DashboardResource, DashboardListResource - - api.add_resource(AccountResource, '/v1/accounts/') - api.add_resource(AccountListResource, '/v1/accounts') - api.add_resource(AccountRoleResource, '/v1/accounts//role') - api.add_resource(RoleResource, '/v1/roles/') - api.add_resource(RolesResource, '/v1/roles') - api.add_resource(TokenResource, '/v1/token') - api.add_resource(ValidateTokenResource, '/v1/token/validate') - api.add_resource(DeviceResource, '/v1/devices/') - api.add_resource(DeviceRecordingResource, - '/v1/devices//recordings') - api.add_resource(DeviceListResource, '/v1/devices') - api.add_resource(DeviceTypeResource, - '/v1/devices/types/') - api.add_resource(DeviceTypeListResource, '/v1/devices/types') - api.add_resource(DeviceConfigurationResource, - '/v1/devices//configuration') - api.add_resource(DashboardListResource, '/v1/dashboards') - api.add_resource(DashboardResource, - '/v1/dashboards/') - - -add_resources() - - -@api_bp.errorhandler(ValidationError) -@api_bp.errorhandler(422) -def handle_validation_error(e): - return {'status': 'error', 'message': str(e)}, 422 - - -@api_bp.errorhandler(Exception) -def handle_unknown_errors(e): - return ({ - 'status': 'failed', - 'message': 'Unknown error has occurred! ({0})'.format(str(e)) - }, 500) diff --git a/app/api/auth_protection.py b/app/api/auth_protection.py new file mode 100644 index 0000000..48459d2 --- /dev/null +++ b/app/api/auth_protection.py @@ -0,0 +1,32 @@ +import sys +from functools import wraps +from flask import request, g +from flask_restful import Resource, abort +from app.accounts.api import validate_token + + +def protected(func): + @wraps(func) + def protected_function(*args, **kwargs): + try: + token = request.headers['Authorization'] or None + + if not token: + abort(401, message='Unauthorized', status='error') + + g.current_account = validate_token(token.replace("Bearer ", "")) + if not g.current_account: + abort(401, message='Unauthorized', status='error') + except Exception: + error_type, error_instance, traceback = sys.exc_info() + print(str(error_type)) + print(str(error_instance)) + abort(401, message='Unauthorized', status='error') + + return func(*args, **kwargs) + + return protected_function + + +class ProtectedResource(Resource): + method_decorators = [protected] diff --git a/app/api/blueprint.py b/app/api/blueprint.py new file mode 100644 index 0000000..969abc5 --- /dev/null +++ b/app/api/blueprint.py @@ -0,0 +1,60 @@ +from flask_restful import Api +from marshmallow import ValidationError +from flask import Blueprint + + +api_bp = Blueprint('api', __name__) +api = Api(api_bp) + + +def add_resources(): + from .resources.account import (AccountResource, + AccountListResource, + AccountRoleResource, + RoleResource, + RolesResource) + from .resources.token import TokenResource, ValidateTokenResource + from .resources.device import (DeviceResource, + DeviceRecordingResource, + DeviceListResource, + DeviceTypeResource, + DeviceTypeListResource, + DeviceConfigurationResource) + from .resources.dashboard import DashboardResource, DashboardListResource + + api.add_resource(AccountResource, '/v1/accounts/') + api.add_resource(AccountListResource, '/v1/accounts') + api.add_resource(AccountRoleResource, '/v1/accounts//role') + api.add_resource(RoleResource, '/v1/roles/') + api.add_resource(RolesResource, '/v1/roles') + api.add_resource(TokenResource, '/v1/token') + api.add_resource(ValidateTokenResource, '/v1/token/validate') + api.add_resource(DeviceResource, '/v1/devices/') + api.add_resource(DeviceRecordingResource, + '/v1/devices//recordings') + api.add_resource(DeviceListResource, '/v1/devices') + api.add_resource(DeviceTypeResource, + '/v1/devices/types/') + api.add_resource(DeviceTypeListResource, '/v1/devices/types') + api.add_resource(DeviceConfigurationResource, + '/v1/devices//configuration') + api.add_resource(DashboardListResource, '/v1/dashboards') + api.add_resource(DashboardResource, + '/v1/dashboards/') + + +add_resources() + + +@api_bp.errorhandler(ValidationError) +@api_bp.errorhandler(422) +def handle_validation_error(e): + return {'status': 'error', 'message': str(e)}, 422 + + +@api_bp.errorhandler(Exception) +def handle_unknown_errors(e): + return ({ + 'status': 'failed', + 'message': 'Unknown error has occurred! ({0})'.format(str(e)) + }, 500) diff --git a/app/api/permission_protection.py b/app/api/permission_protection.py new file mode 100644 index 0000000..66d3207 --- /dev/null +++ b/app/api/permission_protection.py @@ -0,0 +1,19 @@ +from flask import g +from flask_restful import abort +from functools import wraps + + +def requires_permission(permission, action_name='Action'): + def requires_permission_decorator(func): + @wraps(func) + def permission_protected_function(*args, **kwargs): + if permission not in g.current_account.role.permissions: + abort(403, + message=(action_name+' is not allowed'), + status='error') + + return func(*args, **kwargs) + + return permission_protected_function + + return requires_permission_decorator diff --git a/app/api/resources/account.py b/app/api/resources/account.py index 8fbe961..828774e 100644 --- a/app/api/resources/account.py +++ b/app/api/resources/account.py @@ -4,7 +4,8 @@ from marshmallow import Schema, fields from webargs.flaskparser import use_args from flasgger import swag_from import app.accounts.api as accounts -from app.api import ProtectedResource, requires_permission +from app.api.auth_protection import ProtectedResource +from app.api.permission_protection import requires_permission class UserSchema(Schema): diff --git a/app/api/resources/dashboard.py b/app/api/resources/dashboard.py index a046fd2..ae3652f 100644 --- a/app/api/resources/dashboard.py +++ b/app/api/resources/dashboard.py @@ -4,7 +4,7 @@ from marshmallow import Schema, fields from webargs.flaskparser import use_args from flasgger import swag_from import app.dashboards.api as dashboard -from app.api import ProtectedResource +from app.api.auth_protection import ProtectedResource class DashboardSchema(Schema): diff --git a/app/api/resources/device.py b/app/api/resources/device.py index 30a3fbe..1c6d8e1 100644 --- a/app/api/resources/device.py +++ b/app/api/resources/device.py @@ -4,7 +4,7 @@ from webargs.flaskparser import use_args from flasgger import swag_from from flask import g, request import app.devices.api as devices -from app.api import ProtectedResource +from app.api.auth_protection import ProtectedResource class DeviceTypeSchema(Schema): diff --git a/app/api/resources/token.py b/app/api/resources/token.py index 26d80bf..16d3b2e 100644 --- a/app/api/resources/token.py +++ b/app/api/resources/token.py @@ -2,7 +2,7 @@ from flask_restful import Resource, abort from webargs import fields from webargs.flaskparser import use_args from flasgger import swag_from -from app.api import ProtectedResource +from app.api.auth_protection import ProtectedResource import app.accounts.api as accounts diff --git a/app/core.py b/app/core.py index 03a41bb..eaa411e 100644 --- a/app/core.py +++ b/app/core.py @@ -30,7 +30,7 @@ def setup_blueprints(app): from .accounts.blueprint import accounts_bp from .devices.blueprint import devices_bp from .dashboards.blueprint import dashboard_bp - from .api import api_bp + from .api.blueprint import api_bp from .mqtt.blueprint import mqtt_bp app.register_blueprint(accounts_bp) From 3c0644357b4a0a83cf3efceb4f48829db87a8e47 Mon Sep 17 00:00:00 2001 From: esensar Date: Sat, 6 Oct 2018 14:18:04 +0200 Subject: [PATCH 6/7] Add list of possible permissions --- app/api/permission_protection.py | 13 +++++++++++++ app/api/resources/account.py | 10 ++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/api/permission_protection.py b/app/api/permission_protection.py index 66d3207..220e9e2 100644 --- a/app/api/permission_protection.py +++ b/app/api/permission_protection.py @@ -3,7 +3,20 @@ from flask_restful import abort from functools import wraps +valid_permissions = [ + 'CREATE_DEVICE_TYPE', + 'CREATE_ROLE', + 'ASSIGN_ROLE', + 'CREATE_DEVICE', + 'CREATE_DASHBOARD', + 'READ_DEVICE_TYPES', + 'READ_ROLES'] + + def requires_permission(permission, action_name='Action'): + if permission not in valid_permissions: + raise ValueError('Permission ' + str(permission) + ' does not exist!') + def requires_permission_decorator(func): @wraps(func) def permission_protected_function(*args, **kwargs): diff --git a/app/api/resources/account.py b/app/api/resources/account.py index 828774e..9868f47 100644 --- a/app/api/resources/account.py +++ b/app/api/resources/account.py @@ -5,7 +5,8 @@ from webargs.flaskparser import use_args from flasgger import swag_from import app.accounts.api as accounts from app.api.auth_protection import ProtectedResource -from app.api.permission_protection import requires_permission +from app.api.permission_protection import (requires_permission, + valid_permissions) class UserSchema(Schema): @@ -18,11 +19,16 @@ class RoleUpdateSchema(Schema): role_id = fields.Integer(required=True, load_only=True, location='json') +def validate_role_permissions(permissions_list): + return set(permissions_list).issubset(valid_permissions) + + class RoleSchema(Schema): id = fields.Integer(required=True, location='json') display_name = fields.String(required=True, location='json') permissions = fields.List(fields.String, required=True, - location='json', many=True) + location='json', many=True, + validate=validate_role_permissions) class RoleWrapperSchema(Schema): From d52adf0f39cd6fe4409c7a1393a14418c99b1ad7 Mon Sep 17 00:00:00 2001 From: esensar Date: Sat, 6 Oct 2018 14:22:49 +0200 Subject: [PATCH 7/7] Restore server MQTT_CLIENT_ID --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 55e4c11..f9e0237 100644 --- a/config.py +++ b/config.py @@ -28,7 +28,7 @@ CSRF_SESSION_KEY = "secret" SECRET_KEY = "?['Z(Z\x83Y \x06T\x12\x96<\xff\x12\xe0\x1b\xd1J\xe0\xd9ld" # MQTT configuration -MQTT_CLIENT_ID = 'final-iot-backend-server-local2' +MQTT_CLIENT_ID = 'final-iot-backend-server' MQTT_BROKER_URL = 'broker.hivemq.com' MQTT_BROKER_PORT = 1883 MQTT_USERNAME = 'user'