Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
798438d
UN-3494 [FEAT] Email group members on resource share and group member…
kirtimanmishrazipstack Jul 31, 2026
d8b1008
UN-3494 [FIX] Restore direct-user sharing emails on the share endpoint
kirtimanmishrazipstack Aug 3, 2026
77ca124
UN-3494 [FEAT] Email users and group members when resource access is …
kirtimanmishrazipstack Aug 4, 2026
3392ebe
UN-3494 [FIX] Gate co-owner removal behind the share modal's Apply bu…
kirtimanmishrazipstack Aug 5, 2026
3eb0649
Merge branch 'main' into UN-3494-group-sharing-notification
kirtimanmishrazipstack Aug 5, 2026
c1d3095
UN-3494 [FIX] Address review findings on sharing notifications
kirtimanmishrazipstack Aug 5, 2026
9e9f57c
UN-3494 [MISC] Drop the local-only worker-pg-notification compose ser…
kirtimanmishrazipstack Aug 5, 2026
c663694
UN-3494 [FIX] Exclude members who joined a group after its access was…
kirtimanmishrazipstack Aug 5, 2026
6ab867b
UN-3494 [FIX] Skip a queued grant email when the group's access is al…
kirtimanmishrazipstack Aug 5, 2026
70b42c8
UN-3494 [FIX] Stamp the revoke cutoff before the feature-flag round-trip
kirtimanmishrazipstack Aug 5, 2026
3f7016e
UN-3494 [TEST] Cover share/revoke notifications for users and groups
kirtimanmishrazipstack Aug 5, 2026
014b6c8
UN-3494 [MISC] Merge main and make the group-notification dispatch PG…
kirtimanmishrazipstack Sep 10, 2026
5a519a7
Merge branch 'main' of github.com:Zipstack/unstract into UN-3494-grou…
kirtimanmishrazipstack Sep 10, 2026
be6f999
UN-3494 [MISC] Drop the group-notification feature flag
kirtimanmishrazipstack Sep 10, 2026
a30d1ee
UN-3494 [FIX] Close review findings on the group-notification path
kirtimanmishrazipstack Sep 14, 2026
3c78994
UN-3494 [FIX] Correct the retry budget, the share_action default and …
kirtimanmishrazipstack Sep 15, 2026
0f82e0d
UN-3494 [FIX] Bound the notification POST per phase, and close the re…
kirtimanmishrazipstack Sep 15, 2026
69474ee
UN-3494 [FIX] Widen the request-sent exception class, and delete the …
kirtimanmishrazipstack Sep 15, 2026
96beeb6
UN-3494 [TEST] Cover the worker leg and the recipient-selection guards
kirtimanmishrazipstack Sep 15, 2026
384aac2
UN-3494 [FIX] Stop telling retained users they lost access
kirtimanmishrazipstack Sep 15, 2026
665b0a9
UN-3494 [FIX] Close the gaps two independent code reviews found
kirtimanmishrazipstack Sep 16, 2026
2d366bb
Merge branch 'main' of github.com:Zipstack/unstract into UN-3494-grou…
kirtimanmishrazipstack Sep 16, 2026
cc57826
UN-3494 [MISC] Keep docker-compose.yaml out of this PR
kirtimanmishrazipstack Sep 16, 2026
8cb9a5a
UN-3494 [FIX] Show the real saved values when editing a pipeline noti…
kirtimanmishrazipstack Sep 16, 2026
99e2d8c
UN-3494 [FIX] Trim the lazy-init comment down to the non-obvious reason
kirtimanmishrazipstack Sep 16, 2026
921acf7
UN-3494 [FIX] Skip the removal email when access never depended on sh…
kirtimanmishrazipstack Sep 16, 2026
a28b770
UN-3494 [FIX] Correct LookupDefinition's registered primary-key field
kirtimanmishrazipstack Sep 16, 2026
4a35f60
Merge branch 'main' of github.com:Zipstack/unstract into UN-3494-grou…
kirtimanmishrazipstack Sep 18, 2026
f624ffe
Merge branch 'main' into UN-3494-group-sharing-notification
kirtimanmishrazipstack Sep 18, 2026
04402d0
Merge branch 'main' into UN-3494-group-sharing-notification
kirtimanmishrazipstack Sep 21, 2026
c92d309
Merge branch 'main' of github.com:Zipstack/unstract into UN-3494-grou…
kirtimanmishrazipstack Sep 22, 2026
ba181a8
UN-3494 [FIX] Address standardized review: 4 High + 6 Medium + 9 Low
kirtimanmishrazipstack Sep 22, 2026
5cb6a3b
UN-3494 [FIX] Cover the another-group retained-access route on revoke
kirtimanmishrazipstack Sep 22, 2026
8b9f034
UN-3494 [FIX] Concurrent group sends, shared resource-type mapping, f…
kirtimanmishrazipstack Sep 22, 2026
9f8fd5e
UN-3494 [FIX] Tri-state send result, fix the retry-loop live on dev, …
kirtimanmishrazipstack Sep 24, 2026
b75e894
UN-3494 [FIX] Close out ali's remaining Low/Medium round-2 findings o…
kirtimanmishrazipstack Sep 24, 2026
3f1de4c
Merge branch 'main' into UN-3494-group-sharing-notification
kirtimanmishrazipstack Sep 24, 2026
0761e13
Merge branch 'main' into UN-3494-group-sharing-notification
kirtimanmishrazipstack Sep 28, 2026
7a7140a
Merge branch 'main' into UN-3494-group-sharing-notification
kirtimanmishrazipstack Oct 1, 2026
437b61c
Merge branch 'main' into UN-3494-group-sharing-notification
kirtimanmishrazipstack Oct 8, 2026
d55b873
Merge branch 'main' into UN-3494-group-sharing-notification
kirtimanmishrazipstack Oct 8, 2026
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
60 changes: 5 additions & 55 deletions backend/adapter_processor_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@
from .models import AdapterInstance, UserDefaultAdapter

notification_plugin = get_plugin("notification")
if notification_plugin:
from plugins.notification.constants import ResourceType

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -162,12 +160,11 @@ class AdapterInstanceViewSet(
def get_notification_resource_type(self, resource: Any) -> str | None:
if not notification_plugin:
return None
return {
"LLM": ResourceType.LLM.value,
"EMBEDDING": ResourceType.EMBEDDING.value,
"VECTOR_DB": ResourceType.VECTOR_DB.value,
"X2TEXT": ResourceType.X2TEXT.value,
}.get(resource.adapter_type, ResourceType.LLM.value)
from tenant_account_v2.notification_resource_types import (
adapter_notification_type,
)

return adapter_notification_type(resource.adapter_type)

def get_permissions(self) -> list[Any]:
# Frictionless adapters: hidden from non-owners (update/retrieve),
Expand Down Expand Up @@ -410,17 +407,6 @@ def destroy(
raise DeleteAdapterInUseError(adapter_name=adapter_instance.adapter_name)
return Response(status=status.HTTP_204_NO_CONTENT)

def partial_update(
self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any]
) -> Response:
adapter = self.get_object()
before = self.snapshot_share_axes(adapter)

response = super().partial_update(request, *args, **kwargs)
if response.status_code == 200 and notification_plugin:
self._notify_shared_users(adapter, before, request.data, request.user)
return response

@action(detail=True, methods=["post"], url_path="share")
def share(self, request: Request, pk: str | None = None) -> Response:
"""Apply share state, then clear default-adapter links for any user
Expand Down Expand Up @@ -467,42 +453,6 @@ def on_owner_removed(self, resource: AdapterInstance, user: User) -> None:
return
self._clear_default_adapter_for_removed_users(resource, {user.pk})

def _notify_shared_users(
self,
adapter: AdapterInstance,
before: dict[str, set[Any]],
request_data: dict[str, Any],
actor: Any,
) -> None:
"""Email users newly added to ``shared_users`` (best-effort)."""
users_diff = self.diff_share_axes(adapter, before, request_data).get(
"shared_users"
)
if not (users_diff and users_diff.added):
return
try:
adapter_type_to_resource = {
"LLM": ResourceType.LLM.value,
"EMBEDDING": ResourceType.EMBEDDING.value,
"VECTOR_DB": ResourceType.VECTOR_DB.value,
"X2TEXT": ResourceType.X2TEXT.value,
}
resource_type = adapter_type_to_resource.get(
adapter.adapter_type, ResourceType.LLM.value
)
service_class = notification_plugin["service_class"]
notification_service = service_class()
notification_service.send_sharing_notification(
resource_type=resource_type,
resource_name=adapter.adapter_name,
resource_id=str(adapter.id),
shared_by=actor,
shared_to=list(users_diff.added),
resource_instance=adapter,
)
except Exception as e:
logger.exception("Failed to send sharing notification: %s", e)

def _clear_default_adapter_for_removed_users(
self,
adapter: AdapterInstance,
Expand Down
37 changes: 0 additions & 37 deletions backend/api_v2/api_deployment_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,40 +466,3 @@ def list_of_shared_users(self, request: Request, pk: str | None = None) -> Respo
instance = self.get_object()
serializer = SharedUserListSerializer(instance)
return Response(serializer.data)

def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
"""Override partial_update to handle sharing notifications."""
instance = self.get_object()
before = self.snapshot_share_axes(instance)

response = super().partial_update(request, *args, **kwargs)
if response.status_code == 200 and notification_plugin:
self._notify_shared_users(instance, before, request.data, request.user)
return response

def _notify_shared_users(
self,
instance: APIDeployment,
before: dict[str, set[Any]],
request_data: dict[str, Any],
actor: Any,
) -> None:
"""Email users newly added to ``shared_users`` (best-effort)."""
users_diff = self.diff_share_axes(instance, before, request_data).get(
"shared_users"
)
if not (users_diff and users_diff.added):
return
try:
service_class = notification_plugin["service_class"]
notification_service = service_class()
notification_service.send_sharing_notification(
resource_type=ResourceType.API_DEPLOYMENT.value,
resource_name=instance.display_name,
resource_id=str(instance.id),
shared_by=actor,
shared_to=list(users_diff.added),
resource_instance=instance,
)
except Exception as e:
logger.exception("Failed to send sharing notification: %s", e)
6 changes: 6 additions & 0 deletions backend/backend/internal_base_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,10 @@ def test_middleware_debug(request):
include("prompt_studio.prompt_studio_core_v2.internal_urls"),
name="prompt_studio_internal",
),
# Group-sharing email notification APIs
path(
"v1/group-notification/",
include("tenant_account_v2.internal_urls"),
name="group_notification_internal",
),
]
43 changes: 0 additions & 43 deletions backend/connector_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
notification_plugin = get_plugin("notification")
if notification_plugin:
from plugins.notification.constants import ResourceType
from plugins.notification.sharing_notification import SharingNotificationService

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -283,45 +282,3 @@ def perform_destroy(self, instance: ConnectorInstance) -> None:
f" named {instance.connector_name}"
)
raise DeleteConnectorInUseError(connector_name=instance.connector_name)

def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
"""Override to handle sharing notifications."""
instance = self.get_object()
before = self.snapshot_share_axes(instance)

response = super().partial_update(request, *args, **kwargs)
if response.status_code == 200 and notification_plugin:
self._notify_shared_users(instance, before, request.data, request.user)
return response

def _notify_shared_users(
self,
instance: ConnectorInstance,
before: dict[str, set[Any]],
request_data: dict[str, Any],
actor: Any,
) -> None:
"""Email users newly added to ``shared_users`` (best-effort)."""
users_diff = self.diff_share_axes(instance, before, request_data).get(
"shared_users"
)
if not (users_diff and users_diff.added):
return
try:
SharingNotificationService().send_sharing_notification(
resource_type=ResourceType.CONNECTOR.value,
resource_name=instance.connector_name,
resource_id=str(instance.id),
shared_by=actor,
shared_to=list(users_diff.added),
resource_instance=instance,
)
logger.info(
"Sent sharing notifications for connector to %d users",
len(users_diff.added),
)
except Exception as e:
logger.exception(
"Failed to send sharing notification, continuing update though: %s",
str(e),
)
14 changes: 13 additions & 1 deletion backend/permissions/membership_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from tenant_account_v2.sharing_helpers import serialize_owner_refs
from tenant_account_v2.sharing_helpers import retained_user_ids, serialize_owner_refs

from permissions.membership_serializers import AddOwnerSerializer, RemoveOwnerSerializer

Expand Down Expand Up @@ -86,6 +86,11 @@ def _owner_refs(resource: Any) -> list[dict[str, Any]]:
# --- notifications: reuse the user-sharing service, best-effort ---

def _notification_context(self, resource: Any) -> tuple[str, str] | None:
"""``(resource_type, resource_name)``, or ``None`` if not notifiable.

Also used by ``ResourceShareManagementMixin``, which every host mixes
in alongside this one.
"""
if not notification_plugin or not self.notification_resource_name_field:
return None
resource_type = self.get_notification_resource_type(resource)
Expand Down Expand Up @@ -115,6 +120,13 @@ def _notify_owner_removed(self, resource: Any, user: User, actor: Any) -> None:
ctx = self._notification_context(resource)
if ctx is None:
return
# The OWNER row is already gone by the time this runs (serializer.save()
# ran first) -- but the demoted user may still reach the resource another
# way (a group, a direct share, an org-wide share, org admin), same as
# the direct-share and group-revoke paths already check.
retained = retained_user_ids(resource)
if retained is None or user.pk in retained:
return
Comment thread
greptile-apps[bot] marked this conversation as resolved.
resource_type, resource_name = ctx
try:
notification_plugin["service_class"]().send_access_removed_notification(
Expand Down
Loading
Loading