From 337cf298845351d152337f11a1219837bf87aa7f Mon Sep 17 00:00:00 2001 From: def Date: Mon, 1 Aug 2022 13:40:40 +0300 Subject: [PATCH] Add GraphQJ user and ssh management (#12) Co-authored-by: Inex Code Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api/pulls/12 Co-authored-by: def Co-committed-by: def --- .pylintrc | 2 +- selfprivacy_api/app.py | 1 - .../graphql/common_types/__init__.py | 0 selfprivacy_api/graphql/common_types/user.py | 78 ++ .../graphql/mutations/ssh_mutations.py | 54 ++ .../graphql/mutations/ssh_utils.py | 74 ++ .../graphql/mutations/system_mutations.py | 2 +- .../graphql/mutations/users_mutations.py | 65 ++ .../graphql/mutations/users_utils.py | 111 +++ selfprivacy_api/graphql/queries/users.py | 38 + selfprivacy_api/graphql/schema.py | 19 +- selfprivacy_api/utils/__init__.py | 14 + tests/common.py | 4 + tests/test_graphql/_test_system.py | 8 +- tests/test_graphql/test_api.py | 1 - tests/test_graphql/test_api_recovery.py | 3 - tests/test_graphql/test_ssh.py | 353 +++++++++ tests/test_graphql/test_ssh/some_users.json | 71 ++ tests/test_graphql/test_users.py | 733 ++++++++++++++++++ tests/test_graphql/test_users/no_users.json | 54 ++ tests/test_graphql/test_users/one_user.json | 61 ++ tests/test_graphql/test_users/some_users.json | 71 ++ tests/test_graphql/test_users/undefined.json | 52 ++ 23 files changed, 1855 insertions(+), 14 deletions(-) create mode 100644 selfprivacy_api/graphql/common_types/__init__.py create mode 100644 selfprivacy_api/graphql/common_types/user.py create mode 100644 selfprivacy_api/graphql/mutations/ssh_mutations.py create mode 100644 selfprivacy_api/graphql/mutations/ssh_utils.py create mode 100644 selfprivacy_api/graphql/mutations/users_mutations.py create mode 100644 selfprivacy_api/graphql/mutations/users_utils.py create mode 100644 selfprivacy_api/graphql/queries/users.py create mode 100644 tests/test_graphql/test_ssh.py create mode 100644 tests/test_graphql/test_ssh/some_users.json create mode 100644 tests/test_graphql/test_users.py create mode 100644 tests/test_graphql/test_users/no_users.json create mode 100644 tests/test_graphql/test_users/one_user.json create mode 100644 tests/test_graphql/test_users/some_users.json create mode 100644 tests/test_graphql/test_users/undefined.json diff --git a/.pylintrc b/.pylintrc index 6a584c2..c6d73d8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,2 +1,2 @@ [MASTER] -init-hook='import sys; sys.path.append("/path/to/root")' +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" diff --git a/selfprivacy_api/app.py b/selfprivacy_api/app.py index 77281e8..15142f0 100644 --- a/selfprivacy_api/app.py +++ b/selfprivacy_api/app.py @@ -53,7 +53,6 @@ def create_app(test_config=None): pass elif request.path.startswith("/auth/recovery_token/use"): pass - # TODO: REMOVE THIS elif request.path.startswith("/graphql"): pass else: diff --git a/selfprivacy_api/graphql/common_types/__init__.py b/selfprivacy_api/graphql/common_types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/selfprivacy_api/graphql/common_types/user.py b/selfprivacy_api/graphql/common_types/user.py new file mode 100644 index 0000000..8cc5f2c --- /dev/null +++ b/selfprivacy_api/graphql/common_types/user.py @@ -0,0 +1,78 @@ +import typing +from enum import Enum +import strawberry + +from selfprivacy_api.utils import ReadUserData +from selfprivacy_api.graphql.mutations.mutation_interface import ( + MutationReturnInterface, +) + + +@strawberry.enum +class UserType(Enum): + NORMAL = "NORMAL" + PRIMARY = "PRIMARY" + ROOT = "ROOT" + + +@strawberry.type +class User: + + user_type: UserType + username: str + # userHomeFolderspace: UserHomeFolderUsage + ssh_keys: typing.List[str] = strawberry.field(default_factory=list) + + +@strawberry.type +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"] = [] + + +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"], + ) + + return None diff --git a/selfprivacy_api/graphql/mutations/ssh_mutations.py b/selfprivacy_api/graphql/mutations/ssh_mutations.py new file mode 100644 index 0000000..b30f474 --- /dev/null +++ b/selfprivacy_api/graphql/mutations/ssh_mutations.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Users management module""" +# pylint: disable=too-few-public-methods + +import strawberry + +from selfprivacy_api.graphql import IsAuthenticated +from selfprivacy_api.graphql.mutations.ssh_utils import ( + create_ssh_key, + remove_ssh_key, +) +from selfprivacy_api.graphql.common_types.user import ( + UserMutationReturn, + get_user_by_username, +) + + +@strawberry.input +class SshMutationInput: + """Input type for ssh mutation""" + + username: str + ssh_key: str + + +@strawberry.type +class SshMutations: + """Mutations ssh""" + + @strawberry.mutation(permission_classes=[IsAuthenticated]) + 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) + + return UserMutationReturn( + success=success, + message=message, + code=code, + user=get_user_by_username(ssh_input.username), + ) + + @strawberry.mutation(permission_classes=[IsAuthenticated]) + 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) + + return UserMutationReturn( + success=success, + message=message, + code=code, + user=get_user_by_username(ssh_input.username), + ) diff --git a/selfprivacy_api/graphql/mutations/ssh_utils.py b/selfprivacy_api/graphql/mutations/ssh_utils.py new file mode 100644 index 0000000..3dbc152 --- /dev/null +++ b/selfprivacy_api/graphql/mutations/ssh_utils.py @@ -0,0 +1,74 @@ +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 diff --git a/selfprivacy_api/graphql/mutations/system_mutations.py b/selfprivacy_api/graphql/mutations/system_mutations.py index d080af3..517a697 100644 --- a/selfprivacy_api/graphql/mutations/system_mutations.py +++ b/selfprivacy_api/graphql/mutations/system_mutations.py @@ -7,7 +7,7 @@ from selfprivacy_api.graphql import IsAuthenticated from selfprivacy_api.graphql.mutations.mutation_interface import ( MutationReturnInterface, ) -from selfprivacy_api.utils import ReadUserData, WriteUserData +from selfprivacy_api.utils import WriteUserData @strawberry.type diff --git a/selfprivacy_api/graphql/mutations/users_mutations.py b/selfprivacy_api/graphql/mutations/users_mutations.py new file mode 100644 index 0000000..a284ff2 --- /dev/null +++ b/selfprivacy_api/graphql/mutations/users_mutations.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Users management module""" +# pylint: disable=too-few-public-methods +import strawberry +from selfprivacy_api.graphql import IsAuthenticated +from selfprivacy_api.graphql.common_types.user import ( + UserMutationReturn, + get_user_by_username, +) +from selfprivacy_api.graphql.mutations.mutation_interface import ( + GenericMutationReturn, +) +from selfprivacy_api.graphql.mutations.users_utils import ( + create_user, + delete_user, + update_user, +) + + +@strawberry.input +class UserMutationInput: + """Input type for user mutation""" + + username: str + password: str + + +@strawberry.type +class UserMutations: + """Mutations change user settings""" + + @strawberry.mutation(permission_classes=[IsAuthenticated]) + def create_user(self, user: UserMutationInput) -> UserMutationReturn: + + success, message, code = create_user(user.username, user.password) + + return UserMutationReturn( + success=success, + message=message, + code=code, + 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) + + return GenericMutationReturn( + success=success, + message=message, + code=code, + ) + + @strawberry.mutation(permission_classes=[IsAuthenticated]) + def update_user(self, user: UserMutationInput) -> UserMutationReturn: + """Update user mutation""" + + success, message, code = update_user(user.username, user.password) + + return UserMutationReturn( + success=success, + message=message, + code=code, + user=get_user_by_username(user.username), + ) diff --git a/selfprivacy_api/graphql/mutations/users_utils.py b/selfprivacy_api/graphql/mutations/users_utils.py new file mode 100644 index 0000000..f649b45 --- /dev/null +++ b/selfprivacy_api/graphql/mutations/users_utils.py @@ -0,0 +1,111 @@ +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 diff --git a/selfprivacy_api/graphql/queries/users.py b/selfprivacy_api/graphql/queries/users.py new file mode 100644 index 0000000..fc18a84 --- /dev/null +++ b/selfprivacy_api/graphql/queries/users.py @@ -0,0 +1,38 @@ +"""Users""" +# pylint: disable=too-few-public-methods +import typing +import strawberry + +from selfprivacy_api.graphql.common_types.user import ( + User, + ensure_ssh_and_users_fields_exist, + get_user_by_username, +) +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]) + def get_user(self, username: str) -> typing.Optional[User]: + """Get users""" + return get_user_by_username(username) + + all_users: typing.List[User] = strawberry.field( + permission_classes=[IsAuthenticated], resolver=get_users + ) diff --git a/selfprivacy_api/graphql/schema.py b/selfprivacy_api/graphql/schema.py index c4daac3..c2d6a10 100644 --- a/selfprivacy_api/graphql/schema.py +++ b/selfprivacy_api/graphql/schema.py @@ -1,9 +1,10 @@ """GraphQL API for SelfPrivacy.""" # pylint: disable=too-few-public-methods -import typing + import strawberry from selfprivacy_api.graphql import IsAuthenticated from selfprivacy_api.graphql.mutations.api_mutations import ApiMutations +from selfprivacy_api.graphql.mutations.ssh_mutations import SshMutations from selfprivacy_api.graphql.mutations.storage_mutation import StorageMutations from selfprivacy_api.graphql.mutations.system_mutations import SystemMutations @@ -11,6 +12,9 @@ from selfprivacy_api.graphql.queries.api_queries import Api from selfprivacy_api.graphql.queries.storage import Storage from selfprivacy_api.graphql.queries.system import System +from selfprivacy_api.graphql.mutations.users_mutations import UserMutations +from selfprivacy_api.graphql.queries.users import Users + @strawberry.type class Query: @@ -26,6 +30,11 @@ class Query: """API access status""" return Api() + @strawberry.field(permission_classes=[IsAuthenticated]) + def users(self) -> Users: + """Users queries""" + return Users() + @strawberry.field(permission_classes=[IsAuthenticated]) def storage(self) -> Storage: """Storage queries""" @@ -33,7 +42,13 @@ class Query: @strawberry.type -class Mutation(ApiMutations, SystemMutations, StorageMutations): +class Mutation( + ApiMutations, + SystemMutations, + UserMutations, + SshMutations, + StorageMutations, +): """Root schema for mutations""" pass diff --git a/selfprivacy_api/utils/__init__.py b/selfprivacy_api/utils/__init__.py index 1adb189..8ab26d1 100644 --- a/selfprivacy_api/utils/__init__.py +++ b/selfprivacy_api/utils/__init__.py @@ -159,3 +159,17 @@ def get_dkim_key(domain): dkim = cat_process.communicate()[0] return str(dkim, "utf-8") return None + + +def hash_password(password): + hashing_command = ["mkpasswd", "-m", "sha-512", 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() + return hashed_password diff --git a/tests/common.py b/tests/common.py index 01975e8..18e065c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -20,5 +20,9 @@ def generate_system_query(query_array): return "query TestSystem {\n system {" + "\n".join(query_array) + "}\n}" +def generate_users_query(query_array): + return "query TestUsers {\n users {" + "\n".join(query_array) + "}\n}" + + def mnemonic_to_hex(mnemonic): return Mnemonic(language="english").to_entropy(mnemonic).hex() diff --git a/tests/test_graphql/_test_system.py b/tests/test_graphql/_test_system.py index d5cf6e6..476846a 100644 --- a/tests/test_graphql/_test_system.py +++ b/tests/test_graphql/_test_system.py @@ -1,12 +1,10 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument # pylint: disable=missing-function-docstring -import json import os import pytest -import datetime -from tests.common import generate_system_query, read_json, write_json +from tests.common import generate_system_query, read_json @pytest.fixture @@ -56,7 +54,7 @@ class ProcessMock: self.args = args self.kwargs = kwargs - def communicate(): + def communicate(): # pylint: disable=no-method-argument return (b"", None) returncode = 0 @@ -65,7 +63,7 @@ class ProcessMock: class BrokenServiceMock(ProcessMock): """Mock subprocess.Popen for broken service""" - def communicate(): + def communicate(): # pylint: disable=no-method-argument return (b"Testing error", None) returncode = 3 diff --git a/tests/test_graphql/test_api.py b/tests/test_graphql/test_api.py index 031e052..6343d8f 100644 --- a/tests/test_graphql/test_api.py +++ b/tests/test_graphql/test_api.py @@ -1,7 +1,6 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument # pylint: disable=missing-function-docstring -import pytest from tests.common import generate_api_query from tests.test_graphql.test_api_devices import API_DEVICES_QUERY diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 2d1e16a..be0fdff 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -1,9 +1,6 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument # pylint: disable=missing-function-docstring -import json -from time import strftime -import pytest import datetime from tests.common import generate_api_query, mnemonic_to_hex, read_json, write_json diff --git a/tests/test_graphql/test_ssh.py b/tests/test_graphql/test_ssh.py new file mode 100644 index 0000000..7b48c83 --- /dev/null +++ b/tests/test_graphql/test_ssh.py @@ -0,0 +1,353 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +import pytest + +from tests.common import read_json + + +class ProcessMock: + """Mock subprocess.Popen""" + + def __init__(self, args, **kwargs): + self.args = args + self.kwargs = kwargs + + def communicate(): # pylint: disable=no-method-argument + return (b"NEW_HASHED", None) + + returncode = 0 + + +@pytest.fixture +def mock_subprocess_popen(mocker): + mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) + return mock + + +@pytest.fixture +def some_users(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "some_users.json") + assert read_json(datadir / "some_users.json")["users"] == [ + { + "username": "user1", + "hashedPassword": "HASHED_PASSWORD_1", + "sshKeys": ["ssh-rsa KEY user1@pc"], + }, + {"username": "user2", "hashedPassword": "HASHED_PASSWORD_2", "sshKeys": []}, + {"username": "user3", "hashedPassword": "HASHED_PASSWORD_3"}, + ] + return datadir + + +# TESTS ######################################################## + + +API_CREATE_SSH_KEY_MUTATION = """ +mutation addSshKey($sshInput: SshMutationInput!) { + addSshKey(sshInput: $sshInput) { + success + message + code + user { + username + sshKeys + } + } +} +""" + + +def test_graphql_add_ssh_key_unauthorized(client, some_users, mock_subprocess_popen): + response = client.post( + "/graphql", + json={ + "query": API_CREATE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "user1", + "sshKey": "ssh-rsa KEY test_key@pc", + }, + }, + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is None + + +def test_graphql_add_ssh_key(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "user1", + "sshKey": "ssh-rsa KEY test_key@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "user1" + assert response.json["data"]["addSshKey"]["user"]["sshKeys"] == [ + "ssh-rsa KEY user1@pc", + "ssh-rsa KEY test_key@pc", + ] + + +def test_graphql_add_root_ssh_key(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "root", + "sshKey": "ssh-rsa KEY test_key@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "root" + assert response.json["data"]["addSshKey"]["user"]["sshKeys"] == [ + "ssh-ed25519 KEY test@pc", + "ssh-rsa KEY test_key@pc", + ] + + +def test_graphql_add_main_ssh_key(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "tester", + "sshKey": "ssh-rsa KEY test_key@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "tester" + assert response.json["data"]["addSshKey"]["user"]["sshKeys"] == [ + "ssh-rsa KEY test@pc", + "ssh-rsa KEY test_key@pc", + ] + + +def test_graphql_add_bad_ssh_key(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "user1", + "sshKey": "trust me, this is the ssh key", + }, + }, + }, + ) + assert response.status_code == 200 + 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 + + +def test_graphql_add_ssh_key_nonexistent_user( + authorized_client, some_users, mock_subprocess_popen +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "user666", + "sshKey": "ssh-rsa KEY test_key@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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 + + +API_REMOVE_SSH_KEY_MUTATION = """ +mutation removeSshKey($sshInput: SshMutationInput!) { + removeSshKey(sshInput: $sshInput) { + success + message + code + user { + username + sshKeys + } + } +} +""" + + +def test_graphql_remove_ssh_key_unauthorized(client, some_users, mock_subprocess_popen): + response = client.post( + "/graphql", + json={ + "query": API_REMOVE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "user1", + "sshKey": "ssh-rsa KEY test_key@pc", + }, + }, + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is None + + +def test_graphql_remove_ssh_key(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_REMOVE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "user1", + "sshKey": "ssh-rsa KEY user1@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "user1" + assert response.json["data"]["removeSshKey"]["user"]["sshKeys"] == [] + + +def test_graphql_remove_root_ssh_key( + authorized_client, some_users, mock_subprocess_popen +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_REMOVE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "root", + "sshKey": "ssh-ed25519 KEY test@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "root" + assert response.json["data"]["removeSshKey"]["user"]["sshKeys"] == [] + + +def test_graphql_remove_main_ssh_key( + authorized_client, some_users, mock_subprocess_popen +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_REMOVE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "tester", + "sshKey": "ssh-rsa KEY test@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "tester" + assert response.json["data"]["removeSshKey"]["user"]["sshKeys"] == [] + + +def test_graphql_remove_nonexistent_ssh_key( + authorized_client, some_users, mock_subprocess_popen +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_REMOVE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "user1", + "sshKey": "ssh-rsa KEY test_key@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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 + + +def test_graphql_remove_ssh_key_nonexistent_user( + authorized_client, some_users, mock_subprocess_popen +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_REMOVE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": "user666", + "sshKey": "ssh-rsa KEY test_key@pc", + }, + }, + }, + ) + assert response.status_code == 200 + 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 diff --git a/tests/test_graphql/test_ssh/some_users.json b/tests/test_graphql/test_ssh/some_users.json new file mode 100644 index 0000000..569253a --- /dev/null +++ b/tests/test_graphql/test_ssh/some_users.json @@ -0,0 +1,71 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ], + "users": [ + { + "username": "user1", + "hashedPassword": "HASHED_PASSWORD_1", + "sshKeys": [ + "ssh-rsa KEY user1@pc" + ] + }, + { + "username": "user2", + "hashedPassword": "HASHED_PASSWORD_2", + "sshKeys": [ + ] + }, + { + "username": "user3", + "hashedPassword": "HASHED_PASSWORD_3" + } + ] +} \ No newline at end of file diff --git a/tests/test_graphql/test_users.py b/tests/test_graphql/test_users.py new file mode 100644 index 0000000..afae1da --- /dev/null +++ b/tests/test_graphql/test_users.py @@ -0,0 +1,733 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +import pytest + +from tests.common import ( + generate_users_query, + read_json, +) + +invalid_usernames = [ + "messagebus", + "postfix", + "polkituser", + "dovecot2", + "dovenull", + "nginx", + "postgres", + "systemd-journal-gateway", + "prosody", + "systemd-network", + "systemd-resolve", + "systemd-timesync", + "opendkim", + "rspamd", + "sshd", + "selfprivacy-api", + "restic", + "redis", + "pleroma", + "ocserv", + "nextcloud", + "memcached", + "knot-resolver", + "gitea", + "bitwarden_rs", + "vaultwarden", + "acme", + "virtualMail", + "nixbld1", + "nixbld2", + "nixbld29", + "nobody", +] + + +## FIXTURES ################################################### + + +@pytest.fixture +def no_users(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "no_users.json") + assert read_json(datadir / "no_users.json")["users"] == [] + return datadir + + +@pytest.fixture +def one_user(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "one_user.json") + assert read_json(datadir / "one_user.json")["users"] == [ + { + "username": "user1", + "hashedPassword": "HASHED_PASSWORD_1", + "sshKeys": ["ssh-rsa KEY user1@pc"], + } + ] + return datadir + + +@pytest.fixture +def some_users(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "some_users.json") + assert read_json(datadir / "some_users.json")["users"] == [ + { + "username": "user1", + "hashedPassword": "HASHED_PASSWORD_1", + "sshKeys": ["ssh-rsa KEY user1@pc"], + }, + {"username": "user2", "hashedPassword": "HASHED_PASSWORD_2", "sshKeys": []}, + {"username": "user3", "hashedPassword": "HASHED_PASSWORD_3"}, + ] + return datadir + + +@pytest.fixture +def undefined_settings(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") + assert "users" not in read_json(datadir / "undefined.json") + return datadir + + +class ProcessMock: + """Mock subprocess.Popen""" + + def __init__(self, args, **kwargs): + self.args = args + self.kwargs = kwargs + + def communicate(): # pylint: disable=no-method-argument + return (b"NEW_HASHED", None) + + returncode = 0 + + +@pytest.fixture +def mock_subprocess_popen(mocker): + mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) + return mock + + +## TESTS ###################################################### + +API_USERS_INFO = """ +allUsers { + username + sshKeys +} +""" + + +def test_graphql_get_users_unauthorized(client, some_users, mock_subprocess_popen): + """Test wrong auth""" + response = client.get( + "/graphql", + json={ + "query": generate_users_query([API_USERS_INFO]), + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is None + + +def test_graphql_get_some_users(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.get( + "/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"] == [ + "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"][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( + "/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"]) == 1 + assert response.json["data"]["users"]["allUsers"][0]["username"] == "tester" + assert response.json["data"]["users"]["allUsers"][0]["sshKeys"] == [ + "ssh-rsa KEY test@pc" + ] + + +API_GET_USERS = """ +query TestUsers($username: String!) { + users { + getUser(username: $username) { + sshKeys + username + } + } +} +""" + + +def test_graphql_get_one_user_unauthorized(client, one_user, mock_subprocess_popen): + response = client.get( + "/graphql", + json={ + "query": API_GET_USERS, + "variables": { + "username": "user1", + }, + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is None + + +def test_graphql_get_one_user(authorized_client, one_user, mock_subprocess_popen): + + response = authorized_client.get( + "/graphql", + json={ + "query": API_GET_USERS, + "variables": { + "username": "user1", + }, + }, + ) + assert response.status_code == 200 + 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"] == [ + "ssh-rsa KEY user1@pc" + ] + + +def test_graphql_get_some_user(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.get( + "/graphql", + json={ + "query": API_GET_USERS, + "variables": { + "username": "user2", + }, + }, + ) + assert response.status_code == 200 + 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"] == [] + + +def test_graphql_get_root_user(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.get( + "/graphql", + json={ + "query": API_GET_USERS, + "variables": { + "username": "root", + }, + }, + ) + assert response.status_code == 200 + 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"] == [ + "ssh-ed25519 KEY test@pc" + ] + + +def test_graphql_get_main_user(authorized_client, one_user, mock_subprocess_popen): + response = authorized_client.get( + "/graphql", + json={ + "query": API_GET_USERS, + "variables": { + "username": "tester", + }, + }, + ) + assert response.status_code == 200 + 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"] == [ + "ssh-rsa KEY test@pc" + ] + + +def test_graphql_get_nonexistent_user( + authorized_client, one_user, mock_subprocess_popen +): + response = authorized_client.get( + "/graphql", + json={ + "query": API_GET_USERS, + "variables": { + "username": "tyler_durden", + }, + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is not None + + assert response.json["data"]["users"]["getUser"] is None + + +API_CREATE_USERS_MUTATION = """ +mutation createUser($user: UserMutationInput!) { + createUser(user: $user) { + success + message + code + user { + username + sshKeys + } + } +} +""" + + +def test_graphql_add_user_unauthorize(client, one_user, mock_subprocess_popen): + response = client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": "user2", + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is None + + +def test_graphql_add_user(authorized_client, one_user, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": "user2", + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "user2" + assert response.json["data"]["createUser"]["user"]["sshKeys"] == [] + + +def test_graphql_add_undefined_settings( + authorized_client, undefined_settings, mock_subprocess_popen +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": "user2", + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "user2" + assert response.json["data"]["createUser"]["user"]["sshKeys"] == [] + + +def test_graphql_add_without_password( + authorized_client, one_user, mock_subprocess_popen +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": "user2", + "password": "", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"] is None + + +def test_graphql_add_without_both(authorized_client, one_user, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": "", + "password": "", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"] is None + + +@pytest.mark.parametrize("username", invalid_usernames) +def test_graphql_add_system_username( + authorized_client, one_user, mock_subprocess_popen, username +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": username, + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"] is None + + +def test_graphql_add_existing_user(authorized_client, one_user, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": "user1", + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "user1" + assert ( + response.json["data"]["createUser"]["user"]["sshKeys"][0] + == "ssh-rsa KEY user1@pc" + ) + + +def test_graphql_add_main_user(authorized_client, one_user, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": "tester", + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "tester" + assert ( + response.json["data"]["createUser"]["user"]["sshKeys"][0] + == "ssh-rsa KEY test@pc" + ) + + +def test_graphql_add_long_username(authorized_client, one_user, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": "a" * 32, + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"] is None + + +@pytest.mark.parametrize("username", ["", "1", "фыр", "user1@", "^-^"]) +def test_graphql_add_invalid_username( + authorized_client, one_user, mock_subprocess_popen, username +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_USERS_MUTATION, + "variables": { + "user": { + "username": username, + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"] is None + + +API_DELETE_USER_MUTATION = """ +mutation deleteUser($username: String!) { + deleteUser(username: $username) { + success + message + code + } +} +""" + + +def test_graphql_delete_user_unauthorized(client, some_users, mock_subprocess_popen): + response = client.post( + "/graphql", + json={ + "query": API_DELETE_USER_MUTATION, + "variables": {"username": "user1"}, + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is None + + +def test_graphql_delete_user(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_DELETE_USER_MUTATION, + "variables": {"username": "user1"}, + }, + ) + assert response.status_code == 200 + 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 + + +@pytest.mark.parametrize("username", ["", "def"]) +def test_graphql_delete_nonexistent_users( + authorized_client, some_users, mock_subprocess_popen, username +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_DELETE_USER_MUTATION, + "variables": {"username": username}, + }, + ) + assert response.status_code == 200 + 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 + + +@pytest.mark.parametrize("username", invalid_usernames) +def test_graphql_delete_system_users( + authorized_client, some_users, mock_subprocess_popen, username +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_DELETE_USER_MUTATION, + "variables": {"username": username}, + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is not None + + assert ( + 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 + + +def test_graphql_delete_main_user(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_DELETE_USER_MUTATION, + "variables": {"username": "tester"}, + }, + ) + assert response.status_code == 200 + 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 + + +API_UPDATE_USER_MUTATION = """ +mutation updateUser($user: UserMutationInput!) { + updateUser(user: $user) { + success + message + code + user { + username + sshKeys + } + } +} +""" + + +def test_graphql_update_user_unauthorized(client, some_users, mock_subprocess_popen): + response = client.post( + "/graphql", + json={ + "query": API_UPDATE_USER_MUTATION, + "variables": { + "user": { + "username": "user1", + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + assert response.json.get("data") is None + + +def test_graphql_update_user(authorized_client, some_users, mock_subprocess_popen): + response = authorized_client.post( + "/graphql", + json={ + "query": API_UPDATE_USER_MUTATION, + "variables": { + "user": { + "username": "user1", + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"]["username"] == "user1" + assert response.json["data"]["updateUser"]["user"]["sshKeys"] == [ + "ssh-rsa KEY user1@pc" + ] + assert mock_subprocess_popen.call_count == 1 + + +def test_graphql_update_nonexistent_user( + authorized_client, some_users, mock_subprocess_popen +): + response = authorized_client.post( + "/graphql", + json={ + "query": API_UPDATE_USER_MUTATION, + "variables": { + "user": { + "username": "user666", + "password": "12345678", + }, + }, + }, + ) + assert response.status_code == 200 + 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"]["user"] is None + assert mock_subprocess_popen.call_count == 1 diff --git a/tests/test_graphql/test_users/no_users.json b/tests/test_graphql/test_users/no_users.json new file mode 100644 index 0000000..e5efe86 --- /dev/null +++ b/tests/test_graphql/test_users/no_users.json @@ -0,0 +1,54 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ], + "users": [ + ] +} \ No newline at end of file diff --git a/tests/test_graphql/test_users/one_user.json b/tests/test_graphql/test_users/one_user.json new file mode 100644 index 0000000..5df2108 --- /dev/null +++ b/tests/test_graphql/test_users/one_user.json @@ -0,0 +1,61 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ], + "users": [ + { + "username": "user1", + "hashedPassword": "HASHED_PASSWORD_1", + "sshKeys": [ + "ssh-rsa KEY user1@pc" + ] + } + ] +} \ No newline at end of file diff --git a/tests/test_graphql/test_users/some_users.json b/tests/test_graphql/test_users/some_users.json new file mode 100644 index 0000000..569253a --- /dev/null +++ b/tests/test_graphql/test_users/some_users.json @@ -0,0 +1,71 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ], + "users": [ + { + "username": "user1", + "hashedPassword": "HASHED_PASSWORD_1", + "sshKeys": [ + "ssh-rsa KEY user1@pc" + ] + }, + { + "username": "user2", + "hashedPassword": "HASHED_PASSWORD_2", + "sshKeys": [ + ] + }, + { + "username": "user3", + "hashedPassword": "HASHED_PASSWORD_3" + } + ] +} \ No newline at end of file diff --git a/tests/test_graphql/test_users/undefined.json b/tests/test_graphql/test_users/undefined.json new file mode 100644 index 0000000..7b2cf8b --- /dev/null +++ b/tests/test_graphql/test_users/undefined.json @@ -0,0 +1,52 @@ +{ + "backblaze": { + "accountId": "ID", + "accountKey": "KEY", + "bucket": "selfprivacy" + }, + "api": { + "token": "TEST_TOKEN", + "enableSwagger": false + }, + "bitwarden": { + "enable": false + }, + "cloudflare": { + "apiKey": "TOKEN" + }, + "databasePassword": "PASSWORD", + "domain": "test.tld", + "hashedMasterPassword": "HASHED_PASSWORD", + "hostname": "test-instance", + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "resticPassword": "PASS", + "ssh": { + "enable": true, + "passwordAuthentication": true, + "rootKeys": [ + "ssh-ed25519 KEY test@pc" + ] + }, + "username": "tester", + "gitea": { + "enable": false + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "autoUpgrade": { + "enable": true, + "allowReboot": true + }, + "timezone": "Europe/Moscow", + "sshKeys": [ + "ssh-rsa KEY test@pc" + ] +} \ No newline at end of file