Skip to content
This repository has been archived by the owner on Dec 4, 2023. It is now read-only.

Commit

Permalink
CVE-2020-14319: Deny mutation operations unless an existing session e…
Browse files Browse the repository at this point in the history
…xists

(cherry picked from commit ee03e22)
  • Loading branch information
k-wall committed Jul 29, 2020
1 parent 70465d8 commit 5b9796f
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 99 deletions.
72 changes: 8 additions & 64 deletions cmd/console-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,19 @@
package main

import (
"context"
"flag"
"fmt"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
adminv1beta2 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/admin/v1beta2"
enmassev1beta1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/enmasse/v1beta1"
"github.com/enmasseproject/enmasse/pkg/consolegraphql/cache"
"github.com/enmasseproject/enmasse/pkg/consolegraphql/metric"
"github.com/enmasseproject/enmasse/pkg/consolegraphql/resolvers"
"github.com/enmasseproject/enmasse/pkg/consolegraphql/server"
"github.com/enmasseproject/enmasse/pkg/consolegraphql/server/query"
"github.com/enmasseproject/enmasse/pkg/consolegraphql/watchers"
"github.com/enmasseproject/enmasse/pkg/util"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/vektah/gqlparser/v2/gqlerror"
"k8s.io/client-go/tools/clientcmd"
"log"
"net/http"
Expand Down Expand Up @@ -54,10 +48,10 @@ func main() {
port := util.GetEnvOrDefault("PORT", "8080")
metricsPort := util.GetEnvOrDefault("METRICS_PORT", "8889")

var impersonationConfig *server.ImpersonationConfig
var impersonationConfig *query.ImpersonationConfig
impersonationEnable := util.GetBooleanEnvOrDefault("IMPERSONATION_ENABLE", false)
if impersonationEnable {
impersonationConfig = &server.ImpersonationConfig{}
impersonationConfig = &query.ImpersonationConfig{}
userHeader, ok := os.LookupEnv("IMPERSONATION_USER_HEADER")
if ok {
impersonationConfig.UserHeader = &userHeader
Expand Down Expand Up @@ -147,68 +141,17 @@ http://localhost:` + port + `/graphql
}, updateMetricsPeriod)
}

queryTimeMetric := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "console_query_time_seconds",
Help: "The query time in seconds",
Buckets: prometheus.DefBuckets,
}, []string{"operationName"})
queryErrorCountMetric := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "console_query_error_total",
Help: "Number of queries that have ended in error",
}, []string{"operationName"})
sessionCountMetric := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "console_active_sessions",
Help: "Number of active HTTP sessions",
})
queryTimeMetric, queryErrorCountMetric, sessionCountMetric := server.CreateMetrics()
prometheus.MustRegister(queryTimeMetric, queryErrorCountMetric, sessionCountMetric)

queryServer := http.NewServeMux()

gqlServer := handler.New(resolvers.NewExecutableSchema(resolvers.Config{Resolvers: &resolver}))

queryEndpoint := "/graphql/query"
if graphqlPlayground || *developmentMode {
playgroundHandler := playground.Handler("GraphQL playground", queryEndpoint)
queryServer.Handle("/graphql/", playgroundHandler)
gqlServer.Use(extension.Introspection{})
}

sessionManager := server.CreateSessionManager(sessionLifetime, sessionIdleTimeout,
func() { manager.BeginWatching() },
func() { manager.EndWatching() },
sessionCountMetric)

gqlServer.AddTransport(transport.GET{})
gqlServer.AddTransport(transport.POST{})

gqlServer.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
rctx := graphql.GetOperationContext(ctx)
if rctx != nil {
log.Printf("Query error - op: %s query: %s vars: %+v error: %s\n", rctx.OperationName, rctx.RawQuery, rctx.Variables, err)
}
queryErrorCountMetric.WithLabelValues(rctx.OperationName).Inc()
return graphql.DefaultErrorPresenter(ctx, err)
})

gqlServer.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
loggedOnUser := "<unknown>"
start := time.Now()
result := next(ctx)
loggedOnUser = server.UpdateAccessControllerState(ctx, loggedOnUser, sessionManager)
rctx := graphql.GetOperationContext(ctx)
if rctx != nil {
since := time.Since(start)
log.Printf("[%s] Query execution - op: %s %s\n", loggedOnUser, rctx.OperationName, since)
queryTimeMetric.WithLabelValues(rctx.OperationName).Observe(since.Seconds())
}
return result
})

if *developmentMode {
queryServer.Handle(queryEndpoint, server.DevelopmentHandler(gqlServer, sessionManager, config.BearerToken))
} else {
queryServer.Handle(queryEndpoint, server.AuthHandler(gqlServer, sessionManager, impersonationConfig))
}
queryServer := query.CreateQueryServer(resolver, graphqlPlayground, *developmentMode,
sessionManager,
queryErrorCountMetric, queryTimeMetric, config, impersonationConfig, query.CreateClientSets)

go func() {
err = http.ListenAndServe("127.0.0.1:"+port, sessionManager.LoadAndSave(queryServer))
Expand All @@ -226,3 +169,4 @@ http://localhost:` + port + `/graphql
}

}

27 changes: 27 additions & 0 deletions pkg/consolegraphql/server/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2020, EnMasse authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*
*/

package server

import "github.com/prometheus/client_golang/prometheus"

func CreateMetrics() (*prometheus.HistogramVec, *prometheus.CounterVec, prometheus.Gauge) {
queryTimeMetric := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "console_query_time_seconds",
Help: "The query time in seconds",
Buckets: prometheus.DefBuckets,
}, []string{"operationName"})
queryErrorCountMetric := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "console_query_error_total",
Help: "Number of queries that have ended in error",
}, []string{"operationName"})
sessionCountMetric := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "console_active_sessions",
Help: "Number of active HTTP sessions",
})
return queryTimeMetric, queryErrorCountMetric, sessionCountMetric
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
*/

package server
package query

import (
"bytes"
Expand All @@ -14,6 +14,7 @@ import (
"github.com/alexedwards/scs/v2"
"github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/enmasse/v1beta1"
"github.com/enmasseproject/enmasse/pkg/consolegraphql/accesscontroller"
"github.com/enmasseproject/enmasse/pkg/consolegraphql/server"
"github.com/enmasseproject/enmasse/pkg/util"
userapiv1 "github.com/openshift/api/user/v1"
userv1 "github.com/openshift/client-go/user/clientset/versioned/typed/user/v1"
Expand Down Expand Up @@ -44,6 +45,8 @@ type ImpersonationConfig struct {
UserHeader *string
}

type clientSetsCreator func(config *restclient.Config) (kubeClient kubernetes.Interface, userClient userv1.UserV1Interface, coreClient v1beta1.EnmasseV1beta1Interface, err error)

func getImpersonatedUser(req *http.Request, impersonationConfig *ImpersonationConfig) string {
if impersonationConfig != nil {
userHeader := forwardedUserHeader
Expand All @@ -55,11 +58,11 @@ func getImpersonatedUser(req *http.Request, impersonationConfig *ImpersonationCo
return ""
}

func AuthHandler(next http.Handler, sessionManager *scs.SessionManager, impersonationConfig *ImpersonationConfig) http.Handler {
func AuthHandler(next http.Handler, sessionManager *scs.SessionManager, impersonationConfig *ImpersonationConfig, clientSetsCreator clientSetsCreator) http.Handler {
gob.Register(userapiv1.User{})

return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
var state *RequestState
var state *server.RequestState

useSession := true
if healthProbe, _ := strconv.ParseBool(req.Header.Get("X-Health")); healthProbe {
Expand All @@ -82,6 +85,7 @@ func AuthHandler(next http.Handler, sessionManager *scs.SessionManager, imperson
rw.WriteHeader(500)
}

newSession := false
if useSession {
if sessionManager.Exists(req.Context(), sessionOwnerSessionAttribute) {
sessionOwnerAccessTokenSha := sessionManager.Get(req.Context(), sessionOwnerSessionAttribute).([]byte)
Expand All @@ -90,9 +94,11 @@ func AuthHandler(next http.Handler, sessionManager *scs.SessionManager, imperson
// New session created automatically.
_ = sessionManager.Destroy(req.Context())
sessionManager.Put(req.Context(), sessionOwnerSessionAttribute, sessionSha)
newSession = true
}
} else {
sessionManager.Put(req.Context(), sessionOwnerSessionAttribute, sessionSha)
newSession = true
}
}

Expand All @@ -114,6 +120,11 @@ func AuthHandler(next http.Handler, sessionManager *scs.SessionManager, imperson
}

config, err := kubeConfig.ClientConfig()
if err != nil {
log.Printf("Failed to build config : %v", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

// Set impersonation configuration options if they are provided.
if impersonatedUser != "" {
Expand All @@ -126,21 +137,7 @@ func AuthHandler(next http.Handler, sessionManager *scs.SessionManager, imperson
// return &Tracer{RoundTripper: rt}
// }

kubeClient, err := kubernetes.NewForConfig(config)
if err != nil {
log.Printf("Failed to build client set : %v", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

userClient, err := userv1.NewForConfig(config)
if err != nil {
log.Printf("Failed to build client set : %v", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

coreClient, err := v1beta1.NewForConfig(config)
kubeClient, userClient, coreClient, err := clientSetsCreator(config)
if err != nil {
log.Printf("Failed to build client set : %v", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
Expand All @@ -165,22 +162,23 @@ func AuthHandler(next http.Handler, sessionManager *scs.SessionManager, imperson
}

controller := accesscontroller.NewKubernetesRBACAccessController(kubeClient, accessControllerState)
state = &RequestState{
state = &server.RequestState{
EnmasseV1beta1Client: coreClient,
AccessController: controller,
User: loggedOnUser,
UserAccessToken: config.BearerToken,
UseSession: useSession,
NewSession: newSession,
ImpersonatedUser: impersonatedUser,
}

ctx := ContextWithRequestState(state, req.Context())
ctx := server.ContextWithRequestState(state, req.Context())

next.ServeHTTP(rw, req.WithContext(ctx))
})
}

func DevelopmentHandler(next http.Handler, sessionManager *scs.SessionManager, accessToken string) http.Handler {
func DevelopmentHandler(next http.Handler, sessionManager *scs.SessionManager, accessToken string, clientSetsCreator clientSetsCreator) http.Handler {
gob.Register(userapiv1.User{})

return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
Expand All @@ -197,15 +195,7 @@ func DevelopmentHandler(next http.Handler, sessionManager *scs.SessionManager, a
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

userclient, err := userv1.NewForConfig(config)
if err != nil {
log.Printf("Failed to build client set : %v", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

coreClient, err := v1beta1.NewForConfig(config)
_, userclient, coreClient, err := clientSetsCreator(config)
if err != nil {
log.Printf("Failed to build client set : %v", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
Expand All @@ -220,7 +210,7 @@ func DevelopmentHandler(next http.Handler, sessionManager *scs.SessionManager, a
sessionManager.Put(req.Context(), loggedOnUserSessionAttribute, loggedOnUser)
}

requestState := &RequestState{
requestState := &server.RequestState{
EnmasseV1beta1Client: coreClient,
AccessController: accesscontroller.NewAllowAllAccessController(),
User: loggedOnUser,
Expand All @@ -229,13 +219,13 @@ func DevelopmentHandler(next http.Handler, sessionManager *scs.SessionManager, a
ImpersonatedUser: "",
}

ctx := ContextWithRequestState(requestState, req.Context())
ctx := server.ContextWithRequestState(requestState, req.Context())
next.ServeHTTP(rw, req.WithContext(ctx))
})
}

func UpdateAccessControllerState(ctx context.Context, loggedOnUser string, sessionManager *scs.SessionManager) string {
requestState := GetRequestStateFromContext(ctx)
requestState := server.GetRequestStateFromContext(ctx)
if requestState != nil {
loggedOnUser = requestState.User.Name
if requestState.UseSession {
Expand All @@ -251,7 +241,25 @@ func UpdateAccessControllerState(ctx context.Context, loggedOnUser string, sessi
return loggedOnUser
}

func getLoggedOnUser(req *http.Request, userClient *userv1.UserV1Client, impersonatedUser string) userapiv1.User {
func CreateClientSets(config *restclient.Config) (kubeClient kubernetes.Interface, userClient userv1.UserV1Interface, coreClient v1beta1.EnmasseV1beta1Interface, err error) {
kubeClient, err = kubernetes.NewForConfig(config)
if err != nil {
return
}

userClient, err = userv1.NewForConfig(config)
if err != nil {
return
}

coreClient, err = v1beta1.NewForConfig(config)
if err != nil {
return
}
return
}

func getLoggedOnUser(req *http.Request, userClient userv1.UserV1Interface, impersonatedUser string) userapiv1.User {
createUser := func(userId string) userapiv1.User {
return userapiv1.User{
ObjectMeta: metav1.ObjectMeta{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
*/

package server
package query

import (
"github.com/stretchr/testify/assert"
Expand Down
Loading

0 comments on commit 5b9796f

Please sign in to comment.