Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 6 additions & 25 deletions posthog/api/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
from posthog.exceptions_capture import capture_exception
from posthog.helpers.fuzzy_search import fuzzy_filter
from posthog.integrations import IntegrationNotFound, IntegrationRecord
from posthog.models import OrganizationMembership, ProxyRecord, Team, User
from posthog.models import OrganizationMembership, ProxyRecord, User
from posthog.models.github_integration_base import GitHubIntegrationBase
from posthog.models.integration import (
ANTHROPIC_DEFAULT_INTEGRATION_ID_PREFIX,
Expand Down Expand Up @@ -143,6 +143,7 @@
resolve_aliased_oauth_kind,
)
from posthog.models.integration.auth_types import rows_without_source_marker_q
from posthog.models.integration.email import EMAIL_FROM_CODE_FLAG, email_flag_enabled, email_from_code_enabled
from posthog.models.integration.github_audit import GitHubAudit
from posthog.models.integration.stripe import stripe_signing_secret
from posthog.models.integration.twitter_ads import TwitterAdsIntegration
Expand All @@ -156,10 +157,9 @@
TimeSensitiveActionPermission,
get_authenticator_scopes,
)
from posthog.ph_client import feature_enabled_or_false
from posthog.rate_limit import GitHubRepositoryRefreshThrottle, PushTokenImportThrottle
from posthog.tasks.email import send_integration_access_request
from posthog.utils import absolute_uri, flag_identity, is_relative_url
from posthog.utils import absolute_uri, is_relative_url

from products.access_control.backend.presentation.access_control import UserAccessControlSerializerMixin
from products.batch_exports.backend.facade.api import list_batch_exports_using_integration
Expand All @@ -185,8 +185,6 @@

GITHUB_REPOSITORY_NAME_RE = re.compile(r"[A-Za-z0-9_.\-]+")

EMAIL_FROM_CODE_FLAG = "workflows-email-from-code"


class EmailProviderUnavailable(APIException):
# A stable code lets the setup page keep polling through an outage instead of treating it as a broken sender.
Expand Down Expand Up @@ -1579,24 +1577,6 @@ def has_object_permission(self, request: Request, view: APIView, obj: object) ->
return True


def email_flag_enabled(flag: str, team: Team, user: User) -> bool:
identity = flag_identity(user, team, team.organization)
if identity is None:
return False
return feature_enabled_or_false(
flag,
identity.distinct_id,
groups=identity.groups,
person_properties=identity.person_properties,
group_properties=identity.group_properties,
send_feature_flag_events=False,
)


def email_from_code_enabled(team: Team, user: User) -> bool:
return email_flag_enabled(EMAIL_FROM_CODE_FLAG, team, user)


class EmailDomainAgentSetupPermission(BasePermission):
message = "This action does not support personal API key access"
token_actions = frozenset({"domain_connect_check", "domain_connect_apply_url", "email_update"})
Expand Down Expand Up @@ -2909,7 +2889,7 @@ def email_status(self, request: ValidatedRequest, **kwargs: Any) -> Response:
if instance.kind != "email":
raise ValidationError("email/status is only supported for email integrations")
try:
report = EmailIntegration(instance).status(
report = EmailIntegration(instance, acting_user=cast(User, request.user)).status(
refresh=request.validated_query_data["refresh"],
)
except EmailProviderUnavailableError as error:
Expand Down Expand Up @@ -2952,7 +2932,7 @@ def _get_email_integration(self) -> EmailIntegration:
instance = self.get_object()
if instance.kind != "email":
raise ValidationError("This endpoint is only supported for email integrations")
return EmailIntegration(instance)
return EmailIntegration(instance, acting_user=cast(User, self.request.user))

@validated_request(
query_serializer=EmailDomainConnectCheckQuerySerializer,
Expand Down Expand Up @@ -3004,6 +2984,7 @@ def domain_connect_apply_url(self, request: ValidatedRequest, *args: Any, **kwar
integration_id,
self.team_id,
on_domain_verified=lambda instance: self._report_email_domain_verified(request, instance),
acting_user=cast(User, request.user),
)
except IntegrationNotFound:
raise NotFound("Email integration not found.")
Expand Down
44 changes: 43 additions & 1 deletion posthog/api/test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
list_org_github_installations,
)
from posthog.api.github_callback.types import FlowKind, GitHubAuthorizeState
from posthog.api.integration import EMAIL_FROM_CODE_FLAG, IntegrationSerializer, IntegrationViewSet
from posthog.api.integration import IntegrationSerializer, IntegrationViewSet
from posthog.constants import AvailableFeature
from posthog.credentials_service.errors import (
CredentialsServiceFailure,
Expand Down Expand Up @@ -75,6 +75,7 @@
github_account_type,
)
from posthog.models.integration.databricks import NOT_A_DATABRICKS_HOST_MESSAGE
from posthog.models.integration.email import EMAIL_FROM_CODE_FLAG
from posthog.models.integration.github_audit import GitHubAudit, GitHubAuditPayload
from posthog.models.integration.twitter_ads import TwitterAdsIntegration
from posthog.models.oauth import OAuthAccessToken, OAuthApplication, OAuthRefreshToken
Expand Down Expand Up @@ -845,6 +846,47 @@ def test_email_verify_updates_all_other_integrations_with_same_domain(
assert integration2.config["verified"]
assert not integrationOtherDomain.config["verified"]

@patch("products.workflows.backend.facade.api.update_ses_mail_from_subdomain")
def test_editing_the_label_shares_it_when_the_flag_is_on_for_the_requesting_user(
self, _mock_update_mail_from_subdomain, client: HttpClient
):
admin = User.objects.create_and_join(
self.organization, "admin@example.com", "test", level=OrganizationMembership.Level.ADMIN
)
edited, sibling = (
Integration.objects.create(
team=self.team,
kind="email",
integration_id=email,
config={"email": email, "domain": "example.com", "mail_from_subdomain": "feedback", "provider": "ses"},
)
for email in ["edited@example.com", "sibling@example.com"]
)
client.force_login(admin)

with patch(
"posthoganalytics.feature_enabled",
side_effect=lambda key, distinct_id, **_kwargs: (
key == EMAIL_FROM_CODE_FLAG and distinct_id == admin.distinct_id
),
):
response = client.patch(
f"/api/environments/{self.team.pk}/integrations/{edited.pk}/email",
{
"config": {
"email": "edited@example.com",
"name": "Edited",
"provider": "ses",
"mail_from_subdomain": "bounce",
}
},
content_type="application/json",
)

assert response.status_code == status.HTTP_200_OK, response.json()
sibling.refresh_from_db()
assert sibling.config["mail_from_subdomain"] == "bounce"


class TestDatabricksIntegration:
@pytest.fixture(autouse=True)
Expand Down
6 changes: 4 additions & 2 deletions posthog/domain_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

if TYPE_CHECKING:
from posthog.integrations import IntegrationRecord
from posthog.models.user import User

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -254,6 +255,7 @@ def resolve_email_context(
team_id: int,
*,
on_domain_verified: Callable[["IntegrationRecord"], None] | None = None,
acting_user: "User | None" = None,
) -> DomainConnectContext:
"""Resolve Domain Connect parameters for an email integration.

Expand All @@ -268,14 +270,14 @@ def resolve_email_context(

instance = IntegrationManager.email.credentials.get(team_id, integration_id)

email_integration = EmailIntegration(instance)
email_integration = EmailIntegration(instance, acting_user=acting_user)
verification_result = email_integration.verify(
on_domain_verified=(lambda: on_domain_verified(instance)) if on_domain_verified else None
)

dns_records = verification_result.get("dnsRecords", [])
domain_parts = extract_root_domain_and_host(instance.config.get("domain", ""))
mail_from_subdomain = instance.config.get("mail_from_subdomain", "feedback")
mail_from_subdomain = email_integration.mail_from_subdomain

verify_token = ""
dkim_tokens: list[str] = []
Expand Down
Loading
Loading