diff --git a/Dockerfile b/Dockerfile index 3777d7207ad..3f007f9c504 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ COPY ./ /ocis/ WORKDIR /ocis/ocis RUN make ci-node-generate -FROM owncloudci/golang:1.18 as build +FROM owncloudci/golang:1.19 as build COPY --from=generate /ocis /ocis diff --git a/changelog/unreleased/userlog-improvements.md b/changelog/unreleased/userlog-improvements.md new file mode 100644 index 00000000000..95e4e94bc54 --- /dev/null +++ b/changelog/unreleased/userlog-improvements.md @@ -0,0 +1,5 @@ +Enhancement: Userlog + +Enhane userlog service with proper api and messages + +https://github.com/owncloud/ocis/pull/5699 diff --git a/services/frontend/pkg/revaconfig/config.go b/services/frontend/pkg/revaconfig/config.go index 80570badc76..70da96c8e4b 100644 --- a/services/frontend/pkg/revaconfig/config.go +++ b/services/frontend/pkg/revaconfig/config.go @@ -267,6 +267,9 @@ func FrontendConfigFromStruct(cfg *config.Config) (map[string]interface{}, error "share_jail": cfg.EnableShareJail, "max_quota": cfg.MaxQuota, }, + "notifications": map[string]interface{}{ + "endpoints": []string{"list", "get", "delete"}, + }, }, "version": map[string]interface{}{ "product": "Infinite Scale", diff --git a/services/userlog/pkg/command/server.go b/services/userlog/pkg/command/server.go index 7ca044d297d..6f09a537159 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 00000000000..a3463508944 --- /dev/null +++ b/services/userlog/pkg/service/conversion.go @@ -0,0 +1,249 @@ +package service + +import ( + "bytes" + "errors" + "text/template" + "time" + + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/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" + "github.com/cs3org/reva/v2/pkg/utils" + ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" +) + +var ( + _resourceTypeSpace = "storagespace" + _resourceTypeShare = "share" +) + +// 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"` + SubjectRaw string `json:"subjectRich"` + Message string `json:"message"` + MessageRaw string `json:"messageRich"` + MessageDetails map[string]interface{} `json:"messageRichParameters"` +} + +// ConvertEvent converts an eventhistory event to an OC10Notification +func (ul *UserlogService) ConvertEvent(event *ehmsg.Event) (OC10Notification, error) { + etype, ok := ul.registeredEvents[event.Type] + if !ok { + // this should not happen + return OC10Notification{}, errors.New("eventtype not registered") + } + + einterface, err := etype.Unmarshal(event.Event) + if err != nil { + // this shouldn't happen either + return OC10Notification{}, errors.New("cant unmarshal event") + } + + switch ev := einterface.(type) { + default: + return OC10Notification{}, errors.New("unknown event type") + // space related + case events.SpaceDisabled: + return ul.spaceMessage(event.Id, SpaceDisabled, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) + case events.SpaceDeleted: + return ul.spaceDeletedMessage(event.Id, ev.Executant, ev.ID.GetOpaqueId(), ev.SpaceName, ev.Timestamp) + case events.SpaceShared: + return ul.spaceMessage(event.Id, SpaceShared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) + case events.SpaceUnshared: + return ul.spaceMessage(event.Id, SpaceUnshared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) + case events.SpaceMembershipExpired: + return ul.spaceMessage(event.Id, SpaceMembershipExpired, ev.SpaceOwner, ev.SpaceID.GetOpaqueId(), ev.ExpiredAt) + + // share related + case events.ShareCreated: + return ul.shareMessage(event.Id, ShareCreated, ev.Executant, ev.ItemID, ev.ShareID, utils.TSToTime(ev.CTime)) + case events.ShareExpired: + return ul.shareMessage(event.Id, ShareExpired, ev.ShareOwner, ev.ItemID, ev.ShareID, ev.ExpiredAt) + case events.ShareRemoved: + return ul.shareMessage(event.Id, ShareRemoved, ev.Executant, ev.ItemID, ev.ShareID, ev.Timestamp) + } +} + +func (ul *UserlogService) spaceDeletedMessage(eventid string, executant *user.UserId, spaceid string, spacename string, ts time.Time) (OC10Notification, error) { + _, user, err := utils.Impersonate(executant, ul.gwClient, ul.cfg.MachineAuthAPIKey) + if err != nil { + return OC10Notification{}, err + } + + subj, subjraw, msg, msgraw, err := ul.composeMessage(SpaceDeleted, map[string]string{ + "username": user.GetDisplayName(), + "spacename": spacename, + }) + if err != nil { + return OC10Notification{}, err + } + + details := ul.getDetails(user, nil, nil, nil) + details["space"] = map[string]string{ + "id": spaceid, + "name": spacename, + } + + return OC10Notification{ + EventID: eventid, + Service: ul.cfg.Service.Name, + UserName: user.GetUsername(), + Timestamp: ts.Format(time.RFC3339Nano), + ResourceID: spaceid, + ResourceType: _resourceTypeSpace, + Subject: subj, + SubjectRaw: subjraw, + Message: msg, + MessageRaw: msgraw, + MessageDetails: details, + }, nil +} + +func (ul *UserlogService) spaceMessage(eventid string, eventname string, executant *user.UserId, spaceid string, ts time.Time) (OC10Notification, error) { + ctx, user, err := utils.Impersonate(executant, ul.gwClient, ul.cfg.MachineAuthAPIKey) + if err != nil { + return OC10Notification{}, err + } + + space, err := ul.getSpace(ctx, spaceid) + if err != nil { + return OC10Notification{}, err + } + + subj, subjraw, msg, msgraw, err := ul.composeMessage(eventname, map[string]string{ + "username": user.GetDisplayName(), + "spacename": space.GetName(), + }) + if err != nil { + return OC10Notification{}, err + } + + return OC10Notification{ + EventID: eventid, + Service: ul.cfg.Service.Name, + UserName: user.GetUsername(), + Timestamp: ts.Format(time.RFC3339Nano), + ResourceID: spaceid, + ResourceType: _resourceTypeSpace, + Subject: subj, + SubjectRaw: subjraw, + Message: msg, + MessageRaw: msgraw, + MessageDetails: ul.getDetails(user, space, nil, nil), + }, nil +} + +func (ul *UserlogService) shareMessage(eventid string, eventname string, executant *user.UserId, resourceid *storageprovider.ResourceId, shareid *collaboration.ShareId, ts time.Time) (OC10Notification, error) { + ctx, user, err := utils.Impersonate(executant, ul.gwClient, ul.cfg.MachineAuthAPIKey) + if err != nil { + return OC10Notification{}, err + } + + info, err := ul.getResource(ctx, resourceid) + if err != nil { + return OC10Notification{}, err + } + + subj, subjraw, msg, msgraw, err := ul.composeMessage(eventname, map[string]string{ + "username": user.GetDisplayName(), + "resourcename": info.GetName(), + }) + if err != nil { + return OC10Notification{}, err + } + + return OC10Notification{ + EventID: eventid, + Service: ul.cfg.Service.Name, + UserName: user.GetUsername(), + Timestamp: ts.Format(time.RFC3339Nano), + ResourceID: storagespace.FormatResourceID(*info.GetId()), + ResourceType: _resourceTypeShare, + Subject: subj, + SubjectRaw: subjraw, + Message: msg, + MessageRaw: msgraw, + MessageDetails: ul.getDetails(user, nil, info, shareid), + }, nil +} + +func (ul *UserlogService) composeMessage(eventname string, vars map[string]string) (string, string, string, string, error) { + tpl, ok := _templates[eventname] + if !ok { + return "", "", "", "", errors.New("unknown template name") + } + + subject := ul.executeTemplate(tpl.Subject, vars) + + subjectraw := ul.executeTemplate(tpl.Subject, map[string]string{ + "username": "{user}", + "spacename": "{space}", + "resourcename": "{resource}", + }) + + message := ul.executeTemplate(tpl.Message, vars) + + messageraw := ul.executeTemplate(tpl.Message, map[string]string{ + "username": "{user}", + "spacename": "{space}", + "resourcename": "{resource}", + }) + + return subject, subjectraw, message, messageraw, nil + +} + +func (ul *UserlogService) getDetails(user *user.User, space *storageprovider.StorageSpace, item *storageprovider.ResourceInfo, shareid *collaboration.ShareId) map[string]interface{} { + details := make(map[string]interface{}) + + if user != nil { + details["user"] = map[string]string{ + "id": user.GetId().GetOpaqueId(), + "name": user.GetUsername(), + "displayname": user.GetDisplayName(), + } + } + + 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(), + } + } + + if shareid != nil { + details["share"] = map[string]string{ + "id": shareid.GetOpaqueId(), + } + } + + return details +} + +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 4ad2011ac12..725275e04d4 100644 --- a/services/userlog/pkg/service/http.go +++ b/services/userlog/pkg/service/http.go @@ -1,17 +1,10 @@ package service import ( - "context" "encoding/json" - "errors" - "fmt" "net/http" - "time" - storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" revactx "github.com/cs3org/reva/v2/pkg/ctx" - "github.com/cs3org/reva/v2/pkg/events" - ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ) // ServeHTTP fulfills Handler interface @@ -37,7 +30,7 @@ func (ul *UserlogService) HandleGetEvents(w http.ResponseWriter, r *http.Request resp := GetEventResponseOC10{} for _, e := range evs { - noti, err := ul.convertEvent(r.Context(), e) + noti, err := ul.ConvertEvent(e) if err != nil { ul.log.Error().Err(err).Str("eventid", e.Id).Str("eventtype", e.Type).Msg("failed to convert event") continue @@ -60,14 +53,14 @@ func (ul *UserlogService) HandleDeleteEvents(w http.ResponseWriter, r *http.Requ return } - var ids []string - if err := json.NewDecoder(r.Body).Decode(&ids); err != nil { + var req DeleteEventsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { ul.log.Error().Err(err).Int("returned statuscode", http.StatusBadRequest).Msg("request body is malformed") w.WriteHeader(http.StatusBadRequest) return } - if err := ul.DeleteEvents(u.GetId().GetOpaqueId(), ids); err != nil { + if err := ul.DeleteEvents(u.GetId().GetOpaqueId(), req.IDs); err != nil { ul.log.Error().Err(err).Int("returned statuscode", http.StatusInternalServerError).Msg("delete events failed") w.WriteHeader(http.StatusInternalServerError) return @@ -76,140 +69,6 @@ func (ul *UserlogService) HandleDeleteEvents(w http.ResponseWriter, r *http.Requ w.WriteHeader(http.StatusOK) } -func (ul *UserlogService) convertEvent(ctx context.Context, event *ehmsg.Event) (OC10Notification, error) { - etype, ok := ul.registeredEvents[event.Type] - if !ok { - // this should not happen - return OC10Notification{}, errors.New("eventtype not registered") - } - - einterface, err := etype.Unmarshal(event.Event) - if err != nil { - // this shouldn't happen either - return OC10Notification{}, errors.New("cant unmarshal event") - } - - noti := OC10Notification{ - EventID: event.Id, - Service: "userlog", - Timestamp: time.Now().Format(time.RFC3339Nano), - } - - // 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()) - - // 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) - 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) - } - - 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 { @@ -221,3 +80,8 @@ type GetEventResponseOC10 struct { Data []OC10Notification `json:"data"` } `json:"ocs"` } + +// DeleteEventsRequest is the expected body for the delete request +type DeleteEventsRequest struct { + IDs []string `json:"ids"` +} diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index 838f75dc9cf..3a8e8559974 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -25,7 +25,6 @@ import ( // UserlogService is the service responsible for user activities type UserlogService struct { log log.Logger - ch <-chan events.Event m *chi.Mux store store.Store cfg *config.Config @@ -52,7 +51,6 @@ func NewUserlogService(opts ...Option) (*UserlogService, error) { ul := &UserlogService{ log: o.Logger, - ch: ch, m: o.Mux, store: o.Store, cfg: o.Config, @@ -71,80 +69,50 @@ func NewUserlogService(opts ...Option) (*UserlogService, error) { r.Delete("/*", ul.HandleDeleteEvents) }) - go ul.MemorizeEvents() + go ul.MemorizeEvents(ch) return ul, nil } // MemorizeEvents stores eventIDs a user wants to receive -func (ul *UserlogService) MemorizeEvents() { - for event := range ul.ch { +func (ul *UserlogService) MemorizeEvents(ch <-chan events.Event) { + for event := range ch { // for each event we need to: // I) find users eligible to receive the event var ( users []string err error ) + 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) + for u, _ := range e.FinalMembers { + users = append(users, u) + } case events.SpaceShared: - users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), manager) + users, err = ul.resolveID(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID) 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) + users, err = ul.resolveID(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID) case events.SpaceMembershipExpired: - users, err = ul.resolveShare(ul.impersonate(e.SpaceOwner), e.GranteeUserID, e.GranteeGroupID, e.SpaceID.GetOpaqueId()) + users, err = ul.resolveID(ul.impersonate(e.SpaceOwner), e.GranteeUserID, e.GranteeGroupID) // 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()) + users, err = ul.resolveID(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID) + case events.ShareRemoved: + users, err = ul.resolveID(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID) 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) - + users, err = ul.resolveID(ul.impersonate(e.ShareOwner), e.GranteeUserID, e.GranteeGroupID) } if err != nil { - ul.log.Error().Err(err).Interface("event", event).Msg("error gathering members for event") + // TODO: Find out why this errors on ci pipeline + ul.log.Debug().Err(err).Interface("event", event).Msg("error gathering members for event") continue } @@ -218,20 +186,6 @@ func (ul *UserlogService) DeleteEvents(userid string, evids []string) error { }) } -func (ul *UserlogService) impersonate(u *user.UserId) context.Context { - if u == nil { - ul.log.Debug().Msg("cannot impersonate nil user") - return nil - } - - ctx, _, err := utils.Impersonate(u, ul.gwClient, ul.cfg.MachineAuthAPIKey) - if err != nil { - ul.log.Error().Err(err).Str("userid", u.GetOpaqueId()).Msg("failed to impersonate user") - return nil - } - return ctx -} - func (ul *UserlogService) addEventsToUser(userid string, eventids ...string) error { return ul.alterUserEventList(userid, func(ids []string) []string { return append(ids, eventids...) @@ -318,23 +272,6 @@ func (ul *UserlogService) findSpaceMembers(ctx context.Context, spaceID string, return users, nil } -func (ul *UserlogService) getSpace(ctx context.Context, spaceID string) (*storageprovider.StorageSpace, error) { - res, err := ul.gwClient.ListStorageSpaces(ctx, listStorageSpaceRequest(spaceID)) - if err != nil { - return nil, err - } - - if res.GetStatus().GetCode() != rpc.Code_CODE_OK { - return nil, fmt.Errorf("Unexpected status code while getting space: %v", res.GetStatus().GetCode()) - } - - if len(res.StorageSpaces) == 0 { - return nil, fmt.Errorf("error getting storage space %s: no space returned", spaceID) - } - - return res.StorageSpaces[0], nil -} - func (ul *UserlogService) gatherSpaceMembers(ctx context.Context, space *storageprovider.StorageSpace, hasRequiredRole permissionChecker) ([]string, error) { var permissionsMap map[string]*storageprovider.ResourcePermissions if err := utils.ReadJSONFromOpaque(space.GetOpaque(), "grants", &permissionsMap); err != nil { @@ -378,49 +315,99 @@ func (ul *UserlogService) gatherSpaceMembers(ctx context.Context, space *storage return users, nil } +func (ul *UserlogService) resolveID(ctx context.Context, userid *user.UserId, groupid *group.GroupId) ([]string, error) { + if userid != nil { + return []string{userid.GetOpaqueId()}, nil + } + + return ul.resolveGroup(ctx, groupid.GetOpaqueId()) +} + // resolves the users of a group func (ul *UserlogService) resolveGroup(ctx context.Context, groupID string) ([]string, error) { - if ctx == nil { - return nil, errors.New("need authenticated context to resolve groups") + grp, err := ul.getGroup(ctx, groupID) + if err != nil { + return nil, err } - r, err := ul.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: &group.GroupId{OpaqueId: groupID}}) + var userIDs []string + for _, m := range grp.GetMembers() { + userIDs = append(userIDs, m.GetOpaqueId()) + } + + return userIDs, nil +} + +func (ul *UserlogService) impersonate(u *user.UserId) context.Context { + if u == nil { + ul.log.Debug().Msg("cannot impersonate nil user") + return nil + } + + ctx, _, err := utils.Impersonate(u, ul.gwClient, ul.cfg.MachineAuthAPIKey) + if err != nil { + ul.log.Error().Err(err).Str("userid", u.GetOpaqueId()).Msg("failed to impersonate user") + return nil + } + return ctx +} + +func (ul *UserlogService) getSpace(ctx context.Context, spaceID string) (*storageprovider.StorageSpace, error) { + res, err := ul.gwClient.ListStorageSpaces(ctx, listStorageSpaceRequest(spaceID)) if err != nil { return nil, err } - if r.GetStatus().GetCode() != rpc.Code_CODE_OK { - return nil, fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode()) + if res.GetStatus().GetCode() != rpc.Code_CODE_OK { + return nil, fmt.Errorf("Unexpected status code while getting space: %v", res.GetStatus().GetCode()) } - var userIDs []string - for _, m := range r.GetGroup().GetMembers() { - userIDs = append(userIDs, m.GetOpaqueId()) + if len(res.StorageSpaces) == 0 { + return nil, fmt.Errorf("error getting storage space %s: no space returned", spaceID) } - return userIDs, nil + return res.StorageSpaces[0], nil } -func (ul *UserlogService) resolveID(ctx context.Context, userid *user.UserId, groupid *group.GroupId) ([]string, error) { - if userid != nil { - return []string{userid.GetOpaqueId()}, 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 } - return ul.resolveGroup(ctx, groupid.GetOpaqueId()) + if getUserResponse.Status.Code != rpc.Code_CODE_OK { + return nil, fmt.Errorf("error getting user: %s", getUserResponse.Status.Message) + } + + return getUserResponse.GetUser(), nil } -func (ul *UserlogService) resolveShare(ctx context.Context, userid *user.UserId, groupid *group.GroupId, spaceid string) ([]string, error) { - users, err := ul.resolveID(ctx, userid, groupid) +func (ul *UserlogService) getGroup(ctx context.Context, groupid string) (*group.Group, error) { + r, err := ul.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: &group.GroupId{OpaqueId: groupid}}) if err != nil { return nil, err } - usr, err := ul.findSpaceMembers(ctx, spaceid, editor) + if r.GetStatus().GetCode() != rpc.Code_CODE_OK { + return nil, fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode()) + } + + return r.GetGroup(), nil +} + +func (ul *UserlogService) getResource(ctx context.Context, resourceid *storageprovider.ResourceId) (*storageprovider.ResourceInfo, error) { + res, err := ul.gwClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: resourceid}}) if err != nil { return nil, err } - return append(users, usr...), nil + if res.GetStatus().GetCode() != rpc.Code_CODE_OK { + return nil, fmt.Errorf("Unexpected status code while getting space: %v", res.GetStatus().GetCode()) + } + + return res.GetInfo(), nil } func listStorageSpaceRequest(spaceID string) *storageprovider.ListStorageSpacesRequest { diff --git a/services/userlog/pkg/service/service_test.go b/services/userlog/pkg/service/service_test.go index 724d4c7d92a..89ce6871fa2 100644 --- a/services/userlog/pkg/service/service_test.go +++ b/services/userlog/pkg/service/service_test.go @@ -64,7 +64,7 @@ var _ = Describe("UserlogService", func() { service.GatewayClient(&gwc), service.HistoryClient(&ehc), service.RegisteredEvents([]events.Unmarshaller{ - events.UploadReady{}, + events.SpaceDisabled{}, }), ) Expect(err).ToNot(HaveOccurred()) @@ -73,9 +73,11 @@ var _ = Describe("UserlogService", func() { It("it stores, returns and deletes a couple of events", func() { ids := make(map[string]struct{}) - ids[bus.Publish(events.SpaceCreated{Executant: &user.UserId{OpaqueId: "userid"}})] = struct{}{} - ids[bus.Publish(events.UploadReady{SpaceOwner: &user.UserId{OpaqueId: "userid"}})] = struct{}{} - ids[bus.Publish(events.ContainerCreated{SpaceOwner: &user.UserId{OpaqueId: "userid"}})] = struct{}{} + ids[bus.Publish(events.SpaceDisabled{Executant: &user.UserId{OpaqueId: "userid"}})] = struct{}{} + ids[bus.Publish(events.SpaceDisabled{Executant: &user.UserId{OpaqueId: "userid"}})] = struct{}{} + ids[bus.Publish(events.SpaceDisabled{Executant: &user.UserId{OpaqueId: "userid"}})] = struct{}{} + // ids[bus.Publish(events.SpaceMembershipExpired{SpaceOwner: &user.UserId{OpaqueId: "userid"}})] = struct{}{} + // ids[bus.Publish(events.ShareCreated{Executant: &user.UserId{OpaqueId: "userid"}})] = struct{}{} time.Sleep(500 * time.Millisecond) diff --git a/services/userlog/pkg/service/templates.go b/services/userlog/pkg/service/templates.go new file mode 100644 index 00000000000..bc9bf33d2b7 --- /dev/null +++ b/services/userlog/pkg/service/templates.go @@ -0,0 +1,65 @@ +package service + +import "text/template" + +// the available templates +var ( + SpaceShared = "space-shared" + SpaceSharedSubject = "Space shared" + SpaceSharedMessage = "{{ .username }} added you to Space {{ .spacename }}" + + SpaceUnshared = "space-unshared" + SpaceUnsharedSubject = "Removed from Space" + SpaceUnsharedMessage = "{{ .username }} removed you from Space {{ .spacename }}" + + SpaceDisabled = "space-disabled" + SpaceDisabledSubject = "Space disabled" + SpaceDisabledMessage = "{{ .username }} disabled Space {{ .spacename }}" + + SpaceDeleted = "space-deleted" + SpaceDeletedSubject = "Space deleted" + SpaceDeletedMessage = "{{ .username }} deleted Space {{ .spacename }}" + + SpaceMembershipExpired = "space-membership-expired" + SpaceMembershipExpiredSubject = "Membership expired" + SpaceMembershipExpiredMessage = "Access to Space {{ .spacename }} lost" + + ShareCreated = "item-shared" + ShareCreatedSubject = "Resource shared" + ShareCreatedMessage = "{{ .username }} shared {{ .resourcename }} with you" + + ShareRemoved = "item-unshared" + ShareRemovedSubject = "Resource unshared" + ShareRemovedMessage = "{{ .username }} unshared {{ .resourcename }} with you" + + ShareExpired = "share-expired" + ShareExpiredSubject = "Share expired" + ShareExpiredMessage = "Access to {{ .resourcename }} expired" +) + +// rendered templates +var ( + _templates = map[string]NotificationTemplate{ + SpaceShared: notiTmpl(SpaceSharedSubject, SpaceSharedMessage), + SpaceUnshared: notiTmpl(SpaceUnsharedSubject, SpaceUnsharedMessage), + SpaceDisabled: notiTmpl(SpaceDisabledSubject, SpaceDisabledMessage), + SpaceDeleted: notiTmpl(SpaceDeletedSubject, SpaceDeletedMessage), + SpaceMembershipExpired: notiTmpl(SpaceMembershipExpiredSubject, SpaceMembershipExpiredMessage), + ShareCreated: notiTmpl(ShareCreatedSubject, ShareCreatedMessage), + ShareRemoved: notiTmpl(ShareRemovedSubject, ShareRemovedMessage), + ShareExpired: notiTmpl(ShareExpiredSubject, ShareExpiredMessage), + } +) + +// NotificationTemplate is the data structure for the notifications +type NotificationTemplate struct { + Subject *template.Template + Message *template.Template +} + +func notiTmpl(subjectname string, messagename string) NotificationTemplate { + return NotificationTemplate{ + Subject: template.Must(template.New("").Parse(subjectname)), + Message: template.Must(template.New("").Parse(messagename)), + } +}