diff --git a/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--dark.png b/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--dark.png index 9d2f7741f2a58..ce9d0f124d869 100644 Binary files a/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--dark.png and b/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--dark.png differ diff --git a/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--light.png b/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--light.png index dca3d5e9622ce..d5702c46ffe69 100644 Binary files a/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--light.png and b/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--default--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--default--dark.png index ec8494f1cb683..30d6848c0dcbf 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-input-select--default--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-input-select--default--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--default--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--default--light.png index 5496b3c4488fa..115f6d83d8d7f 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-input-select--default--light.png and b/frontend/__snapshots__/lemon-ui-lemon-input-select--default--light.png differ diff --git a/frontend/src/layout/navigation/ProjectNotice.tsx b/frontend/src/layout/navigation/ProjectNotice.tsx index 859b239597aec..5a0a9c4dd28ce 100644 --- a/frontend/src/layout/navigation/ProjectNotice.tsx +++ b/frontend/src/layout/navigation/ProjectNotice.tsx @@ -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' @@ -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 @@ -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] diff --git a/frontend/src/layout/navigation/navigationLogic.ts b/frontend/src/layout/navigation/navigationLogic.ts index 37f7749f14777..29a50b54a6bfb 100644 --- a/frontend/src/layout/navigation/navigationLogic.ts +++ b/frontend/src/layout/navigation/navigationLogic.ts @@ -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' @@ -21,7 +20,6 @@ export type ProjectNoticeVariant = | 'unverified_email' | 'is_impersonated' | 'internet_connection_issue' - | 'region_blocked' export const navigationLogic = kea([ path(['layout', 'navigation', 'navigationLogic']), @@ -122,8 +120,6 @@ export const navigationLogic = kea([ 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, diff --git a/frontend/src/scenes/sceneLogic.ts b/frontend/src/scenes/sceneLogic.ts index ffe3cfda2a900..0fe41a9e7e6ff 100644 --- a/frontend/src/scenes/sceneLogic.ts +++ b/frontend/src/scenes/sceneLogic.ts @@ -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' @@ -169,10 +168,6 @@ export const sceneLogic = kea([ 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()) diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 216f126975655..6d7999850bf0b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -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' diff --git a/posthog/middleware.py b/posthog/middleware.py index a90ada57996f3..71b5e2563e084 100644 --- a/posthog/middleware.py +++ b/posthog/middleware.py @@ -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 @@ -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 @@ -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, ) diff --git a/posthog/test/test_middleware.py b/posthog/test/test_middleware.py index f5b4190ef4293..507c247d5b0c1 100644 --- a/posthog/test/test_middleware.py +++ b/posthog/test/test_middleware.py @@ -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") @@ -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) @@ -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( @@ -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( @@ -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( @@ -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): diff --git a/posthog/utils.py b/posthog/utils.py index e64e3e1e14aa7..db8157d4d0ca1 100644 --- a/posthog/utils.py +++ b/posthog/utils.py @@ -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. @@ -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 @@ -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