diff --git a/selfprivacy_api/models/services.py b/selfprivacy_api/models/services.py index 638ecf8..ea9b9dd 100644 --- a/selfprivacy_api/models/services.py +++ b/selfprivacy_api/models/services.py @@ -19,6 +19,7 @@ class ServiceDnsRecord(BaseModel): type: str name: str content: str + ttl: int display_name: str priority: Optional[int] = None diff --git a/selfprivacy_api/services/__init__.py b/selfprivacy_api/services/__init__.py index 83e804c..e829775 100644 --- a/selfprivacy_api/services/__init__.py +++ b/selfprivacy_api/services/__init__.py @@ -8,6 +8,7 @@ from os import makedirs from os import listdir from os.path import join +from shutil import copyfile, copytree, rmtree from selfprivacy_api.services.bitwarden import Bitwarden from selfprivacy_api.services.forgejo import Forgejo from selfprivacy_api.services.jitsimeet import JitsiMeet @@ -25,7 +26,7 @@ import selfprivacy_api.utils.network as network_utils from selfprivacy_api.services.api_icon import API_ICON from selfprivacy_api.utils import USERDATA_FILE, DKIM_DIR, SECRETS_FILE, get_domain from selfprivacy_api.utils.block_devices import BlockDevices -from shutil import copyfile, copytree, rmtree +from selfprivacy_api.utils import read_account_uri CONFIG_STASH_DIR = "/etc/selfprivacy/dump" @@ -61,8 +62,22 @@ class ServiceManager(Service): def get_all_required_dns_records() -> list[ServiceDnsRecord]: ip4 = network_utils.get_ip4() ip6 = network_utils.get_ip6() + dns_records: list[ServiceDnsRecord] = [] + try: + dns_records.append( + ServiceDnsRecord( + type="CAA", + name=get_domain(), + content=f'128 issue "letsencrypt.org;accounturi={read_account_uri()}"', + ttl=3600, + display_name="CAA record", + ) + ) + except Exception as e: + print(f"Error creating CAA: {e}") + for service in ServiceManager.get_enabled_services(): dns_records += service.get_dns_records(ip4, ip6) return dns_records diff --git a/selfprivacy_api/utils/__init__.py b/selfprivacy_api/utils/__init__.py index ba72378..a7cdb5f 100644 --- a/selfprivacy_api/utils/__init__.py +++ b/selfprivacy_api/utils/__init__.py @@ -7,6 +7,7 @@ import os import subprocess import portalocker import typing +import glob from traceback import format_tb as format_traceback @@ -20,6 +21,10 @@ USERDATA_FILE = "/etc/nixos/userdata.json" SECRETS_FILE = "/etc/selfprivacy/secrets.json" DKIM_DIR = "/var/dkim/" +ACCOUNT_PATH_PATTERN = ( + "/var/lib/acme/.lego/accounts/*/acme-v02.api.letsencrypt.org/*/account.json" +) + class UserDataFiles(Enum): """Enum for userdata files""" @@ -234,3 +239,16 @@ def write_to_log(message): def pretty_error(e: Exception) -> str: traceback = "/r".join(format_traceback(e.__traceback__)) return type(e).__name__ + ": " + str(e) + ": " + traceback + + +def read_account_uri() -> str: + account_file = glob.glob(ACCOUNT_PATH_PATTERN) + + if not account_file: + raise FileNotFoundError( + f"No account files found matching: {ACCOUNT_PATH_PATTERN}" + ) + + with open(account_file[0], "r") as file: + account_info = json.load(file) + return account_info["registration"]["uri"] diff --git a/tests/test_graphql/test_system.py b/tests/test_graphql/test_system.py index af3a01e..1f67498 100644 --- a/tests/test_graphql/test_system.py +++ b/tests/test_graphql/test_system.py @@ -18,6 +18,41 @@ from tests.test_graphql.common import ( from tests.test_dkim import no_dkim_file, dkim_file from tests.test_system import assert_provider +from unittest.mock import mock_open + + +@pytest.fixture +def account_file_mock(mocker, datadir): + mocker.patch( + "selfprivacy_api.utils.__init__.glob.glob", + return_value=[str(datadir / "account.json")], + ) + return datadir + + +@pytest.fixture +def account_file_404(mocker, datadir): + mocker.patch("selfprivacy_api.utils.__init__.glob.glob", return_value=[]) + return datadir + + +@pytest.fixture +def no_uri_account_file_mock(mocker, datadir): + mocker.patch( + "selfprivacy_api.utils.__init__.glob.glob", + return_value=[str(datadir / "no_uri_account.json")], + ) + return datadir + + +@pytest.fixture +def blank_account_file_mock(mocker, datadir): + mocker.patch( + "selfprivacy_api.utils.__init__.glob.glob", + return_value=[str(datadir / "blank_file_account.json")], + ) + return datadir + @pytest.fixture def turned_on(mocker, datadir): @@ -272,7 +307,12 @@ def is_dns_record_in_array(records, dns_record) -> bool: def test_graphql_get_domain( - authorized_client, mock_get_ip4, mock_get_ip6, turned_on, mock_dkim_key + authorized_client, + mock_get_ip4, + mock_get_ip6, + turned_on, + mock_dkim_key, + account_file_mock, ): """Test get domain""" response = authorized_client.post( @@ -354,10 +394,24 @@ def test_graphql_get_domain( ttl=18000, ), ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="CAA", + content='128 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/234340396"', + ttl=3600, + ), + ) def test_dns_records_no_duplicates( - authorized_client, mock_get_ip4, mock_get_ip6, turned_on, mock_dkim_key + authorized_client, + mock_get_ip4, + mock_get_ip6, + turned_on, + mock_dkim_key, + account_file_mock, ): """Check for duplicate DNS records""" response = authorized_client.post( @@ -387,6 +441,7 @@ def test_graphql_get_domain_no_dkim( mock_get_ip6, no_dkim_file, turned_on, + account_file_mock, ): """Test no DKIM file situation gets properly handled""" response = authorized_client.post( @@ -403,6 +458,303 @@ def test_graphql_get_domain_no_dkim( raise ValueError("unexpected record found:", record) +def test_graphql_get_domain_no_uri_account_file( + authorized_client, + mock_get_ip4, + mock_get_ip6, + turned_on, + mock_dkim_key, + no_uri_account_file_mock, +): + """Test get domain""" + response = authorized_client.post( + "/graphql", + json={ + "query": generate_system_query([API_GET_DOMAIN_INFO]), + }, + ) + assert response.status_code == 200 + assert response.json().get("data") is not None + assert ( + response.json()["data"]["system"]["domainInfo"]["domain"] == "test-domain.tld" + ) + assert ( + response.json()["data"]["system"]["domainInfo"]["hostname"] == "test-instance" + ) + assert response.json()["data"]["system"]["domainInfo"]["provider"] == "CLOUDFLARE" + dns_records = response.json()["data"]["system"]["domainInfo"]["requiredDnsRecords"] + assert is_dns_record_in_array(dns_records, dns_record()) + assert is_dns_record_in_array(dns_records, dns_record(record_type="AAAA")) + assert is_dns_record_in_array(dns_records, dns_record(name="api")) + assert is_dns_record_in_array( + dns_records, dns_record(name="api", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="cloud")) + assert is_dns_record_in_array( + dns_records, dns_record(name="cloud", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="git")) + assert is_dns_record_in_array( + dns_records, dns_record(name="git", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="meet")) + assert is_dns_record_in_array( + dns_records, dns_record(name="meet", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="password")) + assert is_dns_record_in_array( + dns_records, dns_record(name="password", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="social")) + assert is_dns_record_in_array( + dns_records, dns_record(name="social", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="vpn")) + assert is_dns_record_in_array( + dns_records, dns_record(name="vpn", record_type="AAAA") + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="MX", + content="test-domain.tld", + priority=10, + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="_dmarc", record_type="TXT", content="v=DMARC1; p=none", ttl=18000 + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="TXT", + content="v=spf1 a mx ip4:157.90.247.192 -all", + ttl=18000, + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="selector._domainkey", + record_type="TXT", + content="I am a DKIM key", + ttl=18000, + ), + ) + assert not is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="CAA", + content='128 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/234340396"', + ttl=3600, + ), + ) + + +def test_graphql_get_domain_not_found_account_file( + authorized_client, + mock_get_ip4, + mock_get_ip6, + turned_on, + mock_dkim_key, + account_file_404, +): + """Test get domain""" + response = authorized_client.post( + "/graphql", + json={ + "query": generate_system_query([API_GET_DOMAIN_INFO]), + }, + ) + assert response.status_code == 200 + assert response.json().get("data") is not None + assert ( + response.json()["data"]["system"]["domainInfo"]["domain"] == "test-domain.tld" + ) + assert ( + response.json()["data"]["system"]["domainInfo"]["hostname"] == "test-instance" + ) + assert response.json()["data"]["system"]["domainInfo"]["provider"] == "CLOUDFLARE" + dns_records = response.json()["data"]["system"]["domainInfo"]["requiredDnsRecords"] + assert is_dns_record_in_array(dns_records, dns_record()) + assert is_dns_record_in_array(dns_records, dns_record(record_type="AAAA")) + assert is_dns_record_in_array(dns_records, dns_record(name="api")) + assert is_dns_record_in_array( + dns_records, dns_record(name="api", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="cloud")) + assert is_dns_record_in_array( + dns_records, dns_record(name="cloud", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="git")) + assert is_dns_record_in_array( + dns_records, dns_record(name="git", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="meet")) + assert is_dns_record_in_array( + dns_records, dns_record(name="meet", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="password")) + assert is_dns_record_in_array( + dns_records, dns_record(name="password", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="social")) + assert is_dns_record_in_array( + dns_records, dns_record(name="social", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="vpn")) + assert is_dns_record_in_array( + dns_records, dns_record(name="vpn", record_type="AAAA") + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="MX", + content="test-domain.tld", + priority=10, + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="_dmarc", record_type="TXT", content="v=DMARC1; p=none", ttl=18000 + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="TXT", + content="v=spf1 a mx ip4:157.90.247.192 -all", + ttl=18000, + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="selector._domainkey", + record_type="TXT", + content="I am a DKIM key", + ttl=18000, + ), + ) + assert not is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="CAA", + content='128 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/234340396"', + ttl=3600, + ), + ) + + +def test_graphql_get_domain_black_account_file( + authorized_client, + mock_get_ip4, + mock_get_ip6, + turned_on, + mock_dkim_key, + blank_account_file_mock, +): + """Test get domain""" + response = authorized_client.post( + "/graphql", + json={ + "query": generate_system_query([API_GET_DOMAIN_INFO]), + }, + ) + assert response.status_code == 200 + assert response.json().get("data") is not None + assert ( + response.json()["data"]["system"]["domainInfo"]["domain"] == "test-domain.tld" + ) + assert ( + response.json()["data"]["system"]["domainInfo"]["hostname"] == "test-instance" + ) + assert response.json()["data"]["system"]["domainInfo"]["provider"] == "CLOUDFLARE" + dns_records = response.json()["data"]["system"]["domainInfo"]["requiredDnsRecords"] + assert is_dns_record_in_array(dns_records, dns_record()) + assert is_dns_record_in_array(dns_records, dns_record(record_type="AAAA")) + assert is_dns_record_in_array(dns_records, dns_record(name="api")) + assert is_dns_record_in_array( + dns_records, dns_record(name="api", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="cloud")) + assert is_dns_record_in_array( + dns_records, dns_record(name="cloud", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="git")) + assert is_dns_record_in_array( + dns_records, dns_record(name="git", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="meet")) + assert is_dns_record_in_array( + dns_records, dns_record(name="meet", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="password")) + assert is_dns_record_in_array( + dns_records, dns_record(name="password", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="social")) + assert is_dns_record_in_array( + dns_records, dns_record(name="social", record_type="AAAA") + ) + assert is_dns_record_in_array(dns_records, dns_record(name="vpn")) + assert is_dns_record_in_array( + dns_records, dns_record(name="vpn", record_type="AAAA") + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="MX", + content="test-domain.tld", + priority=10, + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="_dmarc", record_type="TXT", content="v=DMARC1; p=none", ttl=18000 + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="TXT", + content="v=spf1 a mx ip4:157.90.247.192 -all", + ttl=18000, + ), + ) + assert is_dns_record_in_array( + dns_records, + dns_record( + name="selector._domainkey", + record_type="TXT", + content="I am a DKIM key", + ttl=18000, + ), + ) + assert not is_dns_record_in_array( + dns_records, + dns_record( + name="test-domain.tld", + record_type="CAA", + content='128 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/234340396"', + ttl=3600, + ), + ) + + API_GET_TIMEZONE = """ settings { timezone diff --git a/tests/test_graphql/test_system/account.json b/tests/test_graphql/test_system/account.json new file mode 100644 index 0000000..ad9ac9e --- /dev/null +++ b/tests/test_graphql/test_system/account.json @@ -0,0 +1,12 @@ +{ + "email": "meowmeow@bloodwine.cyou", + "registration": { + "body": { + "status": "valid", + "contact": [ + "mailto:meow@bloodwine.cyou" + ] + }, + "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/234340396" + } +} \ No newline at end of file diff --git a/tests/test_graphql/test_system/blank_file_account.json b/tests/test_graphql/test_system/blank_file_account.json new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_graphql/test_system/no_uri_account.json b/tests/test_graphql/test_system/no_uri_account.json new file mode 100644 index 0000000..d68ba73 --- /dev/null +++ b/tests/test_graphql/test_system/no_uri_account.json @@ -0,0 +1,11 @@ +{ + "email": "meowmeow@bloodwine.cyou", + "registration": { + "body": { + "status": "valid", + "contact": [ + "mailto:meow@bloodwine.cyou" + ] + } + } +} \ No newline at end of file