From ec2924dc7bee0db012de862182026fb9e96a004b Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Sat, 2 Nov 2024 19:13:36 +0100 Subject: [PATCH] Implement admin login --- balancer/pkg/bundle/bundle.go | 11 +++++ balancer/pkg/testutil/testUtils.go | 3 ++ balancer/routes/join.go | 41 +++++++++++++++++ balancer/routes/join_test.go | 70 +++++++++++++++++++++++++++--- 4 files changed, 119 insertions(+), 6 deletions(-) diff --git a/balancer/pkg/bundle/bundle.go b/balancer/pkg/bundle/bundle.go index 31031fb1..471a21e9 100644 --- a/balancer/pkg/bundle/bundle.go +++ b/balancer/pkg/bundle/bundle.go @@ -39,6 +39,11 @@ type Config struct { JuiceShopConfig JuiceShopConfig `json:"juiceShop"` MaxInstances int `json:"maxInstances"` CookieConfig CookieConfig `json:"cookie"` + AdminConfig *AdminConfig +} + +type AdminConfig struct { + Password string `json:"password"` } type CookieConfig struct { @@ -116,12 +121,18 @@ func New() *Bundle { panic(errors.New("environment variable 'MULTI_JUICER_CONFIG_COOKIE_SIGNING_KEY' must be set")) } + adminPasswordKey := os.Getenv("MULTI_JUICER_CONFIG_ADMIN_PASSWORD") + if adminPasswordKey == "" { + panic(errors.New("environment variable 'MULTI_JUICER_CONFIG_ADMIN_PASSWORD' must be set")) + } + config, err := readConfigFromFile("/config/config.json") if err != nil { panic(err) } config.CookieConfig.SigningKey = cookieSigningKey + config.AdminConfig = &AdminConfig{Password: adminPasswordKey} // read /challenges.json file challengesBytes, err := os.ReadFile("/challenges.json") diff --git a/balancer/pkg/testutil/testUtils.go b/balancer/pkg/testutil/testUtils.go index 2a376abf..8ed78cd9 100644 --- a/balancer/pkg/testutil/testUtils.go +++ b/balancer/pkg/testutil/testUtils.go @@ -57,6 +57,9 @@ func NewTestBundleWithCustomFakeClient(clientset kubernetes.Interface) *bundle.B Name: "team", Secure: false, }, + AdminConfig: &bundle.AdminConfig{ + Password: "mock-admin-password", + }, }, } } diff --git a/balancer/routes/join.go b/balancer/routes/join.go index 48c67b8a..2353d295 100644 --- a/balancer/routes/join.go +++ b/balancer/routes/join.go @@ -45,6 +45,12 @@ func init() { func handleTeamJoin(bundle *bundle.Bundle) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { team := r.PathValue("team") + + if team == "admin" { + handleAdminLogin(bundle, w, r) + return + } + deployment, err := getDeployment(r.Context(), bundle, team) if err != nil && errors.IsNotFound(err) { isMaxLimitReached, err := isMaxInstanceLimitReached(r.Context(), bundle) @@ -65,6 +71,41 @@ func handleTeamJoin(bundle *bundle.Bundle) http.Handler { }) } +func handleAdminLogin(bundle *bundle.Bundle, w http.ResponseWriter, r *http.Request) { + if r.Body == nil { + writeUnauthorizedResponse(w) + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "failed to read request body", http.StatusInternalServerError) + return + } + + var requestBody joinRequestBody + if err := json.Unmarshal(body, &requestBody); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + + if requestBody.Passcode != bundle.Config.AdminConfig.Password { + failedLoginCounter.WithLabelValues("admin").Inc() + writeUnauthorizedResponse(w) + return + } + + err = setSignedTeamCookie(bundle, "admin", w) + if err != nil { + http.Error(w, "failed to sign team cookie", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "Signed in as admin"}`)) + loginCounter.WithLabelValues("login", "admin").Inc() +} + func getDeployment(context context.Context, bundle *bundle.Bundle, team string) (*appsv1.Deployment, error) { return bundle.ClientSet.AppsV1().Deployments(bundle.RuntimeEnvironment.Namespace).Get( context, diff --git a/balancer/routes/join_test.go b/balancer/routes/join_test.go index 5e1bd6aa..309cf385 100644 --- a/balancer/routes/join_test.go +++ b/balancer/routes/join_test.go @@ -201,8 +201,6 @@ func TestJoinHandler(t *testing.T) { server.ServeHTTP(rr, req) - actions := clientset.Actions() - _ = actions assert.Equal(t, http.StatusUnauthorized, rr.Code) assert.Equal(t, "", rr.Header().Get("Set-Cookie")) }) @@ -221,8 +219,6 @@ func TestJoinHandler(t *testing.T) { server.ServeHTTP(rr, req) - actions := clientset.Actions() - _ = actions assert.Equal(t, http.StatusOK, rr.Code) assert.Regexp(t, regexp.MustCompile(`team=foobar\..*; Path=/; HttpOnly; SameSite=Strict`), rr.Header().Get("Set-Cookie")) }) @@ -241,9 +237,71 @@ func TestJoinHandler(t *testing.T) { server.ServeHTTP(rr, req) - actions := clientset.Actions() - _ = actions assert.Equal(t, http.StatusUnauthorized, rr.Code) assert.Equal(t, "", rr.Header().Get("Set-Cookie")) }) + + t.Run("allows admins login with the correct passcode", func(t *testing.T) { + jsonPayload, _ := json.Marshal(map[string]string{"passcode": "mock-admin-password"}) + req, _ := http.NewRequest("POST", "/balancer/teams/admin/join", bytes.NewReader(jsonPayload)) + rr := httptest.NewRecorder() + + server := http.NewServeMux() + + bundle := testutil.NewTestBundle() + AddRoutes(server, bundle) + + server.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Regexp(t, regexp.MustCompile(`team=admin\..*; Path=/; HttpOnly; SameSite=Strict`), rr.Header().Get("Set-Cookie")) + }) + + t.Run("admin login returns usual 'requires auth' response when it get's no request body passed", func(t *testing.T) { + req, _ := http.NewRequest("POST", "/balancer/teams/admin/join", nil) + rr := httptest.NewRecorder() + + server := http.NewServeMux() + + bundle := testutil.NewTestBundle() + AddRoutes(server, bundle) + + server.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + assert.Equal(t, "", rr.Header().Get("Set-Cookie")) + }) + + t.Run("admin account requires the correct passcod", func(t *testing.T) { + jsonPayload, _ := json.Marshal(map[string]string{"passcode": "wrong-password"}) + req, _ := http.NewRequest("POST", "/balancer/teams/admin/join", bytes.NewReader(jsonPayload)) + rr := httptest.NewRecorder() + + server := http.NewServeMux() + + bundle := testutil.NewTestBundle() + AddRoutes(server, bundle) + + server.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + assert.Equal(t, "", rr.Header().Get("Set-Cookie")) + }) + + t.Run("admin login doesn't make any kubernetes api calls / creates not kubernetes resources", func(t *testing.T) { + jsonPayload, _ := json.Marshal(map[string]string{"passcode": "mock-admin-password"}) + req, _ := http.NewRequest("POST", "/balancer/teams/admin/join", bytes.NewReader(jsonPayload)) + rr := httptest.NewRecorder() + + server := http.NewServeMux() + + clientset := fake.NewSimpleClientset(balancerDeployment) + bundle := testutil.NewTestBundleWithCustomFakeClient(clientset) + AddRoutes(server, bundle) + + server.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Len(t, clientset.Actions(), 0) + }) }