From 767c504a1d66ead570898cf0e5f30527453b7ddd Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 15 Nov 2021 15:49:06 +0200 Subject: [PATCH] Move to JSON controlled server settings --- .vscode/settings.json | 3 + pyproject.toml | 2 +- requirements.txt | 1 + .../resources/services/bitwarden.py | 42 +++-- selfprivacy_api/resources/services/gitea.py | 42 +++-- .../resources/services/nextcloud.py | 42 +++-- selfprivacy_api/resources/services/ocserv.py | 42 +++-- selfprivacy_api/resources/services/pleroma.py | 42 +++-- selfprivacy_api/resources/services/ssh.py | 73 ++++---- selfprivacy_api/resources/users.py | 174 ++++++------------ 10 files changed, 226 insertions(+), 237 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de288e1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 07de284..c1feda0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools", "wheel", "portalocker"] build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b2c0098..028c332 100755 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ flask flask_restful flask_socketio setuptools +portalocker diff --git a/selfprivacy_api/resources/services/bitwarden.py b/selfprivacy_api/resources/services/bitwarden.py index a5e2b81..d0d10f9 100644 --- a/selfprivacy_api/resources/services/bitwarden.py +++ b/selfprivacy_api/resources/services/bitwarden.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 from flask_restful import Resource +import portalocker +import json from selfprivacy_api.resources.services import api # Enable Bitwarden class EnableBitwarden(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/passmgr/bitwarden.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = false;", "enable = true;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/passmgr/bitwarden.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "bitwarden" not in data: + data["bitwarden"] = {} + data["bitwarden"]["enable"] = True + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "Bitwarden enabled", } @@ -24,17 +30,21 @@ class EnableBitwarden(Resource): # Disable Bitwarden class DisableBitwarden(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/passmgr/bitwarden.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = true;", "enable = false;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/passmgr/bitwarden.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "bitwarden" not in data: + data["bitwarden"] = {} + data["bitwarden"]["enable"] = False + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "Bitwarden disabled", } diff --git a/selfprivacy_api/resources/services/gitea.py b/selfprivacy_api/resources/services/gitea.py index 1ec84cd..743071f 100644 --- a/selfprivacy_api/resources/services/gitea.py +++ b/selfprivacy_api/resources/services/gitea.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 from flask_restful import Resource +import portalocker +import json from selfprivacy_api.resources.services import api # Enable Gitea class EnableGitea(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/git/gitea.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = false;", "enable = true;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/git/gitea.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "gitea" not in data: + data["gitea"] = {} + data["gitea"]["enable"] = True + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "Gitea enabled", } @@ -24,17 +30,21 @@ class EnableGitea(Resource): # Disable Gitea class DisableGitea(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/git/gitea.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = true;", "enable = false;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/git/gitea.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "gitea" not in data: + data["gitea"] = {} + data["gitea"]["enable"] = False + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "Gitea disabled", } diff --git a/selfprivacy_api/resources/services/nextcloud.py b/selfprivacy_api/resources/services/nextcloud.py index c7b5fa4..899ab5f 100644 --- a/selfprivacy_api/resources/services/nextcloud.py +++ b/selfprivacy_api/resources/services/nextcloud.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 from flask_restful import Resource +import portalocker +import json from selfprivacy_api.resources.services import api # Enable Nextcloud class EnableNextcloud(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/nextcloud/nextcloud.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = false;", "enable = true;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/nextcloud/nextcloud.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "nextcloud" not in data: + data["nextcloud"] = {} + data["nextcloud"]["enable"] = True + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "Nextcloud enabled", } @@ -24,17 +30,21 @@ class EnableNextcloud(Resource): # Disable Nextcloud class DisableNextcloud(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/nextcloud/nextcloud.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = true;", "enable = false;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/nextcloud/nextcloud.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "nextcloud" not in data: + data["nextcloud"] = {} + data["nextcloud"]["enable"] = False + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "Nextcloud disabled", } diff --git a/selfprivacy_api/resources/services/ocserv.py b/selfprivacy_api/resources/services/ocserv.py index a177176..00d9ee2 100644 --- a/selfprivacy_api/resources/services/ocserv.py +++ b/selfprivacy_api/resources/services/ocserv.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 from flask_restful import Resource +import portalocker +import json from selfprivacy_api.resources.services import api # Enable OpenConnect VPN server class EnableOcserv(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/vpn/ocserv.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = false;", "enable = true;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/vpn/ocserv.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "ocserv" not in data: + data["ocserv"] = {} + data["ocserv"]["enable"] = True + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "OpenConnect VPN server enabled", } @@ -24,17 +30,21 @@ class EnableOcserv(Resource): # Disable OpenConnect VPN server class DisableOcserv(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/vpn/ocserv.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = true;", "enable = false;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/vpn/ocserv.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "ocserv" not in data: + data["ocserv"] = {} + data["ocserv"]["enable"] = False + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "OpenConnect VPN server disabled", } diff --git a/selfprivacy_api/resources/services/pleroma.py b/selfprivacy_api/resources/services/pleroma.py index 10ae768..dbc393d 100644 --- a/selfprivacy_api/resources/services/pleroma.py +++ b/selfprivacy_api/resources/services/pleroma.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 from flask_restful import Resource +import portalocker +import json from selfprivacy_api.resources.services import api # Enable Pleroma class EnablePleroma(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/social/pleroma.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = false;", "enable = true;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/social/pleroma.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "pleroma" not in data: + data["pleroma"] = {} + data["pleroma"]["enable"] = True + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "Pleroma enabled", } @@ -24,17 +30,21 @@ class EnablePleroma(Resource): # Disable Pleroma class DisablePleroma(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/social/pleroma.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = true;", "enable = false;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/social/pleroma.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "pleroma" not in data: + data["pleroma"] = {} + data["pleroma"]["enable"] = False + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "Pleroma disabled", } diff --git a/selfprivacy_api/resources/services/ssh.py b/selfprivacy_api/resources/services/ssh.py index 96ad4c5..953511e 100644 --- a/selfprivacy_api/resources/services/ssh.py +++ b/selfprivacy_api/resources/services/ssh.py @@ -1,23 +1,29 @@ #!/usr/bin/env python3 from flask import Blueprint, request from flask_restful import Resource, reqparse +import portalocker +import json from selfprivacy_api.resources.services import api # Enable SSH class EnableSSH(Resource): def post(self): - readOnlyFileDescriptor = open("/etc/nixos/configuration.nix", "rt") - fileContent = readOnlyFileDescriptor.read() - fileContent = fileContent.replace("enable = false;", "enable = true;") - readOnlyFileDescriptor.close() - readWriteFileDescriptor = open("/etc/nixos/configuration.nix", "wt") - writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) - readWriteFileDescriptor.close() + with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "ssh" not in data: + data["ssh"] = {} + data["ssh"]["enable"] = True + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": writeOperationDescriptor, "message": "SSH enabled", } @@ -33,41 +39,28 @@ class WriteSSHKey(Resource): publicKey = args["public_key"] - print("[INFO] Opening /etc/nixos/configuration.nix...", sep="") - readOnlyFileDescriptor = open("/etc/nixos/configuration.nix", "r") - print("done") - fileContent = list() - index = int(0) - - print("[INFO] Reading file content...", sep="") - - while True: - line = readOnlyFileDescriptor.readline() - - if not line: - break - else: - fileContent.append(line) - print("[DEBUG] Read line!") - - for line in fileContent: - index += 1 - if "openssh.authorizedKeys.keys = [" in line: - print("[DEBUG] Found SSH key configuration snippet match!") - print("[INFO] Writing new SSH key", sep="") - fileContent.append('\n "' + publicKey + '"') - print("done") - break - - print("[INFO] Writing data from memory to file...", sep="") - readWriteFileDescriptor = open("/etc/nixos/configuration.nix", "w") - print("done") - operationResult = readWriteFileDescriptor.writelines(fileContent) + with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + if "ssh" not in data: + data["ssh"] = {} + # Return 400 if key already in array + for key in data["ssh"]["rootSshKeys"]: + if key == publicKey: + return { + "error": "Key already exists", + }, 400 + data["ssh"]["rootSshKeys"].append(publicKey) + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) return { "status": 0, - "descriptor": operationResult, - "message": "New SSH key successfully written to /etc/nixos/configuration.nix", + "message": "New SSH key successfully written", } diff --git a/selfprivacy_api/resources/users.py b/selfprivacy_api/resources/users.py index 002927f..66e4360 100644 --- a/selfprivacy_api/resources/users.py +++ b/selfprivacy_api/resources/users.py @@ -2,6 +2,9 @@ from flask import Blueprint, jsonify, request from flask_restful import Resource, Api import subprocess +import portalocker +import json +import re from selfprivacy_api import resources @@ -24,123 +27,62 @@ class Users(Resource): hashedPassword = hashedPassword.decode("ascii") hashedPassword = hashedPassword.rstrip() - print("[TRACE] {0}".format(hashedPassword)) - - print("[INFO] Opening /etc/nixos/users.nix...", sep="") - readOnlyFileDescriptor = open("/etc/nixos/users.nix", "r") - print("done") - fileContent = list() - index = int(0) - - print("[INFO] Reading file content...", sep="") - - while True: - line = readOnlyFileDescriptor.readline() - - if not line: - break - else: - fileContent.append(line) - print("[DEBUG] Read line!") - - userTemplate = """ - - #begin - \"{0}\" = {{ - isNormalUser = true; - hashedPassword = \"{1}\"; - }}; - #end - """.format( - request.headers.get("X-User"), hashedPassword - ) - - mailUserTemplate = """ - \"{0}@{2}\" = {{ - hashedPassword = - \"{1}\"; - catchAll = [ \"{2}\" ]; - - sieveScript = '' - require [\"fileinto\", \"mailbox\"]; - if header :contains \"Chat-Version\" \"1.0\" - {{ - fileinto :create \"DeltaChat\"; - stop; - }} - ''; - }};""".format( - request.headers.get("X-User"), - hashedPassword, - request.headers.get("X-Domain"), - ) - - for line in fileContent: - index += 1 - if line.startswith(" #begin"): - print("[DEBUG] Found user configuration snippet match!") - print( - "[INFO] Writing new user configuration snippet to memory...", sep="" + with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + # Return 400 if username is not provided + if request.headers.get("X-User") is None: + return {"error": "username is required"}, 400 + # Return 400 if password is not provided + if request.headers.get("X-Password") is None: + return {"error": "password is required"}, 400 + # Check is username passes regex + if not re.match(r"^[a-z_][a-z0-9_]+$", request.headers.get("X-User")): + return {"error": "username must be alphanumeric"}, 400 + # Check if username less than 32 characters + if len(request.headers.get("X-User")) > 32: + return {"error": "username must be less than 32 characters"}, 400 + # Return 400 if user already exists + for user in data["users"]: + if user["username"] == request.headers.get("X-User"): + return {"error": "User already exists"}, 400 + if "users" not in data: + data["users"] = [] + data["users"].append( + { + "username": request.headers.get("X-User"), + "hashedPassword": hashedPassword, + } ) - fileContent.insert(index - 1, userTemplate) - print("done") - break + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) - print("[INFO] Writing data from memory to file...", sep="") - readWriteFileDescriptor = open("/etc/nixos/users.nix", "w") - userConfigurationWriteOperationResult = readWriteFileDescriptor.writelines( - fileContent - ) - print("done") - - readOnlyFileDescriptor.close() - readWriteFileDescriptor.close() - - print( - "[INFO] Opening /etc/nixos/mailserver/system/mailserver.nix.nix for reading...", - sep="", - ) - readOnlyFileDescriptor = open("/etc/nixos/mailserver/system/mailserver.nix") - print("done") - - fileContent = list() - index = int(0) - - while True: - line = readOnlyFileDescriptor.readline() - - if not line: - break - else: - fileContent.append(line) - print("[DEBUG] Read line!") - - for line in fileContent: - if line.startswith(" loginAccounts = {"): - print("[DEBUG] Found mailuser configuration snippet match!") - print( - "[INFO] Writing new user configuration snippet to memory...", sep="" - ) - fileContent.insert(index + 1, mailUserTemplate) - print("done") - break - index += 1 - - readWriteFileDescriptor = open( - "/etc/nixos/mailserver/system/mailserver.nix", "w" - ) - - mailUserConfigurationWriteOperationResult = readWriteFileDescriptor.writelines( - fileContent - ) - - return { - "result": 0, - "descriptor0": userConfigurationWriteOperationResult, - "descriptor1": mailUserConfigurationWriteOperationResult, - } + return {"result": 0} def delete(self): - user = subprocess.Popen(["userdel", request.headers.get("X-User")]) - user.communicate()[0] - return user.returncode + with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f: + portalocker.lock(f, portalocker.LOCK_EX) + try: + data = json.load(f) + # Return 400 if username is not provided + if request.headers.get("X-User") is None: + return {"error": "username is required"}, 400 + # Return 400 if user does not exist + for user in data["users"]: + if user["username"] == request.headers.get("X-User"): + data["users"].remove(user) + break + else: + return {"error": "User does not exist"}, 400 + + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + finally: + portalocker.unlock(f) + + return {"result": 0}