Merge pull request 'Move to JSON controlled server settings' (#3) from inex/selfprivacy-rest-api:json-manipulations into master

Reviewed-on: https://git.selfprivacy.org/ilchub/selfprivacy-rest-api/pulls/3
This commit is contained in:
Illia Chub 2021-11-16 09:42:52 +02:00
commit a86f2fe2bb
10 changed files with 226 additions and 237 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"python.formatting.provider": "black"
}

View file

@ -1,3 +1,3 @@
[build-system] [build-system]
requires = ["setuptools", "wheel"] requires = ["setuptools", "wheel", "portalocker"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"

View file

@ -3,3 +3,4 @@ flask
flask_restful flask_restful
flask_socketio flask_socketio
setuptools setuptools
portalocker

View file

@ -1,22 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from flask_restful import Resource from flask_restful import Resource
import portalocker
import json
from selfprivacy_api.resources.services import api from selfprivacy_api.resources.services import api
# Enable Bitwarden # Enable Bitwarden
class EnableBitwarden(Resource): class EnableBitwarden(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/passmgr/bitwarden.nix", "rt") with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = false;", "enable = true;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/passmgr/bitwarden.nix", "wt") if "bitwarden" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["bitwarden"] = {}
readWriteFileDescriptor.close() data["bitwarden"]["enable"] = True
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "Bitwarden enabled", "message": "Bitwarden enabled",
} }
@ -24,17 +30,21 @@ class EnableBitwarden(Resource):
# Disable Bitwarden # Disable Bitwarden
class DisableBitwarden(Resource): class DisableBitwarden(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/passmgr/bitwarden.nix", "rt") with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = true;", "enable = false;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/passmgr/bitwarden.nix", "wt") if "bitwarden" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["bitwarden"] = {}
readWriteFileDescriptor.close() data["bitwarden"]["enable"] = False
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "Bitwarden disabled", "message": "Bitwarden disabled",
} }

View file

@ -1,22 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from flask_restful import Resource from flask_restful import Resource
import portalocker
import json
from selfprivacy_api.resources.services import api from selfprivacy_api.resources.services import api
# Enable Gitea # Enable Gitea
class EnableGitea(Resource): class EnableGitea(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/git/gitea.nix", "rt") with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = false;", "enable = true;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/git/gitea.nix", "wt") if "gitea" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["gitea"] = {}
readWriteFileDescriptor.close() data["gitea"]["enable"] = True
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "Gitea enabled", "message": "Gitea enabled",
} }
@ -24,17 +30,21 @@ class EnableGitea(Resource):
# Disable Gitea # Disable Gitea
class DisableGitea(Resource): class DisableGitea(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/git/gitea.nix", "rt") with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = true;", "enable = false;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/git/gitea.nix", "wt") if "gitea" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["gitea"] = {}
readWriteFileDescriptor.close() data["gitea"]["enable"] = False
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "Gitea disabled", "message": "Gitea disabled",
} }

View file

@ -1,22 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from flask_restful import Resource from flask_restful import Resource
import portalocker
import json
from selfprivacy_api.resources.services import api from selfprivacy_api.resources.services import api
# Enable Nextcloud # Enable Nextcloud
class EnableNextcloud(Resource): class EnableNextcloud(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/nextcloud/nextcloud.nix", "rt") with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = false;", "enable = true;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/nextcloud/nextcloud.nix", "wt") if "nextcloud" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["nextcloud"] = {}
readWriteFileDescriptor.close() data["nextcloud"]["enable"] = True
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "Nextcloud enabled", "message": "Nextcloud enabled",
} }
@ -24,17 +30,21 @@ class EnableNextcloud(Resource):
# Disable Nextcloud # Disable Nextcloud
class DisableNextcloud(Resource): class DisableNextcloud(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/nextcloud/nextcloud.nix", "rt") with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = true;", "enable = false;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/nextcloud/nextcloud.nix", "wt") if "nextcloud" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["nextcloud"] = {}
readWriteFileDescriptor.close() data["nextcloud"]["enable"] = False
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "Nextcloud disabled", "message": "Nextcloud disabled",
} }

View file

@ -1,22 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from flask_restful import Resource from flask_restful import Resource
import portalocker
import json
from selfprivacy_api.resources.services import api from selfprivacy_api.resources.services import api
# Enable OpenConnect VPN server # Enable OpenConnect VPN server
class EnableOcserv(Resource): class EnableOcserv(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/vpn/ocserv.nix", "rt") with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = false;", "enable = true;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/vpn/ocserv.nix", "wt") if "ocserv" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["ocserv"] = {}
readWriteFileDescriptor.close() data["ocserv"]["enable"] = True
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "OpenConnect VPN server enabled", "message": "OpenConnect VPN server enabled",
} }
@ -24,17 +30,21 @@ class EnableOcserv(Resource):
# Disable OpenConnect VPN server # Disable OpenConnect VPN server
class DisableOcserv(Resource): class DisableOcserv(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/vpn/ocserv.nix", "rt") with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = true;", "enable = false;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/vpn/ocserv.nix", "wt") if "ocserv" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["ocserv"] = {}
readWriteFileDescriptor.close() data["ocserv"]["enable"] = False
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "OpenConnect VPN server disabled", "message": "OpenConnect VPN server disabled",
} }

View file

@ -1,22 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from flask_restful import Resource from flask_restful import Resource
import portalocker
import json
from selfprivacy_api.resources.services import api from selfprivacy_api.resources.services import api
# Enable Pleroma # Enable Pleroma
class EnablePleroma(Resource): class EnablePleroma(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/social/pleroma.nix", "rt") with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = false;", "enable = true;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/social/pleroma.nix", "wt") if "pleroma" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["pleroma"] = {}
readWriteFileDescriptor.close() data["pleroma"]["enable"] = True
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "Pleroma enabled", "message": "Pleroma enabled",
} }
@ -24,17 +30,21 @@ class EnablePleroma(Resource):
# Disable Pleroma # Disable Pleroma
class DisablePleroma(Resource): class DisablePleroma(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/social/pleroma.nix", "rt") with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = true;", "enable = false;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/social/pleroma.nix", "wt") if "pleroma" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["pleroma"] = {}
readWriteFileDescriptor.close() data["pleroma"]["enable"] = False
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "Pleroma disabled", "message": "Pleroma disabled",
} }

View file

@ -1,23 +1,29 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from flask import Blueprint, request from flask import Blueprint, request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
import portalocker
import json
from selfprivacy_api.resources.services import api from selfprivacy_api.resources.services import api
# Enable SSH # Enable SSH
class EnableSSH(Resource): class EnableSSH(Resource):
def post(self): def post(self):
readOnlyFileDescriptor = open("/etc/nixos/configuration.nix", "rt") with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f:
fileContent = readOnlyFileDescriptor.read() portalocker.lock(f, portalocker.LOCK_EX)
fileContent = fileContent.replace("enable = false;", "enable = true;") try:
readOnlyFileDescriptor.close() data = json.load(f)
readWriteFileDescriptor = open("/etc/nixos/configuration.nix", "wt") if "ssh" not in data:
writeOperationDescriptor = readWriteFileDescriptor.write(fileContent) data["ssh"] = {}
readWriteFileDescriptor.close() data["ssh"]["enable"] = True
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return { return {
"status": 0, "status": 0,
"descriptor": writeOperationDescriptor,
"message": "SSH enabled", "message": "SSH enabled",
} }
@ -33,41 +39,28 @@ class WriteSSHKey(Resource):
publicKey = args["public_key"] publicKey = args["public_key"]
print("[INFO] Opening /etc/nixos/configuration.nix...", sep="") with portalocker.Lock("/etc/nixos/userdata/userdata.json", "r+") as f:
readOnlyFileDescriptor = open("/etc/nixos/configuration.nix", "r") portalocker.lock(f, portalocker.LOCK_EX)
print("done") try:
fileContent = list() data = json.load(f)
index = int(0) if "ssh" not in data:
data["ssh"] = {}
print("[INFO] Reading file content...", sep="") # Return 400 if key already in array
for key in data["ssh"]["rootSshKeys"]:
while True: if key == publicKey:
line = readOnlyFileDescriptor.readline() return {
"error": "Key already exists",
if not line: }, 400
break data["ssh"]["rootSshKeys"].append(publicKey)
else: f.seek(0)
fileContent.append(line) json.dump(data, f, indent=4)
print("[DEBUG] Read line!") f.truncate()
finally:
for line in fileContent: portalocker.unlock(f)
index += 1
if "openssh.authorizedKeys.keys = [" in line:
print("[DEBUG] Found SSH key configuration snippet match!")
print("[INFO] Writing new SSH key", sep="")
fileContent.append('\n "' + publicKey + '"')
print("done")
break
print("[INFO] Writing data from memory to file...", sep="")
readWriteFileDescriptor = open("/etc/nixos/configuration.nix", "w")
print("done")
operationResult = readWriteFileDescriptor.writelines(fileContent)
return { return {
"status": 0, "status": 0,
"descriptor": operationResult, "message": "New SSH key successfully written",
"message": "New SSH key successfully written to /etc/nixos/configuration.nix",
} }

View file

@ -2,6 +2,9 @@
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from flask_restful import Resource, Api from flask_restful import Resource, Api
import subprocess import subprocess
import portalocker
import json
import re
from selfprivacy_api import resources from selfprivacy_api import resources
@ -24,123 +27,62 @@ class Users(Resource):
hashedPassword = hashedPassword.decode("ascii") hashedPassword = hashedPassword.decode("ascii")
hashedPassword = hashedPassword.rstrip() hashedPassword = hashedPassword.rstrip()
print("[TRACE] {0}".format(hashedPassword)) with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f:
portalocker.lock(f, portalocker.LOCK_EX)
print("[INFO] Opening /etc/nixos/users.nix...", sep="") try:
readOnlyFileDescriptor = open("/etc/nixos/users.nix", "r") data = json.load(f)
print("done") # Return 400 if username is not provided
fileContent = list() if request.headers.get("X-User") is None:
index = int(0) return {"error": "username is required"}, 400
# Return 400 if password is not provided
print("[INFO] Reading file content...", sep="") if request.headers.get("X-Password") is None:
return {"error": "password is required"}, 400
while True: # Check is username passes regex
line = readOnlyFileDescriptor.readline() if not re.match(r"^[a-z_][a-z0-9_]+$", request.headers.get("X-User")):
return {"error": "username must be alphanumeric"}, 400
if not line: # Check if username less than 32 characters
break if len(request.headers.get("X-User")) > 32:
else: return {"error": "username must be less than 32 characters"}, 400
fileContent.append(line) # Return 400 if user already exists
print("[DEBUG] Read line!") for user in data["users"]:
if user["username"] == request.headers.get("X-User"):
userTemplate = """ return {"error": "User already exists"}, 400
if "users" not in data:
#begin data["users"] = []
\"{0}\" = {{ data["users"].append(
isNormalUser = true; {
hashedPassword = \"{1}\"; "username": request.headers.get("X-User"),
}}; "hashedPassword": hashedPassword,
#end
""".format(
request.headers.get("X-User"), hashedPassword
)
mailUserTemplate = """
\"{0}@{2}\" = {{
hashedPassword =
\"{1}\";
catchAll = [ \"{2}\" ];
sieveScript = ''
require [\"fileinto\", \"mailbox\"];
if header :contains \"Chat-Version\" \"1.0\"
{{
fileinto :create \"DeltaChat\";
stop;
}}
'';
}};""".format(
request.headers.get("X-User"),
hashedPassword,
request.headers.get("X-Domain"),
)
for line in fileContent:
index += 1
if line.startswith(" #begin"):
print("[DEBUG] Found user configuration snippet match!")
print(
"[INFO] Writing new user configuration snippet to memory...", sep=""
)
fileContent.insert(index - 1, userTemplate)
print("done")
break
print("[INFO] Writing data from memory to file...", sep="")
readWriteFileDescriptor = open("/etc/nixos/users.nix", "w")
userConfigurationWriteOperationResult = readWriteFileDescriptor.writelines(
fileContent
)
print("done")
readOnlyFileDescriptor.close()
readWriteFileDescriptor.close()
print(
"[INFO] Opening /etc/nixos/mailserver/system/mailserver.nix.nix for reading...",
sep="",
)
readOnlyFileDescriptor = open("/etc/nixos/mailserver/system/mailserver.nix")
print("done")
fileContent = list()
index = int(0)
while True:
line = readOnlyFileDescriptor.readline()
if not line:
break
else:
fileContent.append(line)
print("[DEBUG] Read line!")
for line in fileContent:
if line.startswith(" loginAccounts = {"):
print("[DEBUG] Found mailuser configuration snippet match!")
print(
"[INFO] Writing new user configuration snippet to memory...", sep=""
)
fileContent.insert(index + 1, mailUserTemplate)
print("done")
break
index += 1
readWriteFileDescriptor = open(
"/etc/nixos/mailserver/system/mailserver.nix", "w"
)
mailUserConfigurationWriteOperationResult = readWriteFileDescriptor.writelines(
fileContent
)
return {
"result": 0,
"descriptor0": userConfigurationWriteOperationResult,
"descriptor1": mailUserConfigurationWriteOperationResult,
} }
)
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return {"result": 0}
def delete(self): def delete(self):
user = subprocess.Popen(["userdel", request.headers.get("X-User")]) with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f:
user.communicate()[0] portalocker.lock(f, portalocker.LOCK_EX)
return user.returncode try:
data = json.load(f)
# Return 400 if username is not provided
if request.headers.get("X-User") is None:
return {"error": "username is required"}, 400
# Return 400 if user does not exist
for user in data["users"]:
if user["username"] == request.headers.get("X-User"):
data["users"].remove(user)
break
else:
return {"error": "User does not exist"}, 400
f.seek(0)
json.dump(data, f, indent=4)
f.truncate()
finally:
portalocker.unlock(f)
return {"result": 0}