From c13049a30f18f0b345337dcf20b1ec22961e9b07 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 26 Jan 2025 19:59:16 +0100 Subject: [PATCH] command check - fix subscriber --- .env | 7 ++ .vscode/launch.json | 3 +- pyproject.toml | 1 + uv.lock | 23 ++++++ wallet_api/__init__.py | 45 +++++++++++ wallet_api/admin.py | 5 +- wallet_api/management/__init__.py | 0 wallet_api/management/commands/__init__.py | 0 wallet_api/management/commands/_private.py | 0 .../commands/check_data_integrity.py | 54 +++++++++++++ wallet_api/migrations/0001_initial.py | 6 +- wallet_api/models.py | 4 + wallet_api/mqtt.py | 32 ++++++++ wallet_api/templates/base.html | 10 +-- wallet_api/templates/wallet_api/add_user.html | 48 ++++++++++++ .../templates/wallet_api/edit_user.html | 62 +++++++++++++++ .../templates/wallet_api/list_users.html | 59 +++++++++++--- .../templates/wallet_api/role_info.html | 20 +++++ wallet_api/urls.py | 8 +- wallet_api/views.py | 76 ++++++++++++++----- 20 files changed, 424 insertions(+), 39 deletions(-) create mode 100644 wallet_api/management/__init__.py create mode 100644 wallet_api/management/commands/__init__.py create mode 100644 wallet_api/management/commands/_private.py create mode 100644 wallet_api/management/commands/check_data_integrity.py create mode 100644 wallet_api/mqtt.py create mode 100644 wallet_api/templates/wallet_api/add_user.html create mode 100644 wallet_api/templates/wallet_api/edit_user.html create mode 100644 wallet_api/templates/wallet_api/role_info.html diff --git a/.env b/.env index 79dd8a6..1b03cd8 100644 --- a/.env +++ b/.env @@ -3,3 +3,10 @@ DB_USER=postgres DB_PASSWORD=BatManu#171017 DB_HOST=10.211.114.101 DB_PORT=5432 +MQTT_HOST=10.211.114.214 +MQTT_PORT=1883 +MQTT_KEEPALIVE=60 +MQTT_USER=alex +MQTT_PASSWORD=batt1l0 +MQTT_DS_TOPIC="$CONTROL/dynamic-security/v1" +MQTT_DS_RESP_TOPIC="$CONTROL/dynamic-security/v1/response" diff --git a/.vscode/launch.json b/.vscode/launch.json index 40ea7e0..b6e6120 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,8 @@ "type": "debugpy", "request": "launch", "args": [ - "runserver" + "runserver", + "--noreload", ], "django": true, "autoStartBrowser": false, diff --git a/pyproject.toml b/pyproject.toml index 0845553..7f730ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "cryptography>=44.0.0", "django>=5.1.4", "django-htmx>=1.21.0", + "gunicorn>=23.0.0", "paho-mqtt>=2.1.0", "psycopg2-binary>=2.9.10", "python-dotenv>=1.0.1", diff --git a/uv.lock b/uv.lock index 4ab8c74..f4b97fd 100644 --- a/uv.lock +++ b/uv.lock @@ -140,6 +140,7 @@ dependencies = [ { name = "cryptography" }, { name = "django" }, { name = "django-htmx" }, + { name = "gunicorn" }, { name = "paho-mqtt" }, { name = "psycopg2-binary" }, { name = "python-dotenv" }, @@ -151,11 +152,33 @@ requires-dist = [ { name = "cryptography", specifier = ">=44.0.0" }, { name = "django", specifier = ">=5.1.4" }, { name = "django-htmx", specifier = ">=1.21.0" }, + { name = "gunicorn", specifier = ">=23.0.0" }, { name = "paho-mqtt", specifier = ">=2.1.0" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "python-dotenv", specifier = ">=1.0.1" }, ] +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + [[package]] name = "paho-mqtt" version = "2.1.0" diff --git a/wallet_api/__init__.py b/wallet_api/__init__.py index e69de29..22e855c 100644 --- a/wallet_api/__init__.py +++ b/wallet_api/__init__.py @@ -0,0 +1,45 @@ +from queue import Queue +from dotenv import dotenv_values +import paho.mqtt.client as mqtt +import json +import os +print(f"__init__.py eseguito in PID {os.getpid()}") + +config = dotenv_values(".env") + +listClients_queue = Queue() +listRoles_queue = Queue() +getClient_queue = Queue() +changeClient_queue = Queue() + +command_queue_map = { + "listClients": listClients_queue, + "listRoles": listRoles_queue, + "getClient": getClient_queue, + "createClient": changeClient_queue, + "deleteClient": changeClient_queue, + "enableClient": changeClient_queue, + "disableClient": changeClient_queue +} + +def on_message(client, userdata, msg): + msg_json = json.loads(msg.payload.decode("utf-8")) + command = msg_json['responses'][0]['command'] + print(f"Received message: {msg.payload.decode("utf-8")} - Command: {command}") + + if command in command_queue_map: + command_queue_map[command].put(msg.payload.decode("utf-8")) + +def on_connect(client, userdata, flags, rc, properties): + if rc == 0: + print('Connected successfully. Properties:', properties) + client.subscribe(config['MQTT_DS_RESP_TOPIC']) + else: + print('Bad connection. Code:', rc) + +client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5) +client.on_connect = on_connect +client.on_message = on_message +client.username_pw_set(config['MQTT_USER'], config['MQTT_PASSWORD']) +client.connect(config['MQTT_HOST'], int(config['MQTT_PORT']), int(config['MQTT_KEEPALIVE'])) +client.loop_start() \ No newline at end of file diff --git a/wallet_api/admin.py b/wallet_api/admin.py index 04dcd08..17bcf27 100644 --- a/wallet_api/admin.py +++ b/wallet_api/admin.py @@ -2,4 +2,7 @@ from django.contrib import admin from .models import PasswordEntry # Register your models here. -admin.site.register(PasswordEntry) +class PasswordEntryAdmin(admin.ModelAdmin): + prepopulated_fields = {"slug": ("site", "username")} + +admin.site.register(PasswordEntry, PasswordEntryAdmin) diff --git a/wallet_api/management/__init__.py b/wallet_api/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wallet_api/management/commands/__init__.py b/wallet_api/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wallet_api/management/commands/_private.py b/wallet_api/management/commands/_private.py new file mode 100644 index 0000000..e69de29 diff --git a/wallet_api/management/commands/check_data_integrity.py b/wallet_api/management/commands/check_data_integrity.py new file mode 100644 index 0000000..6dd59d7 --- /dev/null +++ b/wallet_api/management/commands/check_data_integrity.py @@ -0,0 +1,54 @@ +from django.core.management.base import BaseCommand, CommandError +from wallet_api.models import PasswordEntry +import json + +class Command(BaseCommand): + help = "Check data alignament across DB and Dynamic Security mosquitto file" + + def add_arguments(self, parser): + parser.add_argument( + "dynsecjson", + nargs="+", + type=str, + help="Mosquitto dynamic security json file", + ) + + def handle(self, *args, **options): + for dynsec in options["dynsecjson"]: + try: + with open(dynsec, "r") as dynsecconfig: + config = json.load(dynsecconfig) + for client in config['clients']: + if client['username'] == 'admin': + continue + user_name = client['username'] + try: + password_entry = PasswordEntry.objects.get(username=user_name) + if password_entry: + self.stdout.write(self.style.SUCCESS(f'username {user_name} exists in the database.')) + else: + self.stdout.write(self.style.WARNING(f'Username {user_name} does not exist in the database.')) + except PasswordEntry.DoesNotExist: + self.stdout.write(self.style.WARNING(f'Username {user_name} does not exist in the database.')) + + for role in config['roles']: + if role['rolename'] == 'admin': + continue + role_name = role['rolename'] + try: + password_entry = PasswordEntry.objects.get(role=role_name) + if password_entry: + self.stdout.write(self.style.SUCCESS(f'Role {role_name} exists in the database.')) + acls_db = password_entry.acls + acls_json = role.get('acls', []) + for acltype_json in acls_json: + if acltype_json['acltype'] not in [d['acltype'] for d in acls_db]: + self.stdout.write(self.style.WARNING(f'ACL type {acltype_json["acltype"]} for role {role_name} does not exist in the database.')) + else: + self.stdout.write(self.style.WARNING(f'Role {role_name} does not exist in the database.')) + except PasswordEntry.DoesNotExist: + self.stdout.write(self.style.WARNING(f'Role {role_name} does not exist in the database.')) + except Exception as e: + self.stdout.write(self.style.ERROR(f'Check data alignament across DB and Dynamic Security mosquitto file {dynsec} - {e}')) + + diff --git a/wallet_api/migrations/0001_initial.py b/wallet_api/migrations/0001_initial.py index 9f1d087..37f16aa 100644 --- a/wallet_api/migrations/0001_initial.py +++ b/wallet_api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2025-01-06 10:24 +# Generated by Django 5.1.4 on 2025-01-15 18:24 from django.db import migrations, models @@ -28,6 +28,10 @@ class Migration(migrations.Migration): ('password', models.TextField()), ('client_id', models.CharField(max_length=255)), ('topic', models.CharField(max_length=255)), + ('role', models.CharField(max_length=255)), + ('acls', models.JSONField()), + ('status', models.CharField(max_length=255)), + ('slug', models.SlugField(default='', null=True)), ('created_at', models.DateTimeField(auto_now_add=True)), ], options={ diff --git a/wallet_api/models.py b/wallet_api/models.py index f0a3108..63a8cab 100644 --- a/wallet_api/models.py +++ b/wallet_api/models.py @@ -8,6 +8,10 @@ class PasswordEntry(models.Model): password = models.TextField() client_id = models.CharField(max_length=255) topic = models.CharField(max_length=255) + role = models.CharField(max_length=255) + acls = models.JSONField() + status = models.CharField(max_length=255) + slug = models.SlugField(default="", null=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: diff --git a/wallet_api/mqtt.py b/wallet_api/mqtt.py new file mode 100644 index 0000000..760ea0e --- /dev/null +++ b/wallet_api/mqtt.py @@ -0,0 +1,32 @@ +import paho.mqtt.client as mqtt + +MQTT_SERVER="10.211.114.214" +MQTT_PORT=1883 +MQTT_KEEPALIVE=60 +MQTT_USER="pippo" +MQTT_PASSWORD="batt1l0" +MQTT_DS_TOPIC="$CONTROL/dynamic-security/v1" + +def on_connect(mqtt_client, userdata, flags, rc, properties): + if rc == 0: + print('Connected successfully. Properties:', properties) + mqtt_client.subscribe(MQTT_TOPIC) + else: + print('Bad connection. Code:', rc) + +def on_disconnect(mqtt_client, obj, flags, rc, properties): + print('disconnected. Code:', rc) + +def on_message(mqtt_client, userdata, msg): + print(f'Received message on topic: {msg.topic} with payload: {msg.payload}') + +client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5) +client.on_connect = on_connect +client.on_disconnect = on_disconnect +client.on_message = on_message +client.username_pw_set(MQTT_USER, MQTT_PASSWORD) +client.connect( + host=MQTT_SERVER, + port=MQTT_PORT, + keepalive=MQTT_KEEPALIVE +) \ No newline at end of file diff --git a/wallet_api/templates/base.html b/wallet_api/templates/base.html index 58facd9..56a5397 100644 --- a/wallet_api/templates/base.html +++ b/wallet_api/templates/base.html @@ -7,9 +7,9 @@ @@ -22,15 +22,15 @@ {% block container %} -
+
{% block content %} {% endblock content %}
{% endblock container %} {% block footer %} {% endblock footer %} diff --git a/wallet_api/templates/wallet_api/add_user.html b/wallet_api/templates/wallet_api/add_user.html new file mode 100644 index 0000000..d93b3e1 --- /dev/null +++ b/wallet_api/templates/wallet_api/add_user.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} {% load static %} {% block content %} +

Add User

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+{% endblock content %} diff --git a/wallet_api/templates/wallet_api/edit_user.html b/wallet_api/templates/wallet_api/edit_user.html new file mode 100644 index 0000000..48469ba --- /dev/null +++ b/wallet_api/templates/wallet_api/edit_user.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} {% load static %} {% block content %} +

Edit User

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+{% endblock content %} diff --git a/wallet_api/templates/wallet_api/list_users.html b/wallet_api/templates/wallet_api/list_users.html index e4bf839..0d2d3c6 100644 --- a/wallet_api/templates/wallet_api/list_users.html +++ b/wallet_api/templates/wallet_api/list_users.html @@ -1,23 +1,60 @@ {% extends "base.html" %} {% load static %} {% block content %}

List Mosquitto Users

- + +
- - - - - + + + + + + {% for user in users %} - - - - - + + + + + + {% empty %} No users diff --git a/wallet_api/templates/wallet_api/role_info.html b/wallet_api/templates/wallet_api/role_info.html new file mode 100644 index 0000000..cd5de40 --- /dev/null +++ b/wallet_api/templates/wallet_api/role_info.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} {% load static %} {% block content %} +

Role Permissions

+
#SiteUsernameClient IDTopicSiteUsernameClient IDTopicRoleActions
{{ user.id }}{{ user.site }}{{ user.username }}{{ user.client_id }}{{ user.topic }}{{user.site}} + {{user.username}} + {{user.client_id}}{{user.topic}} + {{user.role}} + + {% if user.status == "enabled" %} + + {% else %} + + {% endif %} +
+ + + + + + + + + {{#each permissions}} + + + + + {{/each}} + +
PermissionDescription
{{ this.permission }}{{ this.description }}
+{% endblock content %} diff --git a/wallet_api/urls.py b/wallet_api/urls.py index f9d017f..148ca55 100644 --- a/wallet_api/urls.py +++ b/wallet_api/urls.py @@ -2,9 +2,13 @@ from django.urls import path from . import views urlpatterns = [ - path('add/', views.add_password_api, name='add_password'), + path('add/', views.add_password_api, name='add_user'), + path('disable/', views.disable_password_api, name='disable_user'), path('get/', views.get_password_api, name='get_password'), - path('delete/', views.delete_password_api, name='delete_password'), path('list/', views.list_sites_api, name='list_sites'), path('user/list/', views.list_users, name='list_users'), + path('user/add/', views.add_user, name='add_user'), + path('user/edit/', views.edit_user, name='edit_user'), + path('user/role/', views.view_role, name='view_role'), + path('publish', views.publish_message, name='publish'), ] diff --git a/wallet_api/views.py b/wallet_api/views.py index c8b9a0d..8c508e7 100644 --- a/wallet_api/views.py +++ b/wallet_api/views.py @@ -3,14 +3,43 @@ from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from .models import PasswordEntry, MasterHash from .utils import authenticate, derive_key, encrypt_password, decrypt_password +from dotenv import dotenv_values import json +import threading +import paho.mqtt.client as mqtt +import paho.mqtt.publish as publish + +config = dotenv_values(".env") + +@csrf_exempt +def publish_message(request): + request_data = json.loads(request.body) + publish.single(topic=request_data['topic'], + payload=json.dumps(request_data['msg']), + hostname=config['MQTT_HOST'], + port=int(config['MQTT_PORT']), + keepalive=int(config['MQTT_KEEPALIVE']), + auth={'username': config['MQTT_USER'], 'password': config['MQTT_PASSWORD']}, + protocol=mqtt.MQTTv5) + return JsonResponse({'request_data': request_data}) def list_users(request): users = PasswordEntry.objects.all() + lock_users = threading.Lock() return render(request, 'wallet_api/list_users.html', {'users': users}) +def edit_user(request, user_id): + user = PasswordEntry.objects.filter(id=user_id).first() + return render(request, 'wallet_api/edit_user.html', {'user': user}) + +def add_user(request): + return render(request, 'wallet_api/add_user.html') + +def view_role(request, role): + return render(request, 'wallet_api/role_info.html', {'role': role }) + @csrf_exempt -def add_password_api(request): +def disable_password_api(request): if request.method == 'POST': data = json.loads(request.body) master_password = data.get('master_password') @@ -59,23 +88,6 @@ def get_password_api(request): except PasswordEntry.DoesNotExist: return JsonResponse({"error": "Sito non trovato"}, status=404) -@csrf_exempt -def delete_password_api(request): - if request.method == 'POST': - data = json.loads(request.body) - master_password = data.get('master_password') - site = data.get('site') - - if not authenticate(master_password): - return JsonResponse({"error": "Master password errata"}, status=403) - - try: - entry = PasswordEntry.objects.get(site=site) - entry.delete() - return JsonResponse({"message": "Password cancellata con successo"}) - except PasswordEntry.DoesNotExist: - return JsonResponse({"error": "Sito non trovato"}, status=404) - @csrf_exempt def list_sites_api(request): if request.method == 'POST': @@ -87,3 +99,31 @@ def list_sites_api(request): sites = PasswordEntry.objects.values_list('site', flat=True) return JsonResponse({"sites": list(sites)}) + +@csrf_exempt +def add_password_api(request): + if request.method == 'POST': + data = json.loads(request.body) + master_password = data.get('master_password') + site = data.get('site') + username = data.get('username') + password = data.get('password') + client_id = data.get('client_id') + topic = data.get('topic') + + if not authenticate(master_password): + return JsonResponse({"error": "Master password errata"}, status=403) + + key = derive_key(master_password) + encrypted_password = encrypt_password(password, key) + + PasswordEntry.objects.create( + site=site, + username=username, + password=encrypted_password, + client_id=client_id, + topic=topic, + role='', + acls='{}' + ) + return JsonResponse({"message": "Password aggiunta con successo"})