You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/10/12 05:38:37 UTC

(airavata-custos) branch fix-user-identity-lifecycle created (now 2eaf536e0)

This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a change to branch fix-user-identity-lifecycle
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git


      at 2eaf536e0 clear identity cache and allocation memberships when deleting user

This branch includes the following new commits:

     new 2eaf536e0 clear identity cache and allocation memberships when deleting user

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(airavata-custos) 01/01: clear identity cache and allocation memberships when deleting user

Posted by ya...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch fix-user-identity-lifecycle
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git

commit 2eaf536e0d21e524c339323ae9e351d9a793cc2e
Author: Yasith Jayawardana <ya...@icloud.com>
AuthorDate: Mon Oct 12 01:38:27 2026 -0400

    clear identity cache and allocation memberships when deleting user
---
 .../store/compute_allocation_membership_store.go   |  9 ++++++
 internal/store/store.go                            |  2 ++
 pkg/service/identity_resolver_integration_test.go  | 36 ++++++++++++++++++++++
 pkg/service/user.go                                | 11 +++++++
 pkg/service/user_identity.go                       |  2 ++
 pkg/service/user_merge.go                          |  1 +
 pkg/service/user_merge_integration_test.go         | 20 ++++++++++++
 7 files changed, 81 insertions(+)

diff --git a/internal/store/compute_allocation_membership_store.go b/internal/store/compute_allocation_membership_store.go
index 3ef1e286e..703a79b73 100644
--- a/internal/store/compute_allocation_membership_store.go
+++ b/internal/store/compute_allocation_membership_store.go
@@ -143,6 +143,15 @@ func (s *pgComputeAllocationMembershipStore) Delete(ctx context.Context, tx *sql
 	return err
 }
 
+func (s *pgComputeAllocationMembershipStore) DeleteByUser(ctx context.Context, tx *sql.Tx, userID string) ([]models.ComputeAllocationMembership, error) {
+	rows, err := tx.QueryContext(ctx, `DELETE FROM compute_allocation_memberships WHERE user_id = $1 RETURNING `+computeAllocationMembershipColumns, userID)
+	if err != nil {
+		return nil, err
+	}
+	var removed []models.ComputeAllocationMembership
+	return removed, sqlx.StructScan(rows, &removed)
+}
+
 // FindByAllocationWithUser returns memberships for an allocation joined with
 // users so each row carries display_name, email, and the user's project-wide
 // role (MEMBER without one).
diff --git a/internal/store/store.go b/internal/store/store.go
index 5c701e8af..44417f864 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -384,6 +384,8 @@ type ComputeAllocationMembershipStore interface {
 	ReassignUser(ctx context.Context, tx *sql.Tx, fromUserID, toUserID string) error
 	// Delete removes a membership by ID within the provided transaction.
 	Delete(ctx context.Context, tx *sql.Tx, id string) error
+	// DeleteByUser removes a user's memberships within the provided transaction and returns them.
+	DeleteByUser(ctx context.Context, tx *sql.Tx, userID string) ([]models.ComputeAllocationMembership, error)
 }
 
 // ComputeAllocationMembershipResourceOverrideStore defines persistence
diff --git a/pkg/service/identity_resolver_integration_test.go b/pkg/service/identity_resolver_integration_test.go
index 088074c8f..04c7827a0 100644
--- a/pkg/service/identity_resolver_integration_test.go
+++ b/pkg/service/identity_resolver_integration_test.go
@@ -294,3 +294,39 @@ func TestResolveCaller_EmailFallback_PublishesIdentityCreated(t *testing.T) {
 		t.Fatal("no UserIdentityCreateEvent published on identity link")
 	}
 }
+
+func TestResolveCaller_DropsTheCachedCallerOnIdentityChange(t *testing.T) {
+	cases := map[string]func(svc *Service, user, other, identityID string) error{
+		"identity deleted": func(svc *Service, _, _, id string) error { return svc.DeleteUserIdentity(ctx(), id) },
+		"identity rebound": func(svc *Service, _, _, id string) error {
+			return svc.UpdateUserIdentity(ctx(), &models.UserIdentity{ID: id, OIDCSub: "sub-other"})
+		},
+		"user deleted": func(svc *Service, user, _, _ string) error { return svc.DeleteUser(ctx(), user) },
+		"user merged": func(svc *Service, user, other, _ string) error {
+			_, err := svc.MergeUsers(ctx(), other, user)
+			return err
+		},
+	}
+	for name, change := range cases {
+		t.Run(name, func(t *testing.T) {
+			database := setupTestDB(t)
+			svc := newTestService(database)
+			user := seedUser(t, database, "cached@example.edu")
+			seedOIDCIdentity(t, database, user, "sub-cached", "cached@example.edu")
+			var identityID string
+			if err := database.Get(&identityID, "SELECT id FROM user_identities WHERE user_id = $1", user); err != nil {
+				t.Fatalf("read identity: %v", err)
+			}
+			claims := &identity.Claims{Sub: "sub-cached", Email: "cached@example.edu", EmailVerified: true}
+			if _, _, err := svc.ResolveCaller(ctx(), claims); err != nil {
+				t.Fatalf("resolve: %v", err)
+			}
+			if err := change(svc, user, seedUser(t, database, "survivor@example.edu"), identityID); err != nil {
+				t.Fatalf("change: %v", err)
+			}
+			if caller, _, err := svc.ResolveCaller(ctx(), claims); err == nil && caller.UserID == user {
+				t.Fatal("cached caller survived the change")
+			}
+		})
+	}
+}
diff --git a/pkg/service/user.go b/pkg/service/user.go
index 2a7e9e117..0bdd8b564 100644
--- a/pkg/service/user.go
+++ b/pkg/service/user.go
@@ -237,6 +237,16 @@ func (s *Service) DeleteUser(ctx context.Context, id string) error {
 		return ErrNotFound
 	}
 	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		// compute_allocation_memberships has no user foreign key, so nothing cascades.
+		memberships, err := s.memberships.DeleteByUser(ctx, tx, id)
+		if err != nil {
+			return err
+		}
+		for i := range memberships {
+			if err := s.eventBus.Publish(ctx, tx, events.ComputeAllocationMembershipDeleteEvent, &memberships[i]); err != nil {
+				return err
+			}
+		}
 		if err := s.users.Delete(ctx, tx, id); err != nil {
 			return err
 		}
@@ -244,5 +254,6 @@ func (s *Service) DeleteUser(ctx context.Context, id string) error {
 	}); err != nil {
 		return fmt.Errorf("delete user: %w", err)
 	}
+	s.InvalidateAllIdentities()
 	return nil
 }
diff --git a/pkg/service/user_identity.go b/pkg/service/user_identity.go
index dbffc363f..a5e01e7d7 100644
--- a/pkg/service/user_identity.go
+++ b/pkg/service/user_identity.go
@@ -166,6 +166,7 @@ func (s *Service) UpdateUserIdentity(ctx context.Context, e *models.UserIdentity
 	}); err != nil {
 		return fmt.Errorf("update user identity: %w", err)
 	}
+	s.InvalidateAllIdentities()
 	return nil
 }
 
@@ -189,5 +190,6 @@ func (s *Service) DeleteUserIdentity(ctx context.Context, id string) error {
 	}); err != nil {
 		return fmt.Errorf("delete user identity: %w", err)
 	}
+	s.InvalidateAllIdentities()
 	return nil
 }
diff --git a/pkg/service/user_merge.go b/pkg/service/user_merge.go
index 913acbe70..faea905c7 100644
--- a/pkg/service/user_merge.go
+++ b/pkg/service/user_merge.go
@@ -131,6 +131,7 @@ func (s *Service) MergeUsers(ctx context.Context, survivingID, retiringID string
 	}); err != nil {
 		return nil, fmt.Errorf("merge users: %w", err)
 	}
+	s.InvalidateAllIdentities()
 	return survivor, nil
 }
 
diff --git a/pkg/service/user_merge_integration_test.go b/pkg/service/user_merge_integration_test.go
index 92c6200e4..b93ce305d 100644
--- a/pkg/service/user_merge_integration_test.go
+++ b/pkg/service/user_merge_integration_test.go
@@ -135,3 +135,23 @@ func TestMergeUsers_KeepsHigherTagAndPIHoldsNoTag(t *testing.T) {
 		}
 	}
 }
+
+func TestDeleteUser_RemovesTheirAllocationMemberships(t *testing.T) {
+	database := setupTestDB(t)
+	svc := newTestService(database)
+	svc.EventBus().SubscribeComputeAllocationMembershipDeleted("test-subscriber", func(context.Context, models.ComputeAllocationMembership) error { return nil })
+	_, allocID := newClusterAllocation(t, svc, seedUser(t, database, "pi@example.edu"))
+	member := seedUser(t, database, "member@example.edu")
+	if _, err := svc.CreateComputeAllocationMembership(ctx(), &models.ComputeAllocationMembership{ComputeAllocationID: allocID, UserID: member, MembershipStatus: models.ACTIVE}); err != nil {
+		t.Fatalf("seed membership: %v", err)
+	}
+	if err := svc.DeleteUser(ctx(), member); err != nil {
+		t.Fatalf("delete user: %v", err)
+	}
+	if left, _ := svc.memberships.FindByUser(ctx(), member); len(left) != 0 {
+		t.Fatalf("%d memberships left", len(left))
+	}
+	if n := countDeliveries(t, svc); n != 1 {
+		t.Fatalf("%d membership delete events, want 1", n)
+	}
+}