diff --git a/services/userlog/pkg/command/server.go b/services/userlog/pkg/command/server.go index 7ca044d29..6f09a5371 100644 --- a/services/userlog/pkg/command/server.go +++ b/services/userlog/pkg/command/server.go @@ -25,36 +25,17 @@ import ( // all events we care about var _registeredEvents = []events.Unmarshaller{ - // file related - events.UploadReady{}, - events.ContainerCreated{}, - events.FileTouched{}, - events.FileDownloaded{}, - events.FileVersionRestored{}, - events.ItemMoved{}, - events.ItemTrashed{}, - events.ItemPurged{}, - events.ItemRestored{}, - // space related - events.SpaceCreated{}, - events.SpaceRenamed{}, - events.SpaceEnabled{}, events.SpaceDisabled{}, events.SpaceDeleted{}, events.SpaceShared{}, events.SpaceUnshared{}, - events.SpaceUpdated{}, events.SpaceMembershipExpired{}, // share related events.ShareCreated{}, - // events.ShareRemoved{}, // TODO: ShareRemoved doesn't hold sharee information - events.ShareUpdated{}, + events.ShareRemoved{}, events.ShareExpired{}, - events.LinkCreated{}, - // events.LinkRemoved{}, // TODO: LinkRemoved doesn't hold sharee information - events.LinkUpdated{}, } // Server is the entrypoint for the server command. diff --git a/services/userlog/pkg/service/conversion.go b/services/userlog/pkg/service/conversion.go new file mode 100644 index 000000000..46c536ea3 --- /dev/null +++ b/services/userlog/pkg/service/conversion.go @@ -0,0 +1,144 @@ +package service + +import ( + "bytes" + "context" + "path/filepath" + "text/template" + "time" + + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/v2/pkg/events" + "github.com/cs3org/reva/v2/pkg/storagespace" +) + +var ( + _resourceTypeSpace = "storagespace" +) + +// OC10Notification is the oc10 style representation of an event +// some fields are left out for simplicity +type OC10Notification struct { + EventID string `json:"notification_id"` + Service string `json:"app"` + UserName string `json:"user"` + Timestamp string `json:"datetime"` + ResourceID string `json:"object_id"` + ResourceType string `json:"object_type"` + Subject string `json:"subject"` + Message string `json:"message"` + MessageRaw string `json:"messageRich"` + MessageDetails map[string]interface{} `json:"messageRichParameters"` +} + +// SpaceDisabled converts a SpaceDisabled event to an OC10Notification +func (ul *UserlogService) SpaceDisabled(ctx context.Context, eventid string, ev events.SpaceDisabled) (OC10Notification, error) { + user, err := ul.getUser(ctx, ev.Executant) + if err != nil { + return OC10Notification{}, err + } + + space, err := ul.getSpace(ul.impersonate(user.GetId()), ev.ID.GetOpaqueId()) + if err != nil { + return OC10Notification{}, err + } + + subj, msg, msgraw, err := ul.composeMessage("space-disabled", map[string]string{ + "username": user.GetUsername(), + "spacename": space.GetName(), + }) + if err != nil { + return OC10Notification{}, err + } + + return OC10Notification{ + EventID: eventid, + Service: ul.cfg.Service.Name, + Timestamp: time.Now().Format(time.RFC3339Nano), + Subject: subj, + ResourceID: ev.ID.GetOpaqueId(), + ResourceType: _resourceTypeSpace, + Message: msg, + MessageRaw: msgraw, + MessageDetails: ul.getMessageDetails(user, space, nil), + }, nil + +} + +func (ul *UserlogService) composeMessage(eventname string, vars map[string]string) (string, string, string, error) { + subjtpl, err := ul.parseTemplate(eventname + "-subj") + if err != nil { + return "", "", "", err + } + + subject := ul.executeTemplate(subjtpl, vars) + + msgtpl, err := ul.parseTemplate(eventname + "-msg") + if err != nil { + return "", "", "", err + } + + message := ul.executeTemplate(msgtpl, vars) + + messageraw := ul.executeTemplate(msgtpl, map[string]string{ + "username": "{user}", + "spacename": "{space}", + "resource": "{resource}", + }) + + return subject, message, messageraw, nil + +} + +func (ul *UserlogService) getMessageDetails(user *user.User, space *storageprovider.StorageSpace, item *storageprovider.ResourceInfo) map[string]interface{} { + details := make(map[string]interface{}) + + if user != nil { + details["user"] = map[string]string{ + "id": user.GetId().GetOpaqueId(), + "name": user.GetUsername(), + } + } + + if space != nil { + details["space"] = map[string]string{ + "id": space.GetId().GetOpaqueId(), + "name": space.GetName(), + } + } + + if item != nil { + details["resource"] = map[string]string{ + "id": storagespace.FormatResourceID(*item.GetId()), + "name": item.GetName(), + } + } + + return details +} + +func (ul *UserlogService) parseTemplate(templateName string) (*template.Template, error) { + var ( + tpl *template.Template + err error + ) + switch ul.templatePath { + case "": + tpl, err = template.ParseFS(templatesFS, filepath.Join("templates/", templateName+".tmpl")) + default: + tpl, err = template.ParseFiles(filepath.Join(ul.templatePath, templateName+".tmpl")) + } + + return tpl, err +} + +func (ul *UserlogService) executeTemplate(tpl *template.Template, vars map[string]string) string { + var writer bytes.Buffer + if err := tpl.Execute(&writer, vars); err != nil { + ul.log.Error().Err(err).Str("templateName", tpl.Name()).Msg("cannot execute template") + return "" + } + + return writer.String() +} diff --git a/services/userlog/pkg/service/http.go b/services/userlog/pkg/service/http.go index 4ad2011ac..1b9c7d385 100644 --- a/services/userlog/pkg/service/http.go +++ b/services/userlog/pkg/service/http.go @@ -2,6 +2,7 @@ package service import ( "context" + "embed" "encoding/json" "errors" "fmt" @@ -14,6 +15,11 @@ import ( ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ) +var ( + //go:embed templates + templatesFS embed.FS +) + // ServeHTTP fulfills Handler interface func (ul *UserlogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { ul.m.ServeHTTP(w, r) @@ -97,119 +103,39 @@ func (ul *UserlogService) convertEvent(ctx context.Context, event *ehmsg.Event) // TODO: strange bug with getting space -> fix postponed to make master panic-free var space storageprovider.StorageSpace - switch ev := einterface.(type) { - // file related - case events.UploadReady: - noti.UserID = ev.ExecutingUser.GetId().GetOpaqueId() - noti.Subject = "File uploaded" - noti.Message = fmt.Sprintf("File '%s' was uploaded to space '%s' by user '%s'", ev.Filename, space.GetName(), ev.ExecutingUser.GetUsername()) - case events.ContainerCreated: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Folder created" - noti.Message = fmt.Sprintf("Folder '%s' was created", ev.Ref.GetPath()) - case events.FileTouched: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "File touched" - noti.Message = fmt.Sprintf("File '%s' was touched", ev.Ref.GetPath()) - case events.FileDownloaded: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "File downloaded" - noti.Message = fmt.Sprintf("File '%s' was downloaded", ev.Ref.GetPath()) - case events.FileVersionRestored: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "File version restored" - noti.Message = fmt.Sprintf("An older version of file '%s' was restored", ev.Ref.GetPath()) - case events.ItemMoved: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "File moved" - noti.Message = fmt.Sprintf("File '%s' was moved from '%s'", ev.Ref.GetPath(), ev.OldReference.GetPath()) - case events.ItemTrashed: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "File trashed" - noti.Message = fmt.Sprintf("File '%s' was trashed", ev.Ref.GetPath()) - case events.ItemPurged: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "File purged" - noti.Message = fmt.Sprintf("File '%s' was purged", ev.Ref.GetPath()) - case events.ItemRestored: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "File restored" - noti.Message = fmt.Sprintf("File '%s' was restored", ev.Ref.GetPath()) + switch ev := einterface.(type) { // space related - case events.SpaceCreated: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Space created" - noti.Message = fmt.Sprintf("Space '%s' was created", ev.Name) - case events.SpaceRenamed: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Space renamed" - noti.Message = fmt.Sprintf("Space '%s' was renamed", ev.Name) - case events.SpaceEnabled: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Space enabled" - noti.Message = fmt.Sprintf("Space '%s' was renamed", space.Name) case events.SpaceDisabled: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Space disabled" - noti.Message = fmt.Sprintf("Space '%s' was disabled", space.Name) + return ul.SpaceDisabled(ctx, event.Id, ev) case events.SpaceDeleted: - noti.UserID = ev.Executant.GetOpaqueId() noti.Subject = "Space deleted" noti.Message = fmt.Sprintf("Space '%s' was deleted", space.Name) case events.SpaceShared: - noti.UserID = ev.Executant.GetOpaqueId() noti.Subject = "Space shared" noti.Message = fmt.Sprintf("Space '%s' was shared", space.Name) case events.SpaceUnshared: - noti.UserID = ev.Executant.GetOpaqueId() noti.Subject = "Space unshared" noti.Message = fmt.Sprintf("Space '%s' was unshared", space.Name) - case events.SpaceUpdated: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Space updated" - noti.Message = fmt.Sprintf("Space '%s' was updated", space.Name) case events.SpaceMembershipExpired: - noti.UserID = "" noti.Subject = "Space membership expired" noti.Message = fmt.Sprintf("A spacemembership for space '%s' has expired", space.Name) // share related case events.ShareCreated: - noti.UserID = ev.Executant.GetOpaqueId() noti.Subject = "Share received" noti.Message = fmt.Sprintf("A file was shared in space %s", space.Name) - case events.ShareUpdated: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Share updated" - noti.Message = fmt.Sprintf("A share was updated in space %s", space.Name) case events.ShareExpired: noti.Subject = "Share expired" noti.Message = fmt.Sprintf("A share has expired in space %s", space.Name) - case events.LinkCreated: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Share received" - noti.Message = fmt.Sprintf("A link was created in space %s", space.Name) - case events.LinkUpdated: - noti.UserID = ev.Executant.GetOpaqueId() - noti.Subject = "Share received" - noti.Message = fmt.Sprintf("A link was updated in space %s", space.Name) + case events.ShareRemoved: + noti.Subject = "Share removed" + noti.Message = "share was removed" } return noti, nil } -// OC10Notification is the oc10 style representation of an event -// some fields are left out for simplicity -type OC10Notification struct { - EventID string `json:"notification_id"` - Service string `json:"app"` - Timestamp string `json:"datetime"` - UserID string `json:"user"` - Subject string `json:"subject"` - Message string `json:"message"` -} - // GetEventResponseOC10 is the response from GET events endpoint in oc10 style type GetEventResponseOC10 struct { OCS struct { diff --git a/services/userlog/pkg/service/options.go b/services/userlog/pkg/service/options.go index b84c67725..2bb737bd1 100644 --- a/services/userlog/pkg/service/options.go +++ b/services/userlog/pkg/service/options.go @@ -23,6 +23,7 @@ type Options struct { HistoryClient ehsvc.EventHistoryService GatewayClient gateway.GatewayAPIClient RegisteredEvents []events.Unmarshaller + TemplatePath string } // Logger configures a logger for the userlog service @@ -80,3 +81,10 @@ func RegisteredEvents(e []events.Unmarshaller) Option { o.RegisteredEvents = e } } + +// TemplatePath registers the events the service should listen to +func TemplatePath(tmpl string) Option { + return func(o *Options) { + o.TemplatePath = tmpl + } +} diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index 838f75dc9..cb4695373 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -32,6 +32,7 @@ type UserlogService struct { historyClient ehsvc.EventHistoryService gwClient gateway.GatewayAPIClient registeredEvents map[string]events.Unmarshaller + templatePath string // for custom notification templates } // NewUserlogService returns an EventHistory service @@ -59,6 +60,7 @@ func NewUserlogService(opts ...Option) (*UserlogService, error) { historyClient: o.HistoryClient, gwClient: o.GatewayClient, registeredEvents: make(map[string]events.Unmarshaller), + templatePath: o.TemplatePath, } for _, e := range o.RegisteredEvents { @@ -85,62 +87,30 @@ func (ul *UserlogService) MemorizeEvents() { users []string err error ) + + fmt.Println("RECEIVED", event) switch e := event.Event.(type) { default: err = errors.New("unhandled event") - - // file related - case events.UploadReady: - users, err = ul.findSpaceMembers(ul.impersonate(e.SpaceOwner), e.FileRef.GetResourceId().GetSpaceId(), viewer) - case events.ContainerCreated: - users, err = ul.findSpaceMembers(ul.impersonate(e.SpaceOwner), e.Ref.GetResourceId().GetSpaceId(), viewer) - case events.FileTouched: - users, err = ul.findSpaceMembers(ul.impersonate(e.SpaceOwner), e.Ref.GetResourceId().GetSpaceId(), viewer) - case events.FileDownloaded: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.Ref.GetResourceId().GetSpaceId(), viewer) // no space owner in event - case events.FileVersionRestored: - users, err = ul.findSpaceMembers(ul.impersonate(e.SpaceOwner), e.Ref.GetResourceId().GetSpaceId(), editor) - case events.ItemMoved: - users, err = ul.findSpaceMembers(ul.impersonate(e.SpaceOwner), e.Ref.GetResourceId().GetSpaceId(), viewer) - case events.ItemTrashed: - users, err = ul.findSpaceMembers(ul.impersonate(e.SpaceOwner), e.Ref.GetResourceId().GetSpaceId(), viewer) - case events.ItemPurged: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.Ref.GetResourceId().GetSpaceId(), editor) // no space owner in event - case events.ItemRestored: - users, err = ul.findSpaceMembers(ul.impersonate(e.SpaceOwner), e.Ref.GetResourceId().GetSpaceId(), viewer) - // space related // TODO: how to find spaceadmins? - case events.SpaceCreated: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), viewer) - case events.SpaceRenamed: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), viewer) - case events.SpaceEnabled: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), viewer) case events.SpaceDisabled: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), manager) + users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), viewer) case events.SpaceDeleted: users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), manager) case events.SpaceShared: users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), manager) case events.SpaceUnshared: users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), manager) - case events.SpaceUpdated: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), manager) case events.SpaceMembershipExpired: users, err = ul.resolveShare(ul.impersonate(e.SpaceOwner), e.GranteeUserID, e.GranteeGroupID, e.SpaceID.GetOpaqueId()) // share related case events.ShareCreated: users, err = ul.resolveShare(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID, e.ItemID.GetSpaceId()) - case events.ShareUpdated: - users, err = ul.resolveShare(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID, e.ItemID.GetSpaceId()) + case events.ShareRemoved: + err = errors.New("no grantee in share removed event") case events.ShareExpired: users, err = ul.resolveShare(ul.impersonate(e.ShareOwner), e.GranteeUserID, e.GranteeGroupID, e.ItemID.GetSpaceId()) - case events.LinkCreated: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ItemID.GetOpaqueId(), editor) - case events.LinkUpdated: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ItemID.GetOpaqueId(), editor) - } if err != nil { @@ -423,6 +393,21 @@ func (ul *UserlogService) resolveShare(ctx context.Context, userid *user.UserId, return append(users, usr...), nil } +func (ul *UserlogService) getUser(ctx context.Context, userid *user.UserId) (*user.User, error) { + getUserResponse, err := ul.gwClient.GetUser(context.Background(), &user.GetUserRequest{ + UserId: userid, + }) + if err != nil { + return nil, err + } + + if getUserResponse.Status.Code != rpc.Code_CODE_OK { + return nil, fmt.Errorf("error getting user: %s", getUserResponse.Status.Message) + } + + return getUserResponse.GetUser(), nil +} + func listStorageSpaceRequest(spaceID string) *storageprovider.ListStorageSpacesRequest { return &storageprovider.ListStorageSpacesRequest{ Filters: []*storageprovider.ListStorageSpacesRequest_Filter{ diff --git a/services/userlog/pkg/service/templates/space-disabled-msg.tmpl b/services/userlog/pkg/service/templates/space-disabled-msg.tmpl new file mode 100644 index 000000000..0aa5c5e5f --- /dev/null +++ b/services/userlog/pkg/service/templates/space-disabled-msg.tmpl @@ -0,0 +1 @@ +{{ .username }} disabled Space {{ .spacename }} \ No newline at end of file diff --git a/services/userlog/pkg/service/templates/space-disabled-subj.tmpl b/services/userlog/pkg/service/templates/space-disabled-subj.tmpl new file mode 100644 index 000000000..fa07032fe --- /dev/null +++ b/services/userlog/pkg/service/templates/space-disabled-subj.tmpl @@ -0,0 +1 @@ +Space Disabled \ No newline at end of file