From ce5ca3bee686b9db9d63f2d358e03fdd4d32f191 Mon Sep 17 00:00:00 2001 From: Lukas Brodschelm Date: Thu, 3 Nov 2022 00:41:51 +0100 Subject: [PATCH 01/12] Refactor fab_access python --- fab_access/config.py | 9 ++ fab_access/keycloak_handler.py | 44 ++++++++ fab_access/main.py | 98 ++++++++++++++++++ fab_access/mqtt_client.py | 61 +++++++++++ main.py | 178 --------------------------------- 5 files changed, 212 insertions(+), 178 deletions(-) create mode 100644 fab_access/config.py create mode 100644 fab_access/keycloak_handler.py create mode 100644 fab_access/main.py create mode 100644 fab_access/mqtt_client.py delete mode 100644 main.py diff --git a/fab_access/config.py b/fab_access/config.py new file mode 100644 index 0000000..21908b7 --- /dev/null +++ b/fab_access/config.py @@ -0,0 +1,9 @@ +class Config: + broker = 'mosquitto' + port = 1883 + client_id = f'FabMan' + + keycloak_url = 'http://keycloak:8080/auth/' + keycloak_username = os.environ['KEYCLOAK_USER_NAME'] + keycloak_password = os.environ['KEYCLOAK_USER_PW'] + keycloak_realm = os.environ['KEYCLOAK_REALM'] diff --git a/fab_access/keycloak_handler.py b/fab_access/keycloak_handler.py new file mode 100644 index 0000000..02cb661 --- /dev/null +++ b/fab_access/keycloak_handler.py @@ -0,0 +1,44 @@ +from config import Config +from keycloak import KeycloakAdmin + + +class KeycloakHandler: + Config.keycloak_password + + @staticmethod + def login(): + KeycloakHandler.admin = KeycloakAdmin( + server_url=Config.keycloak_url, + username=Config.keycloak_username, + password=Config.keycloak_password, + realm_name=Config.keycloak_realm, + verify=True + ) + + @staticmethod + def get_user_by_card_id(card_id): + users = KeycloakHandler.admin.get_users( + { 'attributes': { 'FabCard': card_id }} + ) + print(f'Found {len(users)} users with card_id: {card_id}') + + match len(users): + case 0: + return None + case 1: + print(f'FabCard matches with user {users[0]["username"]}') + return users[0] + case other: + print(f'Error! too many users with card_id: {card_id}') + return None + + @staticmethod + def user_is_privileged(username): + groups = KeycloakHandler.admin.get_user_groups(user_id=username) + groups = [group['name'] for group in groups] + + if 'Mentoren' in groups: + print('Overrided becouse of "Mentor" group') + return True + else: + return False diff --git a/fab_access/main.py b/fab_access/main.py new file mode 100644 index 0000000..9b9be52 --- /dev/null +++ b/fab_access/main.py @@ -0,0 +1,98 @@ +import json +import os +from config import Config +from mqtt_client import MqttHandler +from keycloak_handler import KeycloakHandler + + +def has_permission(user_permissions, machine_id): + parsed_permissions = [permission.split('.') for permission in user_permissions] + parsed_machine_id = machine_id.split('.') + + for permission in parsed_permissions: + missmatch = False + for i, id_sequence in enumerate(parsed_machine_id): + if permission[i] == '*': + return True + if permission[i] != id_sequence: + missmatch = True + break + if not missmatch: + return True + + return False + + +def gen_display_name(user): + # display names must be not longer than 8 chrs + if 'firstName' in user.keys() and 'lastName' in user.keys(): + full_name = f'{user["firstName"]} {user["lastName"]}' + if len(full_name) > 8: + full_name = 'f{user["firstName"][0]}.{user["lastName"][:6]}' + else: + try: + display_name = user['username'][:8] + except KeyError: + print('user has no username') + return 'Error' + +def handle_request(msg, client): + print('') + print(f'Received `{msg.payload.decode()}` from `{msg.topic}` topic') + fabcard_id = json.loads(msg.payload.decode())['UID'] + reader_id = msg.topic.split('/')[-1] + + user = KeycloakHandler.get_user_by_card_id(fabcard_id) + if not user: + MqttHandler.print_to_display(reader_id, 16, fabcard_id) + return + + # TODO Database + # - get machine_id from database + # - get last_user from database + # - get machine_status from database, is 0 for off, 1 for on + # - get plug_id from database + machine_id = 'space.foo.lazerspacer' + last_user = 'foo.bar' + machine_status = 0 # or 1 + plug_id = 'lazerspacer' + + try: + user_permissions = json.loads(user['attributes']['FabPermissions'][0]) + except KeyError: + print(f'user with id {fabcard_id} is missing FabPermissions attr') + except IndexError: + print(f'user with id {fabcard_id} is missing FabPermissions attr') + + if not has_permission(user_permissions, machine_id): + MqttHandler.print_to_display(reader_id, 7, '') + return + + username = user['username'] + display_name = gen_display_name(user) + + if machine_status == 0: + print(f'Turn Plug {plug_id} on') + MqttHandler.switch_plug(plug_id, 1) + MqttHandler.print_to_display(reader_id, 20, f'Login\n{display_name}') + else: + if not (username == last_user or KeycloakHandler.user_is_privileged(username)): + MqttHandler.print_to_display(reader_id, 9, last_user) + return + print(f'Turn Plug {plug_id} off') + MqttHandler.switch_plug(plug_id, 0) + MqttHandler.print_to_display(reader_id, 20, f'Bitte anmelden') + + # TODO Update Database: + # - last user + # - machine_status + MqttHandler.publish(f'/FabLogging/{plug_id}/USER', username) + + +def main(): + MqttHandler.setup(handle_request) + MqttHandler.subscribe("/rfid_reader/#") + MqttHandler.loop() + +if __name__ == '__main__': + main() diff --git a/fab_access/mqtt_client.py b/fab_access/mqtt_client.py new file mode 100644 index 0000000..4003232 --- /dev/null +++ b/fab_access/mqtt_client.py @@ -0,0 +1,61 @@ +from paho.mqtt import client as mqtt_client +from config import Config +import json + +class MqttHandler: + @staticmethod + def setup(msg_handler): + MqttHandler.msg_handler = msg_handler + MqttHandler.client = None + + @staticmethod + def connect_mqtt(): + def on_connect(userdata, flags, rc): + if rc == 0: + print('Connected to MQTT Broker!') + else: + print('Failed to connect, return code %d\n', rc) + + MqttHandler.client = mqtt_client.Client(Config.client_id) + MqttHandler.client.username_pw_set('admin', 'user') + MqttHandler.client.on_connect = on_connect + MqttHandler.client.connect(Config.broker, Config.port) + + @staticmethod + def publish(topic, msg): + result = MqttHandler.client.publish(topic, msg) + # result: [0, 1] + status = result[0] + if status == 0: + #print(f'Send `{msg}` to topic `{topic}`') + pass + else: + print(f'Failed to send message to topic {topic}') + + + @staticmethod + def subscribe(topic): + def on_message(client, userdata, msg): + MqttHandler.msg_handler(msg, client) + + MqttHandler.client.subscribe(topic) + MqttHandler.client.on_message = on_message + + @staticmethod + def loop(): + MqttHandler.client.loop_forever() + + @staticmethod + def switch_plug(PlugID, state): + MqttHandler.publish(f'/FabLogging/{PlugID}/POWER', 1 if state else 0) + MqttHandler.publish(f'/aktoren/{PlugID}/cmnd/POWER', 'On' if state else 'OFF') + + @staticmethod + def print_to_display(MachineID, status, text): + message = { + 'Cmd': 'message', + 'MssgID': status, + 'ClrTxt': '', + 'AddnTxt': text, + } + MqttHandler.publish(f'/cmnd/reader/{MachineID}', json.dumps(message)) diff --git a/main.py b/main.py deleted file mode 100644 index 9ce93f7..0000000 --- a/main.py +++ /dev/null @@ -1,178 +0,0 @@ -from paho.mqtt import client as mqtt_client -from keycloak import KeycloakAdmin -import json -import mysql.connector -import os - -broker = 'mosquitto' -port = 1883 -client_id = f'FabMan' - -KEYCLOAK_URL = "http://keycloak:8080/auth/" -KEYCLOAK_USERNAME = os.environ['KEYCLOAK_USER_NAME'] -KEYCLOAK_PASSWORD = os.environ['KEYCLOAK_USER_PW'] -REALM = os.environ['KEYCLOAK_REALM'] - -FabDB = mysql.connector.connect( - host="FabDB", - user=os.environ['FABDB_DB_USER_NAME'], - password=os.environ['FABDB_DB_USER_PW'], - database=os.environ['FABDB_DB_NAME'] -) - -def connect_mqtt(): - def on_connect(client, userdata, flags, rc): - if rc == 0: - print("Connected to MQTT Broker!") - else: - print("Failed to connect, return code %d\n", rc) - - client = mqtt_client.Client(client_id) - client.username_pw_set("admin", "user") - client.on_connect = on_connect - client.connect(broker, port) - return client - -def publish(client,topic,msg): - result = client.publish(topic, msg) - # result: [0, 1] - status = result[0] - if status == 0: - #print(f"Send `{msg}` to topic `{topic}`") - pass - else: - print(f"Failed to send message to topic {topic}") - -def changePlug(client,PlugID,state): - publish(client,f"/FabLogging/{PlugID}/POWER",state) - cmd = ["OFF", "On"][state] - publish(client,f"/aktoren/{PlugID}/cmnd/POWER",cmd) - -def changeDisplay(client,MachineID,status,text): - publish(client,f"/cmnd/reader/{MachineID}",'{"Cmd": "message", "MssgID": %s , "ClrTxt":"" , "AddnTxt":"%s"}' % ((status), (text))) - -def checkPermission(UserPermsJSON,PermissionPath): - permission = False - error = 0 - MachinePermArray = PermissionPath.split(".") - for UserPerm in UserPermsJSON: - #print(f"check {UserPerm}") - UserPermArray = UserPerm.split(".") - x = 0 - for UserPermPart in UserPermArray: - if(error == 0): - #print(f"Compare {UserPermPart} and {MachinePermArray[x]}") - if not (MachinePermArray[x] == UserPermPart): - if(UserPermPart == "*"): - #print("* regelt") - permission = True - else: - error = 1 - #print(f"MISmatch between {MachinePermArray[x]} and {UserPermPart}") - else: - pass - #print(f"Match between {MachinePermArray[x]} and {UserPermPart}") - x = x + 1 - if(error == 1): - pass - #print("Error") - else: - #print("Hurra") - permission = True - error = 0 - return permission - -def msg_handler(msg,client): - print("") - print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic") - topic = msg.topic - MQTT_KEY = json.loads(msg.payload.decode())["UID"] - keycloak_admin = KeycloakAdmin( server_url=KEYCLOAK_URL, username=KEYCLOAK_USERNAME, password=KEYCLOAK_PASSWORD, realm_name=REALM, verify=True) - users = keycloak_admin.get_users({}) - knownUser = False - ReaderID = msg.topic.split("/")[-1] - for user in users: - try: - FabCardID = user['attributes']['FabCard'][0] - UserPermissions = user['attributes']['FabPermissions'][0] - if (FabCardID == MQTT_KEY): - knownUser = True - print(f"FabCard matches with user {user['username']}") - - mycursor = FabDB.cursor() - mycursor.execute(f'SELECT PlugName,PermissionPath,Status,LastUser FROM ReaderPlug WHERE ReaderID="{ReaderID}";') - myValues = mycursor.fetchall()[0] - PlugID = myValues[0] - PermissionPath = myValues[1] - MachineStatus = myValues[2] - LastUser = myValues[3] - #print(PermissionPath) - UserPermsJSON = json.loads(UserPermissions) - permission = checkPermission(UserPermsJSON, PermissionPath) - print('System "use" access %s' % ['denied','granted'][permission]) - - if(permission): - error = False - status = MachineStatus - ActualUser = user['username'] - if(status): - status = False - if(LastUser == ActualUser): - error = False - else: - try: - if (keycloak_admin.get_user_groups(user_id=user['id'])[0]['name'] == 'Mentoren'): - print('Overrided becouse of "Mentor" Group') - else: - print(f'Bereits benutzt von {LastUser}') - error = True - except IndexError: - print("No groups available") - error = True - if(error): - changeDisplay(client, ReaderID, 9, LastUser) - else: - status = True - error = False - if(error == False): - #KSstatus ^= True - switch = ["off", "on"][status] - print(f"Turn Plug {switch}") - #print(status) - changePlug(client, PlugID, status) - publish(client,f"/FabLogging/{PlugID}/USER",ActualUser) - try: - firstCombo = user['firstName']+" " - DisplayUser = firstCombo+user['lastName'] - if(len(firstCombo) >= 7): - DisplayUser = user['firstName'][:7] - except KeyError: - DisplayUser = user['username'] - if(len(DisplayUser) > 7): - DisplayUser = DisplayUser[0:7] + "." + DisplayUser[7+1: ] - DisplayUser = DisplayUser[:8] - changeDisplay(client, ReaderID, [20, 20][status], ["Bitte anmelden", f"Login\n{DisplayUser}"][status]) - mycursor.execute(f'UPDATE ReaderPlug SET Status={status}, LastUser="{ActualUser}" WHERE ReaderID="{ReaderID}";') - FabDB.commit() - else: - changeDisplay(client, ReaderID, 7, "") - except KeyError: - # Key is not present - pass - if not (knownUser): - changeDisplay(client, ReaderID, 16, MQTT_KEY) - -def subscribe(client: mqtt_client, topic): - def on_message(client, userdata, msg): - msg_handler(msg,client) - - client.subscribe(topic) - client.on_message = on_message - -def run(): - client = connect_mqtt() - subscribe(client,"/rfid_reader/#") - client.loop_forever() - -if __name__ == '__main__': - run() \ No newline at end of file From 96172f7edcc67fb2ba269959a161bbd51993170c Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Thu, 3 Nov 2022 11:56:02 +0000 Subject: [PATCH 02/12] Update README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index e69de29..a0879dc 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,22 @@ +# Fab Access + +The backend implementation that handles access control for the FabLab. In order to test, a `.env` file with the following content must be created. A test setup can then be started using `docker-compose up --build` + + +```sh +KEYCLOAK_URL=auth.sfz-aalen.space +KEYCLOAK_USER_NAME=YOUR_USERNAME +KEYCLOAK_USER_PW=YOUR_PASSWORD +KEYCLOAK_REALM=master + +DB_HOSTNAME=db +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_DATABASE=fab_access + +MQTT_USERNAME=user +MQTT_PASSWORD=password +MQTT_BROKER=mqtt +MQTT_CLIENT=FabMan +``` + From 0dc851977fabc4491961279e35442fe003681b7e Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Thu, 3 Nov 2022 21:13:35 +0100 Subject: [PATCH 03/12] Fix docker shit --- .gitignore | 2 + Dockerfile | 13 +- docker-compose.yml | 563 +++---------------------------------------- fab_access/config.py | 8 +- requirements.txt | 4 + 5 files changed, 57 insertions(+), 533 deletions(-) create mode 100644 .gitignore create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41edece --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/sql +/.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4903558..311b1cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,9 @@ -FROM python:3.11.0a3-bullseye -RUN pip install keycloak_wrapper paho-mqtt python-keycloak mysql.connector -RUN mkdir /app/ -COPY main.py /app/ +FROM python:3.11-alpine +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY ./fab_access/* /app + ENTRYPOINT [ "python" ] -CMD [ "-u", "/app/main.py" ] \ No newline at end of file +CMD [ "-u", "main.py" ] diff --git a/docker-compose.yml b/docker-compose.yml index 9454629..5ca50c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,536 +1,49 @@ version: '3' - -networks: - auth: - FabDB: - FabLoggingDB: - FabLoggingTelegraf: - reverse-proxy: - mediawiki: - nextcloud: - gitea: - octofarm: - octoproxy: - ipam: - config: - - subnet: 172.42.0.0/24 - odoo: - roundcube: - partdb: - -volumes: - build-trigger: - build-output: - db: - nextcloud: - -secrets: - samba-admin-password: - file: ./samba-admin-password - services: - AD: - image: instantlinux/samba-dc:latest - restart: always - network_mode: host - cap_add: - - CAP_SYS_ADMIN - hostname: dc.sfz.lab - environment: - DOMAIN_ACTION: provision - INTERFACES: -lo eth0 - REALM: ad.sfz.lab - TZ: Europe/Berlin - WORKGROUP: AD - volumes: - - ./data/samba/config:/etc/samba - - ./data/samba/data:/var/lib/samba - secrets: - - samba-admin-password - - reverse-proxy: - image: nginx-openid - build: - context: ./data/reverse-proxy/ - dockerfile: Dockerfile - ports: - - "80:80" - - "443:443" - volumes: - - ./data/reverse-proxy/content/:/usr/share/nginx/html/ - - ./data/reverse-proxy/config/:/etc/nginx/conf.d/ - - /etc/letsencrypt:/etc/nginx/certs - - /etc/localtime:/etc/localtime:ro - - ./data/mirror/config/:/mirror/config_web/ - networks: - reverse-proxy: - restart: unless-stopped - - - mysql: - image: mysql:5.7 - volumes: - - ./data/keycloak/DB/:/var/lib/mysql - environment: - MYSQL_RANDOM_ROOT_PASSWORD: "yes" - MYSQL_DATABASE: ${KEYCLOAK_DB_NAME} - MYSQL_USER: ${KEYCLOAK_DB_USER} - MYSQL_PASSWORD: ${KEYCLOAK_DB_PW} - networks: - auth: - - keycloak: - image: keycloak-sfz - build: - context: ./data/keycloak/ - dockerfile: Dockerfile - environment: - - DB_VENDOR=MYSQL - - DB_ADDR=mysql - - DB_DATABASE=${KEYCLOAK_DB_NAME} - - DB_USER=${KEYCLOAK_DB_USER} - - DB_PASSWORD=${KEYCLOAK_DB_PW} - - KEYCLOAK_USER=${KEYCLOAK_USER_NAME} - - KEYCLOAK_PASSWORD=${KEYCLOAK_USER_PW} - - PROXY_ADDRESS_FORWARDING=true + backend: + build: . depends_on: - - mysql - networks: - auth: - reverse-proxy: - volumes: - - ./data/keycloak/data/:/lib/jvm/jre-11/lib/security/ - - ./data/keycloak/cert/:/etc/pki/java/ - - - mosquitto: - image: hivemq/hivemq4 - ports: - - 1883:1883 - - 9001:9001 - networks: - - FabDB - - FabBackend: - image: fabbackend - build: - context: ./data/FabBackend - dockerfile: Dockerfile + - db + - mqtt environment: - KEYCLOAK_USER_NAME: ${KEYCLOAK_USER_NAME} - KEYCLOAK_USER_PW: ${KEYCLOAK_USER_PW} - KEYCLOAK_REALM: ${KEYCLOAK_REALM} - FABDB_DB_USER_NAME: ${FABDB_DB_USER_NAME} - FABDB_DB_USER_PW: ${FABDB_DB_USER_PW} - FABDB_DB_NAME: ${FABDB_DB_NAME} - networks: - - FabDB - - auth - restart: unless-stopped - - FabDB: - image: fabdb - build: - context: ./data/FabBackend - dockerfile: Dockerfile-DB - volumes: - - ./data/FabBackend/DB/:/var/lib/mysql - environment: - MYSQL_RANDOM_ROOT_PASSWORD: "yes" - MYSQL_DATABASE: ${FABDB_DB_NAME} - MYSQL_USER: ${FABDB_DB_USER_NAME} - MYSQL_PASSWORD: ${FABDB_DB_USER_PW} - networks: - - FabDB - - FabLoggingDB: - image: influxdb:1.5 - volumes: - - ./data/FabLogging/DB/data/:/var/lib/influxdb/ - - ./data/FabLogging/DB/config/:/etc/influxdb/ - restart: always - networks: - - FabLoggingDB - - FabLoggingTelegraf - - FabLoggingDBTelegraf: - image: telegraf - volumes: - - ./data/FabLogging/telegraf/telegraf.conf:/etc/telegraf/telegraf.conf - - /var/run/docker.sock:/var/run/docker.sock - restart: always - user: "0" - networks: - - FabDB - - FabLoggingTelegraf - - grafana: - image: grafana/grafana - user: "0" - #ports: - # - "3000:3000" - volumes: - - ./data/FabLogging/data/grafana:/var/lib/grafana - restart: always - networks: - - FabLoggingDB - - buildserver-worker: - image: buildserver - build: - context: ./data/buildserver/worker/ - dockerfile: Dockerfile - volumes: - - build-trigger:/trigger/ - - build-output:/output/ - - /etc/localtime:/etc/localtime:ro - restart: unless-stopped - - buildserver-web-trigger: - image: buildserver-web-trigger - build: - context: ./data/buildserver/trigger/ - dockerfile: Dockerfile - networks: - reverse-proxy: - volumes: - - build-trigger:/var/www/trigger/ - - /etc/localtime:/etc/localtime:ro - environment: - - DEBUG=true - - HISTCONTROL=ignoredups - restart: unless-stopped - - buildserver-web-server: - image: httpd:latest - networks: - reverse-proxy: - volumes: - - build-output:/usr/local/apache2/htdocs/ - - /etc/localtime:/etc/localtime:ro - restart: unless-stopped - - mediawiki: - image: mediawikisfz - build: - dockerfile: Dockerfile - context: ./data/mediawiki/ - networks: - reverse-proxy: - mediawiki: - auth: - depends_on: - - mediawiki-mysql - restart: unless-stopped - volumes: - - ./data/mediawiki/images:/var/www/html/images/ - - mediawiki-mysql: - image: mariadb - volumes: - - ./data/mediawiki/DB:/var/lib/mysql - environment: - MYSQL_ROOT_PASSWORD: ${MEDIAWIKIDB_ROOT_PW} - networks: - mediawiki: - hostname: mediawiki-mysql - restart: unless-stopped - - cdn01: - image: httpd - networks: - reverse-proxy: - volumes: - - ./data/CDN:/usr/local/apache2/htdocs/ - restart: unless-stopped - - cdn02: - image: httpd - networks: - reverse-proxy: - volumes: - - ./data/CDN:/usr/local/apache2/htdocs/ + # Keycloak config + KEYCLOAK_URL: ${KEYCLOAK_URL:?err} + KEYCLOAK_USER_NAME: ${KEYCLOAK_USER_NAME:?err} + KEYCLOAK_USER_PW: ${KEYCLOAK_USER_PW:?err} + KEYCLOAK_REALM: ${KEYCLOAK_REALM:?err} + # DB config + DB_HOSTNAME: ${DB_HOSTNAME:?err} + DB_USERNAME: ${DB_USERNAME:?err} + DB_PASSWORD: ${DB_PASSWORD:?err} + DB_DATABASE: ${DB_DATABASE:?err} + # MQTT config + MQTT_USERNAME: ${MQTT_USERNAME:?err} + MQTT_PASSWORD: ${MQTT_PASSWORD:?err} + MQTT_BROKER: ${MQTT_BROKER:?err} + MQTT_CLIENT: ${MQTT_CLIENT:?err} restart: unless-stopped - cdn03: - image: httpd - networks: - reverse-proxy: - volumes: - - ./data/CDN:/usr/local/apache2/htdocs/ - restart: unless-stopped - db: - image: mariadb:10.5 - command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW - restart: always + image: postgres:15-alpine volumes: - - db:/var/lib/mysql - networks: - nextcloud: + - ./sql/:/docker-entrypoint-initdb.d/:ro,Z environment: - - MYSQL_RANDOM_ROOT_PASSWORD="yes" - - MYSQL_PASSWORD=${NEXTCLOUD_MYSQL_PW} - - MYSQL_DATABASE=nextcloud - - MYSQL_USER=nextcloud + POSTGRES_DB: ${DB_DATABASE} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} - redis: - image: redis:alpine - restart: always - networks: - nextcloud: - - app: - image: nextcloud:fpm-alpine - restart: always - volumes: - - nextcloud:/var/www/html + mqtt: + image: eclipse-mosquitto:2 environment: - - MYSQL_HOST=db - - REDIS_HOST=redis - - MYSQL_PASSWORD=${NEXTCLOUD_MYSQL_PW} - - MYSQL_DATABASE=nextcloud - - MYSQL_USER=nextcloud - depends_on: - - db - - redis - networks: - nextcloud: - - web: - build: ./data/nextcloud/ - restart: always - volumes: - - nextcloud:/var/www/html:ro - depends_on: - - app - networks: - reverse-proxy: - nextcloud: - - cron: - image: nextcloud:fpm-alpine - restart: always - volumes: - - nextcloud:/var/www/html - entrypoint: /cron.sh - depends_on: - - db - - redis - - gitea: - image: gitea/gitea:1.15.9 - environment: - - USER_UID=1000 - - USER_GID=1000 - - GITEA__database__DB_TYPE=mysql - - GITEA__database__HOST=git-db:3306 - - GITEA__database__NAME=gitea - - GITEA__database__USER=gitea - - GITEA__database__PASSWD=${GIT_DB_PW} - restart: always - networks: - - gitea - - reverse-proxy - volumes: - - ./data/gitea/data:/data - - /etc/timezone:/etc/timezone:ro - - /etc/localtime:/etc/localtime:ro - depends_on: - - git-db - - git-db: - image: mysql:8 - restart: always - environment: - - MYSQL_RANDOM_ROOT_PASSWORD="yes" - - MYSQL_USER=gitea - - MYSQL_PASSWORD=${GIT_DB_PW} - - MYSQL_DATABASE=gitea - networks: - - gitea - volumes: - - ./data/gitea/db:/var/lib/mysql - - octofarm-db: - image: mongo:4.4 - environment: - MONGO_INITDB_ROOT_USERNAME: octofarm - MONGO_INITDB_ROOT_PASSWORD: ${OCTOFARM_DB_PW} - MONGO_INITDB_DATABASE: octofarm - volumes: - - ./data/OctoFarm/data/mongodb-data:/data/db - restart: unless-stopped - networks: - octofarm: - - octofarm: - image: octofarm/octofarm:latest - restart: unless-stopped - mem_limit: 400m # Feel free to adjust! 400 MB is quite high and a safety limit. - networks: - reverse-proxy: - octofarm: - octoproxy: - ipv4_address: 172.42.0.3 - environment: - - MONGO=mongodb://octofarm:${OCTOFARM_DB_PW}@octofarm-db:27017/octofarm?authSource=admin - ports: - - 4000:4000 - expose: - - 4000 - volumes: - - ./data/OctoFarm/logs:/app/logs - - ./data/OctoFarm/scripts:/app/scripts - - ./data/OctoFarm/images:/app/images - - ./data/OctoFarm/hosts:/etc/hosts:ro - - octoproxy: - image: nginx:latest - volumes: - - ./data/octoproxy/config/:/etc/nginx/conf.d/ - - /etc/localtime:/etc/localtime:ro - - /etc/letsencrypt:/etc/nginx/certs - networks: - octoproxy: - ipv4_address: 172.42.0.2 - restart: unless-stopped - - octostreamer: - image: gersilex/cvlc - command: rtsp://admin:@10.10.42.60 --sout '#transcode{vcodec=MJPG,venc=ffmpeg{strict=1}}:standard{access=http{mime=multipart/x-mixed-replace;boundary=--7b3cc56e5f51db803f790dad720ed50a},mux=mpjpeg,dst=:8080/}' - networks: - reverse-proxy: - environment: - - RS_SNAPSHOT_INTERVAL=1000 - - octorestreamer: - image: datarhei/restreamer:latest - restart: always - networks: - reverse-proxy: - environment: - - RS_USERNAME=admin - - RS_PASSWORD=${OCTORESTREAMER_PW} - ports: - - 8087:8080 - volumes: - - ./data/restreamer/db:/restreamer/db - -# docker-zabbix-agent: -# restart: always -# ports: -# - '10060:10050' -# volumes: -# - /etc/localtime:/etc/localtime:ro -# - /etc/timezone:/etc/timezone:ro -# environment: -# - ZBX_SERVER_HOST=172.21.0.1 -# - ZBX_HOSTNAME=USV -# image: apcupsd -# devices: -# - /dev/usb/hiddev0 -# build: -# context: ./data/usv/ -# dockerfile: Dockerfile - - docker-zabbix-agent2: - restart: always - ports: - - '10061:10050' - volumes: - - /etc/localtime:/etc/localtime:ro - - /etc/timezone:/etc/timezone:ro - environment: - - ZBX_SERVER_HOST=192.168.64.1 - - ZBX_HOSTNAME=SSL - image: zabbix/zabbix-agent2 - - odoo: - image: odoo-sfz - build: - context: ./data/odoo - dockerfile: Dockerfile - depends_on: - - odoo-db - environment: - - HOST=odoo-db - - USER=odoo - - PASSWORD=${ODOO_DB_PW} - networks: - reverse-proxy: - odoo: - volumes: - - ./data/odoo/addons:/var/lib/odoo/.local/share/Odoo/addons/14.0/ - - ./data/odoo/conf:/etc/odoo - - ./data/odoo/data:/var/lib/odoo - - odoo-db: - image: postgres:13 - environment: - - POSTGRES_DB=postgres - - POSTGRES_PASSWORD=${ODOO_DB_PW} - - POSTGRES_USER=odoo - networks: - odoo: - volumes: - - ./data/odoo/db:/var/lib/postgresql/data - - roundcubedb: - image: mysql:5.7 - container_name: roundcubedb - restart: unless-stopped - volumes: - - ./data/webmail/db/mysql:/var/lib/mysql - environment: - - MYSQL_ROOT_PASSWORD=${WEBMAIL_PW} - - MYSQL_DATABASE=roundcubemail - networks: - roundcube: - - documentserver: - restart: always - image: onlyoffice/documentserver - networks: - reverse-proxy: - - roundcubemail: - image: roundcube/roundcubemail:latest - container_name: roundcubemail - restart: unless-stopped - networks: - reverse-proxy: - roundcube: - depends_on: - - roundcubedb - volumes: - - ./data/webmail/www:/var/www/html - environment: - - ROUNDCUBEMAIL_DB_TYPE=mysql - - ROUNDCUBEMAIL_DB_HOST=roundcubedb - - ROUNDCUBEMAIL_DB_PASSWORD=${WEBMAIL_PW} - - ROUNDCUBEMAIL_SKIN=elastic - - ROUNDCUBEMAIL_DEFAULT_HOST=tls://mail.sfz-aalen.space - - ROUNDCUBEMAIL_SMTP_SERVER=tls://mail.sfz-aalen.space - - partdb-db: - image: mysql - command: --default-authentication-plugin=mysql_native_password - restart: always - networks: - partdb: - environment: - MYSQL_ROOT_PASSWORD: ${PARTDB_ROOT_PW} - - partdb: - container_name: partdb - # By default Part-DB will be running under Port 8080, you can change it here - image: jbtronics/part-db1:master - volumes: - # By default - - ./uploads:/var/www/html/uploads - - ./public_media:/var/www/html/public/media - restart: unless-stopped - networks: - reverse-proxy: - partdb: + MQTT_USERNAME: ${MQTT_USERNAME} + MQTT_PASSWORD: ${MQTT_PASSWORD} + entrypoint: + - sh + - -c + - | + touch /mosquitto/config/passwd + mosquitto_passwd -b /mosquitto/config/passwd $${MQTT_USERNAME:?err} $${MQTT_PASSWORD:?err} + echo "bind_address 0.0.0.0" > /mosquitto/config/mosquitto.conf + echo "password_file /mosquitto/config/passwd" >> /mosquitto/config/mosquitto.conf + echo "allow_anonymous false" >> /mosquitto/config/mosquitto.conf + /usr/sbin/mosquitto -c /mosquitto/config/mosquitto.conf diff --git a/fab_access/config.py b/fab_access/config.py index 21908b7..e5ab1c0 100644 --- a/fab_access/config.py +++ b/fab_access/config.py @@ -1,9 +1,11 @@ +import os + class Config: broker = 'mosquitto' port = 1883 client_id = f'FabMan' keycloak_url = 'http://keycloak:8080/auth/' - keycloak_username = os.environ['KEYCLOAK_USER_NAME'] - keycloak_password = os.environ['KEYCLOAK_USER_PW'] - keycloak_realm = os.environ['KEYCLOAK_REALM'] + keycloak_username = os.getenv('KEYCLOAK_USER_NAME') + keycloak_password = os.getenv('KEYCLOAK_USER_PW') + keycloak_realm = os.getenv('KEYCLOAK_REALM') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c57afff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +keycloak_wrapper +paho-mqtt +psycopg[binary] +python-keycloak From 3fe6318bb71bd50910b07a53d89c3dd71322dc90 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Thu, 3 Nov 2022 21:21:23 +0100 Subject: [PATCH 04/12] FIx ENV Variables --- README.md | 1 + fab_access/config.py | 17 ++++++++++++----- fab_access/keycloak_handler.py | 2 +- fab_access/mqtt_client.py | 5 +++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a0879dc..ad8cb98 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ DB_USERNAME=postgres DB_PASSWORD=postgres DB_DATABASE=fab_access +MQTT_PORT=1883 MQTT_USERNAME=user MQTT_PASSWORD=password MQTT_BROKER=mqtt diff --git a/fab_access/config.py b/fab_access/config.py index e5ab1c0..0910ad8 100644 --- a/fab_access/config.py +++ b/fab_access/config.py @@ -1,11 +1,18 @@ import os class Config: - broker = 'mosquitto' - port = 1883 - client_id = f'FabMan' + mqtt_broker = os.getenv('MQTT_BROKER') + mqtt_port = os.getenv('MQTT_PORT') + mqtt_client_id = os.getenv('MQTT_CLIENT') + mqtt_user_name = os.getenv('MQTT_USERNAME') + mqtt_password = os.getenv('MQTT_PASSWORD') - keycloak_url = 'http://keycloak:8080/auth/' - keycloak_username = os.getenv('KEYCLOAK_USER_NAME') + keycloak_url = os.getenv('KEYCLOAK_URL') + keycloak_user_name = os.getenv('KEYCLOAK_USER_NAME') keycloak_password = os.getenv('KEYCLOAK_USER_PW') keycloak_realm = os.getenv('KEYCLOAK_REALM') + + db_host_name = os.getenv('DB_HOSTNAME') + db_user_name = os.getenv('DB_USERNAME') + db_password = os.getenv('DB_PASSWORD') + db_database = os.getenv('DB_DATABASE') \ No newline at end of file diff --git a/fab_access/keycloak_handler.py b/fab_access/keycloak_handler.py index 02cb661..2428bd7 100644 --- a/fab_access/keycloak_handler.py +++ b/fab_access/keycloak_handler.py @@ -9,7 +9,7 @@ class KeycloakHandler: def login(): KeycloakHandler.admin = KeycloakAdmin( server_url=Config.keycloak_url, - username=Config.keycloak_username, + username=Config.keycloak_user_name, password=Config.keycloak_password, realm_name=Config.keycloak_realm, verify=True diff --git a/fab_access/mqtt_client.py b/fab_access/mqtt_client.py index 4003232..a287e8d 100644 --- a/fab_access/mqtt_client.py +++ b/fab_access/mqtt_client.py @@ -16,10 +16,11 @@ class MqttHandler: else: print('Failed to connect, return code %d\n', rc) - MqttHandler.client = mqtt_client.Client(Config.client_id) + MqttHandler.client = mqtt_client.Client(Config.mqtt_client_id) MqttHandler.client.username_pw_set('admin', 'user') MqttHandler.client.on_connect = on_connect - MqttHandler.client.connect(Config.broker, Config.port) + MqttHandler.client.username_pw_set(Config.mqtt_user_name, Config.mqtt_password) + MqttHandler.client.connect(Config.mqtt_broker, Config.mqtt_port) @staticmethod def publish(topic, msg): From 2a1016aebad6f55a395ea5fb44c4fbe98eb1337c Mon Sep 17 00:00:00 2001 From: Lukas Brodschelm Date: Thu, 3 Nov 2022 21:22:03 +0100 Subject: [PATCH 05/12] Fix Missing connect --- fab_access/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fab_access/main.py b/fab_access/main.py index 9b9be52..600f007 100644 --- a/fab_access/main.py +++ b/fab_access/main.py @@ -91,6 +91,7 @@ def handle_request(msg, client): def main(): MqttHandler.setup(handle_request) + MqttHandler.connect_mqtt() MqttHandler.subscribe("/rfid_reader/#") MqttHandler.loop() From c83c88c4682cc31eb52c1de25c1f77cb978260e0 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Thu, 3 Nov 2022 21:25:09 +0100 Subject: [PATCH 06/12] Fix MQTT Port --- docker-compose.yml | 1 + fab_access/config.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5ca50c4..059110a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: DB_PASSWORD: ${DB_PASSWORD:?err} DB_DATABASE: ${DB_DATABASE:?err} # MQTT config + MQTT_PORT: ${MQTT_PORT:?err} MQTT_USERNAME: ${MQTT_USERNAME:?err} MQTT_PASSWORD: ${MQTT_PASSWORD:?err} MQTT_BROKER: ${MQTT_BROKER:?err} diff --git a/fab_access/config.py b/fab_access/config.py index 0910ad8..3ddd366 100644 --- a/fab_access/config.py +++ b/fab_access/config.py @@ -2,7 +2,7 @@ import os class Config: mqtt_broker = os.getenv('MQTT_BROKER') - mqtt_port = os.getenv('MQTT_PORT') + mqtt_port = int(os.getenv('MQTT_PORT')) mqtt_client_id = os.getenv('MQTT_CLIENT') mqtt_user_name = os.getenv('MQTT_USERNAME') mqtt_password = os.getenv('MQTT_PASSWORD') From 1b30093322abc48366a5ac4fb0f80b81e87b8e56 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Thu, 3 Nov 2022 21:33:54 +0100 Subject: [PATCH 07/12] Fix Keycloak login --- docker-compose.yml | 2 ++ fab_access/main.py | 1 + fab_access/mqtt_client.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 059110a..6a38032 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,8 @@ services: mqtt: image: eclipse-mosquitto:2 + ports: + - 1883:1883 environment: MQTT_USERNAME: ${MQTT_USERNAME} MQTT_PASSWORD: ${MQTT_PASSWORD} diff --git a/fab_access/main.py b/fab_access/main.py index 600f007..09dcddc 100644 --- a/fab_access/main.py +++ b/fab_access/main.py @@ -93,6 +93,7 @@ def main(): MqttHandler.setup(handle_request) MqttHandler.connect_mqtt() MqttHandler.subscribe("/rfid_reader/#") + KeycloakHandler.login() MqttHandler.loop() if __name__ == '__main__': diff --git a/fab_access/mqtt_client.py b/fab_access/mqtt_client.py index a287e8d..e64d9e5 100644 --- a/fab_access/mqtt_client.py +++ b/fab_access/mqtt_client.py @@ -10,7 +10,7 @@ class MqttHandler: @staticmethod def connect_mqtt(): - def on_connect(userdata, flags, rc): + def on_connect(client, userdata, flags, rc): if rc == 0: print('Connected to MQTT Broker!') else: From 0c6e188367e69d140db84ddb7140220ca08c98e1 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Thu, 3 Nov 2022 22:43:43 +0100 Subject: [PATCH 08/12] Fix get_user_by_card_id by implementing own filter --- fab_access/keycloak_handler.py | 15 ++++++++------- test.py | 7 +++++++ 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 test.py diff --git a/fab_access/keycloak_handler.py b/fab_access/keycloak_handler.py index 2428bd7..2ba45b6 100644 --- a/fab_access/keycloak_handler.py +++ b/fab_access/keycloak_handler.py @@ -17,17 +17,18 @@ class KeycloakHandler: @staticmethod def get_user_by_card_id(card_id): - users = KeycloakHandler.admin.get_users( - { 'attributes': { 'FabCard': card_id }} - ) - print(f'Found {len(users)} users with card_id: {card_id}') + users = KeycloakHandler.admin.get_users() + # Filter not working for Attributes because of multidimensional JSON - match len(users): + user = [user for user in users if "attributes" in user and "FabCard" in user["attributes"] and user["attributes"]["FabCard"] == [card_id]] + print(f'Found {len(user)} user(s) with card_id: {card_id}') + + match len(user): case 0: return None case 1: - print(f'FabCard matches with user {users[0]["username"]}') - return users[0] + print(f'FabCard matches with user {user[0]["username"]}') + return user[0] case other: print(f'Error! too many users with card_id: {card_id}') return None diff --git a/test.py b/test.py new file mode 100644 index 0000000..ccfeef2 --- /dev/null +++ b/test.py @@ -0,0 +1,7 @@ +from keycloak import KeycloakAdmin + +keycloak_admin = KeycloakAdmin(server_url="https://auth.sfz-aalen.space/auth/", + username='luca.lutz', + password='LiviT2005', + realm_name="master", + verify=True) \ No newline at end of file From eb7e7c693c589f38ca5cfd9cf00c51a4717e35a2 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Thu, 3 Nov 2022 22:52:28 +0100 Subject: [PATCH 09/12] Adding message with missing machine permission if user is missing a specific permission --- fab_access/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fab_access/main.py b/fab_access/main.py index 09dcddc..5e895a2 100644 --- a/fab_access/main.py +++ b/fab_access/main.py @@ -65,6 +65,7 @@ def handle_request(msg, client): print(f'user with id {fabcard_id} is missing FabPermissions attr') if not has_permission(user_permissions, machine_id): + print(f"user with id {fabcard_id} is missing {machine_id}") MqttHandler.print_to_display(reader_id, 7, '') return From 8b10defed2438a28418a0c885846827535701239 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Fri, 4 Nov 2022 00:15:23 +0100 Subject: [PATCH 10/12] First work adding DB --- FabAccess.sql | 5 --- docker-compose.yml | 1 - fab_access/config.py | 33 ++++++++++++-------- fab_access/main.py | 11 +++++-- fab_access/sql_handler.py | 65 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 +-- 6 files changed, 96 insertions(+), 23 deletions(-) delete mode 100644 FabAccess.sql create mode 100644 fab_access/sql_handler.py diff --git a/FabAccess.sql b/FabAccess.sql deleted file mode 100644 index 4cfdb37..0000000 --- a/FabAccess.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP DATABASE IF EXISTS FabAccess; -CREATE DATABASE IF NOT EXISTS FabAccess; -USE FabAccess; -CREATE TABLE `ReaderPlug` (`ReaderID` INT NOT NULL , `PlugName` VARCHAR(255) NOT NULL, `PermissionPath` VARCHAR(255) NOT NULL, `Status` BOOLEAN NOT NULL, `LastUser` VARCHAR(255) NOT NULL) ENGINE = InnoDB; -INSERT INTO `ReaderPlug` (`ReaderID`, `PlugName`, `PermissionPath`, `Status`, `LastUser`) VALUES ("001", "lasercutter", "sfz.lasercutter.trotec", 0, "luca.lutz"); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6a38032..cf082e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,6 @@ services: DB_PASSWORD: ${DB_PASSWORD:?err} DB_DATABASE: ${DB_DATABASE:?err} # MQTT config - MQTT_PORT: ${MQTT_PORT:?err} MQTT_USERNAME: ${MQTT_USERNAME:?err} MQTT_PASSWORD: ${MQTT_PASSWORD:?err} MQTT_BROKER: ${MQTT_BROKER:?err} diff --git a/fab_access/config.py b/fab_access/config.py index 3ddd366..ecb8aa6 100644 --- a/fab_access/config.py +++ b/fab_access/config.py @@ -1,18 +1,25 @@ import os class Config: - mqtt_broker = os.getenv('MQTT_BROKER') - mqtt_port = int(os.getenv('MQTT_PORT')) - mqtt_client_id = os.getenv('MQTT_CLIENT') - mqtt_user_name = os.getenv('MQTT_USERNAME') - mqtt_password = os.getenv('MQTT_PASSWORD') - keycloak_url = os.getenv('KEYCLOAK_URL') - keycloak_user_name = os.getenv('KEYCLOAK_USER_NAME') - keycloak_password = os.getenv('KEYCLOAK_USER_PW') - keycloak_realm = os.getenv('KEYCLOAK_REALM') + def _read_from_env(key: str, default: str = None) -> str: + if (value := os.environ.get(key, default)) is not None: + return value + raise Exception(f'[Config] Error: Cannot find required value {key}') - db_host_name = os.getenv('DB_HOSTNAME') - db_user_name = os.getenv('DB_USERNAME') - db_password = os.getenv('DB_PASSWORD') - db_database = os.getenv('DB_DATABASE') \ No newline at end of file + mqtt_broker = _read_from_env('MQTT_BROKER') + mqtt_port = int(_read_from_env('MQTT_PORT','1883')) + mqtt_client_id = _read_from_env('MQTT_CLIENT') + mqtt_user_name = _read_from_env('MQTT_USERNAME') + mqtt_password = _read_from_env('MQTT_PASSWORD') + + keycloak_url = _read_from_env('KEYCLOAK_URL') + keycloak_user_name = _read_from_env('KEYCLOAK_USER_NAME') + keycloak_password = _read_from_env('KEYCLOAK_USER_PW') + keycloak_realm = _read_from_env('KEYCLOAK_REALM') + + db_host_name = _read_from_env('DB_HOSTNAME') + db_user_name = _read_from_env('DB_USERNAME') + db_password = _read_from_env('DB_PASSWORD') + db_database = _read_from_env('DB_DATABASE') + db_port = int(_read_from_env('DB_PORT','5432')) \ No newline at end of file diff --git a/fab_access/main.py b/fab_access/main.py index 5e895a2..35e6ad7 100644 --- a/fab_access/main.py +++ b/fab_access/main.py @@ -3,7 +3,7 @@ import os from config import Config from mqtt_client import MqttHandler from keycloak_handler import KeycloakHandler - +from sql_handler import SQLHandler def has_permission(user_permissions, machine_id): parsed_permissions = [permission.split('.') for permission in user_permissions] @@ -52,6 +52,9 @@ def handle_request(msg, client): # - get last_user from database # - get machine_status from database, is 0 for off, 1 for on # - get plug_id from database + + print(SQLHandler.get_machine_data(reader_id)) + machine_id = 'space.foo.lazerspacer' last_user = 'foo.bar' machine_status = 0 # or 1 @@ -92,9 +95,13 @@ def handle_request(msg, client): def main(): MqttHandler.setup(handle_request) + KeycloakHandler.login() + SQLHandler.setup() + MqttHandler.connect_mqtt() MqttHandler.subscribe("/rfid_reader/#") - KeycloakHandler.login() + SQLHandler.init_db() + MqttHandler.loop() if __name__ == '__main__': diff --git a/fab_access/sql_handler.py b/fab_access/sql_handler.py new file mode 100644 index 0000000..a1058d3 --- /dev/null +++ b/fab_access/sql_handler.py @@ -0,0 +1,65 @@ +import psycopg2 +from psycopg2 import sql, extensions + +from config import Config + +class SQLHandler: + @staticmethod + def setup(): + SQLHandler.cursor = None + SQLHandler.conn = None + SQLHandler.conn = psycopg2.connect(host=Config.db_host_name, user=Config.db_user_name, port=Config.db_port, password=Config.db_password, dbname=Config.db_database) + # get the isolation leve for autocommit + autocommit = extensions.ISOLATION_LEVEL_AUTOCOMMIT + print ("ISOLATION_LEVEL_AUTOCOMMIT:", extensions.ISOLATION_LEVEL_AUTOCOMMIT) + + # set the isolation level for the connection's cursors + # will raise ActiveSqlTransaction exception otherwise + SQLHandler.conn.set_isolation_level( autocommit ) + SQLHandler.cursor = SQLHandler.conn.cursor() + + @staticmethod + def get_machine_data(reader_id): + # TODO Database + # - get machine_id from database + # - get last_user from database + # - get machine_status from database, is 0 for off, 1 for on + # - get plug_id from database + SQLHandler.cursor.execute("SELECT machine_id,last_user,machine_status,plug_id FROM readerplug WHERE reader_id = %s;", (reader_id,)) + data = [row for row in SQLHandler.cursor.fetchall()] + print(data) + return { + 'machine_id': data[0], + 'last_user': data[1], + 'machine_status': data[2], + 'plug_id': data[3] + } + + + @staticmethod + def init_db(): + SQLHandler.cursor.execute("SELECT datname FROM pg_database;") + dbs = [row[0] for row in SQLHandler.cursor.fetchall()] + if not (Config.db_database in dbs): + print(f"Missing database ({Config.db_database}) -> creating new db") + SQLHandler.cursor.execute(sql.SQL("CREATE DATABASE {};").format(sql.Identifier( Config.db_database ))) + else: + print(f"Found DB {Config.db_database} -> Using existing one") + + SQLHandler.cursor.execute("SELECT * FROM pg_catalog.pg_tables\ + WHERE schemaname != 'pg_catalog' AND \ + schemaname != 'information_schema';") + tables = [row[1] for row in SQLHandler.cursor.fetchall()] + if not ("readerplug" in tables): + print("Missing table -> creating new table in db") + SQLHandler.cursor.execute("\ + CREATE TABLE readerplug (\ + reader_id int NOT NULL, \ + plug_id varchar(255) NOT NULL, \ + machine_id varchar(255) NOT NULL, \ + machine_status boolean NOT NULL, \ + last_user varchar(255) NOT NULL \ + )") + else: + print("Found Table -> Using existing one") + SQLHandler.conn.commit() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c57afff..6a6ef13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ keycloak_wrapper paho-mqtt -psycopg[binary] -python-keycloak +psycopg2-binary +python-keycloak \ No newline at end of file From 2c2c990e2a8f59b082094c9929691c7ab04cf498 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Fri, 4 Nov 2022 18:38:44 +0100 Subject: [PATCH 11/12] DB part 2 --- docker-compose.yml | 2 ++ fab_access/config.py | 4 +++- fab_access/keycloak_handler.py | 2 +- fab_access/main.py | 19 +++++++++++-------- fab_access/sql_handler.py | 34 ++++++++++++++++++++++++++-------- 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index cf082e9..1abe1ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,8 @@ services: MQTT_PASSWORD: ${MQTT_PASSWORD:?err} MQTT_BROKER: ${MQTT_BROKER:?err} MQTT_CLIENT: ${MQTT_CLIENT:?err} + # Various + MACHINES: ${MACHINES:?err} restart: unless-stopped db: diff --git a/fab_access/config.py b/fab_access/config.py index ecb8aa6..3701757 100644 --- a/fab_access/config.py +++ b/fab_access/config.py @@ -22,4 +22,6 @@ class Config: db_user_name = _read_from_env('DB_USERNAME') db_password = _read_from_env('DB_PASSWORD') db_database = _read_from_env('DB_DATABASE') - db_port = int(_read_from_env('DB_PORT','5432')) \ No newline at end of file + db_port = int(_read_from_env('DB_PORT','5432')) + + machines = _read_from_env('MACHINES') \ No newline at end of file diff --git a/fab_access/keycloak_handler.py b/fab_access/keycloak_handler.py index 2ba45b6..faddd3f 100644 --- a/fab_access/keycloak_handler.py +++ b/fab_access/keycloak_handler.py @@ -35,7 +35,7 @@ class KeycloakHandler: @staticmethod def user_is_privileged(username): - groups = KeycloakHandler.admin.get_user_groups(user_id=username) + groups = KeycloakHandler.admin.get_user_groups(user_id=KeycloakHandler.admin.get_user_id(username)) groups = [group['name'] for group in groups] if 'Mentoren' in groups: diff --git a/fab_access/main.py b/fab_access/main.py index 35e6ad7..ad95f4f 100644 --- a/fab_access/main.py +++ b/fab_access/main.py @@ -41,7 +41,8 @@ def handle_request(msg, client): print(f'Received `{msg.payload.decode()}` from `{msg.topic}` topic') fabcard_id = json.loads(msg.payload.decode())['UID'] reader_id = msg.topic.split('/')[-1] - + + KeycloakHandler.login() user = KeycloakHandler.get_user_by_card_id(fabcard_id) if not user: MqttHandler.print_to_display(reader_id, 16, fabcard_id) @@ -53,12 +54,13 @@ def handle_request(msg, client): # - get machine_status from database, is 0 for off, 1 for on # - get plug_id from database - print(SQLHandler.get_machine_data(reader_id)) + db_data = SQLHandler.get_machine_data(reader_id) + #print(data) - machine_id = 'space.foo.lazerspacer' - last_user = 'foo.bar' - machine_status = 0 # or 1 - plug_id = 'lazerspacer' + machine_id = db_data["machine_id"] + last_user = data["last_user"] + machine_status = data["machine_status"] + plug_id = data["plug_id"] try: user_permissions = json.loads(user['attributes']['FabPermissions'][0]) @@ -75,11 +77,12 @@ def handle_request(msg, client): username = user['username'] display_name = gen_display_name(user) - if machine_status == 0: + if not machine_status: print(f'Turn Plug {plug_id} on') MqttHandler.switch_plug(plug_id, 1) MqttHandler.print_to_display(reader_id, 20, f'Login\n{display_name}') else: + print("user1"+last_user+"user2"+username) if not (username == last_user or KeycloakHandler.user_is_privileged(username)): MqttHandler.print_to_display(reader_id, 9, last_user) return @@ -90,6 +93,7 @@ def handle_request(msg, client): # TODO Update Database: # - last user # - machine_status + SQLHandler.update_machine(reader_id, last_user, machine_status) MqttHandler.publish(f'/FabLogging/{plug_id}/USER', username) @@ -101,7 +105,6 @@ def main(): MqttHandler.connect_mqtt() MqttHandler.subscribe("/rfid_reader/#") SQLHandler.init_db() - MqttHandler.loop() if __name__ == '__main__': diff --git a/fab_access/sql_handler.py b/fab_access/sql_handler.py index a1058d3..e210d31 100644 --- a/fab_access/sql_handler.py +++ b/fab_access/sql_handler.py @@ -1,5 +1,6 @@ import psycopg2 from psycopg2 import sql, extensions +import json from config import Config @@ -25,16 +26,24 @@ class SQLHandler: # - get last_user from database # - get machine_status from database, is 0 for off, 1 for on # - get plug_id from database - SQLHandler.cursor.execute("SELECT machine_id,last_user,machine_status,plug_id FROM readerplug WHERE reader_id = %s;", (reader_id,)) + SQLHandler.cursor.execute("SELECT * FROM readerplug WHERE readerplug.reader_id = %s;", (reader_id,)) data = [row for row in SQLHandler.cursor.fetchall()] - print(data) - return { - 'machine_id': data[0], - 'last_user': data[1], - 'machine_status': data[2], - 'plug_id': data[3] - } + if(len(data) > 0): + return { + 'machine_id': data[0][2], + 'last_user': data[0][4], + 'machine_status': data[0][3], + 'plug_id': data[0][0] + } + else: + print("No maching Card Reader found in db") + return "Error" + @staticmethod + def update_machine(reader_id,last_user,machine_status): + SQLHandler.cursor.execute("UPDATE readerplug SET machine_status = %s WHERE readerplug.reader_id = %s", (False if machine_status else True,reader_id)) + SQLHandler.cursor.execute("UPDATE readerplug SET last_user = %s WHERE readerplug.reader_id = %s", (last_user,reader_id)) + SQLHandler.conn.commit() @staticmethod def init_db(): @@ -62,4 +71,13 @@ class SQLHandler: )") else: print("Found Table -> Using existing one") + SQLHandler.conn.commit() + + SQLHandler.cursor.execute("SELECT * FROM readerplug;") + if(len(SQLHandler.cursor.fetchall()) < 1): + print("Found no machines in table, adding machines from config") + machines = json.loads(Config.machines) + for machine in machines: + SQLHandler.cursor.execute("INSERT INTO readerplug (reader_id, plug_id, machine_id, machine_status, last_user) VALUES (%s, %s, %s, False, 'no_user');", (machine[0],machine[1],machine[2])) + print("-------------") SQLHandler.conn.commit() \ No newline at end of file From a19a4e1f06e48ca6aa321602c3530ea1d8a14f1c Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Fri, 4 Nov 2022 19:07:35 +0100 Subject: [PATCH 12/12] fix db; Adding ENV Variable --- fab_access/main.py | 23 ++++++----------------- fab_access/sql_handler.py | 1 - 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/fab_access/main.py b/fab_access/main.py index ad95f4f..9a014e0 100644 --- a/fab_access/main.py +++ b/fab_access/main.py @@ -22,19 +22,19 @@ def has_permission(user_permissions, machine_id): return False - def gen_display_name(user): # display names must be not longer than 8 chrs if 'firstName' in user.keys() and 'lastName' in user.keys(): full_name = f'{user["firstName"]} {user["lastName"]}' if len(full_name) > 8: - full_name = 'f{user["firstName"][0]}.{user["lastName"][:6]}' + display_name = f'{user["firstName"][0]}.{user["lastName"][:6]}' else: try: display_name = user['username'][:8] except KeyError: print('user has no username') return 'Error' + return(display_name) def handle_request(msg, client): print('') @@ -48,19 +48,12 @@ def handle_request(msg, client): MqttHandler.print_to_display(reader_id, 16, fabcard_id) return - # TODO Database - # - get machine_id from database - # - get last_user from database - # - get machine_status from database, is 0 for off, 1 for on - # - get plug_id from database - db_data = SQLHandler.get_machine_data(reader_id) - #print(data) machine_id = db_data["machine_id"] - last_user = data["last_user"] - machine_status = data["machine_status"] - plug_id = data["plug_id"] + last_user = db_data["last_user"] + machine_status = db_data["machine_status"] + plug_id = db_data["plug_id"] try: user_permissions = json.loads(user['attributes']['FabPermissions'][0]) @@ -82,7 +75,6 @@ def handle_request(msg, client): MqttHandler.switch_plug(plug_id, 1) MqttHandler.print_to_display(reader_id, 20, f'Login\n{display_name}') else: - print("user1"+last_user+"user2"+username) if not (username == last_user or KeycloakHandler.user_is_privileged(username)): MqttHandler.print_to_display(reader_id, 9, last_user) return @@ -90,10 +82,7 @@ def handle_request(msg, client): MqttHandler.switch_plug(plug_id, 0) MqttHandler.print_to_display(reader_id, 20, f'Bitte anmelden') - # TODO Update Database: - # - last user - # - machine_status - SQLHandler.update_machine(reader_id, last_user, machine_status) + SQLHandler.update_machine(reader_id, username, machine_status) MqttHandler.publish(f'/FabLogging/{plug_id}/USER', username) diff --git a/fab_access/sql_handler.py b/fab_access/sql_handler.py index e210d31..d321a98 100644 --- a/fab_access/sql_handler.py +++ b/fab_access/sql_handler.py @@ -79,5 +79,4 @@ class SQLHandler: machines = json.loads(Config.machines) for machine in machines: SQLHandler.cursor.execute("INSERT INTO readerplug (reader_id, plug_id, machine_id, machine_status, last_user) VALUES (%s, %s, %s, False, 'no_user');", (machine[0],machine[1],machine[2])) - print("-------------") SQLHandler.conn.commit() \ No newline at end of file