From ec7ff62d5902363190a641ec5ed67980e4940bcf Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 22 Nov 2021 18:50:50 +0200 Subject: [PATCH 01/15] Add SSH and system settings endpoints --- selfprivacy_api/app.py | 4 +- selfprivacy_api/resources/common.py | 2 +- .../resources/services/bitwarden.py | 39 +- selfprivacy_api/resources/services/gitea.py | 39 +- .../resources/services/nextcloud.py | 39 +- selfprivacy_api/resources/services/ocserv.py | 39 +- selfprivacy_api/resources/services/pleroma.py | 39 +- selfprivacy_api/resources/services/ssh.py | 359 ++++++++++++++++-- selfprivacy_api/resources/system.py | 140 ++++++- selfprivacy_api/resources/users.py | 93 ++--- selfprivacy_api/utils.py | 42 ++ 11 files changed, 583 insertions(+), 252 deletions(-) diff --git a/selfprivacy_api/app.py b/selfprivacy_api/app.py index e7c8f92..ab412c9 100644 --- a/selfprivacy_api/app.py +++ b/selfprivacy_api/app.py @@ -22,6 +22,8 @@ def create_app(): api = Api(app) app.config["AUTH_TOKEN"] = os.environ.get("AUTH_TOKEN") + if app.config["AUTH_TOKEN"] is None: + raise ValueError("AUTH_TOKEN is not set") app.config["ENABLE_SWAGGER"] = os.environ.get("ENABLE_SWAGGER", "0") # Check bearer token @@ -49,7 +51,7 @@ def create_app(): def spec(): if app.config["ENABLE_SWAGGER"] == "1": swag = swagger(app) - swag["info"]["version"] = "1.0.0" + swag["info"]["version"] = "1.1.0" swag["info"]["title"] = "SelfPrivacy API" swag["info"]["description"] = "SelfPrivacy API" swag["securityDefinitions"] = { diff --git a/selfprivacy_api/resources/common.py b/selfprivacy_api/resources/common.py index fa96a39..a9663aa 100644 --- a/selfprivacy_api/resources/common.py +++ b/selfprivacy_api/resources/common.py @@ -24,7 +24,7 @@ class ApiVersion(Resource): 401: description: Unauthorized """ - return {"version": "1.0.0"} + return {"version": "1.1.0"} class DecryptDisk(Resource): diff --git a/selfprivacy_api/resources/services/bitwarden.py b/selfprivacy_api/resources/services/bitwarden.py index 5c037c9..412ba8a 100644 --- a/selfprivacy_api/resources/services/bitwarden.py +++ b/selfprivacy_api/resources/services/bitwarden.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 """Bitwarden management module""" -import json -import portalocker from flask_restful import Resource from selfprivacy_api.resources.services import api +from selfprivacy_api.utils import WriteUserData class EnableBitwarden(Resource): @@ -24,20 +23,10 @@ class EnableBitwarden(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "bitwarden" not in data: - data["bitwarden"] = {} - data["bitwarden"]["enable"] = True - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "bitwarden" not in data: + data["bitwarden"] = {} + data["bitwarden"]["enable"] = True return { "status": 0, @@ -62,20 +51,10 @@ class DisableBitwarden(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "bitwarden" not in data: - data["bitwarden"] = {} - data["bitwarden"]["enable"] = False - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "bitwarden" not in data: + data["bitwarden"] = {} + data["bitwarden"]["enable"] = False return { "status": 0, diff --git a/selfprivacy_api/resources/services/gitea.py b/selfprivacy_api/resources/services/gitea.py index 4ae0b6a..bd4b8de 100644 --- a/selfprivacy_api/resources/services/gitea.py +++ b/selfprivacy_api/resources/services/gitea.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 """Gitea management module""" -import json -import portalocker from flask_restful import Resource from selfprivacy_api.resources.services import api +from selfprivacy_api.utils import WriteUserData class EnableGitea(Resource): @@ -24,20 +23,10 @@ class EnableGitea(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "gitea" not in data: - data["gitea"] = {} - data["gitea"]["enable"] = True - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "gitea" not in data: + data["gitea"] = {} + data["gitea"]["enable"] = True return { "status": 0, @@ -62,20 +51,10 @@ class DisableGitea(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "gitea" not in data: - data["gitea"] = {} - data["gitea"]["enable"] = False - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "gitea" not in data: + data["gitea"] = {} + data["gitea"]["enable"] = False return { "status": 0, diff --git a/selfprivacy_api/resources/services/nextcloud.py b/selfprivacy_api/resources/services/nextcloud.py index fc0bdbe..3aa9d06 100644 --- a/selfprivacy_api/resources/services/nextcloud.py +++ b/selfprivacy_api/resources/services/nextcloud.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 """Nextcloud management module""" -import json -import portalocker from flask_restful import Resource from selfprivacy_api.resources.services import api +from selfprivacy_api.utils import WriteUserData class EnableNextcloud(Resource): @@ -24,20 +23,10 @@ class EnableNextcloud(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "nextcloud" not in data: - data["nextcloud"] = {} - data["nextcloud"]["enable"] = True - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "nextcloud" not in data: + data["nextcloud"] = {} + data["nextcloud"]["enable"] = True return { "status": 0, @@ -62,20 +51,10 @@ class DisableNextcloud(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "nextcloud" not in data: - data["nextcloud"] = {} - data["nextcloud"]["enable"] = False - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "nextcloud" not in data: + data["nextcloud"] = {} + data["nextcloud"]["enable"] = False return { "status": 0, diff --git a/selfprivacy_api/resources/services/ocserv.py b/selfprivacy_api/resources/services/ocserv.py index 6ef5667..4dc83da 100644 --- a/selfprivacy_api/resources/services/ocserv.py +++ b/selfprivacy_api/resources/services/ocserv.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 """OpenConnect VPN server management module""" -import json -import portalocker from flask_restful import Resource from selfprivacy_api.resources.services import api +from selfprivacy_api.utils import WriteUserData class EnableOcserv(Resource): @@ -24,20 +23,10 @@ class EnableOcserv(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "ocserv" not in data: - data["ocserv"] = {} - data["ocserv"]["enable"] = True - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "ocserv" not in data: + data["ocserv"] = {} + data["ocserv"]["enable"] = True return { "status": 0, @@ -62,20 +51,10 @@ class DisableOcserv(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "ocserv" not in data: - data["ocserv"] = {} - data["ocserv"]["enable"] = False - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "ocserv" not in data: + data["ocserv"] = {} + data["ocserv"]["enable"] = False return { "status": 0, diff --git a/selfprivacy_api/resources/services/pleroma.py b/selfprivacy_api/resources/services/pleroma.py index 201a5a6..aaf08f0 100644 --- a/selfprivacy_api/resources/services/pleroma.py +++ b/selfprivacy_api/resources/services/pleroma.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 """Pleroma management module""" -import json -import portalocker from flask_restful import Resource from selfprivacy_api.resources.services import api +from selfprivacy_api.utils import WriteUserData class EnablePleroma(Resource): @@ -24,20 +23,10 @@ class EnablePleroma(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "pleroma" not in data: - data["pleroma"] = {} - data["pleroma"]["enable"] = True - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "pleroma" not in data: + data["pleroma"] = {} + data["pleroma"]["enable"] = True return { "status": 0, @@ -62,20 +51,10 @@ class DisablePleroma(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "pleroma" not in data: - data["pleroma"] = {} - data["pleroma"]["enable"] = False - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "pleroma" not in data: + data["pleroma"] = {} + data["pleroma"]["enable"] = False return { "status": 0, diff --git a/selfprivacy_api/resources/services/ssh.py b/selfprivacy_api/resources/services/ssh.py index 86ecc90..d924660 100644 --- a/selfprivacy_api/resources/services/ssh.py +++ b/selfprivacy_api/resources/services/ssh.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 """SSH management module""" -import json -import portalocker from flask_restful import Resource, reqparse from selfprivacy_api.resources.services import api +from selfprivacy_api.utils import WriteUserData, ReadUserData class EnableSSH(Resource): @@ -24,20 +23,10 @@ class EnableSSH(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "ssh" not in data: - data["ssh"] = {} - data["ssh"]["enable"] = True - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + if "ssh" not in data: + data["ssh"] = {} + data["ssh"]["enable"] = True return { "status": 0, @@ -45,6 +34,82 @@ class EnableSSH(Resource): } +class SSHSettings(Resource): + """Enable/disable SSH""" + + def get(self): + """ + Get current SSH settings + --- + tags: + - SSH + security: + - bearerAuth: [] + responses: + 200: + description: SSH settings + 400: + description: Bad request + """ + with ReadUserData() as data: + if "ssh" not in data: + return {"enable": True, "passwordAuthentication": True} + if "enable" not in data["ssh"]: + data["ssh"]["enable"] = True + if "passwordAuthentication" not in data["ssh"]: + data["ssh"]["passwordAuthentication"] = True + return { + "enable": data["ssh"]["enable"], + "passwordAuthentication": data["ssh"]["passwordAuthentication"], + } + + def put(self): + """ + Change SSH settings + --- + tags: + - SSH + security: + - bearerAuth: [] + parameters: + - name: sshSettings + in: body + required: true + description: SSH settings + schema: + type: object + required: + - enable + - passwordAuthentication + properties: + enable: + type: boolean + passwordAuthentication: + type: boolean + responses: + 200: + description: New settings saved + 400: + description: Bad request + """ + parser = reqparse.RequestParser() + parser.add_argument("enable", type=bool, required=False) + parser.add_argument("passwordAuthentication", type=bool, required=False) + args = parser.parse_args() + enable = args["enable"] + password_authentication = args["passwordAuthentication"] + + with WriteUserData() as data: + if "ssh" not in data: + data["ssh"] = {} + if enable is not None: + data["ssh"]["enable"] = enable + if password_authentication is not None: + data["ssh"]["passwordAuthentication"] = password_authentication + + return "SSH settings changed" + + class WriteSSHKey(Resource): """Write new SSH key""" @@ -89,28 +154,26 @@ class WriteSSHKey(Resource): public_key = args["public_key"] - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - if "ssh" not in data: - data["ssh"] = {} - if "rootKeys" not in data["ssh"]: - data["ssh"]["rootKeys"] = [] - # Return 409 if key already in array - for key in data["ssh"]["rootKeys"]: - if key == public_key: - return { - "error": "Key already exists", - }, 409 - data["ssh"]["rootKeys"].append(public_key) - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + # Validate SSH public key + # It may be ssh-ed25519 or ssh-rsa + if not public_key.startswith("ssh-ed25519"): + if not public_key.startswith("ssh-rsa"): + return { + "error": "Invalid key type. Only ssh-ed25519 and ssh-rsa are supported.", + }, 400 + + with WriteUserData() as data: + if "ssh" not in data: + data["ssh"] = {} + if "rootKeys" not in data["ssh"]: + data["ssh"]["rootKeys"] = [] + # Return 409 if key already in array + for key in data["ssh"]["rootKeys"]: + if key == public_key: + return { + "error": "Key already exists", + }, 409 + data["ssh"]["rootKeys"].append(public_key) return { "status": 0, @@ -118,5 +181,225 @@ class WriteSSHKey(Resource): }, 201 +class SSHKeys(Resource): + """List SSH keys""" + + def get(self, username): + """ + List SSH keys + --- + tags: + - SSH + security: + - bearerAuth: [] + parameters: + - in: path + name: username + type: string + required: true + description: User to list keys for + responses: + 200: + description: SSH keys + 401: + description: Unauthorized + """ + with ReadUserData() as data: + if username == "root": + if "ssh" not in data: + data["ssh"] = {} + if "rootKeys" not in data["ssh"]: + data["ssh"]["rootKeys"] = [] + return data["ssh"]["rootKeys"] + if username == data["username"]: + if "sshKeys" not in data: + data["sshKeys"] = [] + return data["sshKeys"] + else: + if "users" not in data: + data["users"] = [] + for user in data["users"]: + if user["name"] == username: + if "sshKeys" not in user: + user["sshKeys"] = [] + return user["ssh"]["sshKeys"] + return { + "error": "User not found", + }, 404 + + def post(self, username): + """ + Add SSH key to the user + --- + tags: + - SSH + security: + - bearerAuth: [] + parameters: + - in: body + required: true + name: public_key + schema: + type: object + required: + - public_key + properties: + public_key: + type: string + - in: path + name: username + type: string + required: true + description: User to add keys for + responses: + 201: + description: SSH key added + 401: + description: Unauthorized + 404: + description: User not found + 409: + description: Key already exists + """ + parser = reqparse.RequestParser() + parser.add_argument( + "public_key", type=str, required=True, help="Key cannot be blank!" + ) + args = parser.parse_args() + + if username == "root": + return { + "error": "Use /ssh/key/send to add root keys", + }, 400 + + # Validate SSH public key + # It may be ssh-ed25519 or ssh-rsa + if not args["public_key"].startswith("ssh-ed25519"): + if not args["public_key"].startswith("ssh-rsa"): + return { + "error": "Invalid key type. Only ssh-ed25519 and ssh-rsa are supported.", + }, 400 + + with WriteUserData() as data: + if username == data["username"]: + if "sshKeys" not in data: + data["sshKeys"] = [] + data["sshKeys"].append(args["public_key"]) + return { + "message": "New SSH key successfully written", + }, 201 + + if "users" not in data: + data["users"] = [] + for user in data["users"]: + if user["username"] == username: + if "sshKeys" not in user: + user["sshKeys"] = [] + # Return 409 if key already in array + for key in user["sshKeys"]: + if key == args["public_key"]: + return { + "error": "Key already exists", + }, 409 + user["sshKeys"].append(args["public_key"]) + return { + "message": "New SSH key successfully written", + }, 201 + return { + "error": "User not found", + }, 404 + + def delete(self, username): + """ + Delete SSH key + --- + tags: + - SSH + security: + - bearerAuth: [] + parameters: + - in: body + name: public_key + required: true + description: Key to delete + schema: + type: object + required: + - public_key + properties: + public_key: + type: string + - in: path + name: username + type: string + required: true + description: User to delete keys for + responses: + 200: + description: SSH key deleted + 401: + description: Unauthorized + 404: + description: Key not found + """ + parser = reqparse.RequestParser() + parser.add_argument( + "public_key", type=str, required=True, help="Key cannot be blank!" + ) + args = parser.parse_args() + + with WriteUserData() as data: + if username == "root": + if "ssh" not in data: + data["ssh"] = {} + if "rootKeys" not in data["ssh"]: + data["ssh"]["rootKeys"] = [] + # Return 404 if key not in array + for key in data["ssh"]["rootKeys"]: + if key == args["public_key"]: + data["ssh"]["rootKeys"].remove(key) + return { + "message": "SSH key deleted", + }, 200 + return { + "error": "Key not found", + }, 404 + if username == data["username"]: + if "sshKeys" not in data: + data["sshKeys"] = [] + # Return 404 if key not in array + for key in data["sshKeys"]: + if key == args["public_key"]: + data["sshKeys"].remove(key) + return { + "message": "SSH key deleted", + }, 200 + return { + "error": "Key not found", + }, 404 + if "users" not in data: + data["users"] = [] + for user in data["users"]: + if user["username"] == username: + if "sshKeys" not in user: + user["sshKeys"] = [] + # Return 404 if key not in array + for key in user["sshKeys"]: + if key == args["public_key"]: + user["sshKeys"].remove(key) + return { + "message": "SSH key successfully deleted", + }, 200 + return { + "error": "Key not found", + }, 404 + return { + "error": "User not found", + }, 404 + + api.add_resource(EnableSSH, "/ssh/enable") +api.add_resource(SSHSettings, "/ssh") + api.add_resource(WriteSSHKey, "/ssh/key/send") +api.add_resource(SSHKeys, "/ssh/keys/") diff --git a/selfprivacy_api/resources/system.py b/selfprivacy_api/resources/system.py index 7c3ad77..ddce3be 100644 --- a/selfprivacy_api/resources/system.py +++ b/selfprivacy_api/resources/system.py @@ -1,13 +1,149 @@ #!/usr/bin/env python3 """System management module""" import subprocess +import pytz from flask import Blueprint -from flask_restful import Resource, Api +from flask_restful import Resource, Api, reqparse + +from selfprivacy_api.utils import WriteUserData, ReadUserData api_system = Blueprint("system", __name__, url_prefix="/system") api = Api(api_system) +class Timezone(Resource): + """Change timezone of NixOS""" + + def get(self): + """ + Get current system timezone + --- + tags: + - System + security: + - bearerAuth: [] + responses: + 200: + description: Timezone + 400: + description: Bad request + """ + with ReadUserData() as data: + if "timezone" not in data: + return "Europe/Uzhgorod" + return data["timezone"] + + def put(self): + """ + Change system timezone + --- + tags: + - System + security: + - bearerAuth: [] + parameters: + - name: timezone + in: body + required: true + description: Timezone to set + schema: + type: object + required: + - timezone + properties: + timezone: + type: string + responses: + 200: + description: Timezone changed + 400: + description: Bad request + """ + parser = reqparse.RequestParser() + parser.add_argument("timezone", type=str, required=True) + timezone = parser.parse_args()["timezone"] + + # Check if timezone is a valid tzdata string + if timezone not in pytz.all_timezones: + return {"error": "Invalid timezone"}, 400 + + with WriteUserData() as data: + data["timezone"] = timezone + return "Timezone changed" + + +class AutoUpgrade(Resource): + """Enable/disable automatic upgrades and reboots""" + + def get(self): + """ + Get current system autoupgrade settings + --- + tags: + - System + security: + - bearerAuth: [] + responses: + 200: + description: Auto-upgrade settings + 400: + description: Bad request + """ + with ReadUserData() as data: + if "autoUpgrade" not in data: + return {"enable": True, "allowReboot": False} + if "enable" not in data["autoUpgrade"]: + data["autoUpgrade"]["enable"] = True + if "allowReboot" not in data["autoUpgrade"]: + data["autoUpgrade"]["allowReboot"] = False + return data["autoUpgrade"] + + def put(self): + """ + Change system auto upgrade settings + --- + tags: + - System + security: + - bearerAuth: [] + parameters: + - name: autoUpgrade + in: body + required: true + description: Auto upgrade settings + schema: + type: object + required: + - enable + - allowReboot + properties: + enable: + type: boolean + allowReboot: + type: boolean + responses: + 200: + description: New settings saved + 400: + description: Bad request + """ + parser = reqparse.RequestParser() + parser.add_argument("enable", type=bool, required=False) + parser.add_argument("allowReboot", type=bool, required=False) + args = parser.parse_args() + enable = args["enable"] + allow_reboot = args["allowReboot"] + + with WriteUserData() as data: + if "autoUpgrade" not in data: + data["autoUpgrade"] = {} + if enable is not None: + data["autoUpgrade"]["enable"] = enable + if allow_reboot is not None: + data["autoUpgrade"]["allowReboot"] = allow_reboot + return "Auto-upgrade settings changed" + + class RebuildSystem(Resource): """Rebuild NixOS""" @@ -145,6 +281,8 @@ class PythonVersion(Resource): return subprocess.check_output(["python", "-V"]).decode("utf-8").strip() +api.add_resource(Timezone, "/configuration/timezone") +api.add_resource(AutoUpgrade, "/configuration/autoUpgrade") api.add_resource(RebuildSystem, "/configuration/apply") api.add_resource(RollbackSystem, "/configuration/rollback") api.add_resource(UpgradeSystem, "/configuration/upgrade") diff --git a/selfprivacy_api/resources/users.py b/selfprivacy_api/resources/users.py index 2f373bc..057a5e3 100644 --- a/selfprivacy_api/resources/users.py +++ b/selfprivacy_api/resources/users.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """Users management module""" import subprocess -import json import re -import portalocker from flask_restful import Resource, reqparse +from selfprivacy_api.utils import WriteUserData, ReadUserData + class Users(Resource): """Users management""" @@ -24,17 +24,10 @@ class Users(Resource): 401: description: Unauthorized """ - with open( - "/etc/nixos/userdata/userdata.json", "r", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_SH) - try: - data = json.load(userdata_file) - users = [] - for user in data["users"]: - users.append(user["username"]) - finally: - portalocker.unlock(userdata_file) + with ReadUserData() as data: + users = [] + for user in data["users"]: + users.append(user["username"]) return users def post(self): @@ -97,32 +90,21 @@ class Users(Resource): if len(args["username"]) > 32: return {"error": "username must be less than 32 characters"}, 400 - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) + with WriteUserData() as data: + if "users" not in data: + data["users"] = [] - if "users" not in data: - data["users"] = [] + # Return 400 if user already exists + for user in data["users"]: + if user["username"] == args["username"]: + return {"error": "User already exists"}, 409 - # Return 400 if user already exists - for user in data["users"]: - if user["username"] == args["username"]: - return {"error": "User already exists"}, 409 - - data["users"].append( - { - "username": args["username"], - "hashedPassword": hashed_password, - } - ) - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + data["users"].append( + { + "username": args["username"], + "hashedPassword": hashed_password, + } + ) return {"result": 0, "username": args["username"]}, 201 @@ -154,29 +136,18 @@ class User(Resource): 404: description: User not found """ - with open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" - ) as userdata_file: - portalocker.lock(userdata_file, portalocker.LOCK_EX) - try: - data = json.load(userdata_file) - # Return 400 if username is not provided - if username is None: - return {"error": "username is required"}, 400 - if username == data["username"]: - return {"error": "Cannot delete root user"}, 400 - # Return 400 if user does not exist - for user in data["users"]: - if user["username"] == username: - data["users"].remove(user) - break - else: - return {"error": "User does not exist"}, 404 - - userdata_file.seek(0) - json.dump(data, userdata_file, indent=4) - userdata_file.truncate() - finally: - portalocker.unlock(userdata_file) + with WriteUserData() as data: + # Return 400 if username is not provided + if username is None: + return {"error": "username is required"}, 400 + if username == data["username"]: + return {"error": "Cannot delete root user"}, 400 + # Return 400 if user does not exist + for user in data["users"]: + if user["username"] == username: + data["users"].remove(user) + break + else: + return {"error": "User does not exist"}, 404 return {"result": 0, "username": username} diff --git a/selfprivacy_api/utils.py b/selfprivacy_api/utils.py index b7ef2a8..8a8006c 100644 --- a/selfprivacy_api/utils.py +++ b/selfprivacy_api/utils.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 """Various utility functions""" +import json +import portalocker def get_domain(): @@ -7,3 +9,43 @@ def get_domain(): with open("/var/domain", "r", encoding="utf-8") as domain_file: domain = domain_file.readline().rstrip() return domain + + +class WriteUserData(object): + """Write userdata.json with lock""" + + def __init__(self): + self.userdata_file = open( + "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" + ) + portalocker.lock(self.userdata_file, portalocker.LOCK_EX) + self.data = json.load(self.userdata_file) + + def __enter__(self): + return self.data + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is None: + self.userdata_file.seek(0) + json.dump(self.data, self.userdata_file, indent=4) + self.userdata_file.truncate() + portalocker.unlock(self.userdata_file) + self.userdata_file.close() + + +class ReadUserData(object): + """Read userdata.json with lock""" + + def __init__(self): + self.userdata_file = open( + "/etc/nixos/userdata/userdata.json", "r", encoding="utf-8" + ) + portalocker.lock(self.userdata_file, portalocker.LOCK_SH) + self.data = json.load(self.userdata_file) + + def __enter__(self): + return self.data + + def __exit__(self, *args): + portalocker.unlock(self.userdata_file) + self.userdata_file.close() From b185724000dafcfbbb7b5eb52acf20f52228269d Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 23 Nov 2021 20:32:51 +0200 Subject: [PATCH 02/15] Move SSH key validation to utils --- selfprivacy_api/resources/services/ssh.py | 24 +++++++++-------------- selfprivacy_api/utils.py | 9 +++++++++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/selfprivacy_api/resources/services/ssh.py b/selfprivacy_api/resources/services/ssh.py index d924660..8cc96d9 100644 --- a/selfprivacy_api/resources/services/ssh.py +++ b/selfprivacy_api/resources/services/ssh.py @@ -3,7 +3,7 @@ from flask_restful import Resource, reqparse from selfprivacy_api.resources.services import api -from selfprivacy_api.utils import WriteUserData, ReadUserData +from selfprivacy_api.utils import WriteUserData, ReadUserData, validate_ssh_public_key class EnableSSH(Resource): @@ -154,13 +154,10 @@ class WriteSSHKey(Resource): public_key = args["public_key"] - # Validate SSH public key - # It may be ssh-ed25519 or ssh-rsa - if not public_key.startswith("ssh-ed25519"): - if not public_key.startswith("ssh-rsa"): - return { - "error": "Invalid key type. Only ssh-ed25519 and ssh-rsa are supported.", - }, 400 + if not validate_ssh_public_key(public_key): + return { + "error": "Invalid key type. Only ssh-ed25519 and ssh-rsa are supported.", + }, 400 with WriteUserData() as data: if "ssh" not in data: @@ -272,13 +269,10 @@ class SSHKeys(Resource): "error": "Use /ssh/key/send to add root keys", }, 400 - # Validate SSH public key - # It may be ssh-ed25519 or ssh-rsa - if not args["public_key"].startswith("ssh-ed25519"): - if not args["public_key"].startswith("ssh-rsa"): - return { - "error": "Invalid key type. Only ssh-ed25519 and ssh-rsa are supported.", - }, 400 + if not validate_ssh_public_key(args["public_key"]): + return { + "error": "Invalid key type. Only ssh-ed25519 and ssh-rsa are supported.", + }, 400 with WriteUserData() as data: if username == data["username"]: diff --git a/selfprivacy_api/utils.py b/selfprivacy_api/utils.py index 8a8006c..a2953b1 100644 --- a/selfprivacy_api/utils.py +++ b/selfprivacy_api/utils.py @@ -49,3 +49,12 @@ class ReadUserData(object): def __exit__(self, *args): portalocker.unlock(self.userdata_file) self.userdata_file.close() + + +def validate_ssh_public_key(key): + """Validate SSH public key. It may be ssh-ed25519 or ssh-rsa.""" + if not key.startswith("ssh-ed25519"): + if not key.startswith("ssh-rsa"): + return False + return True + \ No newline at end of file From 245964c9982476afb27e19fc5b65854dc1f0bc8e Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 29 Nov 2021 22:16:08 +0300 Subject: [PATCH 03/15] First wave of unit tests, and bugfixes caused by them --- requirements.txt | 6 + selfprivacy_api/app.py | 13 +- .../resources/services/mailserver.py | 20 +- selfprivacy_api/resources/services/ssh.py | 6 + selfprivacy_api/utils.py | 7 +- tests/__init__.py | 0 tests/conftest.py | 38 +++ tests/services/test_bitwarden.py | 80 ++++++ .../test_bitwarden/enable_undefined.json | 51 ++++ tests/services/test_bitwarden/turned_off.json | 52 ++++ tests/services/test_bitwarden/turned_on.json | 52 ++++ tests/services/test_bitwarden/undefined.json | 49 ++++ tests/services/test_gitea.py | 80 ++++++ .../services/test_gitea/enable_undefined.json | 51 ++++ tests/services/test_gitea/turned_off.json | 52 ++++ tests/services/test_gitea/turned_on.json | 52 ++++ tests/services/test_gitea/undefined.json | 49 ++++ tests/services/test_mailserver.py | 65 +++++ tests/services/test_nextcloud.py | 80 ++++++ .../test_nextcloud/enable_undefined.json | 51 ++++ tests/services/test_nextcloud/turned_off.json | 52 ++++ tests/services/test_nextcloud/turned_on.json | 52 ++++ tests/services/test_nextcloud/undefined.json | 44 +++ tests/services/test_ocserv.py | 80 ++++++ .../test_ocserv/enable_undefined.json | 51 ++++ tests/services/test_ocserv/turned_off.json | 52 ++++ tests/services/test_ocserv/turned_on.json | 52 ++++ tests/services/test_ocserv/undefined.json | 49 ++++ tests/services/test_pleroma.py | 80 ++++++ .../test_pleroma/enable_undefined.json | 51 ++++ tests/services/test_pleroma/turned_off.json | 52 ++++ tests/services/test_pleroma/turned_on.json | 52 ++++ tests/services/test_pleroma/undefined.json | 49 ++++ tests/services/test_services.py | 131 +++++++++ tests/services/test_ssh.py | 262 ++++++++++++++++++ tests/services/test_ssh/all_off.json | 52 ++++ .../test_ssh/root_and_admin_have_keys.json | 52 ++++ tests/services/test_ssh/turned_off.json | 46 +++ tests/services/test_ssh/turned_on.json | 46 +++ tests/services/test_ssh/undefined.json | 45 +++ 40 files changed, 2190 insertions(+), 14 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/services/test_bitwarden.py create mode 100644 tests/services/test_bitwarden/enable_undefined.json create mode 100644 tests/services/test_bitwarden/turned_off.json create mode 100644 tests/services/test_bitwarden/turned_on.json create mode 100644 tests/services/test_bitwarden/undefined.json create mode 100644 tests/services/test_gitea.py create mode 100644 tests/services/test_gitea/enable_undefined.json create mode 100644 tests/services/test_gitea/turned_off.json create mode 100644 tests/services/test_gitea/turned_on.json create mode 100644 tests/services/test_gitea/undefined.json create mode 100644 tests/services/test_mailserver.py create mode 100644 tests/services/test_nextcloud.py create mode 100644 tests/services/test_nextcloud/enable_undefined.json create mode 100644 tests/services/test_nextcloud/turned_off.json create mode 100644 tests/services/test_nextcloud/turned_on.json create mode 100644 tests/services/test_nextcloud/undefined.json create mode 100644 tests/services/test_ocserv.py create mode 100644 tests/services/test_ocserv/enable_undefined.json create mode 100644 tests/services/test_ocserv/turned_off.json create mode 100644 tests/services/test_ocserv/turned_on.json create mode 100644 tests/services/test_ocserv/undefined.json create mode 100644 tests/services/test_pleroma.py create mode 100644 tests/services/test_pleroma/enable_undefined.json create mode 100644 tests/services/test_pleroma/turned_off.json create mode 100644 tests/services/test_pleroma/turned_on.json create mode 100644 tests/services/test_pleroma/undefined.json create mode 100644 tests/services/test_services.py create mode 100644 tests/services/test_ssh.py create mode 100644 tests/services/test_ssh/all_off.json create mode 100644 tests/services/test_ssh/root_and_admin_have_keys.json create mode 100644 tests/services/test_ssh/turned_off.json create mode 100644 tests/services/test_ssh/turned_on.json create mode 100644 tests/services/test_ssh/undefined.json diff --git a/requirements.txt b/requirements.txt index b451222..8f41c0e 100755 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,9 @@ setuptools portalocker flask-swagger flask-swagger-ui +pytz + +pytest +coverage +pytest-mock +pytest-datadir diff --git a/selfprivacy_api/app.py b/selfprivacy_api/app.py index ab412c9..5138b66 100644 --- a/selfprivacy_api/app.py +++ b/selfprivacy_api/app.py @@ -16,15 +16,18 @@ swagger_blueprint = get_swaggerui_blueprint( ) -def create_app(): +def create_app(test_config=None): """Initiate Flask app and bind routes""" app = Flask(__name__) api = Api(app) - app.config["AUTH_TOKEN"] = os.environ.get("AUTH_TOKEN") - if app.config["AUTH_TOKEN"] is None: - raise ValueError("AUTH_TOKEN is not set") - app.config["ENABLE_SWAGGER"] = os.environ.get("ENABLE_SWAGGER", "0") + if test_config is None: + app.config["AUTH_TOKEN"] = os.environ.get("AUTH_TOKEN") + if app.config["AUTH_TOKEN"] is None: + raise ValueError("AUTH_TOKEN is not set") + app.config["ENABLE_SWAGGER"] = os.environ.get("ENABLE_SWAGGER", "0") + else: + app.config.update(test_config) # Check bearer token @app.before_request diff --git a/selfprivacy_api/resources/services/mailserver.py b/selfprivacy_api/resources/services/mailserver.py index 4015f9a..1185d20 100644 --- a/selfprivacy_api/resources/services/mailserver.py +++ b/selfprivacy_api/resources/services/mailserver.py @@ -2,6 +2,7 @@ """Mail server management module""" import base64 import subprocess +import os from flask_restful import Resource from selfprivacy_api.resources.services import api @@ -25,15 +26,20 @@ class DKIMKey(Resource): description: DKIM key encoded in base64 401: description: Unauthorized + 404: + description: DKIM key not found """ domain = get_domain() - cat_process = subprocess.Popen( - ["cat", "/var/dkim/" + domain + ".selector.txt"], stdout=subprocess.PIPE - ) - dkim = cat_process.communicate()[0] - dkim = base64.b64encode(dkim) - dkim = str(dkim, "utf-8") - return dkim + + if os.path.exists("/var/dkim/" + domain + ".selector.txt"): + cat_process = subprocess.Popen( + ["cat", "/var/dkim/" + domain + ".selector.txt"], stdout=subprocess.PIPE + ) + dkim = cat_process.communicate()[0] + dkim = base64.b64encode(dkim) + dkim = str(dkim, "utf-8") + return dkim + return "DKIM file not found", 404 api.add_resource(DKIMKey, "/mailserver/dkim") diff --git a/selfprivacy_api/resources/services/ssh.py b/selfprivacy_api/resources/services/ssh.py index 8cc96d9..2b90087 100644 --- a/selfprivacy_api/resources/services/ssh.py +++ b/selfprivacy_api/resources/services/ssh.py @@ -278,6 +278,12 @@ class SSHKeys(Resource): if username == data["username"]: if "sshKeys" not in data: data["sshKeys"] = [] + # Return 409 if key already in array + for key in data["sshKeys"]: + if key == args["public_key"]: + return { + "error": "Key already exists", + }, 409 data["sshKeys"].append(args["public_key"]) return { "message": "New SSH key successfully written", diff --git a/selfprivacy_api/utils.py b/selfprivacy_api/utils.py index a2953b1..4970db0 100644 --- a/selfprivacy_api/utils.py +++ b/selfprivacy_api/utils.py @@ -2,8 +2,11 @@ """Various utility functions""" import json import portalocker +from flask import current_app +USERDATA_FILE = "/etc/nixos/userdata/userdata.json" + def get_domain(): """Get domain from /var/domain without trailing new line""" with open("/var/domain", "r", encoding="utf-8") as domain_file: @@ -16,7 +19,7 @@ class WriteUserData(object): def __init__(self): self.userdata_file = open( - "/etc/nixos/userdata/userdata.json", "r+", encoding="utf-8" + USERDATA_FILE, "r+", encoding="utf-8" ) portalocker.lock(self.userdata_file, portalocker.LOCK_EX) self.data = json.load(self.userdata_file) @@ -38,7 +41,7 @@ class ReadUserData(object): def __init__(self): self.userdata_file = open( - "/etc/nixos/userdata/userdata.json", "r", encoding="utf-8" + USERDATA_FILE, "r", encoding="utf-8" ) portalocker.lock(self.userdata_file, portalocker.LOCK_SH) self.data = json.load(self.userdata_file) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..72fd132 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,38 @@ +import pytest +from flask import testing +from selfprivacy_api.app import create_app + + +@pytest.fixture +def app(): + app = create_app({ + "AUTH_TOKEN": "TEST_TOKEN", + "ENABLE_SWAGGER": "0", + }) + + yield app + + +@pytest.fixture +def client(app): + return app.test_client() + +class AuthorizedClient(testing.FlaskClient): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.token = "TEST_TOKEN" + + def open(self, *args, **kwargs): + if "headers" not in kwargs: + kwargs["headers"] = {} + kwargs["headers"]["Authorization"] = f"Bearer {self.token}" + return super().open(*args, **kwargs) + +@pytest.fixture +def authorized_client(app): + app.test_client_class = AuthorizedClient + return app.test_client() + +@pytest.fixture +def runner(app): + return app.test_cli_runner() \ No newline at end of file diff --git a/tests/services/test_bitwarden.py b/tests/services/test_bitwarden.py new file mode 100644 index 0000000..7e009a4 --- /dev/null +++ b/tests/services/test_bitwarden.py @@ -0,0 +1,80 @@ +import json +import pytest + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +############################################################################### + +@pytest.fixture +def bitwarden_off(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") + assert read_json(datadir / "turned_off.json")["bitwarden"]["enable"] == False + return datadir + +@pytest.fixture +def bitwarden_on(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") + assert read_json(datadir / "turned_on.json")["bitwarden"]["enable"] == True + return datadir + +@pytest.fixture +def bitwarden_enable_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + assert "enable" not in read_json(datadir / "enable_undefined.json")["bitwarden"] + return datadir + +@pytest.fixture +def bitwarden_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") + assert "bitwarden" not in read_json(datadir / "undefined.json") + return datadir + +############################################################################### + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_unauthorized(client, bitwarden_off, endpoint): + response = client.post(f"/services/bitwarden/{endpoint}") + assert response.status_code == 401 + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_illegal_methods(authorized_client, bitwarden_off, endpoint): + response = authorized_client.get(f"/services/bitwarden/{endpoint}") + assert response.status_code == 405 + response = authorized_client.put(f"/services/bitwarden/{endpoint}") + assert response.status_code == 405 + response = authorized_client.delete(f"/services/bitwarden/{endpoint}") + assert response.status_code == 405 + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_off(authorized_client, bitwarden_off, endpoint, target_file): + response = authorized_client.post(f"/services/bitwarden/{endpoint}") + assert response.status_code == 200 + assert read_json(bitwarden_off / "turned_off.json") == read_json(bitwarden_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_on(authorized_client, bitwarden_on, endpoint, target_file): + response = authorized_client.post(f"/services/bitwarden/{endpoint}") + assert response.status_code == 200 + assert read_json(bitwarden_on / "turned_on.json") == read_json(bitwarden_on / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_twice(authorized_client, bitwarden_off, endpoint, target_file): + response = authorized_client.post(f"/services/bitwarden/{endpoint}") + assert response.status_code == 200 + response = authorized_client.post(f"/services/bitwarden/{endpoint}") + assert response.status_code == 200 + assert read_json(bitwarden_off / "turned_off.json") == read_json(bitwarden_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_attribute_deleted(authorized_client, bitwarden_enable_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/bitwarden/{endpoint}") + assert response.status_code == 200 + assert read_json(bitwarden_enable_undefined / "enable_undefined.json") == read_json(bitwarden_enable_undefined / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_bitwarden_undefined(authorized_client, bitwarden_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/bitwarden/{endpoint}") + assert response.status_code == 200 + assert read_json(bitwarden_undefined / "undefined.json") == read_json(bitwarden_undefined / target_file) diff --git a/tests/services/test_bitwarden/enable_undefined.json b/tests/services/test_bitwarden/enable_undefined.json new file mode 100644 index 0000000..05e04c1 --- /dev/null +++ b/tests/services/test_bitwarden/enable_undefined.json @@ -0,0 +1,51 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_bitwarden/turned_off.json b/tests/services/test_bitwarden/turned_off.json new file mode 100644 index 0000000..7b2cf8b --- /dev/null +++ b/tests/services/test_bitwarden/turned_off.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_bitwarden/turned_on.json b/tests/services/test_bitwarden/turned_on.json new file mode 100644 index 0000000..337e47f --- /dev/null +++ b/tests/services/test_bitwarden/turned_on.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": true + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_bitwarden/undefined.json b/tests/services/test_bitwarden/undefined.json new file mode 100644 index 0000000..625422b --- /dev/null +++ b/tests/services/test_bitwarden/undefined.json @@ -0,0 +1,49 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_gitea.py b/tests/services/test_gitea.py new file mode 100644 index 0000000..b2d57b9 --- /dev/null +++ b/tests/services/test_gitea.py @@ -0,0 +1,80 @@ +import json +import pytest + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +############################################################################### + +@pytest.fixture +def gitea_off(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") + assert read_json(datadir / "turned_off.json")["gitea"]["enable"] == False + return datadir + +@pytest.fixture +def gitea_on(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") + assert read_json(datadir / "turned_on.json")["gitea"]["enable"] == True + return datadir + +@pytest.fixture +def gitea_enable_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + assert "enable" not in read_json(datadir / "enable_undefined.json")["gitea"] + return datadir + +@pytest.fixture +def gitea_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") + assert "gitea" not in read_json(datadir / "undefined.json") + return datadir + +############################################################################### + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_unauthorized(client, gitea_off, endpoint): + response = client.post(f"/services/gitea/{endpoint}") + assert response.status_code == 401 + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_illegal_methods(authorized_client, gitea_off, endpoint): + response = authorized_client.get(f"/services/gitea/{endpoint}") + assert response.status_code == 405 + response = authorized_client.put(f"/services/gitea/{endpoint}") + assert response.status_code == 405 + response = authorized_client.delete(f"/services/gitea/{endpoint}") + assert response.status_code == 405 + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_off(authorized_client, gitea_off, endpoint, target_file): + response = authorized_client.post(f"/services/gitea/{endpoint}") + assert response.status_code == 200 + assert read_json(gitea_off / "turned_off.json") == read_json(gitea_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_on(authorized_client, gitea_on, endpoint, target_file): + response = authorized_client.post(f"/services/gitea/{endpoint}") + assert response.status_code == 200 + assert read_json(gitea_on / "turned_on.json") == read_json(gitea_on / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_twice(authorized_client, gitea_off, endpoint, target_file): + response = authorized_client.post(f"/services/gitea/{endpoint}") + assert response.status_code == 200 + response = authorized_client.post(f"/services/gitea/{endpoint}") + assert response.status_code == 200 + assert read_json(gitea_off / "turned_off.json") == read_json(gitea_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_attribute_deleted(authorized_client, gitea_enable_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/gitea/{endpoint}") + assert response.status_code == 200 + assert read_json(gitea_enable_undefined / "enable_undefined.json") == read_json(gitea_enable_undefined / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_gitea_undefined(authorized_client, gitea_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/gitea/{endpoint}") + assert response.status_code == 200 + assert read_json(gitea_undefined / "undefined.json") == read_json(gitea_undefined / target_file) diff --git a/tests/services/test_gitea/enable_undefined.json b/tests/services/test_gitea/enable_undefined.json new file mode 100644 index 0000000..07b0e78 --- /dev/null +++ b/tests/services/test_gitea/enable_undefined.json @@ -0,0 +1,51 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_gitea/turned_off.json b/tests/services/test_gitea/turned_off.json new file mode 100644 index 0000000..7b2cf8b --- /dev/null +++ b/tests/services/test_gitea/turned_off.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_gitea/turned_on.json b/tests/services/test_gitea/turned_on.json new file mode 100644 index 0000000..acb98ce --- /dev/null +++ b/tests/services/test_gitea/turned_on.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": true + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_gitea/undefined.json b/tests/services/test_gitea/undefined.json new file mode 100644 index 0000000..f689b2e --- /dev/null +++ b/tests/services/test_gitea/undefined.json @@ -0,0 +1,49 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_mailserver.py b/tests/services/test_mailserver.py new file mode 100644 index 0000000..aa008c1 --- /dev/null +++ b/tests/services/test_mailserver.py @@ -0,0 +1,65 @@ +import base64 +import json +import pytest + +def read_json(file_path): + with open(file_path, "r", encoding="utf-8") as f: + return json.load(f) + +############################################################################### + +class ProcessMock(): + """Mock subprocess.Popen""" + def __init__(self, args, **kwargs): + self.args = args + self.kwargs = kwargs + + def communicate(): + return (b"I am a DKIM key", None) + +class NoFileMock(ProcessMock): + def communicate(): + return (b"", None) + + +@pytest.fixture +def mock_subproccess_popen(mocker): + mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) + mocker.patch("selfprivacy_api.resources.services.mailserver.get_domain", autospec=True, return_value="example.com") + mocker.patch("os.path.exists", autospec=True, return_value=True) + return mock + +@pytest.fixture +def mock_no_file(mocker): + mock = mocker.patch("subprocess.Popen", autospec=True, return_value=NoFileMock) + mocker.patch("selfprivacy_api.resources.services.mailserver.get_domain", autospec=True, return_value="example.com") + mocker.patch("os.path.exists", autospec=True, return_value=False) + return mock + +############################################################################### + +def test_unauthorized(client, mock_subproccess_popen): + """Test unauthorized""" + response = client.get("/services/mailserver/dkim") + assert response.status_code == 401 + +def test_illegal_methods(authorized_client, mock_subproccess_popen): + response = authorized_client.post("/services/mailserver/dkim") + assert response.status_code == 405 + response = authorized_client.put("/services/mailserver/dkim") + assert response.status_code == 405 + response = authorized_client.delete("/services/mailserver/dkim") + assert response.status_code == 405 + +def test_dkim_key(authorized_client, mock_subproccess_popen): + """Test DKIM key""" + response = authorized_client.get("/services/mailserver/dkim") + assert response.status_code == 200 + assert base64.b64decode(response.data) == b"I am a DKIM key" + assert mock_subproccess_popen.call_args[0][0] == ["cat", "/var/dkim/example.com.selector.txt"] + +def test_no_dkim_key(authorized_client, mock_no_file): + """Test no DKIM key""" + response = authorized_client.get("/services/mailserver/dkim") + assert response.status_code == 404 + assert mock_no_file.called == False \ No newline at end of file diff --git a/tests/services/test_nextcloud.py b/tests/services/test_nextcloud.py new file mode 100644 index 0000000..031e0f2 --- /dev/null +++ b/tests/services/test_nextcloud.py @@ -0,0 +1,80 @@ +import json +import pytest + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +############################################################################### + +@pytest.fixture +def nextcloud_off(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") + assert read_json(datadir / "turned_off.json")["nextcloud"]["enable"] == False + return datadir + +@pytest.fixture +def nextcloud_on(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") + assert read_json(datadir / "turned_on.json")["nextcloud"]["enable"] == True + return datadir + +@pytest.fixture +def nextcloud_enable_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + assert "enable" not in read_json(datadir / "enable_undefined.json")["nextcloud"] + return datadir + +@pytest.fixture +def nextcloud_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") + assert "nextcloud" not in read_json(datadir / "undefined.json") + return datadir + +############################################################################### + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_unauthorized(client, nextcloud_off, endpoint): + response = client.post(f"/services/nextcloud/{endpoint}") + assert response.status_code == 401 + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_illegal_methods(authorized_client, nextcloud_off, endpoint): + response = authorized_client.get(f"/services/nextcloud/{endpoint}") + assert response.status_code == 405 + response = authorized_client.put(f"/services/nextcloud/{endpoint}") + assert response.status_code == 405 + response = authorized_client.delete(f"/services/nextcloud/{endpoint}") + assert response.status_code == 405 + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_off(authorized_client, nextcloud_off, endpoint, target_file): + response = authorized_client.post(f"/services/nextcloud/{endpoint}") + assert response.status_code == 200 + assert read_json(nextcloud_off / "turned_off.json") == read_json(nextcloud_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_on(authorized_client, nextcloud_on, endpoint, target_file): + response = authorized_client.post(f"/services/nextcloud/{endpoint}") + assert response.status_code == 200 + assert read_json(nextcloud_on / "turned_on.json") == read_json(nextcloud_on / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_twice(authorized_client, nextcloud_off, endpoint, target_file): + response = authorized_client.post(f"/services/nextcloud/{endpoint}") + assert response.status_code == 200 + response = authorized_client.post(f"/services/nextcloud/{endpoint}") + assert response.status_code == 200 + assert read_json(nextcloud_off / "turned_off.json") == read_json(nextcloud_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_attribute_deleted(authorized_client, nextcloud_enable_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/nextcloud/{endpoint}") + assert response.status_code == 200 + assert read_json(nextcloud_enable_undefined / "enable_undefined.json") == read_json(nextcloud_enable_undefined / target_file) + +@pytest.mark.parametrize("endpoint,target", [("enable", True), ("disable", False)]) +def test_on_nextcloud_undefined(authorized_client, nextcloud_undefined, endpoint, target): + response = authorized_client.post(f"/services/nextcloud/{endpoint}") + assert response.status_code == 200 + assert read_json(nextcloud_undefined / "undefined.json")["nextcloud"]["enable"] == target diff --git a/tests/services/test_nextcloud/enable_undefined.json b/tests/services/test_nextcloud/enable_undefined.json new file mode 100644 index 0000000..68127f0 --- /dev/null +++ b/tests/services/test_nextcloud/enable_undefined.json @@ -0,0 +1,51 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN" + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_nextcloud/turned_off.json b/tests/services/test_nextcloud/turned_off.json new file mode 100644 index 0000000..375e70f --- /dev/null +++ b/tests/services/test_nextcloud/turned_off.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_nextcloud/turned_on.json b/tests/services/test_nextcloud/turned_on.json new file mode 100644 index 0000000..7b2cf8b --- /dev/null +++ b/tests/services/test_nextcloud/turned_on.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_nextcloud/undefined.json b/tests/services/test_nextcloud/undefined.json new file mode 100644 index 0000000..fb02c69 --- /dev/null +++ b/tests/services/test_nextcloud/undefined.json @@ -0,0 +1,44 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_ocserv.py b/tests/services/test_ocserv.py new file mode 100644 index 0000000..2d658ea --- /dev/null +++ b/tests/services/test_ocserv.py @@ -0,0 +1,80 @@ +import json +import pytest + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +############################################################################### + +@pytest.fixture +def ocserv_off(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") + assert read_json(datadir / "turned_off.json")["ocserv"]["enable"] == False + return datadir + +@pytest.fixture +def ocserv_on(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") + assert read_json(datadir / "turned_on.json")["ocserv"]["enable"] == True + return datadir + +@pytest.fixture +def ocserv_enable_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + assert "enable" not in read_json(datadir / "enable_undefined.json")["ocserv"] + return datadir + +@pytest.fixture +def ocserv_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") + assert "ocserv" not in read_json(datadir / "undefined.json") + return datadir + +############################################################################### + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_unauthorized(client, ocserv_off, endpoint): + response = client.post(f"/services/ocserv/{endpoint}") + assert response.status_code == 401 + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_illegal_methods(authorized_client, ocserv_off, endpoint): + response = authorized_client.get(f"/services/ocserv/{endpoint}") + assert response.status_code == 405 + response = authorized_client.put(f"/services/ocserv/{endpoint}") + assert response.status_code == 405 + response = authorized_client.delete(f"/services/ocserv/{endpoint}") + assert response.status_code == 405 + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_off(authorized_client, ocserv_off, endpoint, target_file): + response = authorized_client.post(f"/services/ocserv/{endpoint}") + assert response.status_code == 200 + assert read_json(ocserv_off / "turned_off.json") == read_json(ocserv_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_on(authorized_client, ocserv_on, endpoint, target_file): + response = authorized_client.post(f"/services/ocserv/{endpoint}") + assert response.status_code == 200 + assert read_json(ocserv_on / "turned_on.json") == read_json(ocserv_on / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_twice(authorized_client, ocserv_off, endpoint, target_file): + response = authorized_client.post(f"/services/ocserv/{endpoint}") + assert response.status_code == 200 + response = authorized_client.post(f"/services/ocserv/{endpoint}") + assert response.status_code == 200 + assert read_json(ocserv_off / "turned_off.json") == read_json(ocserv_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_attribute_deleted(authorized_client, ocserv_enable_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/ocserv/{endpoint}") + assert response.status_code == 200 + assert read_json(ocserv_enable_undefined / "enable_undefined.json") == read_json(ocserv_enable_undefined / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_ocserv_undefined(authorized_client, ocserv_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/ocserv/{endpoint}") + assert response.status_code == 200 + assert read_json(ocserv_undefined / "undefined.json") == read_json(ocserv_undefined / target_file) diff --git a/tests/services/test_ocserv/enable_undefined.json b/tests/services/test_ocserv/enable_undefined.json new file mode 100644 index 0000000..88d804d --- /dev/null +++ b/tests/services/test_ocserv/enable_undefined.json @@ -0,0 +1,51 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_ocserv/turned_off.json b/tests/services/test_ocserv/turned_off.json new file mode 100644 index 0000000..6220561 --- /dev/null +++ b/tests/services/test_ocserv/turned_off.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": false + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_ocserv/turned_on.json b/tests/services/test_ocserv/turned_on.json new file mode 100644 index 0000000..375e70f --- /dev/null +++ b/tests/services/test_ocserv/turned_on.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_ocserv/undefined.json b/tests/services/test_ocserv/undefined.json new file mode 100644 index 0000000..f7e21bf --- /dev/null +++ b/tests/services/test_ocserv/undefined.json @@ -0,0 +1,49 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_pleroma.py b/tests/services/test_pleroma.py new file mode 100644 index 0000000..8b7a877 --- /dev/null +++ b/tests/services/test_pleroma.py @@ -0,0 +1,80 @@ +import json +import pytest + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +############################################################################### + +@pytest.fixture +def pleroma_off(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") + assert read_json(datadir / "turned_off.json")["pleroma"]["enable"] == False + return datadir + +@pytest.fixture +def pleroma_on(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") + assert read_json(datadir / "turned_on.json")["pleroma"]["enable"] == True + return datadir + +@pytest.fixture +def pleroma_enable_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + assert "enable" not in read_json(datadir / "enable_undefined.json")["pleroma"] + return datadir + +@pytest.fixture +def pleroma_undefined(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") + assert "pleroma" not in read_json(datadir / "undefined.json") + return datadir + +############################################################################### + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_unauthorized(client, pleroma_off, endpoint): + response = client.post(f"/services/pleroma/{endpoint}") + assert response.status_code == 401 + +@pytest.mark.parametrize("endpoint", ["enable", "disable"]) +def test_illegal_methods(authorized_client, pleroma_off, endpoint): + response = authorized_client.get(f"/services/pleroma/{endpoint}") + assert response.status_code == 405 + response = authorized_client.put(f"/services/pleroma/{endpoint}") + assert response.status_code == 405 + response = authorized_client.delete(f"/services/pleroma/{endpoint}") + assert response.status_code == 405 + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_off(authorized_client, pleroma_off, endpoint, target_file): + response = authorized_client.post(f"/services/pleroma/{endpoint}") + assert response.status_code == 200 + assert read_json(pleroma_off / "turned_off.json") == read_json(pleroma_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_from_on(authorized_client, pleroma_on, endpoint, target_file): + response = authorized_client.post(f"/services/pleroma/{endpoint}") + assert response.status_code == 200 + assert read_json(pleroma_on / "turned_on.json") == read_json(pleroma_on / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_switch_twice(authorized_client, pleroma_off, endpoint, target_file): + response = authorized_client.post(f"/services/pleroma/{endpoint}") + assert response.status_code == 200 + response = authorized_client.post(f"/services/pleroma/{endpoint}") + assert response.status_code == 200 + assert read_json(pleroma_off / "turned_off.json") == read_json(pleroma_off / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_attribute_deleted(authorized_client, pleroma_enable_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/pleroma/{endpoint}") + assert response.status_code == 200 + assert read_json(pleroma_enable_undefined / "enable_undefined.json") == read_json(pleroma_enable_undefined / target_file) + +@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) +def test_on_pleroma_undefined(authorized_client, pleroma_undefined, endpoint, target_file): + response = authorized_client.post(f"/services/pleroma/{endpoint}") + assert response.status_code == 200 + assert read_json(pleroma_undefined / "undefined.json") == read_json(pleroma_undefined / target_file) diff --git a/tests/services/test_pleroma/enable_undefined.json b/tests/services/test_pleroma/enable_undefined.json new file mode 100644 index 0000000..20ab960 --- /dev/null +++ b/tests/services/test_pleroma/enable_undefined.json @@ -0,0 +1,51 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": false + }, + "pleroma": { + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_pleroma/turned_off.json b/tests/services/test_pleroma/turned_off.json new file mode 100644 index 0000000..b6d5fd6 --- /dev/null +++ b/tests/services/test_pleroma/turned_off.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": false + }, + "pleroma": { + "enable": false + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_pleroma/turned_on.json b/tests/services/test_pleroma/turned_on.json new file mode 100644 index 0000000..6220561 --- /dev/null +++ b/tests/services/test_pleroma/turned_on.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": false + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_pleroma/undefined.json b/tests/services/test_pleroma/undefined.json new file mode 100644 index 0000000..b909a95 --- /dev/null +++ b/tests/services/test_pleroma/undefined.json @@ -0,0 +1,49 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": false + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": false + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_services.py b/tests/services/test_services.py new file mode 100644 index 0000000..0516c2d --- /dev/null +++ b/tests/services/test_services.py @@ -0,0 +1,131 @@ +import base64 +import json +import pytest + + +def read_json(file_path): + with open(file_path, "r", encoding="utf-8") as f: + return json.load(f) + +def call_args_asserts(mocked_object): + assert mocked_object.call_count == 8 + assert mocked_object.call_args_list[0][0][0] == [ + "systemctl", + "status", + "dovecot2.service", + ] + assert mocked_object.call_args_list[1][0][0] == [ + "systemctl", + "status", + "postfix.service", + ] + assert mocked_object.call_args_list[2][0][0] == [ + "systemctl", + "status", + "nginx.service", + ] + assert mocked_object.call_args_list[3][0][0] == [ + "systemctl", + "status", + "bitwarden_rs.service", + ] + assert mocked_object.call_args_list[4][0][0] == [ + "systemctl", + "status", + "gitea.service", + ] + assert mocked_object.call_args_list[5][0][0] == [ + "systemctl", + "status", + "phpfpm-nextcloud.service", + ] + assert mocked_object.call_args_list[6][0][0] == [ + "systemctl", + "status", + "ocserv.service", + ] + assert mocked_object.call_args_list[7][0][0] == [ + "systemctl", + "status", + "pleroma.service", + ] + +class ProcessMock: + """Mock subprocess.Popen""" + + def __init__(self, args, **kwargs): + self.args = args + self.kwargs = kwargs + + def communicate(): + return (b"", None) + + returncode = 0 + + +class BrokenServiceMock(ProcessMock): + returncode = 3 + + +@pytest.fixture +def mock_subproccess_popen(mocker): + mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) + return mock + + +@pytest.fixture +def mock_broken_service(mocker): + mock = mocker.patch( + "subprocess.Popen", autospec=True, return_value=BrokenServiceMock + ) + return mock + + +############################################################################### + + +def test_unauthorized(client, mock_subproccess_popen): + """Test unauthorized""" + response = client.get("/services/status") + assert response.status_code == 401 + + +def test_illegal_methods(authorized_client, mock_subproccess_popen): + response = authorized_client.post("/services/status") + assert response.status_code == 405 + response = authorized_client.put("/services/status") + assert response.status_code == 405 + response = authorized_client.delete("/services/status") + assert response.status_code == 405 + + +def test_dkim_key(authorized_client, mock_subproccess_popen): + response = authorized_client.get("/services/status") + assert response.status_code == 200 + assert response.get_json() == { + "imap": 0, + "smtp": 0, + "http": 0, + "bitwarden": 0, + "gitea": 0, + "nextcloud": 0, + "ocserv": 0, + "pleroma": 0, + } + call_args_asserts(mock_subproccess_popen) + + +def test_no_dkim_key(authorized_client, mock_broken_service): + response = authorized_client.get("/services/status") + assert response.status_code == 200 + assert response.get_json() == { + "imap": 3, + "smtp": 3, + "http": 3, + "bitwarden": 3, + "gitea": 3, + "nextcloud": 3, + "ocserv": 3, + "pleroma": 3, + } + call_args_asserts(mock_broken_service) diff --git a/tests/services/test_ssh.py b/tests/services/test_ssh.py new file mode 100644 index 0000000..c140123 --- /dev/null +++ b/tests/services/test_ssh.py @@ -0,0 +1,262 @@ +import json +from os import read +import pytest + + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + + +############################################################################### + + +@pytest.fixture +def ssh_off(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") + assert read_json(datadir / "turned_off.json")["ssh"]["enable"] == False + assert ( + read_json(datadir / "turned_off.json")["ssh"]["passwordAuthentication"] == True + ) + return datadir + + +@pytest.fixture +def ssh_on(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") + assert ( + read_json(datadir / "turned_off.json")["ssh"]["passwordAuthentication"] == True + ) + assert read_json(datadir / "turned_on.json")["ssh"]["enable"] == True + return datadir + + +@pytest.fixture +def all_off(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "all_off.json") + assert read_json(datadir / "all_off.json")["ssh"]["passwordAuthentication"] == False + assert read_json(datadir / "all_off.json")["ssh"]["enable"] == False + return datadir + + +@pytest.fixture +def undefined_settings(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") + assert "ssh" not in read_json(datadir / "undefined.json") + return datadir + + +@pytest.fixture +def root_and_admin_have_keys(mocker, datadir): + mocker.patch( + "selfprivacy_api.utils.USERDATA_FILE", + new=datadir / "root_and_admin_have_keys.json", + ) + assert read_json(datadir / "root_and_admin_have_keys.json")["ssh"]["enable"] == True + assert ( + read_json(datadir / "root_and_admin_have_keys.json")["ssh"][ + "passwordAuthentication" + ] + == True + ) + assert read_json(datadir / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [ + "ssh-ed25519 KEY test@pc" + ] + assert read_json(datadir / "root_and_admin_have_keys.json")["sshKeys"] == [ + "ssh-rsa KEY test@pc" + ] + return datadir + + +############################################################################### + + +@pytest.mark.parametrize( + "endpoint", ["ssh", "ssh/enable", "ssh/key/send", "ssh/keys/user"] +) +def test_unauthorized(client, ssh_off, endpoint): + response = client.post(f"/services/{endpoint}") + assert response.status_code == 401 + + +def test_legacy_enable(authorized_client, ssh_off): + response = authorized_client.post(f"/services/ssh/enable") + assert response.status_code == 200 + assert read_json(ssh_off / "turned_off.json") == read_json( + ssh_off / "turned_on.json" + ) + + +def test_legacy_enable_when_enabled(authorized_client, ssh_on): + response = authorized_client.post(f"/services/ssh/enable") + assert response.status_code == 200 + assert read_json(ssh_on / "turned_on.json") == read_json(ssh_on / "turned_on.json") + + +def test_get_current_settings_ssh_off(authorized_client, ssh_off): + response = authorized_client.get("/services/ssh") + assert response.status_code == 200 + assert response.json == {"enable": False, "passwordAuthentication": True} + + +def test_get_current_settings_ssh_on(authorized_client, ssh_on): + response = authorized_client.get("/services/ssh") + assert response.status_code == 200 + assert response.json == {"enable": True, "passwordAuthentication": True} + + +def test_get_current_settings_all_off(authorized_client, all_off): + response = authorized_client.get("/services/ssh") + assert response.status_code == 200 + assert response.json == {"enable": False, "passwordAuthentication": False} + + +def test_get_current_settings_undefined(authorized_client, undefined_settings): + response = authorized_client.get("/services/ssh") + assert response.status_code == 200 + assert response.json == {"enable": True, "passwordAuthentication": True} + + +available_settings = [ + {"enable": True, "passwordAuthentication": True}, + {"enable": True, "passwordAuthentication": False}, + {"enable": False, "passwordAuthentication": True}, + {"enable": False, "passwordAuthentication": False}, + {"enable": True}, + {"enable": False}, + {"passwordAuthentication": True}, + {"passwordAuthentication": False}, +] + + +@pytest.mark.parametrize("settings", available_settings) +def test_set_settings_ssh_off(authorized_client, ssh_off, settings): + response = authorized_client.put(f"/services/ssh", json=settings) + assert response.status_code == 200 + data = read_json(ssh_off / "turned_off.json")["ssh"] + if "enable" in settings: + assert data["enable"] == settings["enable"] + if "passwordAuthentication" in settings: + assert data["passwordAuthentication"] == settings["passwordAuthentication"] + + +@pytest.mark.parametrize("settings", available_settings) +def test_set_settings_ssh_on(authorized_client, ssh_on, settings): + response = authorized_client.put(f"/services/ssh", json=settings) + assert response.status_code == 200 + data = read_json(ssh_on / "turned_on.json")["ssh"] + if "enable" in settings: + assert data["enable"] == settings["enable"] + if "passwordAuthentication" in settings: + assert data["passwordAuthentication"] == settings["passwordAuthentication"] + + +@pytest.mark.parametrize("settings", available_settings) +def test_set_settings_all_off(authorized_client, all_off, settings): + response = authorized_client.put(f"/services/ssh", json=settings) + assert response.status_code == 200 + data = read_json(all_off / "all_off.json")["ssh"] + if "enable" in settings: + assert data["enable"] == settings["enable"] + if "passwordAuthentication" in settings: + assert data["passwordAuthentication"] == settings["passwordAuthentication"] + + +@pytest.mark.parametrize("settings", available_settings) +def test_set_settings_undefined(authorized_client, undefined_settings, settings): + response = authorized_client.put(f"/services/ssh", json=settings) + assert response.status_code == 200 + data = read_json(undefined_settings / "undefined.json")["ssh"] + if "enable" in settings: + assert data["enable"] == settings["enable"] + if "passwordAuthentication" in settings: + assert data["passwordAuthentication"] == settings["passwordAuthentication"] + +def test_add_root_key(authorized_client, ssh_on): + response = authorized_client.put(f"/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"}) + assert response.status_code == 201 + assert read_json(ssh_on / "turned_on.json")["ssh"]["rootKeys"] == [ + "ssh-rsa KEY test@pc", + ] + +def test_add_root_key_one_more(authorized_client, root_and_admin_have_keys): + response = authorized_client.put(f"/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"}) + assert response.status_code == 201 + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [ + "ssh-ed25519 KEY test@pc", + "ssh-rsa KEY test@pc", + ] + +def test_add_existing_root_key(authorized_client, root_and_admin_have_keys): + response = authorized_client.put(f"/services/ssh/key/send", json={"public_key": "ssh-ed25519 KEY test@pc"}) + assert response.status_code == 409 + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [ + "ssh-ed25519 KEY test@pc", + ] + +def test_add_invalid_root_key(authorized_client, ssh_on): + response = authorized_client.put(f"/services/ssh/key/send", json={"public_key": "INVALID KEY test@pc"}) + assert response.status_code == 400 + +def test_add_root_key_via_wrong_endpoint(authorized_client, ssh_on): + response = authorized_client.post(f"/services/ssh/keys/root", json={"public_key": "ssh-rsa KEY test@pc"}) + assert response.status_code == 400 + +def test_get_root_key(authorized_client, root_and_admin_have_keys): + response = authorized_client.get(f"/services/ssh/keys/root") + assert response.status_code == 200 + assert response.json == ["ssh-ed25519 KEY test@pc"] + +def test_get_root_key_when_none(authorized_client, ssh_on): + response = authorized_client.get(f"/services/ssh/keys/root") + assert response.status_code == 200 + assert response.json == [] + +def test_delete_root_key(authorized_client, root_and_admin_have_keys): + response = authorized_client.delete(f"/services/ssh/keys/root", json={"public_key": "ssh-ed25519 KEY test@pc"}) + assert response.status_code == 200 + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [] + +def test_delete_root_nonexistent_key(authorized_client, root_and_admin_have_keys): + response = authorized_client.delete(f"/services/ssh/keys/root", json={"public_key": "ssh-rsa KEY test@pc"}) + assert response.status_code == 404 + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [ + "ssh-ed25519 KEY test@pc", + ] + +def test_get_admin_key(authorized_client, root_and_admin_have_keys): + response = authorized_client.get(f"/services/ssh/keys/tester") + assert response.status_code == 200 + assert response.json == ["ssh-rsa KEY test@pc"] + +def test_get_admin_key_when_none(authorized_client, ssh_on): + response = authorized_client.get(f"/services/ssh/keys/tester") + assert response.status_code == 200 + assert response.json == [] + +def test_delete_admin_key(authorized_client, root_and_admin_have_keys): + response = authorized_client.delete(f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"}) + assert response.status_code == 200 + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["sshKeys"] == [] + +def test_add_admin_key(authorized_client, ssh_on): + response = authorized_client.post(f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"}) + assert response.status_code == 201 + assert read_json(ssh_on / "turned_on.json")["sshKeys"] == [ + "ssh-rsa KEY test@pc", + ] + +def test_add_admin_key_one_more(authorized_client, root_and_admin_have_keys): + response = authorized_client.post(f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY_2 test@pc"}) + assert response.status_code == 201 + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["sshKeys"] == [ + "ssh-rsa KEY test@pc", + "ssh-rsa KEY_2 test@pc" + ] + +def test_add_existing_admin_key(authorized_client, root_and_admin_have_keys): + response = authorized_client.post(f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"}) + assert response.status_code == 409 + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["sshKeys"] == [ + "ssh-rsa KEY test@pc", + ] \ No newline at end of file diff --git a/tests/services/test_ssh/all_off.json b/tests/services/test_ssh/all_off.json new file mode 100644 index 0000000..e1b8510 --- /dev/null +++ b/tests/services/test_ssh/all_off.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": false, + "passwordAuthentication": false, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_ssh/root_and_admin_have_keys.json b/tests/services/test_ssh/root_and_admin_have_keys.json new file mode 100644 index 0000000..7b2cf8b --- /dev/null +++ b/tests/services/test_ssh/root_and_admin_have_keys.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file diff --git a/tests/services/test_ssh/turned_off.json b/tests/services/test_ssh/turned_off.json new file mode 100644 index 0000000..b09395b --- /dev/null +++ b/tests/services/test_ssh/turned_off.json @@ -0,0 +1,46 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": false, + "passwordAuthentication": true + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow" +} \ No newline at end of file diff --git a/tests/services/test_ssh/turned_on.json b/tests/services/test_ssh/turned_on.json new file mode 100644 index 0000000..44b28ce --- /dev/null +++ b/tests/services/test_ssh/turned_on.json @@ -0,0 +1,46 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow" +} \ No newline at end of file diff --git a/tests/services/test_ssh/undefined.json b/tests/services/test_ssh/undefined.json new file mode 100644 index 0000000..3f5545f --- /dev/null +++ b/tests/services/test_ssh/undefined.json @@ -0,0 +1,45 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file From 69eb91a1eaae6860385988439dcffc3676a9227f Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 1 Dec 2021 00:53:39 +0300 Subject: [PATCH 04/15] Reformat code with Black --- selfprivacy_api/resources/services/restic.py | 23 +++-- selfprivacy_api/resources/services/update.py | 12 +-- selfprivacy_api/utils.py | 10 +- tests/conftest.py | 15 ++- tests/services/test_bitwarden.py | 71 ++++++++++++--- tests/services/test_gitea.py | 63 ++++++++++--- tests/services/test_mailserver.py | 34 +++++-- tests/services/test_nextcloud.py | 67 +++++++++++--- tests/services/test_ocserv.py | 67 +++++++++++--- tests/services/test_pleroma.py | 71 ++++++++++++--- tests/services/test_services.py | 2 + tests/services/test_ssh.py | 96 +++++++++++++++----- 12 files changed, 412 insertions(+), 119 deletions(-) diff --git a/selfprivacy_api/resources/services/restic.py b/selfprivacy_api/resources/services/restic.py index 863d090..282488d 100644 --- a/selfprivacy_api/resources/services/restic.py +++ b/selfprivacy_api/resources/services/restic.py @@ -147,23 +147,26 @@ class AsyncRestoreBackup(Resource): 401: description: Unauthorized """ - backup_restoration_command = ["restic", "-r", "rclone:backblaze:sfbackup", "var", "--json"] + backup_restoration_command = [ + "restic", + "-r", + "rclone:backblaze:sfbackup", + "var", + "--json", + ] - with open("/tmp/backup.log", "w", encoding="utf-8") as backup_log_file_descriptor: + with open( + "/tmp/backup.log", "w", encoding="utf-8" + ) as backup_log_file_descriptor: with subprocess.Popen( backup_restoration_command, shell=False, stdout=subprocess.PIPE, stderr=backup_log_file_descriptor, ) as backup_restoration_process_descriptor: - backup_restoration_status = ( - "Backup restoration procedure started" - ) - - return { - "status": 0, - "message": backup_restoration_status - } + backup_restoration_status = "Backup restoration procedure started" + + return {"status": 0, "message": backup_restoration_status} api.add_resource(ListAllBackups, "/restic/backup/list") diff --git a/selfprivacy_api/resources/services/update.py b/selfprivacy_api/resources/services/update.py index 1d15fbe..73698d5 100644 --- a/selfprivacy_api/resources/services/update.py +++ b/selfprivacy_api/resources/services/update.py @@ -32,28 +32,24 @@ class PullRepositoryChanges(Resource): current_working_directory = os.getcwd() os.chdir("/etc/nixos") - git_pull_process_descriptor = subprocess.Popen( git_pull_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - shell=False + shell=False, ) - git_pull_process_descriptor.communicate()[0] os.chdir(current_working_directory) if git_pull_process_descriptor.returncode == 0: - return { - "status": 0, - "message": "Update completed successfully" - } + return {"status": 0, "message": "Update completed successfully"} elif git_pull_process_descriptor.returncode > 0: return { "status": git_pull_process_descriptor.returncode, - "message": "Something went wrong" + "message": "Something went wrong", }, 500 + api.add_resource(PullRepositoryChanges, "/update") diff --git a/selfprivacy_api/utils.py b/selfprivacy_api/utils.py index 4970db0..b0a7686 100644 --- a/selfprivacy_api/utils.py +++ b/selfprivacy_api/utils.py @@ -7,6 +7,7 @@ from flask import current_app USERDATA_FILE = "/etc/nixos/userdata/userdata.json" + def get_domain(): """Get domain from /var/domain without trailing new line""" with open("/var/domain", "r", encoding="utf-8") as domain_file: @@ -18,9 +19,7 @@ class WriteUserData(object): """Write userdata.json with lock""" def __init__(self): - self.userdata_file = open( - USERDATA_FILE, "r+", encoding="utf-8" - ) + self.userdata_file = open(USERDATA_FILE, "r+", encoding="utf-8") portalocker.lock(self.userdata_file, portalocker.LOCK_EX) self.data = json.load(self.userdata_file) @@ -40,9 +39,7 @@ class ReadUserData(object): """Read userdata.json with lock""" def __init__(self): - self.userdata_file = open( - USERDATA_FILE, "r", encoding="utf-8" - ) + self.userdata_file = open(USERDATA_FILE, "r", encoding="utf-8") portalocker.lock(self.userdata_file, portalocker.LOCK_SH) self.data = json.load(self.userdata_file) @@ -60,4 +57,3 @@ def validate_ssh_public_key(key): if not key.startswith("ssh-rsa"): return False return True - \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 72fd132..e963224 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,10 +5,12 @@ from selfprivacy_api.app import create_app @pytest.fixture def app(): - app = create_app({ - "AUTH_TOKEN": "TEST_TOKEN", - "ENABLE_SWAGGER": "0", - }) + app = create_app( + { + "AUTH_TOKEN": "TEST_TOKEN", + "ENABLE_SWAGGER": "0", + } + ) yield app @@ -17,6 +19,7 @@ def app(): def client(app): return app.test_client() + class AuthorizedClient(testing.FlaskClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -28,11 +31,13 @@ class AuthorizedClient(testing.FlaskClient): kwargs["headers"]["Authorization"] = f"Bearer {self.token}" return super().open(*args, **kwargs) + @pytest.fixture def authorized_client(app): app.test_client_class = AuthorizedClient return app.test_client() + @pytest.fixture def runner(app): - return app.test_cli_runner() \ No newline at end of file + return app.test_cli_runner() diff --git a/tests/services/test_bitwarden.py b/tests/services/test_bitwarden.py index 7e009a4..3977253 100644 --- a/tests/services/test_bitwarden.py +++ b/tests/services/test_bitwarden.py @@ -1,43 +1,54 @@ import json import pytest + def read_json(file_path): with open(file_path, "r") as f: return json.load(f) + ############################################################################### + @pytest.fixture def bitwarden_off(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") assert read_json(datadir / "turned_off.json")["bitwarden"]["enable"] == False return datadir + @pytest.fixture def bitwarden_on(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") assert read_json(datadir / "turned_on.json")["bitwarden"]["enable"] == True return datadir + @pytest.fixture def bitwarden_enable_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + mocker.patch( + "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" + ) assert "enable" not in read_json(datadir / "enable_undefined.json")["bitwarden"] return datadir + @pytest.fixture def bitwarden_undefined(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") assert "bitwarden" not in read_json(datadir / "undefined.json") return datadir + ############################################################################### + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_unauthorized(client, bitwarden_off, endpoint): response = client.post(f"/services/bitwarden/{endpoint}") assert response.status_code == 401 + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_illegal_methods(authorized_client, bitwarden_off, endpoint): response = authorized_client.get(f"/services/bitwarden/{endpoint}") @@ -47,34 +58,68 @@ def test_illegal_methods(authorized_client, bitwarden_off, endpoint): response = authorized_client.delete(f"/services/bitwarden/{endpoint}") assert response.status_code == 405 -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_off(authorized_client, bitwarden_off, endpoint, target_file): response = authorized_client.post(f"/services/bitwarden/{endpoint}") assert response.status_code == 200 - assert read_json(bitwarden_off / "turned_off.json") == read_json(bitwarden_off / target_file) + assert read_json(bitwarden_off / "turned_off.json") == read_json( + bitwarden_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_on(authorized_client, bitwarden_on, endpoint, target_file): response = authorized_client.post(f"/services/bitwarden/{endpoint}") assert response.status_code == 200 - assert read_json(bitwarden_on / "turned_on.json") == read_json(bitwarden_on / target_file) + assert read_json(bitwarden_on / "turned_on.json") == read_json( + bitwarden_on / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_twice(authorized_client, bitwarden_off, endpoint, target_file): response = authorized_client.post(f"/services/bitwarden/{endpoint}") assert response.status_code == 200 response = authorized_client.post(f"/services/bitwarden/{endpoint}") assert response.status_code == 200 - assert read_json(bitwarden_off / "turned_off.json") == read_json(bitwarden_off / target_file) + assert read_json(bitwarden_off / "turned_off.json") == read_json( + bitwarden_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) -def test_on_attribute_deleted(authorized_client, bitwarden_enable_undefined, endpoint, target_file): + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) +def test_on_attribute_deleted( + authorized_client, bitwarden_enable_undefined, endpoint, target_file +): response = authorized_client.post(f"/services/bitwarden/{endpoint}") assert response.status_code == 200 - assert read_json(bitwarden_enable_undefined / "enable_undefined.json") == read_json(bitwarden_enable_undefined / target_file) + assert read_json(bitwarden_enable_undefined / "enable_undefined.json") == read_json( + bitwarden_enable_undefined / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) -def test_on_bitwarden_undefined(authorized_client, bitwarden_undefined, endpoint, target_file): + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) +def test_on_bitwarden_undefined( + authorized_client, bitwarden_undefined, endpoint, target_file +): response = authorized_client.post(f"/services/bitwarden/{endpoint}") assert response.status_code == 200 - assert read_json(bitwarden_undefined / "undefined.json") == read_json(bitwarden_undefined / target_file) + assert read_json(bitwarden_undefined / "undefined.json") == read_json( + bitwarden_undefined / target_file + ) diff --git a/tests/services/test_gitea.py b/tests/services/test_gitea.py index b2d57b9..0a50c19 100644 --- a/tests/services/test_gitea.py +++ b/tests/services/test_gitea.py @@ -1,43 +1,54 @@ import json import pytest + def read_json(file_path): with open(file_path, "r") as f: return json.load(f) + ############################################################################### + @pytest.fixture def gitea_off(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") assert read_json(datadir / "turned_off.json")["gitea"]["enable"] == False return datadir + @pytest.fixture def gitea_on(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") assert read_json(datadir / "turned_on.json")["gitea"]["enable"] == True return datadir + @pytest.fixture def gitea_enable_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + mocker.patch( + "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" + ) assert "enable" not in read_json(datadir / "enable_undefined.json")["gitea"] return datadir + @pytest.fixture def gitea_undefined(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") assert "gitea" not in read_json(datadir / "undefined.json") return datadir + ############################################################################### + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_unauthorized(client, gitea_off, endpoint): response = client.post(f"/services/gitea/{endpoint}") assert response.status_code == 401 + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_illegal_methods(authorized_client, gitea_off, endpoint): response = authorized_client.get(f"/services/gitea/{endpoint}") @@ -47,34 +58,64 @@ def test_illegal_methods(authorized_client, gitea_off, endpoint): response = authorized_client.delete(f"/services/gitea/{endpoint}") assert response.status_code == 405 -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_off(authorized_client, gitea_off, endpoint, target_file): response = authorized_client.post(f"/services/gitea/{endpoint}") assert response.status_code == 200 - assert read_json(gitea_off / "turned_off.json") == read_json(gitea_off / target_file) + assert read_json(gitea_off / "turned_off.json") == read_json( + gitea_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_on(authorized_client, gitea_on, endpoint, target_file): response = authorized_client.post(f"/services/gitea/{endpoint}") assert response.status_code == 200 assert read_json(gitea_on / "turned_on.json") == read_json(gitea_on / target_file) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_twice(authorized_client, gitea_off, endpoint, target_file): response = authorized_client.post(f"/services/gitea/{endpoint}") assert response.status_code == 200 response = authorized_client.post(f"/services/gitea/{endpoint}") assert response.status_code == 200 - assert read_json(gitea_off / "turned_off.json") == read_json(gitea_off / target_file) + assert read_json(gitea_off / "turned_off.json") == read_json( + gitea_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) -def test_on_attribute_deleted(authorized_client, gitea_enable_undefined, endpoint, target_file): + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) +def test_on_attribute_deleted( + authorized_client, gitea_enable_undefined, endpoint, target_file +): response = authorized_client.post(f"/services/gitea/{endpoint}") assert response.status_code == 200 - assert read_json(gitea_enable_undefined / "enable_undefined.json") == read_json(gitea_enable_undefined / target_file) + assert read_json(gitea_enable_undefined / "enable_undefined.json") == read_json( + gitea_enable_undefined / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_on_gitea_undefined(authorized_client, gitea_undefined, endpoint, target_file): response = authorized_client.post(f"/services/gitea/{endpoint}") assert response.status_code == 200 - assert read_json(gitea_undefined / "undefined.json") == read_json(gitea_undefined / target_file) + assert read_json(gitea_undefined / "undefined.json") == read_json( + gitea_undefined / target_file + ) diff --git a/tests/services/test_mailserver.py b/tests/services/test_mailserver.py index aa008c1..f45ad1e 100644 --- a/tests/services/test_mailserver.py +++ b/tests/services/test_mailserver.py @@ -2,14 +2,18 @@ import base64 import json import pytest + def read_json(file_path): with open(file_path, "r", encoding="utf-8") as f: return json.load(f) + ############################################################################### -class ProcessMock(): + +class ProcessMock: """Mock subprocess.Popen""" + def __init__(self, args, **kwargs): self.args = args self.kwargs = kwargs @@ -17,6 +21,7 @@ class ProcessMock(): def communicate(): return (b"I am a DKIM key", None) + class NoFileMock(ProcessMock): def communicate(): return (b"", None) @@ -25,24 +30,36 @@ class NoFileMock(ProcessMock): @pytest.fixture def mock_subproccess_popen(mocker): mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) - mocker.patch("selfprivacy_api.resources.services.mailserver.get_domain", autospec=True, return_value="example.com") + mocker.patch( + "selfprivacy_api.resources.services.mailserver.get_domain", + autospec=True, + return_value="example.com", + ) mocker.patch("os.path.exists", autospec=True, return_value=True) - return mock + return mock + @pytest.fixture def mock_no_file(mocker): mock = mocker.patch("subprocess.Popen", autospec=True, return_value=NoFileMock) - mocker.patch("selfprivacy_api.resources.services.mailserver.get_domain", autospec=True, return_value="example.com") + mocker.patch( + "selfprivacy_api.resources.services.mailserver.get_domain", + autospec=True, + return_value="example.com", + ) mocker.patch("os.path.exists", autospec=True, return_value=False) return mock + ############################################################################### + def test_unauthorized(client, mock_subproccess_popen): """Test unauthorized""" response = client.get("/services/mailserver/dkim") assert response.status_code == 401 + def test_illegal_methods(authorized_client, mock_subproccess_popen): response = authorized_client.post("/services/mailserver/dkim") assert response.status_code == 405 @@ -51,15 +68,20 @@ def test_illegal_methods(authorized_client, mock_subproccess_popen): response = authorized_client.delete("/services/mailserver/dkim") assert response.status_code == 405 + def test_dkim_key(authorized_client, mock_subproccess_popen): """Test DKIM key""" response = authorized_client.get("/services/mailserver/dkim") assert response.status_code == 200 assert base64.b64decode(response.data) == b"I am a DKIM key" - assert mock_subproccess_popen.call_args[0][0] == ["cat", "/var/dkim/example.com.selector.txt"] + assert mock_subproccess_popen.call_args[0][0] == [ + "cat", + "/var/dkim/example.com.selector.txt", + ] + def test_no_dkim_key(authorized_client, mock_no_file): """Test no DKIM key""" response = authorized_client.get("/services/mailserver/dkim") assert response.status_code == 404 - assert mock_no_file.called == False \ No newline at end of file + assert mock_no_file.called == False diff --git a/tests/services/test_nextcloud.py b/tests/services/test_nextcloud.py index 031e0f2..b05c363 100644 --- a/tests/services/test_nextcloud.py +++ b/tests/services/test_nextcloud.py @@ -1,43 +1,54 @@ import json import pytest + def read_json(file_path): with open(file_path, "r") as f: return json.load(f) + ############################################################################### + @pytest.fixture def nextcloud_off(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") assert read_json(datadir / "turned_off.json")["nextcloud"]["enable"] == False return datadir + @pytest.fixture def nextcloud_on(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") assert read_json(datadir / "turned_on.json")["nextcloud"]["enable"] == True return datadir + @pytest.fixture def nextcloud_enable_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + mocker.patch( + "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" + ) assert "enable" not in read_json(datadir / "enable_undefined.json")["nextcloud"] return datadir + @pytest.fixture def nextcloud_undefined(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") assert "nextcloud" not in read_json(datadir / "undefined.json") return datadir + ############################################################################### + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_unauthorized(client, nextcloud_off, endpoint): response = client.post(f"/services/nextcloud/{endpoint}") assert response.status_code == 401 + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_illegal_methods(authorized_client, nextcloud_off, endpoint): response = authorized_client.get(f"/services/nextcloud/{endpoint}") @@ -47,34 +58,66 @@ def test_illegal_methods(authorized_client, nextcloud_off, endpoint): response = authorized_client.delete(f"/services/nextcloud/{endpoint}") assert response.status_code == 405 -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_off(authorized_client, nextcloud_off, endpoint, target_file): response = authorized_client.post(f"/services/nextcloud/{endpoint}") assert response.status_code == 200 - assert read_json(nextcloud_off / "turned_off.json") == read_json(nextcloud_off / target_file) + assert read_json(nextcloud_off / "turned_off.json") == read_json( + nextcloud_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_on(authorized_client, nextcloud_on, endpoint, target_file): response = authorized_client.post(f"/services/nextcloud/{endpoint}") assert response.status_code == 200 - assert read_json(nextcloud_on / "turned_on.json") == read_json(nextcloud_on / target_file) + assert read_json(nextcloud_on / "turned_on.json") == read_json( + nextcloud_on / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_twice(authorized_client, nextcloud_off, endpoint, target_file): response = authorized_client.post(f"/services/nextcloud/{endpoint}") assert response.status_code == 200 response = authorized_client.post(f"/services/nextcloud/{endpoint}") assert response.status_code == 200 - assert read_json(nextcloud_off / "turned_off.json") == read_json(nextcloud_off / target_file) + assert read_json(nextcloud_off / "turned_off.json") == read_json( + nextcloud_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) -def test_on_attribute_deleted(authorized_client, nextcloud_enable_undefined, endpoint, target_file): + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) +def test_on_attribute_deleted( + authorized_client, nextcloud_enable_undefined, endpoint, target_file +): response = authorized_client.post(f"/services/nextcloud/{endpoint}") assert response.status_code == 200 - assert read_json(nextcloud_enable_undefined / "enable_undefined.json") == read_json(nextcloud_enable_undefined / target_file) + assert read_json(nextcloud_enable_undefined / "enable_undefined.json") == read_json( + nextcloud_enable_undefined / target_file + ) + @pytest.mark.parametrize("endpoint,target", [("enable", True), ("disable", False)]) -def test_on_nextcloud_undefined(authorized_client, nextcloud_undefined, endpoint, target): +def test_on_nextcloud_undefined( + authorized_client, nextcloud_undefined, endpoint, target +): response = authorized_client.post(f"/services/nextcloud/{endpoint}") assert response.status_code == 200 - assert read_json(nextcloud_undefined / "undefined.json")["nextcloud"]["enable"] == target + assert ( + read_json(nextcloud_undefined / "undefined.json")["nextcloud"]["enable"] + == target + ) diff --git a/tests/services/test_ocserv.py b/tests/services/test_ocserv.py index 2d658ea..8f43e70 100644 --- a/tests/services/test_ocserv.py +++ b/tests/services/test_ocserv.py @@ -1,43 +1,54 @@ import json import pytest + def read_json(file_path): with open(file_path, "r") as f: return json.load(f) + ############################################################################### + @pytest.fixture def ocserv_off(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") assert read_json(datadir / "turned_off.json")["ocserv"]["enable"] == False return datadir + @pytest.fixture def ocserv_on(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") assert read_json(datadir / "turned_on.json")["ocserv"]["enable"] == True return datadir + @pytest.fixture def ocserv_enable_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + mocker.patch( + "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" + ) assert "enable" not in read_json(datadir / "enable_undefined.json")["ocserv"] return datadir + @pytest.fixture def ocserv_undefined(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") assert "ocserv" not in read_json(datadir / "undefined.json") return datadir + ############################################################################### + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_unauthorized(client, ocserv_off, endpoint): response = client.post(f"/services/ocserv/{endpoint}") assert response.status_code == 401 + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_illegal_methods(authorized_client, ocserv_off, endpoint): response = authorized_client.get(f"/services/ocserv/{endpoint}") @@ -47,34 +58,66 @@ def test_illegal_methods(authorized_client, ocserv_off, endpoint): response = authorized_client.delete(f"/services/ocserv/{endpoint}") assert response.status_code == 405 -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_off(authorized_client, ocserv_off, endpoint, target_file): response = authorized_client.post(f"/services/ocserv/{endpoint}") assert response.status_code == 200 - assert read_json(ocserv_off / "turned_off.json") == read_json(ocserv_off / target_file) + assert read_json(ocserv_off / "turned_off.json") == read_json( + ocserv_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_on(authorized_client, ocserv_on, endpoint, target_file): response = authorized_client.post(f"/services/ocserv/{endpoint}") assert response.status_code == 200 assert read_json(ocserv_on / "turned_on.json") == read_json(ocserv_on / target_file) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_twice(authorized_client, ocserv_off, endpoint, target_file): response = authorized_client.post(f"/services/ocserv/{endpoint}") assert response.status_code == 200 response = authorized_client.post(f"/services/ocserv/{endpoint}") assert response.status_code == 200 - assert read_json(ocserv_off / "turned_off.json") == read_json(ocserv_off / target_file) + assert read_json(ocserv_off / "turned_off.json") == read_json( + ocserv_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) -def test_on_attribute_deleted(authorized_client, ocserv_enable_undefined, endpoint, target_file): + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) +def test_on_attribute_deleted( + authorized_client, ocserv_enable_undefined, endpoint, target_file +): response = authorized_client.post(f"/services/ocserv/{endpoint}") assert response.status_code == 200 - assert read_json(ocserv_enable_undefined / "enable_undefined.json") == read_json(ocserv_enable_undefined / target_file) + assert read_json(ocserv_enable_undefined / "enable_undefined.json") == read_json( + ocserv_enable_undefined / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) -def test_on_ocserv_undefined(authorized_client, ocserv_undefined, endpoint, target_file): + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) +def test_on_ocserv_undefined( + authorized_client, ocserv_undefined, endpoint, target_file +): response = authorized_client.post(f"/services/ocserv/{endpoint}") assert response.status_code == 200 - assert read_json(ocserv_undefined / "undefined.json") == read_json(ocserv_undefined / target_file) + assert read_json(ocserv_undefined / "undefined.json") == read_json( + ocserv_undefined / target_file + ) diff --git a/tests/services/test_pleroma.py b/tests/services/test_pleroma.py index 8b7a877..0d7f149 100644 --- a/tests/services/test_pleroma.py +++ b/tests/services/test_pleroma.py @@ -1,43 +1,54 @@ import json import pytest + def read_json(file_path): with open(file_path, "r") as f: return json.load(f) + ############################################################################### + @pytest.fixture def pleroma_off(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") assert read_json(datadir / "turned_off.json")["pleroma"]["enable"] == False return datadir + @pytest.fixture def pleroma_on(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") assert read_json(datadir / "turned_on.json")["pleroma"]["enable"] == True return datadir + @pytest.fixture def pleroma_enable_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json") + mocker.patch( + "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" + ) assert "enable" not in read_json(datadir / "enable_undefined.json")["pleroma"] return datadir + @pytest.fixture def pleroma_undefined(mocker, datadir): mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") assert "pleroma" not in read_json(datadir / "undefined.json") return datadir + ############################################################################### + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_unauthorized(client, pleroma_off, endpoint): response = client.post(f"/services/pleroma/{endpoint}") assert response.status_code == 401 + @pytest.mark.parametrize("endpoint", ["enable", "disable"]) def test_illegal_methods(authorized_client, pleroma_off, endpoint): response = authorized_client.get(f"/services/pleroma/{endpoint}") @@ -47,34 +58,68 @@ def test_illegal_methods(authorized_client, pleroma_off, endpoint): response = authorized_client.delete(f"/services/pleroma/{endpoint}") assert response.status_code == 405 -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_off(authorized_client, pleroma_off, endpoint, target_file): response = authorized_client.post(f"/services/pleroma/{endpoint}") assert response.status_code == 200 - assert read_json(pleroma_off / "turned_off.json") == read_json(pleroma_off / target_file) + assert read_json(pleroma_off / "turned_off.json") == read_json( + pleroma_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_from_on(authorized_client, pleroma_on, endpoint, target_file): response = authorized_client.post(f"/services/pleroma/{endpoint}") assert response.status_code == 200 - assert read_json(pleroma_on / "turned_on.json") == read_json(pleroma_on / target_file) + assert read_json(pleroma_on / "turned_on.json") == read_json( + pleroma_on / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) def test_switch_twice(authorized_client, pleroma_off, endpoint, target_file): response = authorized_client.post(f"/services/pleroma/{endpoint}") assert response.status_code == 200 response = authorized_client.post(f"/services/pleroma/{endpoint}") assert response.status_code == 200 - assert read_json(pleroma_off / "turned_off.json") == read_json(pleroma_off / target_file) + assert read_json(pleroma_off / "turned_off.json") == read_json( + pleroma_off / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) -def test_on_attribute_deleted(authorized_client, pleroma_enable_undefined, endpoint, target_file): + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) +def test_on_attribute_deleted( + authorized_client, pleroma_enable_undefined, endpoint, target_file +): response = authorized_client.post(f"/services/pleroma/{endpoint}") assert response.status_code == 200 - assert read_json(pleroma_enable_undefined / "enable_undefined.json") == read_json(pleroma_enable_undefined / target_file) + assert read_json(pleroma_enable_undefined / "enable_undefined.json") == read_json( + pleroma_enable_undefined / target_file + ) -@pytest.mark.parametrize("endpoint,target_file", [("enable", "turned_on.json"), ("disable", "turned_off.json")]) -def test_on_pleroma_undefined(authorized_client, pleroma_undefined, endpoint, target_file): + +@pytest.mark.parametrize( + "endpoint,target_file", + [("enable", "turned_on.json"), ("disable", "turned_off.json")], +) +def test_on_pleroma_undefined( + authorized_client, pleroma_undefined, endpoint, target_file +): response = authorized_client.post(f"/services/pleroma/{endpoint}") assert response.status_code == 200 - assert read_json(pleroma_undefined / "undefined.json") == read_json(pleroma_undefined / target_file) + assert read_json(pleroma_undefined / "undefined.json") == read_json( + pleroma_undefined / target_file + ) diff --git a/tests/services/test_services.py b/tests/services/test_services.py index 0516c2d..859da5a 100644 --- a/tests/services/test_services.py +++ b/tests/services/test_services.py @@ -7,6 +7,7 @@ def read_json(file_path): with open(file_path, "r", encoding="utf-8") as f: return json.load(f) + def call_args_asserts(mocked_object): assert mocked_object.call_count == 8 assert mocked_object.call_args_list[0][0][0] == [ @@ -50,6 +51,7 @@ def call_args_asserts(mocked_object): "pleroma.service", ] + class ProcessMock: """Mock subprocess.Popen""" diff --git a/tests/services/test_ssh.py b/tests/services/test_ssh.py index c140123..7233a53 100644 --- a/tests/services/test_ssh.py +++ b/tests/services/test_ssh.py @@ -172,91 +172,143 @@ def test_set_settings_undefined(authorized_client, undefined_settings, settings) if "passwordAuthentication" in settings: assert data["passwordAuthentication"] == settings["passwordAuthentication"] + def test_add_root_key(authorized_client, ssh_on): - response = authorized_client.put(f"/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"}) + response = authorized_client.put( + f"/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"} + ) assert response.status_code == 201 assert read_json(ssh_on / "turned_on.json")["ssh"]["rootKeys"] == [ "ssh-rsa KEY test@pc", ] + def test_add_root_key_one_more(authorized_client, root_and_admin_have_keys): - response = authorized_client.put(f"/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"}) + response = authorized_client.put( + f"/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"} + ) assert response.status_code == 201 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [ + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][ + "rootKeys" + ] == [ "ssh-ed25519 KEY test@pc", "ssh-rsa KEY test@pc", ] + def test_add_existing_root_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.put(f"/services/ssh/key/send", json={"public_key": "ssh-ed25519 KEY test@pc"}) + response = authorized_client.put( + f"/services/ssh/key/send", json={"public_key": "ssh-ed25519 KEY test@pc"} + ) assert response.status_code == 409 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [ + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][ + "rootKeys" + ] == [ "ssh-ed25519 KEY test@pc", ] + def test_add_invalid_root_key(authorized_client, ssh_on): - response = authorized_client.put(f"/services/ssh/key/send", json={"public_key": "INVALID KEY test@pc"}) + response = authorized_client.put( + f"/services/ssh/key/send", json={"public_key": "INVALID KEY test@pc"} + ) assert response.status_code == 400 + def test_add_root_key_via_wrong_endpoint(authorized_client, ssh_on): - response = authorized_client.post(f"/services/ssh/keys/root", json={"public_key": "ssh-rsa KEY test@pc"}) + response = authorized_client.post( + f"/services/ssh/keys/root", json={"public_key": "ssh-rsa KEY test@pc"} + ) assert response.status_code == 400 + def test_get_root_key(authorized_client, root_and_admin_have_keys): response = authorized_client.get(f"/services/ssh/keys/root") assert response.status_code == 200 assert response.json == ["ssh-ed25519 KEY test@pc"] + def test_get_root_key_when_none(authorized_client, ssh_on): response = authorized_client.get(f"/services/ssh/keys/root") assert response.status_code == 200 assert response.json == [] + def test_delete_root_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.delete(f"/services/ssh/keys/root", json={"public_key": "ssh-ed25519 KEY test@pc"}) + response = authorized_client.delete( + f"/services/ssh/keys/root", json={"public_key": "ssh-ed25519 KEY test@pc"} + ) assert response.status_code == 200 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [] + assert ( + read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][ + "rootKeys" + ] + == [] + ) + def test_delete_root_nonexistent_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.delete(f"/services/ssh/keys/root", json={"public_key": "ssh-rsa KEY test@pc"}) + response = authorized_client.delete( + f"/services/ssh/keys/root", json={"public_key": "ssh-rsa KEY test@pc"} + ) assert response.status_code == 404 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [ + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][ + "rootKeys" + ] == [ "ssh-ed25519 KEY test@pc", ] + def test_get_admin_key(authorized_client, root_and_admin_have_keys): response = authorized_client.get(f"/services/ssh/keys/tester") assert response.status_code == 200 assert response.json == ["ssh-rsa KEY test@pc"] + def test_get_admin_key_when_none(authorized_client, ssh_on): response = authorized_client.get(f"/services/ssh/keys/tester") assert response.status_code == 200 assert response.json == [] + def test_delete_admin_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.delete(f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"}) + response = authorized_client.delete( + f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"} + ) assert response.status_code == 200 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["sshKeys"] == [] + assert ( + read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["sshKeys"] + == [] + ) + def test_add_admin_key(authorized_client, ssh_on): - response = authorized_client.post(f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"}) + response = authorized_client.post( + f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"} + ) assert response.status_code == 201 assert read_json(ssh_on / "turned_on.json")["sshKeys"] == [ "ssh-rsa KEY test@pc", ] + def test_add_admin_key_one_more(authorized_client, root_and_admin_have_keys): - response = authorized_client.post(f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY_2 test@pc"}) + response = authorized_client.post( + f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY_2 test@pc"} + ) assert response.status_code == 201 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["sshKeys"] == [ - "ssh-rsa KEY test@pc", - "ssh-rsa KEY_2 test@pc" - ] + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")[ + "sshKeys" + ] == ["ssh-rsa KEY test@pc", "ssh-rsa KEY_2 test@pc"] + def test_add_existing_admin_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.post(f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"}) + response = authorized_client.post( + f"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"} + ) assert response.status_code == 409 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["sshKeys"] == [ + assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")[ + "sshKeys" + ] == [ "ssh-rsa KEY test@pc", - ] \ No newline at end of file + ] From 077c886f40e79c605785ca50c1ddd320640a1c8d Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 1 Dec 2021 17:51:38 +0300 Subject: [PATCH 05/15] Add endpoint to update b2 keys --- selfprivacy_api/resources/services/restic.py | 55 +++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/selfprivacy_api/resources/services/restic.py b/selfprivacy_api/resources/services/restic.py index 282488d..6aef99f 100644 --- a/selfprivacy_api/resources/services/restic.py +++ b/selfprivacy_api/resources/services/restic.py @@ -3,9 +3,10 @@ import json import subprocess from flask import request -from flask_restful import Resource +from flask_restful import Resource, reqparse from selfprivacy_api.resources.services import api +from selfprivacy_api.utils import WriteUserData class ListAllBackups(Resource): @@ -169,7 +170,59 @@ class AsyncRestoreBackup(Resource): return {"status": 0, "message": backup_restoration_status} +class BackblazeConfig(Resource): + """Backblaze config""" + + def put(self): + """ + Set the new key for backblaze + --- + tags: + - Backups + security: + - bearerAuth: [] + parameters: + - in: body + required: true + name: backblazeSettings + description: New Backblaze settings + schema: + type: object + required: + - accountId + - accountKey + - bucket + properties: + accountId: + type: string + accountKey: + type: string + bucket: + type: string + responses: + 200: + description: New Backblaze settings + 400: + description: Bad request + 401: + description: Unauthorized + """ + parser = reqparse.RequestParser() + parser.add_argument("accountId", type=str, required=True) + parser.add_argument("accountKey", type=str, required=True) + parser.add_argument("bucket", type=str, required=True) + args = parser.parse_args() + + with WriteUserData() as data: + data["backblaze"]["accountId"] = args["accountId"] + data["backblaze"]["accountKey"] = args["accountKey"] + data["backblaze"]["bucket"] = args["bucket"] + + return "New Backblaze settings saved" + + api.add_resource(ListAllBackups, "/restic/backup/list") api.add_resource(AsyncCreateBackup, "/restic/backup/create") api.add_resource(CheckBackupStatus, "/restic/backup/status") api.add_resource(AsyncRestoreBackup, "/restic/backup/restore") +api.add_resource(BackblazeConfig, "/restic/backblaze/config") From c53cab7b88b0eb16ec7c45c451c585d924e311ee Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 1 Dec 2021 17:51:55 +0300 Subject: [PATCH 06/15] Move PullRepositoryChanges to system resource --- selfprivacy_api/resources/services/update.py | 55 -------------------- selfprivacy_api/resources/system.py | 47 +++++++++++++++++ 2 files changed, 47 insertions(+), 55 deletions(-) delete mode 100644 selfprivacy_api/resources/services/update.py diff --git a/selfprivacy_api/resources/services/update.py b/selfprivacy_api/resources/services/update.py deleted file mode 100644 index 73698d5..0000000 --- a/selfprivacy_api/resources/services/update.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env/python3 -"""Update dispatch module""" -import os -import subprocess -from flask_restful import Resource, reqparse - -from selfprivacy_api.resources.services import api - - -class PullRepositoryChanges(Resource): - def get(self): - """ - Pull Repository Changes - --- - tags: - - Update - security: - - bearerAuth: [] - responses: - 200: - description: Got update - 201: - description: Nothing to update - 401: - description: Unauthorized - 500: - description: Something went wrong - """ - - git_pull_command = ["git", "pull"] - - current_working_directory = os.getcwd() - os.chdir("/etc/nixos") - - git_pull_process_descriptor = subprocess.Popen( - git_pull_command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - shell=False, - ) - - git_pull_process_descriptor.communicate()[0] - - os.chdir(current_working_directory) - - if git_pull_process_descriptor.returncode == 0: - return {"status": 0, "message": "Update completed successfully"} - elif git_pull_process_descriptor.returncode > 0: - return { - "status": git_pull_process_descriptor.returncode, - "message": "Something went wrong", - }, 500 - - -api.add_resource(PullRepositoryChanges, "/update") diff --git a/selfprivacy_api/resources/system.py b/selfprivacy_api/resources/system.py index ddce3be..d13f1fc 100644 --- a/selfprivacy_api/resources/system.py +++ b/selfprivacy_api/resources/system.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """System management module""" +import os import subprocess import pytz from flask import Blueprint @@ -281,6 +282,51 @@ class PythonVersion(Resource): return subprocess.check_output(["python", "-V"]).decode("utf-8").strip() +class PullRepositoryChanges(Resource): + def get(self): + """ + Pull Repository Changes + --- + tags: + - System + security: + - bearerAuth: [] + responses: + 200: + description: Got update + 201: + description: Nothing to update + 401: + description: Unauthorized + 500: + description: Something went wrong + """ + + git_pull_command = ["git", "pull"] + + current_working_directory = os.getcwd() + os.chdir("/etc/nixos") + + git_pull_process_descriptor = subprocess.Popen( + git_pull_command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=False, + ) + + git_pull_process_descriptor.communicate()[0] + + os.chdir(current_working_directory) + + if git_pull_process_descriptor.returncode == 0: + return {"status": 0, "message": "Update completed successfully"} + elif git_pull_process_descriptor.returncode > 0: + return { + "status": git_pull_process_descriptor.returncode, + "message": "Something went wrong", + }, 500 + + api.add_resource(Timezone, "/configuration/timezone") api.add_resource(AutoUpgrade, "/configuration/autoUpgrade") api.add_resource(RebuildSystem, "/configuration/apply") @@ -289,3 +335,4 @@ api.add_resource(UpgradeSystem, "/configuration/upgrade") api.add_resource(RebootSystem, "/reboot") api.add_resource(SystemVersion, "/version") api.add_resource(PythonVersion, "/pythonVersion") +api.add_resource(PullRepositoryChanges, "/update") From 71fc93914afbcbc952e7fe1d2e4b55d1d2c8c2dd Mon Sep 17 00:00:00 2001 From: Inex Code Date: Thu, 2 Dec 2021 18:06:23 +0300 Subject: [PATCH 07/15] Update backup endpoints --- selfprivacy_api/app.py | 1 + selfprivacy_api/resources/services/restic.py | 75 ++++++++++++++------ selfprivacy_api/resources/system.py | 11 ++- 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/selfprivacy_api/app.py b/selfprivacy_api/app.py index 5138b66..c81017f 100644 --- a/selfprivacy_api/app.py +++ b/selfprivacy_api/app.py @@ -26,6 +26,7 @@ def create_app(test_config=None): if app.config["AUTH_TOKEN"] is None: raise ValueError("AUTH_TOKEN is not set") app.config["ENABLE_SWAGGER"] = os.environ.get("ENABLE_SWAGGER", "0") + app.config["B2_BUCKET"] = os.environ.get("B2_BUCKET") else: app.config.update(test_config) diff --git a/selfprivacy_api/resources/services/restic.py b/selfprivacy_api/resources/services/restic.py index 6aef99f..6480382 100644 --- a/selfprivacy_api/resources/services/restic.py +++ b/selfprivacy_api/resources/services/restic.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """Backups management module""" import json +import os import subprocess -from flask import request +from flask import current_app from flask_restful import Resource, reqparse from selfprivacy_api.resources.services import api @@ -28,12 +29,11 @@ class ListAllBackups(Resource): 401: description: Unauthorized """ - repository_name = request.headers.get("X-Repository-Name") - + bucket = current_app.config["B2_BUCKET"] backup_listing_command = [ "restic", "-r", - f"rclone:backblaze:{repository_name}:/sfbackup", + f"rclone:backblaze:{bucket}/sfbackup", "snapshots", "--json", ] @@ -46,7 +46,11 @@ class ListAllBackups(Resource): ) as backup_listing_process_descriptor: snapshots_list = backup_listing_process_descriptor.communicate()[0] - return snapshots_list.decode("utf-8") + try: + json.loads(snapshots_list.decode("utf-8")) + except ValueError: + return {"error": snapshots_list.decode("utf-8")}, 500 + return json.loads(snapshots_list.decode("utf-8")) class AsyncCreateBackup(Resource): @@ -68,18 +72,27 @@ class AsyncCreateBackup(Resource): 401: description: Unauthorized """ - repository_name = request.headers.get("X-Repository-Name") + bucket = current_app.config["B2_BUCKET"] + + init_command = [ + "restic", + "-r", + f"rclone:backblaze:{bucket}/sfbackup", + "init", + ] backup_command = [ "restic", "-r", - f"rclone:backblaze:{repository_name}:/sfbackup", + f"rclone:backblaze:{bucket}/sfbackup", "--verbose", "--json", "backup", "/var", ] + subprocess.call(init_command) + with open("/tmp/backup.log", "w", encoding="utf-8") as log_file: subprocess.Popen( backup_command, shell=False, stdout=log_file, stderr=subprocess.STDOUT @@ -112,6 +125,10 @@ class CheckBackupStatus(Resource): """ backup_status_check_command = ["tail", "-1", "/tmp/backup.log"] + # If the log file does not exists + if os.path.exists("/tmp/backup.log") is False: + return {"message_type": "not_started", "message": "Backup not started"} + with subprocess.Popen( backup_status_check_command, shell=False, @@ -125,8 +142,8 @@ class CheckBackupStatus(Resource): try: json.loads(backup_process_status) except ValueError: - return {"message": backup_process_status} - return backup_process_status + return {"message_type": "error", "message": backup_process_status} + return json.loads(backup_process_status) class AsyncRestoreBackup(Resource): @@ -140,6 +157,18 @@ class AsyncRestoreBackup(Resource): - Backups security: - bearerAuth: [] + parameters: + - in: body + required: true + name: backup + description: Backup to restore + schema: + type: object + required: + - backupId + properties: + backupId: + type: string responses: 200: description: Backup restoration process started @@ -148,26 +177,32 @@ class AsyncRestoreBackup(Resource): 401: description: Unauthorized """ + parser = reqparse.RequestParser() + parser.add_argument("backupId", type=str, required=True) + args = parser.parse_args() + bucket = current_app.config["B2_BUCKET"] + backup_id = args["backupId"] + backup_restoration_command = [ "restic", "-r", - "rclone:backblaze:sfbackup", - "var", + f"rclone:backblaze:{bucket}/sfbackup", + "restore", + backup_id, + "--target", + "/var", "--json", ] - with open( - "/tmp/backup.log", "w", encoding="utf-8" - ) as backup_log_file_descriptor: - with subprocess.Popen( + with open("/tmp/backup.log", "w", encoding="utf-8") as log_file: + subprocess.Popen( backup_restoration_command, shell=False, - stdout=subprocess.PIPE, - stderr=backup_log_file_descriptor, - ) as backup_restoration_process_descriptor: - backup_restoration_status = "Backup restoration procedure started" + stdout=log_file, + stderr=subprocess.STDOUT, + ) - return {"status": 0, "message": backup_restoration_status} + return {"status": 0, "message": "Backup restoration procedure started"} class BackblazeConfig(Resource): diff --git a/selfprivacy_api/resources/system.py b/selfprivacy_api/resources/system.py index d13f1fc..5dbc858 100644 --- a/selfprivacy_api/resources/system.py +++ b/selfprivacy_api/resources/system.py @@ -314,16 +314,21 @@ class PullRepositoryChanges(Resource): shell=False, ) - git_pull_process_descriptor.communicate()[0] + data = git_pull_process_descriptor.communicate()[0].decode("utf-8") os.chdir(current_working_directory) if git_pull_process_descriptor.returncode == 0: - return {"status": 0, "message": "Update completed successfully"} + return { + "status": 0, + "message": "Update completed successfully", + "data": data, + } elif git_pull_process_descriptor.returncode > 0: return { "status": git_pull_process_descriptor.returncode, "message": "Something went wrong", + "data": data, }, 500 @@ -335,4 +340,4 @@ api.add_resource(UpgradeSystem, "/configuration/upgrade") api.add_resource(RebootSystem, "/reboot") api.add_resource(SystemVersion, "/version") api.add_resource(PythonVersion, "/pythonVersion") -api.add_resource(PullRepositoryChanges, "/update") +api.add_resource(PullRepositoryChanges, "/configuration/pull") From dc4c9a89e134c886f10cb0b4b9bbb91de0b44a32 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Thu, 2 Dec 2021 23:08:25 +0300 Subject: [PATCH 08/15] Make list backups init backups --- selfprivacy_api/resources/services/restic.py | 29 +++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/selfprivacy_api/resources/services/restic.py b/selfprivacy_api/resources/services/restic.py index 6480382..4da364f 100644 --- a/selfprivacy_api/resources/services/restic.py +++ b/selfprivacy_api/resources/services/restic.py @@ -38,19 +38,31 @@ class ListAllBackups(Resource): "--json", ] + init_command = [ + "restic", + "-r", + f"rclone:backblaze:{bucket}/sfbackup", + "init", + ] + with subprocess.Popen( backup_listing_command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) as backup_listing_process_descriptor: - snapshots_list = backup_listing_process_descriptor.communicate()[0] + snapshots_list = backup_listing_process_descriptor.communicate()[0].decode( + "utf-8" + ) try: - json.loads(snapshots_list.decode("utf-8")) + json.loads(snapshots_list) except ValueError: - return {"error": snapshots_list.decode("utf-8")}, 500 - return json.loads(snapshots_list.decode("utf-8")) + if "Is there a repository at the following location?" in snapshots_list: + subprocess.call(init_command) + return {"error": "Initializating"}, 500 + return {"error": snapshots_list}, 500 + return json.loads(snapshots_list) class AsyncCreateBackup(Resource): @@ -74,13 +86,6 @@ class AsyncCreateBackup(Resource): """ bucket = current_app.config["B2_BUCKET"] - init_command = [ - "restic", - "-r", - f"rclone:backblaze:{bucket}/sfbackup", - "init", - ] - backup_command = [ "restic", "-r", @@ -91,8 +96,6 @@ class AsyncCreateBackup(Resource): "/var", ] - subprocess.call(init_command) - with open("/tmp/backup.log", "w", encoding="utf-8") as log_file: subprocess.Popen( backup_command, shell=False, stdout=log_file, stderr=subprocess.STDOUT From f68bd88a31d41320132b51d0be20127f23315102 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 6 Dec 2021 09:48:29 +0300 Subject: [PATCH 09/15] Restic controller --- requirements.txt | 2 + selfprivacy_api/app.py | 8 + selfprivacy_api/resources/services/restic.py | 159 +++++------ selfprivacy_api/restic_controller/__init__.py | 255 ++++++++++++++++++ selfprivacy_api/restic_controller/tasks.py | 72 +++++ selfprivacy_api/utils.py | 1 - 6 files changed, 403 insertions(+), 94 deletions(-) create mode 100644 selfprivacy_api/restic_controller/__init__.py create mode 100644 selfprivacy_api/restic_controller/tasks.py diff --git a/requirements.txt b/requirements.txt index 8f41c0e..62e65ac 100755 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,8 @@ portalocker flask-swagger flask-swagger-ui pytz +huey +gevent pytest coverage diff --git a/selfprivacy_api/app.py b/selfprivacy_api/app.py index c81017f..f74f650 100644 --- a/selfprivacy_api/app.py +++ b/selfprivacy_api/app.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 """SelfPrivacy server management API""" import os +from gevent import monkey + + from flask import Flask, request, jsonify from flask_restful import Api from flask_swagger import swagger @@ -11,6 +14,8 @@ from selfprivacy_api.resources.common import ApiVersion, DecryptDisk from selfprivacy_api.resources.system import api_system from selfprivacy_api.resources.services import services as api_services +from selfprivacy_api.restic_controller.tasks import huey, init_restic + swagger_blueprint = get_swaggerui_blueprint( "/api/docs", "/api/swagger.json", config={"app_name": "SelfPrivacy API"} ) @@ -77,5 +82,8 @@ def create_app(test_config=None): if __name__ == "__main__": + monkey.patch_all() created_app = create_app() + huey.start() + init_restic() created_app.run(port=5050, debug=False) diff --git a/selfprivacy_api/resources/services/restic.py b/selfprivacy_api/resources/services/restic.py index 4da364f..64ce2a8 100644 --- a/selfprivacy_api/resources/services/restic.py +++ b/selfprivacy_api/resources/services/restic.py @@ -1,13 +1,11 @@ #!/usr/bin/env python3 """Backups management module""" -import json -import os -import subprocess -from flask import current_app from flask_restful import Resource, reqparse from selfprivacy_api.resources.services import api from selfprivacy_api.utils import WriteUserData +from selfprivacy_api.restic_controller import tasks as restic_tasks +from selfprivacy_api.restic_controller import ResticController, ResticStates class ListAllBackups(Resource): @@ -29,40 +27,9 @@ class ListAllBackups(Resource): 401: description: Unauthorized """ - bucket = current_app.config["B2_BUCKET"] - backup_listing_command = [ - "restic", - "-r", - f"rclone:backblaze:{bucket}/sfbackup", - "snapshots", - "--json", - ] - init_command = [ - "restic", - "-r", - f"rclone:backblaze:{bucket}/sfbackup", - "init", - ] - - with subprocess.Popen( - backup_listing_command, - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) as backup_listing_process_descriptor: - snapshots_list = backup_listing_process_descriptor.communicate()[0].decode( - "utf-8" - ) - - try: - json.loads(snapshots_list) - except ValueError: - if "Is there a repository at the following location?" in snapshots_list: - subprocess.call(init_command) - return {"error": "Initializating"}, 500 - return {"error": snapshots_list}, 500 - return json.loads(snapshots_list) + restic = ResticController() + return restic.snapshot_list class AsyncCreateBackup(Resource): @@ -83,24 +50,17 @@ class AsyncCreateBackup(Resource): description: Bad request 401: description: Unauthorized + 409: + description: Backup already in progress """ - bucket = current_app.config["B2_BUCKET"] - - backup_command = [ - "restic", - "-r", - f"rclone:backblaze:{bucket}/sfbackup", - "--verbose", - "--json", - "backup", - "/var", - ] - - with open("/tmp/backup.log", "w", encoding="utf-8") as log_file: - subprocess.Popen( - backup_command, shell=False, stdout=log_file, stderr=subprocess.STDOUT - ) - + restic = ResticController() + if restic.state is ResticStates.NO_KEY: + return {"error": "No key provided"}, 400 + if restic.state is ResticStates.INITIALIZING: + return {"error": "Backup is initializing"}, 400 + if restic.state is ResticStates.BACKING_UP: + return {"error": "Backup is already running"}, 409 + restic_tasks.start_backup() return { "status": 0, "message": "Backup creation has started", @@ -126,27 +86,39 @@ class CheckBackupStatus(Resource): 401: description: Unauthorized """ - backup_status_check_command = ["tail", "-1", "/tmp/backup.log"] + restic = ResticController() - # If the log file does not exists - if os.path.exists("/tmp/backup.log") is False: - return {"message_type": "not_started", "message": "Backup not started"} + return { + "status": restic.state.name, + "progress": restic.progress, + "error_message": restic.error_message, + } - with subprocess.Popen( - backup_status_check_command, - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) as backup_status_check_process_descriptor: - backup_process_status = ( - backup_status_check_process_descriptor.communicate()[0].decode("utf-8") - ) - try: - json.loads(backup_process_status) - except ValueError: - return {"message_type": "error", "message": backup_process_status} - return json.loads(backup_process_status) +class ForceReloadSnapshots(Resource): + """Force reload snapshots""" + + def get(self): + """ + Force reload snapshots + --- + tags: + - Backups + security: + - bearerAuth: [] + responses: + 200: + description: Snapshots reloaded + 400: + description: Bad request + 401: + description: Unauthorized + """ + restic_tasks.load_snapshots() + return { + "status": 0, + "message": "Snapshots reload started", + } class AsyncRestoreBackup(Resource): @@ -183,29 +155,27 @@ class AsyncRestoreBackup(Resource): parser = reqparse.RequestParser() parser.add_argument("backupId", type=str, required=True) args = parser.parse_args() - bucket = current_app.config["B2_BUCKET"] - backup_id = args["backupId"] - backup_restoration_command = [ - "restic", - "-r", - f"rclone:backblaze:{bucket}/sfbackup", - "restore", - backup_id, - "--target", - "/var", - "--json", - ] + restic = ResticController() + if restic.state is ResticStates.NO_KEY: + return {"error": "No key provided"}, 400 + if restic.state is ResticStates.NOT_INITIALIZED: + return {"error": "Repository is not initialized"}, 400 + if restic.state is ResticStates.BACKING_UP: + return {"error": "Backup is already running"}, 409 + if restic.state is ResticStates.INITIALIZING: + return {"error": "Repository is initializing"}, 400 + if restic.state is ResticStates.RESTORING: + return {"error": "Restore is already running"}, 409 + for backup in restic.snapshot_list: + if backup["short_id"] == args["backupId"]: + restic_tasks.restore_from_backup(args["backupId"]) + return { + "status": 0, + "message": "Backup restoration procedure started", + } - with open("/tmp/backup.log", "w", encoding="utf-8") as log_file: - subprocess.Popen( - backup_restoration_command, - shell=False, - stdout=log_file, - stderr=subprocess.STDOUT, - ) - - return {"status": 0, "message": "Backup restoration procedure started"} + return {"error": "Backup not found"}, 404 class BackblazeConfig(Resource): @@ -256,6 +226,8 @@ class BackblazeConfig(Resource): data["backblaze"]["accountKey"] = args["accountKey"] data["backblaze"]["bucket"] = args["bucket"] + restic_tasks.update_keys_from_userdata() + return "New Backblaze settings saved" @@ -264,3 +236,4 @@ api.add_resource(AsyncCreateBackup, "/restic/backup/create") api.add_resource(CheckBackupStatus, "/restic/backup/status") api.add_resource(AsyncRestoreBackup, "/restic/backup/restore") api.add_resource(BackblazeConfig, "/restic/backblaze/config") +api.add_resource(ForceReloadSnapshots, "/restic/backup/reload") diff --git a/selfprivacy_api/restic_controller/__init__.py b/selfprivacy_api/restic_controller/__init__.py new file mode 100644 index 0000000..2187cb3 --- /dev/null +++ b/selfprivacy_api/restic_controller/__init__.py @@ -0,0 +1,255 @@ +"""Restic singleton controller.""" +from datetime import datetime +import json +import subprocess +import os +from threading import Lock +from enum import Enum +import portalocker +from selfprivacy_api.utils import ReadUserData + + +class ResticStates(Enum): + """Restic states enum.""" + + NO_KEY = 0 + NOT_INITIALIZED = 1 + INITIALIZED = 2 + BACKING_UP = 3 + RESTORING = 4 + ERROR = 5 + INITIALIZING = 6 + + +class ResticController: + """ + States in wich the restic_controller may be + - no backblaze key + - backblaze key is provided, but repository is not initialized + - backblaze key is provided, repository is initialized + - fetching list of snapshots + - creating snapshot, current progress can be retrieved + - recovering from snapshot + + Any ongoing operation acquires the lock + Current state can be fetched with get_state() + """ + + _instance = None + _lock = Lock() + _initialized = False + + def __new__(cls): + print("new is called!") + if not cls._instance: + cls._instance = super(ResticController, cls).__new__(cls) + return cls._instance + + def __init__(self): + if self._initialized: + return + self.state = ResticStates.NO_KEY + self.lock = False + self.progress = 0 + self._backblaze_account = None + self._backblaze_key = None + self._repository_name = None + self.snapshot_list = [] + self.error_message = None + print("init is called!") + self.load_configuration() + self.write_rclone_config() + self.load_snapshots() + self._initialized = True + + def load_configuration(self): + """Load current configuration from user data to singleton.""" + with ReadUserData() as user_data: + self._backblaze_account = user_data["backblaze"]["accountId"] + self._backblaze_key = user_data["backblaze"]["accountKey"] + self._repository_name = user_data["backblaze"]["bucket"] + if self._backblaze_account and self._backblaze_key and self._repository_name: + self.state = ResticStates.INITIALIZING + else: + self.state = ResticStates.NO_KEY + + def write_rclone_config(self): + """ + Open /root/.config/rclone/rclone.conf with portalocker + and write configuration in the following format: + [backblaze] + type = b2 + account = {self.backblaze_account} + key = {self.backblaze_key} + """ + with portalocker.Lock( + "/root/.config/rclone/rclone.conf", "w", timeout=None + ) as rclone_config: + rclone_config.write( + f"[backblaze]\n" + f"type = b2\n" + f"account = {self._backblaze_account}\n" + f"key = {self._backblaze_key}\n" + ) + + def load_snapshots(self): + """ + Load list of snapshots from repository + """ + backup_listing_command = [ + "restic", + "-r", + f"rclone:backblaze:{self._repository_name}/sfbackup", + "snapshots", + "--json", + ] + + if ( + self.state == ResticStates.BACKING_UP + or self.state == ResticStates.RESTORING + ): + return + + with self._lock: + with subprocess.Popen( + backup_listing_command, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) as backup_listing_process_descriptor: + snapshots_list = backup_listing_process_descriptor.communicate()[ + 0 + ].decode("utf-8") + try: + starting_index = snapshots_list.find("[") + json.loads(snapshots_list[starting_index:]) + self.snapshot_list = json.loads(snapshots_list[starting_index:]) + self.state = ResticStates.INITIALIZED + except ValueError: + if "Is there a repository at the following location?" in snapshots_list: + self.state = ResticStates.NOT_INITIALIZED + return + else: + self.state = ResticStates.ERROR + self.error_message = snapshots_list + return + + def initialize_repository(self): + """ + Initialize repository with restic + """ + initialize_repository_command = [ + "restic", + "-r", + f"rclone:backblaze:{self._repository_name}/sfbackup", + "init", + ] + with self._lock: + with subprocess.Popen( + initialize_repository_command, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) as initialize_repository_process_descriptor: + msg = initialize_repository_process_descriptor.communicate()[0].decode( + "utf-8" + ) + if initialize_repository_process_descriptor.returncode == 0: + self.state = ResticStates.INITIALIZED + else: + self.state = ResticStates.ERROR + self.error_message = msg + + self.state = ResticStates.INITIALIZED + + def start_backup(self): + """ + Start backup with restic + """ + backup_command = [ + "restic", + "-r", + f"rclone:backblaze:{self._repository_name}/sfbackup", + "--verbose", + "--json", + "backup", + "/var", + ] + with self._lock: + with open("/tmp/backup.log", "w", encoding="utf-8") as log_file: + subprocess.Popen( + backup_command, + shell=False, + stdout=log_file, + stderr=subprocess.STDOUT, + ) + + self.state = ResticStates.BACKING_UP + self.progress = 0 + + def check_progress(self): + """ + Check progress of ongoing backup operation + """ + backup_status_check_command = ["tail", "-1", "/tmp/backup.log"] + + if ( + self.state == ResticStates.NO_KEY + or self.state == ResticStates.NOT_INITIALIZED + ): + return + + # If the log file does not exists + if os.path.exists("/tmp/backup.log") is False: + self.state = ResticStates.INITIALIZED + + with subprocess.Popen( + backup_status_check_command, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) as backup_status_check_process_descriptor: + backup_process_status = ( + backup_status_check_process_descriptor.communicate()[0].decode("utf-8") + ) + + try: + status = json.loads(backup_process_status) + except ValueError: + print(backup_process_status) + self.error_message = backup_process_status + return + if status["message_type"] == "status": + self.progress = status["percent_done"] + self.state = ResticStates.BACKING_UP + elif status["message_type"] == "summary": + self.state = ResticStates.INITIALIZED + self.progress = 0 + self.snapshot_list.append( + { + "short_id": status["snapshot_id"], + # Current time in format 2021-12-02T00:02:51.086452543+03:00 + "time": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f%z"), + } + ) + + def restore_from_backup(self, snapshot_id): + """ + Restore from backup with restic + """ + backup_restoration_command = [ + "restic", + "-r", + f"rclone:backblaze:{self._repository_name}/sfbackup", + "restore", + snapshot_id, + "--target", + "/", + ] + + self.state = ResticStates.RESTORING + + with self._lock: + subprocess.run(backup_restoration_command, shell=False) + + self.state = ResticStates.INITIALIZED diff --git a/selfprivacy_api/restic_controller/tasks.py b/selfprivacy_api/restic_controller/tasks.py new file mode 100644 index 0000000..4c610c4 --- /dev/null +++ b/selfprivacy_api/restic_controller/tasks.py @@ -0,0 +1,72 @@ +"""Tasks for the restic controller.""" +from huey import crontab +from huey.contrib.mini import MiniHuey +from . import ResticController, ResticStates + +huey = MiniHuey() + + +@huey.task() +def init_restic(): + controller = ResticController() + if controller.state == ResticStates.NOT_INITIALIZED: + initialize_repository() + + +@huey.task() +def update_keys_from_userdata(): + controller = ResticController() + controller.load_configuration() + controller.write_rclone_config() + initialize_repository() + + +# Check every morning at 5:00 AM +@huey.task(crontab(hour=5, minute=0)) +def cron_load_snapshots(): + controller = ResticController() + controller.load_snapshots() + + +# Check every morning at 5:00 AM +@huey.task() +def load_snapshots(): + controller = ResticController() + controller.load_snapshots() + if controller.state == ResticStates.NOT_INITIALIZED: + load_snapshots.schedule(delay=120) + + +@huey.task() +def initialize_repository(): + controller = ResticController() + if controller.state is not ResticStates.NO_KEY: + controller.initialize_repository() + load_snapshots() + + +@huey.task() +def fetch_backup_status(): + controller = ResticController() + if controller.state is ResticStates.BACKING_UP: + controller.check_progress() + if controller.state is ResticStates.BACKING_UP: + fetch_backup_status.schedule(delay=2) + else: + load_snapshots.schedule(delay=240) + + +@huey.task() +def start_backup(): + controller = ResticController() + if controller.state is ResticStates.NOT_INITIALIZED: + resp = initialize_repository() + resp.get() + controller.start_backup() + fetch_backup_status.schedule(delay=3) + + +@huey.task() +def restore_from_backup(snapshot): + controller = ResticController() + controller.restore_from_backup(snapshot) diff --git a/selfprivacy_api/utils.py b/selfprivacy_api/utils.py index b0a7686..1b0c43c 100644 --- a/selfprivacy_api/utils.py +++ b/selfprivacy_api/utils.py @@ -2,7 +2,6 @@ """Various utility functions""" import json import portalocker -from flask import current_app USERDATA_FILE = "/etc/nixos/userdata/userdata.json" From 340b50bb0d79f7e7ac2477ea20af92951a23a5aa Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 6 Dec 2021 12:00:53 +0300 Subject: [PATCH 10/15] Remove locks --- selfprivacy_api/restic_controller/__init__.py | 100 ++++++++---------- 1 file changed, 47 insertions(+), 53 deletions(-) diff --git a/selfprivacy_api/restic_controller/__init__.py b/selfprivacy_api/restic_controller/__init__.py index 2187cb3..46dd9b5 100644 --- a/selfprivacy_api/restic_controller/__init__.py +++ b/selfprivacy_api/restic_controller/__init__.py @@ -3,7 +3,6 @@ from datetime import datetime import json import subprocess import os -from threading import Lock from enum import Enum import portalocker from selfprivacy_api.utils import ReadUserData @@ -36,7 +35,6 @@ class ResticController: """ _instance = None - _lock = Lock() _initialized = False def __new__(cls): @@ -110,29 +108,28 @@ class ResticController: ): return - with self._lock: - with subprocess.Popen( - backup_listing_command, - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) as backup_listing_process_descriptor: - snapshots_list = backup_listing_process_descriptor.communicate()[ - 0 - ].decode("utf-8") - try: - starting_index = snapshots_list.find("[") - json.loads(snapshots_list[starting_index:]) - self.snapshot_list = json.loads(snapshots_list[starting_index:]) - self.state = ResticStates.INITIALIZED - except ValueError: - if "Is there a repository at the following location?" in snapshots_list: - self.state = ResticStates.NOT_INITIALIZED - return - else: - self.state = ResticStates.ERROR - self.error_message = snapshots_list - return + with subprocess.Popen( + backup_listing_command, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) as backup_listing_process_descriptor: + snapshots_list = backup_listing_process_descriptor.communicate()[0].decode( + "utf-8" + ) + try: + starting_index = snapshots_list.find("[") + json.loads(snapshots_list[starting_index:]) + self.snapshot_list = json.loads(snapshots_list[starting_index:]) + self.state = ResticStates.INITIALIZED + except ValueError: + if "Is there a repository at the following location?" in snapshots_list: + self.state = ResticStates.NOT_INITIALIZED + return + else: + self.state = ResticStates.ERROR + self.error_message = snapshots_list + return def initialize_repository(self): """ @@ -144,23 +141,22 @@ class ResticController: f"rclone:backblaze:{self._repository_name}/sfbackup", "init", ] - with self._lock: - with subprocess.Popen( - initialize_repository_command, - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) as initialize_repository_process_descriptor: - msg = initialize_repository_process_descriptor.communicate()[0].decode( - "utf-8" - ) - if initialize_repository_process_descriptor.returncode == 0: - self.state = ResticStates.INITIALIZED - else: - self.state = ResticStates.ERROR - self.error_message = msg + with subprocess.Popen( + initialize_repository_command, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) as initialize_repository_process_descriptor: + msg = initialize_repository_process_descriptor.communicate()[0].decode( + "utf-8" + ) + if initialize_repository_process_descriptor.returncode == 0: + self.state = ResticStates.INITIALIZED + else: + self.state = ResticStates.ERROR + self.error_message = msg - self.state = ResticStates.INITIALIZED + self.state = ResticStates.INITIALIZED def start_backup(self): """ @@ -175,17 +171,16 @@ class ResticController: "backup", "/var", ] - with self._lock: - with open("/tmp/backup.log", "w", encoding="utf-8") as log_file: - subprocess.Popen( - backup_command, - shell=False, - stdout=log_file, - stderr=subprocess.STDOUT, - ) + with open("/tmp/backup.log", "w", encoding="utf-8") as log_file: + subprocess.Popen( + backup_command, + shell=False, + stdout=log_file, + stderr=subprocess.STDOUT, + ) - self.state = ResticStates.BACKING_UP - self.progress = 0 + self.state = ResticStates.BACKING_UP + self.progress = 0 def check_progress(self): """ @@ -249,7 +244,6 @@ class ResticController: self.state = ResticStates.RESTORING - with self._lock: - subprocess.run(backup_restoration_command, shell=False) + subprocess.run(backup_restoration_command, shell=False) self.state = ResticStates.INITIALIZED From f4288dacd692a6b14a33828bc9c86af428f81d4c Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 6 Dec 2021 12:07:03 +0300 Subject: [PATCH 11/15] debugging --- selfprivacy_api/restic_controller/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/selfprivacy_api/restic_controller/__init__.py b/selfprivacy_api/restic_controller/__init__.py index 46dd9b5..6522852 100644 --- a/selfprivacy_api/restic_controller/__init__.py +++ b/selfprivacy_api/restic_controller/__init__.py @@ -3,6 +3,7 @@ from datetime import datetime import json import subprocess import os +from threading import Lock from enum import Enum import portalocker from selfprivacy_api.utils import ReadUserData @@ -35,12 +36,14 @@ class ResticController: """ _instance = None + _lock = Lock() _initialized = False def __new__(cls): print("new is called!") if not cls._instance: - cls._instance = super(ResticController, cls).__new__(cls) + with cls._lock: + cls._instance = super(ResticController, cls).__new__(cls) return cls._instance def __init__(self): @@ -54,11 +57,11 @@ class ResticController: self._repository_name = None self.snapshot_list = [] self.error_message = None + self._initialized = True print("init is called!") self.load_configuration() self.write_rclone_config() self.load_snapshots() - self._initialized = True def load_configuration(self): """Load current configuration from user data to singleton.""" @@ -107,21 +110,22 @@ class ResticController: or self.state == ResticStates.RESTORING ): return - + print("preparing to read snapshots") with subprocess.Popen( backup_listing_command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) as backup_listing_process_descriptor: - snapshots_list = backup_listing_process_descriptor.communicate()[0].decode( - "utf-8" - ) + snapshots_list = backup_listing_process_descriptor.communicate()[ + 0 + ].decode("utf-8") try: starting_index = snapshots_list.find("[") json.loads(snapshots_list[starting_index:]) self.snapshot_list = json.loads(snapshots_list[starting_index:]) self.state = ResticStates.INITIALIZED + print(snapshots_list) except ValueError: if "Is there a repository at the following location?" in snapshots_list: self.state = ResticStates.NOT_INITIALIZED From 710925f3eaad793037b24c5038cf303cc378cd3c Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 6 Dec 2021 21:35:41 +0300 Subject: [PATCH 12/15] Linting --- selfprivacy_api/restic_controller/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/selfprivacy_api/restic_controller/__init__.py b/selfprivacy_api/restic_controller/__init__.py index 6522852..6e3e5a5 100644 --- a/selfprivacy_api/restic_controller/__init__.py +++ b/selfprivacy_api/restic_controller/__init__.py @@ -117,9 +117,9 @@ class ResticController: stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) as backup_listing_process_descriptor: - snapshots_list = backup_listing_process_descriptor.communicate()[ - 0 - ].decode("utf-8") + snapshots_list = backup_listing_process_descriptor.communicate()[0].decode( + "utf-8" + ) try: starting_index = snapshots_list.find("[") json.loads(snapshots_list[starting_index:]) From 3cb6b769dfecd84c14b0a464879ba943d87cfc9c Mon Sep 17 00:00:00 2001 From: Illia Chub Date: Mon, 6 Dec 2021 21:11:04 +0200 Subject: [PATCH 13/15] Added CI/CD integration --- .drone.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .drone.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..262a1cc --- /dev/null +++ b/.drone.yml @@ -0,0 +1,9 @@ +kind: pipelinetype: exec +name: default +platform: + os: linux + arch: amd64 +steps: +- name: test + commands: + - pytest \ No newline at end of file From 0e8e78de0876e4135c1a38466d58892b7865b63a Mon Sep 17 00:00:00 2001 From: Illia Chub Date: Mon, 6 Dec 2021 21:12:30 +0200 Subject: [PATCH 14/15] Fixed identation --- .drone.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 262a1cc..6ab4b9f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,8 +1,11 @@ -kind: pipelinetype: exec +kind: pipeline +type: exec name: default + platform: os: linux arch: amd64 + steps: - name: test commands: From f24323606f80b2dbc98915fdbc757b8b3ca6092c Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 8 Dec 2021 07:45:27 +0300 Subject: [PATCH 15/15] Fix restic to hide files instead of deleting --- selfprivacy_api/restic_controller/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/selfprivacy_api/restic_controller/__init__.py b/selfprivacy_api/restic_controller/__init__.py index 6e3e5a5..cefef53 100644 --- a/selfprivacy_api/restic_controller/__init__.py +++ b/selfprivacy_api/restic_controller/__init__.py @@ -99,6 +99,8 @@ class ResticController: """ backup_listing_command = [ "restic", + "-o", + "rclone.args=serve restic --stdio", "-r", f"rclone:backblaze:{self._repository_name}/sfbackup", "snapshots", @@ -141,6 +143,8 @@ class ResticController: """ initialize_repository_command = [ "restic", + "-o", + "rclone.args=serve restic --stdio", "-r", f"rclone:backblaze:{self._repository_name}/sfbackup", "init", @@ -168,6 +172,8 @@ class ResticController: """ backup_command = [ "restic", + "-o", + "rclone.args=serve restic --stdio", "-r", f"rclone:backblaze:{self._repository_name}/sfbackup", "--verbose", @@ -238,6 +244,8 @@ class ResticController: """ backup_restoration_command = [ "restic", + "-o", + "rclone.args=serve restic --stdio", "-r", f"rclone:backblaze:{self._repository_name}/sfbackup", "restore",