add basic system getters

This commit is contained in:
Inex Code 2022-06-24 21:14:20 +03:00
parent c6a3588e33
commit 80e5550f7d
14 changed files with 294 additions and 33 deletions

View file

@ -93,10 +93,7 @@ def create_app(test_config=None):
return jsonify({}), 404 return jsonify({}), 404
app.add_url_rule( app.add_url_rule(
"/graphql", "/graphql", view_func=AsyncGraphQLView.as_view("graphql", schema=schema)
view_func=AsyncGraphQLView.as_view(
"graphql", schema=schema
)
) )
if app.config["ENABLE_SWAGGER"] == "1": if app.config["ENABLE_SWAGGER"] == "1":

View file

@ -7,8 +7,10 @@ from flask import request
from selfprivacy_api.utils.auth import is_token_valid from selfprivacy_api.utils.auth import is_token_valid
class IsAuthenticated(BasePermission): class IsAuthenticated(BasePermission):
"""Is authenticated permission""" """Is authenticated permission"""
message = "You must be authenticated to access this resource." message = "You must be authenticated to access this resource."
def has_permission(self, source: typing.Any, info: Info, **kwargs) -> bool: def has_permission(self, source: typing.Any, info: Info, **kwargs) -> bool:

View file

@ -18,20 +18,28 @@ from selfprivacy_api.utils.auth import (
get_token_name, get_token_name,
) )
def get_api_version() -> str: def get_api_version() -> str:
"""Get API version""" """Get API version"""
return "1.2.7" return "1.2.7"
@strawberry.type @strawberry.type
class ApiDevice: class ApiDevice:
"""A single device with SelfPrivacy app installed""" """A single device with SelfPrivacy app installed"""
name: str name: str
creation_date: datetime.datetime creation_date: datetime.datetime
is_caller: bool is_caller: bool
def get_devices() -> typing.List[ApiDevice]: def get_devices() -> typing.List[ApiDevice]:
"""Get list of devices""" """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) 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() tokens = get_tokens_info()
return [ return [
ApiDevice( ApiDevice(
@ -46,34 +54,52 @@ def get_devices() -> typing.List[ApiDevice]:
@strawberry.type @strawberry.type
class ApiRecoveryKeyStatus: class ApiRecoveryKeyStatus:
"""Recovery key status""" """Recovery key status"""
exists: bool exists: bool
valid: bool valid: bool
creation_date: typing.Optional[datetime.datetime] creation_date: typing.Optional[datetime.datetime]
expiration_date: typing.Optional[datetime.datetime] expiration_date: typing.Optional[datetime.datetime]
uses_left: typing.Optional[int] uses_left: typing.Optional[int]
def get_recovery_key_status() -> ApiRecoveryKeyStatus: def get_recovery_key_status() -> ApiRecoveryKeyStatus:
"""Get recovery key status""" """Get recovery key status"""
if not is_recovery_token_exists(): if not is_recovery_token_exists():
return ApiRecoveryKeyStatus( return ApiRecoveryKeyStatus(
exists=False, valid=False, creation_date=None, expiration_date=None, uses_left=None exists=False,
valid=False,
creation_date=None,
expiration_date=None,
uses_left=None,
) )
status = get_recovery_token_status() status = get_recovery_token_status()
if status is None: if status is None:
return ApiRecoveryKeyStatus( return ApiRecoveryKeyStatus(
exists=False, valid=False, creation_date=None, expiration_date=None, uses_left=None exists=False,
valid=False,
creation_date=None,
expiration_date=None,
uses_left=None,
) )
return ApiRecoveryKeyStatus( return ApiRecoveryKeyStatus(
exists=True, exists=True,
valid=is_recovery_token_valid(), valid=is_recovery_token_valid(),
creation_date=parse_date(status["date"]), creation_date=parse_date(status["date"]),
expiration_date=parse_date(status["expiration"]) if status["expiration"] is not None else None, expiration_date=parse_date(status["expiration"])
if status["expiration"] is not None
else None,
uses_left=status["uses_left"] if status["uses_left"] is not None else None, uses_left=status["uses_left"] if status["uses_left"] is not None else None,
) )
@strawberry.type @strawberry.type
class Api: class Api:
"""API access status""" """API access status"""
version: str = strawberry.field(resolver=get_api_version) version: str = strawberry.field(resolver=get_api_version)
devices: typing.List[ApiDevice] = strawberry.field(resolver=get_devices, permission_classes=[IsAuthenticated]) devices: typing.List[ApiDevice] = strawberry.field(
recovery_key: ApiRecoveryKeyStatus = strawberry.field(resolver=get_recovery_key_status, permission_classes=[IsAuthenticated]) resolver=get_devices, permission_classes=[IsAuthenticated]
)
recovery_key: ApiRecoveryKeyStatus = strawberry.field(
resolver=get_recovery_key_status, permission_classes=[IsAuthenticated]
)

View file

@ -4,22 +4,26 @@ import datetime
import typing import typing
import strawberry import strawberry
@strawberry.enum @strawberry.enum
class Severity(Enum): class Severity(Enum):
""" """
Severity of an alert. Severity of an alert.
""" """
INFO = "INFO" INFO = "INFO"
WARNING = "WARNING" WARNING = "WARNING"
ERROR = "ERROR" ERROR = "ERROR"
CRITICAL = "CRITICAL" CRITICAL = "CRITICAL"
SUCCESS = "SUCCESS" SUCCESS = "SUCCESS"
@strawberry.type @strawberry.type
class Alert: class Alert:
""" """
Alert type. Alert type.
""" """
severity: Severity severity: Severity
title: str title: str
message: str message: str

View file

@ -4,10 +4,12 @@ import datetime
import typing import typing
import strawberry import strawberry
@strawberry.enum @strawberry.enum
class DnsProvider(Enum): class DnsProvider(Enum):
CLOUDFLARE = "CLOUDFLARE" CLOUDFLARE = "CLOUDFLARE"
@strawberry.enum @strawberry.enum
class ServerProvider(Enum): class ServerProvider(Enum):
HETZNER = "HETZNER" HETZNER = "HETZNER"

View file

@ -1,68 +1,158 @@
"""Common system information and settings""" """Common system information and settings"""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
import subprocess
import typing import typing
import strawberry import strawberry
from selfprivacy_api.graphql.queries.common import Alert from selfprivacy_api.graphql.queries.common import Alert
from selfprivacy_api.graphql.queries.providers import DnsProvider, ServerProvider from selfprivacy_api.graphql.queries.providers import DnsProvider, ServerProvider
from selfprivacy_api.utils import ReadUserData
@strawberry.type @strawberry.type
class DnsRecord: class DnsRecord:
"""DNS record""" """DNS record"""
recordType: str recordType: str
name: str name: str
content: str content: str
ttl: int ttl: int
priority: typing.Optional[int] priority: typing.Optional[int]
@strawberry.type @strawberry.type
class SystemDomainInfo: class SystemDomainInfo:
"""Information about the system domain""" """Information about the system domain"""
domain: str domain: str
hostname: str hostname: str
provider: DnsProvider provider: DnsProvider
required_dns_records: typing.List[DnsRecord] required_dns_records: typing.List[DnsRecord]
def get_system_domain_info() -> SystemDomainInfo:
"""Get basic system domain info"""
with ReadUserData() as user_data:
return SystemDomainInfo(
domain=user_data["domain"],
hostname=user_data["hostname"],
provider=DnsProvider.CLOUDFLARE,
# TODO: get ip somehow
required_dns_records=[],
)
@strawberry.type @strawberry.type
class AutoUpgradeOptions: class AutoUpgradeOptions:
"""Automatic upgrade options""" """Automatic upgrade options"""
enable: bool enable: bool
allow_reboot: bool allow_reboot: bool
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"],
)
@strawberry.type @strawberry.type
class SshSettings: class SshSettings:
"""SSH settings and root SSH keys""" """SSH settings and root SSH keys"""
enable: bool enable: bool
password_authentication: bool password_authentication: bool
root_ssh_keys: typing.List[str] root_ssh_keys: typing.List[str]
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"],
)
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"]
@strawberry.type @strawberry.type
class SystemSettings: class SystemSettings:
"""Common system settings""" """Common system settings"""
auto_upgrade: AutoUpgradeOptions
ssh: SshSettings auto_upgrade: AutoUpgradeOptions = strawberry.field(
timezone: str resolver=get_auto_upgrade_options
)
ssh: SshSettings = strawberry.field(resolver=get_ssh_settings)
timezone: str = strawberry.field(resolver=get_system_timezone)
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()
@strawberry.type @strawberry.type
class SystemInfo: class SystemInfo:
"""System components versions""" """System components versions"""
system_version: str
python_version: str system_version: str = strawberry.field(resolver=get_system_version)
python_version: str = strawberry.field(resolver=get_python_version)
@strawberry.type @strawberry.type
class SystemProviderInfo: class SystemProviderInfo:
"""Information about the VPS/Dedicated server provider""" """Information about the VPS/Dedicated server provider"""
provider: ServerProvider provider: ServerProvider
id: str id: str
def get_system_provider_info() -> SystemProviderInfo:
"""Get system provider info"""
return SystemProviderInfo(provider=ServerProvider.HETZNER, id="UNKNOWN")
@strawberry.type @strawberry.type
class System: class System:
""" """
Base system type which represents common system status Base system type which represents common system status
""" """
status: Alert status: Alert
domain: SystemDomainInfo domain: SystemDomainInfo = strawberry.field(resolver=get_system_domain_info)
settings: SystemSettings settings: SystemSettings
info: SystemInfo info: SystemInfo
provider: SystemProviderInfo provider: SystemProviderInfo = strawberry.field(resolver=get_system_provider_info)
busy: bool busy: bool = False

View file

@ -10,10 +10,13 @@ from selfprivacy_api.graphql.queries.system import System
@strawberry.type @strawberry.type
class Query: class Query:
"""Root schema for queries""" """Root schema for queries"""
system: System system: System
@strawberry.field @strawberry.field
def api(self) -> Api: def api(self) -> Api:
"""API access status""" """API access status"""
return Api() return Api()
schema = strawberry.Schema(query=Query) schema = strawberry.Schema(query=Query)

View file

@ -3,6 +3,7 @@
from flask_restful import Resource from flask_restful import Resource
from selfprivacy_api.graphql.queries.api import get_api_version from selfprivacy_api.graphql.queries.api import get_api_version
class ApiVersion(Resource): class ApiVersion(Resource):
"""SelfPrivacy API version""" """SelfPrivacy API version"""

View file

@ -5,6 +5,10 @@ import subprocess
import pytz import pytz
from flask import Blueprint from flask import Blueprint
from flask_restful import Resource, Api, reqparse 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 from selfprivacy_api.utils import WriteUserData, ReadUserData
@ -256,9 +260,7 @@ class SystemVersion(Resource):
description: Unauthorized description: Unauthorized
""" """
return { return {
"system_version": subprocess.check_output(["uname", "-a"]) "system_version": get_system_version(),
.decode("utf-8")
.strip()
} }
@ -279,7 +281,7 @@ class PythonVersion(Resource):
401: 401:
description: Unauthorized description: Unauthorized
""" """
return subprocess.check_output(["python", "-V"]).decode("utf-8").strip() return get_python_version()
class PullRepositoryChanges(Resource): class PullRepositoryChanges(Resource):

View file

@ -121,7 +121,12 @@ def is_username_forbidden(username):
return False return False
def parse_date(date_str: str) -> datetime.datetime: def parse_date(date_str: str) -> datetime.datetime:
"""Parse date string which can be in """Parse date string which can be in
%Y-%m-%dT%H:%M:%S.%fZ or %Y-%m-%d %H:%M:%S.%f format""" %Y-%m-%dT%H:%M:%S.%fZ or %Y-%m-%d %H:%M:%S.%f format"""
return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ") if date_str.endswith("Z") else datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S.%f") return (
datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ")
if date_str.endswith("Z")
else datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S.%f")
)

11
tests/common.py Normal file
View file

@ -0,0 +1,11 @@
import json
def read_json(file_path):
with open(file_path, "r", encoding="utf-8") as file:
return json.load(file)
def write_json(file_path, data):
with open(file_path, "w", encoding="utf-8") as file:
json.dump(data, file, indent=4)

View file

@ -6,6 +6,8 @@ import re
import pytest import pytest
from mnemonic import Mnemonic from mnemonic import Mnemonic
from .common import read_json, write_json
TOKENS_FILE_CONTETS = { TOKENS_FILE_CONTETS = {
"tokens": [ "tokens": [
@ -23,16 +25,6 @@ TOKENS_FILE_CONTETS = {
} }
def read_json(file_path):
with open(file_path, "r", encoding="utf-8") as file:
return json.load(file)
def write_json(file_path, data):
with open(file_path, "w", encoding="utf-8") as file:
json.dump(data, file, indent=4)
def test_get_tokens_info(authorized_client, tokens_file): def test_get_tokens_info(authorized_client, tokens_file):
response = authorized_client.get("/auth/tokens") response = authorized_client.get("/auth/tokens")
assert response.status_code == 200 assert response.status_code == 200

View file

@ -0,0 +1,14 @@
{
"tokens": [
{
"token": "TEST_TOKEN",
"name": "test_token",
"date": "2022-01-14 08:31:10.789314"
},
{
"token": "TEST_TOKEN2",
"name": "test_token2",
"date": "2022-01-14 08:31:10.789314"
}
]
}

View file

@ -0,0 +1,112 @@
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
import json
import pytest
TOKENS_FILE_CONTETS = {
"tokens": [
{
"token": "TEST_TOKEN",
"name": "test_token",
"date": "2022-01-14 08:31:10.789314",
},
{
"token": "TEST_TOKEN2",
"name": "test_token2",
"date": "2022-01-14 08:31:10.789314",
},
]
}
def test_graphql_get_api_version(authorized_client):
response = authorized_client.get(
"/graphql",
json={
"query": """
query {
api {
version
}
}
"""
},
)
assert response.status_code == 200
assert "version" in response.get_json()["data"]["api"]
def test_graphql_api_version_unauthorized(client):
response = client.get(
"/graphql",
json={
"query": """
query {
api {
version
}
}
"""
},
)
assert response.status_code == 200
assert "version" in response.get_json()["data"]["api"]
def test_graphql_tokens_info(authorized_client, tokens_file):
response = authorized_client.get(
"/graphql",
json={
"query": """
query {
api {
devices {
creationDate
isCaller
name
}
}
}
"""
},
)
assert response.status_code == 200
assert response.json == {
"data": {
"api": {
"devices": [
{
"creationDate": "2022-01-14T08:31:10.789314",
"isCaller": True,
"name": "test_token",
},
{
"creationDate": "2022-01-14T08:31:10.789314",
"isCaller": False,
"name": "test_token2",
},
]
}
}
}
def test_graphql_tokens_info_unauthorized(client, tokens_file):
response = client.get(
"/graphql",
json={
"query": """
query {
api {
devices {
creationDate
isCaller
name
}
}
}
"""
},
)
assert response.status_code == 200
assert response.json["data"] is None