From bca4d4f9fd14a3ed222a466014c92e56806c6793 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Thu, 29 Jun 2023 15:33:17 +0200 Subject: [PATCH 1/5] add post event handler Signed-off-by: jkoberg --- services/userlog/pkg/service/conversion.go | 40 ++++++++++++++ services/userlog/pkg/service/http.go | 62 ++++++++++++++++++++++ services/userlog/pkg/service/service.go | 61 +++++++++++++++++++++ services/userlog/pkg/service/templates.go | 6 +++ 4 files changed, 169 insertions(+) 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 From 6ab2f1038610ab670dd3a14504b0c462ab553817 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Fri, 30 Jun 2023 11:48:57 +0200 Subject: [PATCH 2/5] only allow admins to set global notifications Signed-off-by: jkoberg --- services/userlog/pkg/command/server.go | 2 + services/userlog/pkg/server/http/option.go | 8 ++++ services/userlog/pkg/server/http/server.go | 1 + services/userlog/pkg/service/http.go | 47 +++++++++++++++++++++- services/userlog/pkg/service/options.go | 9 +++++ services/userlog/pkg/service/service.go | 9 ++++- 6 files changed, 73 insertions(+), 3 deletions(-) diff --git a/services/userlog/pkg/command/server.go b/services/userlog/pkg/command/server.go index 7417c1709b..3698714291 100644 --- a/services/userlog/pkg/command/server.go +++ b/services/userlog/pkg/command/server.go @@ -104,6 +104,7 @@ func Server(cfg *config.Config) *cli.Command { hClient := ehsvc.NewEventHistoryService("com.owncloud.api.eventhistory", ogrpc.DefaultClient()) vClient := settingssvc.NewValueService("com.owncloud.api.settings", ogrpc.DefaultClient()) + rClient := settingssvc.NewRoleService("com.owncloud.api.settings", ogrpc.DefaultClient()) { server, err := http.Server( @@ -116,6 +117,7 @@ func Server(cfg *config.Config) *cli.Command { http.GatewaySelector(gatewaySelector), http.History(hClient), http.Value(vClient), + http.Role(rClient), http.RegisteredEvents(_registeredEvents), ) diff --git a/services/userlog/pkg/server/http/option.go b/services/userlog/pkg/server/http/option.go index efe691101c..6b55f5ba6b 100644 --- a/services/userlog/pkg/server/http/option.go +++ b/services/userlog/pkg/server/http/option.go @@ -31,6 +31,7 @@ type Options struct { GatewaySelector pool.Selectable[gateway.GatewayAPIClient] HistoryClient ehsvc.EventHistoryService ValueClient settingssvc.ValueService + RoleClient settingssvc.RoleService RegisteredEvents []events.Unmarshaller } @@ -128,3 +129,10 @@ func Value(vs settingssvc.ValueService) Option { o.ValueClient = vs } } + +// Roles provides a function to configure the roles service client +func Role(rs settingssvc.RoleService) Option { + return func(o *Options) { + o.RoleClient = rs + } +} diff --git a/services/userlog/pkg/server/http/server.go b/services/userlog/pkg/server/http/server.go index 705529ed24..4fd8527d83 100644 --- a/services/userlog/pkg/server/http/server.go +++ b/services/userlog/pkg/server/http/server.go @@ -77,6 +77,7 @@ func Server(opts ...Option) (http.Service, error) { svc.HistoryClient(options.HistoryClient), svc.GatewaySelector(options.GatewaySelector), svc.ValueClient(options.ValueClient), + svc.RoleClient(options.RoleClient), svc.RegisteredEvents(options.RegisteredEvents), ) if err != nil { diff --git a/services/userlog/pkg/service/http.go b/services/userlog/pkg/service/http.go index e23f15662a..277dc84d7b 100644 --- a/services/userlog/pkg/service/http.go +++ b/services/userlog/pkg/service/http.go @@ -6,6 +6,10 @@ import ( "github.com/cs3org/reva/v2/pkg/ctx" revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/ocis-pkg/roles" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" + settings "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0" ) // HeaderAcceptLanguage is the header where the client can set the locale @@ -122,8 +126,6 @@ func (ul *UserlogService) HandlePostEvent(w http.ResponseWriter, r *http.Request 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") @@ -192,3 +194,44 @@ type PostEventsRequest struct { type DeprovisionData struct { DeprovisionDate string `json:"date"` } + +// RequireAdmin middleware is used to require the user in context to be an admin / have account management permissions +func RequireAdmin(rm *roles.Manager, logger log.Logger) func(next http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + u, ok := revactx.ContextGetUser(r.Context()) + if !ok { + errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized") + return + } + if u.Id == nil || u.Id.OpaqueId == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "user is missing an id") + return + } + // get roles from context + roleIDs, ok := roles.ReadRoleIDsFromContext(r.Context()) + if !ok { + logger.Debug().Str("userid", u.Id.OpaqueId).Msg("No roles in context, contacting settings service") + var err error + roleIDs, err = rm.FindRoleIDsForUser(r.Context(), u.Id.OpaqueId) + if err != nil { + logger.Err(err).Str("userid", u.Id.OpaqueId).Msg("failed to get roles for user") + errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized") + return + } + if len(roleIDs) == 0 { + errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized") + return + } + } + + // check if permission is present in roles of the authenticated account + if rm.FindPermissionByID(r.Context(), roleIDs, settings.AccountManagementPermissionID) != nil { + next.ServeHTTP(w, r) + return + } + + errorcode.AccessDenied.Render(w, r, http.StatusForbidden, "Forbidden") + } + } +} diff --git a/services/userlog/pkg/service/options.go b/services/userlog/pkg/service/options.go index 98628cfd6f..12e454ec27 100644 --- a/services/userlog/pkg/service/options.go +++ b/services/userlog/pkg/service/options.go @@ -25,6 +25,7 @@ type Options struct { HistoryClient ehsvc.EventHistoryService GatewaySelector pool.Selectable[gateway.GatewayAPIClient] ValueClient settingssvc.ValueService + RoleClient settingssvc.RoleService RegisteredEvents []events.Unmarshaller } @@ -84,8 +85,16 @@ func RegisteredEvents(e []events.Unmarshaller) Option { } } +// ValueClient adds a grpc client for the value service func ValueClient(vs settingssvc.ValueService) Option { return func(o *Options) { o.ValueClient = vs } } + +// RoleClient adds a grpc client for the role service +func RoleClient(rs settingssvc.RoleService) Option { + return func(o *Options) { + o.RoleClient = rs + } +} diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index cc102b4d47..a91727d445 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -19,6 +19,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/middleware" + "github.com/owncloud/ocis/v2/ocis-pkg/roles" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" @@ -80,9 +81,15 @@ func NewUserlogService(opts ...Option) (*UserlogService, error) { ul.registeredEvents[typ.String()] = e } + m := roles.NewManager( + // TODO: caching? + roles.Logger(o.Logger), + roles.RoleService(o.RoleClient), + ) + ul.m.Route("/ocs/v2.php/apps/notifications/api/v1/notifications", func(r chi.Router) { r.Get("/", ul.HandleGetEvents) - r.Post("/", ul.HandlePostEvent) + r.Post("/", RequireAdmin(&m, ul.log)(ul.HandlePostEvent)) r.Delete("/", ul.HandleDeleteEvents) if !ul.cfg.DisableSSE { From d8667bf7363b5bd63e7a04a3c98322d80a0d7cc1 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Fri, 30 Jun 2023 12:40:02 +0200 Subject: [PATCH 3/5] use hard typed deprovision date Signed-off-by: jkoberg --- services/userlog/pkg/service/conversion.go | 2 +- services/userlog/pkg/service/globalevents.go | 15 +++++++++++ services/userlog/pkg/service/http.go | 11 +++------ services/userlog/pkg/service/service.go | 26 ++++++++++++++------ 4 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 services/userlog/pkg/service/globalevents.go diff --git a/services/userlog/pkg/service/conversion.go b/services/userlog/pkg/service/conversion.go index 4cef4f0c2f..266e65e0b1 100644 --- a/services/userlog/pkg/service/conversion.go +++ b/services/userlog/pkg/service/conversion.go @@ -305,7 +305,7 @@ func (c *Converter) policiesMessage(eventid string, nt NotificationTemplate, exe }, nil } -func (c *Converter) deprovisionMessage(nt NotificationTemplate, deproDate string) (OC10Notification, error) { +func (c *Converter) deprovisionMessage(nt NotificationTemplate, deproDate time.Time) (OC10Notification, error) { subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{ "date": deproDate, }) diff --git a/services/userlog/pkg/service/globalevents.go b/services/userlog/pkg/service/globalevents.go new file mode 100644 index 0000000000..2f7369d1b4 --- /dev/null +++ b/services/userlog/pkg/service/globalevents.go @@ -0,0 +1,15 @@ +package service + +import "time" + +var ( + _globalEventsKey = "global-events" +) + +// DeprovisionData is the data needed for the deprovision global event +type DeprovisionData struct { + // The deprovision date + DeprovisionDate time.Time `json:"deprovision_date"` + // The user who stored the deprovision message + Deprovisioner string +} diff --git a/services/userlog/pkg/service/http.go b/services/userlog/pkg/service/http.go index 277dc84d7b..2cc4e6cb60 100644 --- a/services/userlog/pkg/service/http.go +++ b/services/userlog/pkg/service/http.go @@ -186,13 +186,10 @@ type DeleteEventsRequest struct { // 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"` + // the event type, e.g. "deprovision" + Type string `json:"type"` + // arbitray data for the event + Data map[string]string `json:"data"` } // RequireAdmin middleware is used to require the user in context to be an admin / have account management permissions diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index a91727d445..2ab8396371 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "reflect" + "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" @@ -244,19 +245,30 @@ 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 { +func (ul *UserlogService) StoreGlobalEvent(typ string, data map[string]string) 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 + dps, ok := data["deprovision_date"] + if !ok { + return errors.New("need 'deprovision_date' in request body") } - // TODO: check for proper time format + format := data["deprovision_date_format"] + if format == "" { + format = time.RFC3339 + } - return ul.storeGlobalEvent(typ, req) + date, err := time.Parse(format, dps) + if err != nil { + fmt.Println("", format, "\n", dps) + return fmt.Errorf("cannot parse time to format. time: '%s' format: '%s'", dps, format) + } + + return ul.storeGlobalEvent(typ, DeprovisionData{ + DeprovisionDate: date, + }) } } @@ -265,7 +277,7 @@ func (ul *UserlogService) StoreGlobalEvent(typ string, data json.RawMessage) err func (ul *UserlogService) GetGlobalEvents() (map[string]json.RawMessage, error) { out := make(map[string]json.RawMessage) - recs, err := ul.store.Read("global-events") + recs, err := ul.store.Read(_globalEventsKey) if err != nil && err != store.ErrNotFound { return out, err } From a83492d7ab2b37f01a482ad9152f7fe3f01d1780 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Tue, 4 Jul 2023 10:18:36 +0200 Subject: [PATCH 4/5] allow deleting global evs Signed-off-by: jkoberg --- services/userlog/pkg/service/conversion.go | 4 +- services/userlog/pkg/service/globalevents.go | 2 + services/userlog/pkg/service/http.go | 52 ++++++++++---------- services/userlog/pkg/service/service.go | 41 ++++++++++----- 4 files changed, 61 insertions(+), 38 deletions(-) diff --git a/services/userlog/pkg/service/conversion.go b/services/userlog/pkg/service/conversion.go index 266e65e0b1..df472094a8 100644 --- a/services/userlog/pkg/service/conversion.go +++ b/services/userlog/pkg/service/conversion.go @@ -129,7 +129,7 @@ func (c *Converter) ConvertGlobalEvent(typ string, data json.RawMessage) (OC10No return OC10Notification{}, err } - return c.deprovisionMessage(PlatformDeprovision, dd.DeprovisionDate) + return c.deprovisionMessage(PlatformDeprovision, dd.DeprovisionDate.Format(dd.DeprovisionFormat)) } } @@ -305,7 +305,7 @@ func (c *Converter) policiesMessage(eventid string, nt NotificationTemplate, exe }, nil } -func (c *Converter) deprovisionMessage(nt NotificationTemplate, deproDate time.Time) (OC10Notification, error) { +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, }) diff --git a/services/userlog/pkg/service/globalevents.go b/services/userlog/pkg/service/globalevents.go index 2f7369d1b4..b1ba5e568b 100644 --- a/services/userlog/pkg/service/globalevents.go +++ b/services/userlog/pkg/service/globalevents.go @@ -10,6 +10,8 @@ var ( type DeprovisionData struct { // The deprovision date DeprovisionDate time.Time `json:"deprovision_date"` + // The Format of the deprvision date + DeprovisionFormat string // The user who stored the deprovision message Deprovisioner string } diff --git a/services/userlog/pkg/service/http.go b/services/userlog/pkg/service/http.go index 2cc4e6cb60..6d42bab474 100644 --- a/services/userlog/pkg/service/http.go +++ b/services/userlog/pkg/service/http.go @@ -110,22 +110,8 @@ 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 - } - +// HandlePostGlobaelEvent is the POST handler for global events +func (ul *UserlogService) HandlePostGlobalEvent(w http.ResponseWriter, r *http.Request) { 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") @@ -142,6 +128,24 @@ func (ul *UserlogService) HandlePostEvent(w http.ResponseWriter, r *http.Request w.WriteHeader(http.StatusOK) } +// HandleDeleteGlobalEvent is the DELETE handler for global events +func (ul *UserlogService) HandleDeleteGlobalEvent(w http.ResponseWriter, r *http.Request) { + 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.DeleteGlobalEvents(req.IDs); err != nil { + ul.log.Error().Err(err).Int("returned statuscode", http.StatusInternalServerError).Msg("delete events failed") + 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()) @@ -197,12 +201,9 @@ func RequireAdmin(rm *roles.Manager, logger log.Logger) func(next http.HandlerFu return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { u, ok := revactx.ContextGetUser(r.Context()) - if !ok { - errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized") - return - } - if u.Id == nil || u.Id.OpaqueId == "" { - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "user is missing an id") + if !ok || u.GetId().GetOpaqueId() == "" { + logger.Error().Str("userid", u.Id.OpaqueId).Msg("user not in context") + errorcode.AccessDenied.Render(w, r, http.StatusInternalServerError, "") return } // get roles from context @@ -213,11 +214,12 @@ func RequireAdmin(rm *roles.Manager, logger log.Logger) func(next http.HandlerFu roleIDs, err = rm.FindRoleIDsForUser(r.Context(), u.Id.OpaqueId) if err != nil { logger.Err(err).Str("userid", u.Id.OpaqueId).Msg("failed to get roles for user") - errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized") + errorcode.AccessDenied.Render(w, r, http.StatusInternalServerError, "") return } if len(roleIDs) == 0 { - errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized") + logger.Err(err).Str("userid", u.Id.OpaqueId).Msg("user has no roles") + errorcode.AccessDenied.Render(w, r, http.StatusInternalServerError, "") return } } @@ -228,7 +230,7 @@ func RequireAdmin(rm *roles.Manager, logger log.Logger) func(next http.HandlerFu return } - errorcode.AccessDenied.Render(w, r, http.StatusForbidden, "Forbidden") + errorcode.AccessDenied.Render(w, r, http.StatusNotFound, "Forbidden") } } } diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index 2ab8396371..cb97beef53 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -90,8 +90,9 @@ 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("/", RequireAdmin(&m, ul.log)(ul.HandlePostEvent)) r.Delete("/", ul.HandleDeleteEvents) + r.Post("/global", RequireAdmin(&m, ul.log)(ul.HandlePostGlobalEvent)) + r.Delete("/global", RequireAdmin(&m, ul.log)(ul.HandleDeleteGlobalEvent)) if !ul.cfg.DisableSSE { r.Get("/sse", ul.HandleSSE) @@ -266,11 +267,22 @@ func (ul *UserlogService) StoreGlobalEvent(typ string, data map[string]string) e return fmt.Errorf("cannot parse time to format. time: '%s' format: '%s'", dps, format) } - return ul.storeGlobalEvent(typ, DeprovisionData{ - DeprovisionDate: date, - }) + ev := DeprovisionData{ + DeprovisionDate: date, + DeprovisionFormat: format, + } + b, err := json.Marshal(ev) + if err != nil { + return err + } + + return ul.alterGlobalEvents(func(evs map[string]json.RawMessage) error { + evs[typ] = b + return nil + }) } + } // GetGlobalEvents will return all global events @@ -291,6 +303,16 @@ func (ul *UserlogService) GetGlobalEvents() (map[string]json.RawMessage, error) return out, nil } +// DeleteGlobalEvents will delete the specified event +func (ul *UserlogService) DeleteGlobalEvents(evnames []string) error { + return ul.alterGlobalEvents(func(evs map[string]json.RawMessage) error { + for _, name := range evnames { + delete(evs, name) + } + return nil + }) +} + func (ul *UserlogService) addEventToUser(userid string, event events.Event) error { if !ul.cfg.DisableSSE { if err := ul.sendSSE(userid, event); err != nil { @@ -368,18 +390,15 @@ 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 - } - +func (ul *UserlogService) alterGlobalEvents(alter func(map[string]json.RawMessage) error) error { evs, err := ul.GetGlobalEvents() if err != nil && err != store.ErrNotFound { return err } - evs[typ] = b + if err := alter(evs); err != nil { + return err + } val, err := json.Marshal(evs) if err != nil { From ff61d25e1c2b42d919173c7dac66bcbbc18fbbb1 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Thu, 6 Jul 2023 14:53:29 +0200 Subject: [PATCH 5/5] improve documentation Signed-off-by: jkoberg --- services/userlog/README.md | 10 ++++++++++ services/userlog/pkg/service/templates.go | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/services/userlog/README.md b/services/userlog/README.md index 701fbfa293..ca686f19c9 100644 --- a/services/userlog/README.md +++ b/services/userlog/README.md @@ -34,10 +34,20 @@ The `userlog` service provides an API to retrieve configured events. For now, th Additionally to the oc10 API, the `userlog` service also provides an `/sse` (Server-Sent Events) endpoint to be informed by the server when an event happens. See [What is Server-Sent Events](https://medium.com/yemeksepeti-teknoloji/what-is-server-sent-events-sse-and-how-to-implement-it-904938bffd73) for a simple introduction and examples of server sent events. The `sse` endpoint will respect language changes of the user without needing to reconnect. Note that SSE has a limitation of six open connections per browser which can be reached if one has opened various tabs of the Web UI pointing to the same Infinite Scale instance. +## Posting + +The userlog service is able to store global messages that will be displayed in the Web UI to all users. These messages can only be activated and deleted by users with the `admin` role but not by ordinary users. If a user deletes the message in the Web UI, it reappears on reload. Global messages use the endpoint `/ocs/v2.php/apps/notifications/api/v1/notifications/global` and are activated by sending a `POST` request. Note that sending another `POST` request of the same type overwrites the previous one. For the time being, only the type `deprovision` is supported. + +### Deprovisioning + +Deprovision messages announce a deprovision text including a deprovision date of the instance to all users. With this message, users get informed that the instance will be shut down and deprovisioned and no further access to their data is possible past the given date. This implies that users must download their data before the given date. The text shown to users refers to this information. Note that the task to deprovision the instance does not depend on the message. The text of the message can be translated according to the translation settings, see section [Translations](#translations). The endpoint only expects a `deprovision_date` parameter in the `POST` request body as the final text is assembled automatically. The string hast to be in `RFC3339` format, however, this format can be changed by using `deprovision_date_format`. See the [go time formating](https://pkg.go.dev/time#pkg-constants) for more details. + ## Deleting To delete events for an user, use a `DELETE` request to `ocs/v2.php/apps/notifications/api/v1/notifications` containing the IDs to delete. +Only users with the `admin` role can send a `DELETE` request to the `ocs/v2.php/apps/notifications/api/v1/notifications/global` endpoint to remove a global message, see the [Posting](#posting) section for more details.) + ## Translations The `userlog` service has embedded translations sourced via transifex to provide a basic set of translated languages. These embedded translations are available for all deployment scenarios. In addition, the service supports custom translations, though it is currently not possible to just add custom translations to embedded ones. If custom translations are configured, the embedded ones are not used. To configure custom translations, the `USERLOG_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation files. This path must be available from all instances of the userlog service, a shared storage is recommended. Translation files must be of type [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files) or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to be `userlog.po` (or `userlog.mo`) and stored in a folder structure defining the language code. In general the path/name pattern for a translation file needs to be: diff --git a/services/userlog/pkg/service/templates.go b/services/userlog/pkg/service/templates.go index 28c75a15ef..6acf6fb8ca 100644 --- a/services/userlog/pkg/service/templates.go +++ b/services/userlog/pkg/service/templates.go @@ -56,8 +56,8 @@ var ( } PlatformDeprovision = NotificationTemplate{ - Subject: Template("Platform will be deprovisioned"), - Message: Template("Attention! The platform will be deprovisioned at {date}"), + Subject: Template("Instance will be shut down and deprovisioned"), + Message: Template("Attention! The instance will be shut down and deprovisioned on {date}. Download all your data before that date as no access past that date is possible."), } )