mirror of
https://github.com/usestrix/strix.git
synced 2026-10-11 03:37:54 +00:00
fix: add provider domain verification classifier and tests
This commit is contained in:
parent
3f4bf25309
commit
8e3c7aee7a
2 changed files with 65 additions and 216 deletions
|
|
@ -1,144 +1,85 @@
|
|||
"""Domain verification for cloud-hosted applications.
|
||||
"""Classify provider-managed domains for cloud verification.
|
||||
|
||||
Handles verification of domains across various cloud providers and custom domains.
|
||||
Supports both custom DNS verification and provider-specific domain verification flows.
|
||||
Provider default hostnames are not proof of ownership by themselves. This
|
||||
module only selects the verification strategy; the managed API remains the
|
||||
authority that checks the authenticated provider integration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@dataclass(frozen=True)
|
||||
class ProviderDomain:
|
||||
"""Metadata for a provider-owned default hostname suffix."""
|
||||
|
||||
# Cloud provider default domain patterns that don't require custom DNS verification
|
||||
PROVIDER_DEFAULT_DOMAINS = {
|
||||
# Vercel free tier domains
|
||||
"vercel.app": {"provider": "vercel", "requires_dns": False},
|
||||
# Netlify default domains
|
||||
"netlify.app": {"provider": "netlify", "requires_dns": False},
|
||||
# GitHub Pages
|
||||
"github.io": {"provider": "github", "requires_dns": False},
|
||||
# Heroku
|
||||
"herokuapp.com": {"provider": "heroku", "requires_dns": False},
|
||||
provider: str
|
||||
requires_dns: bool = False
|
||||
|
||||
|
||||
PROVIDER_DEFAULT_DOMAINS: Final[dict[str, ProviderDomain]] = {
|
||||
"vercel.app": ProviderDomain("vercel"),
|
||||
"netlify.app": ProviderDomain("netlify"),
|
||||
"github.io": ProviderDomain("github"),
|
||||
"herokuapp.com": ProviderDomain("heroku"),
|
||||
}
|
||||
|
||||
|
||||
def is_provider_default_domain(domain: str) -> bool:
|
||||
"""Check if a domain is a default provider-hosted domain that doesn't require custom DNS.
|
||||
def _normalise(domain: str | None) -> str:
|
||||
"""Return a comparable hostname, without a trailing root dot."""
|
||||
if not isinstance(domain, str):
|
||||
return ""
|
||||
return domain.strip().lower().rstrip(".")
|
||||
|
||||
Args:
|
||||
domain: The domain name to check (e.g., 'demo.vercel.app')
|
||||
|
||||
Returns:
|
||||
True if the domain matches a known provider default domain pattern.
|
||||
def get_provider_info(domain: str | None) -> ProviderDomain | None:
|
||||
"""Return provider metadata when *domain* is a provider default hostname.
|
||||
|
||||
Examples:
|
||||
>>> is_provider_default_domain('demo.vercel.app')
|
||||
True
|
||||
>>> is_provider_default_domain('my-site.netlify.app')
|
||||
True
|
||||
>>> is_provider_default_domain('example.com')
|
||||
False
|
||||
Matching is label-aware: ``notvercel.app`` and ``example.vercel.app.evil``
|
||||
do not match ``vercel.app``.
|
||||
"""
|
||||
if not domain:
|
||||
return False
|
||||
|
||||
domain_lower = domain.lower().rstrip(".")
|
||||
|
||||
# Check for direct matches and suffix matches
|
||||
for pattern in PROVIDER_DEFAULT_DOMAINS:
|
||||
if domain_lower == pattern or domain_lower.endswith(f".{pattern}"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_provider_info(domain: str) -> dict[str, Any] | None:
|
||||
"""Get provider information for a provider-default domain.
|
||||
|
||||
Args:
|
||||
domain: The domain name to check
|
||||
|
||||
Returns:
|
||||
Dictionary with provider info (provider name, whether DNS is required),
|
||||
or None if not a recognized provider domain.
|
||||
"""
|
||||
if not domain:
|
||||
hostname = _normalise(domain)
|
||||
if not hostname or any(len(label) > 63 for label in hostname.split(".")):
|
||||
return None
|
||||
|
||||
domain_lower = domain.lower().rstrip(".")
|
||||
|
||||
for pattern, info in PROVIDER_DEFAULT_DOMAINS.items():
|
||||
if domain_lower == pattern or domain_lower.endswith(f".{pattern}"):
|
||||
for suffix, info in PROVIDER_DEFAULT_DOMAINS.items():
|
||||
if hostname.endswith(f".{suffix}"):
|
||||
return info
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_domain_verification_method(domain: str) -> tuple[bool, str]:
|
||||
"""Determine the appropriate verification method for a domain.
|
||||
def is_provider_default_domain(domain: str | None) -> bool:
|
||||
"""Return whether *domain* is hosted under a known provider suffix."""
|
||||
return get_provider_info(domain) is not None
|
||||
|
||||
Args:
|
||||
domain: The domain to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (can_verify, verification_method)
|
||||
- can_verify: Whether verification is possible
|
||||
- verification_method: Either "dns", "provider", or "unknown"
|
||||
|
||||
Examples:
|
||||
>>> validate_domain_verification_method('demo.vercel.app')
|
||||
(True, 'provider')
|
||||
>>> validate_domain_verification_method('custom.example.com')
|
||||
(True, 'dns')
|
||||
"""
|
||||
if not domain:
|
||||
def validate_domain_verification_method(domain: str | None) -> tuple[bool, str]:
|
||||
"""Select ``provider`` for default hostnames and ``dns`` otherwise."""
|
||||
hostname = _normalise(domain)
|
||||
if not hostname or any(not label for label in hostname.split(".")):
|
||||
return False, "unknown"
|
||||
|
||||
# Check if it's a provider default domain
|
||||
provider_info = get_provider_info(domain)
|
||||
if provider_info and not provider_info.get("requires_dns", True):
|
||||
info = get_provider_info(hostname)
|
||||
if info is not None and not info.requires_dns:
|
||||
return True, "provider"
|
||||
|
||||
# For other domains, use DNS verification
|
||||
return True, "dns"
|
||||
|
||||
|
||||
async def verify_provider_domain(
|
||||
domain: str,
|
||||
domain: str | None,
|
||||
provider_token: str | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""Verify a provider-hosted domain through the provider's API.
|
||||
"""Return the provider verification decision for API callers.
|
||||
|
||||
Args:
|
||||
domain: The provider domain (e.g., 'demo.vercel.app')
|
||||
provider_token: Optional provider API token for verification
|
||||
|
||||
Returns:
|
||||
Tuple of (verified, message)
|
||||
|
||||
Note:
|
||||
This is a placeholder for actual provider-specific verification.
|
||||
Real implementation should call the provider's domain verification API.
|
||||
The CLI cannot prove that a hostname belongs to the user without calling
|
||||
the managed API. ``provider_token`` is intentionally accepted for API
|
||||
integrations, but is never logged or sent anywhere by this helper.
|
||||
"""
|
||||
provider_info = get_provider_info(domain)
|
||||
if not provider_info:
|
||||
del provider_token
|
||||
info = get_provider_info(domain)
|
||||
if info is None:
|
||||
return False, f"Domain {domain} is not a recognized provider domain"
|
||||
|
||||
provider = provider_info.get("provider", "unknown")
|
||||
|
||||
# For Vercel domains, the domain ownership is implicit if it exists in the
|
||||
# user's Vercel account, which is validated during the connection setup.
|
||||
if provider == "vercel":
|
||||
# In a real implementation, you would call the Vercel API to list
|
||||
# the user's deployments and verify the domain exists.
|
||||
logger.info(f"Vercel domain {domain} verification skipped (implicit via account)")
|
||||
return True, f"Vercel domain {domain} verified through account connection"
|
||||
|
||||
# Other providers would have similar verification flows
|
||||
logger.warning(f"Provider {provider} verification not fully implemented")
|
||||
return False, f"Verification for {provider} domains not yet implemented"
|
||||
return (
|
||||
True,
|
||||
f"{info.provider} domain {domain} requires provider-account verification",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,115 +1,23 @@
|
|||
"""Tests for domain verification, especially for provider default domains."""
|
||||
|
||||
import pytest
|
||||
"""Tests for provider default-domain classification."""
|
||||
|
||||
from strix.interface.cloud.domain_verifier import (
|
||||
get_provider_info,
|
||||
is_provider_default_domain,
|
||||
validate_domain_verification_method,
|
||||
verify_provider_domain,
|
||||
)
|
||||
|
||||
|
||||
class TestIsProviderDefaultDomain:
|
||||
"""Test detection of provider-default domains."""
|
||||
|
||||
def test_vercel_app_domain_is_recognized(self) -> None:
|
||||
"""Vercel free tier domain should be recognized."""
|
||||
assert is_provider_default_domain("demo.vercel.app")
|
||||
|
||||
def test_vercel_app_with_subdomain_is_recognized(self) -> None:
|
||||
"""Vercel domain with multiple subdomains should be recognized."""
|
||||
assert is_provider_default_domain("my-app-123.vercel.app")
|
||||
|
||||
def test_vercel_app_case_insensitive(self) -> None:
|
||||
"""Domain matching should be case-insensitive."""
|
||||
assert is_provider_default_domain("DEMO.VERCEL.APP")
|
||||
assert is_provider_default_domain("Demo.Vercel.App")
|
||||
|
||||
def test_vercel_app_with_trailing_dot(self) -> None:
|
||||
"""Should handle trailing dots."""
|
||||
assert is_provider_default_domain("demo.vercel.app.")
|
||||
|
||||
def test_netlify_app_domain_is_recognized(self) -> None:
|
||||
"""Netlify free tier domain should be recognized."""
|
||||
assert is_provider_default_domain("my-site.netlify.app")
|
||||
|
||||
def test_github_pages_domain_is_recognized(self) -> None:
|
||||
"""GitHub Pages domain should be recognized."""
|
||||
assert is_provider_default_domain("user.github.io")
|
||||
|
||||
def test_custom_domain_not_recognized(self) -> None:
|
||||
"""Custom domains should not be recognized as provider domains."""
|
||||
assert not is_provider_default_domain("example.com")
|
||||
assert not is_provider_default_domain("custom.example.com")
|
||||
|
||||
def test_vercel_com_not_recognized(self) -> None:
|
||||
"""vercel.com (Vercel's own site) should not be recognized as a free domain."""
|
||||
# vercel.com itself is not a free domain pattern
|
||||
assert not is_provider_default_domain("vercel.com")
|
||||
|
||||
def test_empty_domain(self) -> None:
|
||||
"""Empty domain should not be recognized."""
|
||||
assert not is_provider_default_domain("")
|
||||
assert not is_provider_default_domain(None)
|
||||
def test_provider_domains_are_case_insensitive_and_label_aware() -> None:
|
||||
assert is_provider_default_domain("Demo.Vercel.App.")
|
||||
assert is_provider_default_domain("user.github.io")
|
||||
assert not is_provider_default_domain("notvercel.app")
|
||||
assert not is_provider_default_domain("demo.vercel.app.evil.example")
|
||||
|
||||
|
||||
class TestGetProviderInfo:
|
||||
"""Test retrieval of provider information."""
|
||||
|
||||
def test_vercel_provider_info(self) -> None:
|
||||
"""Should return Vercel provider info."""
|
||||
info = get_provider_info("demo.vercel.app")
|
||||
assert info is not None
|
||||
assert info["provider"] == "vercel"
|
||||
assert info["requires_dns"] is False
|
||||
|
||||
def test_netlify_provider_info(self) -> None:
|
||||
"""Should return Netlify provider info."""
|
||||
info = get_provider_info("my-site.netlify.app")
|
||||
assert info is not None
|
||||
assert info["provider"] == "netlify"
|
||||
assert info["requires_dns"] is False
|
||||
|
||||
def test_custom_domain_returns_none(self) -> None:
|
||||
"""Should return None for custom domains."""
|
||||
assert get_provider_info("example.com") is None
|
||||
|
||||
|
||||
class TestValidateDomainVerificationMethod:
|
||||
"""Test domain verification method selection."""
|
||||
|
||||
def test_vercel_domain_uses_provider_verification(self) -> None:
|
||||
"""Vercel domains should use provider verification."""
|
||||
can_verify, method = validate_domain_verification_method("demo.vercel.app")
|
||||
assert can_verify is True
|
||||
assert method == "provider"
|
||||
|
||||
def test_custom_domain_uses_dns_verification(self) -> None:
|
||||
"""Custom domains should use DNS verification."""
|
||||
can_verify, method = validate_domain_verification_method("example.com")
|
||||
assert can_verify is True
|
||||
assert method == "dns"
|
||||
|
||||
def test_empty_domain_returns_unknown(self) -> None:
|
||||
"""Empty domain should return unknown method."""
|
||||
can_verify, method = validate_domain_verification_method("")
|
||||
assert can_verify is False
|
||||
assert method == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_vercel_domain() -> None:
|
||||
"""Vercel domain verification should succeed."""
|
||||
verified, message = await verify_provider_domain("demo.vercel.app")
|
||||
assert verified is True
|
||||
assert "vercel" in message.lower()
|
||||
assert "demo.vercel.app" in message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_unknown_provider_domain() -> None:
|
||||
"""Unknown provider domain should fail."""
|
||||
verified, message = await verify_provider_domain("example.com")
|
||||
assert verified is False
|
||||
assert "not a recognized provider domain" in message
|
||||
def test_provider_info_and_verification_method() -> None:
|
||||
info = get_provider_info("demo.vercel.app")
|
||||
assert info is not None
|
||||
assert info.provider == "vercel"
|
||||
assert validate_domain_verification_method("demo.vercel.app") == (True, "provider")
|
||||
assert validate_domain_verification_method("example.com") == (True, "dns")
|
||||
assert validate_domain_verification_method("") == (False, "unknown")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue