19 changed files with 876 additions and 290 deletions
@ -1,3 +1,5 @@ |
|||
{ |
|||
"python.formatting.provider": "black" |
|||
"python.formatting.provider": "black", |
|||
"python.linting.pylintEnabled": true, |
|||
"python.linting.enabled": true |
|||
} |
@ -1,3 +1,3 @@ |
|||
[build-system] |
|||
requires = ["setuptools", "wheel", "portalocker"] |
|||
requires = ["setuptools", "wheel", "portalocker", "flask-swagger", "flask-swagger-ui"] |
|||
build-backend = "setuptools.build_meta" |
@ -1,40 +1,74 @@ |
|||
#!/usr/bin/env python3 |
|||
"""SelfPrivacy server management API""" |
|||
import os |
|||
from flask import Flask, request, jsonify |
|||
from flask_restful import Api |
|||
import os |
|||
from flask_swagger import swagger |
|||
from flask_swagger_ui import get_swaggerui_blueprint |
|||
|
|||
from selfprivacy_api.resources.users import Users |
|||
from selfprivacy_api.resources.users import User, Users |
|||
from selfprivacy_api.resources.common import DecryptDisk |
|||
from selfprivacy_api.resources.system import api_system |
|||
from selfprivacy_api.resources.services import services as api_services |
|||
|
|||
swagger_blueprint = get_swaggerui_blueprint( |
|||
"/api/docs", "/api/swagger.json", config={"app_name": "SelfPrivacy API"} |
|||
) |
|||
|
|||
|
|||
def create_app(): |
|||
"""Initiate Flask app and bind routes""" |
|||
app = Flask(__name__) |
|||
api = Api(app) |
|||
|
|||
app.config['AUTH_TOKEN'] = os.environ.get('AUTH_TOKEN') |
|||
app.config["AUTH_TOKEN"] = os.environ.get("AUTH_TOKEN") |
|||
app.config["ENABLE_SWAGGER"] = os.environ.get("ENABLE_SWAGGER", "0") |
|||
|
|||
# Check bearer token |
|||
@app.before_request |
|||
def check_auth(): |
|||
auth = request.headers.get("Authorization") |
|||
if auth is None: |
|||
return jsonify({"error": "Missing Authorization header"}), 401 |
|||
|
|||
# Check if token is valid |
|||
if auth != "Bearer " + app.config['AUTH_TOKEN']: |
|||
return jsonify({"error": "Invalid token"}), 401 |
|||
|
|||
# Exclude swagger-ui |
|||
if not request.path.startswith("/api"): |
|||
auth = request.headers.get("Authorization") |
|||
if auth is None: |
|||
return jsonify({"error": "Missing Authorization header"}), 401 |
|||
|
|||
# Check if token is valid |
|||
if auth != "Bearer " + app.config["AUTH_TOKEN"]: |
|||
return jsonify({"error": "Invalid token"}), 401 |
|||
|
|||
api.add_resource(Users, "/users") |
|||
api.add_resource(User, "/users/<string:username>") |
|||
api.add_resource(DecryptDisk, "/decryptDisk") |
|||
from selfprivacy_api.resources.system import api_system |
|||
from selfprivacy_api.resources.services import services as api_services |
|||
|
|||
app.register_blueprint(api_system) |
|||
app.register_blueprint(api_services) |
|||
|
|||
@app.route("/api/swagger.json") |
|||
def spec(): |
|||
if app.config["ENABLE_SWAGGER"] == "1": |
|||
swag = swagger(app) |
|||
swag["info"]["version"] = "1.0" |
|||
swag["info"]["title"] = "SelfPrivacy API" |
|||
swag["info"]["description"] = "SelfPrivacy API" |
|||
swag["securityDefinitions"] = { |
|||
"bearerAuth": { |
|||
"type": "apiKey", |
|||
"name": "Authorization", |
|||
"in": "header", |
|||
} |
|||
} |
|||
swag["security"] = [{"bearerAuth": []}] |
|||
|
|||
return jsonify(swag) |
|||
return jsonify({}), 404 |
|||
|
|||
if app.config["ENABLE_SWAGGER"] == "1": |
|||
app.register_blueprint(swagger_blueprint, url_prefix="/api/docs") |
|||
|
|||
return app |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
app = create_app() |
|||
app.run(port=5050, debug=False) |
|||
created_app = create_app() |
|||
created_app.run(port=5050, debug=False) |
|||
|
@ -1,20 +1,56 @@ |
|||
#!/usr/bin/env python3 |
|||
from flask import Flask, jsonify, request, json |
|||
from flask_restful import Resource |
|||
"""Unassigned views""" |
|||
import subprocess |
|||
from flask_restful import Resource, reqparse |
|||
|
|||
from selfprivacy_api.utils import get_domain |
|||
|
|||
# Decrypt disk |
|||
class DecryptDisk(Resource): |
|||
"""Decrypt disk""" |
|||
|
|||
def post(self): |
|||
decryptionCommand = """ |
|||
echo -n {0} | cryptsetup luksOpen /dev/sdb decryptedVar""".format( |
|||
request.headers.get("X-Decryption-Key") |
|||
) |
|||
""" |
|||
Decrypt /dev/sdb using cryptsetup luksOpen |
|||
--- |
|||
consumes: |
|||
- application/json |
|||
tags: |
|||
- System |
|||
security: |
|||
- bearerAuth: [] |
|||
parameters: |
|||
- in: body |
|||
name: body |
|||
required: true |
|||
description: Provide a password for decryption |
|||
schema: |
|||
type: object |
|||
required: |
|||
- password |
|||
properties: |
|||
password: |
|||
type: string |
|||
description: Decryption password. |
|||
responses: |
|||
201: |
|||
description: OK |
|||
400: |
|||
description: Bad request |
|||
401: |
|||
description: Unauthorized |
|||
""" |
|||
parser = reqparse.RequestParser(bundle_errors=True) |
|||
parser.add_argument("password", type=str, required=True) |
|||
args = parser.parse_args() |
|||
|
|||
decryption_command = ["cryptsetup", "luksOpen", "/dev/sdb", "decryptedVar"] |
|||
|
|||
# TODO: Check if this works at all |
|||
|
|||
decryptionService = subprocess.Popen( |
|||
decryptionCommand, shell=True, stdout=subprocess.PIPE |
|||
decryption_service = subprocess.Popen( |
|||
decryption_command, |
|||
shell=False, |
|||
stdin=subprocess.PIPE, |
|||
stdout=subprocess.PIPE, |
|||
) |
|||
decryptionService.communicate() |
|||
return {"status": decryptionService.returncode} |
|||
decryption_service.communicate(input=args["password"]) |
|||
return {"status": decryption_service.returncode}, 201 |
|||
|
@ -1,88 +1,181 @@ |
|||
#!/usr/bin/env python3 |
|||
from flask import Blueprint, jsonify, request |
|||
from flask_restful import Resource, Api |
|||
"""Users management module""" |
|||
import subprocess |
|||
import portalocker |
|||
import json |
|||
import re |
|||
import portalocker |
|||
from flask_restful import Resource, reqparse |
|||
|
|||
from selfprivacy_api import resources |
|||
|
|||
api_users = Blueprint("api_users", __name__) |
|||
api = Api(api_users) |
|||
|
|||
# Create a new user |
|||
class Users(Resource): |
|||
"""Users management""" |
|||
|
|||
def get(self): |
|||
""" |
|||
Get a list of users |
|||
--- |
|||
tags: |
|||
- Users |
|||
security: |
|||
- bearerAuth: [] |
|||
responses: |
|||
200: |
|||
description: A list of users |
|||
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) |
|||
return users |
|||
|
|||
def post(self): |
|||
rawPassword = request.headers.get("X-Password") |
|||
hashingCommand = """ |
|||
mkpasswd -m sha-512 {0} |
|||
""".format( |
|||
rawPassword |
|||
) |
|||
passwordHashProcessDescriptor = subprocess.Popen( |
|||
hashingCommand, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT |
|||
""" |
|||
Create a new user |
|||
--- |
|||
consumes: |
|||
- application/json |
|||
tags: |
|||
- Users |
|||
security: |
|||
- bearerAuth: [] |
|||
parameters: |
|||
- in: body |
|||
name: user |
|||
required: true |
|||
description: User to create |
|||
schema: |
|||
type: object |
|||
required: |
|||
- username |
|||
- password |
|||
properties: |
|||
username: |
|||
type: string |
|||
description: Unix username. Must be alphanumeric and less than 32 characters |
|||
password: |
|||
type: string |
|||
description: Unix password. |
|||
responses: |
|||
201: |
|||
description: Created user |
|||
400: |
|||
description: Bad request |
|||
401: |
|||
description: Unauthorized |
|||
409: |
|||
description: User already exists |
|||
""" |
|||
parser = reqparse.RequestParser(bundle_errors=True) |
|||
parser.add_argument("username", type=str, required=True) |
|||
parser.add_argument("password", type=str, required=True) |
|||
args = parser.parse_args() |
|||
|
|||
hashing_command = ["mkpasswd", "-m", "sha-512", args["password"]] |
|||
password_hash_process_descriptor = subprocess.Popen( |
|||
hashing_command, |
|||
shell=False, |
|||
stdout=subprocess.PIPE, |
|||
stderr=subprocess.STDOUT, |
|||
) |
|||
hashedPassword = passwordHashProcessDescriptor.communicate()[0] |
|||
hashedPassword = hashedPassword.decode("ascii") |
|||
hashedPassword = hashedPassword.rstrip() |
|||
hashed_password = password_hash_process_descriptor.communicate()[0] |
|||
hashed_password = hashed_password.decode("ascii") |
|||
hashed_password = hashed_password.rstrip() |
|||
|
|||
# Check is username passes regex |
|||
if not re.match(r"^[a-z_][a-z0-9_]+$", args["username"]): |
|||
return {"error": "username must be alphanumeric"}, 400 |
|||
# Check if username less than 32 characters |
|||
if len(args["username"]) > 32: |
|||
return {"error": "username must be less than 32 characters"}, 400 |
|||
|
|||
with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f: |
|||
portalocker.lock(f, portalocker.LOCK_EX) |
|||
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(f) |
|||
# Return 400 if username is not provided |
|||
if request.headers.get("X-User") is None: |
|||
return {"error": "username is required"}, 400 |
|||
# Return 400 if password is not provided |
|||
if request.headers.get("X-Password") is None: |
|||
return {"error": "password is required"}, 400 |
|||
# Check is username passes regex |
|||
if not re.match(r"^[a-z_][a-z0-9_]+$", request.headers.get("X-User")): |
|||
return {"error": "username must be alphanumeric"}, 400 |
|||
# Check if username less than 32 characters |
|||
if len(request.headers.get("X-User")) > 32: |
|||
return {"error": "username must be less than 32 characters"}, 400 |
|||
data = json.load(userdata_file) |
|||
|
|||
# Return 400 if user already exists |
|||
for user in data["users"]: |
|||
if user["username"] == request.headers.get("X-User"): |
|||
return {"error": "User already exists"}, 400 |
|||
if user["username"] == args["username"]: |
|||
return {"error": "User already exists"}, 409 |
|||
|
|||
if "users" not in data: |
|||
data["users"] = [] |
|||
data["users"].append( |
|||
{ |
|||
"username": request.headers.get("X-User"), |
|||
"hashedPassword": hashedPassword, |
|||
"username": args["username"], |
|||
"hashedPassword": hashed_password, |
|||
} |
|||
) |
|||
f.seek(0) |
|||
json.dump(data, f, indent=4) |
|||
f.truncate() |
|||
userdata_file.seek(0) |
|||
json.dump(data, userdata_file, indent=4) |
|||
userdata_file.truncate() |
|||
finally: |
|||
portalocker.unlock(f) |
|||
portalocker.unlock(userdata_file) |
|||
|
|||
return {"result": 0, "username": args["username"]}, 201 |
|||
|
|||
|
|||
return {"result": 0} |
|||
class User(Resource): |
|||
"""Single user managment""" |
|||
|
|||
def delete(self): |
|||
with open("/etc/nixos/userdata/userdata.json", "r+", encoding="utf8") as f: |
|||
portalocker.lock(f, portalocker.LOCK_EX) |
|||
def delete(self, username): |
|||
""" |
|||
Delete a user |
|||
--- |
|||
tags: |
|||
- Users |
|||
security: |
|||
- bearerAuth: [] |
|||
parameters: |
|||
- in: path |
|||
name: username |
|||
required: true |
|||
description: User to delete |
|||
type: string |
|||
responses: |
|||
200: |
|||
description: Deleted user |
|||
400: |
|||
description: Bad request |
|||