allow static secret access to global notification endpoints

Signed-off-by: jkoberg <jkoberg@owncloud.com>
This commit is contained in:
jkoberg
2023-08-02 15:43:02 +02:00
parent 5e4b9baa1c
commit 604ce5174c
4 changed files with 61 additions and 33 deletions

View File

@@ -0,0 +1,5 @@
Enhancement: Allow calling the global notifications endpoints when knowing a static secret
The global notifications POST and DELETE endpoints (used only for deprovision notifications at the moment) can now be called by adding a static secret to the header. Admins can still call this endpoint without knowing the secret
https://github.com/owncloud/ocis/pull/6946

View File

@@ -28,7 +28,9 @@ type Config struct {
Events Events `yaml:"events"`
Persistence Persistence `yaml:"persistence"`
DisableSSE bool `yaml:"disable_sse" env:"USERLOG_DISABLE_SSE" desc:"Disables server-sent events (sse). When disabled, clients will no longer be able to connect to the sse endpoint."`
DisableSSE bool `yaml:"disable_sse" env:"OCIS_DISABLE_SSE,USERLOG_DISABLE_SSE" desc:"Disables server-sent events (sse). When disabled, clients will no longer be able to connect to the sse endpoint."`
GlobalNotificationsSecret string `yaml:"global_notifications_secret" env:"USERLOG_GLOBAL_NOTIFICATIONS_SECRET" desc:"The secret to secure the global notifications endpoint. Only users knowing that secret and system admins can call the global notifications POST/DELETE endpoints"`
Context context.Context `yaml:"-"`
}

View File

@@ -1,11 +1,13 @@
package service
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/cs3org/reva/v2/pkg/appctx"
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"
@@ -202,41 +204,60 @@ type PostEventsRequest struct {
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 {
// RequireAdminOrSecret middleware allows only requests if the requesting user is an admin or knows the static secret
func RequireAdminOrSecret(rm *roles.Manager, secret string) func(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 {
// allow bypassing admin requirement by sending the correct secret
if secret != "" && r.Header.Get("secret") == secret {
next.ServeHTTP(w, r)
return
}
errorcode.AccessDenied.Render(w, r, http.StatusNotFound, "Forbidden")
isadmin, err := isAdmin(r.Context(), rm)
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "")
return
}
if isadmin {
next.ServeHTTP(w, r)
return
}
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Not found")
return
}
}
}
// isAdmin determines if the user in the context is an admin / has account management permissions
func isAdmin(ctx context.Context, rm *roles.Manager) (bool, error) {
logger := appctx.GetLogger(ctx)
u, ok := revactx.ContextGetUser(ctx)
uid := u.GetId().GetOpaqueId()
if !ok || uid == "" {
logger.Error().Str("userid", uid).Msg("user not in context")
return false, errors.New("no user in context")
}
// get roles from context
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
if !ok {
logger.Debug().Str("userid", uid).Msg("No roles in context, contacting settings service")
var err error
roleIDs, err = rm.FindRoleIDsForUser(ctx, uid)
if err != nil {
logger.Err(err).Str("userid", uid).Msg("failed to get roles for user")
return false, err
}
if len(roleIDs) == 0 {
logger.Err(err).Str("userid", uid).Msg("user has no roles")
return false, errors.New("user has no roles")
}
}
// check if permission is present in roles of the authenticated account
return rm.FindPermissionByID(ctx, roleIDs, settings.AccountManagementPermissionID) != nil, nil
}

View File

@@ -98,8 +98,8 @@ 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.Delete("/", ul.HandleDeleteEvents)
r.Post("/global", RequireAdmin(&m, ul.log)(ul.HandlePostGlobalEvent))
r.Delete("/global", RequireAdmin(&m, ul.log)(ul.HandleDeleteGlobalEvent))
r.Post("/global", RequireAdminOrSecret(&m, o.Config.GlobalNotificationsSecret)(ul.HandlePostGlobalEvent))
r.Delete("/global", RequireAdminOrSecret(&m, o.Config.GlobalNotificationsSecret)(ul.HandleDeleteGlobalEvent))
if !ul.cfg.DisableSSE {
r.Get("/sse", ul.HandleSSE)