From 604ce5174c2383ddf7312738a74fb04c209ebef5 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 2 Aug 2023 15:43:02 +0200 Subject: [PATCH 1/3] allow static secret access to global notification endpoints Signed-off-by: jkoberg --- .../global-notifications-static-secret.md | 5 ++ services/userlog/pkg/config/config.go | 4 +- services/userlog/pkg/service/http.go | 81 ++++++++++++------- services/userlog/pkg/service/service.go | 4 +- 4 files changed, 61 insertions(+), 33 deletions(-) create mode 100644 changelog/unreleased/global-notifications-static-secret.md diff --git a/changelog/unreleased/global-notifications-static-secret.md b/changelog/unreleased/global-notifications-static-secret.md new file mode 100644 index 0000000000..7110d5e0b7 --- /dev/null +++ b/changelog/unreleased/global-notifications-static-secret.md @@ -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 diff --git a/services/userlog/pkg/config/config.go b/services/userlog/pkg/config/config.go index b5dcc4e46e..96836855fe 100644 --- a/services/userlog/pkg/config/config.go +++ b/services/userlog/pkg/config/config.go @@ -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:"-"` } diff --git a/services/userlog/pkg/service/http.go b/services/userlog/pkg/service/http.go index 18d791b9bf..d9dc3679b0 100644 --- a/services/userlog/pkg/service/http.go +++ b/services/userlog/pkg/service/http.go @@ -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 +} diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index ad4fd9c0a5..72c9efa87e 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -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) From 4a850abc860218a4e238def36c065c17677f5cb9 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 2 Aug 2023 15:45:55 +0200 Subject: [PATCH 2/3] allow configuring support_sse capability Signed-off-by: jkoberg --- ...notifications-static-secret.md => gn-static-secret.md} | 2 +- services/frontend/pkg/config/config.go | 1 + services/frontend/pkg/revaconfig/config.go | 2 +- services/userlog/README.md | 8 ++++++-- services/userlog/pkg/config/config.go | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) rename changelog/unreleased/{global-notifications-static-secret.md => gn-static-secret.md} (75%) diff --git a/changelog/unreleased/global-notifications-static-secret.md b/changelog/unreleased/gn-static-secret.md similarity index 75% rename from changelog/unreleased/global-notifications-static-secret.md rename to changelog/unreleased/gn-static-secret.md index 7110d5e0b7..9656756ad7 100644 --- a/changelog/unreleased/global-notifications-static-secret.md +++ b/changelog/unreleased/gn-static-secret.md @@ -1,4 +1,4 @@ -Enhancement: Allow calling the global notifications endpoints when knowing a static secret +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 diff --git a/services/frontend/pkg/config/config.go b/services/frontend/pkg/config/config.go index f781595383..887185b34e 100644 --- a/services/frontend/pkg/config/config.go +++ b/services/frontend/pkg/config/config.go @@ -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."` diff --git a/services/frontend/pkg/revaconfig/config.go b/services/frontend/pkg/revaconfig/config.go index 993b9b855f..3383251c1d 100644 --- a/services/frontend/pkg/revaconfig/config.go +++ b/services/frontend/pkg/revaconfig/config.go @@ -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, diff --git a/services/userlog/README.md b/services/userlog/README.md index ca686f19c9..250f92c869 100644 --- a/services/userlog/README.md +++ b/services/userlog/README.md @@ -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 access them. There are two different possiblities to authenticate for this endpoint. First, users with the `admin` role can always access the endpoints. However it is possible to define a static secret via the `USERLOG_GLOBAL_NOTIFICATIONS_SECRET`. Users knowing the secret can send a header containing it on their requests. Having the secret in the header bypasses the `admin` requirement. ### 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 diff --git a/services/userlog/pkg/config/config.go b/services/userlog/pkg/config/config.go index 96836855fe..5c723d5be1 100644 --- a/services/userlog/pkg/config/config.go +++ b/services/userlog/pkg/config/config.go @@ -30,7 +30,7 @@ type Config struct { 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"` + 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:"-"` } From dcffab7134bb2e4dd3f1f104a9ee429eca39adaa Mon Sep 17 00:00:00 2001 From: kobergj Date: Thu, 3 Aug 2023 09:52:47 +0200 Subject: [PATCH 3/3] Upate userlog readme Co-authored-by: Martin Signed-off-by: jkoberg --- services/userlog/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/userlog/README.md b/services/userlog/README.md index 250f92c869..474fcd07f7 100644 --- a/services/userlog/README.md +++ b/services/userlog/README.md @@ -36,11 +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. 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 access them. There are two different possiblities to authenticate for this endpoint. First, users with the `admin` role can always access the endpoints. However it is possible to define a static secret via the `USERLOG_GLOBAL_NOTIFICATIONS_SECRET`. Users knowing the secret can send a header containing it on their requests. Having the secret in the header bypasses the `admin` requirement. +`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 @@ -50,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. -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.) +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