Move to fastapi

pull/13/head
Inex Code 2022-08-11 03:36:36 +04:00
parent 9132b70e70
commit dfd28ad0cd
85 changed files with 3546 additions and 3612 deletions

2
.gitignore vendored
View File

@ -145,3 +145,5 @@ dmypy.json
cython_debug/
# End of https://www.toptal.com/developers/gitignore/api/flask
*.db

View File

@ -1,17 +0,0 @@
wheel
flask
flask_restful
flask_socketio
setuptools
portalocker
flask-swagger
flask-swagger-ui
pytz
huey
gevent
mnemonic
pytest
coverage
pytest-mock
pytest-datadir

View File

@ -0,0 +1,123 @@
"""App tokens actions"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
from selfprivacy_api.utils.auth import (
delete_token,
generate_recovery_token,
get_recovery_token_status,
get_tokens_info,
is_recovery_token_exists,
is_recovery_token_valid,
is_token_name_exists,
is_token_name_pair_valid,
refresh_token,
get_token_name,
)
class TokenInfoWithIsCaller(BaseModel):
"""Token info"""
name: str
date: datetime
is_caller: bool
def get_api_tokens_with_caller_flag(caller_token: str) -> list[TokenInfoWithIsCaller]:
"""Get the tokens info"""
caller_name = get_token_name(caller_token)
tokens = get_tokens_info()
return [
TokenInfoWithIsCaller(
name=token.name,
date=token.date,
is_caller=token.name == caller_name,
)
for token in tokens
]
class NotFoundException(Exception):
"""Not found exception"""
pass
class CannotDeleteCallerException(Exception):
"""Cannot delete caller exception"""
pass
def delete_api_token(caller_token: str, token_name: str) -> None:
"""Delete the token"""
if is_token_name_pair_valid(token_name, caller_token):
raise CannotDeleteCallerException("Cannot delete caller's token")
if not is_token_name_exists(token_name):
raise NotFoundException("Token not found")
delete_token(token_name)
def refresh_api_token(caller_token: str) -> str:
"""Refresh the token"""
new_token = refresh_token(caller_token)
if new_token is None:
raise NotFoundException("Token not found")
return new_token
class RecoveryTokenStatus(BaseModel):
"""Recovery token status"""
exists: bool
valid: bool
date: Optional[datetime] = None
expiration: Optional[datetime] = None
uses_left: Optional[int] = None
def get_api_recovery_token_status() -> RecoveryTokenStatus:
"""Get the recovery token status"""
if not is_recovery_token_exists():
return RecoveryTokenStatus(exists=False, valid=False)
status = get_recovery_token_status()
if status is None:
return RecoveryTokenStatus(exists=False, valid=False)
is_valid = is_recovery_token_valid()
return RecoveryTokenStatus(
exists=True,
valid=is_valid,
date=status["date"],
expiration=status["expiration"],
uses_left=status["uses_left"],
)
class InvalidExpirationDate(Exception):
"""Invalid expiration date exception"""
pass
class InvalidUsesLeft(Exception):
"""Invalid uses left exception"""
pass
def get_new_api_recovery_key(
expiration_date: Optional[datetime] = None, uses_left: Optional[int] = None
) -> str:
"""Get new recovery key"""
if expiration_date is not None:
current_time = datetime.now().timestamp()
if expiration_date.timestamp() < current_time:
raise InvalidExpirationDate("Expiration date is in the past")
if uses_left is not None:
if uses_left <= 0:
raise InvalidUsesLeft("Uses must be greater than 0")
key = generate_recovery_token(expiration_date, uses_left)
return key

View File

@ -0,0 +1,149 @@
"""Actions to manage the SSH."""
from typing import Optional
from pydantic import BaseModel
from selfprivacy_api.actions.users import (
UserNotFound,
ensure_ssh_and_users_fields_exist,
)
from selfprivacy_api.utils import WriteUserData, ReadUserData, validate_ssh_public_key
def enable_ssh():
with WriteUserData() as data:
if "ssh" not in data:
data["ssh"] = {}
data["ssh"]["enable"] = True
class UserdataSshSettings(BaseModel):
"""Settings for the SSH."""
enable: bool = True
passwordAuthentication: bool = True
rootKeys: list[str] = []
def get_ssh_settings() -> UserdataSshSettings:
with ReadUserData() as data:
if "ssh" not in data:
return UserdataSshSettings()
if "enable" not in data["ssh"]:
data["ssh"]["enable"] = True
if "passwordAuthentication" not in data["ssh"]:
data["ssh"]["passwordAuthentication"] = True
if "rootKeys" not in data["ssh"]:
data["ssh"]["rootKeys"] = []
return UserdataSshSettings(**data["ssh"])
def set_ssh_settings(
enable: Optional[bool] = None, password_authentication: Optional[bool] = None
) -> None:
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
def add_root_ssh_key(public_key: str):
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:
raise KeyAlreadyExists()
data["ssh"]["rootKeys"].append(public_key)
class KeyAlreadyExists(Exception):
"""Key already exists"""
pass
class InvalidPublicKey(Exception):
"""Invalid public key"""
pass
def create_ssh_key(username: str, ssh_key: str):
"""Create a new ssh key"""
if not validate_ssh_public_key(ssh_key):
raise InvalidPublicKey()
with WriteUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == data["username"]:
if ssh_key in data["sshKeys"]:
raise KeyAlreadyExists()
data["sshKeys"].append(ssh_key)
return
if username == "root":
if ssh_key in data["ssh"]["rootKeys"]:
raise KeyAlreadyExists()
data["ssh"]["rootKeys"].append(ssh_key)
return
for user in data["users"]:
if user["username"] == username:
if "sshKeys" not in user:
user["sshKeys"] = []
if ssh_key in user["sshKeys"]:
raise KeyAlreadyExists()
user["sshKeys"].append(ssh_key)
return
raise UserNotFound()
class KeyNotFound(Exception):
"""Key not found"""
pass
def remove_ssh_key(username: str, ssh_key: str):
"""Delete a ssh key"""
with WriteUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == "root":
if ssh_key in data["ssh"]["rootKeys"]:
data["ssh"]["rootKeys"].remove(ssh_key)
return
raise KeyNotFound()
if username == data["username"]:
if ssh_key in data["sshKeys"]:
data["sshKeys"].remove(ssh_key)
return
raise KeyNotFound()
for user in data["users"]:
if user["username"] == username:
if "sshKeys" not in user:
user["sshKeys"] = []
if ssh_key in user["sshKeys"]:
user["sshKeys"].remove(ssh_key)
return
raise KeyNotFound()
raise UserNotFound()

View File

@ -0,0 +1,139 @@
"""Actions to manage the system."""
import os
import subprocess
import pytz
from typing import Optional
from pydantic import BaseModel
from selfprivacy_api.utils import WriteUserData, ReadUserData
def get_timezone() -> str:
"""Get the timezone of the server"""
with ReadUserData() as user_data:
if "timezone" in user_data:
return user_data["timezone"]
return "Europe/Uzhgorod"
class InvalidTimezone(Exception):
"""Invalid timezone"""
pass
def change_timezone(timezone: str) -> None:
"""Change the timezone of the server"""
if timezone not in pytz.all_timezones:
raise InvalidTimezone(f"Invalid timezone: {timezone}")
with WriteUserData() as user_data:
user_data["timezone"] = timezone
class UserDataAutoUpgradeSettings(BaseModel):
"""Settings for auto-upgrading user data"""
enable: bool = True
allowReboot: bool = False
def get_auto_upgrade_settings() -> UserDataAutoUpgradeSettings:
"""Get the auto-upgrade settings"""
with ReadUserData() as user_data:
if "autoUpgrade" in user_data:
return UserDataAutoUpgradeSettings(**user_data["autoUpgrade"])
return UserDataAutoUpgradeSettings()
def set_auto_upgrade_settings(
enalbe: Optional[bool] = None, allowReboot: Optional[bool] = None
) -> None:
"""Set the auto-upgrade settings"""
with WriteUserData() as user_data:
if "autoUpgrade" not in user_data:
user_data["autoUpgrade"] = {}
if enalbe is not None:
user_data["autoUpgrade"]["enable"] = enalbe
if allowReboot is not None:
user_data["autoUpgrade"]["allowReboot"] = allowReboot
def rebuild_system() -> int:
"""Rebuild the system"""
rebuild_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-rebuild.service"], start_new_session=True
)
rebuild_result.communicate()[0]
return rebuild_result.returncode
def rollback_system() -> int:
"""Rollback the system"""
rollback_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-rollback.service"], start_new_session=True
)
rollback_result.communicate()[0]
return rollback_result.returncode
def upgrade_system() -> int:
"""Upgrade the system"""
upgrade_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-upgrade.service"], start_new_session=True
)
upgrade_result.communicate()[0]
return upgrade_result.returncode
def reboot_system() -> None:
"""Reboot the system"""
subprocess.Popen(["reboot"], start_new_session=True)
def get_system_version() -> str:
"""Get system version"""
return subprocess.check_output(["uname", "-a"]).decode("utf-8").strip()
def get_python_version() -> str:
"""Get Python version"""
return subprocess.check_output(["python", "-V"]).decode("utf-8").strip()
class SystemActionResult(BaseModel):
"""System action result"""
status: int
message: str
data: str
def pull_repository_changes() -> SystemActionResult:
"""Pull repository changes"""
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,
)
data = git_pull_process_descriptor.communicate()[0].decode("utf-8")
os.chdir(current_working_directory)
if git_pull_process_descriptor.returncode == 0:
return SystemActionResult(
status=0,
message="Pulled repository changes",
data=data,
)
return SystemActionResult(
status=git_pull_process_descriptor.returncode,
message="Failed to pull repository changes",
data=data,
)

View File

@ -0,0 +1,219 @@
"""Actions to manage the users."""
import re
from typing import Optional
from pydantic import BaseModel
from enum import Enum
from selfprivacy_api.utils import (
ReadUserData,
WriteUserData,
hash_password,
is_username_forbidden,
)
class UserDataUserOrigin(Enum):
"""Origin of the user in the user data"""
NORMAL = "NORMAL"
PRIMARY = "PRIMARY"
ROOT = "ROOT"
class UserDataUser(BaseModel):
"""The user model from the userdata file"""
username: str
ssh_keys: list[str]
origin: UserDataUserOrigin
def ensure_ssh_and_users_fields_exist(data):
if "ssh" not in data:
data["ssh"] = {}
data["ssh"]["rootKeys"] = []
elif data["ssh"].get("rootKeys") is None:
data["ssh"]["rootKeys"] = []
if "sshKeys" not in data:
data["sshKeys"] = []
if "users" not in data:
data["users"] = []
def get_users(
exclude_primary: bool = False,
exclude_root: bool = False,
) -> list[UserDataUser]:
"""Get the list of users"""
users = []
with ReadUserData() as user_data:
ensure_ssh_and_users_fields_exist(user_data)
users = [
UserDataUser(
username=user["username"],
ssh_keys=user.get("sshKeys", []),
origin=UserDataUserOrigin.NORMAL,
)
for user in user_data["users"]
]
if not exclude_primary:
users.append(
UserDataUser(
username=user_data["username"],
ssh_keys=user_data["sshKeys"],
origin=UserDataUserOrigin.PRIMARY,
)
)
if not exclude_root:
users.append(
UserDataUser(
username="root",
ssh_keys=user_data["ssh"]["rootKeys"],
origin=UserDataUserOrigin.ROOT,
)
)
return users
class UsernameForbidden(Exception):
"""Attemted to create a user with a forbidden username"""
pass
class UserAlreadyExists(Exception):
"""Attemted to create a user that already exists"""
pass
class UsernameNotAlphanumeric(Exception):
"""Attemted to create a user with a non-alphanumeric username"""
pass
class UsernameTooLong(Exception):
"""Attemted to create a user with a too long username. Username must be less than 32 characters"""
pass
class PasswordIsEmpty(Exception):
"""Attemted to create a user with an empty password"""
pass
def create_user(username: str, password: str):
if password == "":
raise PasswordIsEmpty("Password is empty")
if is_username_forbidden(username):
raise UsernameForbidden("Username is forbidden")
if not re.match(r"^[a-z_][a-z0-9_]+$", username):
raise UsernameNotAlphanumeric(
"Username must be alphanumeric and start with a letter"
)
if len(username) >= 32:
raise UsernameTooLong("Username must be less than 32 characters")
with ReadUserData() as user_data:
ensure_ssh_and_users_fields_exist(user_data)
if username == user_data["username"]:
raise UserAlreadyExists("User already exists")
if username in [user["username"] for user in user_data["users"]]:
raise UserAlreadyExists("User already exists")
hashed_password = hash_password(password)
with WriteUserData() as user_data:
ensure_ssh_and_users_fields_exist(user_data)
user_data["users"].append(
{"username": username, "sshKeys": [], "hashedPassword": hashed_password}
)
class UserNotFound(Exception):
"""Attemted to get a user that does not exist"""
pass
class UserIsProtected(Exception):
"""Attemted to delete a user that is protected"""
pass
def delete_user(username: str):
with WriteUserData() as user_data:
ensure_ssh_and_users_fields_exist(user_data)
if username == user_data["username"] or username == "root":
raise UserIsProtected("Cannot delete main or root user")
for data_user in user_data["users"]:
if data_user["username"] == username:
user_data["users"].remove(data_user)
break
else:
raise UserNotFound("User did not exist")
def update_user(username: str, password: str):
if password == "":
raise PasswordIsEmpty("Password is empty")
hashed_password = hash_password(password)
with WriteUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == data["username"]:
data["hashedMasterPassword"] = hashed_password
# Return 404 if user does not exist
else:
for data_user in data["users"]:
if data_user["username"] == username:
data_user["hashedPassword"] = hashed_password
break
else:
raise UserNotFound("User does not exist")
def get_user_by_username(username: str) -> Optional[UserDataUser]:
with ReadUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == "root":
return UserDataUser(
origin=UserDataUserOrigin.ROOT,
username="root",
ssh_keys=data["ssh"]["rootKeys"],
)
if username == data["username"]:
return UserDataUser(
origin=UserDataUserOrigin.PRIMARY,
username=username,
ssh_keys=data["sshKeys"],
)
for user in data["users"]:
if user["username"] == username:
if "sshKeys" not in user:
user["sshKeys"] = []
return UserDataUser(
origin=UserDataUserOrigin.NORMAL,
username=username,
ssh_keys=user["sshKeys"],
)
return None

View File

@ -1,111 +1,51 @@
#!/usr/bin/env python3
"""SelfPrivacy server management API"""
import os
from gevent import monkey
from fastapi import FastAPI, Depends, Request, WebSocket, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from strawberry.fastapi import BaseContext, GraphQLRouter
from flask import Flask, request, jsonify
from flask_restful import Api
from flask_swagger import swagger
from flask_swagger_ui import get_swaggerui_blueprint
from flask_cors import CORS
from strawberry.flask.views import AsyncGraphQLView
from selfprivacy_api.resources.users import User, Users
from selfprivacy_api.resources.common import ApiVersion
from selfprivacy_api.resources.system import api_system
from selfprivacy_api.resources.services import services as api_services
from selfprivacy_api.resources.api_auth import auth as api_auth
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.dependencies import get_api_version, get_graphql_context
from selfprivacy_api.graphql.schema import schema
from selfprivacy_api.migrations import run_migrations
from selfprivacy_api.restic_controller.tasks import init_restic
from selfprivacy_api.migrations import run_migrations
from selfprivacy_api.rest import (
system,
users,
api_auth,
services,
)
from selfprivacy_api.utils.auth import is_token_valid
app = FastAPI()
graphql_app = GraphQLRouter(
schema,
context_getter=get_graphql_context,
)
from selfprivacy_api.graphql.schema import schema
swagger_blueprint = get_swaggerui_blueprint(
"/api/docs", "/api/swagger.json", config={"app_name": "SelfPrivacy API"}
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def create_app(test_config=None):
"""Initiate Flask app and bind routes"""
app = Flask(__name__)
api = Api(app)
CORS(app)
if test_config is None:
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)
# Check bearer token
@app.before_request
def check_auth():
# Exclude swagger-ui, /auth/new_device/authorize, /auth/recovery_token/use
if request.path.startswith("/api"):
pass
elif request.path.startswith("/auth/new_device/authorize"):
pass
elif request.path.startswith("/auth/recovery_token/use"):
pass
elif request.path.startswith("/graphql"):
pass
else:
auth = request.headers.get("Authorization")
if auth is None:
return jsonify({"error": "Missing Authorization header"}), 401
# Strip Bearer from auth header
auth = auth.replace("Bearer ", "")
if not is_token_valid(auth):
return jsonify({"error": "Invalid token"}), 401
api.add_resource(ApiVersion, "/api/version")
api.add_resource(Users, "/users")
api.add_resource(User, "/users/<string:username>")
app.register_blueprint(api_system)
app.register_blueprint(api_services)
app.register_blueprint(api_auth)
@app.route("/api/swagger.json")
def spec():
if app.config["ENABLE_SWAGGER"] == "1":
swag = swagger(app)
swag["info"]["version"] = "1.2.7"
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
app.add_url_rule(
"/graphql", view_func=AsyncGraphQLView.as_view("graphql", schema=schema)
)
if app.config["ENABLE_SWAGGER"] == "1":
app.register_blueprint(swagger_blueprint, url_prefix="/api/docs")
return app
app.include_router(system.router)
app.include_router(users.router)
app.include_router(api_auth.router)
app.include_router(services.router)
app.include_router(graphql_app, prefix="/graphql")
if __name__ == "__main__":
monkey.patch_all()
created_app = create_app()
@app.get("/api/version")
async def get_version():
"""Get the version of the server"""
return {"version": get_api_version()}
@app.on_event("startup")
async def startup():
run_migrations()
huey.start()
init_restic()
created_app.run(port=5050, debug=False)

View File

@ -0,0 +1,55 @@
from fastapi import Depends, FastAPI, HTTPException, status
from typing import Optional
from strawberry.fastapi import BaseContext
from fastapi.security import APIKeyHeader
from pydantic import BaseModel
from selfprivacy_api.utils.auth import is_token_valid
class TokenHeader(BaseModel):
token: str
async def get_token_header(
token: str = Depends(APIKeyHeader(name="Authorization", auto_error=False))
) -> TokenHeader:
if token is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token not provided"
)
else:
token = token.replace("Bearer ", "")
if not is_token_valid(token):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
)
return TokenHeader(token=token)
class GraphQlContext(BaseContext):
def __init__(self, auth_token: Optional[str] = None):
self.auth_token = auth_token
self.is_authenticated = auth_token is not None
async def get_graphql_context(
token: str = Depends(
APIKeyHeader(
name="Authorization",
auto_error=False,
)
)
) -> GraphQlContext:
if token is None:
return GraphQlContext()
else:
token = token.replace("Bearer ", "")
if not is_token_valid(token):
return GraphQlContext()
return GraphQlContext(auth_token=token)
def get_api_version() -> str:
"""Get API version"""
return "2.0.0"

View File

@ -3,7 +3,6 @@
import typing
from strawberry.permission import BasePermission
from strawberry.types import Info
from flask import request
from selfprivacy_api.utils.auth import is_token_valid
@ -14,11 +13,4 @@ class IsAuthenticated(BasePermission):
message = "You must be authenticated to access this resource."
def has_permission(self, source: typing.Any, info: Info, **kwargs) -> bool:
auth = request.headers.get("Authorization")
if auth is None:
return False
# Strip Bearer from auth header
auth = auth.replace("Bearer ", "")
if not is_token_valid(auth):
return False
return True
return info.context.is_authenticated

View File

@ -1,8 +1,8 @@
import typing
from enum import Enum
import strawberry
import selfprivacy_api.actions.users as users_actions
from selfprivacy_api.utils import ReadUserData
from selfprivacy_api.graphql.mutations.mutation_interface import (
MutationReturnInterface,
)
@ -28,51 +28,30 @@ class User:
class UserMutationReturn(MutationReturnInterface):
"""Return type for user mutation"""
user: typing.Optional[User]
def ensure_ssh_and_users_fields_exist(data):
if "ssh" not in data:
data["ssh"] = []
data["ssh"]["rootKeys"] = []
elif data["ssh"].get("rootKeys") is None:
data["ssh"]["rootKeys"] = []
if "sshKeys" not in data:
data["sshKeys"] = []
if "users" not in data:
data["users"] = []
user: typing.Optional[User] = None
def get_user_by_username(username: str) -> typing.Optional[User]:
with ReadUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == "root":
return User(
user_type=UserType.ROOT,
username="root",
ssh_keys=data["ssh"]["rootKeys"],
)
if username == data["username"]:
return User(
user_type=UserType.PRIMARY,
username=username,
ssh_keys=data["sshKeys"],
)
for user in data["users"]:
if user["username"] == username:
if "sshKeys" not in user:
user["sshKeys"] = []
return User(
user_type=UserType.NORMAL,
username=username,
ssh_keys=user["sshKeys"],
)
user = users_actions.get_user_by_username(username)
if user is None:
return None
return User(
user_type=UserType(user.origin.value),
username=user.username,
ssh_keys=user.ssh_keys,
)
def get_users() -> typing.List[User]:
"""Get users"""
users = users_actions.get_users(exclude_root=True)
return [
User(
user_type=UserType(user.origin.value),
username=user.username,
ssh_keys=user.ssh_keys,
)
for user in users
]

View File

@ -2,8 +2,16 @@
# pylint: disable=too-few-public-methods
import datetime
import typing
from flask import request
import strawberry
from strawberry.types import Info
from selfprivacy_api.actions.api_tokens import (
CannotDeleteCallerException,
InvalidExpirationDate,
InvalidUsesLeft,
NotFoundException,
delete_api_token,
get_new_api_recovery_key,
)
from selfprivacy_api.graphql import IsAuthenticated
from selfprivacy_api.graphql.mutations.mutation_interface import (
GenericMutationReturn,
@ -12,11 +20,7 @@ from selfprivacy_api.graphql.mutations.mutation_interface import (
from selfprivacy_api.utils.auth import (
delete_new_device_auth_token,
delete_token,
generate_recovery_token,
get_new_device_auth_token,
is_token_name_exists,
is_token_name_pair_valid,
refresh_token,
use_mnemonic_recoverery_token,
use_new_device_auth_token,
@ -64,27 +68,24 @@ class ApiMutations:
self, limits: typing.Optional[RecoveryKeyLimitsInput] = None
) -> ApiKeyMutationReturn:
"""Generate recovery key"""
if limits is not None:
if limits.expiration_date is not None:
if limits.expiration_date < datetime.datetime.now():
return ApiKeyMutationReturn(
success=False,
message="Expiration date must be in the future",
code=400,
key=None,
)
if limits.uses is not None:
if limits.uses < 1:
return ApiKeyMutationReturn(
success=False,
message="Uses must be greater than 0",
code=400,
key=None,
)
if limits is not None:
key = generate_recovery_token(limits.expiration_date, limits.uses)
else:
key = generate_recovery_token(None, None)
if limits is None:
limits = RecoveryKeyLimitsInput()
try:
key = get_new_api_recovery_key(limits.expiration_date, limits.uses)
except InvalidExpirationDate:
return ApiKeyMutationReturn(
success=False,
message="Expiration date must be in the future",
code=400,
key=None,
)
except InvalidUsesLeft:
return ApiKeyMutationReturn(
success=False,
message="Uses must be greater than 0",
code=400,
key=None,
)
return ApiKeyMutationReturn(
success=True,
message="Recovery key generated",
@ -113,13 +114,9 @@ class ApiMutations:
)
@strawberry.mutation(permission_classes=[IsAuthenticated])
def refresh_device_api_token(self) -> DeviceApiTokenMutationReturn:
def refresh_device_api_token(self, info: Info) -> DeviceApiTokenMutationReturn:
"""Refresh device api token"""
token = (
request.headers.get("Authorization").split(" ")[1]
if request.headers.get("Authorization") is not None
else None
)
token = info.context.auth_token
if token is None:
return DeviceApiTokenMutationReturn(
success=False,
@ -143,26 +140,29 @@ class ApiMutations:
)
@strawberry.mutation(permission_classes=[IsAuthenticated])
def delete_device_api_token(self, device: str) -> GenericMutationReturn:
def delete_device_api_token(self, device: str, info: Info) -> GenericMutationReturn:
"""Delete device api token"""
self_token = (
request.headers.get("Authorization").split(" ")[1]
if request.headers.get("Authorization") is not None
else None
)
if self_token is not None and is_token_name_pair_valid(device, self_token):
return GenericMutationReturn(
success=False,
message="Cannot delete caller's token",
code=400,
)
if not is_token_name_exists(device):
self_token = info.context.auth_token
try:
delete_api_token(self_token, device)
except NotFoundException:
return GenericMutationReturn(
success=False,
message="Token not found",
code=404,
)
delete_token(device)
except CannotDeleteCallerException:
return GenericMutationReturn(
success=False,
message="Cannot delete caller token",
code=400,
)
except Exception as e:
return GenericMutationReturn(
success=False,
message=str(e),
code=500,
)
return GenericMutationReturn(
success=True,
message="Token deleted",

View File

@ -3,9 +3,13 @@
# pylint: disable=too-few-public-methods
import strawberry
from selfprivacy_api.actions.users import UserNotFound
from selfprivacy_api.graphql import IsAuthenticated
from selfprivacy_api.graphql.mutations.ssh_utils import (
from selfprivacy_api.actions.ssh import (
InvalidPublicKey,
KeyAlreadyExists,
KeyNotFound,
create_ssh_key,
remove_ssh_key,
)
@ -31,12 +35,37 @@ class SshMutations:
def add_ssh_key(self, ssh_input: SshMutationInput) -> UserMutationReturn:
"""Add a new ssh key"""
success, message, code = create_ssh_key(ssh_input.username, ssh_input.ssh_key)
try:
create_ssh_key(ssh_input.username, ssh_input.ssh_key)
except KeyAlreadyExists:
return UserMutationReturn(
success=False,
message="Key already exists",
code=409,
)
except InvalidPublicKey:
return UserMutationReturn(
success=False,
message="Invalid key type. Only ssh-ed25519 and ssh-rsa are supported",
code=400,
)
except UserNotFound:
return UserMutationReturn(
success=False,
message="User not found",
code=404,
)
except Exception as e:
return UserMutationReturn(
success=False,
message=str(e),
code=500,
)
return UserMutationReturn(
success=success,
message=message,
code=code,
success=True,
message="New SSH key successfully written",
code=201,
user=get_user_by_username(ssh_input.username),
)
@ -44,11 +73,30 @@ class SshMutations:
def remove_ssh_key(self, ssh_input: SshMutationInput) -> UserMutationReturn:
"""Remove ssh key from user"""
success, message, code = remove_ssh_key(ssh_input.username, ssh_input.ssh_key)
try:
remove_ssh_key(ssh_input.username, ssh_input.ssh_key)
except KeyNotFound:
return UserMutationReturn(
success=False,
message="Key not found",
code=404,
)
except UserNotFound:
return UserMutationReturn(
success=False,
message="User not found",
code=404,
)
except Exception as e:
return UserMutationReturn(
success=False,
message=str(e),
code=500,
)
return UserMutationReturn(
success=success,
message=message,
code=code,
success=True,
message="SSH key successfully removed",
code=200,
user=get_user_by_username(ssh_input.username),
)

View File

@ -1,74 +0,0 @@
from selfprivacy_api.graphql.common_types.user import ensure_ssh_and_users_fields_exist
from selfprivacy_api.utils import (
WriteUserData,
validate_ssh_public_key,
)
def create_ssh_key(username: str, ssh_key: str) -> tuple[bool, str, int]:
"""Create a new ssh key"""
if not validate_ssh_public_key(ssh_key):
return (
False,
"Invalid key type. Only ssh-ed25519 and ssh-rsa are supported",
400,
)
with WriteUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == data["username"]:
if ssh_key in data["sshKeys"]:
return False, "Key already exists", 409
data["sshKeys"].append(ssh_key)
return True, "New SSH key successfully written", 201
if username == "root":
if ssh_key in data["ssh"]["rootKeys"]:
return False, "Key already exists", 409
data["ssh"]["rootKeys"].append(ssh_key)
return True, "New SSH key successfully written", 201
for user in data["users"]:
if user["username"] == username:
if ssh_key in user["sshKeys"]:
return False, "Key already exists", 409
user["sshKeys"].append(ssh_key)
return True, "New SSH key successfully written", 201
return False, "User not found", 404
def remove_ssh_key(username: str, ssh_key: str) -> tuple[bool, str, int]:
"""Delete a ssh key"""
with WriteUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == "root":
if ssh_key in data["ssh"]["rootKeys"]:
data["ssh"]["rootKeys"].remove(ssh_key)
return True, "SSH key deleted", 200
return False, "Key not found", 404
if username == data["username"]:
if ssh_key in data["sshKeys"]:
data["sshKeys"].remove(ssh_key)
return True, "SSH key deleted", 200
return False, "Key not found", 404
for user in data["users"]:
if user["username"] == username:
if ssh_key in user["sshKeys"]:
user["sshKeys"].remove(ssh_key)
return True, "SSH key deleted", 200
return False, "Key not found", 404
return False, "User not found", 404

View File

@ -1,5 +1,4 @@
"""Storage devices mutations"""
import typing
import strawberry
from selfprivacy_api.graphql import IsAuthenticated
from selfprivacy_api.utils.block_devices import BlockDevices

View File

@ -1,15 +1,14 @@
"""System management mutations"""
# pylint: disable=too-few-public-methods
import subprocess
import typing
import pytz
import strawberry
from selfprivacy_api.graphql import IsAuthenticated
from selfprivacy_api.graphql.mutations.mutation_interface import (
GenericMutationReturn,
MutationReturnInterface,
)
from selfprivacy_api.utils import WriteUserData
import selfprivacy_api.actions.system as system_actions
@strawberry.type
@ -42,15 +41,15 @@ class SystemMutations:
@strawberry.mutation(permission_classes=[IsAuthenticated])
def change_timezone(self, timezone: str) -> TimezoneMutationReturn:
"""Change the timezone of the server. Timezone is a tzdatabase name."""
if timezone not in pytz.all_timezones:
try:
system_actions.change_timezone(timezone)
except system_actions.InvalidTimezone as e:
return TimezoneMutationReturn(
success=False,
message="Invalid timezone",
message=str(e),
code=400,
timezone=None,
)
with WriteUserData() as data:
data["timezone"] = timezone
return TimezoneMutationReturn(
success=True,
message="Timezone changed",
@ -63,36 +62,23 @@ class SystemMutations:
self, settings: AutoUpgradeSettingsInput
) -> AutoUpgradeSettingsMutationReturn:
"""Change auto upgrade settings of the server."""
with WriteUserData() as data:
if "autoUpgrade" not in data:
data["autoUpgrade"] = {}
if "enable" not in data["autoUpgrade"]:
data["autoUpgrade"]["enable"] = True
if "allowReboot" not in data["autoUpgrade"]:
data["autoUpgrade"]["allowReboot"] = False
system_actions.set_auto_upgrade_settings(
settings.enableAutoUpgrade, settings.allowReboot
)
if settings.enableAutoUpgrade is not None:
data["autoUpgrade"]["enable"] = settings.enableAutoUpgrade
if settings.allowReboot is not None:
data["autoUpgrade"]["allowReboot"] = settings.allowReboot
auto_upgrade = data["autoUpgrade"]["enable"]
allow_reboot = data["autoUpgrade"]["allowReboot"]
new_settings = system_actions.get_auto_upgrade_settings()
return AutoUpgradeSettingsMutationReturn(
success=True,
message="Auto-upgrade settings changed",
code=200,
enableAutoUpgrade=auto_upgrade,
allowReboot=allow_reboot,
enableAutoUpgrade=new_settings.enable,
allowReboot=new_settings.allowReboot,
)
@strawberry.mutation(permission_classes=[IsAuthenticated])
def run_system_rebuild(self) -> GenericMutationReturn:
rebuild_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-rebuild.service"], start_new_session=True
)
rebuild_result.communicate()[0]
system_actions.rebuild_system()
return GenericMutationReturn(
success=True,
message="Starting rebuild system",
@ -101,10 +87,7 @@ class SystemMutations:
@strawberry.mutation(permission_classes=[IsAuthenticated])
def run_system_rollback(self) -> GenericMutationReturn:
rollback_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-rollback.service"], start_new_session=True
)
rollback_result.communicate()[0]
system_actions.rollback_system()
return GenericMutationReturn(
success=True,
message="Starting rebuild system",
@ -113,10 +96,7 @@ class SystemMutations:
@strawberry.mutation(permission_classes=[IsAuthenticated])
def run_system_upgrade(self) -> GenericMutationReturn:
upgrade_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-upgrade.service"], start_new_session=True
)
upgrade_result.communicate()[0]
system_actions.upgrade_system()
return GenericMutationReturn(
success=True,
message="Starting rebuild system",
@ -125,7 +105,7 @@ class SystemMutations:
@strawberry.mutation(permission_classes=[IsAuthenticated])
def reboot_system(self) -> GenericMutationReturn:
subprocess.Popen(["reboot"], start_new_session=True)
system_actions.reboot_system()
return GenericMutationReturn(
success=True,
message="System reboot has started",

View File

@ -10,11 +10,7 @@ from selfprivacy_api.graphql.common_types.user import (
from selfprivacy_api.graphql.mutations.mutation_interface import (
GenericMutationReturn,
)
from selfprivacy_api.graphql.mutations.users_utils import (
create_user,
delete_user,
update_user,
)
import selfprivacy_api.actions.users as users_actions
@strawberry.input
@ -31,35 +27,91 @@ class UserMutations:
@strawberry.mutation(permission_classes=[IsAuthenticated])
def create_user(self, user: UserMutationInput) -> UserMutationReturn:
success, message, code = create_user(user.username, user.password)
try:
users_actions.create_user(user.username, user.password)
except users_actions.PasswordIsEmpty as e:
return UserMutationReturn(
success=False,
message=str(e),
code=400,
)
except users_actions.UsernameForbidden as e:
return UserMutationReturn(
success=False,
message=str(e),
code=409,
)
except users_actions.UsernameNotAlphanumeric as e:
return UserMutationReturn(
success=False,
message=str(e),
code=400,
)
except users_actions.UsernameTooLong as e:
return UserMutationReturn(
success=False,
message=str(e),
code=400,
)
except users_actions.UserAlreadyExists as e:
return UserMutationReturn(
success=False,
message=str(e),
code=409,
user=get_user_by_username(user.username),
)
return UserMutationReturn(
success=success,
message=message,
code=code,
success=True,
message="User created",
code=201,
user=get_user_by_username(user.username),
)
@strawberry.mutation(permission_classes=[IsAuthenticated])
def delete_user(self, username: str) -> GenericMutationReturn:
success, message, code = delete_user(username)
try:
users_actions.delete_user(username)
except users_actions.UserNotFound as e:
return GenericMutationReturn(
success=False,
message=str(e),
code=404,
)
except users_actions.UserIsProtected as e:
return GenericMutationReturn(
success=False,
message=str(e),
code=400,
)
return GenericMutationReturn(
success=success,
message=message,
code=code,
success=True,
message="User deleted",
code=200,
)
@strawberry.mutation(permission_classes=[IsAuthenticated])
def update_user(self, user: UserMutationInput) -> UserMutationReturn:
"""Update user mutation"""
success, message, code = update_user(user.username, user.password)
try:
users_actions.update_user(user.username, user.password)
except users_actions.PasswordIsEmpty as e:
return UserMutationReturn(
success=False,
message=str(e),
code=400,
)
except users_actions.UserNotFound as e:
return UserMutationReturn(
success=False,
message=str(e),
code=404,
)
return UserMutationReturn(
success=success,
message=message,
code=code,
success=True,
message="User updated",
code=200,
user=get_user_by_username(user.username),
)

View File

@ -1,111 +0,0 @@
import re
from selfprivacy_api.utils import (
WriteUserData,
ReadUserData,
is_username_forbidden,
)
from selfprivacy_api.utils import hash_password
def ensure_ssh_and_users_fields_exist(data):
if "ssh" not in data:
data["ssh"] = []
data["ssh"]["rootKeys"] = []
elif data["ssh"].get("rootKeys") is None:
data["ssh"]["rootKeys"] = []
if "sshKeys" not in data:
data["sshKeys"] = []
if "users" not in data:
data["users"] = []
def create_user(username: str, password: str) -> tuple[bool, str, int]:
"""Create a new user"""
# Check if password is null or none
if password == "":
return False, "Password is null", 400
# Check if username is forbidden
if is_username_forbidden(username):
return False, "Username is forbidden", 409
# Check is username passes regex
if not re.match(r"^[a-z_][a-z0-9_]+$", username):
return False, "Username must be alphanumeric", 400
# Check if username less than 32 characters
if len(username) >= 32:
return False, "Username must be less than 32 characters", 400
with ReadUserData() as data:
ensure_ssh_and_users_fields_exist(data)
# Return 409 if user already exists
if data["username"] == username:
return False, "User already exists", 409
for data_user in data["users"]:
if data_user["username"] == username:
return False, "User already exists", 409
hashed_password = hash_password(password)
with WriteUserData() as data:
ensure_ssh_and_users_fields_exist(data)
data["users"].append(
{
"username": username,
"hashedPassword": hashed_password,
"sshKeys": [],
}
)
return True, "User was successfully created!", 201
def delete_user(username: str) -> tuple[bool, str, int]:
with WriteUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == data["username"] or username == "root":
return False, "Cannot delete main or root user", 400
# Return 404 if user does not exist
for data_user in data["users"]:
if data_user["username"] == username:
data["users"].remove(data_user)
break
else:
return False, "User does not exist", 404
return True, "User was deleted", 200
def update_user(username: str, password: str) -> tuple[bool, str, int]:
# Check if password is null or none
if password == "":
return False, "Password is null", 400
hashed_password = hash_password(password)
with WriteUserData() as data:
ensure_ssh_and_users_fields_exist(data)
if username == data["username"]:
data["hashedMasterPassword"] = hashed_password
# Return 404 if user does not exist
else:
for data_user in data["users"]:
if data_user["username"] == username:
data_user["hashedPassword"] = hashed_password
break
else:
return False, "User does not exist", 404
return True, "User was successfully updated", 200

View File

@ -2,20 +2,16 @@
# pylint: disable=too-few-public-methods
import datetime
import typing
from flask import request
import strawberry
from strawberry.types import Info
from selfprivacy_api.actions.api_tokens import get_api_tokens_with_caller_flag
from selfprivacy_api.graphql import IsAuthenticated
from selfprivacy_api.utils import parse_date
from selfprivacy_api.utils.auth import (
get_recovery_token_status,
get_tokens_info,
is_recovery_token_exists,
is_recovery_token_valid,
is_token_name_exists,
is_token_name_pair_valid,
refresh_token,
get_token_name,
)
@ -33,24 +29,6 @@ class ApiDevice:
is_caller: bool
def get_devices() -> typing.List[ApiDevice]:
"""Get list of devices"""
caller_name = get_token_name(
request.headers.get("Authorization").split(" ")[1]
if request.headers.get("Authorization") is not None
else None
)
tokens = get_tokens_info()
return [
ApiDevice(
name=token["name"],
creation_date=parse_date(token["date"]),
is_caller=token["name"] == caller_name,
)
for token in tokens
]
@strawberry.type
class ApiRecoveryKeyStatus:
"""Recovery key status"""
@ -97,9 +75,18 @@ class Api:
"""API access status"""
version: str = strawberry.field(resolver=get_api_version)
devices: typing.List[ApiDevice] = strawberry.field(
resolver=get_devices, permission_classes=[IsAuthenticated]
)
@strawberry.field(permission_classes=[IsAuthenticated])
def devices(self, info: Info) -> typing.List[ApiDevice]:
return [
ApiDevice(
name=device.name,
creation_date=device.date,
is_caller=device.is_caller,
)
for device in get_api_tokens_with_caller_flag(info.context.auth_token)
]
recovery_key: ApiRecoveryKeyStatus = strawberry.field(
resolver=get_recovery_key_status, permission_classes=[IsAuthenticated]
)

View File

@ -1,7 +1,5 @@
"""Enums representing different service providers."""
from enum import Enum
import datetime
import typing
import strawberry

View File

@ -1,12 +1,13 @@
"""Common system information and settings"""
# pylint: disable=too-few-public-methods
import subprocess
import typing
import strawberry
from selfprivacy_api.graphql.queries.common import Alert, Severity
from selfprivacy_api.graphql.queries.providers import DnsProvider, ServerProvider
from selfprivacy_api.utils import ReadUserData
import selfprivacy_api.actions.system as system_actions
import selfprivacy_api.actions.ssh as ssh_actions
@strawberry.type
@ -52,17 +53,11 @@ class AutoUpgradeOptions:
def get_auto_upgrade_options() -> AutoUpgradeOptions:
"""Get automatic upgrade options"""
with ReadUserData() as user_data:
if "autoUpgrade" not in user_data:
return AutoUpgradeOptions(enable=True, allow_reboot=False)
if "enable" not in user_data["autoUpgrade"]:
user_data["autoUpgrade"]["enable"] = True
if "allowReboot" not in user_data["autoUpgrade"]:
user_data["autoUpgrade"]["allowReboot"] = False
return AutoUpgradeOptions(
enable=user_data["autoUpgrade"]["enable"],
allow_reboot=user_data["autoUpgrade"]["allowReboot"],
)
settings = system_actions.get_auto_upgrade_settings()
return AutoUpgradeOptions(
enable=settings.enable,
allow_reboot=settings.allowReboot,
)
@strawberry.type
@ -76,30 +71,18 @@ class SshSettings:
def get_ssh_settings() -> SshSettings:
"""Get SSH settings"""
with ReadUserData() as user_data:
if "ssh" not in user_data:
return SshSettings(
enable=False, password_authentication=False, root_ssh_keys=[]
)
if "enable" not in user_data["ssh"]:
user_data["ssh"]["enable"] = False
if "passwordAuthentication" not in user_data["ssh"]:
user_data["ssh"]["passwordAuthentication"] = False
if "rootKeys" not in user_data["ssh"]:
user_data["ssh"]["rootKeys"] = []
return SshSettings(
enable=user_data["ssh"]["enable"],
password_authentication=user_data["ssh"]["passwordAuthentication"],
root_ssh_keys=user_data["ssh"]["rootKeys"],
)
settings = ssh_actions.get_ssh_settings()
return SshSettings(
enable=settings.enable,
password_authentication=settings.passwordAuthentication,
root_ssh_keys=settings.rootSshKeys,
)
def get_system_timezone() -> str:
"""Get system timezone"""
with ReadUserData() as user_data:
if "timezone" not in user_data:
return "Europe/Uzhgorod"
return user_data["timezone"]
return system_actions.get_timezone()
@strawberry.type
@ -115,12 +98,12 @@ class SystemSettings:
def get_system_version() -> str:
"""Get system version"""
return subprocess.check_output(["uname", "-a"]).decode("utf-8").strip()
return system_actions.get_system_version()
def get_python_version() -> str:
"""Get Python version"""
return subprocess.check_output(["python", "-V"]).decode("utf-8").strip()
return system_actions.get_python_version()
@strawberry.type

View File

@ -5,27 +5,12 @@ import strawberry
from selfprivacy_api.graphql.common_types.user import (
User,
ensure_ssh_and_users_fields_exist,
get_user_by_username,
get_users,
)
from selfprivacy_api.utils import ReadUserData
from selfprivacy_api.graphql import IsAuthenticated
def get_users() -> typing.List[User]:
"""Get users"""
user_list = []
with ReadUserData() as data:
ensure_ssh_and_users_fields_exist(data)
for user in data["users"]:
user_list.append(get_user_by_username(user["username"]))
user_list.append(get_user_by_username(data["username"]))
return user_list
@strawberry.type
class Users:
@strawberry.field(permission_classes=[IsAuthenticated])

View File

@ -129,7 +129,9 @@ class Jobs:
"""
self.observers.append(observer)
def remove_observer(self, observer: typing.Callable[[typing.List[Job]], None]) -> None:
def remove_observer(
self, observer: typing.Callable[[typing.List[Job]], None]
) -> None:
"""
Remove an observer from the jobs list.
"""
@ -143,7 +145,12 @@ class Jobs:
observer(self.jobs)
def add(
self, name: str, description: str, status: JobStatus = JobStatus.CREATED, status_text: str = "", progress: int = 0
self,
name: str,
description: str,
status: JobStatus = JobStatus.CREATED,
status_text: str = "",
progress: int = 0,
) -> Job:
"""
Add a job to the jobs list.

View File

@ -1,7 +1,9 @@
import time
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.jobs import JobStatus, Jobs
huey = Huey()
@huey.task()
def test_job():

View File

@ -1,14 +0,0 @@
#!/usr/bin/env python3
"""API authentication module"""
from flask import Blueprint
from flask_restful import Api
auth = Blueprint("auth", __name__, url_prefix="/auth")
api = Api(auth)
from . import (
new_device,
recovery_token,
app_tokens,
)

View File

@ -1,118 +0,0 @@
#!/usr/bin/env python3
"""App tokens management module"""
from flask import request
from flask_restful import Resource, reqparse
from selfprivacy_api.resources.api_auth import api
from selfprivacy_api.utils.auth import (
delete_token,
get_tokens_info,
is_token_name_exists,
is_token_name_pair_valid,
refresh_token,
get_token_name,
)
class Tokens(Resource):
"""Token management class
GET returns the list of active devices.
DELETE invalidates token unless it is the last one or the caller uses this token.
POST refreshes the token of the caller.
"""
def get(self):
"""
Get current device tokens
---
tags:
- Tokens
security:
- bearerAuth: []
responses:
200:
description: List of tokens
400:
description: Bad request
"""
caller_name = get_token_name(request.headers.get("Authorization").split(" ")[1])
tokens = get_tokens_info()
# Retrun a list of tokens and if it is the caller's token
# it will be marked with a flag
return [
{
"name": token["name"],
"date": token["date"],
"is_caller": token["name"] == caller_name,
}
for token in tokens
]
def delete(self):
"""
Delete token
---
tags:
- Tokens
security:
- bearerAuth: []
parameters:
- in: body
name: token
required: true
description: Token's name to delete
schema:
type: object
properties:
token_name:
type: string
description: Token name to delete
required: true
responses:
200:
description: Token deleted
400:
description: Bad request
404:
description: Token not found
"""
parser = reqparse.RequestParser()
parser.add_argument(
"token_name", type=str, required=True, help="Token to delete"
)
args = parser.parse_args()
token_name = args["token_name"]
if is_token_name_pair_valid(
token_name, request.headers.get("Authorization").split(" ")[1]
):
return {"message": "Cannot delete caller's token"}, 400
if not is_token_name_exists(token_name):
return {"message": "Token not found"}, 404
delete_token(token_name)
return {"message": "Token deleted"}, 200
def post(self):
"""
Refresh token
---
tags:
- Tokens
security:
- bearerAuth: []
responses:
200:
description: Token refreshed
400:
description: Bad request
404:
description: Token not found
"""
# Get token from header
token = request.headers.get("Authorization").split(" ")[1]
new_token = refresh_token(token)
if new_token is None:
return {"message": "Token not found"}, 404
return {"token": new_token}, 200
api.add_resource(Tokens, "/tokens")

View File

@ -1,103 +0,0 @@
#!/usr/bin/env python3
"""New device auth module"""
from flask_restful import Resource, reqparse
from selfprivacy_api.resources.api_auth import api
from selfprivacy_api.utils.auth import (
get_new_device_auth_token,
use_new_device_auth_token,
delete_new_device_auth_token,
)
class NewDevice(Resource):
"""New device auth class
POST returns a new token for the caller.
"""
def post(self):
"""
Get new device token
---
tags:
- Tokens
security:
- bearerAuth: []
responses:
200:
description: New device token
400:
description: Bad request
"""
token = get_new_device_auth_token()
return {"token": token}
def delete(self):
"""
Delete new device token
---
tags:
- Tokens
security:
- bearerAuth: []
responses:
200:
description: New device token deleted
400:
description: Bad request
"""
delete_new_device_auth_token()
return {"token": None}
class AuthorizeDevice(Resource):
"""Authorize device class
POST authorizes the caller.
"""
def post(self):
"""
Authorize device
---
tags:
- Tokens
parameters:
- in: body
name: data
required: true
description: Who is authorizing
schema:
type: object
properties:
token:
type: string
description: Mnemonic token to authorize
device:
type: string
description: Device to authorize
responses:
200:
description: Device authorized
400:
description: Bad request
404:
description: Token not found
"""
parser = reqparse.RequestParser()
parser.add_argument(
"token", type=str, required=True, help="Mnemonic token to authorize"
)
parser.add_argument(
"device", type=str, required=True, help="Device to authorize"
)
args = parser.parse_args()
auth_token = args["token"]
device = args["device"]
token = use_new_device_auth_token(auth_token, device)
if token is None:
return {"message": "Token not found"}, 404
return {"message": "Device authorized", "token": token}, 200
api.add_resource(NewDevice, "/new_device")
api.add_resource(AuthorizeDevice, "/new_device/authorize")

View File

@ -1,205 +0,0 @@
#!/usr/bin/env python3
"""Recovery token module"""
from datetime import datetime
from flask_restful import Resource, reqparse
from selfprivacy_api.resources.api_auth import api
from selfprivacy_api.utils import parse_date
from selfprivacy_api.utils.auth import (
is_recovery_token_exists,
is_recovery_token_valid,
get_recovery_token_status,
generate_recovery_token,
use_mnemonic_recoverery_token,
)
class RecoveryToken(Resource):
"""Recovery token class
GET returns the status of the recovery token.
POST generates a new recovery token.
"""
def get(self):
"""
Get recovery token status
---
tags:
- Tokens
security:
- bearerAuth: []
responses:
200:
description: Recovery token status
schema:
type: object
properties:
exists:
type: boolean
description: Recovery token exists
valid:
type: boolean
description: Recovery token is valid
date:
type: string
description: Recovery token date
expiration:
type: string
description: Recovery token expiration date
uses_left:
type: integer
description: Recovery token uses left
400:
description: Bad request
"""
if not is_recovery_token_exists():
return {
"exists": False,
"valid": False,
"date": None,
"expiration": None,
"uses_left": None,
}
status = get_recovery_token_status()
# check if status is None
if status is None:
return {
"exists": False,
"valid": False,
"date": None,
"expiration": None,
"uses_left": None,
}
if not is_recovery_token_valid():
return {
"exists": True,
"valid": False,
"date": status["date"],
"expiration": status["expiration"],
"uses_left": status["uses_left"],
}
return {
"exists": True,
"valid": True,
"date": status["date"],
"expiration": status["expiration"],
"uses_left": status["uses_left"],
}
def post(self):
"""
Generate recovery token
---
tags:
- Tokens
security:
- bearerAuth: []
parameters:
- in: body
name: data
required: true
description: Token data
schema:
type: object
properties:
expiration:
type: string
description: Token expiration date
uses:
type: integer
description: Token uses
responses:
200:
description: Recovery token generated
schema:
type: object
properties:
token:
type: string
description: Mnemonic recovery token
400:
description: Bad request
"""
parser = reqparse.RequestParser()
parser.add_argument(
"expiration", type=str, required=False, help="Token expiration date"
)
parser.add_argument("uses", type=int, required=False, help="Token uses")
args = parser.parse_args()
# Convert expiration date to datetime and return 400 if it is not valid
if args["expiration"]:
try:
expiration = parse_date(args["expiration"])
# Retrun 400 if expiration date is in the past
if expiration < datetime.now():
return {"message": "Expiration date cannot be in the past"}, 400
except ValueError:
return {
"error": "Invalid expiration date. Use YYYY-MM-DDTHH:MM:SS.SSS"
}, 400
else:
expiration = None
if args["uses"] is not None and args["uses"] < 1:
return {"message": "Uses must be greater than 0"}, 400
# Generate recovery token
token = generate_recovery_token(expiration, args["uses"])
return {"token": token}
class UseRecoveryToken(Resource):
"""Use recovery token class
POST uses the recovery token.
"""
def post(self):
"""
Use recovery token
---
tags:
- Tokens
parameters:
- in: body
name: data
required: true
description: Token data
schema:
type: object
properties:
token:
type: string
description: Mnemonic recovery token
device:
type: string
description: Device to authorize
responses:
200:
description: Recovery token used
schema:
type: object
properties:
token:
type: string
description: Device authorization token
400:
description: Bad request
404:
description: Token not found
"""
parser = reqparse.RequestParser()
parser.add_argument(
"token", type=str, required=True, help="Mnemonic recovery token"
)
parser.add_argument(
"device", type=str, required=True, help="Device to authorize"
)
args = parser.parse_args()
# Use recovery token
token = use_mnemonic_recoverery_token(args["token"], args["device"])
if token is None:
return {"error": "Token not found"}, 404
return {"token": token}
api.add_resource(RecoveryToken, "/recovery_token")
api.add_resource(UseRecoveryToken, "/recovery_token/use")

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
"""Unassigned views"""
from flask_restful import Resource
from selfprivacy_api.graphql.queries.api_queries import get_api_version
class ApiVersion(Resource):
"""SelfPrivacy API version"""
def get(self):
"""Get API version
---
tags:
- System
responses:
200:
description: API version
schema:
type: object
properties:
version:
type: string
description: API version
401:
description: Unauthorized
"""
return {"version": get_api_version()}

View File

@ -1,19 +0,0 @@
#!/usr/bin/env python3
"""Services management module"""
from flask import Blueprint
from flask_restful import Api
services = Blueprint("services", __name__, url_prefix="/services")
api = Api(services)
from . import (
bitwarden,
gitea,
mailserver,
main,
nextcloud,
ocserv,
pleroma,
restic,
ssh,
)

View File

@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""Bitwarden management module"""
from flask_restful import Resource
from selfprivacy_api.resources.services import api
from selfprivacy_api.utils import WriteUserData
class EnableBitwarden(Resource):
"""Enable Bitwarden"""
def post(self):
"""
Enable Bitwarden
---
tags:
- Bitwarden
security:
- bearerAuth: []
responses:
200:
description: Bitwarden enabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "bitwarden" not in data:
data["bitwarden"] = {}
data["bitwarden"]["enable"] = True
return {
"status": 0,
"message": "Bitwarden enabled",
}
class DisableBitwarden(Resource):
"""Disable Bitwarden"""
def post(self):
"""
Disable Bitwarden
---
tags:
- Bitwarden
security:
- bearerAuth: []
responses:
200:
description: Bitwarden disabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "bitwarden" not in data:
data["bitwarden"] = {}
data["bitwarden"]["enable"] = False
return {
"status": 0,
"message": "Bitwarden disabled",
}
api.add_resource(EnableBitwarden, "/bitwarden/enable")
api.add_resource(DisableBitwarden, "/bitwarden/disable")

View File

@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""Gitea management module"""
from flask_restful import Resource
from selfprivacy_api.resources.services import api
from selfprivacy_api.utils import WriteUserData
class EnableGitea(Resource):
"""Enable Gitea"""
def post(self):
"""
Enable Gitea
---
tags:
- Gitea
security:
- bearerAuth: []
responses:
200:
description: Gitea enabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "gitea" not in data:
data["gitea"] = {}
data["gitea"]["enable"] = True
return {
"status": 0,
"message": "Gitea enabled",
}
class DisableGitea(Resource):
"""Disable Gitea"""
def post(self):
"""
Disable Gitea
---
tags:
- Gitea
security:
- bearerAuth: []
responses:
200:
description: Gitea disabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "gitea" not in data:
data["gitea"] = {}
data["gitea"]["enable"] = False
return {
"status": 0,
"message": "Gitea disabled",
}
api.add_resource(EnableGitea, "/gitea/enable")
api.add_resource(DisableGitea, "/gitea/disable")

View File

@ -1,41 +0,0 @@
#!/usr/bin/env python3
"""Mail server management module"""
import base64
import subprocess
import os
from flask_restful import Resource
from selfprivacy_api.resources.services import api
from selfprivacy_api.utils import get_dkim_key, get_domain
class DKIMKey(Resource):
"""Get DKIM key from file"""
def get(self):
"""
Get DKIM key from file
---
tags:
- Email
security:
- bearerAuth: []
responses:
200:
description: DKIM key encoded in base64
401:
description: Unauthorized
404:
description: DKIM key not found
"""
domain = get_domain()
dkim = get_dkim_key(domain)
if dkim is None:
return "DKIM file not found", 404
dkim = base64.b64encode(dkim.encode("utf-8")).decode("utf-8")
return dkim
api.add_resource(DKIMKey, "/mailserver/dkim")

View File

@ -1,84 +0,0 @@
#!/usr/bin/env python3
"""Services status module"""
import subprocess
from flask_restful import Resource
from . import api
class ServiceStatus(Resource):
"""Get service status"""
def get(self):
"""
Get service status
---
tags:
- Services
responses:
200:
description: Service status
schema:
type: object
properties:
imap:
type: integer
description: Dovecot service status
smtp:
type: integer
description: Postfix service status
http:
type: integer
description: Nginx service status
bitwarden:
type: integer
description: Bitwarden service status
gitea:
type: integer
description: Gitea service status
nextcloud:
type: integer
description: Nextcloud service status
ocserv:
type: integer
description: OpenConnect VPN service status
pleroma:
type: integer
description: Pleroma service status
401:
description: Unauthorized
"""
imap_service = subprocess.Popen(["systemctl", "status", "dovecot2.service"])
imap_service.communicate()[0]
smtp_service = subprocess.Popen(["systemctl", "status", "postfix.service"])
smtp_service.communicate()[0]
http_service = subprocess.Popen(["systemctl", "status", "nginx.service"])
http_service.communicate()[0]
bitwarden_service = subprocess.Popen(
["systemctl", "status", "vaultwarden.service"]
)
bitwarden_service.communicate()[0]
gitea_service = subprocess.Popen(["systemctl", "status", "gitea.service"])
gitea_service.communicate()[0]
nextcloud_service = subprocess.Popen(
["systemctl", "status", "phpfpm-nextcloud.service"]
)
nextcloud_service.communicate()[0]
ocserv_service = subprocess.Popen(["systemctl", "status", "ocserv.service"])
ocserv_service.communicate()[0]
pleroma_service = subprocess.Popen(["systemctl", "status", "pleroma.service"])
pleroma_service.communicate()[0]
return {
"imap": imap_service.returncode,
"smtp": smtp_service.returncode,
"http": http_service.returncode,
"bitwarden": bitwarden_service.returncode,
"gitea": gitea_service.returncode,
"nextcloud": nextcloud_service.returncode,
"ocserv": ocserv_service.returncode,
"pleroma": pleroma_service.returncode,
}
api.add_resource(ServiceStatus, "/status")

View File

@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""Nextcloud management module"""
from flask_restful import Resource
from selfprivacy_api.resources.services import api
from selfprivacy_api.utils import WriteUserData
class EnableNextcloud(Resource):
"""Enable Nextcloud"""
def post(self):
"""
Enable Nextcloud
---
tags:
- Nextcloud
security:
- bearerAuth: []
responses:
200:
description: Nextcloud enabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "nextcloud" not in data:
data["nextcloud"] = {}
data["nextcloud"]["enable"] = True
return {
"status": 0,
"message": "Nextcloud enabled",
}
class DisableNextcloud(Resource):
"""Disable Nextcloud"""
def post(self):
"""
Disable Nextcloud
---
tags:
- Nextcloud
security:
- bearerAuth: []
responses:
200:
description: Nextcloud disabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "nextcloud" not in data:
data["nextcloud"] = {}
data["nextcloud"]["enable"] = False
return {
"status": 0,
"message": "Nextcloud disabled",
}
api.add_resource(EnableNextcloud, "/nextcloud/enable")
api.add_resource(DisableNextcloud, "/nextcloud/disable")

View File

@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""OpenConnect VPN server management module"""
from flask_restful import Resource
from selfprivacy_api.resources.services import api
from selfprivacy_api.utils import WriteUserData
class EnableOcserv(Resource):
"""Enable OpenConnect VPN server"""
def post(self):
"""
Enable OCserv
---
tags:
- OCserv
security:
- bearerAuth: []
responses:
200:
description: OCserv enabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "ocserv" not in data:
data["ocserv"] = {}
data["ocserv"]["enable"] = True
return {
"status": 0,
"message": "OpenConnect VPN server enabled",
}
class DisableOcserv(Resource):
"""Disable OpenConnect VPN server"""
def post(self):
"""
Disable OCserv
---
tags:
- OCserv
security:
- bearerAuth: []
responses:
200:
description: OCserv disabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "ocserv" not in data:
data["ocserv"] = {}
data["ocserv"]["enable"] = False
return {
"status": 0,
"message": "OpenConnect VPN server disabled",
}
api.add_resource(EnableOcserv, "/ocserv/enable")
api.add_resource(DisableOcserv, "/ocserv/disable")

View File

@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""Pleroma management module"""
from flask_restful import Resource
from selfprivacy_api.resources.services import api
from selfprivacy_api.utils import WriteUserData
class EnablePleroma(Resource):
"""Enable Pleroma"""
def post(self):
"""
Enable Pleroma
---
tags:
- Pleroma
security:
- bearerAuth: []
responses:
200:
description: Pleroma enabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "pleroma" not in data:
data["pleroma"] = {}
data["pleroma"]["enable"] = True
return {
"status": 0,
"message": "Pleroma enabled",
}
class DisablePleroma(Resource):
"""Disable Pleroma"""
def post(self):
"""
Disable Pleroma
---
tags:
- Pleroma
security:
- bearerAuth: []
responses:
200:
description: Pleroma disabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "pleroma" not in data:
data["pleroma"] = {}
data["pleroma"]["enable"] = False
return {
"status": 0,
"message": "Pleroma disabled",
}
api.add_resource(EnablePleroma, "/pleroma/enable")
api.add_resource(DisablePleroma, "/pleroma/disable")

View File

@ -1,241 +0,0 @@
#!/usr/bin/env python3
"""Backups management module"""
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):
"""List all restic backups"""
def get(self):
"""
Get all restic backups
---
tags:
- Backups
security:
- bearerAuth: []
responses:
200:
description: A list of snapshots
400:
description: Bad request
401:
description: Unauthorized
"""
restic = ResticController()
return restic.snapshot_list
class AsyncCreateBackup(Resource):
"""Create a new restic backup"""
def put(self):
"""
Initiate a new restic backup
---
tags:
- Backups
security:
- bearerAuth: []
responses:
200:
description: Backup creation has started
400:
description: Bad request
401:
description: Unauthorized
409:
description: Backup already in progress
"""
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",
}
class CheckBackupStatus(Resource):
"""Check current backup status"""
def get(self):
"""
Get backup status
---
tags:
- Backups
security:
- bearerAuth: []
responses:
200:
description: Backup status
400:
description: Bad request
401:
description: Unauthorized
"""
restic = ResticController()
return {
"status": restic.state.name,
"progress": restic.progress,
"error_message": restic.error_message,
}
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):
"""Trigger backup restoration process"""
def put(self):
"""
Start backup restoration
---
tags:
- 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
400:
description: Bad request
401:
description: Unauthorized
"""
parser = reqparse.RequestParser()
parser.add_argument("backupId", type=str, required=True)
args = parser.parse_args()
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",
}
return {"error": "Backup not found"}, 404
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:
if "backblaze" not in data:
data["backblaze"] = {}
data["backblaze"]["accountId"] = args["accountId"]
data["backblaze"]["accountKey"] = args["accountKey"]
data["backblaze"]["bucket"] = args["bucket"]
restic_tasks.update_keys_from_userdata()
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")
api.add_resource(ForceReloadSnapshots, "/restic/backup/reload")

View File

@ -1,407 +0,0 @@
#!/usr/bin/env python3
"""SSH management module"""
from flask_restful import Resource, reqparse
from selfprivacy_api.resources.services import api
from selfprivacy_api.utils import WriteUserData, ReadUserData, validate_ssh_public_key
class EnableSSH(Resource):
"""Enable SSH"""
def post(self):
"""
Enable SSH
---
tags:
- SSH
security:
- bearerAuth: []
responses:
200:
description: SSH enabled
401:
description: Unauthorized
"""
with WriteUserData() as data:
if "ssh" not in data:
data["ssh"] = {}
data["ssh"]["enable"] = True
return {
"status": 0,
"message": "SSH enabled",
}
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"""
def put(self):
"""
Add a SSH root key
---
consumes:
- application/json
tags:
- SSH
security:
- bearerAuth: []
parameters:
- in: body
name: body
required: true
description: Public key to add
schema:
type: object
required:
- public_key
properties:
public_key:
type: string
description: ssh-ed25519 public key.
responses:
201:
description: Key added
400:
description: Bad request
401:
description: Unauthorized
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()
public_key = args["public_key"]
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:
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,
"message": "New SSH key successfully written",
}, 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"]
if "users" not in data:
data["users"] = []
for user in data["users"]:
if user["username"] == username:
if "sshKeys" not in user:
user["sshKeys"] = []
return user["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
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"]:
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",
}, 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)
# If rootKeys became zero length, delete it
if len(data["ssh"]["rootKeys"]) == 0:
del data["ssh"]["rootKeys"]
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/<string:username>")

View File

@ -1,346 +0,0 @@
#!/usr/bin/env python3
"""System management module"""
import os
import subprocess
import pytz
from flask import Blueprint
from flask_restful import Resource, Api, reqparse
from selfprivacy_api.graphql.queries.system import (
get_python_version,
get_system_version,
)
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"""
def get(self):
"""
Rebuild NixOS with nixos-rebuild switch
---
tags:
- System
security:
- bearerAuth: []
responses:
200:
description: System rebuild has started
401:
description: Unauthorized
"""
rebuild_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-rebuild.service"], start_new_session=True
)
rebuild_result.communicate()[0]
return rebuild_result.returncode
class RollbackSystem(Resource):
"""Rollback NixOS"""
def get(self):
"""
Rollback NixOS with nixos-rebuild switch --rollback
---
tags:
- System
security:
- bearerAuth: []
responses:
200:
description: System rollback has started
401:
description: Unauthorized
"""
rollback_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-rollback.service"], start_new_session=True
)
rollback_result.communicate()[0]
return rollback_result.returncode
class UpgradeSystem(Resource):
"""Upgrade NixOS"""
def get(self):
"""
Upgrade NixOS with nixos-rebuild switch --upgrade
---
tags:
- System
security:
- bearerAuth: []
responses:
200:
description: System upgrade has started
401:
description: Unauthorized
"""
upgrade_result = subprocess.Popen(
["systemctl", "start", "sp-nixos-upgrade.service"], start_new_session=True
)
upgrade_result.communicate()[0]
return upgrade_result.returncode
class RebootSystem(Resource):
"""Reboot the system"""
def get(self):
"""
Reboot the system
---
tags:
- System
security:
- bearerAuth: []
responses:
200:
description: System reboot has started
401:
description: Unauthorized
"""
subprocess.Popen(["reboot"], start_new_session=True)
return "System reboot has started"
class SystemVersion(Resource):
"""Get system version from uname"""
def get(self):
"""
Get system version from uname -a
---
tags:
- System
security:
- bearerAuth: []
responses:
200:
description: OK
401:
description: Unauthorized
"""
return {
"system_version": get_system_version(),
}
class PythonVersion(Resource):
"""Get python version"""
def get(self):
"""
Get python version used by this API
---
tags:
- System
security:
- bearerAuth: []
responses:
200:
description: OK
401:
description: Unauthorized
"""
return get_python_version()
class PullRepositoryChanges(Resource):
"""Pull NixOS config repository changes"""
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,
)
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",
"data": data,
}
return {
"status": git_pull_process_descriptor.returncode,
"message": "Something went wrong",
"data": data,
}, 500
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")
api.add_resource(RebootSystem, "/reboot")
api.add_resource(SystemVersion, "/version")
api.add_resource(PythonVersion, "/pythonVersion")
api.add_resource(PullRepositoryChanges, "/configuration/pull")

View File

@ -1,162 +0,0 @@
#!/usr/bin/env python3
"""Users management module"""
import subprocess
import re
from flask_restful import Resource, reqparse
from selfprivacy_api.utils import WriteUserData, ReadUserData, is_username_forbidden
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
"""
parser = reqparse.RequestParser(bundle_errors=True)
parser.add_argument("withMainUser", type=bool, required=False)
args = parser.parse_args()
with_main_user = False if args["withMainUser"] is None else args["withMainUser"]
with ReadUserData() as data:
users = []
if with_main_user:
users.append(data["username"])
if "users" in data:
for user in data["users"]:
users.append(user["username"])
return users
def post(self):
"""
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,
)
hashed_password = password_hash_process_descriptor.communicate()[0]
hashed_password = hashed_password.decode("ascii")
hashed_password = hashed_password.rstrip()
# Check if username is forbidden
if is_username_forbidden(args["username"]):
return {"message": "Username is forbidden"}, 409
# 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 WriteUserData() as data:
if "users" not in data:
data["users"] = []
# Return 409 if user already exists
if data["username"] == args["username"]:
return {"error": "User already exists"}, 409
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,
}
)
return {"result": 0, "username": args["username"]}, 201
class User(Resource):
"""Single user managment"""
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
401:
description: Unauthorized
404:
description: User not found
"""
with WriteUserData() as data:
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}

View File

View File

@ -0,0 +1,127 @@
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from selfprivacy_api.actions.api_tokens import (
CannotDeleteCallerException,
InvalidExpirationDate,
InvalidUsesLeft,
NotFoundException,
delete_api_token,
get_api_recovery_token_status,
get_api_tokens_with_caller_flag,
get_new_api_recovery_key,
refresh_api_token,
)
from selfprivacy_api.dependencies import TokenHeader, get_token_header
from selfprivacy_api.utils.auth import (
delete_new_device_auth_token,
get_new_device_auth_token,
use_mnemonic_recoverery_token,
use_new_device_auth_token,
)
router = APIRouter(
prefix="/auth",
tags=["auth"],
responses={404: {"description": "Not found"}},
)
@router.get("/tokens")
async def rest_get_tokens(auth_token: TokenHeader = Depends(get_token_header)):
"""Get the tokens info"""
return get_api_tokens_with_caller_flag(auth_token.token)
class DeleteTokenInput(BaseModel):
"""Delete token input"""
token_name: str
@router.delete("/tokens")
async def rest_delete_tokens(
token: DeleteTokenInput, auth_token: TokenHeader = Depends(get_token_header)
):
"""Delete the tokens"""
try:
delete_api_token(auth_token.token, token.token_name)
except NotFoundException:
raise HTTPException(status_code=404, detail="Token not found")
except CannotDeleteCallerException:
raise HTTPException(status_code=400, detail="Cannot delete caller's token")
return {"message": "Token deleted"}
@router.post("/tokens")
async def rest_refresh_token(auth_token: TokenHeader = Depends(get_token_header)):
"""Refresh the token"""
try:
new_token = refresh_api_token(auth_token.token)
except NotFoundException:
raise HTTPException(status_code=404, detail="Token not found")
return {"token": new_token}
@router.get("/recovery_token")
async def rest_get_recovery_token_status(
auth_token: TokenHeader = Depends(get_token_header),
):
return get_api_recovery_token_status()
class CreateRecoveryTokenInput(BaseModel):
expiration: Optional[datetime] = None
uses: Optional[int] = None
@router.post("/recovery_token")
async def rest_create_recovery_token(
limits: CreateRecoveryTokenInput = CreateRecoveryTokenInput(),
auth_token: TokenHeader = Depends(get_token_header),
):
try:
token = get_new_api_recovery_key(limits.expiration, limits.uses)
except InvalidExpirationDate as e:
raise HTTPException(status_code=400, detail=str(e))
except InvalidUsesLeft as e:
raise HTTPException(status_code=400, detail=str(e))
return {"token": token}
class UseTokenInput(BaseModel):
token: str
device: str
@router.post("/recovery_token/use")
async def rest_use_recovery_token(input: UseTokenInput):
token = use_mnemonic_recoverery_token(input.token, input.device)
if token is None:
raise HTTPException(status_code=404, detail="Token not found")
return {"token": token}
@router.post("/new_device")
async def rest_new_device(auth_token: TokenHeader = Depends(get_token_header)):
token = get_new_device_auth_token()
return {"token": token}
@router.delete("/new_device")
async def rest_delete_new_device_token(
auth_token: TokenHeader = Depends(get_token_header),
):
delete_new_device_auth_token()
return {"token": None}
@router.post("/new_device/authorize")
async def rest_new_device_authorize(input: UseTokenInput):
token = use_new_device_auth_token(input.token, input.device)
if token is None:
raise HTTPException(status_code=404, detail="Token not found")
return {"message": "Device authorized", "token": token}

View File

@ -0,0 +1,371 @@
"""Basic services legacy api"""
import base64
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from selfprivacy_api.actions.ssh import (
InvalidPublicKey,
KeyAlreadyExists,
KeyNotFound,
create_ssh_key,
enable_ssh,
get_ssh_settings,
remove_ssh_key,
set_ssh_settings,
)
from selfprivacy_api.actions.users import UserNotFound, get_user_by_username
from selfprivacy_api.dependencies import get_token_header
from selfprivacy_api.restic_controller import ResticController, ResticStates
from selfprivacy_api.restic_controller import tasks as restic_tasks
from selfprivacy_api.services.bitwarden import Bitwarden
from selfprivacy_api.services.gitea import Gitea
from selfprivacy_api.services.mailserver import MailServer
from selfprivacy_api.services.nextcloud import Nextcloud
from selfprivacy_api.services.ocserv import Ocserv
from selfprivacy_api.services.pleroma import Pleroma
from selfprivacy_api.services.service import ServiceStatus
from selfprivacy_api.utils import WriteUserData, get_dkim_key, get_domain
router = APIRouter(
prefix="/services",
tags=["services"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)
def service_status_to_return_code(status: ServiceStatus):
if status == ServiceStatus.RUNNING:
return 0
elif status == ServiceStatus.ERROR:
return 1
elif status == ServiceStatus.STOPPED:
return 3
elif status == ServiceStatus.OFF:
return 4
else:
return 2
@router.get("/status")
async def get_status():
"""Get the status of the services"""
mail_status = MailServer.get_status()
bitwarden_status = Bitwarden.get_status()
gitea_status = Gitea.get_status()
nextcloud_status = Nextcloud.get_status()
ocserv_stauts = Ocserv.get_status()
pleroma_status = Pleroma.get_status()
return {
"imap": service_status_to_return_code(mail_status),
"smtp": service_status_to_return_code(mail_status),
"http": 0,
"bitwarden": service_status_to_return_code(bitwarden_status),
"gitea": service_status_to_return_code(gitea_status),
"nextcloud": service_status_to_return_code(nextcloud_status),
"ocserv": service_status_to_return_code(ocserv_stauts),
"pleroma": service_status_to_return_code(pleroma_status),
}
@router.post("/bitwarden/enable")
async def enable_bitwarden():
"""Enable Bitwarden"""
Bitwarden.enable()
return {
"status": 0,
"message": "Bitwarden enabled",
}
@router.post("/bitwarden/disable")
async def disable_bitwarden():
"""Disable Bitwarden"""
Bitwarden.disable()
return {
"status": 0,
"message": "Bitwarden disabled",
}
@router.post("/gitea/enable")
async def enable_gitea():
"""Enable Gitea"""
Gitea.enable()
return {
"status": 0,
"message": "Gitea enabled",
}
@router.post("/gitea/disable")
async def disable_gitea():
"""Disable Gitea"""
Gitea.disable()
return {
"status": 0,
"message": "Gitea disabled",
}
@router.get("/mailserver/dkim")
async def get_mailserver_dkim():
"""Get the DKIM record for the mailserver"""
domain = get_domain()
dkim = get_dkim_key(domain)
if dkim is None:
raise HTTPException(status_code=404, detail="DKIM record not found")
dkim = base64.b64encode(dkim.encode("utf-8")).decode("utf-8")
return dkim
@router.post("/nextcloud/enable")
async def enable_nextcloud():
"""Enable Nextcloud"""
Nextcloud.enable()
return {
"status": 0,
"message": "Nextcloud enabled",
}
@router.post("/nextcloud/disable")
async def disable_nextcloud():
"""Disable Nextcloud"""
Nextcloud.disable()
return {
"status": 0,
"message": "Nextcloud disabled",
}
@router.post("/ocserv/enable")
async def enable_ocserv():
"""Enable Ocserv"""
Ocserv.enable()
return {
"status": 0,
"message": "Ocserv enabled",
}
@router.post("/ocserv/disable")
async def disable_ocserv():
"""Disable Ocserv"""
Ocserv.disable()
return {
"status": 0,
"message": "Ocserv disabled",
}
@router.post("/pleroma/enable")
async def enable_pleroma():
"""Enable Pleroma"""
Pleroma.enable()
return {
"status": 0,
"message": "Pleroma enabled",
}
@router.post("/pleroma/disable")
async def disable_pleroma():
"""Disable Pleroma"""
Pleroma.disable()
return {
"status": 0,
"message": "Pleroma disabled",
}
@router.get("/restic/backup/list")
async def get_restic_backup_list():
restic = ResticController()
return restic.snapshot_list
@router.put("/restic/backup/create")
async def create_restic_backup():
restic = ResticController()
if restic.state is ResticStates.NO_KEY:
raise HTTPException(status_code=400, detail="Backup key not provided")
if restic.state is ResticStates.INITIALIZING:
raise HTTPException(status_code=400, detail="Backup is initializing")
if restic.state is ResticStates.BACKING_UP:
raise HTTPException(status_code=409, detail="Backup is already running")
restic_tasks.start_backup()
return {
"status": 0,
"message": "Backup creation has started",
}
@router.get("/restic/backup/status")
async def get_restic_backup_status():
restic = ResticController()
return {
"status": restic.state.name,
"progress": restic.progress,
"error_message": restic.error_message,
}
@router.get("/restic/backup/reload")
async def reload_restic_backup():
restic_tasks.load_snapshots()
return {
"status": 0,
"message": "Snapshots reload started",
}
class BackupRestoreInput(BaseModel):
backupId: str
@router.put("/restic/backup/restore")
async def restore_restic_backup(backup: BackupRestoreInput):
restic = ResticController()
if restic.state is ResticStates.NO_KEY:
raise HTTPException(status_code=400, detail="Backup key not provided")
if restic.state is ResticStates.NOT_INITIALIZED:
raise HTTPException(
status_code=400, detail="Backups repository is not initialized"
)
if restic.state is ResticStates.BACKING_UP:
raise HTTPException(status_code=409, detail="Backup is already running")
if restic.state is ResticStates.INITIALIZING:
raise HTTPException(status_code=400, detail="Repository is initializing")
if restic.state is ResticStates.RESTORING:
raise HTTPException(status_code=409, detail="Restore is already running")
for backup_item in restic.snapshot_list:
if backup_item["short_id"] == backup.backupId:
restic_tasks.restore_from_backup(backup.backupId)
return {
"status": 0,
"message": "Backup restoration procedure started",
}
raise HTTPException(status_code=404, detail="Backup not found")
class BackblazeConfigInput(BaseModel):
accountId: str
accountKey: str
bucket: str
@router.put("/restic/backblaze/config")
async def set_backblaze_config(backblaze_config: BackblazeConfigInput):
with WriteUserData() as data:
if "backblaze" not in data:
data["backblaze"] = {}
data["backblaze"]["accountId"] = backblaze_config.accountId
data["backblaze"]["accountKey"] = backblaze_config.accountKey
data["backblaze"]["bucket"] = backblaze_config.bucket
restic_tasks.update_keys_from_userdata()
return "New Backblaze settings saved"
@router.post("/ssh/enable")
async def rest_enable_ssh():
"""Enable SSH"""
enable_ssh()
return {
"status": 0,
"message": "SSH enabled",
}
@router.get("/ssh")
async def rest_get_ssh():
"""Get the SSH configuration"""
settings = get_ssh_settings()
return {
"enable": settings.enable,
"passwordAuthentication": settings.passwordAuthentication,
}
class SshConfigInput(BaseModel):
enable: Optional[bool] = None
passwordAuthentication: Optional[bool] = None
@router.put("/ssh")
async def rest_set_ssh(ssh_config: SshConfigInput):
"""Set the SSH configuration"""
set_ssh_settings(ssh_config.enable, ssh_config.passwordAuthentication)
return "SSH settings changed"
class SshKeyInput(BaseModel):
public_key: str
@router.put("/ssh/key/send", status_code=201)
async def rest_send_ssh_key(input: SshKeyInput):
"""Send the SSH key"""
try:
create_ssh_key("root", input.public_key)
except KeyAlreadyExists:
raise HTTPException(status_code=409, detail="Key already exists")
except InvalidPublicKey:
raise HTTPException(
status_code=400,
detail="Invalid key type. Only ssh-ed25519 and ssh-rsa are supported",
)
return {
"status": 0,
"message": "SSH key sent",
}
@router.get("/ssh/keys/{username}")
async def rest_get_ssh_keys(username: str):
"""Get the SSH keys for a user"""
user = get_user_by_username(username)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user.ssh_keys
@router.post("/ssh/keys/{username}", status_code=201)
async def rest_add_ssh_key(username: str, input: SshKeyInput):
try:
create_ssh_key(username, input.public_key)
except KeyAlreadyExists:
raise HTTPException(status_code=409, detail="Key already exists")
except InvalidPublicKey:
raise HTTPException(
status_code=400,
detail="Invalid key type. Only ssh-ed25519 and ssh-rsa are supported",
)
except UserNotFound:
raise HTTPException(status_code=404, detail="User not found")
return {
"message": "New SSH key successfully written",
}
@router.delete("/ssh/keys/{username}")
async def rest_delete_ssh_key(username: str, input: SshKeyInput):
try:
remove_ssh_key(username, input.public_key)
except KeyNotFound:
raise HTTPException(status_code=404, detail="Key not found")
except UserNotFound:
raise HTTPException(status_code=404, detail="User not found")
return {"message": "SSH key deleted"}

View File

@ -0,0 +1,105 @@
from typing import Optional
from fastapi import APIRouter, Body, Depends, HTTPException
from pydantic import BaseModel
from selfprivacy_api.dependencies import get_token_header
import selfprivacy_api.actions.system as system_actions
router = APIRouter(
prefix="/system",
tags=["system"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)
@router.get("/configuration/timezone")
async def get_timezone():
"""Get the timezone of the server"""
return system_actions.get_timezone()
class ChangeTimezoneRequestBody(BaseModel):
"""Change the timezone of the server"""
timezone: str
@router.put("/configuration/timezone")
async def change_timezone(timezone: ChangeTimezoneRequestBody):
"""Change the timezone of the server"""
try:
system_actions.change_timezone(timezone.timezone)
except system_actions.InvalidTimezone as e:
raise HTTPException(status_code=400, detail=str(e))
return {"timezone": timezone.timezone}
@router.get("/configuration/autoUpgrade")
async def get_auto_upgrade_settings():
"""Get the auto-upgrade settings"""
return system_actions.get_auto_upgrade_settings().dict()
class AutoUpgradeSettings(BaseModel):
"""Settings for auto-upgrading user data"""
enable: Optional[bool] = None
allowReboot: Optional[bool] = None
@router.put("/configuration/autoUpgrade")
async def set_auto_upgrade_settings(settings: AutoUpgradeSettings):
"""Set the auto-upgrade settings"""
system_actions.set_auto_upgrade_settings(settings.enable, settings.allowReboot)
return "Auto-upgrade settings changed"
@router.get("/configuration/apply")
async def apply_configuration():
"""Apply the configuration"""
return_code = system_actions.rebuild_system()
return return_code
@router.get("/configuration/rollback")
async def rollback_configuration():
"""Rollback the configuration"""
return_code = system_actions.rollback_system()
return return_code
@router.get("/configuration/upgrade")
async def upgrade_configuration():
"""Upgrade the configuration"""
return_code = system_actions.upgrade_system()
return return_code
@router.get("/reboot")
async def reboot_system():
"""Reboot the system"""
system_actions.reboot_system()
return "System reboot has started"
@router.get("/version")
async def get_system_version():
"""Get the system version"""
return {"system_version": system_actions.get_system_version()}
@router.get("/pythonVersion")
async def get_python_version():
"""Get the Python version"""
return system_actions.get_python_version()
@router.get("/configuration/pull")
async def pull_configuration():
"""Pull the configuration"""
action_result = system_actions.pull_repository_changes()
if action_result.status == 0:
return action_result.dict()
raise HTTPException(status_code=500, detail=action_result.dict())

View File

@ -0,0 +1,62 @@
"""Users management module"""
from typing import Optional
from fastapi import APIRouter, Body, Depends, HTTPException
from pydantic import BaseModel
import selfprivacy_api.actions.users as users_actions
from selfprivacy_api.dependencies import get_token_header
router = APIRouter(
prefix="/users",
tags=["users"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)
@router.get("")
async def get_users(withMainUser: bool = False):
"""Get the list of users"""
users: list[users_actions.UserDataUser] = users_actions.get_users(
exclude_primary=not withMainUser, exclude_root=True
)
return [user.username for user in users]
class UserInput(BaseModel):
"""User input"""
username: str
password: str
@router.post("", status_code=201)
async def create_user(user: UserInput):
try:
users_actions.create_user(user.username, user.password)
except users_actions.PasswordIsEmpty as e:
raise HTTPException(status_code=400, detail=str(e))
except users_actions.UsernameForbidden as e:
raise HTTPException(status_code=409, detail=str(e))
except users_actions.UsernameNotAlphanumeric as e:
raise HTTPException(status_code=400, detail=str(e))
except users_actions.UsernameTooLong as e:
raise HTTPException(status_code=400, detail=str(e))
except users_actions.UserAlreadyExists as e:
raise HTTPException(status_code=409, detail=str(e))
return {"result": 0, "username": user.username}
@router.delete("/{username}")
async def delete_user(username: str):
try:
users_actions.delete_user(username)
except users_actions.UserNotFound as e:
raise HTTPException(status_code=404, detail=str(e))
except users_actions.UserIsProtected as e:
raise HTTPException(status_code=400, detail=str(e))
return {"result": 0, "username": username}

View File

@ -1,8 +1,10 @@
"""Tasks for the restic controller."""
from huey import crontab
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.utils.huey import Huey
from . import ResticController, ResticStates
huey = Huey()
@huey.task()
def init_restic():

View File

@ -0,0 +1,163 @@
"""Class representing Bitwarden service"""
import base64
import subprocess
import typing
from selfprivacy_api.jobs import Job, JobStatus, Jobs
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.network import get_ip4
huey = Huey()
class Bitwarden(Service):
"""Class representing Bitwarden service."""
@staticmethod
def get_id() -> str:
"""Return service id."""
return "bitwarden"
@staticmethod
def get_display_name() -> str:
"""Return service display name."""
return "Bitwarden"
@staticmethod
def get_description() -> str:
"""Return service description."""
return "Bitwarden is a password manager."
@staticmethod
def get_svg_icon(self) -> str:
"""Read SVG icon from file and return it as base64 encoded string."""
with open("selfprivacy_api/services/bitwarden/bitwarden.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def is_movable() -> bool:
return True
@staticmethod
def is_required() -> bool:
return False
@staticmethod
def is_enabled() -> bool:
with ReadUserData() as user_data:
return user_data.get("bitwarden", {}).get("enable", False)
@staticmethod
def get_status() -> ServiceStatus:
"""
Return Bitwarden status from systemd.
Use command return code to determine status.
Return code 0 means service is running.
Return code 1 or 2 means service is in error stat.
Return code 3 means service is stopped.
Return code 4 means service is off.
"""
return get_service_status("vaultwarden.service")
@staticmethod
def enable():
"""Enable Bitwarden service."""
with WriteUserData() as user_data:
if "bitwarden" not in user_data:
user_data["bitwarden"] = {}
user_data["bitwarden"]["enable"] = True
@staticmethod
def disable():
"""Disable Bitwarden service."""
with WriteUserData() as user_data:
if "bitwarden" not in user_data:
user_data["bitwarden"] = {}
user_data["bitwarden"]["enable"] = False
@staticmethod
def stop():
subprocess.run(["systemctl", "stop", "vaultwarden.service"])
@staticmethod
def start():
subprocess.run(["systemctl", "start", "vaultwarden.service"])
@staticmethod
def restart():
subprocess.run(["systemctl", "restart", "vaultwarden.service"])
@staticmethod
def get_configuration():
return {}
@staticmethod
def set_configuration(config_items):
return super().set_configuration(config_items)
@staticmethod
def get_logs():
return ""
@staticmethod
def get_storage_usage() -> int:
storage_usage = 0
storage_usage += get_storage_usage("/var/lib/bitwarden")
storage_usage += get_storage_usage("/var/lib/bitwarden_rs")
return storage_usage
@staticmethod
def get_location() -> str:
with ReadUserData() as user_data:
if user_data.get("useBinds", False):
return user_data.get("bitwarden", {}).get("location", "sda1")
else:
return "sda1"
@staticmethod
def get_dns_records() -> typing.List[ServiceDnsRecord]:
"""Return list of DNS records for Bitwarden service."""
return [
ServiceDnsRecord(
type="A",
name="password",
content=get_ip4(),
ttl=3600,
),
]
def move_to_volume(self, volume: BlockDevice):
job = Jobs.get_instance().add(
name="services.bitwarden.move",
description=f"Moving Bitwarden data to {volume.name}",
)
move_service(
self,
volume,
job,
[
FolderMoveNames(
name="bitwarden",
bind_location="/var/lib/bitwarden",
group="vaultwarden",
owner="vaultwarden",
),
FolderMoveNames(
name="bitwarden",
bind_location="/var/lib/bitwarden_rs",
group="vaultwarden",
owner="vaultwarden",
),
],
"bitwarden",
)
return job

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.125 2C4.2962 2 3.50134 2.32924 2.91529 2.91529C2.32924 3.50134 2 4.2962 2 5.125L2 18.875C2 19.7038 2.32924 20.4987 2.91529 21.0847C3.50134 21.6708 4.2962 22 5.125 22H18.875C19.7038 22 20.4987 21.6708 21.0847 21.0847C21.6708 20.4987 22 19.7038 22 18.875V5.125C22 4.2962 21.6708 3.50134 21.0847 2.91529C20.4987 2.32924 19.7038 2 18.875 2H5.125ZM6.25833 4.43333H17.7583C17.9317 4.43333 18.0817 4.49667 18.2083 4.62333C18.2688 4.68133 18.3168 4.7511 18.3494 4.82835C18.3819 4.9056 18.3983 4.98869 18.3975 5.0725V12.7392C18.3975 13.3117 18.2858 13.8783 18.0633 14.4408C17.8558 14.9751 17.5769 15.4789 17.2342 15.9383C16.8824 16.3987 16.4882 16.825 16.0567 17.2117C15.6008 17.6242 15.18 17.9667 14.7942 18.24C14.4075 18.5125 14.005 18.77 13.5858 19.0133C13.1667 19.2558 12.8692 19.4208 12.6925 19.5075C12.5158 19.5942 12.375 19.6608 12.2675 19.7075C12.1872 19.7472 12.0987 19.7674 12.0092 19.7667C11.919 19.7674 11.8299 19.7468 11.7492 19.7067C11.6062 19.6429 11.4645 19.5762 11.3242 19.5067C11.0218 19.3511 10.7242 19.1866 10.4317 19.0133C10.0175 18.7738 9.6143 18.5158 9.22333 18.24C8.7825 17.9225 8.36093 17.5791 7.96083 17.2117C7.52907 16.825 7.13456 16.3987 6.7825 15.9383C6.44006 15.4788 6.16141 14.9751 5.95417 14.4408C5.73555 13.9 5.62213 13.3225 5.62 12.7392V5.0725C5.62 4.89917 5.68333 4.75 5.80917 4.6225C5.86726 4.56188 5.93717 4.51382 6.01457 4.48129C6.09196 4.44875 6.17521 4.43243 6.25917 4.43333H6.25833ZM12.0083 6.35V17.7C12.8 17.2817 13.5092 16.825 14.135 16.3333C15.6992 15.1083 16.4808 13.9108 16.4808 12.7392V6.35H12.0083Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,238 @@
"""Generic handler for moving services"""
import base64
import subprocess
import time
import typing
import pathlib
import shutil
from pydantic import BaseModel
from selfprivacy_api.jobs import Job, JobStatus, Jobs
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
huey = Huey()
class FolderMoveNames(BaseModel):
name: str
bind_location: str
owner: str
group: str
@huey.task()
def move_service(
service: Service,
volume: BlockDevice,
job: Job,
folder_names: list[FolderMoveNames],
userdata_location: str,
):
"""Move a service to another volume."""
job = Jobs.get_instance().update(
job=job,
status_text="Performing pre-move checks...",
status=JobStatus.RUNNING,
)
service_name = service.get_display_name()
with ReadUserData() as user_data:
if not user_data.get("useBinds", False):
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Server is not using binds.",
)
return
# Check if we are on the same volume
old_volume = service.get_location()
if old_volume == volume.name:
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error=f"{service_name} is already on this volume.",
)
return
# Check if there is enough space on the new volume
if volume.fsavail < service.get_storage_usage():
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Not enough space on the new volume.",
)
return
# Make sure the volume is mounted
if f"/volumes/{volume.name}" not in volume.mountpoints:
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Volume is not mounted.",
)
return
# Make sure current actual directory exists and if its user and group are correct
for folder in folder_names:
if not pathlib.Path(f"/volumes/{old_volume}/{folder.name}").exists():
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error=f"{service_name} is not found.",
)
return
if not pathlib.Path(f"/volumes/{old_volume}/{folder.name}").is_dir():
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error=f"{service_name} is not a directory.",
)
return
if (
not pathlib.Path(f"/volumes/{old_volume}/{folder.name}").owner()
== folder.owner
):
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error=f"{service_name} owner is not {folder.owner}.",
)
return
# Stop service
Jobs.get_instance().update(
job=job,
status=JobStatus.RUNNING,
status_text=f"Stopping {service_name}...",
progress=5,
)
service.stop()
# Wait for Nextcloud to stop, check every second
# If it does not stop in 30 seconds, abort
for _ in range(30):
if service.get_status() != ServiceStatus.RUNNING:
break
time.sleep(1)
else:
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error=f"{service_name} did not stop in 30 seconds.",
)
return
# Unmount old volume
Jobs.get_instance().update(
job=job,
status_text="Unmounting old folder...",
status=JobStatus.RUNNING,
progress=10,
)
for folder in folder_names:
try:
subprocess.run(
["umount", folder.bind_location],
check=True,
)
except subprocess.CalledProcessError:
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Unable to unmount old volume.",
)
return
# Move data to new volume and set correct permissions
Jobs.get_instance().update(
job=job,
status_text="Moving data to new volume...",
status=JobStatus.RUNNING,
progress=20,
)
current_progress = 20
folder_percentage = 50 / len(folder_names)
for folder in folder_names:
shutil.move(
f"/volumes/{old_volume}/{folder.name}",
f"/volumes/{volume.name}/{folder.name}",
)
Jobs.get_instance().update(
job=job,
status_text="Moving data to new volume...",
status=JobStatus.RUNNING,
progress=current_progress + folder_percentage,
)
Jobs.get_instance().update(
job=job,
status_text=f"Making sure {service_name} owns its files...",
status=JobStatus.RUNNING,
progress=70,
)
for folder in folder_names:
try:
subprocess.run(
[
"chown",
"-R",
f"{folder.owner}:f{folder.group}",
f"/volumes/{volume.name}/{folder.name}",
],
check=True,
)
except subprocess.CalledProcessError as error:
print(error.output)
Jobs.get_instance().update(
job=job,
status=JobStatus.RUNNING,
error=f"Unable to set ownership of new volume. {service_name} may not be able to access its files. Continuing anyway.",
)
return
# Mount new volume
Jobs.get_instance().update(
job=job,
status_text=f"Mounting {service_name} data...",
status=JobStatus.RUNNING,
progress=90,
)
for folder in folder_names:
try:
subprocess.run(
[
"mount",
"--bind",
f"/volumes/{volume.name}/{folder.name}",
folder.bind_location,
],
check=True,
)
except subprocess.CalledProcessError as error:
print(error.output)
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Unable to mount new volume.",
)
return
# Update userdata
Jobs.get_instance().update(
job=job,
status_text="Finishing move...",
status=JobStatus.RUNNING,
progress=95,
)
with WriteUserData() as user_data:
if userdata_location not in user_data:
user_data[userdata_location] = {}
user_data[userdata_location]["location"] = volume.name
# Start service
service.start()
Jobs.get_instance().update(
job=job,
status=JobStatus.FINISHED,
result=f"{service_name} moved successfully.",
status_text=f"Starting {service}...",
progress=100,
)

View File

@ -0,0 +1,16 @@
"""Generic size counter using pathlib"""
import pathlib
def get_storage_usage(path: str) -> int:
"""
Calculate the real storage usage of path and all subdirectories.
Calculate using pathlib.
Do not follow symlinks.
"""
storage_usage = 0
for iter_path in pathlib.Path(path).rglob("**/*"):
if iter_path.is_dir():
continue
storage_usage += iter_path.stat().st_size
return storage_usage

View File

@ -0,0 +1,29 @@
"""Generic service status fetcher using systemctl"""
import subprocess
import typing
from selfprivacy_api.services.service import ServiceStatus
def get_service_status(service: str) -> ServiceStatus:
"""
Return service status from systemd.
Use command return code to determine status.
Return code 0 means service is running.
Return code 1 or 2 means service is in error stat.
Return code 3 means service is stopped.
Return code 4 means service is off.
"""
service_status = subprocess.Popen(["systemctl", "status", service])
service_status.communicate()[0]
if service_status.returncode == 0:
return ServiceStatus.RUNNING
elif service_status.returncode == 1 or service_status.returncode == 2:
return ServiceStatus.ERROR
elif service_status.returncode == 3:
return ServiceStatus.STOPPED
elif service_status.returncode == 4:
return ServiceStatus.OFF
else:
return ServiceStatus.DEGRADED

View File

@ -0,0 +1,154 @@
"""Class representing Bitwarden service"""
import base64
import subprocess
import typing
from selfprivacy_api.jobs import Job, JobStatus, Jobs
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.network import get_ip4
huey = Huey()
class Gitea(Service):
"""Class representing Gitea service"""
@staticmethod
def get_id() -> str:
"""Return service id."""
return "gitea"
@staticmethod
def get_display_name() -> str:
"""Return service display name."""
return "Gitea"
@staticmethod
def get_description() -> str:
"""Return service description."""
return "Gitea is a Git forge."
@staticmethod
def get_svg_icon() -> str:
"""Read SVG icon from file and return it as base64 encoded string."""
with open("selfprivacy_api/services/gitea/gitea.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def is_movable() -> bool:
return True
@staticmethod
def is_required() -> bool:
return False
@staticmethod
def is_enabled() -> bool:
with ReadUserData() as user_data:
return user_data.get("gitea", {}).get("enable", False)
@staticmethod
def get_status() -> ServiceStatus:
"""
Return Gitea status from systemd.
Use command return code to determine status.
Return code 0 means service is running.
Return code 1 or 2 means service is in error stat.
Return code 3 means service is stopped.
Return code 4 means service is off.
"""
return get_service_status("gitea.service")
@staticmethod
def enable():
"""Enable Gitea service."""
with WriteUserData() as user_data:
if "gitea" not in user_data:
user_data["gitea"] = {}
user_data["gitea"]["enable"] = True
@staticmethod
def disable():
"""Disable Gitea service."""
with WriteUserData() as user_data:
if "gitea" not in user_data:
user_data["gitea"] = {}
user_data["gitea"]["enable"] = False
@staticmethod
def stop():
subprocess.run(["systemctl", "stop", "gitea.service"])
@staticmethod
def start():
subprocess.run(["systemctl", "start", "gitea.service"])
@staticmethod
def restart():
subprocess.run(["systemctl", "restart", "gitea.service"])
@staticmethod
def get_configuration():
return {}
@staticmethod
def set_configuration(config_items):
return super().set_configuration(config_items)
@staticmethod
def get_logs():
return ""
@staticmethod
def get_storage_usage() -> int:
storage_usage = 0
storage_usage += get_storage_usage("/var/lib/gitea")
return storage_usage
@staticmethod
def get_location() -> str:
with ReadUserData() as user_data:
if user_data.get("useBinds", False):
return user_data.get("gitea", {}).get("location", "sda1")
else:
return "sda1"
@staticmethod
def get_dns_records() -> typing.List[ServiceDnsRecord]:
return [
ServiceDnsRecord(
type="A",
name="git",
content=get_ip4(),
ttl=3600,
),
]
def move_to_volume(self, volume: BlockDevice):
job = Jobs.get_instance().add(
name="services.gitea.move",
description=f"Moving Gitea data to {volume.name}",
)
move_service(
self,
volume,
job,
[
FolderMoveNames(
name="gitea",
bind_location="/var/lib/gitea",
group="gitea",
owner="gitea",
),
],
"bitwarden",
)
return job

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.60007 10.5899L8.38007 4.79995L10.0701 6.49995C9.83007 7.34995 10.2201 8.27995 11.0001 8.72995V14.2699C10.4001 14.6099 10.0001 15.2599 10.0001 15.9999C10.0001 16.5304 10.2108 17.0391 10.5859 17.4142C10.9609 17.7892 11.4696 17.9999 12.0001 17.9999C12.5305 17.9999 13.0392 17.7892 13.4143 17.4142C13.7894 17.0391 14.0001 16.5304 14.0001 15.9999C14.0001 15.2599 13.6001 14.6099 13.0001 14.2699V9.40995L15.0701 11.4999C15.0001 11.6499 15.0001 11.8199 15.0001 11.9999C15.0001 12.5304 15.2108 13.0391 15.5859 13.4142C15.9609 13.7892 16.4696 13.9999 17.0001 13.9999C17.5305 13.9999 18.0392 13.7892 18.4143 13.4142C18.7894 13.0391 19.0001 12.5304 19.0001 11.9999C19.0001 11.4695 18.7894 10.9608 18.4143 10.5857C18.0392 10.2107 17.5305 9.99995 17.0001 9.99995C16.8201 9.99995 16.6501 9.99995 16.5001 10.0699L13.9301 7.49995C14.1901 6.56995 13.7101 5.54995 12.7801 5.15995C12.3501 4.99995 11.9001 4.95995 11.5001 5.06995L9.80007 3.37995L10.5901 2.59995C11.3701 1.80995 12.6301 1.80995 13.4101 2.59995L21.4001 10.5899C22.1901 11.3699 22.1901 12.6299 21.4001 13.4099L13.4101 21.3999C12.6301 22.1899 11.3701 22.1899 10.5901 21.3999L2.60007 13.4099C1.81007 12.6299 1.81007 11.3699 2.60007 10.5899Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,172 @@
"""Class representing Dovecot and Postfix services"""
import base64
import subprocess
import typing
from selfprivacy_api.jobs import Job, JobStatus, Jobs
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_dkim_key, get_domain
from selfprivacy_api.utils import huey
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.network import get_ip4
huey = Huey()
class MailServer(Service):
"""Class representing mail service"""
@staticmethod
def get_id() -> str:
return "mailserver"
@staticmethod
def get_display_name() -> str:
return "Mail Server"
@staticmethod
def get_description() -> str:
return "E-Mail for company and family."
@staticmethod
def get_svg_icon() -> str:
with open("selfprivacy_api/services/mailserver/mailserver.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def is_movable() -> bool:
return True
@staticmethod
def is_required() -> bool:
return True
@staticmethod
def is_enabled() -> bool:
return True
@staticmethod
def get_status() -> ServiceStatus:
imap_status = get_service_status("dovecot2.service")
smtp_status = get_service_status("postfix.service")
if (
imap_status == ServiceStatus.RUNNING
and smtp_status == ServiceStatus.RUNNING
):
return ServiceStatus.RUNNING
elif imap_status == ServiceStatus.ERROR or smtp_status == ServiceStatus.ERROR:
return ServiceStatus.ERROR
elif (
imap_status == ServiceStatus.STOPPED or smtp_status == ServiceStatus.STOPPED
):
return ServiceStatus.STOPPED
elif imap_status == ServiceStatus.OFF or smtp_status == ServiceStatus.OFF:
return ServiceStatus.OFF
else:
return ServiceStatus.DEGRADED
@staticmethod
def enable():
raise NotImplementedError("enable is not implemented for MailServer")
@staticmethod
def disable():
raise NotImplementedError("disable is not implemented for MailServer")
@staticmethod
def stop():
subprocess.run(["systemctl", "stop", "dovecot2.service"])
subprocess.run(["systemctl", "stop", "postfix.service"])
@staticmethod
def start():
subprocess.run(["systemctl", "start", "dovecot2.service"])
subprocess.run(["systemctl", "start", "postfix.service"])
@staticmethod
def restart():
subprocess.run(["systemctl", "restart", "dovecot2.service"])
subprocess.run(["systemctl", "restart", "postfix.service"])
@staticmethod
def get_configuration():
return {}
@staticmethod
def set_configuration(config_items):
return super().set_configuration(config_items)
@staticmethod
def get_logs():
return ""
@staticmethod
def get_storage_usage() -> int:
return get_storage_usage("/var/vmail")
@staticmethod
def get_location() -> str:
with ReadUserData() as user_data:
if user_data.get("useBinds", False):
return user_data.get("mailserver", {}).get("location", "sda1")
else:
return "sda1"
@staticmethod
def get_dns_records() -> typing.List[ServiceDnsRecord]:
dkim_record = get_dkim_key()
domain = get_domain()
ip4 = get_ip4()
if dkim_record is None:
return []
return [
ServiceDnsRecord(
type="MX", name=domain, data=domain, ttl=3600, priority=10
),
ServiceDnsRecord(
type="TXT", name="_dmarc", data=f"v=DMARC1; p=none", ttl=3600
),
ServiceDnsRecord(
type="TXT", name=domain, data=f"v=spf1 a mx ip4:{ip4} -all", ttl=3600
),
ServiceDnsRecord(
type="TXT", name="selector._domainkey", data=dkim_record, ttl=3600
),
]
def move_to_volume(self, volume: BlockDevice):
job = Jobs.get_instance().add(
name="services.mailserver.move",
description=f"Moving mailserver data to {volume.name}",
)
move_service(
self,
volume,
job,
[
FolderMoveNames(
name="vmail",
bind_location="/var/vmail",
group="virtualMail",
owner="virtualMail",
),
FolderMoveNames(
name="sieve",
bind_location="/var/sieve",
group="virtualMail",
owner="virtualMail",
),
],
"mailserver",
)
return job

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3333 2.66675H2.66665C1.93331 2.66675 1.33998 3.26675 1.33998 4.00008L1.33331 12.0001C1.33331 12.7334 1.93331 13.3334 2.66665 13.3334H13.3333C14.0666 13.3334 14.6666 12.7334 14.6666 12.0001V4.00008C14.6666 3.26675 14.0666 2.66675 13.3333 2.66675ZM13.3333 12.0001H2.66665V5.33341L7.99998 8.66675L13.3333 5.33341V12.0001ZM7.99998 7.33341L2.66665 4.00008H13.3333L7.99998 7.33341Z" fill="#201A19"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -1,42 +1,56 @@
"""Class representing Nextcloud service."""
import base64
import subprocess
import time
import typing
import psutil
import pathlib
import shutil
from selfprivacy_api.jobs import Job, JobStatus, Jobs
from selfprivacy_api.jobs import Jobs
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.utils.network import get_ip4
class Nextcloud(Service):
"""Class representing Nextcloud service."""
@staticmethod
def get_id(self) -> str:
"""Return service id."""
return "nextcloud"
def get_display_name(self) -> str:
@staticmethod
def get_display_name() -> str:
"""Return service display name."""
return "Nextcloud"
def get_description(self) -> str:
@staticmethod
def get_description() -> str:
"""Return service description."""
return "Nextcloud is a cloud storage service that offers a web interface and a desktop client."
def get_svg_icon(self) -> str:
@staticmethod
def get_svg_icon() -> str:
"""Read SVG icon from file and return it as base64 encoded string."""
with open("selfprivacy_api/services/nextcloud/nextcloud.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def is_enabled(self) -> bool:
@staticmethod
def is_movable() -> bool:
return True
@staticmethod
def is_required() -> bool:
return False
@staticmethod
def is_enabled() -> bool:
with ReadUserData() as user_data:
return user_data.get("nextcloud", {}).get("enable", False)
def get_status(self) -> ServiceStatus:
@staticmethod
def get_status() -> ServiceStatus:
"""
Return Nextcloud status from systemd.
Use command return code to determine status.
@ -46,72 +60,64 @@ class Nextcloud(Service):
Return code 3 means service is stopped.
Return code 4 means service is off.
"""
service_status = subprocess.Popen(
["systemctl", "status", "phpfpm-nextcloud.service"]
)
service_status.communicate()[0]
if service_status.returncode == 0:
return ServiceStatus.RUNNING
elif service_status.returncode == 1 or service_status.returncode == 2:
return ServiceStatus.ERROR
elif service_status.returncode == 3:
return ServiceStatus.STOPPED
elif service_status.returncode == 4:
return ServiceStatus.OFF
else:
return ServiceStatus.DEGRADED
return get_service_status("phpfpm-nextcloud.service")
def enable(self):
@staticmethod
def enable():
"""Enable Nextcloud service."""
with WriteUserData() as user_data:
if "nextcloud" not in user_data:
user_data["nextcloud"] = {}
user_data["nextcloud"]["enable"] = True
def disable(self):
@staticmethod
def disable():
"""Disable Nextcloud service."""
with WriteUserData() as user_data:
if "nextcloud" not in user_data:
user_data["nextcloud"] = {}
user_data["nextcloud"]["enable"] = False
def stop(self):
@staticmethod
def stop():
"""Stop Nextcloud service."""
subprocess.Popen(["systemctl", "stop", "phpfpm-nextcloud.service"])
def start(self):
@staticmethod
def start():
"""Start Nextcloud service."""
subprocess.Popen(["systemctl", "start", "phpfpm-nextcloud.service"])
def restart(self):
@staticmethod
def restart():
"""Restart Nextcloud service."""
subprocess.Popen(["systemctl", "restart", "phpfpm-nextcloud.service"])
def get_configuration(self) -> dict:
@staticmethod
def get_configuration() -> dict:
"""Return Nextcloud configuration."""
return {}
def set_configuration(self, config_items):
@staticmethod
def set_configuration(config_items):
return super().set_configuration(config_items)
def get_logs(self):
@staticmethod
def get_logs():
"""Return Nextcloud logs."""
return ""
def get_storage_usage(self) -> int:
@staticmethod
def get_storage_usage() -> int:
"""
Calculate the real storage usage of /var/lib/nextcloud and all subdirectories.
Calculate using pathlib.
Do not follow symlinks.
"""
storage_usage = 0
for path in pathlib.Path("/var/lib/nextcloud").rglob("**/*"):
if path.is_dir():
continue
storage_usage += path.stat().st_size
return storage_usage
return get_storage_usage("/var/lib/nextcloud")
def get_location(self) -> str:
@staticmethod
def get_location() -> str:
"""Get the name of disk where Nextcloud is installed."""
with ReadUserData() as user_data:
if user_data.get("useBinds", False):
@ -119,185 +125,34 @@ class Nextcloud(Service):
else:
return "sda1"
def get_dns_records(self) -> typing.List[ServiceDnsRecord]:
return super().get_dns_records()
@staticmethod
def get_dns_records() -> typing.List[ServiceDnsRecord]:
return [
ServiceDnsRecord(
type="A",
name="cloud",
content=get_ip4(),
ttl=3600,
),
]
def move_to_volume(self, volume: BlockDevice):
job = Jobs.get_instance().add(
name="services.nextcloud.move",
description=f"Moving Nextcloud to volume {volume.name}",
)
move_nextcloud(self, volume, job)
move_service(
self,
volume,
job,
[
FolderMoveNames(
name="nextcloud",
bind_location="/var/lib/nextcloud",
owner="nextcloud",
group="nextcloud",
),
],
"nextcloud",
)
return job
@huey.task()
def move_nextcloud(nextcloud: Nextcloud, volume: BlockDevice, job: Job):
"""Move Nextcloud to another volume."""
job = Jobs.get_instance().update(
job=job,
status_text="Performing pre-move checks...",
status=JobStatus.RUNNING,
)
with ReadUserData() as user_data:
if not user_data.get("useBinds", False):
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Server is not using binds.",
)
return
# Check if we are on the same volume
old_location = nextcloud.get_location()
if old_location == volume.name:
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Nextcloud is already on this volume.",
)
return
# Check if there is enough space on the new volume
if volume.fsavail < nextcloud.get_storage_usage():
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Not enough space on the new volume.",
)
return
# Make sure the volume is mounted
if f"/volumes/{volume.name}" not in volume.mountpoints:
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Volume is not mounted.",
)
return
# Make sure current actual directory exists
if not pathlib.Path(f"/volumes/{old_location}/nextcloud").exists():
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Nextcloud is not found.",
)
return
# Stop Nextcloud
Jobs.get_instance().update(
job=job,
status=JobStatus.RUNNING,
status_text="Stopping Nextcloud...",
progress=5,
)
nextcloud.stop()
# Wait for Nextcloud to stop, check every second
# If it does not stop in 30 seconds, abort
for _ in range(30):
if nextcloud.get_status() != ServiceStatus.RUNNING:
break
time.sleep(1)
else:
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Nextcloud did not stop in 30 seconds.",
)
return
# Unmount old volume
Jobs.get_instance().update(
job=job,
status_text="Unmounting old folder...",
status=JobStatus.RUNNING,
progress=10,
)
try:
subprocess.run(["umount", "/var/lib/nextcloud"], check=True)
except subprocess.CalledProcessError:
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Unable to unmount old volume.",
)
return
# Move data to new volume and set correct permissions
Jobs.get_instance().update(
job=job,
status_text="Moving data to new volume...",
status=JobStatus.RUNNING,
progress=20,
)
shutil.move(
f"/volumes/{old_location}/nextcloud", f"/volumes/{volume.name}/nextcloud"
)
Jobs.get_instance().update(
job=job,
status_text="Making sure Nextcloud owns its files...",
status=JobStatus.RUNNING,
progress=70,
)
try:
subprocess.run(
[
"chown",
"-R",
"nextcloud:nextcloud",
f"/volumes/{volume.name}/nextcloud",
],
check=True,
)
except subprocess.CalledProcessError as error:
print(error.output)
Jobs.get_instance().update(
job=job,
status=JobStatus.RUNNING,
error="Unable to set ownership of new volume. Nextcloud may not be able to access its files. Continuing anyway.",
)
return
# Mount new volume
Jobs.get_instance().update(
job=job,
status_text="Mounting Nextcloud data...",
status=JobStatus.RUNNING,
progress=90,
)
try:
subprocess.run(
[
"mount",
"--bind",
f"/volumes/{volume.name}/nextcloud",
"/var/lib/nextcloud",
],
check=True,
)
except subprocess.CalledProcessError as error:
print(error.output)
Jobs.get_instance().update(
job=job,
status=JobStatus.ERROR,
error="Unable to mount new volume.",
)
return
# Update userdata
Jobs.get_instance().update(
job=job,
status_text="Finishing move...",
status=JobStatus.RUNNING,
progress=95,
)
with WriteUserData() as user_data:
if "nextcloud" not in user_data:
user_data["nextcloud"] = {}
user_data["nextcloud"]["location"] = volume.name
# Start Nextcloud
nextcloud.start()
Jobs.get_instance().update(
job=job,
status=JobStatus.FINISHED,
result="Nextcloud moved successfully.",
status_text="Starting Nextcloud...",
progress=100,
)

View File

@ -0,0 +1,99 @@
"""Class representing ocserv service."""
import base64
import subprocess
import typing
from selfprivacy_api.jobs import Jobs
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.network import get_ip4
class Ocserv(Service):
"""Class representing ocserv service."""
@staticmethod
def get_id() -> str:
return "ocserv"
@staticmethod
def get_display_name() -> str:
return "OpenConnect VPN"
@staticmethod
def get_description() -> str:
return "OpenConnect VPN to connect your devices and access the internet."
@staticmethod
def get_svg_icon() -> str:
with open("selfprivacy_api/services/ocserv/ocserv.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def is_movable() -> bool:
return False
@staticmethod
def is_required() -> bool:
return False
@staticmethod
def is_enabled() -> bool:
with ReadUserData() as user_data:
return user_data.get("ocserv", {}).get("enable", False)
@staticmethod
def get_status() -> ServiceStatus:
return get_service_status("ocserv.service")
@staticmethod
def enable():
with WriteUserData() as user_data:
if "ocserv" not in user_data:
user_data["ocserv"] = {}
user_data["ocserv"]["enable"] = True
@staticmethod
def disable():
with WriteUserData() as user_data:
if "ocserv" not in user_data:
user_data["ocserv"] = {}
user_data["ocserv"]["enable"] = False
@staticmethod
def stop():
subprocess.run(["systemctl", "stop", "ocserv.service"])
@staticmethod
def start():
subprocess.run(["systemctl", "start", "ocserv.service"])
@staticmethod
def restart():
subprocess.run(["systemctl", "restart", "ocserv.service"])
@staticmethod
def get_configuration():
return {}
@staticmethod
def get_logs():
return ""
@staticmethod
def get_location() -> str:
return "sda1"
@staticmethod
def get_dns_records() -> typing.List[ServiceDnsRecord]:
return []
@staticmethod
def get_storage_usage() -> int:
return 0
def move_to_volume(self, volume: BlockDevice):
raise NotImplementedError("ocserv service is not movable")

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1L3 5V11C3 16.55 6.84 21.74 12 23C17.16 21.74 21 16.55 21 11V5L12 1ZM12 11.99H19C18.47 16.11 15.72 19.78 12 20.93V12H5V6.3L12 3.19V11.99Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@ -0,0 +1,144 @@
"""Class representing Nextcloud service."""
import base64
import subprocess
import typing
from selfprivacy_api.jobs import Jobs
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.network import get_ip4
class Pleroma(Service):
"""Class representing Pleroma service."""
@staticmethod
def get_id() -> str:
return "pleroma"
@staticmethod
def get_display_name() -> str:
return "Pleroma"
@staticmethod
def get_description() -> str:
return "Pleroma is a microblogging service that offers a web interface and a desktop client."
@staticmethod
def get_svg_icon() -> str:
with open("selfprivacy_api/services/pleroma/pleroma.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def is_movable() -> bool:
return True
@staticmethod
def is_required() -> bool:
return False
@staticmethod
def is_enabled() -> bool:
with ReadUserData() as user_data:
return user_data.get("pleroma", {}).get("enable", False)
@staticmethod
def get_status() -> ServiceStatus:
return get_service_status("pleroma.service")
@staticmethod
def enable():
with WriteUserData() as user_data:
if "pleroma" not in user_data:
user_data["pleroma"] = {}
user_data["pleroma"]["enable"] = True
@staticmethod
def disable():
with WriteUserData() as user_data:
if "pleroma" not in user_data:
user_data["pleroma"] = {}
user_data["pleroma"]["enable"] = False
@staticmethod
def stop():
subprocess.run(["systemctl", "stop", "pleroma.service"])
subprocess.run(["systemctl", "stop", "postgresql.service"])
@staticmethod
def start():
subprocess.run(["systemctl", "start", "pleroma.service"])
subprocess.run(["systemctl", "start", "postgresql.service"])
@staticmethod
def restart():
subprocess.run(["systemctl", "restart", "pleroma.service"])
subprocess.run(["systemctl", "restart", "postgresql.service"])
@staticmethod
def get_configuration(config_items):
return {}
@staticmethod
def set_configuration(config_items):
return super().set_configuration(config_items)
@staticmethod
def get_logs():
return ""
@staticmethod
def get_storage_usage() -> int:
storage_usage = 0
storage_usage += get_storage_usage("/var/lib/pleroma")
storage_usage += get_storage_usage("/var/lib/postgresql")
return storage_usage
@staticmethod
def get_location() -> str:
with ReadUserData() as user_data:
if user_data.get("useBinds", False):
return user_data.get("pleroma", {}).get("location", "sda1")
else:
return "sda1"
@staticmethod
def get_dns_records() -> typing.List[ServiceDnsRecord]:
return [
ServiceDnsRecord(
type="A",
name="social",
content=get_ip4(),
ttl=3600,
),
]
def move_to_volume(self, volume: BlockDevice):
job = Jobs.get_instance().add(
name="services.pleroma.move",
description=f"Moving Pleroma to volume {volume.name}",
)
move_service(
self,
volume,
job,
[
FolderMoveNames(
name="pleroma",
bind_location="/var/lib/pleroma",
owner="pleroma",
group="pleroma",
),
FolderMoveNames(
name="postgresql",
bind_location="/var/lib/postgresql",
owner="postgres",
group="postgres",
),
],
"pleroma",
)
return job

View File

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51106_4998)">
<path d="M6.35999 1.07076e-06C6.11451 -0.000261753 5.87139 0.0478616 5.64452 0.14162C5.41766 0.235378 5.21149 0.372932 5.03782 0.546418C4.86415 0.719904 4.72638 0.925919 4.63237 1.15269C4.53837 1.37945 4.48999 1.62252 4.48999 1.868V24H10.454V1.07076e-06H6.35999ZM13.473 1.07076e-06V12H17.641C18.1364 12 18.6115 11.8032 18.9619 11.4529C19.3122 11.1026 19.509 10.6274 19.509 10.132V1.07076e-06H13.473ZM13.473 18.036V24H17.641C18.1364 24 18.6115 23.8032 18.9619 23.4529C19.3122 23.1026 19.509 22.6274 19.509 22.132V18.036H13.473Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_51106_4998">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 794 B

View File

@ -46,6 +46,14 @@ class Service(ABC):
def get_svg_icon(self) -> str:
pass
@abstractmethod
def is_movable() -> bool:
pass
@abstractmethod
def is_required() -> bool:
pass
@abstractmethod
def is_enabled(self) -> bool:
pass

View File

@ -5,6 +5,7 @@ from datetime import datetime, timedelta
import re
import typing
from pydantic import BaseModel
from mnemonic import Mnemonic
from . import ReadUserData, UserDataFiles, WriteUserData, parse_date
@ -96,11 +97,22 @@ def get_token_name(token):
return None
class BasicTokenInfo(BaseModel):
"""Token info"""
name: str
date: datetime
def get_tokens_info():
"""Get all tokens info without tokens themselves"""
with ReadUserData(UserDataFiles.TOKENS) as tokens:
return [
{"name": token["name"], "date": token["date"]} for token in tokens["tokens"]
BasicTokenInfo(
name=t["name"],
date=parse_date(t["date"]),
)
for t in tokens["tokens"]
]

View File

@ -170,7 +170,7 @@ class BlockDevices:
"-J",
"-b",
"-o",
"NAME,PATH,FSAVAIL,FSSIZE,FSTYPE,FSUSED,MOUNTPOINTS,LABEL,UUID,SIZE,MODEL,SERIAL,TYPE",
"NAME,PATH,FSAVAIL,FSSIZE,FSTYPE,FSUSED,MOUNTPOINT,LABEL,UUID,SIZE,MODEL,SERIAL,TYPE",
]
)
lsblk_output = lsblk_output.decode("utf-8")

View File

@ -1,4 +1,16 @@
"""MiniHuey singleton."""
from huey.contrib.mini import MiniHuey
from huey import SqliteHuey
huey = MiniHuey()
HUEY_DATABASE = "/etc/nixos/userdata/tasks.db"
# Singleton instance containing the huey database.
class Huey:
"""Huey singleton."""
__instance = None
def __new__(cls):
"""Create a new instance of the huey singleton."""
if Huey.__instance is None:
Huey.__instance = SqliteHuey(HUEY_DATABASE)
return Huey.__instance

View File

@ -7,10 +7,12 @@ from selfprivacy_api.services.nextcloud import Nextcloud
from selfprivacy_api.utils import WriteUserData
from selfprivacy_api.utils.block_devices import BlockDevices
class BindMigrationConfig:
"""Config for bind migration.
For each service provide block device name.
"""
email_block_device: str
bitwarden_block_device: str
gitea_block_device: str
@ -23,7 +25,7 @@ def migrate_to_binds(config: BindMigrationConfig):
# Get block devices.
block_devices = BlockDevices().get_block_devices()
block_device_names = [ device.name for device in block_devices ]
block_device_names = [device.name for device in block_devices]
# Get all unique required block devices
required_block_devices = []
@ -80,7 +82,9 @@ def migrate_to_binds(config: BindMigrationConfig):
# Move data from /var/lib/nextcloud to /volumes/<block_device_name>/nextcloud.
# /var/lib/nextcloud is removed and /volumes/<block_device_name>/nextcloud is mounted as bind mount.
nextcloud_data_path = pathlib.Path("/var/lib/nextcloud")
nextcloud_bind_path = pathlib.Path(f"/volumes/{config.nextcloud_block_device}/nextcloud")
nextcloud_bind_path = pathlib.Path(
f"/volumes/{config.nextcloud_block_device}/nextcloud"
)
if nextcloud_data_path.exists():
shutil.move(str(nextcloud_data_path), str(nextcloud_bind_path))
else:
@ -94,10 +98,15 @@ def migrate_to_binds(config: BindMigrationConfig):
shutil.chown(nextcloud_data_path, user="nextcloud", group="nextcloud")
# Mount nextcloud bind mount.
subprocess.run(["mount","--bind", str(nextcloud_bind_path), str(nextcloud_data_path)], check=True)
subprocess.run(
["mount", "--bind", str(nextcloud_bind_path), str(nextcloud_data_path)],
check=True,
)
# Recursively chown all files in nextcloud bind mount.
subprocess.run(["chown", "-R", "nextcloud:nextcloud", str(nextcloud_data_path)], check=True)
subprocess.run(
["chown", "-R", "nextcloud:nextcloud", str(nextcloud_data_path)], check=True
)
# Start Nextcloud
Nextcloud().start()

View File

@ -2,9 +2,10 @@
"""Network utils"""
import subprocess
import re
from typing import Optional
def get_ip4():
def get_ip4() -> Optional[str]:
"""Get IPv4 address"""
try:
ip4 = subprocess.check_output(["ip", "addr", "show", "dev", "eth0"]).decode(
@ -16,7 +17,7 @@ def get_ip4():
return ip4.group(1) if ip4 else None
def get_ip6():
def get_ip6() -> Optional[str]:
"""Get IPv6 address"""
try:
ip6 = subprocess.check_output(["ip", "addr", "show", "dev", "eth0"]).decode(

View File

@ -1,12 +1,8 @@
{ pkgs ? import <nixpkgs> { } }:
let
sp-python = pkgs.python39.withPackages (p: with p; [
flask
flask-restful
setuptools
portalocker
flask-swagger
flask-swagger-ui
pytz
pytest
pytest-mock
@ -18,9 +14,10 @@ let
pylint
pydantic
typing-extensions
flask-cors
psutil
black
fastapi
uvicorn
(buildPythonPackage rec {
pname = "strawberry-graphql";
version = "0.123.0";
@ -32,11 +29,11 @@ let
typing-extensions
python-multipart
python-dateutil
flask
# flask
pydantic
pygments
poetry
flask-cors
# flask-cors
(buildPythonPackage rec {
pname = "graphql-core";
version = "3.2.0";

View File

@ -2,8 +2,7 @@
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
import pytest
from flask import testing
from selfprivacy_api.app import create_app
from fastapi.testclient import TestClient
@pytest.fixture
@ -16,66 +15,36 @@ def tokens_file(mocker, shared_datadir):
@pytest.fixture
def app():
"""Flask application."""
app = create_app(
{
"ENABLE_SWAGGER": "1",
}
def huey_database(mocker, shared_datadir):
"""Mock huey database."""
mock = mocker.patch(
"selfprivacy_api.utils.huey.HUEY_DATABASE", shared_datadir / "huey.db"
)
yield app
return mock
@pytest.fixture
def client(app, tokens_file):
"""Flask unauthorized test client."""
return app.test_client()
def client(tokens_file, huey_database):
from selfprivacy_api.app import app
class AuthorizedClient(testing.FlaskClient):
"""Flask authorized test client."""
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)
class WrongAuthClient(testing.FlaskClient):
"""Flask client with wrong token"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.token = "WRONG_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)
return TestClient(app)
@pytest.fixture
def authorized_client(app, tokens_file):
def authorized_client(tokens_file, huey_database):
"""Authorized test client fixture."""
app.test_client_class = AuthorizedClient
return app.test_client()
from selfprivacy_api.app import app
client = TestClient(app)
client.headers.update({"Authorization": "Bearer TEST_TOKEN"})
return client
@pytest.fixture
def wrong_auth_client(app, tokens_file):
def wrong_auth_client(tokens_file, huey_database):
"""Wrong token test client fixture."""
app.test_client_class = WrongAuthClient
return app.test_client()
from selfprivacy_api.app import app
@pytest.fixture
def runner(app, tokens_file):
"""Flask test runner."""
return app.test_cli_runner()
client = TestClient(app)
client.headers.update({"Authorization": "Bearer WRONG_TOKEN"})
return client

View File

@ -25,7 +25,7 @@ class NoFileMock(ProcessMock):
def mock_subproccess_popen(mocker):
mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock)
mocker.patch(
"selfprivacy_api.resources.services.mailserver.get_domain",
"selfprivacy_api.rest.services.get_domain",
autospec=True,
return_value="example.com",
)
@ -37,7 +37,7 @@ def mock_subproccess_popen(mocker):
def mock_no_file(mocker):
mock = mocker.patch("subprocess.Popen", autospec=True, return_value=NoFileMock)
mocker.patch(
"selfprivacy_api.resources.services.mailserver.get_domain",
"selfprivacy_api.rest.services.get_domain",
autospec=True,
return_value="example.com",
)
@ -67,7 +67,7 @@ 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 base64.b64decode(response.text) == b"I am a DKIM key"
assert mock_subproccess_popen.call_args[0][0] == [
"cat",
"/var/dkim/example.com.selector.txt",

View File

@ -43,7 +43,7 @@ class ResticControllerMock:
@pytest.fixture
def mock_restic_controller(mocker):
mock = mocker.patch(
"selfprivacy_api.resources.services.restic.ResticController",
"selfprivacy_api.rest.services.ResticController",
autospec=True,
return_value=ResticControllerMock,
)
@ -60,7 +60,7 @@ class ResticControllerMockNoKey:
@pytest.fixture
def mock_restic_controller_no_key(mocker):
mock = mocker.patch(
"selfprivacy_api.resources.services.restic.ResticController",
"selfprivacy_api.rest.services.ResticController",
autospec=True,
return_value=ResticControllerMockNoKey,
)
@ -77,7 +77,7 @@ class ResticControllerNotInitialized:
@pytest.fixture
def mock_restic_controller_not_initialized(mocker):
mock = mocker.patch(
"selfprivacy_api.resources.services.restic.ResticController",
"selfprivacy_api.rest.services.ResticController",
autospec=True,
return_value=ResticControllerNotInitialized,
)
@ -94,7 +94,7 @@ class ResticControllerInitializing:
@pytest.fixture
def mock_restic_controller_initializing(mocker):
mock = mocker.patch(
"selfprivacy_api.resources.services.restic.ResticController",
"selfprivacy_api.rest.services.ResticController",
autospec=True,
return_value=ResticControllerInitializing,
)
@ -111,7 +111,7 @@ class ResticControllerBackingUp:
@pytest.fixture
def mock_restic_controller_backing_up(mocker):
mock = mocker.patch(
"selfprivacy_api.resources.services.restic.ResticController",
"selfprivacy_api.rest.services.ResticController",
autospec=True,
return_value=ResticControllerBackingUp,
)
@ -128,7 +128,7 @@ class ResticControllerError:
@pytest.fixture
def mock_restic_controller_error(mocker):
mock = mocker.patch(
"selfprivacy_api.resources.services.restic.ResticController",
"selfprivacy_api.rest.services.ResticController",
autospec=True,
return_value=ResticControllerError,
)
@ -145,7 +145,7 @@ class ResticControllerRestoring:
@pytest.fixture
def mock_restic_controller_restoring(mocker):
mock = mocker.patch(
"selfprivacy_api.resources.services.restic.ResticController",
"selfprivacy_api.rest.services.ResticController",
autospec=True,
return_value=ResticControllerRestoring,
)
@ -154,9 +154,7 @@ def mock_restic_controller_restoring(mocker):
@pytest.fixture
def mock_restic_tasks(mocker):
mock = mocker.patch(
"selfprivacy_api.resources.services.restic.restic_tasks", autospec=True
)
mock = mocker.patch("selfprivacy_api.rest.services.restic_tasks", autospec=True)
return mock
@ -197,7 +195,7 @@ def test_get_snapshots_unauthorized(client, mock_restic_controller, mock_restic_
def test_get_snapshots(authorized_client, mock_restic_controller, mock_restic_tasks):
response = authorized_client.get("/services/restic/backup/list")
assert response.status_code == 200
assert response.get_json() == MOCKED_SNAPSHOTS
assert response.json() == MOCKED_SNAPSHOTS
def test_create_backup_unauthorized(client, mock_restic_controller, mock_restic_tasks):
@ -247,7 +245,7 @@ def test_check_backup_status(
):
response = authorized_client.get("/services/restic/backup/status")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"status": "INITIALIZED",
"progress": 0,
"error_message": None,
@ -259,7 +257,7 @@ def test_check_backup_status_no_key(
):
response = authorized_client.get("/services/restic/backup/status")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"status": "NO_KEY",
"progress": 0,
"error_message": None,
@ -271,7 +269,7 @@ def test_check_backup_status_not_initialized(
):
response = authorized_client.get("/services/restic/backup/status")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"status": "NOT_INITIALIZED",
"progress": 0,
"error_message": None,
@ -283,7 +281,7 @@ def test_check_backup_status_initializing(
):
response = authorized_client.get("/services/restic/backup/status")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"status": "INITIALIZING",
"progress": 0,
"error_message": None,
@ -295,7 +293,7 @@ def test_check_backup_status_backing_up(
):
response = authorized_client.get("/services/restic/backup/status")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"status": "BACKING_UP",
"progress": 0.42,
"error_message": None,
@ -307,7 +305,7 @@ def test_check_backup_status_error(
):
response = authorized_client.get("/services/restic/backup/status")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"status": "ERROR",
"progress": 0,
"error_message": "Error message",
@ -319,7 +317,7 @@ def test_check_backup_status_restoring(
):
response = authorized_client.get("/services/restic/backup/status")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"status": "RESTORING",
"progress": 0,
"error_message": None,
@ -346,7 +344,7 @@ def test_backup_restore_without_backup_id(
authorized_client, mock_restic_controller, mock_restic_tasks
):
response = authorized_client.put("/services/restic/backup/restore", json={})
assert response.status_code == 400
assert response.status_code == 422
assert mock_restic_tasks.restore_from_backup.call_count == 0
@ -440,7 +438,7 @@ def test_set_backblaze_config_without_arguments(
authorized_client, mock_restic_controller, mock_restic_tasks, some_settings
):
response = authorized_client.put("/services/restic/backblaze/config")
assert response.status_code == 400
assert response.status_code == 422
assert mock_restic_tasks.update_keys_from_userdata.call_count == 0
@ -451,7 +449,7 @@ def test_set_backblaze_config_without_all_values(
"/services/restic/backblaze/config",
json={"accountId": "123", "applicationKey": "456"},
)
assert response.status_code == 400
assert response.status_code == 422
assert mock_restic_tasks.update_keys_from_userdata.call_count == 0

View File

@ -9,7 +9,7 @@ def read_json(file_path):
def call_args_asserts(mocked_object):
assert mocked_object.call_count == 8
assert mocked_object.call_count == 7
assert mocked_object.call_args_list[0][0][0] == [
"systemctl",
"status",
@ -23,29 +23,24 @@ def call_args_asserts(mocked_object):
assert mocked_object.call_args_list[2][0][0] == [
"systemctl",
"status",
"nginx.service",
"vaultwarden.service",
]
assert mocked_object.call_args_list[3][0][0] == [
"systemctl",
"status",
"vaultwarden.service",
"gitea.service",
]
assert mocked_object.call_args_list[4][0][0] == [
"systemctl",
"status",
"gitea.service",
"phpfpm-nextcloud.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] == [
assert mocked_object.call_args_list[6][0][0] == [
"systemctl",
"status",
"pleroma.service",
@ -104,7 +99,7 @@ def test_illegal_methods(authorized_client, mock_subproccess_popen):
def test_dkim_key(authorized_client, mock_subproccess_popen):
response = authorized_client.get("/services/status")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"imap": 0,
"smtp": 0,
"http": 0,
@ -120,10 +115,10 @@ def test_dkim_key(authorized_client, 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() == {
assert response.json() == {
"imap": 3,
"smtp": 3,
"http": 3,
"http": 0,
"bitwarden": 3,
"gitea": 3,
"nextcloud": 3,

View File

@ -95,14 +95,18 @@ def some_users(mocker, datadir):
## TEST 401 ######################################################
@pytest.mark.parametrize(
"endpoint", ["ssh", "ssh/enable", "ssh/key/send", "ssh/keys/user"]
)
@pytest.mark.parametrize("endpoint", ["ssh/enable", "ssh/keys/user"])
def test_unauthorized(client, ssh_off, endpoint):
response = client.post(f"/services/{endpoint}")
assert response.status_code == 401
@pytest.mark.parametrize("endpoint", ["ssh", "ssh/key/send"])
def test_unauthorized_put(client, ssh_off, endpoint):
response = client.put(f"/services/{endpoint}")
assert response.status_code == 401
## TEST ENABLE ######################################################
@ -133,31 +137,31 @@ def test_legacy_enable_when_enabled(authorized_client, ssh_on):
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}
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}
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}
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}
assert response.json() == {"enable": True, "passwordAuthentication": True}
def test_get_current_settings_mostly_undefined(authorized_client, undefined_values):
response = authorized_client.get("/services/ssh")
assert response.status_code == 200
assert response.json == {"enable": True, "passwordAuthentication": True}
assert response.json() == {"enable": True, "passwordAuthentication": True}
## PUT ON /ssh ######################################################
@ -275,29 +279,22 @@ def test_add_invalid_root_key(authorized_client, ssh_on):
## /ssh/keys/{user} ######################################################
def test_add_root_key_via_wrong_endpoint(authorized_client, ssh_on):
response = authorized_client.post(
"/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("/services/ssh/keys/root")
assert response.status_code == 200
assert response.json == ["ssh-ed25519 KEY test@pc"]
assert response.json() == ["ssh-ed25519 KEY test@pc"]
def test_get_root_key_when_none(authorized_client, ssh_on):
response = authorized_client.get("/services/ssh/keys/root")
assert response.status_code == 200
assert response.json == []
assert response.json() == []
def test_get_root_key_on_undefined(authorized_client, undefined_settings):
response = authorized_client.get("/services/ssh/keys/root")
assert response.status_code == 200
assert response.json == []
assert response.json() == []
def test_delete_root_key(authorized_client, root_and_admin_have_keys):
@ -310,6 +307,10 @@ def test_delete_root_key(authorized_client, root_and_admin_have_keys):
not in read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")[
"ssh"
]
or read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][
"rootKeys"
]
== []
)
@ -330,19 +331,19 @@ def test_delete_root_key_on_undefined(authorized_client, undefined_settings):
"/services/ssh/keys/root", json={"public_key": "ssh-ed25519 KEY test@pc"}
)
assert response.status_code == 404
assert read_json(undefined_settings / "undefined.json")["ssh"]["rootKeys"] == []
assert "ssh" not in read_json(undefined_settings / "undefined.json")
def test_get_admin_key(authorized_client, root_and_admin_have_keys):
response = authorized_client.get("/services/ssh/keys/tester")
assert response.status_code == 200
assert response.json == ["ssh-rsa KEY test@pc"]
assert response.json() == ["ssh-rsa KEY test@pc"]
def test_get_admin_key_when_none(authorized_client, ssh_on):
response = authorized_client.get("/services/ssh/keys/tester")
assert response.status_code == 200
assert response.json == []
assert response.json() == []
def test_delete_admin_key(authorized_client, root_and_admin_have_keys):
@ -371,7 +372,7 @@ def test_delete_admin_key_on_undefined(authorized_client, undefined_settings):
"/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"}
)
assert response.status_code == 404
assert read_json(undefined_settings / "undefined.json")["sshKeys"] == []
assert "sshKeys" not in read_json(undefined_settings / "undefined.json")
def test_add_admin_key(authorized_client, ssh_on):
@ -418,9 +419,9 @@ def test_get_user_key(authorized_client, some_users, user):
response = authorized_client.get(f"/services/ssh/keys/user{user}")
assert response.status_code == 200
if user == 1:
assert response.json == ["ssh-rsa KEY user1@pc"]
assert response.json() == ["ssh-rsa KEY user1@pc"]
else:
assert response.json == []
assert response.json() == []
def test_get_keys_of_nonexistent_user(authorized_client, some_users):
@ -483,7 +484,13 @@ def test_delete_nonexistent_user_key(authorized_client, some_users, user):
f"/services/ssh/keys/user{user}", json={"public_key": "ssh-rsa KEY user1@pc"}
)
assert response.status_code == 404
assert read_json(some_users / "some_users.json")["users"][user - 1]["sshKeys"] == []
if user == 2:
assert (
read_json(some_users / "some_users.json")["users"][user - 1]["sshKeys"]
== []
)
if user == 3:
"sshKeys" not in read_json(some_users / "some_users.json")["users"][user - 1]
def test_add_keys_of_nonexistent_user(authorized_client, some_users):

View File

@ -36,11 +36,11 @@ DATE_FORMATS = [
def test_get_tokens_info(authorized_client, tokens_file):
response = authorized_client.get("/auth/tokens")
assert response.status_code == 200
assert response.json == [
{"name": "test_token", "date": "2022-01-14 08:31:10.789314", "is_caller": True},
assert response.json() == [
{"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True},
{
"name": "test_token2",
"date": "2022-01-14 08:31:10.789314",
"date": "2022-01-14T08:31:10.789314",
"is_caller": False,
},
]
@ -98,7 +98,7 @@ def test_refresh_token_unauthorized(client, tokens_file):
def test_refresh_token(authorized_client, tokens_file):
response = authorized_client.post("/auth/tokens")
assert response.status_code == 200
new_token = response.json["token"]
new_token = response.json()["token"]
assert read_json(tokens_file)["tokens"][0]["token"] == new_token
@ -106,7 +106,7 @@ def test_refresh_token(authorized_client, tokens_file):
def test_get_new_device_auth_token_unauthorized(client, tokens_file):
response = client.get("/auth/new_device")
response = client.post("/auth/new_device")
assert response.status_code == 401
assert read_json(tokens_file) == TOKENS_FILE_CONTETS
@ -114,19 +114,19 @@ def test_get_new_device_auth_token_unauthorized(client, tokens_file):
def test_get_new_device_auth_token(authorized_client, tokens_file):
response = authorized_client.post("/auth/new_device")
assert response.status_code == 200
assert "token" in response.json
token = Mnemonic(language="english").to_entropy(response.json["token"]).hex()
assert "token" in response.json()
token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex()
assert read_json(tokens_file)["new_device"]["token"] == token
def test_get_and_delete_new_device_token(authorized_client, tokens_file):
response = authorized_client.post("/auth/new_device")
assert response.status_code == 200
assert "token" in response.json
token = Mnemonic(language="english").to_entropy(response.json["token"]).hex()
assert "token" in response.json()
token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex()
assert read_json(tokens_file)["new_device"]["token"] == token
response = authorized_client.delete(
"/auth/new_device", json={"token": response.json["token"]}
"/auth/new_device", json={"token": response.json()["token"]}
)
assert response.status_code == 200
assert read_json(tokens_file) == TOKENS_FILE_CONTETS
@ -141,15 +141,15 @@ def test_delete_token_unauthenticated(client, tokens_file):
def test_get_and_authorize_new_device(client, authorized_client, tokens_file):
response = authorized_client.post("/auth/new_device")
assert response.status_code == 200
assert "token" in response.json
token = Mnemonic(language="english").to_entropy(response.json["token"]).hex()
assert "token" in response.json()
token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex()
assert read_json(tokens_file)["new_device"]["token"] == token
response = client.post(
"/auth/new_device/authorize",
json={"token": response.json["token"], "device": "new_device"},
json={"token": response.json()["token"], "device": "new_device"},
)
assert response.status_code == 200
assert read_json(tokens_file)["tokens"][2]["token"] == response.json["token"]
assert read_json(tokens_file)["tokens"][2]["token"] == response.json()["token"]
assert read_json(tokens_file)["tokens"][2]["name"] == "new_device"
@ -165,19 +165,19 @@ def test_authorize_new_device_with_invalid_token(client, tokens_file):
def test_get_and_authorize_used_token(client, authorized_client, tokens_file):
response = authorized_client.post("/auth/new_device")
assert response.status_code == 200
assert "token" in response.json
token = Mnemonic(language="english").to_entropy(response.json["token"]).hex()
assert "token" in response.json()
token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex()
assert read_json(tokens_file)["new_device"]["token"] == token
response = client.post(
"/auth/new_device/authorize",
json={"token": response.json["token"], "device": "new_device"},
json={"token": response.json()["token"], "device": "new_device"},
)
assert response.status_code == 200
assert read_json(tokens_file)["tokens"][2]["token"] == response.json["token"]
assert read_json(tokens_file)["tokens"][2]["token"] == response.json()["token"]
assert read_json(tokens_file)["tokens"][2]["name"] == "new_device"
response = client.post(
"/auth/new_device/authorize",
json={"token": response.json["token"], "device": "new_device"},
json={"token": response.json()["token"], "device": "new_device"},
)
assert response.status_code == 404
@ -187,8 +187,8 @@ def test_get_and_authorize_token_after_12_minutes(
):
response = authorized_client.post("/auth/new_device")
assert response.status_code == 200
assert "token" in response.json
token = Mnemonic(language="english").to_entropy(response.json["token"]).hex()
assert "token" in response.json()
token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex()
assert read_json(tokens_file)["new_device"]["token"] == token
file_data = read_json(tokens_file)
@ -199,7 +199,7 @@ def test_get_and_authorize_token_after_12_minutes(
response = client.post(
"/auth/new_device/authorize",
json={"token": response.json["token"], "device": "new_device"},
json={"token": response.json()["token"], "device": "new_device"},
)
assert response.status_code == 404
@ -209,7 +209,7 @@ def test_authorize_without_token(client, tokens_file):
"/auth/new_device/authorize",
json={"device": "new_device"},
)
assert response.status_code == 400
assert response.status_code == 422
assert read_json(tokens_file) == TOKENS_FILE_CONTETS
@ -245,7 +245,7 @@ def test_get_recovery_token_status_unauthorized(client, tokens_file):
def test_get_recovery_token_when_none_exists(authorized_client, tokens_file):
response = authorized_client.get("/auth/recovery_token")
assert response.status_code == 200
assert response.json == {
assert response.json() == {
"exists": False,
"valid": False,
"date": None,
@ -259,8 +259,8 @@ def test_generate_recovery_token(authorized_client, client, tokens_file):
# Generate token without expiration and uses_left
response = authorized_client.post("/auth/recovery_token")
assert response.status_code == 200
assert "token" in response.json
mnemonic_token = response.json["token"]
assert "token" in response.json()
mnemonic_token = response.json()["token"]
token = Mnemonic(language="english").to_entropy(mnemonic_token).hex()
assert read_json(tokens_file)["recovery_token"]["token"] == token
@ -274,9 +274,9 @@ def test_generate_recovery_token(authorized_client, client, tokens_file):
)
# Try to get token status
response = client.get("/auth/recovery_token")
response = authorized_client.get("/auth/recovery_token")
assert response.status_code == 200
assert response.json == {
assert response.json() == {
"exists": True,
"valid": True,
"date": time_generated,
@ -290,7 +290,7 @@ def test_generate_recovery_token(authorized_client, client, tokens_file):
json={"token": mnemonic_token, "device": "recovery_device"},
)
assert recovery_response.status_code == 200
new_token = recovery_response.json["token"]
new_token = recovery_response.json()["token"]
assert read_json(tokens_file)["tokens"][2]["token"] == new_token
assert read_json(tokens_file)["tokens"][2]["name"] == "recovery_device"
@ -300,7 +300,7 @@ def test_generate_recovery_token(authorized_client, client, tokens_file):
json={"token": mnemonic_token, "device": "recovery_device2"},
)
assert recovery_response.status_code == 200
new_token = recovery_response.json["token"]
new_token = recovery_response.json()["token"]
assert read_json(tokens_file)["tokens"][3]["token"] == new_token
assert read_json(tokens_file)["tokens"][3]["name"] == "recovery_device2"
@ -318,8 +318,8 @@ def test_generate_recovery_token_with_expiration_date(
json={"expiration": expiration_date_str},
)
assert response.status_code == 200
assert "token" in response.json
mnemonic_token = response.json["token"]
assert "token" in response.json()
mnemonic_token = response.json()["token"]
token = Mnemonic(language="english").to_entropy(mnemonic_token).hex()
assert read_json(tokens_file)["recovery_token"]["token"] == token
assert datetime.datetime.strptime(
@ -336,9 +336,9 @@ def test_generate_recovery_token_with_expiration_date(
)
# Try to get token status
response = client.get("/auth/recovery_token")
response = authorized_client.get("/auth/recovery_token")
assert response.status_code == 200
assert response.json == {
assert response.json() == {
"exists": True,
"valid": True,
"date": time_generated,
@ -352,7 +352,7 @@ def test_generate_recovery_token_with_expiration_date(
json={"token": mnemonic_token, "device": "recovery_device"},
)
assert recovery_response.status_code == 200
new_token = recovery_response.json["token"]
new_token = recovery_response.json()["token"]
assert read_json(tokens_file)["tokens"][2]["token"] == new_token
assert read_json(tokens_file)["tokens"][2]["name"] == "recovery_device"
@ -362,7 +362,7 @@ def test_generate_recovery_token_with_expiration_date(
json={"token": mnemonic_token, "device": "recovery_device2"},
)
assert recovery_response.status_code == 200
new_token = recovery_response.json["token"]
new_token = recovery_response.json()["token"]
assert read_json(tokens_file)["tokens"][3]["token"] == new_token
assert read_json(tokens_file)["tokens"][3]["name"] == "recovery_device2"
@ -381,9 +381,9 @@ def test_generate_recovery_token_with_expiration_date(
assert read_json(tokens_file)["tokens"] == new_data["tokens"]
# Get the status of the token
response = client.get("/auth/recovery_token")
response = authorized_client.get("/auth/recovery_token")
assert response.status_code == 200
assert response.json == {
assert response.json() == {
"exists": True,
"valid": False,
"date": time_generated,
@ -397,7 +397,7 @@ def test_generate_recovery_token_with_expiration_in_the_past(
authorized_client, tokens_file, timeformat
):
# Server must return 400 if expiration date is in the past
expiration_date = datetime.datetime.now() - datetime.timedelta(minutes=5)
expiration_date = datetime.datetime.utcnow() - datetime.timedelta(minutes=5)
expiration_date_str = expiration_date.strftime(timeformat)
response = authorized_client.post(
"/auth/recovery_token",
@ -416,7 +416,7 @@ def test_generate_recovery_token_with_invalid_time_format(
"/auth/recovery_token",
json={"expiration": expiration_date},
)
assert response.status_code == 400
assert response.status_code == 422
assert "recovery_token" not in read_json(tokens_file)
@ -429,8 +429,8 @@ def test_generate_recovery_token_with_limited_uses(
json={"uses": 2},
)
assert response.status_code == 200
assert "token" in response.json
mnemonic_token = response.json["token"]
assert "token" in response.json()
mnemonic_token = response.json()["token"]
token = Mnemonic(language="english").to_entropy(mnemonic_token).hex()
assert read_json(tokens_file)["recovery_token"]["token"] == token
assert read_json(tokens_file)["recovery_token"]["uses_left"] == 2
@ -445,9 +445,9 @@ def test_generate_recovery_token_with_limited_uses(
)
# Try to get token status
response = client.get("/auth/recovery_token")
response = authorized_client.get("/auth/recovery_token")
assert response.status_code == 200
assert response.json == {
assert response.json() == {
"exists": True,
"valid": True,
"date": time_generated,
@ -461,16 +461,16 @@ def test_generate_recovery_token_with_limited_uses(
json={"token": mnemonic_token, "device": "recovery_device"},
)
assert recovery_response.status_code == 200
new_token = recovery_response.json["token"]
new_token = recovery_response.json()["token"]
assert read_json(tokens_file)["tokens"][2]["token"] == new_token
assert read_json(tokens_file)["tokens"][2]["name"] == "recovery_device"
assert read_json(tokens_file)["recovery_token"]["uses_left"] == 1
# Get the status of the token
response = client.get("/auth/recovery_token")
response = authorized_client.get("/auth/recovery_token")
assert response.status_code == 200
assert response.json == {
assert response.json() == {
"exists": True,
"valid": True,
"date": time_generated,
@ -484,14 +484,14 @@ def test_generate_recovery_token_with_limited_uses(
json={"token": mnemonic_token, "device": "recovery_device2"},
)
assert recovery_response.status_code == 200
new_token = recovery_response.json["token"]
new_token = recovery_response.json()["token"]
assert read_json(tokens_file)["tokens"][3]["token"] == new_token
assert read_json(tokens_file)["tokens"][3]["name"] == "recovery_device2"
# Get the status of the token
response = client.get("/auth/recovery_token")
response = authorized_client.get("/auth/recovery_token")
assert response.status_code == 200
assert response.json == {
assert response.json() == {
"exists": True,
"valid": False,
"date": time_generated,

View File

@ -9,19 +9,13 @@ from selfprivacy_api.utils import WriteUserData, ReadUserData
def test_get_api_version(authorized_client):
response = authorized_client.get("/api/version")
assert response.status_code == 200
assert "version" in response.get_json()
assert "version" in response.json()
def test_get_api_version_unauthorized(client):
response = client.get("/api/version")
assert response.status_code == 200
assert "version" in response.get_json()
def test_get_swagger_json(authorized_client):
response = authorized_client.get("/api/swagger.json")
assert response.status_code == 200
assert "swagger" in response.get_json()
assert "version" in response.json()
def test_read_invalid_user_data():

View File

@ -144,7 +144,7 @@ def test_graphql_get_python_version_wrong_auth(
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_get_python_version(authorized_client, mock_subprocess_check_output):
@ -156,8 +156,8 @@ def test_graphql_get_python_version(authorized_client, mock_subprocess_check_out
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["system"]["info"]["pythonVersion"] == "Testing Linux"
assert response.json().get("data") is not None
assert response.json()["data"]["system"]["info"]["pythonVersion"] == "Testing Linux"
assert mock_subprocess_check_output.call_count == 1
assert mock_subprocess_check_output.call_args[0][0] == ["python", "-V"]
@ -181,7 +181,7 @@ def test_graphql_get_system_version_unauthorized(
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
assert mock_subprocess_check_output.call_count == 0
@ -196,9 +196,9 @@ def test_graphql_get_system_version(authorized_client, mock_subprocess_check_out
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["system"]["info"]["systemVersion"] == "Testing Linux"
assert response.json()["data"]["system"]["info"]["systemVersion"] == "Testing Linux"
assert mock_subprocess_check_output.call_count == 1
assert mock_subprocess_check_output.call_args[0][0] == ["uname", "-a"]
@ -258,11 +258,13 @@ def test_graphql_get_domain(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["system"]["domainInfo"]["domain"] == "test.tld"
assert response.json["data"]["system"]["domainInfo"]["hostname"] == "test-instance"
assert response.json["data"]["system"]["domainInfo"]["provider"] == "HETZNER"
dns_records = response.json["data"]["system"]["domainInfo"]["requiredDnsRecords"]
assert response.json().get("data") is not None
assert response.json()["data"]["system"]["domainInfo"]["domain"] == "test.tld"
assert (
response.json()["data"]["system"]["domainInfo"]["hostname"] == "test-instance"
)
assert response.json()["data"]["system"]["domainInfo"]["provider"] == "HETZNER"
dns_records = response.json()["data"]["system"]["domainInfo"]["requiredDnsRecords"]
assert is_dns_record_in_array(dns_records, dns_record())
assert is_dns_record_in_array(dns_records, dns_record(type="AAAA"))
assert is_dns_record_in_array(dns_records, dns_record(name="api.test.tld"))
@ -339,7 +341,7 @@ def test_graphql_get_timezone_unauthorized(client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_get_timezone(authorized_client, turned_on):
@ -351,8 +353,8 @@ def test_graphql_get_timezone(authorized_client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["system"]["settings"]["timezone"] == "Europe/Moscow"
assert response.json().get("data") is not None
assert response.json()["data"]["system"]["settings"]["timezone"] == "Europe/Moscow"
def test_graphql_get_timezone_on_undefined(authorized_client, undefined_config):
@ -364,8 +366,10 @@ def test_graphql_get_timezone_on_undefined(authorized_client, undefined_config):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["system"]["settings"]["timezone"] == "Europe/Uzhgorod"
assert response.json().get("data") is not None
assert (
response.json()["data"]["system"]["settings"]["timezone"] == "Europe/Uzhgorod"
)
API_CHANGE_TIMEZONE_MUTATION = """
@ -392,7 +396,7 @@ def test_graphql_change_timezone_unauthorized(client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_change_timezone(authorized_client, turned_on):
@ -407,11 +411,11 @@ def test_graphql_change_timezone(authorized_client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeTimezone"]["success"] is True
assert response.json["data"]["changeTimezone"]["message"] is not None
assert response.json["data"]["changeTimezone"]["code"] == 200
assert response.json["data"]["changeTimezone"]["timezone"] == "Europe/Helsinki"
assert response.json().get("data") is not None
assert response.json()["data"]["changeTimezone"]["success"] is True
assert response.json()["data"]["changeTimezone"]["message"] is not None
assert response.json()["data"]["changeTimezone"]["code"] == 200
assert response.json()["data"]["changeTimezone"]["timezone"] == "Europe/Helsinki"
assert read_json(turned_on / "turned_on.json")["timezone"] == "Europe/Helsinki"
@ -427,11 +431,11 @@ def test_graphql_change_timezone_on_undefined(authorized_client, undefined_confi
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeTimezone"]["success"] is True
assert response.json["data"]["changeTimezone"]["message"] is not None
assert response.json["data"]["changeTimezone"]["code"] == 200
assert response.json["data"]["changeTimezone"]["timezone"] == "Europe/Helsinki"
assert response.json().get("data") is not None
assert response.json()["data"]["changeTimezone"]["success"] is True
assert response.json()["data"]["changeTimezone"]["message"] is not None
assert response.json()["data"]["changeTimezone"]["code"] == 200
assert response.json()["data"]["changeTimezone"]["timezone"] == "Europe/Helsinki"
assert (
read_json(undefined_config / "undefined.json")["timezone"] == "Europe/Helsinki"
)
@ -449,11 +453,11 @@ def test_graphql_change_timezone_without_timezone(authorized_client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeTimezone"]["success"] is False
assert response.json["data"]["changeTimezone"]["message"] is not None
assert response.json["data"]["changeTimezone"]["code"] == 400
assert response.json["data"]["changeTimezone"]["timezone"] is None
assert response.json().get("data") is not None
assert response.json()["data"]["changeTimezone"]["success"] is False
assert response.json()["data"]["changeTimezone"]["message"] is not None
assert response.json()["data"]["changeTimezone"]["code"] == 400
assert response.json()["data"]["changeTimezone"]["timezone"] is None
assert read_json(turned_on / "turned_on.json")["timezone"] == "Europe/Moscow"
@ -469,11 +473,11 @@ def test_graphql_change_timezone_with_invalid_timezone(authorized_client, turned
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeTimezone"]["success"] is False
assert response.json["data"]["changeTimezone"]["message"] is not None
assert response.json["data"]["changeTimezone"]["code"] == 400
assert response.json["data"]["changeTimezone"]["timezone"] is None
assert response.json().get("data") is not None
assert response.json()["data"]["changeTimezone"]["success"] is False
assert response.json()["data"]["changeTimezone"]["message"] is not None
assert response.json()["data"]["changeTimezone"]["code"] == 400
assert response.json()["data"]["changeTimezone"]["timezone"] is None
assert read_json(turned_on / "turned_on.json")["timezone"] == "Europe/Moscow"
@ -496,7 +500,7 @@ def test_graphql_get_auto_upgrade_unauthorized(client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_get_auto_upgrade(authorized_client, turned_on):
@ -508,9 +512,11 @@ def test_graphql_get_auto_upgrade(authorized_client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["settings"]["autoUpgrade"]["enableAutoUpgrade"] is True
assert response.json["data"]["settings"]["autoUpgrade"]["allowReboot"] is True
assert response.json().get("data") is not None
assert (
response.json()["data"]["settings"]["autoUpgrade"]["enableAutoUpgrade"] is True
)
assert response.json()["data"]["settings"]["autoUpgrade"]["allowReboot"] is True
def test_graphql_get_auto_upgrade_on_undefined(authorized_client, undefined_config):
@ -522,9 +528,11 @@ def test_graphql_get_auto_upgrade_on_undefined(authorized_client, undefined_conf
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["settings"]["autoUpgrade"]["enableAutoUpgrade"] is True
assert response.json["data"]["settings"]["autoUpgrade"]["allowReboot"] is False
assert response.json().get("data") is not None
assert (
response.json()["data"]["settings"]["autoUpgrade"]["enableAutoUpgrade"] is True
)
assert response.json()["data"]["settings"]["autoUpgrade"]["allowReboot"] is False
def test_graphql_get_auto_upgrade_without_vlaues(authorized_client, no_values):
@ -536,9 +544,11 @@ def test_graphql_get_auto_upgrade_without_vlaues(authorized_client, no_values):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["settings"]["autoUpgrade"]["enableAutoUpgrade"] is True
assert response.json["data"]["settings"]["autoUpgrade"]["allowReboot"] is False
assert response.json().get("data") is not None
assert (
response.json()["data"]["settings"]["autoUpgrade"]["enableAutoUpgrade"] is True
)
assert response.json()["data"]["settings"]["autoUpgrade"]["allowReboot"] is False
def test_graphql_get_auto_upgrade_turned_off(authorized_client, turned_off):
@ -550,11 +560,11 @@ def test_graphql_get_auto_upgrade_turned_off(authorized_client, turned_off):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert (
response.json["data"]["settings"]["autoUpgrade"]["enableAutoUpgrade"] is False
response.json()["data"]["settings"]["autoUpgrade"]["enableAutoUpgrade"] is False
)
assert response.json["data"]["settings"]["autoUpgrade"]["allowReboot"] is False
assert response.json()["data"]["settings"]["autoUpgrade"]["allowReboot"] is False
API_CHANGE_AUTO_UPGRADE_SETTINGS = """
@ -585,7 +595,7 @@ def test_graphql_change_auto_upgrade_unauthorized(client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_change_auto_upgrade(authorized_client, turned_on):
@ -603,14 +613,15 @@ def test_graphql_change_auto_upgrade(authorized_client, turned_on):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert (
response.json["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"] is False
response.json()["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"]
is False
)
assert response.json["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert read_json(turned_on / "turned_on.json")["autoUpgrade"]["enable"] is False
assert read_json(turned_on / "turned_on.json")["autoUpgrade"]["allowReboot"] is True
@ -630,14 +641,15 @@ def test_graphql_change_auto_upgrade_on_undefined(authorized_client, undefined_c
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert (
response.json["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"] is False
response.json()["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"]
is False
)
assert response.json["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert (
read_json(undefined_config / "undefined.json")["autoUpgrade"]["enable"] is False
)
@ -662,14 +674,15 @@ def test_graphql_change_auto_upgrade_without_vlaues(authorized_client, no_values
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert (
response.json["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"] is True
response.json()["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"]
is True
)
assert response.json["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert read_json(no_values / "no_values.json")["autoUpgrade"]["enable"] is True
assert read_json(no_values / "no_values.json")["autoUpgrade"]["allowReboot"] is True
@ -689,14 +702,15 @@ def test_graphql_change_auto_upgrade_turned_off(authorized_client, turned_off):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert (
response.json["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"] is True
response.json()["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"]
is True
)
assert response.json["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert read_json(turned_off / "turned_off.json")["autoUpgrade"]["enable"] is True
assert (
read_json(turned_off / "turned_off.json")["autoUpgrade"]["allowReboot"] is True
@ -717,14 +731,15 @@ def test_grphql_change_auto_upgrade_without_enable(authorized_client, turned_off
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert (
response.json["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"] is False
response.json()["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"]
is False
)
assert response.json["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["allowReboot"] is True
assert read_json(turned_off / "turned_off.json")["autoUpgrade"]["enable"] is False
assert (
read_json(turned_off / "turned_off.json")["autoUpgrade"]["allowReboot"] is True
@ -747,14 +762,15 @@ def test_graphql_change_auto_upgrade_without_allow_reboot(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert (
response.json["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"] is True
response.json()["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"]
is True
)
assert response.json["data"]["changeAutoUpgradeSettings"]["allowReboot"] is False
assert response.json()["data"]["changeAutoUpgradeSettings"]["allowReboot"] is False
assert read_json(turned_off / "turned_off.json")["autoUpgrade"]["enable"] is True
assert (
read_json(turned_off / "turned_off.json")["autoUpgrade"]["allowReboot"] is False
@ -773,14 +789,15 @@ def test_graphql_change_auto_upgrade_with_empty_input(authorized_client, turned_
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["success"] is True
assert response.json()["data"]["changeAutoUpgradeSettings"]["message"] is not None
assert response.json()["data"]["changeAutoUpgradeSettings"]["code"] == 200
assert (
response.json["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"] is False
response.json()["data"]["changeAutoUpgradeSettings"]["enableAutoUpgrade"]
is False
)
assert response.json["data"]["changeAutoUpgradeSettings"]["allowReboot"] is False
assert response.json()["data"]["changeAutoUpgradeSettings"]["allowReboot"] is False
assert read_json(turned_off / "turned_off.json")["autoUpgrade"]["enable"] is False
assert (
read_json(turned_off / "turned_off.json")["autoUpgrade"]["allowReboot"] is False
@ -807,7 +824,7 @@ def test_graphql_pull_system_configuration_unauthorized(client, mock_subprocess_
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
assert mock_subprocess_popen.call_count == 0
@ -823,10 +840,10 @@ def test_graphql_pull_system_configuration(
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["pullRepositoryChanges"]["success"] is True
assert response.json["data"]["pullRepositoryChanges"]["message"] is not None
assert response.json["data"]["pullRepositoryChanges"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["pullRepositoryChanges"]["success"] is True
assert response.json()["data"]["pullRepositoryChanges"]["message"] is not None
assert response.json()["data"]["pullRepositoryChanges"]["code"] == 200
assert mock_subprocess_popen.call_count == 1
assert mock_subprocess_popen.call_args[0][0] == ["git", "pull"]
@ -848,10 +865,10 @@ def test_graphql_pull_system_broken_repo(
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["pullRepositoryChanges"]["success"] is False
assert response.json["data"]["pullRepositoryChanges"]["message"] is not None
assert response.json["data"]["pullRepositoryChanges"]["code"] == 500
assert response.json().get("data") is not None
assert response.json()["data"]["pullRepositoryChanges"]["success"] is False
assert response.json()["data"]["pullRepositoryChanges"]["message"] is not None
assert response.json()["data"]["pullRepositoryChanges"]["code"] == 500
assert mock_broken_service.call_count == 1
assert mock_os_chdir.call_count == 2

View File

@ -24,7 +24,7 @@ TOKENS_FILE_CONTETS = {
def test_graphql_get_entire_api_data(authorized_client, tokens_file):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={
"query": generate_api_query(
@ -33,25 +33,25 @@ def test_graphql_get_entire_api_data(authorized_client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert "version" in response.get_json()["data"]["api"]
assert response.json["data"]["api"]["devices"] is not None
assert len(response.json["data"]["api"]["devices"]) == 2
assert response.json().get("data") is not None
assert "version" in response.json()["data"]["api"]
assert response.json()["data"]["api"]["devices"] is not None
assert len(response.json()["data"]["api"]["devices"]) == 2
assert (
response.json["data"]["api"]["devices"][0]["creationDate"]
response.json()["data"]["api"]["devices"][0]["creationDate"]
== "2022-01-14T08:31:10.789314"
)
assert response.json["data"]["api"]["devices"][0]["isCaller"] is True
assert response.json["data"]["api"]["devices"][0]["name"] == "test_token"
assert response.json()["data"]["api"]["devices"][0]["isCaller"] is True
assert response.json()["data"]["api"]["devices"][0]["name"] == "test_token"
assert (
response.json["data"]["api"]["devices"][1]["creationDate"]
response.json()["data"]["api"]["devices"][1]["creationDate"]
== "2022-01-14T08:31:10.789314"
)
assert response.json["data"]["api"]["devices"][1]["isCaller"] is False
assert response.json["data"]["api"]["devices"][1]["name"] == "test_token2"
assert response.json["data"]["api"]["recoveryKey"] is not None
assert response.json["data"]["api"]["recoveryKey"]["exists"] is False
assert response.json["data"]["api"]["recoveryKey"]["valid"] is False
assert response.json["data"]["api"]["recoveryKey"]["creationDate"] is None
assert response.json["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json["data"]["api"]["recoveryKey"]["usesLeft"] is None
assert response.json()["data"]["api"]["devices"][1]["isCaller"] is False
assert response.json()["data"]["api"]["devices"][1]["name"] == "test_token2"
assert response.json()["data"]["api"]["recoveryKey"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["exists"] is False
assert response.json()["data"]["api"]["recoveryKey"]["valid"] is False
assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is None
assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None

View File

@ -31,35 +31,35 @@ devices {
def test_graphql_tokens_info(authorized_client, tokens_file):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_DEVICES_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["api"]["devices"] is not None
assert len(response.json["data"]["api"]["devices"]) == 2
assert response.json().get("data") is not None
assert response.json()["data"]["api"]["devices"] is not None
assert len(response.json()["data"]["api"]["devices"]) == 2
assert (
response.json["data"]["api"]["devices"][0]["creationDate"]
response.json()["data"]["api"]["devices"][0]["creationDate"]
== "2022-01-14T08:31:10.789314"
)
assert response.json["data"]["api"]["devices"][0]["isCaller"] is True
assert response.json["data"]["api"]["devices"][0]["name"] == "test_token"
assert response.json()["data"]["api"]["devices"][0]["isCaller"] is True
assert response.json()["data"]["api"]["devices"][0]["name"] == "test_token"
assert (
response.json["data"]["api"]["devices"][1]["creationDate"]
response.json()["data"]["api"]["devices"][1]["creationDate"]
== "2022-01-14T08:31:10.789314"
)
assert response.json["data"]["api"]["devices"][1]["isCaller"] is False
assert response.json["data"]["api"]["devices"][1]["name"] == "test_token2"
assert response.json()["data"]["api"]["devices"][1]["isCaller"] is False
assert response.json()["data"]["api"]["devices"][1]["name"] == "test_token2"
def test_graphql_tokens_info_unauthorized(client, tokens_file):
response = client.get(
response = client.post(
"/graphql",
json={"query": generate_api_query([API_DEVICES_QUERY])},
)
assert response.status_code == 200
assert response.json["data"] is None
assert response.json()["data"] is None
DELETE_TOKEN_MUTATION = """
@ -84,7 +84,7 @@ def test_graphql_delete_token_unauthorized(client, tokens_file):
},
)
assert response.status_code == 200
assert response.json["data"] is None
assert response.json()["data"] is None
def test_graphql_delete_token(authorized_client, tokens_file):
@ -98,10 +98,10 @@ def test_graphql_delete_token(authorized_client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["deleteDeviceApiToken"]["success"] is True
assert response.json["data"]["deleteDeviceApiToken"]["message"] is not None
assert response.json["data"]["deleteDeviceApiToken"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["deleteDeviceApiToken"]["success"] is True
assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None
assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 200
assert read_json(tokens_file) == {
"tokens": [
{
@ -124,10 +124,10 @@ def test_graphql_delete_self_token(authorized_client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["deleteDeviceApiToken"]["success"] is False
assert response.json["data"]["deleteDeviceApiToken"]["message"] is not None
assert response.json["data"]["deleteDeviceApiToken"]["code"] == 400
assert response.json().get("data") is not None
assert response.json()["data"]["deleteDeviceApiToken"]["success"] is False
assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None
assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 400
assert read_json(tokens_file) == TOKENS_FILE_CONTETS
@ -142,10 +142,10 @@ def test_graphql_delete_nonexistent_token(authorized_client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["deleteDeviceApiToken"]["success"] is False
assert response.json["data"]["deleteDeviceApiToken"]["message"] is not None
assert response.json["data"]["deleteDeviceApiToken"]["code"] == 404
assert response.json().get("data") is not None
assert response.json()["data"]["deleteDeviceApiToken"]["success"] is False
assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None
assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 404
assert read_json(tokens_file) == TOKENS_FILE_CONTETS
@ -167,7 +167,7 @@ def test_graphql_refresh_token_unauthorized(client, tokens_file):
json={"query": REFRESH_TOKEN_MUTATION},
)
assert response.status_code == 200
assert response.json["data"] is None
assert response.json()["data"] is None
def test_graphql_refresh_token(authorized_client, tokens_file):
@ -176,12 +176,12 @@ def test_graphql_refresh_token(authorized_client, tokens_file):
json={"query": REFRESH_TOKEN_MUTATION},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["refreshDeviceApiToken"]["success"] is True
assert response.json["data"]["refreshDeviceApiToken"]["message"] is not None
assert response.json["data"]["refreshDeviceApiToken"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["refreshDeviceApiToken"]["success"] is True
assert response.json()["data"]["refreshDeviceApiToken"]["message"] is not None
assert response.json()["data"]["refreshDeviceApiToken"]["code"] == 200
assert read_json(tokens_file)["tokens"][0] == {
"token": response.json["data"]["refreshDeviceApiToken"]["token"],
"token": response.json()["data"]["refreshDeviceApiToken"]["token"],
"name": "test_token",
"date": "2022-01-14 08:31:10.789314",
}
@ -205,7 +205,7 @@ def test_graphql_get_new_device_auth_key_unauthorized(client, tokens_file):
json={"query": NEW_DEVICE_KEY_MUTATION},
)
assert response.status_code == 200
assert response.json["data"] is None
assert response.json()["data"] is None
def test_graphql_get_new_device_auth_key(authorized_client, tokens_file):
@ -214,14 +214,16 @@ def test_graphql_get_new_device_auth_key(authorized_client, tokens_file):
json={"query": NEW_DEVICE_KEY_MUTATION},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["getNewDeviceApiKey"]["code"] == 200
assert response.json["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12
assert response.json().get("data") is not None
assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200
assert (
response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12
)
token = (
Mnemonic(language="english")
.to_entropy(response.json["data"]["getNewDeviceApiKey"]["key"])
.to_entropy(response.json()["data"]["getNewDeviceApiKey"]["key"])
.hex()
)
assert read_json(tokens_file)["new_device"]["token"] == token
@ -249,7 +251,7 @@ def test_graphql_invalidate_new_device_token_unauthorized(client, tokens_file):
},
)
assert response.status_code == 200
assert response.json["data"] is None
assert response.json()["data"] is None
def test_graphql_get_and_delete_new_device_key(authorized_client, tokens_file):
@ -258,14 +260,16 @@ def test_graphql_get_and_delete_new_device_key(authorized_client, tokens_file):
json={"query": NEW_DEVICE_KEY_MUTATION},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["getNewDeviceApiKey"]["code"] == 200
assert response.json["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12
assert response.json().get("data") is not None
assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200
assert (
response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12
)
token = (
Mnemonic(language="english")
.to_entropy(response.json["data"]["getNewDeviceApiKey"]["key"])
.to_entropy(response.json()["data"]["getNewDeviceApiKey"]["key"])
.hex()
)
assert read_json(tokens_file)["new_device"]["token"] == token
@ -274,10 +278,10 @@ def test_graphql_get_and_delete_new_device_key(authorized_client, tokens_file):
json={"query": INVALIDATE_NEW_DEVICE_KEY_MUTATION},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["invalidateNewDeviceApiKey"]["success"] is True
assert response.json["data"]["invalidateNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["invalidateNewDeviceApiKey"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["invalidateNewDeviceApiKey"]["success"] is True
assert response.json()["data"]["invalidateNewDeviceApiKey"]["message"] is not None
assert response.json()["data"]["invalidateNewDeviceApiKey"]["code"] == 200
assert read_json(tokens_file) == TOKENS_FILE_CONTETS
@ -299,11 +303,11 @@ def test_graphql_get_and_authorize_new_device(client, authorized_client, tokens_
json={"query": NEW_DEVICE_KEY_MUTATION},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["getNewDeviceApiKey"]["code"] == 200
mnemonic_key = response.json["data"]["getNewDeviceApiKey"]["key"]
assert response.json().get("data") is not None
assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200
mnemonic_key = response.json()["data"]["getNewDeviceApiKey"]["key"]
assert mnemonic_key.split(" ").__len__() == 12
key = Mnemonic(language="english").to_entropy(mnemonic_key).hex()
assert read_json(tokens_file)["new_device"]["token"] == key
@ -320,11 +324,13 @@ def test_graphql_get_and_authorize_new_device(client, authorized_client, tokens_
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["success"] is True
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["code"] == 200
token = response.json["data"]["authorizeWithNewDeviceApiKey"]["token"]
assert response.json().get("data") is not None
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is True
assert (
response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
)
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 200
token = response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"]
assert read_json(tokens_file)["tokens"][2]["token"] == token
assert read_json(tokens_file)["tokens"][2]["name"] == "new_device"
@ -343,10 +349,12 @@ def test_graphql_authorize_new_device_with_invalid_key(client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["success"] is False
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404
assert response.json().get("data") is not None
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is False
assert (
response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
)
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404
assert read_json(tokens_file) == TOKENS_FILE_CONTETS
@ -356,11 +364,11 @@ def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_fi
json={"query": NEW_DEVICE_KEY_MUTATION},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["getNewDeviceApiKey"]["code"] == 200
mnemonic_key = response.json["data"]["getNewDeviceApiKey"]["key"]
assert response.json().get("data") is not None
assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200
mnemonic_key = response.json()["data"]["getNewDeviceApiKey"]["key"]
assert mnemonic_key.split(" ").__len__() == 12
key = Mnemonic(language="english").to_entropy(mnemonic_key).hex()
assert read_json(tokens_file)["new_device"]["token"] == key
@ -377,13 +385,15 @@ def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_fi
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["success"] is True
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is True
assert (
response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
)
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 200
assert (
read_json(tokens_file)["tokens"][2]["token"]
== response.json["data"]["authorizeWithNewDeviceApiKey"]["token"]
== response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"]
)
assert read_json(tokens_file)["tokens"][2]["name"] == "new_token"
@ -400,10 +410,12 @@ def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_fi
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["success"] is False
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404
assert response.json().get("data") is not None
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is False
assert (
response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
)
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404
assert read_json(tokens_file)["tokens"].__len__() == 3
@ -415,14 +427,16 @@ def test_graphql_get_and_authorize_key_after_12_minutes(
json={"query": NEW_DEVICE_KEY_MUTATION},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["getNewDeviceApiKey"]["code"] == 200
assert response.json["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12
assert response.json().get("data") is not None
assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True
assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None
assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200
assert (
response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12
)
key = (
Mnemonic(language="english")
.to_entropy(response.json["data"]["getNewDeviceApiKey"]["key"])
.to_entropy(response.json()["data"]["getNewDeviceApiKey"]["key"])
.hex()
)
assert read_json(tokens_file)["new_device"]["token"] == key
@ -446,10 +460,12 @@ def test_graphql_get_and_authorize_key_after_12_minutes(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["success"] is False
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
assert response.json["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404
assert response.json().get("data") is not None
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is False
assert (
response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None
)
assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404
def test_graphql_authorize_without_token(client, tokens_file):
@ -465,4 +481,4 @@ def test_graphql_authorize_without_token(client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None

View File

@ -37,22 +37,22 @@ def test_graphql_recovery_key_status_unauthorized(client, tokens_file):
json={"query": generate_api_query([API_RECOVERY_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_recovery_key_status_when_none_exists(authorized_client, tokens_file):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_RECOVERY_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["api"]["recoveryKey"] is not None
assert response.json["data"]["api"]["recoveryKey"]["exists"] is False
assert response.json["data"]["api"]["recoveryKey"]["valid"] is False
assert response.json["data"]["api"]["recoveryKey"]["creationDate"] is None
assert response.json["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json["data"]["api"]["recoveryKey"]["usesLeft"] is None
assert response.json().get("data") is not None
assert response.json()["data"]["api"]["recoveryKey"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["exists"] is False
assert response.json()["data"]["api"]["recoveryKey"]["valid"] is False
assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is None
assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None
API_RECOVERY_KEY_GENERATE_MUTATION = """
@ -86,18 +86,19 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewRecoveryApiKey"]["success"] is True
assert response.json["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json["data"]["getNewRecoveryApiKey"]["code"] == 200
assert response.json["data"]["getNewRecoveryApiKey"]["key"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is True
assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is not None
assert (
response.json["data"]["getNewRecoveryApiKey"]["key"].split(" ").__len__() == 18
response.json()["data"]["getNewRecoveryApiKey"]["key"].split(" ").__len__()
== 18
)
assert read_json(tokens_file)["recovery_token"] is not None
time_generated = read_json(tokens_file)["recovery_token"]["date"]
assert time_generated is not None
key = response.json["data"]["getNewRecoveryApiKey"]["key"]
key = response.json()["data"]["getNewRecoveryApiKey"]["key"]
assert (
datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f")
- datetime.timedelta(seconds=5)
@ -105,20 +106,20 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file):
)
# Try to get token status
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_RECOVERY_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["api"]["recoveryKey"] is not None
assert response.json["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json["data"]["api"]["recoveryKey"]["valid"] is True
assert response.json["data"]["api"]["recoveryKey"][
assert response.json().get("data") is not None
assert response.json()["data"]["api"]["recoveryKey"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json()["data"]["api"]["recoveryKey"]["valid"] is True
assert response.json()["data"]["api"]["recoveryKey"][
"creationDate"
] == time_generated.replace("Z", "")
assert response.json["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json["data"]["api"]["recoveryKey"]["usesLeft"] is None
assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None
# Try to use token
response = client.post(
@ -134,13 +135,13 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["useRecoveryApiKey"]["success"] is True
assert response.json["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json["data"]["useRecoveryApiKey"]["token"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["useRecoveryApiKey"]["success"] is True
assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None
assert (
response.json["data"]["useRecoveryApiKey"]["token"]
response.json()["data"]["useRecoveryApiKey"]["token"]
== read_json(tokens_file)["tokens"][2]["token"]
)
assert read_json(tokens_file)["tokens"][2]["name"] == "new_test_token"
@ -159,13 +160,13 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["useRecoveryApiKey"]["success"] is True
assert response.json["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json["data"]["useRecoveryApiKey"]["token"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["useRecoveryApiKey"]["success"] is True
assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None
assert (
response.json["data"]["useRecoveryApiKey"]["token"]
response.json()["data"]["useRecoveryApiKey"]["token"]
== read_json(tokens_file)["tokens"][3]["token"]
)
assert read_json(tokens_file)["tokens"][3]["name"] == "new_test_token2"
@ -188,17 +189,18 @@ def test_graphql_generate_recovery_key_with_expiration_date(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewRecoveryApiKey"]["success"] is True
assert response.json["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json["data"]["getNewRecoveryApiKey"]["code"] == 200
assert response.json["data"]["getNewRecoveryApiKey"]["key"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is True
assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is not None
assert (
response.json["data"]["getNewRecoveryApiKey"]["key"].split(" ").__len__() == 18
response.json()["data"]["getNewRecoveryApiKey"]["key"].split(" ").__len__()
== 18
)
assert read_json(tokens_file)["recovery_token"] is not None
key = response.json["data"]["getNewRecoveryApiKey"]["key"]
key = response.json()["data"]["getNewRecoveryApiKey"]["key"]
assert read_json(tokens_file)["recovery_token"]["expiration"] == expiration_date_str
assert read_json(tokens_file)["recovery_token"]["token"] == mnemonic_to_hex(key)
@ -211,23 +213,23 @@ def test_graphql_generate_recovery_key_with_expiration_date(
)
# Try to get token status
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_RECOVERY_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["api"]["recoveryKey"] is not None
assert response.json["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json["data"]["api"]["recoveryKey"]["valid"] is True
assert response.json["data"]["api"]["recoveryKey"][
assert response.json().get("data") is not None
assert response.json()["data"]["api"]["recoveryKey"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json()["data"]["api"]["recoveryKey"]["valid"] is True
assert response.json()["data"]["api"]["recoveryKey"][
"creationDate"
] == time_generated.replace("Z", "")
assert (
response.json["data"]["api"]["recoveryKey"]["expirationDate"]
response.json()["data"]["api"]["recoveryKey"]["expirationDate"]
== expiration_date_str
)
assert response.json["data"]["api"]["recoveryKey"]["usesLeft"] is None
assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None
# Try to use token
response = authorized_client.post(
@ -243,13 +245,13 @@ def test_graphql_generate_recovery_key_with_expiration_date(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["useRecoveryApiKey"]["success"] is True
assert response.json["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json["data"]["useRecoveryApiKey"]["token"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["useRecoveryApiKey"]["success"] is True
assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None
assert (
response.json["data"]["useRecoveryApiKey"]["token"]
response.json()["data"]["useRecoveryApiKey"]["token"]
== read_json(tokens_file)["tokens"][2]["token"]
)
@ -267,13 +269,13 @@ def test_graphql_generate_recovery_key_with_expiration_date(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["useRecoveryApiKey"]["success"] is True
assert response.json["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json["data"]["useRecoveryApiKey"]["token"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["useRecoveryApiKey"]["success"] is True
assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None
assert (
response.json["data"]["useRecoveryApiKey"]["token"]
response.json()["data"]["useRecoveryApiKey"]["token"]
== read_json(tokens_file)["tokens"][3]["token"]
)
@ -296,30 +298,32 @@ def test_graphql_generate_recovery_key_with_expiration_date(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["useRecoveryApiKey"]["success"] is False
assert response.json["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json["data"]["useRecoveryApiKey"]["code"] == 404
assert response.json["data"]["useRecoveryApiKey"]["token"] is None
assert response.json().get("data") is not None
assert response.json()["data"]["useRecoveryApiKey"]["success"] is False
assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["useRecoveryApiKey"]["code"] == 404
assert response.json()["data"]["useRecoveryApiKey"]["token"] is None
assert read_json(tokens_file)["tokens"] == new_data["tokens"]
# Try to get token status
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_RECOVERY_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["api"]["recoveryKey"] is not None
assert response.json["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json["data"]["api"]["recoveryKey"]["valid"] is False
assert response.json["data"]["api"]["recoveryKey"]["creationDate"] == time_generated
assert response.json().get("data") is not None
assert response.json()["data"]["api"]["recoveryKey"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json()["data"]["api"]["recoveryKey"]["valid"] is False
assert (
response.json["data"]["api"]["recoveryKey"]["expirationDate"]
response.json()["data"]["api"]["recoveryKey"]["creationDate"] == time_generated
)
assert (
response.json()["data"]["api"]["recoveryKey"]["expirationDate"]
== new_data["recovery_token"]["expiration"]
)
assert response.json["data"]["api"]["recoveryKey"]["usesLeft"] is None
assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None
def test_graphql_generate_recovery_key_with_expiration_in_the_past(
@ -340,11 +344,11 @@ def test_graphql_generate_recovery_key_with_expiration_in_the_past(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewRecoveryApiKey"]["success"] is False
assert response.json["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json["data"]["getNewRecoveryApiKey"]["code"] == 400
assert response.json["data"]["getNewRecoveryApiKey"]["key"] is None
assert response.json().get("data") is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is False
assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 400
assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None
assert "recovery_token" not in read_json(tokens_file)
@ -366,7 +370,7 @@ def test_graphql_generate_recovery_key_with_invalid_time_format(
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
assert "recovery_token" not in read_json(tokens_file)
@ -388,31 +392,31 @@ def test_graphql_generate_recovery_key_with_limited_uses(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewRecoveryApiKey"]["success"] is True
assert response.json["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json["data"]["getNewRecoveryApiKey"]["code"] == 200
assert response.json["data"]["getNewRecoveryApiKey"]["key"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is True
assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is not None
mnemonic_key = response.json["data"]["getNewRecoveryApiKey"]["key"]
mnemonic_key = response.json()["data"]["getNewRecoveryApiKey"]["key"]
key = mnemonic_to_hex(mnemonic_key)
assert read_json(tokens_file)["recovery_token"]["token"] == key
assert read_json(tokens_file)["recovery_token"]["uses_left"] == 2
# Try to get token status
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_RECOVERY_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["api"]["recoveryKey"] is not None
assert response.json["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json["data"]["api"]["recoveryKey"]["valid"] is True
assert response.json["data"]["api"]["recoveryKey"]["creationDate"] is not None
assert response.json["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json["data"]["api"]["recoveryKey"]["usesLeft"] == 2
assert response.json().get("data") is not None
assert response.json()["data"]["api"]["recoveryKey"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json()["data"]["api"]["recoveryKey"]["valid"] is True
assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] == 2
# Try to use token
response = authorized_client.post(
@ -428,25 +432,25 @@ def test_graphql_generate_recovery_key_with_limited_uses(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["useRecoveryApiKey"]["success"] is True
assert response.json["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json["data"]["useRecoveryApiKey"]["token"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["useRecoveryApiKey"]["success"] is True
assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None
# Try to get token status
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_RECOVERY_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["api"]["recoveryKey"] is not None
assert response.json["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json["data"]["api"]["recoveryKey"]["valid"] is True
assert response.json["data"]["api"]["recoveryKey"]["creationDate"] is not None
assert response.json["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json["data"]["api"]["recoveryKey"]["usesLeft"] == 1
assert response.json().get("data") is not None
assert response.json()["data"]["api"]["recoveryKey"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json()["data"]["api"]["recoveryKey"]["valid"] is True
assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] == 1
# Try to use token
response = authorized_client.post(
@ -462,25 +466,25 @@ def test_graphql_generate_recovery_key_with_limited_uses(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["useRecoveryApiKey"]["success"] is True
assert response.json["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json["data"]["useRecoveryApiKey"]["token"] is not None
assert response.json().get("data") is not None
assert response.json()["data"]["useRecoveryApiKey"]["success"] is True
assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200
assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None
# Try to get token status
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_RECOVERY_QUERY])},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["api"]["recoveryKey"] is not None
assert response.json["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json["data"]["api"]["recoveryKey"]["valid"] is False
assert response.json["data"]["api"]["recoveryKey"]["creationDate"] is not None
assert response.json["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json["data"]["api"]["recoveryKey"]["usesLeft"] == 0
assert response.json().get("data") is not None
assert response.json()["data"]["api"]["recoveryKey"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True
assert response.json()["data"]["api"]["recoveryKey"]["valid"] is False
assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is not None
assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None
assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] == 0
# Try to use token
response = authorized_client.post(
@ -496,11 +500,11 @@ def test_graphql_generate_recovery_key_with_limited_uses(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["useRecoveryApiKey"]["success"] is False
assert response.json["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json["data"]["useRecoveryApiKey"]["code"] == 404
assert response.json["data"]["useRecoveryApiKey"]["token"] is None
assert response.json().get("data") is not None
assert response.json()["data"]["useRecoveryApiKey"]["success"] is False
assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["useRecoveryApiKey"]["code"] == 404
assert response.json()["data"]["useRecoveryApiKey"]["token"] is None
def test_graphql_generate_recovery_key_with_negative_uses(
@ -519,11 +523,11 @@ def test_graphql_generate_recovery_key_with_negative_uses(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewRecoveryApiKey"]["success"] is False
assert response.json["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json["data"]["getNewRecoveryApiKey"]["code"] == 400
assert response.json["data"]["getNewRecoveryApiKey"]["key"] is None
assert response.json().get("data") is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is False
assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 400
assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None
def test_graphql_generate_recovery_key_with_zero_uses(authorized_client, tokens_file):
@ -540,8 +544,8 @@ def test_graphql_generate_recovery_key_with_zero_uses(authorized_client, tokens_
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["getNewRecoveryApiKey"]["success"] is False
assert response.json["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json["data"]["getNewRecoveryApiKey"]["code"] == 400
assert response.json["data"]["getNewRecoveryApiKey"]["key"] is None
assert response.json().get("data") is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is False
assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None
assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 400
assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None

View File

@ -8,18 +8,18 @@ API_VERSION_QUERY = "version"
def test_graphql_get_api_version(authorized_client):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={"query": generate_api_query([API_VERSION_QUERY])},
)
assert response.status_code == 200
assert "version" in response.get_json()["data"]["api"]
assert "version" in response.json()["data"]["api"]
def test_graphql_api_version_unauthorized(client):
response = client.get(
response = client.post(
"/graphql",
json={"query": generate_api_query([API_VERSION_QUERY])},
)
assert response.status_code == 200
assert "version" in response.get_json()["data"]["api"]
assert "version" in response.json()["data"]["api"]

View File

@ -71,7 +71,7 @@ def test_graphql_add_ssh_key_unauthorized(client, some_users, mock_subprocess_po
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_add_ssh_key(authorized_client, some_users, mock_subprocess_popen):
@ -88,14 +88,14 @@ def test_graphql_add_ssh_key(authorized_client, some_users, mock_subprocess_pope
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["addSshKey"]["code"] == 201
assert response.json["data"]["addSshKey"]["message"] is not None
assert response.json["data"]["addSshKey"]["success"] is True
assert response.json()["data"]["addSshKey"]["code"] == 201
assert response.json()["data"]["addSshKey"]["message"] is not None
assert response.json()["data"]["addSshKey"]["success"] is True
assert response.json["data"]["addSshKey"]["user"]["username"] == "user1"
assert response.json["data"]["addSshKey"]["user"]["sshKeys"] == [
assert response.json()["data"]["addSshKey"]["user"]["username"] == "user1"
assert response.json()["data"]["addSshKey"]["user"]["sshKeys"] == [
"ssh-rsa KEY user1@pc",
"ssh-rsa KEY test_key@pc",
]
@ -115,14 +115,14 @@ def test_graphql_add_root_ssh_key(authorized_client, some_users, mock_subprocess
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["addSshKey"]["code"] == 201
assert response.json["data"]["addSshKey"]["message"] is not None
assert response.json["data"]["addSshKey"]["success"] is True
assert response.json()["data"]["addSshKey"]["code"] == 201
assert response.json()["data"]["addSshKey"]["message"] is not None
assert response.json()["data"]["addSshKey"]["success"] is True
assert response.json["data"]["addSshKey"]["user"]["username"] == "root"
assert response.json["data"]["addSshKey"]["user"]["sshKeys"] == [
assert response.json()["data"]["addSshKey"]["user"]["username"] == "root"
assert response.json()["data"]["addSshKey"]["user"]["sshKeys"] == [
"ssh-ed25519 KEY test@pc",
"ssh-rsa KEY test_key@pc",
]
@ -142,14 +142,14 @@ def test_graphql_add_main_ssh_key(authorized_client, some_users, mock_subprocess
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["addSshKey"]["code"] == 201
assert response.json["data"]["addSshKey"]["message"] is not None
assert response.json["data"]["addSshKey"]["success"] is True
assert response.json()["data"]["addSshKey"]["code"] == 201
assert response.json()["data"]["addSshKey"]["message"] is not None
assert response.json()["data"]["addSshKey"]["success"] is True
assert response.json["data"]["addSshKey"]["user"]["username"] == "tester"
assert response.json["data"]["addSshKey"]["user"]["sshKeys"] == [
assert response.json()["data"]["addSshKey"]["user"]["username"] == "tester"
assert response.json()["data"]["addSshKey"]["user"]["sshKeys"] == [
"ssh-rsa KEY test@pc",
"ssh-rsa KEY test_key@pc",
]
@ -169,11 +169,11 @@ def test_graphql_add_bad_ssh_key(authorized_client, some_users, mock_subprocess_
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["addSshKey"]["code"] == 400
assert response.json["data"]["addSshKey"]["message"] is not None
assert response.json["data"]["addSshKey"]["success"] is False
assert response.json()["data"]["addSshKey"]["code"] == 400
assert response.json()["data"]["addSshKey"]["message"] is not None
assert response.json()["data"]["addSshKey"]["success"] is False
def test_graphql_add_ssh_key_nonexistent_user(
@ -192,11 +192,11 @@ def test_graphql_add_ssh_key_nonexistent_user(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["addSshKey"]["code"] == 404
assert response.json["data"]["addSshKey"]["message"] is not None
assert response.json["data"]["addSshKey"]["success"] is False
assert response.json()["data"]["addSshKey"]["code"] == 404
assert response.json()["data"]["addSshKey"]["message"] is not None
assert response.json()["data"]["addSshKey"]["success"] is False
API_REMOVE_SSH_KEY_MUTATION = """
@ -228,7 +228,7 @@ def test_graphql_remove_ssh_key_unauthorized(client, some_users, mock_subprocess
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_remove_ssh_key(authorized_client, some_users, mock_subprocess_popen):
@ -245,14 +245,14 @@ def test_graphql_remove_ssh_key(authorized_client, some_users, mock_subprocess_p
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["removeSshKey"]["code"] == 200
assert response.json["data"]["removeSshKey"]["message"] is not None
assert response.json["data"]["removeSshKey"]["success"] is True
assert response.json()["data"]["removeSshKey"]["code"] == 200
assert response.json()["data"]["removeSshKey"]["message"] is not None
assert response.json()["data"]["removeSshKey"]["success"] is True
assert response.json["data"]["removeSshKey"]["user"]["username"] == "user1"
assert response.json["data"]["removeSshKey"]["user"]["sshKeys"] == []
assert response.json()["data"]["removeSshKey"]["user"]["username"] == "user1"
assert response.json()["data"]["removeSshKey"]["user"]["sshKeys"] == []
def test_graphql_remove_root_ssh_key(
@ -271,14 +271,14 @@ def test_graphql_remove_root_ssh_key(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["removeSshKey"]["code"] == 200
assert response.json["data"]["removeSshKey"]["message"] is not None
assert response.json["data"]["removeSshKey"]["success"] is True
assert response.json()["data"]["removeSshKey"]["code"] == 200
assert response.json()["data"]["removeSshKey"]["message"] is not None
assert response.json()["data"]["removeSshKey"]["success"] is True
assert response.json["data"]["removeSshKey"]["user"]["username"] == "root"
assert response.json["data"]["removeSshKey"]["user"]["sshKeys"] == []
assert response.json()["data"]["removeSshKey"]["user"]["username"] == "root"
assert response.json()["data"]["removeSshKey"]["user"]["sshKeys"] == []
def test_graphql_remove_main_ssh_key(
@ -297,14 +297,14 @@ def test_graphql_remove_main_ssh_key(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["removeSshKey"]["code"] == 200
assert response.json["data"]["removeSshKey"]["message"] is not None
assert response.json["data"]["removeSshKey"]["success"] is True
assert response.json()["data"]["removeSshKey"]["code"] == 200
assert response.json()["data"]["removeSshKey"]["message"] is not None
assert response.json()["data"]["removeSshKey"]["success"] is True
assert response.json["data"]["removeSshKey"]["user"]["username"] == "tester"
assert response.json["data"]["removeSshKey"]["user"]["sshKeys"] == []
assert response.json()["data"]["removeSshKey"]["user"]["username"] == "tester"
assert response.json()["data"]["removeSshKey"]["user"]["sshKeys"] == []
def test_graphql_remove_nonexistent_ssh_key(
@ -323,11 +323,11 @@ def test_graphql_remove_nonexistent_ssh_key(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["removeSshKey"]["code"] == 404
assert response.json["data"]["removeSshKey"]["message"] is not None
assert response.json["data"]["removeSshKey"]["success"] is False
assert response.json()["data"]["removeSshKey"]["code"] == 404
assert response.json()["data"]["removeSshKey"]["message"] is not None
assert response.json()["data"]["removeSshKey"]["success"] is False
def test_graphql_remove_ssh_key_nonexistent_user(
@ -346,8 +346,8 @@ def test_graphql_remove_ssh_key_nonexistent_user(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["removeSshKey"]["code"] == 404
assert response.json["data"]["removeSshKey"]["message"] is not None
assert response.json["data"]["removeSshKey"]["success"] is False
assert response.json()["data"]["removeSshKey"]["code"] == 404
assert response.json()["data"]["removeSshKey"]["message"] is not None
assert response.json()["data"]["removeSshKey"]["success"] is False

View File

@ -72,7 +72,7 @@ def test_graphql_system_rebuild_unauthorized(client, mock_subprocess_popen):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
assert mock_subprocess_popen.call_count == 0
@ -85,10 +85,10 @@ def test_graphql_system_rebuild(authorized_client, mock_subprocess_popen):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["runSystemRebuild"]["success"] is True
assert response.json["data"]["runSystemRebuild"]["message"] is not None
assert response.json["data"]["runSystemRebuild"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["runSystemRebuild"]["success"] is True
assert response.json()["data"]["runSystemRebuild"]["message"] is not None
assert response.json()["data"]["runSystemRebuild"]["code"] == 200
assert mock_subprocess_popen.call_count == 1
assert mock_subprocess_popen.call_args[0][0] == [
"systemctl",
@ -117,7 +117,7 @@ def test_graphql_system_upgrade_unauthorized(client, mock_subprocess_popen):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
assert mock_subprocess_popen.call_count == 0
@ -130,10 +130,10 @@ def test_graphql_system_upgrade(authorized_client, mock_subprocess_popen):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["runSystemUpgrade"]["success"] is True
assert response.json["data"]["runSystemUpgrade"]["message"] is not None
assert response.json["data"]["runSystemUpgrade"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["runSystemUpgrade"]["success"] is True
assert response.json()["data"]["runSystemUpgrade"]["message"] is not None
assert response.json()["data"]["runSystemUpgrade"]["code"] == 200
assert mock_subprocess_popen.call_count == 1
assert mock_subprocess_popen.call_args[0][0] == [
"systemctl",
@ -162,7 +162,7 @@ def test_graphql_system_rollback_unauthorized(client, mock_subprocess_popen):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
assert mock_subprocess_popen.call_count == 0
@ -175,10 +175,10 @@ def test_graphql_system_rollback(authorized_client, mock_subprocess_popen):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json["data"]["runSystemRollback"]["success"] is True
assert response.json["data"]["runSystemRollback"]["message"] is not None
assert response.json["data"]["runSystemRollback"]["code"] == 200
assert response.json().get("data") is not None
assert response.json()["data"]["runSystemRollback"]["success"] is True
assert response.json()["data"]["runSystemRollback"]["message"] is not None
assert response.json()["data"]["runSystemRollback"]["code"] == 200
assert mock_subprocess_popen.call_count == 1
assert mock_subprocess_popen.call_args[0][0] == [
"systemctl",
@ -207,7 +207,7 @@ def test_graphql_reboot_system_unauthorized(client, mock_subprocess_popen):
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
assert mock_subprocess_popen.call_count == 0
@ -221,11 +221,11 @@ def test_graphql_reboot_system(authorized_client, mock_subprocess_popen):
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["rebootSystem"]["success"] is True
assert response.json["data"]["rebootSystem"]["message"] is not None
assert response.json["data"]["rebootSystem"]["code"] == 200
assert response.json()["data"]["rebootSystem"]["success"] is True
assert response.json()["data"]["rebootSystem"]["message"] is not None
assert response.json()["data"]["rebootSystem"]["code"] == 200
assert mock_subprocess_popen.call_count == 1
assert mock_subprocess_popen.call_args[0][0] == ["reboot"]

View File

@ -119,53 +119,53 @@ allUsers {
def test_graphql_get_users_unauthorized(client, some_users, mock_subprocess_popen):
"""Test wrong auth"""
response = client.get(
response = client.post(
"/graphql",
json={
"query": generate_users_query([API_USERS_INFO]),
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_get_some_users(authorized_client, some_users, mock_subprocess_popen):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={
"query": generate_users_query([API_USERS_INFO]),
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert len(response.json["data"]["users"]["allUsers"]) == 4
assert response.json["data"]["users"]["allUsers"][0]["username"] == "user1"
assert response.json["data"]["users"]["allUsers"][0]["sshKeys"] == [
assert response.json().get("data") is not None
assert len(response.json()["data"]["users"]["allUsers"]) == 4
assert response.json()["data"]["users"]["allUsers"][0]["username"] == "user1"
assert response.json()["data"]["users"]["allUsers"][0]["sshKeys"] == [
"ssh-rsa KEY user1@pc"
]
assert response.json["data"]["users"]["allUsers"][1]["username"] == "user2"
assert response.json["data"]["users"]["allUsers"][1]["sshKeys"] == []
assert response.json()["data"]["users"]["allUsers"][1]["username"] == "user2"
assert response.json()["data"]["users"]["allUsers"][1]["sshKeys"] == []
assert response.json["data"]["users"]["allUsers"][3]["username"] == "tester"
assert response.json["data"]["users"]["allUsers"][3]["sshKeys"] == [
assert response.json()["data"]["users"]["allUsers"][3]["username"] == "tester"
assert response.json()["data"]["users"]["allUsers"][3]["sshKeys"] == [
"ssh-rsa KEY test@pc"
]
def test_graphql_get_no_users(authorized_client, no_users, mock_subprocess_popen):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={
"query": generate_users_query([API_USERS_INFO]),
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert len(response.json["data"]["users"]["allUsers"]) == 1
assert response.json["data"]["users"]["allUsers"][0]["username"] == "tester"
assert response.json["data"]["users"]["allUsers"][0]["sshKeys"] == [
assert len(response.json()["data"]["users"]["allUsers"]) == 1
assert response.json()["data"]["users"]["allUsers"][0]["username"] == "tester"
assert response.json()["data"]["users"]["allUsers"][0]["sshKeys"] == [
"ssh-rsa KEY test@pc"
]
@ -183,7 +183,7 @@ query TestUsers($username: String!) {
def test_graphql_get_one_user_unauthorized(client, one_user, mock_subprocess_popen):
response = client.get(
response = client.post(
"/graphql",
json={
"query": API_GET_USERS,
@ -193,12 +193,12 @@ def test_graphql_get_one_user_unauthorized(client, one_user, mock_subprocess_pop
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_get_one_user(authorized_client, one_user, mock_subprocess_popen):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={
"query": API_GET_USERS,
@ -208,17 +208,17 @@ def test_graphql_get_one_user(authorized_client, one_user, mock_subprocess_popen
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert len(response.json["data"]["users"]["getUser"]) == 2
assert response.json["data"]["users"]["getUser"]["username"] == "user1"
assert response.json["data"]["users"]["getUser"]["sshKeys"] == [
assert len(response.json()["data"]["users"]["getUser"]) == 2
assert response.json()["data"]["users"]["getUser"]["username"] == "user1"
assert response.json()["data"]["users"]["getUser"]["sshKeys"] == [
"ssh-rsa KEY user1@pc"
]
def test_graphql_get_some_user(authorized_client, some_users, mock_subprocess_popen):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={
"query": API_GET_USERS,
@ -228,15 +228,15 @@ def test_graphql_get_some_user(authorized_client, some_users, mock_subprocess_po
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert len(response.json["data"]["users"]["getUser"]) == 2
assert response.json["data"]["users"]["getUser"]["username"] == "user2"
assert response.json["data"]["users"]["getUser"]["sshKeys"] == []
assert len(response.json()["data"]["users"]["getUser"]) == 2
assert response.json()["data"]["users"]["getUser"]["username"] == "user2"
assert response.json()["data"]["users"]["getUser"]["sshKeys"] == []
def test_graphql_get_root_user(authorized_client, some_users, mock_subprocess_popen):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={
"query": API_GET_USERS,
@ -246,17 +246,17 @@ def test_graphql_get_root_user(authorized_client, some_users, mock_subprocess_po
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert len(response.json["data"]["users"]["getUser"]) == 2
assert response.json["data"]["users"]["getUser"]["username"] == "root"
assert response.json["data"]["users"]["getUser"]["sshKeys"] == [
assert len(response.json()["data"]["users"]["getUser"]) == 2
assert response.json()["data"]["users"]["getUser"]["username"] == "root"
assert response.json()["data"]["users"]["getUser"]["sshKeys"] == [
"ssh-ed25519 KEY test@pc"
]
def test_graphql_get_main_user(authorized_client, one_user, mock_subprocess_popen):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={
"query": API_GET_USERS,
@ -266,11 +266,11 @@ def test_graphql_get_main_user(authorized_client, one_user, mock_subprocess_pope
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert len(response.json["data"]["users"]["getUser"]) == 2
assert response.json["data"]["users"]["getUser"]["username"] == "tester"
assert response.json["data"]["users"]["getUser"]["sshKeys"] == [
assert len(response.json()["data"]["users"]["getUser"]) == 2
assert response.json()["data"]["users"]["getUser"]["username"] == "tester"
assert response.json()["data"]["users"]["getUser"]["sshKeys"] == [
"ssh-rsa KEY test@pc"
]
@ -278,7 +278,7 @@ def test_graphql_get_main_user(authorized_client, one_user, mock_subprocess_pope
def test_graphql_get_nonexistent_user(
authorized_client, one_user, mock_subprocess_popen
):
response = authorized_client.get(
response = authorized_client.post(
"/graphql",
json={
"query": API_GET_USERS,
@ -288,9 +288,9 @@ def test_graphql_get_nonexistent_user(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["users"]["getUser"] is None
assert response.json()["data"]["users"]["getUser"] is None
API_CREATE_USERS_MUTATION = """
@ -322,7 +322,7 @@ def test_graphql_add_user_unauthorize(client, one_user, mock_subprocess_popen):
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_add_user(authorized_client, one_user, mock_subprocess_popen):
@ -339,14 +339,14 @@ def test_graphql_add_user(authorized_client, one_user, mock_subprocess_popen):
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 201
assert response.json["data"]["createUser"]["success"] is True
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 201
assert response.json()["data"]["createUser"]["success"] is True
assert response.json["data"]["createUser"]["user"]["username"] == "user2"
assert response.json["data"]["createUser"]["user"]["sshKeys"] == []
assert response.json()["data"]["createUser"]["user"]["username"] == "user2"
assert response.json()["data"]["createUser"]["user"]["sshKeys"] == []
def test_graphql_add_undefined_settings(
@ -365,14 +365,14 @@ def test_graphql_add_undefined_settings(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 201
assert response.json["data"]["createUser"]["success"] is True
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 201
assert response.json()["data"]["createUser"]["success"] is True
assert response.json["data"]["createUser"]["user"]["username"] == "user2"
assert response.json["data"]["createUser"]["user"]["sshKeys"] == []
assert response.json()["data"]["createUser"]["user"]["username"] == "user2"
assert response.json()["data"]["createUser"]["user"]["sshKeys"] == []
def test_graphql_add_without_password(
@ -391,13 +391,13 @@ def test_graphql_add_without_password(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 400
assert response.json["data"]["createUser"]["success"] is False
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 400
assert response.json()["data"]["createUser"]["success"] is False
assert response.json["data"]["createUser"]["user"] is None
assert response.json()["data"]["createUser"]["user"] is None
def test_graphql_add_without_both(authorized_client, one_user, mock_subprocess_popen):
@ -414,13 +414,13 @@ def test_graphql_add_without_both(authorized_client, one_user, mock_subprocess_p
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 400
assert response.json["data"]["createUser"]["success"] is False
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 400
assert response.json()["data"]["createUser"]["success"] is False
assert response.json["data"]["createUser"]["user"] is None
assert response.json()["data"]["createUser"]["user"] is None
@pytest.mark.parametrize("username", invalid_usernames)
@ -440,13 +440,13 @@ def test_graphql_add_system_username(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 409
assert response.json["data"]["createUser"]["success"] is False
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 409
assert response.json()["data"]["createUser"]["success"] is False
assert response.json["data"]["createUser"]["user"] is None
assert response.json()["data"]["createUser"]["user"] is None
def test_graphql_add_existing_user(authorized_client, one_user, mock_subprocess_popen):
@ -463,15 +463,15 @@ def test_graphql_add_existing_user(authorized_client, one_user, mock_subprocess_
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 409
assert response.json["data"]["createUser"]["success"] is False
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 409
assert response.json()["data"]["createUser"]["success"] is False
assert response.json["data"]["createUser"]["user"]["username"] == "user1"
assert response.json()["data"]["createUser"]["user"]["username"] == "user1"
assert (
response.json["data"]["createUser"]["user"]["sshKeys"][0]
response.json()["data"]["createUser"]["user"]["sshKeys"][0]
== "ssh-rsa KEY user1@pc"
)
@ -490,15 +490,15 @@ def test_graphql_add_main_user(authorized_client, one_user, mock_subprocess_pope
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 409
assert response.json["data"]["createUser"]["success"] is False
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 409
assert response.json()["data"]["createUser"]["success"] is False
assert response.json["data"]["createUser"]["user"]["username"] == "tester"
assert response.json()["data"]["createUser"]["user"]["username"] == "tester"
assert (
response.json["data"]["createUser"]["user"]["sshKeys"][0]
response.json()["data"]["createUser"]["user"]["sshKeys"][0]
== "ssh-rsa KEY test@pc"
)
@ -517,13 +517,13 @@ def test_graphql_add_long_username(authorized_client, one_user, mock_subprocess_
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 400
assert response.json["data"]["createUser"]["success"] is False
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 400
assert response.json()["data"]["createUser"]["success"] is False
assert response.json["data"]["createUser"]["user"] is None
assert response.json()["data"]["createUser"]["user"] is None
@pytest.mark.parametrize("username", ["", "1", "фыр", "user1@", "^-^"])
@ -543,13 +543,13 @@ def test_graphql_add_invalid_username(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["createUser"]["message"] is not None
assert response.json["data"]["createUser"]["code"] == 400
assert response.json["data"]["createUser"]["success"] is False
assert response.json()["data"]["createUser"]["message"] is not None
assert response.json()["data"]["createUser"]["code"] == 400
assert response.json()["data"]["createUser"]["success"] is False
assert response.json["data"]["createUser"]["user"] is None
assert response.json()["data"]["createUser"]["user"] is None
API_DELETE_USER_MUTATION = """
@ -572,7 +572,7 @@ def test_graphql_delete_user_unauthorized(client, some_users, mock_subprocess_po
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_delete_user(authorized_client, some_users, mock_subprocess_popen):
@ -584,11 +584,11 @@ def test_graphql_delete_user(authorized_client, some_users, mock_subprocess_pope
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["deleteUser"]["code"] == 200
assert response.json["data"]["deleteUser"]["message"] is not None
assert response.json["data"]["deleteUser"]["success"] is True
assert response.json()["data"]["deleteUser"]["code"] == 200
assert response.json()["data"]["deleteUser"]["message"] is not None
assert response.json()["data"]["deleteUser"]["success"] is True
@pytest.mark.parametrize("username", ["", "def"])
@ -603,11 +603,11 @@ def test_graphql_delete_nonexistent_users(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["deleteUser"]["code"] == 404
assert response.json["data"]["deleteUser"]["message"] is not None
assert response.json["data"]["deleteUser"]["success"] is False
assert response.json()["data"]["deleteUser"]["code"] == 404
assert response.json()["data"]["deleteUser"]["message"] is not None
assert response.json()["data"]["deleteUser"]["success"] is False
@pytest.mark.parametrize("username", invalid_usernames)
@ -622,14 +622,14 @@ def test_graphql_delete_system_users(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert (
response.json["data"]["deleteUser"]["code"] == 404
or response.json["data"]["deleteUser"]["code"] == 400
response.json()["data"]["deleteUser"]["code"] == 404
or response.json()["data"]["deleteUser"]["code"] == 400
)
assert response.json["data"]["deleteUser"]["message"] is not None
assert response.json["data"]["deleteUser"]["success"] is False
assert response.json()["data"]["deleteUser"]["message"] is not None
assert response.json()["data"]["deleteUser"]["success"] is False
def test_graphql_delete_main_user(authorized_client, some_users, mock_subprocess_popen):
@ -641,11 +641,11 @@ def test_graphql_delete_main_user(authorized_client, some_users, mock_subprocess
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["deleteUser"]["code"] == 400
assert response.json["data"]["deleteUser"]["message"] is not None
assert response.json["data"]["deleteUser"]["success"] is False
assert response.json()["data"]["deleteUser"]["code"] == 400
assert response.json()["data"]["deleteUser"]["message"] is not None
assert response.json()["data"]["deleteUser"]["success"] is False
API_UPDATE_USER_MUTATION = """
@ -677,7 +677,7 @@ def test_graphql_update_user_unauthorized(client, some_users, mock_subprocess_po
},
)
assert response.status_code == 200
assert response.json.get("data") is None
assert response.json().get("data") is None
def test_graphql_update_user(authorized_client, some_users, mock_subprocess_popen):
@ -694,14 +694,14 @@ def test_graphql_update_user(authorized_client, some_users, mock_subprocess_pope
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["updateUser"]["code"] == 200
assert response.json["data"]["updateUser"]["message"] is not None
assert response.json["data"]["updateUser"]["success"] is True
assert response.json()["data"]["updateUser"]["code"] == 200
assert response.json()["data"]["updateUser"]["message"] is not None
assert response.json()["data"]["updateUser"]["success"] is True
assert response.json["data"]["updateUser"]["user"]["username"] == "user1"
assert response.json["data"]["updateUser"]["user"]["sshKeys"] == [
assert response.json()["data"]["updateUser"]["user"]["username"] == "user1"
assert response.json()["data"]["updateUser"]["user"]["sshKeys"] == [
"ssh-rsa KEY user1@pc"
]
assert mock_subprocess_popen.call_count == 1
@ -723,11 +723,11 @@ def test_graphql_update_nonexistent_user(
},
)
assert response.status_code == 200
assert response.json.get("data") is not None
assert response.json().get("data") is not None
assert response.json["data"]["updateUser"]["code"] == 404
assert response.json["data"]["updateUser"]["message"] is not None
assert response.json["data"]["updateUser"]["success"] is False
assert response.json()["data"]["updateUser"]["code"] == 404
assert response.json()["data"]["updateUser"]["message"] is not None
assert response.json()["data"]["updateUser"]["success"] is False
assert response.json["data"]["updateUser"]["user"] is None
assert response.json()["data"]["updateUser"]["user"] is None
assert mock_subprocess_popen.call_count == 1

View File

@ -123,13 +123,13 @@ def test_get_timezone_unauthorized(client, turned_on):
def test_get_timezone(authorized_client, turned_on):
response = authorized_client.get("/system/configuration/timezone")
assert response.status_code == 200
assert response.get_json() == "Europe/Moscow"
assert response.json() == "Europe/Moscow"
def test_get_timezone_on_undefined(authorized_client, undefined_config):
response = authorized_client.get("/system/configuration/timezone")
assert response.status_code == 200
assert response.get_json() == "Europe/Uzhgorod"
assert response.json() == "Europe/Uzhgorod"
def test_put_timezone_unauthorized(client, turned_on):
@ -159,7 +159,7 @@ def test_put_timezone_on_undefined(authorized_client, undefined_config):
def test_put_timezone_without_timezone(authorized_client, turned_on):
response = authorized_client.put("/system/configuration/timezone", json={})
assert response.status_code == 400
assert response.status_code == 422
assert read_json(turned_on / "turned_on.json")["timezone"] == "Europe/Moscow"
@ -182,7 +182,7 @@ def test_get_auto_upgrade_unauthorized(client, turned_on):
def test_get_auto_upgrade(authorized_client, turned_on):
response = authorized_client.get("/system/configuration/autoUpgrade")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"enable": True,
"allowReboot": True,
}
@ -191,7 +191,7 @@ def test_get_auto_upgrade(authorized_client, turned_on):
def test_get_auto_upgrade_on_undefined(authorized_client, undefined_config):
response = authorized_client.get("/system/configuration/autoUpgrade")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"enable": True,
"allowReboot": False,
}
@ -200,7 +200,7 @@ def test_get_auto_upgrade_on_undefined(authorized_client, undefined_config):
def test_get_auto_upgrade_without_values(authorized_client, no_values):
response = authorized_client.get("/system/configuration/autoUpgrade")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"enable": True,
"allowReboot": False,
}
@ -209,7 +209,7 @@ def test_get_auto_upgrade_without_values(authorized_client, no_values):
def test_get_auto_upgrade_turned_off(authorized_client, turned_off):
response = authorized_client.get("/system/configuration/autoUpgrade")
assert response.status_code == 200
assert response.get_json() == {
assert response.json() == {
"enable": False,
"allowReboot": False,
}
@ -357,7 +357,7 @@ def test_get_system_version_unauthorized(client, mock_subprocess_check_output):
def test_get_system_version(authorized_client, mock_subprocess_check_output):
response = authorized_client.get("/system/version")
assert response.status_code == 200
assert response.get_json() == {"system_version": "Testing Linux"}
assert response.json() == {"system_version": "Testing Linux"}
assert mock_subprocess_check_output.call_count == 1
assert mock_subprocess_check_output.call_args[0][0] == ["uname", "-a"]
@ -384,7 +384,7 @@ def test_get_python_version_unauthorized(client, mock_subprocess_check_output):
def test_get_python_version(authorized_client, mock_subprocess_check_output):
response = authorized_client.get("/system/pythonVersion")
assert response.status_code == 200
assert response.get_json() == "Testing Linux"
assert response.json() == "Testing Linux"
assert mock_subprocess_check_output.call_count == 1
assert mock_subprocess_check_output.call_args[0][0] == ["python", "-V"]

View File

@ -121,31 +121,31 @@ def test_get_users_unauthorized(client, some_users, mock_subprocess_popen):
def test_get_some_users(authorized_client, some_users, mock_subprocess_popen):
response = authorized_client.get("/users")
assert response.status_code == 200
assert response.json == ["user1", "user2", "user3"]
assert response.json() == ["user1", "user2", "user3"]
def test_get_one_user(authorized_client, one_user, mock_subprocess_popen):
response = authorized_client.get("/users")
assert response.status_code == 200
assert response.json == ["user1"]
assert response.json() == ["user1"]
def test_get_one_user_with_main(authorized_client, one_user, mock_subprocess_popen):
response = authorized_client.get("/users?withMainUser=true")
assert response.status_code == 200
assert response.json == ["tester", "user1"]
assert response.json().sort() == ["tester", "user1"].sort()
def test_get_no_users(authorized_client, no_users, mock_subprocess_popen):
response = authorized_client.get("/users")
assert response.status_code == 200
assert response.json == []
assert response.json() == []
def test_get_no_users_with_main(authorized_client, no_users, mock_subprocess_popen):
response = authorized_client.get("/users?withMainUser=true")
assert response.status_code == 200
assert response.json == ["tester"]
assert response.json() == ["tester"]
def test_get_undefined_users(
@ -153,7 +153,7 @@ def test_get_undefined_users(
):
response = authorized_client.get("/users")
assert response.status_code == 200
assert response.json == []
assert response.json() == []
def test_post_users_unauthorized(client, some_users, mock_subprocess_popen):
@ -174,6 +174,7 @@ def test_post_one_user(authorized_client, one_user, mock_subprocess_popen):
},
{
"username": "user4",
"sshKeys": [],
"hashedPassword": "NEW_HASHED",
},
]
@ -181,19 +182,19 @@ def test_post_one_user(authorized_client, one_user, mock_subprocess_popen):
def test_post_without_username(authorized_client, one_user, mock_subprocess_popen):
response = authorized_client.post("/users", json={"password": "password"})
assert response.status_code == 400
assert response.status_code == 422
def test_post_without_password(authorized_client, one_user, mock_subprocess_popen):
response = authorized_client.post("/users", json={"username": "user4"})
assert response.status_code == 400
assert response.status_code == 422
def test_post_without_username_and_password(
authorized_client, one_user, mock_subprocess_popen
):
response = authorized_client.post("/users", json={})
assert response.status_code == 400
assert response.status_code == 422
@pytest.mark.parametrize("username", invalid_usernames)
@ -226,7 +227,7 @@ def test_post_user_to_undefined_users(
)
assert response.status_code == 201
assert read_json(undefined_settings / "undefined.json")["users"] == [
{"username": "user4", "hashedPassword": "NEW_HASHED"}
{"username": "user4", "sshKeys": [], "hashedPassword": "NEW_HASHED"}
]
@ -279,11 +280,6 @@ def test_delete_main_user(authorized_client, some_users, mock_subprocess_popen):
assert response.status_code == 400
def test_delete_without_argument(authorized_client, some_users, mock_subprocess_popen):
response = authorized_client.delete("/users/")
assert response.status_code == 404
def test_delete_just_delete(authorized_client, some_users, mock_subprocess_popen):
response = authorized_client.delete("/users")
assert response.status_code == 405