Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Fully block app based on geoip #25259

Merged
merged 14 commits into from
Oct 3, 2024
12 changes: 0 additions & 12 deletions frontend/src/layout/navigation/ProjectNotice.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { IconGear, IconPlus } from '@posthog/icons'
import { useActions, useValues } from 'kea'
import { supportLogic } from 'lib/components/Support/supportLogic'
import { dayjs } from 'lib/dayjs'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonBannerAction } from 'lib/lemon-ui/LemonBanner/LemonBanner'
Expand Down Expand Up @@ -46,7 +45,6 @@ export function ProjectNotice(): JSX.Element | null {
const { closeProjectNotice } = useActions(navigationLogic)
const { showInviteModal } = useActions(inviteLogic)
const { requestVerificationLink } = useActions(verifyEmailLogic)
const { openSupportForm } = useActions(supportLogic)

if (!projectNoticeVariant) {
return null
Expand Down Expand Up @@ -147,16 +145,6 @@ export function ProjectNotice(): JSX.Element | null {
children: 'Reload page',
},
},
region_blocked: {
message:
'PostHog is not available in your region due to legal restrictions. People in restricted regions will soon be blocked from accessing PostHog. Please contact support if you believe this is a mistake.',
type: 'error',
action: {
'data-attr': 'region-blocked-support',
onClick: () => openSupportForm({ kind: 'support', target_area: 'login' }),
children: 'Contact support',
},
},
}

const relevantNotice = NOTICES[projectNoticeVariant]
Expand Down
4 changes: 0 additions & 4 deletions frontend/src/layout/navigation/navigationLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { windowValues } from 'kea-window-values'
import api from 'lib/api'
import { apiStatusLogic } from 'lib/logic/apiStatusLogic'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { getAppContext } from 'lib/utils/getAppContext'
import { membersLogic } from 'scenes/organization/membersLogic'
import { organizationLogic } from 'scenes/organizationLogic'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
Expand All @@ -21,7 +20,6 @@ export type ProjectNoticeVariant =
| 'unverified_email'
| 'is_impersonated'
| 'internet_connection_issue'
| 'region_blocked'

export const navigationLogic = kea<navigationLogicType>([
path(['layout', 'navigation', 'navigationLogic']),
Expand Down Expand Up @@ -122,8 +120,6 @@ export const navigationLogic = kea<navigationLogicType>([
return 'internet_connection_issue'
} else if (user?.is_impersonated) {
return 'is_impersonated'
} else if (getAppContext()?.is_region_blocked) {
return 'region_blocked'
} else if (currentTeam?.is_demo && !preflight?.demo) {
// If the project is a demo one, show a project-level warning
// Don't show this project-level warning in the PostHog demo environemnt though,
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/scenes/sceneLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { BarStatus } from 'lib/components/CommandBar/types'
import { FEATURE_FLAGS, TeamMembershipLevel } from 'lib/constants'
import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { getAppContext } from 'lib/utils/getAppContext'
import { addProjectIdIfMissing, removeProjectIdIfPresent } from 'lib/utils/router-utils'
import posthog from 'posthog-js'
import { emptySceneParams, preloadedScenes, redirects, routes, sceneConfigurations } from 'scenes/scenes'
Expand Down Expand Up @@ -169,10 +168,6 @@ export const sceneLogic = kea<sceneLogicType>([
return
}

if (getAppContext()?.is_region_blocked) {
// TODO: Later this is where we should redirect to a page to explain the region blocking
}

if (scene === Scene.Signup && preflight && !preflight.can_create_org) {
// If user is on an already initiated self-hosted instance, redirect away from signup
router.actions.replace(urls.login())
Expand Down
1 change: 0 additions & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3360,7 +3360,6 @@ export interface AppContext {
year_in_hog_url?: string
/** Support flow aid: a staff-only list of users who may be impersonated to access this resource. */
suggested_users_with_access?: UserBasicType[]
is_region_blocked?: boolean
}

export type StoredMetricMathOperations = 'max' | 'min' | 'sum'
Expand Down
14 changes: 10 additions & 4 deletions posthog/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
from posthog.geoip import get_geoip_properties
import time
from ipaddress import ip_address, ip_network
from typing import Any, Optional, cast
Expand Down Expand Up @@ -72,7 +73,7 @@ class AllowIPMiddleware:
trusted_proxies: list[str] = []

def __init__(self, get_response):
if not settings.ALLOWED_IP_BLOCKS:
if not settings.ALLOWED_IP_BLOCKS and not settings.BLOCKED_GEOIP_REGIONS:
# this will make Django skip this middleware for all future requests
raise MiddlewareNotUsed()
self.ip_blocks = settings.ALLOWED_IP_BLOCKS
Expand Down Expand Up @@ -108,10 +109,15 @@ def __call__(self, request: HttpRequest):
if request.path.split("/")[1] in ALWAYS_ALLOWED_ENDPOINTS:
return response
ip = self.extract_client_ip(request)
if ip and any(ip_address(ip) in ip_network(block, strict=False) for block in self.ip_blocks):
return response
if ip:
if settings.ALLOWED_IP_BLOCKS:
if any(ip_address(ip) in ip_network(block, strict=False) for block in self.ip_blocks):
return response
elif settings.BLOCKED_GEOIP_REGIONS:
if get_geoip_properties(ip).get("$geoip_country_code", None) not in settings.BLOCKED_GEOIP_REGIONS:
return response
return HttpResponse(
"Your IP is not allowed. Check your ALLOWED_IP_BLOCKS settings. If you are behind a proxy, you need to set TRUSTED_PROXIES. See https://posthog.com/docs/deployment/running-behind-proxy",
"PostHog is not available in your region. If you think this is in error, please contact tim@posthog.com.",
status=403,
)

Expand Down
40 changes: 29 additions & 11 deletions posthog/test/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_ip_range(self):
# not in list
response = self.client.get("/", REMOTE_ADDR="10.0.0.1")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertIn(b"IP is not allowed", response.content)
self.assertIn(b"PostHog is not available", response.content)

response = self.client.get("/batch/", REMOTE_ADDR="10.0.0.1")

Expand All @@ -40,11 +40,11 @@ def test_ip_range(self):
# /31 block
response = self.client.get("/", REMOTE_ADDR="192.168.0.1")
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotIn(b"IP is not allowed", response.content)
self.assertNotIn(b"PostHog is not available", response.content)

response = self.client.get("/", REMOTE_ADDR="192.168.0.2")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertIn(b"IP is not allowed", response.content)
self.assertIn(b"PostHog is not available", response.content)

response = self.client.get("/batch/", REMOTE_ADDR="192.168.0.1")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
Expand All @@ -55,23 +55,23 @@ def test_ip_range(self):
# /24 block
response = self.client.get("/", REMOTE_ADDR="127.0.0.1")
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotIn(b"IP is not allowed", response.content)
self.assertNotIn(b"PostHog is not available", response.content)

response = self.client.get("/", REMOTE_ADDR="127.0.0.100")
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotIn(b"IP is not allowed", response.content)
self.assertNotIn(b"PostHog is not available", response.content)

response = self.client.get("/", REMOTE_ADDR="127.0.0.200")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertIn(b"IP is not allowed", response.content)
self.assertIn(b"PostHog is not available", response.content)

# precise ip
response = self.client.get("/", REMOTE_ADDR="128.0.0.1")
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotIn(b"IP is not allowed", response.content)
self.assertNotIn(b"PostHog is not available", response.content)

response = self.client.get("/", REMOTE_ADDR="128.0.0.2")
self.assertIn(b"IP is not allowed", response.content)
self.assertIn(b"PostHog is not available", response.content)

def test_trusted_proxies(self):
with self.settings(
Expand All @@ -84,7 +84,7 @@ def test_trusted_proxies(self):
REMOTE_ADDR="10.0.0.1",
HTTP_X_FORWARDED_FOR="192.168.0.1,10.0.0.1",
)
self.assertNotIn(b"IP is not allowed", response.content)
self.assertNotIn(b"PostHog is not available", response.content)

def test_attempt_spoofing(self):
with self.settings(
Expand All @@ -97,7 +97,8 @@ def test_attempt_spoofing(self):
REMOTE_ADDR="10.0.0.1",
HTTP_X_FORWARDED_FOR="192.168.0.1,10.0.0.2",
)
self.assertIn(b"IP is not allowed", response.content)
self.assertEqual(response.status_code, 403)
self.assertIn(b"PostHog is not available", response.content)

def test_trust_all_proxies(self):
with self.settings(
Expand All @@ -110,7 +111,24 @@ def test_trust_all_proxies(self):
REMOTE_ADDR="10.0.0.1",
HTTP_X_FORWARDED_FOR="192.168.0.1,10.0.0.1",
)
self.assertNotIn(b"IP is not allowed", response.content)
self.assertNotIn(b"PostHog is not available", response.content)

def test_blocked_geoip_regions(self):
with self.settings(
BLOCKED_GEOIP_REGIONS=["DE"],
USE_X_FORWARDED_HOST=True,
):
with self.settings(TRUST_ALL_PROXIES=True):
response = self.client.get(
"/",
REMOTE_ADDR="45.90.4.87",
)
self.assertIn(b"PostHog is not available", response.content)
response = self.client.get(
"/",
REMOTE_ADDR="28.160.62.192",
)
self.assertNotIn(b"PostHog is not available", response.content)


class TestAutoProjectMiddleware(APIBaseTest):
Expand Down
8 changes: 3 additions & 5 deletions posthog/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,8 @@ def render_template(
context: Optional[dict] = None,
*,
team_for_public_context: Optional["Team"] = None,
status_code: Optional[int] = None,
) -> HttpResponse:
from posthog.geoip import get_geoip_properties

"""Render Django template.

If team_for_public_context is provided, this means this is a public page such as a shared dashboard.
Expand Down Expand Up @@ -344,9 +343,6 @@ def render_template(
"year_in_hog_url": year_in_hog_url,
}

geo_ip_country_code = get_geoip_properties(get_ip_address(request)).get("$geoip_country_code", None)
posthog_app_context["is_region_blocked"] = geo_ip_country_code in settings.BLOCKED_GEOIP_REGIONS

posthog_bootstrap: dict[str, Any] = {}
posthog_distinct_id: Optional[str] = None

Expand Down Expand Up @@ -438,6 +434,8 @@ def render_template(

html = template.render(context, request=request)
response = HttpResponse(html)
if status_code:
response.status_code = status_code
if not request.user.is_anonymous:
patch_cache_control(response, no_store=True)
return response
Expand Down
Loading