share expired notification

Signed-off-by: jkoberg <jkoberg@owncloud.com>
This commit is contained in:
jkoberg
2023-01-04 16:29:11 +01:00
parent 4cba6028f6
commit cfe7f39027
5 changed files with 222 additions and 251 deletions
@@ -0,0 +1,18 @@
Hello {{ .ShareGrantee }},
Your share to {{ .ShareFolder }} has expired at {{ .ExpiredAt }}
Even though this share has been revoked you still might have access through other shares and/or space memberships
----------------------------------------------------------
Hallo {{ .ShareGrantee }},
Deine Freigabe zu {{ .ShareFolder }} ist am {{ .ExpiredAt }} abgelaufen.
Obwohl diese Freigabe nicht mehr zur Verfügung steht könntest du immernoch Zugriff über andere Freigaben und/oder space Mitgliedschaften haben.
---
ownCloud - Store. Share. Work.
https://owncloud.com
@@ -0,0 +1 @@
Share to '{{ .ShareFolder }}' expired at {{ .ExpiredAt }}
+83 -129
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"errors"
"fmt"
"net/url"
"os"
@@ -10,9 +11,10 @@ import (
"syscall"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
groupv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
@@ -23,6 +25,7 @@ import (
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// Service should be named `Runner`
type Service interface {
Run() error
}
@@ -82,122 +85,72 @@ func (s eventsNotifier) Run() error {
}
}
func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
logger := s.logger.With().
Str("event", "ShareCreated").
Str("itemid", e.ItemID.OpaqueId).
Logger()
impersonateRes, err := s.impersonate(e.Sharer)
func (s eventsNotifier) render(bodyTemplate string, subjTemplate string, values map[string]string) (string, string, error) {
msg, err := email.RenderEmailTemplate(bodyTemplate, values, s.emailTemplatePath)
if err != nil {
logger.Error().
Err(err).
Msg("Could not impersonate sharer")
return
return "", "", err
}
ownerCtx := metadata.AppendToOutgoingContext(context.Background(), revactx.TokenHeader, impersonateRes.Token)
resourceInfo, err := s.getResourceInfo(ownerCtx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
sub, err := email.RenderEmailTemplate(subjTemplate, values, s.emailTemplatePath)
if err != nil {
logger.Error().
Err(err).
Msg("could not stat resource")
return
return "", "", err
}
shareLink, err := urlJoinPath(s.ocisURL, "files/shares/with-me")
if err != nil {
logger.Error().
Err(err).
Msg("could not create link to the share")
return
}
shareGrantee := ""
switch {
// Note: We're using the 'ownerCtx' (authenticated as the share owner) here for requesting
// the Grantees of the shares. Ideally the notfication service would use some kind of service
// user for this.
case e.GranteeUserID != nil:
granteeUserResponse, err := s.gwClient.GetUser(ownerCtx, &userv1beta1.GetUserRequest{
UserId: e.GranteeUserID,
})
if err != nil || granteeUserResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
s.logger.Error().
Err(err).
Str("event", "ShareCreated").
Msg("Could not get user response from gatway client")
return
}
shareGrantee = granteeUserResponse.GetUser().DisplayName
case e.GranteeGroupID != nil:
granteeGroupResponse, err := s.gwClient.GetGroup(ownerCtx, &groupv1beta1.GetGroupRequest{
GroupId: e.GranteeGroupID,
})
if err != nil || granteeGroupResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
s.logger.Error().
Err(err).
Str("event", "ShareCreated").
Msg("Could not get group response from gatway client")
return
}
shareGrantee = granteeGroupResponse.GetGroup().DisplayName
default:
s.logger.Error().
Str("event", "ShareCreated").
Msg("Event 'ShareCreated' has no grantee")
return
}
sharerDisplayName := impersonateRes.GetUser().DisplayName
msg, err := email.RenderEmailTemplate("shares/shareCreated.email.body.tmpl", map[string]string{
"ShareGrantee": shareGrantee,
"ShareSharer": sharerDisplayName,
"ShareFolder": resourceInfo.Name,
"ShareLink": shareLink,
}, s.emailTemplatePath)
if err != nil {
s.logger.Error().
Err(err).
Str("event", "ShareCreated").
Msg("Could not render E-Mail body template for shares")
}
emailSubject, err := email.RenderEmailTemplate("shares/shareCreated.email.subject.tmpl", map[string]string{
"ShareSharer": sharerDisplayName,
"ShareFolder": resourceInfo.Name,
}, s.emailTemplatePath)
if err != nil {
s.logger.Error().
Err(err).
Str("event", "SpaceCreated").
Msg("Could not render E-Mail subject template for shares")
}
if e.GranteeUserID != nil {
err = s.channel.SendMessage(ownerCtx, []string{e.GranteeUserID.OpaqueId}, msg, emailSubject, sharerDisplayName)
} else if e.GranteeGroupID != nil {
err = s.channel.SendMessageToGroup(ownerCtx, e.GranteeGroupID, msg, emailSubject, sharerDisplayName)
}
if err != nil {
s.logger.Error().
Err(err).
Str("event", "ShareCreated").
Msg("failed to send a message")
}
return msg, sub, nil
}
func (s eventsNotifier) impersonate(userID *userv1beta1.UserId) (*gateway.AuthenticateResponse, error) {
getUserResponse, err := s.gwClient.GetUser(context.Background(), &userv1beta1.GetUserRequest{
func (s eventsNotifier) send(ctx context.Context, u *user.UserId, g *group.GroupId, msg, subj, sender string) error {
if u != nil {
return s.channel.SendMessage(ctx, []string{u.GetOpaqueId()}, msg, subj, sender)
}
if g != nil {
return s.channel.SendMessageToGroup(ctx, g, msg, subj, sender)
}
return nil
}
func (s eventsNotifier) getGranteeName(ctx context.Context, u *user.UserId, g *group.GroupId) (string, error) {
switch {
case u != nil:
r, err := s.gwClient.GetUser(ctx, &user.GetUserRequest{UserId: u})
if err != nil {
return "", err
}
if r.Status.Code != rpc.Code_CODE_OK {
return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
}
return r.GetUser().GetDisplayName(), nil
case g != nil:
r, err := s.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: g})
if err != nil {
return "", err
}
if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
}
return r.GetGroup().GetDisplayName(), nil
default:
return "", errors.New("Need at least one non-nil grantee")
}
}
func (s eventsNotifier) impersonate(userID *user.UserId) (context.Context, string, error) {
getUserResponse, err := s.gwClient.GetUser(context.Background(), &user.GetUserRequest{
UserId: userID,
})
if err != nil {
return nil, err
return nil, "", err
}
if getUserResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, fmt.Errorf("error getting user: %s", getUserResponse.Status.Message)
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
return nil, "", fmt.Errorf("error getting user: %s", getUserResponse.Status.Message)
}
// Get auth context
@@ -207,13 +160,33 @@ func (s eventsNotifier) impersonate(userID *userv1beta1.UserId) (*gateway.Authen
ClientId: "userid:" + userID.OpaqueId,
ClientSecret: s.machineAuthAPIKey,
})
if err != nil {
return nil, "", err
}
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return nil, "", fmt.Errorf("error impersonating user: %s", authRes.Status.Message)
}
return metadata.AppendToOutgoingContext(context.Background(), revactx.TokenHeader, authRes.Token), authRes.GetUser().GetDisplayName(), nil
}
func (s eventsNotifier) getResourceInfo(ctx context.Context, resourceID *providerv1beta1.ResourceId, fieldmask *fieldmaskpb.FieldMask) (*provider.ResourceInfo, error) {
// TODO: maybe cache this stat to reduce storage iops
md, err := s.gwClient.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: resourceID,
},
FieldMask: fieldmask,
})
if err != nil {
return nil, err
}
if authRes.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
return nil, fmt.Errorf("error impersonating user: %s", authRes.Status.Message)
if md.Status.Code != rpc.Code_CODE_OK {
return nil, fmt.Errorf("could not resource info: %s", md.Status.Message)
}
return authRes, nil
return md.GetInfo(), nil
}
// TODO: this function is a backport for go1.19 url.JoinPath, upon go bump, replace this
@@ -225,22 +198,3 @@ func urlJoinPath(base string, elements ...string) (string, error) {
u.Path = path.Join(append([]string{u.Path}, elements...)...)
return u.String(), nil
}
func (s eventsNotifier) getResourceInfo(ctx context.Context, resourceID *providerv1beta1.ResourceId, fieldmask *fieldmaskpb.FieldMask) (*providerv1beta1.ResourceInfo, error) {
// TODO: maybe cache this stat to reduce storage iops
md, err := s.gwClient.Stat(ctx, &providerv1beta1.StatRequest{
Ref: &providerv1beta1.Reference{
ResourceId: resourceID,
},
FieldMask: fieldmask,
})
if err != nil {
return nil, err
}
if md.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, fmt.Errorf("could not resource info: %s", md.Status.Message)
}
return md.GetInfo(), nil
}
@@ -0,0 +1,99 @@
package service
import (
"github.com/cs3org/reva/v2/pkg/events"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
logger := s.logger.With().
Str("event", "ShareCreated").
Str("itemid", e.ItemID.OpaqueId).
Logger()
ownerCtx, sharerDisplayName, err := s.impersonate(e.Sharer)
if err != nil {
logger.Error().Err(err).Msg("Could not impersonate sharer")
return
}
resourceInfo, err := s.getResourceInfo(ownerCtx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
if err != nil {
logger.Error().
Err(err).
Msg("could not stat resource")
return
}
shareLink, err := urlJoinPath(s.ocisURL, "files/shares/with-me")
if err != nil {
logger.Error().
Err(err).
Msg("could not create link to the share")
return
}
shareGrantee, err := s.getGranteeName(ownerCtx, e.GranteeUserID, e.GranteeGroupID)
if err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee name")
return
}
msg, subj, err := s.render("shares/shareCreated.email.body.tmpl", "shares/shareCreated.email.subject.tmpl", map[string]string{
"ShareGrantee": shareGrantee,
"ShareSharer": sharerDisplayName,
"ShareFolder": resourceInfo.Name,
"ShareLink": shareLink,
})
if err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not render E-Mail body template for shares")
}
if err := s.send(ownerCtx, e.GranteeUserID, e.GranteeGroupID, msg, subj, sharerDisplayName); err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
}
}
func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
logger := s.logger.With().
Str("event", "ShareExpired").
Str("itemid", e.ItemID.GetOpaqueId()).
Logger()
ownerCtx, sharerDisplayName, err := s.impersonate(e.ShareOwner)
if err != nil {
logger.Error().Err(err).Msg("Could not impersonate sharer")
return
}
resourceInfo, err := s.getResourceInfo(ownerCtx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
if err != nil {
logger.Error().
Err(err).
Msg("could not stat resource")
return
}
shareGrantee, err := s.getGranteeName(ownerCtx, e.GranteeUserID, e.GranteeGroupID)
if err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee name")
return
}
msg, subj, err := s.render("shares/shareExpired.email.body.tmpl", "shares/shareExpired.email.subject.tmpl", map[string]string{
"ShareGrantee": shareGrantee,
"ShareFolder": resourceInfo.Name,
"ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"),
})
if err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not render E-Mail body template for shares")
}
if err := s.send(ownerCtx, e.GranteeUserID, e.GranteeGroupID, msg, subj, sharerDisplayName); err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
}
}
+21 -122
View File
@@ -1,16 +1,8 @@
package service
import (
"context"
groupv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/owncloud/ocis/v2/services/notifications/pkg/email"
"google.golang.org/grpc/metadata"
)
func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
@@ -19,14 +11,13 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
Str("itemid", e.ID.OpaqueId).
Logger()
impersonateRes, err := s.impersonate(e.Executant)
ownerCtx, sharerDisplayName, err := s.impersonate(e.Executant)
if err != nil {
logger.Error().
Err(err).
Msg("could not handle space shared event")
return
}
ownerCtx := metadata.AppendToOutgoingContext(context.Background(), revactx.TokenHeader, impersonateRes.Token)
resourceID, err := storagespace.ParseID(e.ID.OpaqueId)
if err != nil {
@@ -52,73 +43,28 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
return
}
spaceGrantee := ""
switch {
// Note: We're using the 'ownerCtx' (authenticated as the share owner) here for requesting
// the Grantees of the shares. Ideally the notfication service would use some kind of service
// user for this.
case e.GranteeUserID != nil:
granteeUserResponse, err := s.gwClient.GetUser(ownerCtx, &userv1beta1.GetUserRequest{
UserId: e.GranteeUserID,
})
if err != nil || granteeUserResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Err(err).
Msg("Could not get user response from gatway client")
return
}
spaceGrantee = granteeUserResponse.GetUser().GetDisplayName()
case e.GranteeGroupID != nil:
granteeGroupResponse, err := s.gwClient.GetGroup(ownerCtx, &groupv1beta1.GetGroupRequest{
GroupId: e.GranteeGroupID,
})
if err != nil || granteeGroupResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Err(err).
Msg("Could not get group response from gatway client")
return
}
spaceGrantee = granteeGroupResponse.GetGroup().GetDisplayName()
default:
logger.Error().
Msg("Event 'SpaceShared' has no grantee")
spaceGrantee, err := s.getGranteeName(ownerCtx, e.GranteeUserID, e.GranteeGroupID)
if err != nil {
logger.Error().Err(err).Msg("Could not get grantee name")
return
}
sharerDisplayName := impersonateRes.GetUser().GetDisplayName()
msg, err := email.RenderEmailTemplate("spaces/sharedSpace.email.body.tmpl", map[string]string{
msg, subj, err := s.render("spaces/sharedSpace.email.body.tmpl", "spaces/sharedSpace.email.subject.tmpl", map[string]string{
"SpaceGrantee": spaceGrantee,
"SpaceSharer": sharerDisplayName,
"SpaceName": resourceInfo.GetSpace().Name,
"ShareLink": shareLink,
}, s.emailTemplatePath)
})
if err != nil {
logger.Error().
Err(err).
Msg("Could not render E-Mail body template for spaces")
logger.Error().Err(err).Msg("Could not render E-Mail template for spaces")
return
}
emailSubject, err := email.RenderEmailTemplate("spaces/sharedSpace.email.subject.tmpl", map[string]string{
"SpaceSharer": sharerDisplayName,
"SpaceName": resourceInfo.GetSpace().Name,
}, s.emailTemplatePath)
if err != nil {
logger.Error().
Err(err).
Msg("Could not render E-Mail subject template for spaces")
}
if e.GranteeUserID != nil {
err = s.channel.SendMessage(ownerCtx, []string{e.GranteeUserID.OpaqueId}, msg, emailSubject, sharerDisplayName)
} else if e.GranteeGroupID != nil {
err = s.channel.SendMessageToGroup(ownerCtx, e.GranteeGroupID, msg, emailSubject, sharerDisplayName)
}
if err != nil {
logger.Error().
Err(err).
Msg("failed to send a message")
if err := s.send(ownerCtx, e.GranteeUserID, e.GranteeGroupID, msg, subj, sharerDisplayName); err != nil {
logger.Error().Err(err).Msg("failed to send a message")
}
}
@@ -128,14 +74,11 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
Str("itemid", e.ID.OpaqueId).
Logger()
impersonateRes, err := s.impersonate(e.Executant)
ownerCtx, sharerDisplayName, err := s.impersonate(e.Executant)
if err != nil {
logger.Error().
Err(err).
Msg("could not handle space unshared event")
logger.Error().Err(err).Msg("could not handle space unshared event")
return
}
ownerCtx := metadata.AppendToOutgoingContext(context.Background(), revactx.TokenHeader, impersonateRes.Token)
resourceID, err := storagespace.ParseID(e.ID.OpaqueId)
if err != nil {
@@ -161,72 +104,28 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
return
}
spaceGrantee := ""
switch {
// Note: We're using the 'ownerCtx' (authenticated as the share owner) here for requesting
// the Grantees of the shares. Ideally the notfication service would use some kind of service
// user for this.
case e.GranteeUserID != nil:
granteeUserResponse, err := s.gwClient.GetUser(ownerCtx, &userv1beta1.GetUserRequest{
UserId: e.GranteeUserID,
})
if err != nil || granteeUserResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Err(err).
Msg("Could not get user response from gatway client")
return
}
spaceGrantee = granteeUserResponse.GetUser().GetDisplayName()
case e.GranteeGroupID != nil:
granteeGroupResponse, err := s.gwClient.GetGroup(ownerCtx, &groupv1beta1.GetGroupRequest{
GroupId: e.GranteeGroupID,
})
if err != nil || granteeGroupResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Err(err).
Msg("Could not get group response from gatway client")
return
}
spaceGrantee = granteeGroupResponse.GetGroup().GetDisplayName()
default:
logger.Error().
Msg("Event 'SpaceShared' has no grantee")
spaceGrantee, err := s.getGranteeName(ownerCtx, e.GranteeUserID, e.GranteeGroupID)
if err != nil {
logger.Error().Err(err).Msg("Could not get grantee name")
return
}
sharerDisplayName := impersonateRes.GetUser().GetDisplayName()
msg, err := email.RenderEmailTemplate("spaces/unsharedSpace.email.body.tmpl", map[string]string{
msg, subj, err := s.render("spaces/unsharedSpace.email.body.tmpl", "spaces/unsharedSpace.email.subject.tmpl", map[string]string{
"SpaceGrantee": spaceGrantee,
"SpaceSharer": sharerDisplayName,
"SpaceName": resourceInfo.GetSpace().Name,
"ShareLink": shareLink,
}, s.emailTemplatePath)
})
if err != nil {
logger.Error().
Err(err).
Msg("Could not render E-Mail body template for spaces")
logger.Error().Err(err).Msg("Could not render E-Mail template for spaces")
return
}
emailSubject, err := email.RenderEmailTemplate("spaces/unsharedSpace.email.subject.tmpl", map[string]string{
"SpaceSharer": sharerDisplayName,
"SpaceName": resourceInfo.GetSpace().Name,
}, s.emailTemplatePath)
if err != nil {
logger.Error().
Err(err).
Msg("Could not render E-Mail subject template for spaces")
}
if e.GranteeUserID != nil {
err = s.channel.SendMessage(ownerCtx, []string{e.GranteeUserID.OpaqueId}, msg, emailSubject, sharerDisplayName)
} else if e.GranteeGroupID != nil {
err = s.channel.SendMessageToGroup(ownerCtx, e.GranteeGroupID, msg, emailSubject, sharerDisplayName)
}
if err != nil {
logger.Error().
Err(err).
Msg("failed to send a message")
if err := s.send(ownerCtx, e.GranteeUserID, e.GranteeGroupID, msg, subj, sharerDisplayName); err != nil {
logger.Error().Err(err).Msg("failed to send a message")
}
}