diff --git a/services/auth-guest/README.md b/services/auth-guest/README.md index fa33324b7c..c016b702b5 100644 --- a/services/auth-guest/README.md +++ b/services/auth-guest/README.md @@ -18,7 +18,9 @@ links feature and start the service. - **Consumes** the share lifecycle events `ShareCreated`, `ShareRemoved` and `ShareExpired`. - **Publishes** the `GuestTokenCreated` event (initial token) and the - `GuestTokenRenewed` event (refresh, carrying the new token and the PIN). + `GuestTokenRenewed` event (refresh, carrying the new token and the PIN). The + `notifications` service consumes both and mails the link (and the PIN) to + the guest. - Exposes unauthenticated endpoints to redeem a token, renew it and verify a PIN; redeeming a token or verifying a PIN sets a session cookie. - Stores only hashes of the token secret and of the PIN and deletes the stored @@ -111,7 +113,7 @@ sequenceDiagram Web->>+Renew: POST renew { permissionId } (+ session cookie or link token) Note right of Renew: Validate share, generate token + PIN, store hashes Renew->>Bus: publish GuestTokenRenewed - Note right of Bus: External consumer delivers token + PIN (out of scope) + Note right of Bus: notifications service mails the new link + PIN to the guest Renew->>-Web: 200 OK alt Guest opens the new link diff --git a/services/notifications/pkg/command/server.go b/services/notifications/pkg/command/server.go index b2d4df6a64..72fd45d432 100644 --- a/services/notifications/pkg/command/server.go +++ b/services/notifications/pkg/command/server.go @@ -92,6 +92,7 @@ func Server(cfg *config.Config) *cobra.Command { events.SendEmailsEvent{}, ocEvents.ResourceMention{}, ocEvents.GuestTokenCreated{}, + ocEvents.GuestTokenRenewed{}, } registeredEvents := make(map[string]events.Unmarshaller) for _, e := range evs { diff --git a/services/notifications/pkg/email/templates.go b/services/notifications/pkg/email/templates.go index e4432171b1..89a1ed01a4 100644 --- a/services/notifications/pkg/email/templates.go +++ b/services/notifications/pkg/email/templates.go @@ -67,6 +67,25 @@ The link is only valid for 30 minutes.`), CallToAction: l10n.Template(`Click here to view it: {ShareLink}`), } + GuestLinkShareRenewed = MessageTemplate{ + textTemplate: _textTemplate, + htmlTemplate: _htmlTemplate, + // GuestLinkShareRenewed email template, Subject field (resolves directly) + Subject: l10n.Template(`Your new link to '{ShareFolder}'`), + // GuestLinkShareRenewed email template, resolves via {{ .Greeting }} + Greeting: l10n.Template(`Hello,`), + // GuestLinkShareRenewed email template, resolves via {{ .MessageBody }} + MessageBody: l10n.Template(`A new link to "{ShareFolder}", shared with you by {ShareSharer}, has been requested. + +Your PIN: {SharePin} + +Open the link below or enter the PIN on the page where you requested the new link. The link and the PIN are personal and can only be used once. Please do not forward them. + +The link and the PIN are only valid for 30 minutes.`), + // GuestLinkShareRenewed email template, resolves via {{ .CallToAction }} + CallToAction: l10n.Template(`Click here to view it: {ShareLink}`), + } + // Spaces templates SharedSpace = MessageTemplate{ textTemplate: _textTemplate, @@ -166,6 +185,7 @@ var _placeholders = map[string]string{ "{ShareFolder}": "{{ .ShareFolder }}", "{ShareGrantee}": "{{ .ShareGrantee }}", "{ShareLink}": "{{ .ShareLink }}", + "{SharePin}": "{{ .SharePin }}", "{SpaceName}": "{{ .SpaceName }}", "{SpaceGrantee}": "{{ .SpaceGrantee }}", "{SpaceSharer}": "{{ .SpaceSharer }}", diff --git a/services/notifications/pkg/service/guestlinks.go b/services/notifications/pkg/service/guestlinks.go index 7f719fc17f..3c4cb7f85a 100644 --- a/services/notifications/pkg/service/guestlinks.go +++ b/services/notifications/pkg/service/guestlinks.go @@ -6,7 +6,10 @@ package service import ( "context" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/opencloud-eu/reva/v2/pkg/utils" + "github.com/rs/zerolog" "google.golang.org/protobuf/types/known/fieldmaskpb" ocevents "github.com/opencloud-eu/opencloud/pkg/events" @@ -23,11 +26,36 @@ func (s eventsNotifier) handleGuestTokenCreated(e ocevents.GuestTokenCreated) { Str("itemid", e.ItemID.GetOpaqueId()). Logger() - if err := validate.Var(e.GranteeEmail, "required,email"); err != nil { + s.sendGuestLinkMail(logger, email.GuestLinkShareCreated, e.Sharer, e.GranteeEmail, e.ItemID, e.ResourceName, e.Token, nil) +} + +// handleGuestTokenRenewed sends the renewed guest link and the PIN to the +// guest's email address, like handleGuestTokenCreated. +func (s eventsNotifier) handleGuestTokenRenewed(e ocevents.GuestTokenRenewed) { + logger := s.logger.With(). + Str("event", "GuestTokenRenewed"). + Str("itemid", e.ItemID.GetOpaqueId()). + Logger() + + if e.Pin == "" { + logger.Error().Msg("guest pin is empty") + return + } + + s.sendGuestLinkMail(logger, email.GuestLinkShareRenewed, e.Sharer, e.GranteeEmail, e.ItemID, "", e.Token, + map[string]string{"SharePin": e.Pin}) +} + +// sendGuestLinkMail renders the guest link template and sends it to the guest. +// The resource is stat'ed when resourceName is empty. vars are passed to the +// template in addition to ShareSharer, ShareFolder and ShareLink. +func (s eventsNotifier) sendGuestLinkMail(logger zerolog.Logger, template email.MessageTemplate, sharer *user.UserId, + granteeEmail string, itemID *provider.ResourceId, resourceName, token string, vars map[string]string) { + if err := validate.Var(granteeEmail, "required,email"); err != nil { logger.Error().Err(err).Msg("invalid guest email address") return } - if e.Token == "" { + if token == "" { logger.Error().Msg("guest token is empty") return } @@ -44,15 +72,15 @@ func (s eventsNotifier) handleGuestTokenCreated(e ocevents.GuestTokenCreated) { return } - owner, err := utils.GetUserNoGroups(ctx, e.Sharer, gatewayClient) + owner, err := utils.GetUserNoGroups(ctx, sharer, gatewayClient) if err != nil { logger.Error().Err(err).Msg("could not get user") return } - shareFolder := e.ResourceName + shareFolder := resourceName if shareFolder == "" { - resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}}) + resourceInfo, err := s.getResourceInfo(ctx, itemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}}) if err != nil { logger.Error().Err(err).Msg("could not stat resource") return @@ -60,25 +88,29 @@ func (s eventsNotifier) handleGuestTokenCreated(e ocevents.GuestTokenCreated) { shareFolder = resourceInfo.GetName() } - shareLink, err := urlJoinPath(s.openCloudURL, "g", e.Token) + shareLink, err := urlJoinPath(s.openCloudURL, "g", token) if err != nil { logger.Error().Err(err).Msg("could not create guest link") return } - msg, err := email.RenderEmailTemplate(email.GuestLinkShareCreated, s.defaultLanguage, s.defaultLanguage, - s.emailTemplatePath, s.translationPath, - map[string]string{ - "ShareSharer": owner.GetDisplayName(), - "ShareFolder": shareFolder, - "ShareLink": shareLink, - }) + templateVars := map[string]string{ + "ShareSharer": owner.GetDisplayName(), + "ShareFolder": shareFolder, + "ShareLink": shareLink, + } + for k, v := range vars { + templateVars[k] = v + } + + msg, err := email.RenderEmailTemplate(template, s.defaultLanguage, s.defaultLanguage, + s.emailTemplatePath, s.translationPath, templateVars) if err != nil { logger.Error().Err(err).Msg("could not render the email") return } msg.Sender = owner.GetDisplayName() - msg.Recipient = []string{e.GranteeEmail} + msg.Recipient = []string{granteeEmail} s.send(ctx, []*channels.Message{msg}) } diff --git a/services/notifications/pkg/service/service.go b/services/notifications/pkg/service/service.go index f4c916d587..23bce4cfff 100644 --- a/services/notifications/pkg/service/service.go +++ b/services/notifications/pkg/service/service.go @@ -133,6 +133,8 @@ EventLoop: s.handleShareRemoved(e, evt.ID) case ocEvents.GuestTokenCreated: s.handleGuestTokenCreated(e) + case ocEvents.GuestTokenRenewed: + s.handleGuestTokenRenewed(e) case events.ScienceMeshInviteTokenGenerated: s.handleScienceMeshInviteTokenGenerated(e) case events.SendEmailsEvent: diff --git a/services/notifications/pkg/service/service_test.go b/services/notifications/pkg/service/service_test.go index 54f0221284..bb19fc8306 100644 --- a/services/notifications/pkg/service/service_test.go +++ b/services/notifications/pkg/service/service_test.go @@ -338,6 +338,39 @@ https://opencloud.eu Token: "v1.sharehash.secret", }, }), + + Entry("Guest Token Renewed", testChannel{ + expectedReceipients: []string{"guest@example.com"}, + expectedSubject: "Your new link to 'secrets of the board'", + expectedTextBody: `Hello, + +A new link to "secrets of the board", shared with you by Dr. S. Harer, has been requested. + +Your PIN: 123456 + +Open the link below or enter the PIN on the page where you requested the new link. The link and the PIN are personal and can only be used once. Please do not forward them. + +The link and the PIN are only valid for 30 minutes. + +Click here to view it: https://cloud.example.com/g/v1.sharehash.newsecret + + +--- +OpenCloud - a safe home for all your data +https://opencloud.eu +`, + expectedSender: sharer.GetDisplayName(), + expectedLink: testOpenCloudURL + "/g/v1.sharehash.newsecret", + done: make(chan struct{}), + }, events.Event{ + Event: ocEvents.GuestTokenRenewed{ + Sharer: sharer.GetId(), + GranteeEmail: "guest@example.com", + ItemID: resourceid, + Token: "v1.sharehash.newsecret", + Pin: "123456", + }, + }), ) })