diff --git a/services/userlog/pkg/service/conversion.go b/services/userlog/pkg/service/conversion.go index e10191f90d..4cef4f0c2f 100644 --- a/services/userlog/pkg/service/conversion.go +++ b/services/userlog/pkg/service/conversion.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "embed" + "encoding/json" "fmt" "io/fs" "strings" @@ -28,6 +29,7 @@ var ( _resourceTypeResource = "resource" _resourceTypeSpace = "storagespace" _resourceTypeShare = "share" + _resourceTypeGlobal = "global" _domain = "userlog" ) @@ -116,6 +118,22 @@ func (c *Converter) ConvertEvent(eventid string, event interface{}) (OC10Notific } } +// ConvertGlobalEvent converts a global event to an OC10Notification +func (c *Converter) ConvertGlobalEvent(typ string, data json.RawMessage) (OC10Notification, error) { + switch typ { + default: + return OC10Notification{}, fmt.Errorf("unknown global event type: %s", typ) + case "deprovision": + var dd DeprovisionData + if err := json.Unmarshal(data, &dd); err != nil { + return OC10Notification{}, err + } + + return c.deprovisionMessage(PlatformDeprovision, dd.DeprovisionDate) + } + +} + func (c *Converter) spaceDeletedMessage(eventid string, executant *user.UserId, spaceid string, spacename string, ts time.Time) (OC10Notification, error) { usr, err := c.getUser(context.Background(), executant) if err != nil { @@ -287,6 +305,28 @@ func (c *Converter) policiesMessage(eventid string, nt NotificationTemplate, exe }, nil } +func (c *Converter) deprovisionMessage(nt NotificationTemplate, deproDate string) (OC10Notification, error) { + subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{ + "date": deproDate, + }) + if err != nil { + return OC10Notification{}, err + } + + return OC10Notification{ + EventID: "deprovision", + Service: c.serviceName, + // UserName: executant.GetUsername(), // TODO: do we need the deprovisioner? + Timestamp: time.Now().Format(time.RFC3339Nano), // Fake timestamp? Or we store one with the event? + ResourceType: _resourceTypeResource, + Subject: subj, + SubjectRaw: subjraw, + Message: msg, + MessageRaw: msgraw, + MessageDetails: map[string]interface{}{}, + }, nil +} + func (c *Converter) authenticate(usr *user.User) (context.Context, error) { if ctx, ok := c.contexts[usr.GetId().GetOpaqueId()]; ok { return ctx, nil diff --git a/services/userlog/pkg/service/http.go b/services/userlog/pkg/service/http.go index b1a9e82588..e23f15662a 100644 --- a/services/userlog/pkg/service/http.go +++ b/services/userlog/pkg/service/http.go @@ -57,6 +57,23 @@ func (ul *UserlogService) HandleGetEvents(w http.ResponseWriter, r *http.Request resp.OCS.Data = append(resp.OCS.Data, noti) } + glevs, err := ul.GetGlobalEvents() + if err != nil { + ul.log.Error().Err(err).Int("returned statuscode", http.StatusInternalServerError).Msg("get global events failed") + w.WriteHeader(http.StatusInternalServerError) + return + } + + for t, data := range glevs { + noti, err := conv.ConvertGlobalEvent(t, data) + if err != nil { + ul.log.Error().Err(err).Str("eventtype", t).Msg("failed to convert event") + continue + } + + resp.OCS.Data = append(resp.OCS.Data, noti) + } + resp.OCS.Meta.StatusCode = http.StatusOK b, _ := json.Marshal(resp) w.Write(b) @@ -89,6 +106,40 @@ func (ul *UserlogService) HandleSSE(w http.ResponseWriter, r *http.Request) { ul.sse.ServeHTTP(w, r) } +// HandlePostEvent is the POST handler for events +func (ul *UserlogService) HandlePostEvent(w http.ResponseWriter, r *http.Request) { + u, ok := ctx.ContextGetUser(r.Context()) + if !ok { + ul.log.Error().Msg("post: no user in context") + w.WriteHeader(http.StatusInternalServerError) + return + } + + uid := u.GetId().GetOpaqueId() + if uid == "" { + ul.log.Error().Msg("post: user in context is broken") + w.WriteHeader(http.StatusInternalServerError) + return + } + + // TODO: Check user is allowed to do this + + var req PostEventsRequest + 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.StoreGlobalEvent(req.Type, req.Data); err != nil { + ul.log.Error().Err(err).Msg("post: error storing global event") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + // HandleDeleteEvents is the DELETE handler for events func (ul *UserlogService) HandleDeleteEvents(w http.ResponseWriter, r *http.Request) { u, ok := revactx.ContextGetUser(r.Context()) @@ -130,3 +181,14 @@ type GetEventResponseOC10 struct { type DeleteEventsRequest struct { IDs []string `json:"ids"` } + +// PostEventsRequest is the expected body for the post request +type PostEventsRequest struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` +} + +// DeprovisionData is the expected `data` for the PostEventsRequest when deprovisioning +type DeprovisionData struct { + DeprovisionDate string `json:"date"` +} diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index 1c105bccd0..cc102b4d47 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -82,6 +82,7 @@ func NewUserlogService(opts ...Option) (*UserlogService, error) { ul.m.Route("/ocs/v2.php/apps/notifications/api/v1/notifications", func(r chi.Router) { r.Get("/", ul.HandleGetEvents) + r.Post("/", ul.HandlePostEvent) r.Delete("/", ul.HandleDeleteEvents) if !ul.cfg.DisableSSE { @@ -235,6 +236,42 @@ func (ul *UserlogService) DeleteEvents(userid string, evids []string) error { }) } +// StoreGlobalEvent will store a global event that will be returned with each `GetEvents` request +func (ul *UserlogService) StoreGlobalEvent(typ string, data json.RawMessage) error { + switch typ { + default: + return fmt.Errorf("unknown event type: %s", typ) + case "deprovision": + var req DeprovisionData + if err := json.Unmarshal(data, &req); err != nil { + return err + } + + // TODO: check for proper time format + + return ul.storeGlobalEvent(typ, req) + + } +} + +// GetGlobalEvents will return all global events +func (ul *UserlogService) GetGlobalEvents() (map[string]json.RawMessage, error) { + out := make(map[string]json.RawMessage) + + recs, err := ul.store.Read("global-events") + if err != nil && err != store.ErrNotFound { + return out, err + } + + if len(recs) > 0 { + if err := json.Unmarshal(recs[0].Value, &out); err != nil { + return out, err + } + } + + return out, nil +} + func (ul *UserlogService) addEventToUser(userid string, event events.Event) error { if !ul.cfg.DisableSSE { if err := ul.sendSSE(userid, event); err != nil { @@ -312,6 +349,30 @@ func (ul *UserlogService) alterUserEventList(userid string, alter func([]string) }) } +func (ul *UserlogService) storeGlobalEvent(typ string, ev interface{}) error { + b, err := json.Marshal(ev) + if err != nil { + return err + } + + evs, err := ul.GetGlobalEvents() + if err != nil && err != store.ErrNotFound { + return err + } + + evs[typ] = b + + val, err := json.Marshal(evs) + if err != nil { + return err + } + + return ul.store.Write(&store.Record{ + Key: "global-events", + Value: val, + }) +} + // we need the spaceid to inform other space members // we need an owner to query space members // we need to check the user has the required role to see the event diff --git a/services/userlog/pkg/service/templates.go b/services/userlog/pkg/service/templates.go index a6616d02e5..28c75a15ef 100644 --- a/services/userlog/pkg/service/templates.go +++ b/services/userlog/pkg/service/templates.go @@ -54,6 +54,11 @@ var ( Subject: Template("Share expired"), Message: Template("Access to {resource} expired"), } + + PlatformDeprovision = NotificationTemplate{ + Subject: Template("Platform will be deprovisioned"), + Message: Template("Attention! The platform will be deprovisioned at {date}"), + } ) // holds the information to turn the raw template into a parseable go template @@ -62,6 +67,7 @@ var _placeholders = map[string]string{ "{space}": "{{ .spacename }}", "{resource}": "{{ .resourcename }}", "{virus}": "{{ .virusdescription }}", + "{date}": "{{ .date }}", } // NotificationTemplate is the data structure for the notifications