Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprovisioning Notification #6672

Merged
merged 5 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions services/userlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions services/userlog/pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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),
)

Expand Down
8 changes: 8 additions & 0 deletions services/userlog/pkg/server/http/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Options struct {
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
HistoryClient ehsvc.EventHistoryService
ValueClient settingssvc.ValueService
RoleClient settingssvc.RoleService
RegisteredEvents []events.Unmarshaller
}

Expand Down Expand Up @@ -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
}
}
1 change: 1 addition & 0 deletions services/userlog/pkg/server/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions services/userlog/pkg/service/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"embed"
"encoding/json"
"fmt"
"io/fs"
"strings"
Expand All @@ -28,6 +29,7 @@ var (
_resourceTypeResource = "resource"
_resourceTypeSpace = "storagespace"
_resourceTypeShare = "share"
_resourceTypeGlobal = "global"

_domain = "userlog"
)
Expand Down Expand Up @@ -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.Format(dd.DeprovisionFormat))
}

}

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 {
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions services/userlog/pkg/service/globalevents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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 Format of the deprvision date
DeprovisionFormat string
// The user who stored the deprovision message
Deprovisioner string
}
104 changes: 104 additions & 0 deletions services/userlog/pkg/service/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,6 +61,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)
Expand Down Expand Up @@ -89,6 +110,42 @@ func (ul *UserlogService) HandleSSE(w http.ResponseWriter, r *http.Request) {
ul.sse.ServeHTTP(w, r)
}

// 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")
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)
}

// 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())
Expand Down Expand Up @@ -130,3 +187,50 @@ type GetEventResponseOC10 struct {
type DeleteEventsRequest struct {
IDs []string `json:"ids"`
}

// PostEventsRequest is the expected body for the post request
type PostEventsRequest struct {
// 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
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 || 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
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.StatusInternalServerError, "")
return
}
if len(roleIDs) == 0 {
logger.Err(err).Str("userid", u.Id.OpaqueId).Msg("user has no roles")
errorcode.AccessDenied.Render(w, r, http.StatusInternalServerError, "")
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.StatusNotFound, "Forbidden")
}
}
}
9 changes: 9 additions & 0 deletions services/userlog/pkg/service/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Options struct {
HistoryClient ehsvc.EventHistoryService
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
ValueClient settingssvc.ValueService
RoleClient settingssvc.RoleService
RegisteredEvents []events.Unmarshaller
}

Expand Down Expand Up @@ -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
}
}
Loading