Merge pull request #5330 from kobergj/ExpirationNotifications
Expiration Notifications
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
Enhancement: Expiration Notifications
|
||||
|
||||
Send emails to the user informing that a share or a space membership expires.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/5330
|
||||
@@ -11,7 +11,7 @@ require (
|
||||
github.com/blevesearch/bleve/v2 v2.3.5
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230110081316-da324d280315
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230112100946-f8c784168ef7
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/ggwhite/go-masker v1.0.9
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
|
||||
@@ -344,8 +344,8 @@ github.com/crewjam/saml v0.4.9 h1:X2jDv4dv3IvfT9t+RhADavzNFAcq3fVxzTCIH3G605U=
|
||||
github.com/crewjam/saml v0.4.9/go.mod h1:9Zh6dWPtB3MSzTRt8fIFH60Z351QQ+s7hCU3J/tTlA4=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965 h1:y4n2j68LLnvac+zw/al8MfPgO5aQiIwLmHM/JzYN8AM=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230110081316-da324d280315 h1:pfOiXbiYW9ZPwpLOxtzSOuocanSoplhi8NE4KbQxhbk=
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230110081316-da324d280315/go.mod h1:GpocVB1w6yxeSr1VBsO9jztmt1SyNC4lCwudLwDzxHQ=
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230112100946-f8c784168ef7 h1:xULPUJdydKJbZ8z0MD4zkOqnz2oE+SXKw+3GzSYr+yY=
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230112100946-f8c784168ef7/go.mod h1:GpocVB1w6yxeSr1VBsO9jztmt1SyNC4lCwudLwDzxHQ=
|
||||
github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI=
|
||||
github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
@@ -873,6 +873,8 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
|
||||
github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kobergj/reva/v2 v2.0.0-20230112095039-1aba27929ce4 h1:jvN+oN9vHYXhrYv76OJ6bhXx7ldDpErblPZSwu0UzmA=
|
||||
github.com/kobergj/reva/v2 v2.0.0-20230112095039-1aba27929ce4/go.mod h1:GpocVB1w6yxeSr1VBsO9jztmt1SyNC4lCwudLwDzxHQ=
|
||||
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
||||
@@ -35,8 +35,10 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
// evs defines a list of events to subscribe to
|
||||
evs := []events.Unmarshaller{
|
||||
events.ShareCreated{},
|
||||
events.ShareExpired{},
|
||||
events.SpaceShared{},
|
||||
events.SpaceUnshared{},
|
||||
events.SpaceMembershipExpired{},
|
||||
}
|
||||
|
||||
evtsCfg := cfg.Notifications.Events
|
||||
|
||||
@@ -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 immer noch 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 }}
|
||||
@@ -0,0 +1,18 @@
|
||||
Hello {{ .SpaceGrantee }},
|
||||
|
||||
Your membership of space {{ .SpaceName }} has expired at {{ .ExpiredAt }}
|
||||
|
||||
Even though this membership has expired you still might have access through other shares and/or space memberships
|
||||
|
||||
----------------------------------------------------------
|
||||
|
||||
Hallo {{ .SpaceGrantee }},
|
||||
|
||||
Deine Mitgliedschaft zu dem Space {{ .SpaceName }} ist am {{ .ExpiredAt }} abgelaufen.
|
||||
|
||||
Obwohl diese Mitgliedschaft nicht mehr zur Verfügung steht, könntest du immer noch Zugriff über andere Freigaben und/oder Space Mitgliedschaften haben.
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
@@ -0,0 +1 @@
|
||||
Membership of '{{ .SpaceName }}' expired at {{ .ExpiredAt }}
|
||||
@@ -1 +1 @@
|
||||
{{ .SpaceSharer }} removed you from {{ .SpaceName }}
|
||||
{{ .SpaceSharer }} removed you from {{ .SpaceName }}
|
||||
@@ -0,0 +1,13 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestNotifications(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Notification Suite")
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -10,19 +11,18 @@ 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"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
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"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/notifications/pkg/channels"
|
||||
"github.com/owncloud/ocis/v2/services/notifications/pkg/email"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
)
|
||||
|
||||
// Service should be named `Runner`
|
||||
type Service interface {
|
||||
Run() error
|
||||
}
|
||||
@@ -70,8 +70,12 @@ func (s eventsNotifier) Run() error {
|
||||
s.handleSpaceShared(e)
|
||||
case events.SpaceUnshared:
|
||||
s.handleSpaceUnshared(e)
|
||||
case events.SpaceMembershipExpired:
|
||||
s.handleSpaceMembershipExpired(e)
|
||||
case events.ShareCreated:
|
||||
s.handleShareCreated(e)
|
||||
case events.ShareExpired:
|
||||
s.handleShareExpired(e)
|
||||
}
|
||||
}()
|
||||
case <-s.signals:
|
||||
@@ -82,138 +86,80 @@ 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{
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
return nil, fmt.Errorf("error getting user: %s", getUserResponse.Status.Message)
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
// Get auth context
|
||||
ownerCtx := revactx.ContextSetUser(context.Background(), getUserResponse.User)
|
||||
authRes, err := s.gwClient.Authenticate(ownerCtx, &gateway.AuthenticateRequest{
|
||||
Type: "machine",
|
||||
ClientId: "userid:" + userID.OpaqueId,
|
||||
ClientSecret: s.machineAuthAPIKey,
|
||||
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) getResourceInfo(ctx context.Context, resourceID *provider.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 +171,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,153 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/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"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/notifications/pkg/service"
|
||||
"github.com/test-go/testify/mock"
|
||||
)
|
||||
|
||||
var _ = Describe("Notifications", func() {
|
||||
var (
|
||||
gwc *cs3mocks.GatewayAPIClient
|
||||
sharer = &user.User{
|
||||
Id: &user.UserId{
|
||||
OpaqueId: "sharer",
|
||||
},
|
||||
DisplayName: "Dr. S. Harer",
|
||||
}
|
||||
sharee = &user.User{
|
||||
Id: &user.UserId{
|
||||
OpaqueId: "sharee",
|
||||
},
|
||||
DisplayName: "Eric Expireling",
|
||||
}
|
||||
resourceid = &provider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
SpaceId: "spaceid",
|
||||
OpaqueId: "itemid",
|
||||
}
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
gwc = &cs3mocks.GatewayAPIClient{}
|
||||
gwc.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
|
||||
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
|
||||
gwc.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)
|
||||
})
|
||||
|
||||
DescribeTable("Sending notifications",
|
||||
func(tc testChannel, ev interface{}) {
|
||||
ch := make(chan interface{})
|
||||
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gwc, "", "", "")
|
||||
go evts.Run()
|
||||
|
||||
ch <- ev
|
||||
select {
|
||||
case <-tc.done:
|
||||
// finished
|
||||
case <-time.Tick(3 * time.Second):
|
||||
Fail("timeout waiting for notification")
|
||||
}
|
||||
},
|
||||
|
||||
Entry("Share Created", testChannel{
|
||||
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
|
||||
expectedSubject: "Dr. S. Harer shared 'secrets of the board' with you",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.ShareCreated{
|
||||
Sharer: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
CTime: utils.TimeToTS(time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC)),
|
||||
ItemID: resourceid,
|
||||
}),
|
||||
|
||||
Entry("Share Expired", testChannel{
|
||||
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
|
||||
expectedSubject: "Share to 'secrets of the board' expired at 2023-04-17 16:42:00",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.ShareExpired{
|
||||
ShareOwner: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
ExpiredAt: time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC),
|
||||
ItemID: resourceid,
|
||||
}),
|
||||
|
||||
Entry("Added to Space", testChannel{
|
||||
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
|
||||
expectedSubject: "Dr. S. Harer invited you to join secret space",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.SpaceShared{
|
||||
Executant: sharer.GetId(),
|
||||
Creator: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
ID: &provider.StorageSpaceId{OpaqueId: "spaceid"},
|
||||
}),
|
||||
|
||||
Entry("Removed from Space", testChannel{
|
||||
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
|
||||
expectedSubject: "Dr. S. Harer removed you from secret space",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.SpaceUnshared{
|
||||
Executant: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
ID: &provider.StorageSpaceId{OpaqueId: "spaceid"},
|
||||
}),
|
||||
|
||||
Entry("Space Expired", testChannel{
|
||||
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
|
||||
expectedSubject: "Membership of 'secret space' expired at 2023-04-17 16:42:00",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.SpaceMembershipExpired{
|
||||
SpaceOwner: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: "spaceid"},
|
||||
SpaceName: "secret space",
|
||||
ExpiredAt: time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// NOTE: This is explictitly not testing the message itself. Should we?
|
||||
type testChannel struct {
|
||||
expectedReceipients map[string]bool
|
||||
expectedSubject string
|
||||
expectedSender string
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (tc testChannel) SendMessage(ctx context.Context, userIDs []string, msg, subject, senderDisplayName string) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
for _, u := range userIDs {
|
||||
Expect(tc.expectedReceipients[u]).To(Equal(true))
|
||||
}
|
||||
|
||||
// TODO: test the message?
|
||||
//Expect(msg).To(Equal(tc.expectedMessage))
|
||||
Expect(subject).To(Equal(tc.expectedSubject))
|
||||
Expect(senderDisplayName).To(Equal(tc.expectedSender))
|
||||
tc.done <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc testChannel) SendMessageToGroup(ctx context.Context, groupID *group.GroupId, msg, subject, senderDisplayName string) error {
|
||||
return tc.SendMessage(ctx, []string{groupID.GetOpaqueId()}, msg, subject, senderDisplayName)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"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, owner, err := utils.Impersonate(e.Sharer, s.gwClient, s.machineAuthAPIKey)
|
||||
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
|
||||
}
|
||||
|
||||
sharerDisplayName := owner.GetDisplayName()
|
||||
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()
|
||||
|
||||
ctx, owner, err := utils.Impersonate(e.ShareOwner, s.gwClient, s.machineAuthAPIKey)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Could not impersonate sharer")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
shareGrantee, err := s.getGranteeName(ctx, 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.GetName(),
|
||||
"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(ctx, e.GranteeUserID, e.GranteeGroupID, msg, subj, owner.GetDisplayName()); err != nil {
|
||||
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
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"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
|
||||
@@ -19,14 +12,13 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
|
||||
Str("itemid", e.ID.OpaqueId).
|
||||
Logger()
|
||||
|
||||
impersonateRes, err := s.impersonate(e.Executant)
|
||||
ownerCtx, owner, err := utils.Impersonate(e.Executant, s.gwClient, s.machineAuthAPIKey)
|
||||
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 +44,29 @@ 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{
|
||||
sharerDisplayName := owner.GetDisplayName()
|
||||
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,
|
||||
"SpaceName": resourceInfo.GetSpace().GetName(),
|
||||
"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 +76,11 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
|
||||
Str("itemid", e.ID.OpaqueId).
|
||||
Logger()
|
||||
|
||||
impersonateRes, err := s.impersonate(e.Executant)
|
||||
ownerCtx, owner, err := utils.Impersonate(e.Executant, s.gwClient, s.machineAuthAPIKey)
|
||||
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 +106,63 @@ 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{
|
||||
sharerDisplayName := owner.GetDisplayName()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExpired) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "SpaceMembershipExpired").
|
||||
Str("itemid", e.SpaceID.GetOpaqueId()).
|
||||
Logger()
|
||||
|
||||
ctx, owner, err := utils.Impersonate(e.SpaceOwner, s.gwClient, s.machineAuthAPIKey)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Could not impersonate sharer")
|
||||
return
|
||||
}
|
||||
|
||||
shareGrantee, err := s.getGranteeName(ctx, 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("spaces/membershipExpired.email.body.tmpl", "spaces/membershipExpired.email.subject.tmpl", map[string]string{
|
||||
"SpaceGrantee": shareGrantee,
|
||||
"SpaceName": e.SpaceName,
|
||||
"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(ctx, e.GranteeUserID, e.GranteeGroupID, msg, subj, owner.GetDisplayName()); err != nil {
|
||||
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user