commit
12ff54d2cd
|
@ -21,7 +21,8 @@ def add_resources():
|
|||
DeviceListResource,
|
||||
DeviceTypeResource,
|
||||
DeviceTypeListResource,
|
||||
DeviceConfigurationResource)
|
||||
DeviceConfigurationResource,
|
||||
DeviceSecretResource)
|
||||
from .resources.dashboard import (DashboardResource,
|
||||
DashboardListResource,
|
||||
DashboardWidgetResource,
|
||||
|
@ -47,6 +48,8 @@ def add_resources():
|
|||
api.add_resource(DeviceTypeListResource, '/v1/devices/types')
|
||||
api.add_resource(DeviceConfigurationResource,
|
||||
'/v1/devices/<int:device_id>/configuration')
|
||||
api.add_resource(DeviceSecretResource,
|
||||
'/v1/devices/<int:device_id>/secret')
|
||||
api.add_resource(DashboardResource,
|
||||
'/v1/dashboards/<int:dashboard_id>')
|
||||
api.add_resource(DashboardListResource, '/v1/dashboards')
|
||||
|
|
|
@ -34,6 +34,11 @@ class RecordingsSchema(BaseResourceSchema):
|
|||
record_value = fields.String()
|
||||
|
||||
|
||||
class DeviceSecretSchema(BaseResourceSchema):
|
||||
device_secret = fields.String(dump_only=True)
|
||||
secret_algorithm = fields.String()
|
||||
|
||||
|
||||
def validate_device_ownership(device_id):
|
||||
if not devices.can_user_access_device(g.current_account.id, device_id):
|
||||
abort(403, message='You are not allowed to access this device',
|
||||
|
@ -127,3 +132,10 @@ class DeviceConfigurationResource(ProtectedResource):
|
|||
def get(self, device_id):
|
||||
validate_device_ownership(device_id)
|
||||
return devices.get_device_configuration(device_id), 200
|
||||
|
||||
|
||||
class DeviceSecretResource(ProtectedResource):
|
||||
@swag_from('swagger/get_device_secret_spec.yaml')
|
||||
def get(self, device_id):
|
||||
validate_device_ownership(device_id)
|
||||
return DeviceSecretSchema().dump(devices.get_device(device_id)), 200
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
Gets a device secret info
|
||||
---
|
||||
tags:
|
||||
- Device
|
||||
parameters:
|
||||
- in: path
|
||||
name: device_id
|
||||
required: true
|
||||
type: integer
|
||||
description: Id of the device
|
||||
responses:
|
||||
200:
|
||||
description: Success
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- content
|
||||
properties:
|
||||
content:
|
||||
$ref: '#/definitions/DeviceSecretInfo'
|
|
@ -1,8 +1,27 @@
|
|||
import sys
|
||||
import hmac
|
||||
import urllib.parse
|
||||
from .models import Device, Recording, DeviceAssociation, DeviceType
|
||||
from app.core import app
|
||||
|
||||
|
||||
# Private helpers
|
||||
def generate_hmac_for_message(device_id, raw_json):
|
||||
device = Device.get(id=device_id)
|
||||
raw_json_bytes = urllib.parse.urlencode(raw_json).encode('utf-8')
|
||||
return hmac.new(
|
||||
bytes(device.device_secret, 'utf-8'),
|
||||
raw_json_bytes,
|
||||
device.secret_algorithm).hexdigest()
|
||||
|
||||
|
||||
def validate_hmac_in_message(device_id, raw_json):
|
||||
hmac_value = raw_json.pop('hmac', None)
|
||||
calculated_hmac = generate_hmac_for_message(device_id, raw_json)
|
||||
if not hmac.compare_digest(hmac_value, calculated_hmac):
|
||||
raise ValueError("Bad hmac")
|
||||
|
||||
|
||||
# Public interface
|
||||
def create_device(name, account_id, device_type=1):
|
||||
"""
|
||||
|
@ -51,6 +70,9 @@ def set_device_configuration(device_id, configuration_json):
|
|||
device = Device.get(id=device_id)
|
||||
device.configuration = configuration_json
|
||||
device.save()
|
||||
configuration_json['hmac'] = generate_hmac_for_message(
|
||||
device_id,
|
||||
configuration_json)
|
||||
send_config.delay(device_id, str(configuration_json))
|
||||
return device
|
||||
|
||||
|
@ -178,7 +200,7 @@ def get_device_types():
|
|||
|
||||
def parse_raw_json_recording(device_id, json_msg):
|
||||
"""
|
||||
Parses raw json recording and creates Recrding object
|
||||
Parses raw json recording and creates Recording object
|
||||
|
||||
:param device_id: Id of device
|
||||
:type device_id: int
|
||||
|
@ -198,7 +220,8 @@ def parse_raw_json_recording(device_id, json_msg):
|
|||
+ str(error_instance))
|
||||
|
||||
|
||||
def create_recording_and_return(device_id, raw_json):
|
||||
def create_recording_and_return(device_id, raw_json,
|
||||
authenticated=False):
|
||||
"""
|
||||
Tries to create recording with given parameters and returns it.
|
||||
Raises error on failure
|
||||
|
@ -212,6 +235,9 @@ def create_recording_and_return(device_id, raw_json):
|
|||
if not Device.exists(id=device_id):
|
||||
raise ValueError("Device does not exist!")
|
||||
|
||||
if not authenticated:
|
||||
validate_hmac_in_message(device_id, raw_json)
|
||||
|
||||
recording = parse_raw_json_recording(device_id, raw_json)
|
||||
recording.save()
|
||||
return recording
|
||||
|
@ -230,6 +256,7 @@ def create_recording(device_id, raw_json):
|
|||
if not Device.exists(id=device_id):
|
||||
raise ValueError("Device does not exist!")
|
||||
|
||||
validate_hmac_in_message(device_id, raw_json)
|
||||
recording = parse_raw_json_recording(device_id, raw_json)
|
||||
with app.app_context():
|
||||
recording.save()
|
||||
|
|
|
@ -3,6 +3,7 @@ from datetime import datetime
|
|||
import datetime as datetime_module
|
||||
from app.core import db
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
from secrets import token_urlsafe
|
||||
|
||||
|
||||
class Recording(db.Model):
|
||||
|
@ -126,6 +127,8 @@ class Device(db.Model):
|
|||
name = db.Column(db.String, nullable=False)
|
||||
device_type_id = db.Column(db.Integer, db.ForeignKey('device_types.id'))
|
||||
device_type = db.relationship("DeviceType", foreign_keys=[device_type_id])
|
||||
device_secret = db.Column(db.String, nullable=False)
|
||||
secret_algorithm = db.Column(db.String, nullable=False)
|
||||
configuration = db.Column(JSON, nullable=True)
|
||||
|
||||
users = db.relationship("DeviceAssociation",
|
||||
|
@ -137,6 +140,8 @@ class Device(db.Model):
|
|||
self.name = name
|
||||
self.configuration = configuration
|
||||
self.device_type_id = device_type
|
||||
self.secret_algorithm = 'sha512'
|
||||
self.device_secret = token_urlsafe(32)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
|
|
|
@ -3,8 +3,7 @@ from app.celery_builder import task_builder
|
|||
from flask import current_app as app
|
||||
|
||||
|
||||
@task_builder.task()
|
||||
def send_config(device_id, config):
|
||||
def connect_and_send_mqtt_message(topic, message):
|
||||
from flask_mqtt import Mqtt, MQTT_ERR_SUCCESS
|
||||
mqtt = Mqtt(app)
|
||||
|
||||
|
@ -15,14 +14,12 @@ def send_config(device_id, config):
|
|||
@mqtt.on_connect()
|
||||
def handle_connect(client, userdata, flags, rc):
|
||||
print('MQTT worker client connected')
|
||||
print("Sending configuration to device: " + str(device_id))
|
||||
print("Configuration: " + str(config))
|
||||
topic = 'device/' + str(device_id) + '/config'
|
||||
print("Targeting topic: " + topic)
|
||||
print("Sending message: " + message)
|
||||
try:
|
||||
(result, mid) = mqtt.publish(topic, config, 2)
|
||||
(result, mid) = mqtt.publish(topic, message, 2)
|
||||
if (result == MQTT_ERR_SUCCESS):
|
||||
print("Success!!!")
|
||||
print("Successfully sent a message")
|
||||
print("Result: " + str(result))
|
||||
print("Message id: " + str(mid))
|
||||
mqtt.client.disconnect()
|
||||
|
@ -33,3 +30,11 @@ def send_config(device_id, config):
|
|||
print("Instance: " + str(error_instance))
|
||||
mqtt.client.disconnect()
|
||||
return
|
||||
|
||||
|
||||
@task_builder.task()
|
||||
def send_config(device_id, config):
|
||||
print("Sending configuration to device: " + str(device_id))
|
||||
print("Configuration: " + str(config))
|
||||
topic = 'device/' + str(device_id) + '/config'
|
||||
connect_and_send_mqtt_message(topic, config)
|
||||
|
|
|
@ -94,6 +94,16 @@ definitions:
|
|||
description: Type of chart
|
||||
example: line
|
||||
|
||||
secret:
|
||||
type: string
|
||||
description: Secret key
|
||||
example: Ranom-Key123
|
||||
|
||||
hashalgorithm:
|
||||
type: string
|
||||
description: Hashing algorithm used
|
||||
example: sha512
|
||||
|
||||
Credentials:
|
||||
type: object
|
||||
required:
|
||||
|
@ -209,6 +219,17 @@ definitions:
|
|||
configuration:
|
||||
$ref: '#/definitions/configuration'
|
||||
|
||||
DeviceSecretInfo:
|
||||
type: object
|
||||
required:
|
||||
- device_secret
|
||||
- secret_algorithm
|
||||
properties:
|
||||
device_secret:
|
||||
$ref: '#/definitions/secret'
|
||||
secret_algorithm:
|
||||
$ref: '#/definitions/hashalgorithm'
|
||||
|
||||
DeviceCreation:
|
||||
type: object
|
||||
required:
|
||||
|
|
|
@ -2,7 +2,7 @@ import os
|
|||
|
||||
# App configuration
|
||||
DEBUG = os.environ['DEBUG']
|
||||
APP_VERSION = '0.3.2'
|
||||
APP_VERSION = '0.3.3'
|
||||
|
||||
# Define the application directory
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: dad1f9b4eec2
|
||||
Revises: b5eb4a04c77e
|
||||
Create Date: 2018-10-29 20:48:49.588509
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
from secrets import token_urlsafe
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'dad1f9b4eec2'
|
||||
down_revision = 'b5eb4a04c77e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('devices', sa.Column('device_secret', sa.String(),
|
||||
nullable=True))
|
||||
op.add_column('devices', sa.Column('secret_algorithm', sa.String(),
|
||||
nullable=True))
|
||||
|
||||
devices = sa.sql.table('devices', sa.column('id', sa.Integer),
|
||||
sa.sql.column('device_secret'),
|
||||
sa.sql.column('secret_algorithm'))
|
||||
|
||||
conn = op.get_bind()
|
||||
res = conn.execute("select id from devices")
|
||||
results = res.fetchall()
|
||||
for result in results:
|
||||
op.execute(devices.update().where(devices.c.id ==
|
||||
op.inline_literal(result[0])).
|
||||
values({'device_secret': token_urlsafe(32),
|
||||
'secret_algorithm': 'sha512'})
|
||||
)
|
||||
|
||||
# Forbid nulls
|
||||
op.alter_column('devices', 'device_secret', nullable=False)
|
||||
op.alter_column('devices', 'secret_algorithm', nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('devices', 'secret_algorithm')
|
||||
op.drop_column('devices', 'device_secret')
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in New Issue