Merge pull request #6946 from kobergj/DeprovisionNotificationStaticSecret

Global Notifications Static Secret
This commit is contained in:
kobergj
2023-08-08 10:42:46 +02:00
committed by GitHub
7 changed files with 69 additions and 36 deletions

View File

@@ -0,0 +1,5 @@
Enhancement: Add static secret to gn endpoints
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

@@ -38,6 +38,7 @@ type Config struct {
EnableFederatedSharingOutgoing bool `yaml:"enable_federated_sharing_outgoing" env:"FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING" desc:"Changing this value is NOT supported. Enables support for outgoing federated sharing for clients. The backend behaviour is not changed."`
SearchMinLength int `yaml:"search_min_length" env:"FRONTEND_SEARCH_MIN_LENGTH" desc:"Minimum number of characters to enter before a client should start a search for Share receivers. This setting can be used to customize the user experience if e.g too many results are displayed."`
Edition string `yaml:"edition" env:"OCIS_EDITION;FRONTEND_EDITION"`
DisableSSE bool `yaml:"disable_sse" env:"OCIS_DISABLE_SSE,FRONTEND_DISABLE_SSE" desc:"When set to true, clients are informed that the Server-Sent Events endpoint is not accessible."`
PublicURL string `yaml:"public_url" env:"OCIS_URL;FRONTEND_PUBLIC_URL" desc:"The public facing URL of the oCIS frontend."`

View File

@@ -192,7 +192,7 @@ func FrontendConfigFromStruct(cfg *config.Config) (map[string]interface{}, error
"hostname": "",
},
"support_url_signing": true,
"support_sse": true,
"support_sse": !cfg.DisableSSE,
},
"graph": map[string]interface{}{
"personal_data_export": true,

View File

@@ -36,7 +36,11 @@ Additionally to the oc10 API, the `userlog` service also provides an `/sse` (Ser
## 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.
The userlog service is able to store global messages that will be displayed in the Web UI to all 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.
### Authentication
`POST` and `DELETE` endpoints provide notifications to all users. Therefore only certain users can configure them. Two authentication methods for this endpoint are provided. Users with the `admin` role can always access these endpoints. Additionally, a static secret via the `USERLOG_GLOBAL_NOTIFICATIONS_SECRET` can be defined to enable access for users knowing this secret, which has to be sent with the header containing the request.
### Deprovisioning
@@ -46,7 +50,7 @@ Deprovision messages announce a deprovision text including a deprovision date of
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.)
Sending a `DELETE` request to the `ocs/v2.php/apps/notifications/api/v1/notifications/global` endpoint to remove a global message is a restricted action, see the [Authentication](#authentication) section for more details.)
## Translations

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 system admins and users knowing that secret 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)