Merge pull request #3765 from maki5/feat/guestlinks_spaceshare_cleanup_events

Feat/guestlinks spaceshare cleanup events
This commit is contained in:
Alex Ababii authored and GitHub committed 2026-10-23 14:22:41 +02:00
commit 984c2c75bf
8 files changed
+171 -240

No files matched your search

+2 -2
View File
@@ -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).
@@ -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.
@@ -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)
@@ -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
}
@@ -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)
@@ -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())
}
@@ -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)
}
@@ -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).