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
-
+
+
- | # |
- Site |
- Username |
- Client ID |
- Topic |
+ Site |
+ Username |
+ Client ID |
+ Topic |
+ Role |
+ Actions |
{% for user in users %}
- | {{ 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 %}
+ |
{% 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
+
+
+
+ | Permission |
+ Description |
+
+
+
+
+ {{#each permissions}}
+
+ | {{ this.permission }} |
+ {{ this.description }} |
+
+ {{/each}}
+
+
+{% 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"})