diff --git a/services/auth-guest/README.md b/services/auth-guest/README.md index 0aae9c3fb2..209465ad45 100644 --- a/services/auth-guest/README.md +++ b/services/auth-guest/README.md @@ -25,8 +25,8 @@ links feature and start the service. ## Overview -- **Consumes** the share lifecycle events `ShareCreated`, `ShareRemoved` and - `ShareExpired`. +- **Consumes** the share lifecycle events `ShareCreated`, `ShareRemoved`, + `ShareExpired`, `SpaceShared`, `SpaceUnshared` and `SpaceMembershipExpired`. - **Publishes** the `GuestTokenCreated` event (initial token) and the `GuestTokenRenewed` event (refresh, carrying the new token and, when authorized by a session, the PIN). diff --git a/services/auth-guest/pkg/command/server.go b/services/auth-guest/pkg/command/server.go index 720a899a87..fcf577a3e3 100644 --- a/services/auth-guest/pkg/command/server.go +++ b/services/auth-guest/pkg/command/server.go @@ -38,6 +38,8 @@ var _registeredEvents = []events.Unmarshaller{ events.ShareRemoved{}, events.ShareExpired{}, events.SpaceShared{}, + events.SpaceUnshared{}, + events.SpaceMembershipExpired{}, } // Server is the entrypoint for the server command. diff --git a/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go b/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go index cafa73801c..11c2f90c65 100644 --- a/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go +++ b/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go @@ -7,8 +7,6 @@ package mocks import ( "context" - "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" mock "github.com/stretchr/testify/mock" @@ -160,80 +158,6 @@ func (_c *AuthGuest_CreateToken_Call) RunAndReturn(run func(ctx context.Context, return _c } -// GetShareForSpaceGrant provides a mock function for the type AuthGuest -func (_mock *AuthGuest) GetShareForSpaceGrant(ctx context.Context, spaceID string, grantee *userv1beta1.UserId) (*collaborationv1beta1.Share, error) { - ret := _mock.Called(ctx, spaceID, grantee) - - if len(ret) == 0 { - panic("no return value specified for GetShareForSpaceGrant") - } - - var r0 *collaborationv1beta1.Share - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, *userv1beta1.UserId) (*collaborationv1beta1.Share, error)); ok { - return returnFunc(ctx, spaceID, grantee) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, *userv1beta1.UserId) *collaborationv1beta1.Share); ok { - r0 = returnFunc(ctx, spaceID, grantee) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*collaborationv1beta1.Share) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, *userv1beta1.UserId) error); ok { - r1 = returnFunc(ctx, spaceID, grantee) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// AuthGuest_GetShareForSpaceGrant_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetShareForSpaceGrant' -type AuthGuest_GetShareForSpaceGrant_Call struct { - *mock.Call -} - -// GetShareForSpaceGrant is a helper method to define mock.On call -// - ctx context.Context -// - spaceID string -// - grantee *userv1beta1.UserId -func (_e *AuthGuest_Expecter) GetShareForSpaceGrant(ctx any, spaceID any, grantee any) *AuthGuest_GetShareForSpaceGrant_Call { - return &AuthGuest_GetShareForSpaceGrant_Call{Call: _e.mock.On("GetShareForSpaceGrant", ctx, spaceID, grantee)} -} - -func (_c *AuthGuest_GetShareForSpaceGrant_Call) Run(run func(ctx context.Context, spaceID string, grantee *userv1beta1.UserId)) *AuthGuest_GetShareForSpaceGrant_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 *userv1beta1.UserId - if args[2] != nil { - arg2 = args[2].(*userv1beta1.UserId) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *AuthGuest_GetShareForSpaceGrant_Call) Return(share *collaborationv1beta1.Share, err error) *AuthGuest_GetShareForSpaceGrant_Call { - _c.Call.Return(share, err) - return _c -} - -func (_c *AuthGuest_GetShareForSpaceGrant_Call) RunAndReturn(run func(ctx context.Context, spaceID string, grantee *userv1beta1.UserId) (*collaborationv1beta1.Share, error)) *AuthGuest_GetShareForSpaceGrant_Call { - _c.Call.Return(run) - return _c -} - // Renew provides a mock function for the type AuthGuest func (_mock *AuthGuest) Renew(ctx context.Context, shareID string, sessionToken string) (string, error) { ret := _mock.Called(ctx, shareID, sessionToken) diff --git a/services/auth-guest/pkg/service/authguest/service.go b/services/auth-guest/pkg/service/authguest/service.go index 10d18f16f0..7b289a5012 100644 --- a/services/auth-guest/pkg/service/authguest/service.go +++ b/services/auth-guest/pkg/service/authguest/service.go @@ -10,10 +10,8 @@ import ( "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/opencloud-eu/opencloud/pkg/events" "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" @@ -24,7 +22,6 @@ import ( "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" revaevents "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - "github.com/opencloud-eu/reva/v2/pkg/storagespace" "github.com/opencloud-eu/reva/v2/pkg/utils" ) @@ -48,7 +45,6 @@ type AuthGuest interface { Renew(ctx context.Context, shareID, sessionToken string) (string, error) VerifyPin(ctx context.Context, shareID, pinValue, nonce string) (*SessionResponse, error) CleanupShare(shareID string) error - GetShareForSpaceGrant(ctx context.Context, spaceID string, grantee *user.UserId) (*collaboration.Share, error) } var _ AuthGuest = (*AuthGuestService)(nil) @@ -511,58 +507,3 @@ func (s *AuthGuestService) getShare(ctx context.Context, shareID string) (*colla return share, nil } - -// GetShareForSpaceGrant fetches the share created for a grant on a space. The -// space root node id is the space id, so the share is addressed by the space -// root resource id and the grantee. -func (s *AuthGuestService) GetShareForSpaceGrant(ctx context.Context, spaceID string, grantee *user.UserId) (*collaboration.Share, error) { - rid, err := storagespace.ParseID(spaceID) - if err != nil { - return nil, err - } - rid.OpaqueId = rid.SpaceId - - gwc, err := s.gatewaySelector.Next() - if err != nil { - return nil, err - } - - ctx, err = utils.GetServiceUserContextWithContext(ctx, gwc, s.serviceAccount.ServiceAccountID, s.serviceAccount.ServiceAccountSecret) - if err != nil { - return nil, err - } - - resp, err := gwc.GetShare(ctx, &collaboration.GetShareRequest{ - Ref: &collaboration.ShareReference{ - Spec: &collaboration.ShareReference_Key{ - Key: &collaboration.ShareKey{ - ResourceId: &rid, - Grantee: &provider.Grantee{ - Type: provider.GranteeType_GRANTEE_TYPE_USER, - Id: &provider.Grantee_UserId{ - UserId: grantee, - }, - }, - }, - }, - }, - }) - if err != nil { - return nil, err - } - - switch resp.GetStatus().GetCode() { - case rpc.Code_CODE_OK: - case rpc.Code_CODE_NOT_FOUND: - return nil, ErrShareNotFound - default: - return nil, fmt.Errorf("could not get share for space %s: %s", spaceID, resp.GetStatus().GetMessage()) - } - - share := resp.GetShare() - if share == nil { - return nil, ErrShareNotFound - } - - return share, nil -} diff --git a/services/auth-guest/pkg/service/authguest/service_test.go b/services/auth-guest/pkg/service/authguest/service_test.go index 6792f48272..342f410235 100644 --- a/services/auth-guest/pkg/service/authguest/service_test.go +++ b/services/auth-guest/pkg/service/authguest/service_test.go @@ -27,7 +27,6 @@ import ( "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" revaevents "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - "github.com/opencloud-eu/reva/v2/pkg/storagespace" "github.com/opencloud-eu/reva/v2/pkg/utils" cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" "github.com/stretchr/testify/assert" @@ -308,91 +307,6 @@ func TestValidateShare(t *testing.T) { } } -func TestGetShareForSpaceGrant(t *testing.T) { - share := &collaboration.Share{ - Id: &collaboration.ShareId{OpaqueId: testShareID}, - ResourceId: &provider.ResourceId{StorageId: "storage", SpaceId: "space", OpaqueId: "space"}, - } - - tests := []struct { - name string - spaceID string - response *collaboration.GetShareResponse - wantErr error - }{ - { - name: "valid", - spaceID: "storage$space", - response: &collaboration.GetShareResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Share: share}, - }, - { - name: "not found", - spaceID: "storage$space", - response: &collaboration.GetShareResponse{Status: &rpc.Status{Code: rpc.Code_CODE_NOT_FOUND}}, - wantErr: ErrShareNotFound, - }, - { - name: "nil share", - spaceID: "storage$space", - response: &collaboration.GetShareResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}}, - wantErr: ErrShareNotFound, - }, - { - name: "malformed space id", - spaceID: "", - wantErr: storagespace.ErrInvalidSpaceID, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := newShareService(t, newGatewayMock(tt.response)) - - got, err := s.GetShareForSpaceGrant(context.Background(), tt.spaceID, &user.UserId{OpaqueId: "guest@example.com", Type: user.UserType_USER_TYPE_GUEST}) - if tt.wantErr != nil { - require.ErrorIs(t, err, tt.wantErr) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.response.GetShare(), got) - }) - } -} - -func TestGetShareForSpaceGrantRequest(t *testing.T) { - share := &collaboration.Share{ - Id: &collaboration.ShareId{OpaqueId: testShareID}, - ResourceId: &provider.ResourceId{StorageId: "storage", SpaceId: "space", OpaqueId: "space"}, - } - - gwc := &cs3mocks.GatewayAPIClient{} - gwc.On("Authenticate", mock.Anything, mock.Anything). - Return(&gateway.AuthenticateResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_OK}, - Token: "token", - }, nil) - gwc.On("GetShare", mock.Anything, mock.MatchedBy(func(req *collaboration.GetShareRequest) bool { - key := req.GetRef().GetKey() - rid := key.GetResourceId() - grantee := key.GetGrantee() - return rid.GetStorageId() == "storage" && - rid.GetSpaceId() == "space" && - rid.GetOpaqueId() == "space" && - grantee.GetUserId().GetOpaqueId() == "guest@example.com" - })).Return(&collaboration.GetShareResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_OK}, - Share: share, - }, nil) - - s := newShareService(t, gwc) - - got, err := s.GetShareForSpaceGrant(context.Background(), "storage$space", &user.UserId{OpaqueId: "guest@example.com", Type: user.UserType_USER_TYPE_GUEST}) - require.NoError(t, err) - assert.Equal(t, testShareID, got.GetId().GetOpaqueId()) - gwc.AssertExpectations(t) -} - func TestVerifyTokenSuccess(t *testing.T) { store := storage.NewFileManager(t.TempDir()) tok, rec := newToken(t) diff --git a/services/auth-guest/pkg/service/events/handlers.go b/services/auth-guest/pkg/service/events/handlers.go index 18aadd272a..c81e661f80 100644 --- a/services/auth-guest/pkg/service/events/handlers.go +++ b/services/auth-guest/pkg/service/events/handlers.go @@ -10,6 +10,7 @@ import ( user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" ocEvents "github.com/opencloud-eu/opencloud/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/events" + "github.com/opencloud-eu/reva/v2/pkg/storagespace" ) // handleShareCreated handles a share created event. @@ -83,22 +84,69 @@ func (s *EventConsumer) handleSpaceShared(ctx context.Context, ev events.SpaceSh return nil } - share, err := s.authGuest.GetShareForSpaceGrant(ctx, ev.ID.GetOpaqueId(), ev.GranteeUserID) + if ev.ShareID.GetOpaqueId() == "" { + s.log.Warn().Msg("space shared event without share id, skipping") + return nil + } + + // The space root node id is the space id. + itemID, err := storagespace.ParseID(ev.ID.GetOpaqueId()) if err != nil { return err } + itemID.OpaqueId = itemID.SpaceId - tok, err := s.authGuest.CreateToken(ctx, share.GetId().GetOpaqueId()) + tok, err := s.authGuest.CreateToken(ctx, ev.ShareID.GetOpaqueId()) if err != nil { return err } return events.Publish(ctx, s.stream, ocEvents.GuestTokenCreated{ - ShareID: share.GetId(), - Sharer: share.GetCreator(), + ShareID: ev.ShareID, + Sharer: ev.Creator, GranteeEmail: ev.GranteeUserID.GetOpaqueId(), - ItemID: share.GetResourceId(), + ItemID: &itemID, Token: tok.String(), Timestamp: time.Now(), }) } + +// handleSpaceUnshared handles a space unshared event. +func (s *EventConsumer) handleSpaceUnshared(ctx context.Context, ev events.SpaceUnshared) error { + _, span := tracer.Start(ctx, "handleSpaceUnshared") + defer span.End() + + if ev.GranteeUserID == nil || ev.GranteeUserID.GetType() != user.UserType_USER_TYPE_GUEST { + s.log.Debug().Msg("space unshared event is not for a guest, skipping") + return nil + } + + if ev.ShareID.GetOpaqueId() == "" { + s.log.Warn().Msg("space unshared event without share id, skipping") + return nil + } + + s.log.Debug().Interface("event", ev).Msg("space unshared event received") + + return s.authGuest.CleanupShare(ev.ShareID.GetOpaqueId()) +} + +// handleSpaceMembershipExpired handles a space membership expired event. +func (s *EventConsumer) handleSpaceMembershipExpired(ctx context.Context, ev events.SpaceMembershipExpired) error { + _, span := tracer.Start(ctx, "handleSpaceMembershipExpired") + defer span.End() + + if ev.GranteeUserID == nil || ev.GranteeUserID.GetType() != user.UserType_USER_TYPE_GUEST { + s.log.Debug().Msg("space membership expired event is not for a guest, skipping") + return nil + } + + if ev.ShareID.GetOpaqueId() == "" { + s.log.Warn().Msg("space membership expired event without share id, skipping") + return nil + } + + s.log.Debug().Interface("event", ev).Msg("space membership expired event received") + + return s.authGuest.CleanupShare(ev.ShareID.GetOpaqueId()) +} diff --git a/services/auth-guest/pkg/service/events/handlers_test.go b/services/auth-guest/pkg/service/events/handlers_test.go index 75c9ffa0cc..a41172786a 100644 --- a/services/auth-guest/pkg/service/events/handlers_test.go +++ b/services/auth-guest/pkg/service/events/handlers_test.go @@ -154,20 +154,14 @@ func TestHandleSpaceShared(t *testing.T) { tok, err := token.NewTokenService().Generate(testShareID) require.NoError(t, err) - share := &collaboration.Share{ - Id: &collaboration.ShareId{OpaqueId: testShareID}, - ResourceId: &provider.ResourceId{StorageId: "storage", SpaceId: "space", OpaqueId: "space"}, - Creator: &user.UserId{OpaqueId: "sharer"}, - } - svcMock := mocks.NewAuthGuest(t) - svcMock.On("GetShareForSpaceGrant", mock.Anything, "storage$space", mock.Anything).Return(share, nil) svcMock.On("CreateToken", mock.Anything, testShareID).Return(tok, nil) svc, bus := newConsumer(t, svcMock) ev := events.SpaceShared{ ID: &provider.StorageSpaceId{OpaqueId: "storage$space"}, + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, Executant: &user.UserId{OpaqueId: "executant"}, Creator: &user.UserId{OpaqueId: "creator"}, GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, @@ -180,10 +174,10 @@ func TestHandleSpaceShared(t *testing.T) { require.Len(t, bus.published, 1) published, ok := bus.published[0].(ocEvents.GuestTokenCreated) require.True(t, ok) - assert.Equal(t, testShareID, published.ShareID.GetOpaqueId()) + assert.Equal(t, ev.ShareID, published.ShareID) + assert.Equal(t, ev.Creator, published.Sharer) + assert.Equal(t, &provider.ResourceId{StorageId: "storage", SpaceId: "space", OpaqueId: "space"}, published.ItemID) assert.Equal(t, "guest@example.org", published.GranteeEmail) - assert.Equal(t, "space", published.ItemID.GetOpaqueId()) - assert.Equal(t, "sharer", published.Sharer.GetOpaqueId()) assert.NotEmpty(t, published.Token) } @@ -198,7 +192,112 @@ func TestHandleSpaceSharedSkipsNonGuest(t *testing.T) { require.NoError(t, svc.handleSpaceShared(context.Background(), ev)) - svcMock.AssertNotCalled(t, "GetShareForSpaceGrant", mock.Anything, mock.Anything, mock.Anything) svcMock.AssertNotCalled(t, "CreateToken", mock.Anything, mock.Anything) assert.Empty(t, bus.published) } + +func TestHandleSpaceSharedSkipsEmptyShareID(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svc, bus := newConsumer(t, svcMock) + + ev := events.SpaceShared{ + ID: &provider.StorageSpaceId{OpaqueId: "storage$space"}, + GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, + } + + require.NoError(t, svc.handleSpaceShared(context.Background(), ev)) + + svcMock.AssertNotCalled(t, "CreateToken", mock.Anything, mock.Anything) + assert.Empty(t, bus.published) +} + +func TestHandleSpaceUnshared(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svcMock.On("CleanupShare", testShareID).Return(nil) + svc, _ := newConsumer(t, svcMock) + + ev := events.SpaceUnshared{ + ID: &provider.StorageSpaceId{OpaqueId: "storage$space"}, + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + Executant: &user.UserId{OpaqueId: "executant"}, + GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, + } + + require.NoError(t, svc.handleSpaceUnshared(context.Background(), ev)) + + svcMock.AssertCalled(t, "CleanupShare", testShareID) +} + +func TestHandleSpaceUnsharedSkipsNonGuest(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svc, _ := newConsumer(t, svcMock) + + ev := events.SpaceUnshared{ + ID: &provider.StorageSpaceId{OpaqueId: "storage$space"}, + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + GranteeUserID: &user.UserId{OpaqueId: "user", Type: user.UserType_USER_TYPE_PRIMARY}, + } + + require.NoError(t, svc.handleSpaceUnshared(context.Background(), ev)) + + svcMock.AssertNotCalled(t, "CleanupShare", mock.Anything) +} + +func TestHandleSpaceUnsharedSkipsEmptyShareID(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svc, _ := newConsumer(t, svcMock) + + ev := events.SpaceUnshared{ + ID: &provider.StorageSpaceId{OpaqueId: "storage$space"}, + GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, + } + + require.NoError(t, svc.handleSpaceUnshared(context.Background(), ev)) + + svcMock.AssertNotCalled(t, "CleanupShare", mock.Anything) +} + +func TestHandleSpaceMembershipExpired(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svcMock.On("CleanupShare", testShareID).Return(nil) + svc, _ := newConsumer(t, svcMock) + + ev := events.SpaceMembershipExpired{ + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + SpaceID: &provider.StorageSpaceId{OpaqueId: "storage$space"}, + GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, + } + + require.NoError(t, svc.handleSpaceMembershipExpired(context.Background(), ev)) + + svcMock.AssertCalled(t, "CleanupShare", testShareID) +} + +func TestHandleSpaceMembershipExpiredSkipsNonGuest(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svc, _ := newConsumer(t, svcMock) + + ev := events.SpaceMembershipExpired{ + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + SpaceID: &provider.StorageSpaceId{OpaqueId: "storage$space"}, + GranteeUserID: &user.UserId{OpaqueId: "user", Type: user.UserType_USER_TYPE_PRIMARY}, + } + + require.NoError(t, svc.handleSpaceMembershipExpired(context.Background(), ev)) + + svcMock.AssertNotCalled(t, "CleanupShare", mock.Anything) +} + +func TestHandleSpaceMembershipExpiredSkipsEmptyShareID(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svc, _ := newConsumer(t, svcMock) + + ev := events.SpaceMembershipExpired{ + SpaceID: &provider.StorageSpaceId{OpaqueId: "storage$space"}, + GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, + } + + require.NoError(t, svc.handleSpaceMembershipExpired(context.Background(), ev)) + + svcMock.AssertNotCalled(t, "CleanupShare", mock.Anything) +} diff --git a/services/auth-guest/pkg/service/events/service.go b/services/auth-guest/pkg/service/events/service.go index aa955ceab6..44bec29f42 100644 --- a/services/auth-guest/pkg/service/events/service.go +++ b/services/auth-guest/pkg/service/events/service.go @@ -138,7 +138,10 @@ func (s *EventConsumer) processEvent(e events.Event) error { return s.handleShareExpired(ctx, ev) case events.SpaceShared: return s.handleSpaceShared(ctx, ev) - // Should we handle space share removed/expired events as well? + case events.SpaceUnshared: + return s.handleSpaceUnshared(ctx, ev) + case events.SpaceMembershipExpired: + return s.handleSpaceMembershipExpired(ctx, ev) default: s.log.Warn(). Str("eventtype", e.Type).