Skip to content
Open
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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ require (
github.com/open-policy-agent/opa v1.19.0
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
github.com/opencloud-eu/reva/v2 v2.48.0
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260812174127-12b756ed83c5
github.com/opensearch-project/opensearch-go/v4 v4.6.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
Expand Down Expand Up @@ -206,7 +206,7 @@ require (
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.19.1 // indirect
github.com/go-git/go-git/v5 v5.19.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand Down Expand Up @@ -942,8 +942,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.48.0 h1:G/4Jbv0DWWOfA5u5DtV0CB75pi9Wwtj7JkJQOEBvErs=
github.com/opencloud-eu/reva/v2 v2.48.0/go.mod h1:ZCo/xQM6if+upZa7rJCmdifZ/Y5XHLCrscHytC39yI4=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260812174127-12b756ed83c5 h1:Lg+7ZFXFi4lLOTjch99QYLp71sRkXunsOwxsrTaheGA=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260812174127-12b756ed83c5/go.mod h1:+2IJFVwi3yBEBfw0T+jgY7M9LBSPv7ecPYDiG3/bWYg=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
Expand Down
1 change: 1 addition & 0 deletions services/notifications/pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func Server(cfg *config.Config) *cobra.Command {
evs := []events.Unmarshaller{
events.ShareCreated{},
events.ShareExpired{},
events.ShareRemoved{},
events.SpaceShared{},
events.SpaceUnshared{},
events.SpaceMembershipExpired{},
Expand Down
13 changes: 13 additions & 0 deletions services/notifications/pkg/email/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ var (
CallToAction: l10n.Template(`Click here to view it: {ShareLink}`),
}

ShareRemoved = MessageTemplate{
textTemplate: _textTemplate,
htmlTemplate: _htmlTemplate,
// ShareRemoved email template, Subject field (resolves directly)
Subject: l10n.Template(`{ShareSharer} unshared '{ShareFolder}' with you`),
// ShareRemoved email template, resolves via {{ .Greeting }}
Greeting: l10n.Template(`Hello {ShareGrantee},`),
// ShareRemoved email template, resolves via {{ .MessageBody }}
MessageBody: l10n.Template(`{ShareSharer} has unshared '{ShareFolder}' with you.

Even though this share has been revoked you still might have access through other shares and/or space memberships.`),
}

ShareExpired = MessageTemplate{
textTemplate: _textTemplate,
htmlTemplate: _htmlTemplate,
Expand Down
19 changes: 19 additions & 0 deletions services/notifications/pkg/service/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ func (s eventsNotifier) createGroupedMail(ctx context.Context, logger zerolog.Lo
"ShareFolder": shareFolder,
"ExpiredAt": te.ExpiredAt.Format("2006-01-02 15:04:05"),
})
case events.ShareRemoved:
logger := logger.With().
Str("event", "ShareRemoved").
Str("eventId", te.ItemID.OpaqueId).
Logger()

executant, shareFolder, _, err := s.prepareShareRemoved(logger, te)
if err != nil {
logger.Error().Err(err).Msg("could not prepare vars for grouped email")
continue
}

mts = append(mts, email.ShareRemoved)
mtsVars = append(mtsVars, map[string]string{
"ShareSharer": executant.GetDisplayName(),
"ShareFolder": shareFolder,
})
default:
logger.Error().Str("eventType", e.Type).Msg("unsupported event type for grouped email")
}
}

Expand Down
2 changes: 2 additions & 0 deletions services/notifications/pkg/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ EventLoop:
s.handleShareCreated(e, evt.ID)
case events.ShareExpired:
s.handleShareExpired(e, evt.ID)
case events.ShareRemoved:
s.handleShareRemoved(e, evt.ID)
case events.ScienceMeshInviteTokenGenerated:
s.handleScienceMeshInviteTokenGenerated(e)
case events.SendEmailsEvent:
Expand Down
42 changes: 37 additions & 5 deletions services/notifications/pkg/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@ var _ = Describe("Notifications", func() {
},
)

gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once()
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once()
gatewayClient.On("GetUser", mock.Anything, mock.MatchedBy(func(req *user.GetUserRequest) bool {
return req.GetUserId().GetOpaqueId() == sharer.GetId().GetOpaqueId()
})).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
gatewayClient.On("GetUser", mock.Anything, mock.MatchedBy(func(req *user.GetUserRequest) bool {
return req.GetUserId().GetOpaqueId() == sharee.GetId().GetOpaqueId()
})).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil)
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Info: &provider.ResourceInfo{Name: "secrets of the board", Space: &provider.StorageSpace{Name: "secret space"}}}, nil)
vs = &settingsmocks.ValueService{}
Expand Down Expand Up @@ -157,6 +161,30 @@ https://opencloud.eu
},
}),

Entry("Share Removed", testChannel{
expectedReceipients: []string{sharee.GetMail()},
expectedSubject: "Dr. S. Harer unshared 'secrets of the board' with you",
expectedTextBody: `Hello Eric Expireling,

Dr. S. Harer has unshared 'secrets of the board' with you.

Even though this share has been revoked you still might have access through other shares and/or space memberships.


---
OpenCloud - a safe home for all your data
https://opencloud.eu
`,
expectedSender: sharer.GetDisplayName(),
done: make(chan struct{}),
}, events.Event{
Event: events.ShareRemoved{
Executant: sharer.GetId(),
GranteeUserID: sharee.GetId(),
ItemID: resourceid,
},
}),

Entry("Added to Space", testChannel{
expectedReceipients: []string{sharee.GetMail()},
expectedSubject: "Dr. S. Harer invited you to join secret space",
Expand Down Expand Up @@ -222,7 +250,7 @@ Even though this membership has expired you still might have access through othe
OpenCloud - a safe home for all your data
https://opencloud.eu
`,
expectedSender: sharer.GetDisplayName(),
expectedSender: "",
done: make(chan struct{}),
}, events.Event{
Event: events.SpaceMembershipExpired{
Expand Down Expand Up @@ -273,8 +301,12 @@ var _ = Describe("Notifications X-Site Scripting", func() {
},
)

gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once()
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once()
gatewayClient.On("GetUser", mock.Anything, mock.MatchedBy(func(req *user.GetUserRequest) bool {
return req.GetUserId().GetOpaqueId() == sharer.GetId().GetOpaqueId()
})).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
gatewayClient.On("GetUser", mock.Anything, mock.MatchedBy(func(req *user.GetUserRequest) bool {
return req.GetUserId().GetOpaqueId() == sharee.GetId().GetOpaqueId()
})).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil)
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_OK},
Expand Down
70 changes: 70 additions & 0 deletions services/notifications/pkg/service/shares.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,73 @@ func (s eventsNotifier) prepareShareExpired(logger zerolog.Logger, e events.Shar

return shareFolder, ctx, err
}

func (s eventsNotifier) handleShareRemoved(e events.ShareRemoved, eventId string) {
logger := s.logger.With().
Str("event", "ShareRemoved").
Str("itemid", e.ItemID.OpaqueId).
Logger()

executant, shareFolder, ctx, err := s.prepareShareRemoved(logger, e)
if err != nil {
logger.Error().Err(err).Msg("could not prepare vars for email")
return
}

granteeList := s.ensureGranteeList(ctx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventShareRemoved)

recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees)
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalDaily, eventId, recipientsDaily)...)
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalWeekly, eventId, recipientsInstantWeekly)...)
if recipientsInstant == nil {
return
}

sharerDisplayName := executant.GetDisplayName()

emails, err := s.render(ctx, email.ShareRemoved,
"ShareGrantee",
map[string]string{
"ShareSharer": sharerDisplayName,
"ShareFolder": shareFolder,
}, recipientsInstant, sharerDisplayName)
if err != nil {
logger.Error().Err(err).Msg("could not get render the email")
return
}
s.send(ctx, emails)
}

func (s eventsNotifier) prepareShareRemoved(logger zerolog.Logger, e events.ShareRemoved) (executant *user.User, shareFolder string, ctx context.Context, err error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
logger.Error().Err(err).Msg("could not select next gateway client")
return executant, shareFolder, ctx, err
}

ctx, err = utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
if err != nil {
logger.Error().Err(err).Msg("could not get service user context")
return executant, shareFolder, ctx, err
}

resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
if err != nil {
logger.Error().
Err(err).
Msg("could not stat resource")
return executant, shareFolder, ctx, err
}
shareFolder = resourceInfo.Name

executant, err = utils.GetUserNoGroups(ctx, e.Executant, gatewayClient)
if err != nil {
logger.Error().
Err(err).
Msg("could not get user")
return executant, shareFolder, ctx, err
}

return executant, shareFolder, ctx, err
}
58 changes: 36 additions & 22 deletions services/notifications/pkg/service/spaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,30 +187,13 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp
Str("itemid", e.SpaceID.GetOpaqueId()).
Logger()

gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
logger.Error().Err(err).Msg("could not select next gateway client")
return
}

ctx, err := utils.GetServiceUserContext(s.serviceAccountID, gatewayClient, s.serviceAccountSecret)
if err != nil {
logger.Error().Err(err).Msg("Could not impersonate sharer")
return
}

owner, err := utils.GetUserNoGroups(ctx, e.SpaceOwner, gatewayClient)
spaceName, ctx, err := s.prepareSpaceMembershipExpired(logger, e)
if err != nil {
logger.Error().
Err(err).
Msg("could not get user")
logger.Error().Err(err).Msg("could not prepare vars for email")
return
}

granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
if granteeList == nil {
return
}
granteeList := s.ensureGranteeList(ctx, nil, e.GranteeUserID, e.GranteeGroupID)
filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventSpaceMembershipExpired)

recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees)
Expand All @@ -223,12 +206,43 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp
emails, err := s.render(ctx, email.MembershipExpired,
"SpaceGrantee",
map[string]string{
"SpaceName": e.SpaceName,
"SpaceName": spaceName,
"ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"),
}, recipientsInstant, owner.GetDisplayName())
}, recipientsInstant, s.defaultEmailSender)
if err != nil {
logger.Error().Err(err).Msg("could not get render the email")
return
}
s.send(ctx, emails)
}

func (s eventsNotifier) prepareSpaceMembershipExpired(logger zerolog.Logger, e events.SpaceMembershipExpired) (spaceName string, ctx context.Context, err error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
logger.Error().Err(err).Msg("could not select next gateway client")
return spaceName, ctx, err
}

ctx, err = utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
if err != nil {
logger.Error().Err(err).Msg("could not get service user context")
return spaceName, ctx, err
}

resourceID, err := storagespace.ParseID(e.SpaceID.GetOpaqueId())
if err != nil {
logger.Error().Err(err).Msg("could not parse SpaceID")
return spaceName, ctx, err
}

resourceInfo, err := s.getResourceInfo(ctx, &resourceID, nil)
if err != nil {
logger.Error().
Err(err).
Msg("could not get space info")
return spaceName, ctx, err
}
spaceName = resourceInfo.GetSpace().GetName()

return spaceName, ctx, err
}
2 changes: 1 addition & 1 deletion services/settings/pkg/store/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ func generateBundleProfileRequest() *settingsmsg.Bundle {
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailFalseDisabled,
&optionMailTrue,
},
},
},
Expand Down
36 changes: 35 additions & 1 deletion services/userlog/pkg/service/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (c *Converter) ConvertEvent(eventid string, event any) (OC10Notification, e
case events.SpaceUnshared:
return c.spaceMessage(eventid, SpaceUnshared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp)
case events.SpaceMembershipExpired:
return c.spaceMessage(eventid, SpaceMembershipExpired, ev.SpaceOwner, ev.SpaceID.GetOpaqueId(), ev.ExpiredAt)
return c.spaceMembershipExpiredMessage(eventid, SpaceMembershipExpired, ev.SpaceID.GetOpaqueId(), ev.ExpiredAt)

// share related
case events.ShareCreated:
Expand Down Expand Up @@ -206,6 +206,40 @@ func (c *Converter) spaceMessage(eventid string, nt NotificationTemplate, execut
}, nil
}

func (c *Converter) spaceMembershipExpiredMessage(eventid string, nt NotificationTemplate, spaceid string, ts time.Time) (OC10Notification, error) {
space, err := c.getSpace(c.serviceAccountContext, spaceid)
if err != nil {
return OC10Notification{}, err
}

subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]any{
"spacename": space.GetName(),
})
if err != nil {
return OC10Notification{}, err
}

dets := map[string]any{
"space": map[string]string{
"id": space.GetId().GetOpaqueId(),
"name": space.GetName(),
},
}

return OC10Notification{
EventID: eventid,
Service: c.serviceName,
Timestamp: ts.Format(time.RFC3339Nano),
ResourceID: spaceid,
ResourceType: _resourceTypeSpace,
Subject: subj,
SubjectRaw: subjraw,
Message: msg,
MessageRaw: msgraw,
MessageDetails: dets,
}, nil
}

func (c *Converter) resourceMention(eventid string, nt NotificationTemplate, executant *user.UserId, resourceid *storageprovider.ResourceId, ts time.Time) (OC10Notification, error) {
usr, err := c.getUser(context.Background(), executant)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions services/userlog/pkg/service/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ func (ulf userlogFilter) filterUsersBySettings(ctx context.Context, users []stri
settingId = defaults.SettingUUIDProfileEventSpaceDeleted
case ocEvents.ResourceMention:
settingId = defaults.SettingUUIDProfileEventResourceMention
case events.PostprocessingStepFinished:
settingId = defaults.SettingUUIDProfileEventPostprocessingStepFinished
default:
// event that cannot be disabled
return users
Expand Down
Loading