Merged in feature/device_secrets (pull request #32)

Feature/device secrets
develop
Ensar Sarajcic 2018-10-29 21:58:16 +00:00
commit e8e19d9503
8 changed files with 154 additions and 10 deletions

View File

@ -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')

View File

@ -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

View File

@ -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/DeviceWithConfig'

View File

@ -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()

View File

@ -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):
"""

View File

@ -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)

View File

@ -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:

View 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 ###