mirror of
https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git
synced 2024-11-22 04:01:27 +00:00
refactor: Add more validation to server configuration
This commit is contained in:
parent
0a112f9c0a
commit
969b3b1417
|
@ -10,12 +10,6 @@ from selfprivacy_api.services import get_service_by_id, get_services_by_location
|
||||||
from selfprivacy_api.services import Service as ServiceInterface
|
from selfprivacy_api.services import Service as ServiceInterface
|
||||||
from selfprivacy_api.services import ServiceDnsRecord
|
from selfprivacy_api.services import ServiceDnsRecord
|
||||||
|
|
||||||
from selfprivacy_api.services.config_item import (
|
|
||||||
ServiceConfigItem,
|
|
||||||
StringServiceConfigItem,
|
|
||||||
BoolServiceConfigItem,
|
|
||||||
EnumServiceConfigItem,
|
|
||||||
)
|
|
||||||
from selfprivacy_api.utils.block_devices import BlockDevices
|
from selfprivacy_api.utils.block_devices import BlockDevices
|
||||||
from selfprivacy_api.utils.network import get_ip4, get_ip6
|
from selfprivacy_api.utils.network import get_ip4, get_ip6
|
||||||
|
|
||||||
|
@ -147,7 +141,7 @@ def config_item_to_graphql(item: dict) -> ConfigItem:
|
||||||
type=item_type,
|
type=item_type,
|
||||||
value=item["value"],
|
value=item["value"],
|
||||||
default_value=item["default_value"],
|
default_value=item["default_value"],
|
||||||
regex=item.get("regex")
|
regex=item.get("regex"),
|
||||||
)
|
)
|
||||||
elif item_type == "bool":
|
elif item_type == "bool":
|
||||||
return BoolConfigItem(
|
return BoolConfigItem(
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
"""Class representing Bitwarden service"""
|
"""Class representing Bitwarden service"""
|
||||||
import base64
|
import base64
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, List
|
from typing import List
|
||||||
|
|
||||||
from selfprivacy_api.utils import get_domain
|
|
||||||
|
|
||||||
from selfprivacy_api.utils.systemd import get_service_status
|
from selfprivacy_api.utils.systemd import get_service_status
|
||||||
from selfprivacy_api.services.service import Service, ServiceStatus
|
from selfprivacy_api.services.service import Service, ServiceStatus
|
||||||
|
@ -37,16 +35,6 @@ class Bitwarden(Service):
|
||||||
def get_user() -> str:
|
def get_user() -> str:
|
||||||
return "vaultwarden"
|
return "vaultwarden"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url(cls) -> Optional[str]:
|
|
||||||
"""Return service url."""
|
|
||||||
domain = get_domain()
|
|
||||||
return f"https://password.{domain}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_subdomain(cls) -> Optional[str]:
|
|
||||||
return "password"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_movable() -> bool:
|
def is_movable() -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -2,6 +2,8 @@ from abc import ABC, abstractmethod
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from selfprivacy_api.utils import check_if_subdomain_is_taken
|
||||||
|
|
||||||
|
|
||||||
class ServiceConfigItem(ABC):
|
class ServiceConfigItem(ABC):
|
||||||
id: str
|
id: str
|
||||||
|
@ -17,6 +19,10 @@ class ServiceConfigItem(ABC):
|
||||||
def set_value(self, value, service_options):
|
def set_value(self, value, service_options):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def validate_value(self, value):
|
||||||
|
return True
|
||||||
|
|
||||||
def as_dict(self, service_options):
|
def as_dict(self, service_options):
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
|
@ -35,18 +41,24 @@ class StringServiceConfigItem(ServiceConfigItem):
|
||||||
description: str,
|
description: str,
|
||||||
regex: Optional[str] = None,
|
regex: Optional[str] = None,
|
||||||
widget: Optional[str] = None,
|
widget: Optional[str] = None,
|
||||||
|
allow_empty: bool = False,
|
||||||
):
|
):
|
||||||
|
if widget == "subdomain" and not regex:
|
||||||
|
raise ValueError("Subdomain widget requires regex")
|
||||||
self.id = id
|
self.id = id
|
||||||
self.type = "string"
|
self.type = "string"
|
||||||
self.default_value = default_value
|
self.default_value = default_value
|
||||||
self.description = description
|
self.description = description
|
||||||
self.regex = re.compile(regex) if regex else None
|
self.regex = re.compile(regex) if regex else None
|
||||||
self.widget = widget if widget else "text"
|
self.widget = widget if widget else "text"
|
||||||
|
self.allow_empty = allow_empty
|
||||||
|
|
||||||
def get_value(self, service_options):
|
def get_value(self, service_options):
|
||||||
return service_options.get(self.id, self.default_value)
|
return service_options.get(self.id, self.default_value)
|
||||||
|
|
||||||
def set_value(self, value, service_options):
|
def set_value(self, value, service_options):
|
||||||
|
if not self.validate_value(value):
|
||||||
|
raise ValueError(f"Value {value} is not valid")
|
||||||
if self.regex and not self.regex.match(value):
|
if self.regex and not self.regex.match(value):
|
||||||
raise ValueError(f"Value {value} does not match regex {self.regex}")
|
raise ValueError(f"Value {value} does not match regex {self.regex}")
|
||||||
service_options[self.id] = value
|
service_options[self.id] = value
|
||||||
|
@ -62,6 +74,18 @@ class StringServiceConfigItem(ServiceConfigItem):
|
||||||
"regex": self.regex.pattern if self.regex else None,
|
"regex": self.regex.pattern if self.regex else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate_value(self, value):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return False
|
||||||
|
if not self.allow_empty and not value:
|
||||||
|
return False
|
||||||
|
if self.regex and not self.regex.match(value):
|
||||||
|
return False
|
||||||
|
if self.widget == "subdomain":
|
||||||
|
if check_if_subdomain_is_taken(value):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class BoolServiceConfigItem(ServiceConfigItem):
|
class BoolServiceConfigItem(ServiceConfigItem):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -81,6 +105,8 @@ class BoolServiceConfigItem(ServiceConfigItem):
|
||||||
return service_options.get(self.id, self.default_value)
|
return service_options.get(self.id, self.default_value)
|
||||||
|
|
||||||
def set_value(self, value, service_options):
|
def set_value(self, value, service_options):
|
||||||
|
if not isinstance(value, bool):
|
||||||
|
raise ValueError(f"Value {value} is not a boolean")
|
||||||
service_options[self.id] = value
|
service_options[self.id] = value
|
||||||
|
|
||||||
def as_dict(self, service_options):
|
def as_dict(self, service_options):
|
||||||
|
@ -93,6 +119,9 @@ class BoolServiceConfigItem(ServiceConfigItem):
|
||||||
"default_value": self.default_value,
|
"default_value": self.default_value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate_value(self, value):
|
||||||
|
return isinstance(value, bool)
|
||||||
|
|
||||||
|
|
||||||
class EnumServiceConfigItem(ServiceConfigItem):
|
class EnumServiceConfigItem(ServiceConfigItem):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -114,6 +143,8 @@ class EnumServiceConfigItem(ServiceConfigItem):
|
||||||
return service_options.get(self.id, self.default_value)
|
return service_options.get(self.id, self.default_value)
|
||||||
|
|
||||||
def set_value(self, value, service_options):
|
def set_value(self, value, service_options):
|
||||||
|
if not self.validate_value(value):
|
||||||
|
raise ValueError(f"Value {value} is not valid")
|
||||||
if value not in self.options:
|
if value not in self.options:
|
||||||
raise ValueError(f"Value {value} not in options {self.options}")
|
raise ValueError(f"Value {value} not in options {self.options}")
|
||||||
service_options[self.id] = value
|
service_options[self.id] = value
|
||||||
|
@ -129,6 +160,11 @@ class EnumServiceConfigItem(ServiceConfigItem):
|
||||||
"options": self.options,
|
"options": self.options,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate_value(self, value):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return False
|
||||||
|
return value in self.options
|
||||||
|
|
||||||
|
|
||||||
# TODO: unused for now
|
# TODO: unused for now
|
||||||
class IntServiceConfigItem(ServiceConfigItem):
|
class IntServiceConfigItem(ServiceConfigItem):
|
||||||
|
@ -156,7 +192,9 @@ class IntServiceConfigItem(ServiceConfigItem):
|
||||||
if self.min_value is not None and value < self.min_value:
|
if self.min_value is not None and value < self.min_value:
|
||||||
raise ValueError(f"Value {value} is less than min_value {self.min_value}")
|
raise ValueError(f"Value {value} is less than min_value {self.min_value}")
|
||||||
if self.max_value is not None and value > self.max_value:
|
if self.max_value is not None and value > self.max_value:
|
||||||
raise ValueError(f"Value {value} is greater than max_value {self.max_value}")
|
raise ValueError(
|
||||||
|
f"Value {value} is greater than max_value {self.max_value}"
|
||||||
|
)
|
||||||
service_options[self.id] = value
|
service_options[self.id] = value
|
||||||
|
|
||||||
def as_dict(self, service_options):
|
def as_dict(self, service_options):
|
||||||
|
@ -170,3 +208,10 @@ class IntServiceConfigItem(ServiceConfigItem):
|
||||||
"min_value": self.min_value,
|
"min_value": self.min_value,
|
||||||
"max_value": self.max_value,
|
"max_value": self.max_value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate_value(self, value):
|
||||||
|
if not isinstance(value, int):
|
||||||
|
return False
|
||||||
|
return (self.min_value is None or value >= self.min_value) and (
|
||||||
|
self.max_value is None or value <= self.max_value
|
||||||
|
)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
"""Class representing Bitwarden service"""
|
"""Class representing Bitwarden service"""
|
||||||
import base64
|
import base64
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, List
|
from typing import List
|
||||||
|
|
||||||
from selfprivacy_api.utils import get_domain, ReadUserData, WriteUserData
|
from selfprivacy_api.utils import ReadUserData, WriteUserData
|
||||||
|
|
||||||
from selfprivacy_api.utils.systemd import get_service_status
|
from selfprivacy_api.utils.systemd import get_service_status
|
||||||
from selfprivacy_api.services.service import Service, ServiceStatus
|
from selfprivacy_api.services.service import Service, ServiceStatus
|
||||||
|
@ -14,6 +14,7 @@ from selfprivacy_api.services.config_item import (
|
||||||
EnumServiceConfigItem,
|
EnumServiceConfigItem,
|
||||||
ServiceConfigItem,
|
ServiceConfigItem,
|
||||||
)
|
)
|
||||||
|
from selfprivacy_api.utils.regex_strings import SUBDOMAIN_REGEX
|
||||||
|
|
||||||
|
|
||||||
class Forgejo(Service):
|
class Forgejo(Service):
|
||||||
|
@ -27,7 +28,7 @@ class Forgejo(Service):
|
||||||
id="subdomain",
|
id="subdomain",
|
||||||
default_value="git",
|
default_value="git",
|
||||||
description="Subdomain",
|
description="Subdomain",
|
||||||
regex=r"^[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]$",
|
regex=SUBDOMAIN_REGEX,
|
||||||
widget="subdomain",
|
widget="subdomain",
|
||||||
),
|
),
|
||||||
"appName": StringServiceConfigItem(
|
"appName": StringServiceConfigItem(
|
||||||
|
@ -90,21 +91,6 @@ class Forgejo(Service):
|
||||||
"""Read SVG icon from file and return it as base64 encoded string."""
|
"""Read SVG icon from file and return it as base64 encoded string."""
|
||||||
return base64.b64encode(FORGEJO_ICON.encode("utf-8")).decode("utf-8")
|
return base64.b64encode(FORGEJO_ICON.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url(cls) -> Optional[str]:
|
|
||||||
"""Return service url."""
|
|
||||||
domain = get_domain()
|
|
||||||
subdomain = cls.get_subdomain()
|
|
||||||
return f"https://{subdomain}.{domain}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_subdomain(cls) -> Optional[str]:
|
|
||||||
with ReadUserData() as data:
|
|
||||||
if "gitea" in data["modules"]:
|
|
||||||
return data["modules"]["gitea"]["subdomain"]
|
|
||||||
|
|
||||||
return "git"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_movable() -> bool:
|
def is_movable() -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
"""Class representing Jitsi Meet service"""
|
"""Class representing Jitsi Meet service"""
|
||||||
import base64
|
import base64
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, List
|
from typing import List
|
||||||
|
|
||||||
from selfprivacy_api.jobs import Job
|
from selfprivacy_api.jobs import Job
|
||||||
from selfprivacy_api.utils.systemd import (
|
from selfprivacy_api.utils.systemd import (
|
||||||
get_service_status_from_several_units,
|
get_service_status_from_several_units,
|
||||||
)
|
)
|
||||||
from selfprivacy_api.services.service import Service, ServiceStatus
|
from selfprivacy_api.services.service import Service, ServiceStatus
|
||||||
from selfprivacy_api.utils import get_domain
|
|
||||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||||
from selfprivacy_api.services.jitsimeet.icon import JITSI_ICON
|
from selfprivacy_api.services.jitsimeet.icon import JITSI_ICON
|
||||||
|
|
||||||
|
@ -36,16 +35,6 @@ class JitsiMeet(Service):
|
||||||
"""Read SVG icon from file and return it as base64 encoded string."""
|
"""Read SVG icon from file and return it as base64 encoded string."""
|
||||||
return base64.b64encode(JITSI_ICON.encode("utf-8")).decode("utf-8")
|
return base64.b64encode(JITSI_ICON.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url(cls) -> Optional[str]:
|
|
||||||
"""Return service url."""
|
|
||||||
domain = get_domain()
|
|
||||||
return f"https://meet.{domain}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_subdomain(cls) -> Optional[str]:
|
|
||||||
return "meet"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_movable() -> bool:
|
def is_movable() -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
"""Class representing Nextcloud service."""
|
"""Class representing Nextcloud service."""
|
||||||
import base64
|
import base64
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, List
|
from typing import List
|
||||||
|
|
||||||
from selfprivacy_api.utils import get_domain
|
|
||||||
|
|
||||||
from selfprivacy_api.utils.systemd import get_service_status
|
from selfprivacy_api.utils.systemd import get_service_status
|
||||||
from selfprivacy_api.services.service import Service, ServiceStatus
|
from selfprivacy_api.services.service import Service, ServiceStatus
|
||||||
|
@ -34,16 +32,6 @@ class Nextcloud(Service):
|
||||||
"""Read SVG icon from file and return it as base64 encoded string."""
|
"""Read SVG icon from file and return it as base64 encoded string."""
|
||||||
return base64.b64encode(NEXTCLOUD_ICON.encode("utf-8")).decode("utf-8")
|
return base64.b64encode(NEXTCLOUD_ICON.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url(cls) -> Optional[str]:
|
|
||||||
"""Return service url."""
|
|
||||||
domain = get_domain()
|
|
||||||
return f"https://cloud.{domain}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_subdomain(cls) -> Optional[str]:
|
|
||||||
return "cloud"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_movable() -> bool:
|
def is_movable() -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -33,10 +33,6 @@ class Ocserv(Service):
|
||||||
"""Return service url."""
|
"""Return service url."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_subdomain(cls) -> typing.Optional[str]:
|
|
||||||
return "vpn"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_movable() -> bool:
|
def is_movable() -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
"""Class representing Nextcloud service."""
|
"""Class representing Nextcloud service."""
|
||||||
import base64
|
import base64
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, List
|
from typing import List
|
||||||
|
|
||||||
from selfprivacy_api.utils import get_domain
|
|
||||||
|
|
||||||
from selfprivacy_api.services.owned_path import OwnedPath
|
from selfprivacy_api.services.owned_path import OwnedPath
|
||||||
from selfprivacy_api.utils.systemd import get_service_status
|
from selfprivacy_api.utils.systemd import get_service_status
|
||||||
|
@ -31,16 +29,6 @@ class Pleroma(Service):
|
||||||
def get_svg_icon() -> str:
|
def get_svg_icon() -> str:
|
||||||
return base64.b64encode(PLEROMA_ICON.encode("utf-8")).decode("utf-8")
|
return base64.b64encode(PLEROMA_ICON.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url(cls) -> Optional[str]:
|
|
||||||
"""Return service url."""
|
|
||||||
domain = get_domain()
|
|
||||||
return f"https://social.{domain}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_subdomain(cls) -> Optional[str]:
|
|
||||||
return "social"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_movable() -> bool:
|
def is_movable() -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -2,14 +2,13 @@
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from selfprivacy_api.jobs import Job
|
from selfprivacy_api.jobs import Job
|
||||||
from selfprivacy_api.utils.systemd import (
|
from selfprivacy_api.utils.systemd import (
|
||||||
get_service_status_from_several_units,
|
get_service_status_from_several_units,
|
||||||
)
|
)
|
||||||
from selfprivacy_api.services.service import Service, ServiceStatus
|
from selfprivacy_api.services.service import Service, ServiceStatus
|
||||||
from selfprivacy_api.utils import ReadUserData, get_domain
|
|
||||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||||
from selfprivacy_api.services.roundcube.icon import ROUNDCUBE_ICON
|
from selfprivacy_api.services.roundcube.icon import ROUNDCUBE_ICON
|
||||||
|
|
||||||
|
@ -37,21 +36,6 @@ class Roundcube(Service):
|
||||||
"""Read SVG icon from file and return it as base64 encoded string."""
|
"""Read SVG icon from file and return it as base64 encoded string."""
|
||||||
return base64.b64encode(ROUNDCUBE_ICON.encode("utf-8")).decode("utf-8")
|
return base64.b64encode(ROUNDCUBE_ICON.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url(cls) -> Optional[str]:
|
|
||||||
"""Return service url."""
|
|
||||||
domain = get_domain()
|
|
||||||
subdomain = cls.get_subdomain()
|
|
||||||
return f"https://{subdomain}.{domain}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_subdomain(cls) -> Optional[str]:
|
|
||||||
with ReadUserData() as data:
|
|
||||||
if "roundcube" in data["modules"]:
|
|
||||||
return data["modules"]["roundcube"]["subdomain"]
|
|
||||||
|
|
||||||
return "roundcube"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_movable() -> bool:
|
def is_movable() -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
"""Abstract class for a service running on a server"""
|
"""Abstract class for a service running on a server"""
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from selfprivacy_api import utils
|
from selfprivacy_api import utils
|
||||||
from selfprivacy_api.utils import ReadUserData, WriteUserData
|
from selfprivacy_api.utils.default_subdomains import DEFAULT_SUBDOMAINS
|
||||||
|
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
|
||||||
from selfprivacy_api.utils.waitloop import wait_until_true
|
from selfprivacy_api.utils.waitloop import wait_until_true
|
||||||
from selfprivacy_api.utils.block_devices import BlockDevice, BlockDevices
|
from selfprivacy_api.utils.block_devices import BlockDevice, BlockDevices
|
||||||
|
|
||||||
|
@ -66,20 +67,27 @@ class Service(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
|
||||||
def get_url(cls) -> Optional[str]:
|
def get_url(cls) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
The url of the service if it is accessible from the internet browser.
|
The url of the service if it is accessible from the internet browser.
|
||||||
"""
|
"""
|
||||||
pass
|
domain = get_domain()
|
||||||
|
subdomain = cls.get_subdomain()
|
||||||
|
return f"https://{subdomain}.{domain}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
|
||||||
def get_subdomain(cls) -> Optional[str]:
|
def get_subdomain(cls) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
The assigned primary subdomain for this service.
|
The assigned primary subdomain for this service.
|
||||||
"""
|
"""
|
||||||
pass
|
name = cls.get_id()
|
||||||
|
with ReadUserData() as user_data:
|
||||||
|
if "modules" in user_data:
|
||||||
|
if name in user_data["modules"]:
|
||||||
|
if "subdomain" in user_data["modules"][name]:
|
||||||
|
return user_data["modules"][name]["subdomain"]
|
||||||
|
|
||||||
|
return DEFAULT_SUBDOMAINS.get(name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user(cls) -> Optional[str]:
|
def get_user(cls) -> Optional[str]:
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"""Class representing Bitwarden service"""
|
"""Class representing Bitwarden service"""
|
||||||
import base64
|
import base64
|
||||||
import typing
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -57,16 +56,6 @@ class DummyService(Service):
|
||||||
# return ""
|
# return ""
|
||||||
return base64.b64encode(BITWARDEN_ICON.encode("utf-8")).decode("utf-8")
|
return base64.b64encode(BITWARDEN_ICON.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url(cls) -> typing.Optional[str]:
|
|
||||||
"""Return service url."""
|
|
||||||
domain = "test.com"
|
|
||||||
return f"https://password.{domain}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_subdomain(cls) -> typing.Optional[str]:
|
|
||||||
return "password"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_movable(cls) -> bool:
|
def is_movable(cls) -> bool:
|
||||||
return cls.movable
|
return cls.movable
|
||||||
|
|
|
@ -8,6 +8,11 @@ import subprocess
|
||||||
import portalocker
|
import portalocker
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from selfprivacy_api.utils.default_subdomains import (
|
||||||
|
DEFAULT_SUBDOMAINS,
|
||||||
|
RESERVED_SUBDOMAINS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
USERDATA_FILE = "/etc/nixos/userdata.json"
|
USERDATA_FILE = "/etc/nixos/userdata.json"
|
||||||
SECRETS_FILE = "/etc/selfprivacy/secrets.json"
|
SECRETS_FILE = "/etc/selfprivacy/secrets.json"
|
||||||
|
@ -133,6 +138,20 @@ def is_username_forbidden(username):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_subdomain_is_taken(subdomain: str) -> bool:
|
||||||
|
"""Check if subdomain is already taken or reserved"""
|
||||||
|
if subdomain in RESERVED_SUBDOMAINS:
|
||||||
|
return True
|
||||||
|
with ReadUserData() as data:
|
||||||
|
for module in data["modules"]:
|
||||||
|
if (
|
||||||
|
data["modules"][module].get("subdomain", DEFAULT_SUBDOMAINS[module])
|
||||||
|
== subdomain
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
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 one of these formats:
|
"""Parse date string which can be in one of these formats:
|
||||||
- %Y-%m-%dT%H:%M:%S.%fZ
|
- %Y-%m-%dT%H:%M:%S.%fZ
|
||||||
|
|
21
selfprivacy_api/utils/default_subdomains.py
Normal file
21
selfprivacy_api/utils/default_subdomains.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
DEFAULT_SUBDOMAINS = {
|
||||||
|
"bitwarden": "password",
|
||||||
|
"gitea": "git",
|
||||||
|
"jitsi-meet": "meet",
|
||||||
|
"simple-nixos-mailserver": "",
|
||||||
|
"nextcloud": "cloud",
|
||||||
|
"ocserv": "vpn",
|
||||||
|
"pleroma": "social",
|
||||||
|
"roundcube": "roundcube",
|
||||||
|
"testservice": "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
RESERVED_SUBDOMAINS = [
|
||||||
|
"admin",
|
||||||
|
"administrator",
|
||||||
|
"api",
|
||||||
|
"auth",
|
||||||
|
"user",
|
||||||
|
"users",
|
||||||
|
"ntfy",
|
||||||
|
]
|
1
selfprivacy_api/utils/regex_strings.py
Normal file
1
selfprivacy_api/utils/regex_strings.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
SUBDOMAIN_REGEX = r"^[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]$"
|
157
tests/test_config_item.py
Normal file
157
tests/test_config_item.py
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import pytest
|
||||||
|
from selfprivacy_api.services.config_item import (
|
||||||
|
StringServiceConfigItem,
|
||||||
|
BoolServiceConfigItem,
|
||||||
|
EnumServiceConfigItem,
|
||||||
|
)
|
||||||
|
from selfprivacy_api.utils.regex_strings import SUBDOMAIN_REGEX
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def service_options():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_service_config_item(service_options):
|
||||||
|
item = StringServiceConfigItem(
|
||||||
|
id="test_string",
|
||||||
|
default_value="1337",
|
||||||
|
description="Test digits string",
|
||||||
|
regex=r"^\d+$",
|
||||||
|
widget="text",
|
||||||
|
allow_empty=False,
|
||||||
|
)
|
||||||
|
assert item.get_value(service_options) == "1337"
|
||||||
|
item.set_value("123", service_options)
|
||||||
|
assert item.get_value(service_options) == "123"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
item.set_value("abc", service_options)
|
||||||
|
assert item.validate_value("123") is True
|
||||||
|
assert item.validate_value("abc") is False
|
||||||
|
assert item.validate_value("123abc") is False
|
||||||
|
assert item.validate_value("") is False
|
||||||
|
assert item.validate_value(None) is False
|
||||||
|
assert item.validate_value(123) is False
|
||||||
|
assert item.validate_value("123.0") is False
|
||||||
|
assert item.validate_value(True) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_service_config_item_allows_empty(service_options):
|
||||||
|
item = StringServiceConfigItem(
|
||||||
|
id="test_string",
|
||||||
|
default_value="1337",
|
||||||
|
description="Test digits string",
|
||||||
|
widget="text",
|
||||||
|
allow_empty=True,
|
||||||
|
)
|
||||||
|
assert item.get_value(service_options) == "1337"
|
||||||
|
item.set_value("", service_options)
|
||||||
|
assert item.get_value(service_options) == ""
|
||||||
|
assert item.validate_value("") is True
|
||||||
|
assert item.validate_value(None) is False
|
||||||
|
assert item.validate_value(123) is False
|
||||||
|
assert item.validate_value("123") is True
|
||||||
|
assert item.validate_value("abc") is True
|
||||||
|
assert item.validate_value("123abc") is True
|
||||||
|
assert item.validate_value("123.0") is True
|
||||||
|
assert item.validate_value(True) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_service_config_item_not_allows_empty(service_options):
|
||||||
|
item = StringServiceConfigItem(
|
||||||
|
id="test_string",
|
||||||
|
default_value="1337",
|
||||||
|
description="Test digits string",
|
||||||
|
widget="text",
|
||||||
|
)
|
||||||
|
assert item.get_value(service_options) == "1337"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
item.set_value("", service_options)
|
||||||
|
assert item.get_value(service_options) == "1337"
|
||||||
|
assert item.validate_value("") is False
|
||||||
|
assert item.validate_value(None) is False
|
||||||
|
assert item.validate_value(123) is False
|
||||||
|
assert item.validate_value("123") is True
|
||||||
|
assert item.validate_value("abc") is True
|
||||||
|
assert item.validate_value("123abc") is True
|
||||||
|
assert item.validate_value("123.0") is True
|
||||||
|
assert item.validate_value(True) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_bool_service_config_item(service_options):
|
||||||
|
item = BoolServiceConfigItem(
|
||||||
|
id="test_bool",
|
||||||
|
default_value=True,
|
||||||
|
description="Test bool",
|
||||||
|
widget="switch",
|
||||||
|
)
|
||||||
|
assert item.get_value(service_options) is True
|
||||||
|
item.set_value(False, service_options)
|
||||||
|
assert item.get_value(service_options) is False
|
||||||
|
assert item.validate_value(True) is True
|
||||||
|
assert item.validate_value(False) is True
|
||||||
|
assert item.validate_value("True") is False
|
||||||
|
assert item.validate_value("False") is False
|
||||||
|
assert item.validate_value(1) is False
|
||||||
|
assert item.validate_value(0) is False
|
||||||
|
assert item.validate_value("1") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_service_config_item(service_options):
|
||||||
|
item = EnumServiceConfigItem(
|
||||||
|
id="test_enum",
|
||||||
|
default_value="option1",
|
||||||
|
description="Test enum",
|
||||||
|
options=["option1", "option2", "option3"],
|
||||||
|
widget="select",
|
||||||
|
)
|
||||||
|
assert item.get_value(service_options) == "option1"
|
||||||
|
item.set_value("option2", service_options)
|
||||||
|
assert item.get_value(service_options) == "option2"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
item.set_value("option4", service_options)
|
||||||
|
assert item.validate_value("option1") is True
|
||||||
|
assert item.validate_value("option4") is False
|
||||||
|
assert item.validate_value("option2") is True
|
||||||
|
assert item.validate_value(1) is False
|
||||||
|
assert item.validate_value("1") is False
|
||||||
|
assert item.validate_value(True) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_service_config_item_subdomain(service_options, dummy_service):
|
||||||
|
item = StringServiceConfigItem(
|
||||||
|
id="test_subdomain",
|
||||||
|
default_value="example",
|
||||||
|
description="Test subdomain string",
|
||||||
|
widget="subdomain",
|
||||||
|
allow_empty=False,
|
||||||
|
regex=SUBDOMAIN_REGEX,
|
||||||
|
)
|
||||||
|
assert item.get_value(service_options) == "example"
|
||||||
|
item.set_value("subdomain", service_options)
|
||||||
|
assert item.get_value(service_options) == "subdomain"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
item.set_value(
|
||||||
|
"invalid-subdomain-because-it-is-very-very-very-very-very-very-long",
|
||||||
|
service_options,
|
||||||
|
)
|
||||||
|
assert item.validate_value("subdomain") is True
|
||||||
|
assert (
|
||||||
|
item.validate_value(
|
||||||
|
"invalid-subdomain-because-it-is-very-very-very-very-very-very-long"
|
||||||
|
)
|
||||||
|
is False
|
||||||
|
)
|
||||||
|
assert item.validate_value("api") is False
|
||||||
|
assert item.validate_value("auth") is False
|
||||||
|
assert item.validate_value("user") is False
|
||||||
|
assert item.validate_value("users") is False
|
||||||
|
assert item.validate_value("ntfy") is False
|
||||||
|
assert item.validate_value("") is False
|
||||||
|
assert item.validate_value(None) is False
|
||||||
|
assert item.validate_value(123) is False
|
||||||
|
assert item.validate_value("123") is True
|
||||||
|
assert item.validate_value("abc") is True
|
||||||
|
assert item.validate_value("123abc") is True
|
||||||
|
assert item.validate_value("123.0") is False
|
||||||
|
assert item.validate_value(True) is False
|
|
@ -188,6 +188,7 @@ allServices {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
isEnabled
|
isEnabled
|
||||||
|
url
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -347,6 +348,7 @@ def test_get_services(authorized_client, only_dummy_service):
|
||||||
assert api_dummy_service["id"] == "testservice"
|
assert api_dummy_service["id"] == "testservice"
|
||||||
assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value
|
assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value
|
||||||
assert api_dummy_service["isEnabled"] is True
|
assert api_dummy_service["isEnabled"] is True
|
||||||
|
assert api_dummy_service["url"] == "https://test.test-domain.tld"
|
||||||
|
|
||||||
|
|
||||||
def test_enable_return_value(authorized_client, only_dummy_service):
|
def test_enable_return_value(authorized_client, only_dummy_service):
|
||||||
|
|
Loading…
Reference in a new issue