mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
feat(backend): block tld (#12240)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
@@ -31,7 +31,13 @@ class DomainBlocker:
|
||||
return None
|
||||
|
||||
def is_domain_blocked(self, email: str) -> bool:
|
||||
"""Check if email domain is blocked"""
|
||||
"""Check if email domain is blocked
|
||||
|
||||
Supports blocking:
|
||||
- Exact domains: 'example.com' blocks 'user@example.com'
|
||||
- Subdomains: 'example.com' blocks 'user@subdomain.example.com'
|
||||
- TLDs: '.us' blocks 'user@company.us' and 'user@subdomain.company.us'
|
||||
"""
|
||||
if not self.is_active():
|
||||
return False
|
||||
|
||||
@@ -44,13 +50,26 @@ class DomainBlocker:
|
||||
logger.debug(f'Could not extract domain from email: {email}')
|
||||
return False
|
||||
|
||||
is_blocked = domain in self.blocked_domains
|
||||
if is_blocked:
|
||||
logger.warning(f'Email domain {domain} is blocked for email: {email}')
|
||||
else:
|
||||
logger.debug(f'Email domain {domain} is not blocked')
|
||||
# Check if domain matches any blocked pattern
|
||||
for blocked_pattern in self.blocked_domains:
|
||||
if blocked_pattern.startswith('.'):
|
||||
# TLD pattern (e.g., '.us') - check if domain ends with it
|
||||
if domain.endswith(blocked_pattern):
|
||||
logger.warning(
|
||||
f'Email domain {domain} is blocked by TLD pattern {blocked_pattern} for email: {email}'
|
||||
)
|
||||
return True
|
||||
else:
|
||||
# Full domain pattern (e.g., 'example.com')
|
||||
# Block exact match or subdomains
|
||||
if domain == blocked_pattern or domain.endswith(f'.{blocked_pattern}'):
|
||||
logger.warning(
|
||||
f'Email domain {domain} is blocked by domain pattern {blocked_pattern} for email: {email}'
|
||||
)
|
||||
return True
|
||||
|
||||
return is_blocked
|
||||
logger.debug(f'Email domain {domain} is not blocked')
|
||||
return False
|
||||
|
||||
|
||||
domain_blocker = DomainBlocker()
|
||||
|
||||
@@ -179,3 +179,315 @@ def test_is_domain_blocked_with_whitespace(domain_blocker):
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TLD Blocking Tests (patterns starting with '.')
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_is_domain_blocked_tld_pattern_blocks_matching_domain(domain_blocker):
|
||||
"""Test that TLD pattern blocks domains ending with that TLD."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@company.us')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_tld_pattern_blocks_subdomain_with_tld(domain_blocker):
|
||||
"""Test that TLD pattern blocks subdomains with that TLD."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@subdomain.company.us')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_tld_pattern_does_not_block_different_tld(domain_blocker):
|
||||
"""Test that TLD pattern does not block domains with different TLD."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@company.com')
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_tld_pattern_does_not_block_substring_match(
|
||||
domain_blocker,
|
||||
):
|
||||
"""Test that TLD pattern does not block domains that contain but don't end with the TLD."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@focus.com')
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_tld_pattern_case_insensitive(domain_blocker):
|
||||
"""Test that TLD pattern matching is case-insensitive."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@COMPANY.US')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_multiple_tld_patterns(domain_blocker):
|
||||
"""Test blocking with multiple TLD patterns."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.us', '.vn', '.com']
|
||||
|
||||
# Act
|
||||
result_us = domain_blocker.is_domain_blocked('user@test.us')
|
||||
result_vn = domain_blocker.is_domain_blocked('user@test.vn')
|
||||
result_com = domain_blocker.is_domain_blocked('user@test.com')
|
||||
result_org = domain_blocker.is_domain_blocked('user@test.org')
|
||||
|
||||
# Assert
|
||||
assert result_us is True
|
||||
assert result_vn is True
|
||||
assert result_com is True
|
||||
assert result_org is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_tld_pattern_with_multi_level_tld(domain_blocker):
|
||||
"""Test that TLD pattern works with multi-level TLDs like .co.uk."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.co.uk']
|
||||
|
||||
# Act
|
||||
result_match = domain_blocker.is_domain_blocked('user@example.co.uk')
|
||||
result_subdomain = domain_blocker.is_domain_blocked('user@api.example.co.uk')
|
||||
result_no_match = domain_blocker.is_domain_blocked('user@example.uk')
|
||||
|
||||
# Assert
|
||||
assert result_match is True
|
||||
assert result_subdomain is True
|
||||
assert result_no_match is False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Subdomain Blocking Tests (domain patterns now block subdomains)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_pattern_blocks_exact_match(domain_blocker):
|
||||
"""Test that domain pattern blocks exact domain match."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['example.com']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@example.com')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_pattern_blocks_subdomain(domain_blocker):
|
||||
"""Test that domain pattern blocks subdomains of that domain."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['example.com']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@subdomain.example.com')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_pattern_blocks_multi_level_subdomain(
|
||||
domain_blocker,
|
||||
):
|
||||
"""Test that domain pattern blocks multi-level subdomains."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['example.com']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@api.v2.example.com')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_pattern_does_not_block_similar_domain(
|
||||
domain_blocker,
|
||||
):
|
||||
"""Test that domain pattern does not block domains that contain but don't match the pattern."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['example.com']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@notexample.com')
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_pattern_does_not_block_different_tld(
|
||||
domain_blocker,
|
||||
):
|
||||
"""Test that domain pattern does not block same domain with different TLD."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['example.com']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@example.org')
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_subdomain_pattern_blocks_exact_and_nested(domain_blocker):
|
||||
"""Test that blocking a subdomain also blocks its nested subdomains."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['api.example.com']
|
||||
|
||||
# Act
|
||||
result_exact = domain_blocker.is_domain_blocked('user@api.example.com')
|
||||
result_nested = domain_blocker.is_domain_blocked('user@v1.api.example.com')
|
||||
result_parent = domain_blocker.is_domain_blocked('user@example.com')
|
||||
|
||||
# Assert
|
||||
assert result_exact is True
|
||||
assert result_nested is True
|
||||
assert result_parent is False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Mixed Pattern Tests (TLD + domain patterns together)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_is_domain_blocked_mixed_patterns_tld_and_domain(domain_blocker):
|
||||
"""Test blocking with both TLD and domain patterns."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.us', 'openhands.dev']
|
||||
|
||||
# Act
|
||||
result_tld = domain_blocker.is_domain_blocked('user@company.us')
|
||||
result_domain = domain_blocker.is_domain_blocked('user@openhands.dev')
|
||||
result_subdomain = domain_blocker.is_domain_blocked('user@api.openhands.dev')
|
||||
result_allowed = domain_blocker.is_domain_blocked('user@example.com')
|
||||
|
||||
# Assert
|
||||
assert result_tld is True
|
||||
assert result_domain is True
|
||||
assert result_subdomain is True
|
||||
assert result_allowed is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_overlapping_patterns(domain_blocker):
|
||||
"""Test that overlapping patterns (TLD and specific domain) both work."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.us', 'test.us']
|
||||
|
||||
# Act
|
||||
result_specific = domain_blocker.is_domain_blocked('user@test.us')
|
||||
result_other_us = domain_blocker.is_domain_blocked('user@other.us')
|
||||
|
||||
# Assert
|
||||
assert result_specific is True
|
||||
assert result_other_us is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_complex_multi_pattern_scenario(domain_blocker):
|
||||
"""Test complex scenario with multiple TLD and domain patterns."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = [
|
||||
'.us',
|
||||
'.vn',
|
||||
'test.com',
|
||||
'openhands.dev',
|
||||
]
|
||||
|
||||
# Act & Assert
|
||||
# TLD patterns
|
||||
assert domain_blocker.is_domain_blocked('user@anything.us') is True
|
||||
assert domain_blocker.is_domain_blocked('user@company.vn') is True
|
||||
|
||||
# Domain patterns (exact)
|
||||
assert domain_blocker.is_domain_blocked('user@test.com') is True
|
||||
assert domain_blocker.is_domain_blocked('user@openhands.dev') is True
|
||||
|
||||
# Domain patterns (subdomains)
|
||||
assert domain_blocker.is_domain_blocked('user@api.test.com') is True
|
||||
assert domain_blocker.is_domain_blocked('user@staging.openhands.dev') is True
|
||||
|
||||
# Not blocked
|
||||
assert domain_blocker.is_domain_blocked('user@allowed.com') is False
|
||||
assert domain_blocker.is_domain_blocked('user@example.org') is False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Edge Case Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_with_hyphens(domain_blocker):
|
||||
"""Test that domain patterns work with hyphenated domains."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['my-company.com']
|
||||
|
||||
# Act
|
||||
result_exact = domain_blocker.is_domain_blocked('user@my-company.com')
|
||||
result_subdomain = domain_blocker.is_domain_blocked('user@api.my-company.com')
|
||||
|
||||
# Assert
|
||||
assert result_exact is True
|
||||
assert result_subdomain is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_with_numbers(domain_blocker):
|
||||
"""Test that domain patterns work with numeric domains."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['test123.com']
|
||||
|
||||
# Act
|
||||
result_exact = domain_blocker.is_domain_blocked('user@test123.com')
|
||||
result_subdomain = domain_blocker.is_domain_blocked('user@api.test123.com')
|
||||
|
||||
# Assert
|
||||
assert result_exact is True
|
||||
assert result_subdomain is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_short_tld(domain_blocker):
|
||||
"""Test that short TLD patterns work correctly."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['.io']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@company.io')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_very_long_subdomain_chain(domain_blocker):
|
||||
"""Test that blocking works with very long subdomain chains."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['example.com']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked(
|
||||
'user@level4.level3.level2.level1.example.com'
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
Reference in New Issue
Block a user