From c7587fd3f057f287dff55c03bee024cea762ec09 Mon Sep 17 00:00:00 2001 From: FSJ <7189895+ShiftedMr@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:13:01 +0100 Subject: [PATCH] Add endpoint for allowing admins to force rotate a user's token (#3272) * Add endpoint for allowing admins to force rotate a user's token * Finishing a missed test for user not found, and finished test for db update error --- handler/api/api.go | 1 + handler/api/users/token.go | 51 +++++++++++++ handler/api/users/token_test.go | 131 ++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 handler/api/users/token.go create mode 100644 handler/api/users/token_test.go diff --git a/handler/api/api.go b/handler/api/api.go index 000afce8a..a2c7d800c 100644 --- a/handler/api/api.go +++ b/handler/api/api.go @@ -340,6 +340,7 @@ func (s Server) Handler() http.Handler { r.Post("/", users.HandleCreate(s.Users, s.Userz, s.Webhook)) r.Get("/{user}", users.HandleFind(s.Users)) r.Patch("/{user}", users.HandleUpdate(s.Users, s.Transferer)) + r.Post("/{user}/token/rotate", users.HandleTokenRotation(s.Users)) r.Delete("/{user}", users.HandleDelete(s.Users, s.Transferer, s.Webhook)) r.Get("/{user}/repos", users.HandleRepoList(s.Users, s.Repos)) }) diff --git a/handler/api/users/token.go b/handler/api/users/token.go new file mode 100644 index 000000000..b277483a4 --- /dev/null +++ b/handler/api/users/token.go @@ -0,0 +1,51 @@ +// Copyright 2019 Drone IO, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package users + +import ( + "net/http" + + "github.com/dchest/uniuri" + "github.com/drone/drone/core" + "github.com/drone/drone/handler/api/render" + "github.com/drone/drone/logger" + "github.com/go-chi/chi" +) + +type userWithMessage struct { + *core.User + Message string `json:"message"` +} + +// HandleToken returns an http.HandlerFunc that writes json-encoded +// account information to the http response body with the user token. +func HandleTokenRotation(users core.UserStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + login := chi.URLParam(r, "user") + user, err := users.FindLogin(r.Context(), login) + if err != nil { + render.NotFound(w, err) + logger.FromRequest(r).WithError(err). + Debugln("api: cannot find user") + return + } + user.Hash = uniuri.NewLen(32) + if err := users.Update(r.Context(), user); err != nil { + render.InternalError(w, err) + return + } + render.JSON(w, &userWithMessage{user, "Token rotated successfully."}, 200) + } +} diff --git a/handler/api/users/token_test.go b/handler/api/users/token_test.go new file mode 100644 index 000000000..1d40547ca --- /dev/null +++ b/handler/api/users/token_test.go @@ -0,0 +1,131 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Drone Non-Commercial License +// that can be found in the LICENSE file. + +package users + +import ( + "context" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/drone/drone/core" + "github.com/drone/drone/handler/api/errors" + "github.com/drone/drone/mock" + "github.com/go-chi/chi" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +// The purpose of this test is to make sure admins can rotate someone +// else's token. +func TestTokenRotate(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + startingHash := "MjAxOC0wOC0xMVQxNTo1ODowN1o" + mockUser := &core.User{ + ID: 1, + Login: "octocat", + Hash: startingHash, + } + + c := new(chi.Context) + c.URLParams.Add("user", "octocat") + + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/", nil) + r = r.WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, c), + ) + + users := mock.NewMockUserStore(controller) + users.EXPECT().FindLogin(gomock.Any(), mockUser.Login).Return(mockUser, nil) + users.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) + + HandleTokenRotation(users)(w, r) + if got, want := w.Code, 200; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := &userWithMessage{}, mockUser + json.NewDecoder(w.Body).Decode(got) + + ignore := cmpopts.IgnoreFields(core.User{}, "Hash") + if diff := cmp.Diff(got.User, want, ignore); len(diff) != 0 { + t.Errorf(diff) + } + if got.Message == "" { + t.Errorf("Expect Message returned") + } + if got, want := mockUser.Hash, startingHash; got == want { + t.Errorf("Expect user hash updated") + } +} + +// the purpose of this unit test is to verify we fail safely when a non existing user is provided +func TestToken_UserNotFound(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + startingHash := "MjAxOC0wOC0xMVQxNTo1ODowN1o" + mockUser := &core.User{ + ID: 1, + Login: "octocat", + Hash: startingHash, + } + c := new(chi.Context) + c.URLParams.Add("user", "octocat") + + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/?rotate=true", nil) + r = r.WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, c), + ) + + users := mock.NewMockUserStore(controller) + users.EXPECT().FindLogin(gomock.Any(), mockUser.Login).Return(mockUser, nil) + users.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errors.ErrNotFound) + + HandleTokenRotation(users)(w, r) + if got, want := w.Code, 500; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := new(errors.Error), errors.ErrNotFound + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) != 0 { + t.Errorf(diff) + } +} + +// the purpose of this unit test is to verify we fail safely when a non existing user is provided +func TestToken_UpdateError(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + c := new(chi.Context) + c.URLParams.Add("user", "octocat") + + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/?rotate=true", nil) + r = r.WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, c), + ) + + users := mock.NewMockUserStore(controller) + users.EXPECT().FindLogin(gomock.Any(), mockUser.Login).Return(nil, errors.ErrNotFound) + + HandleTokenRotation(users)(w, r) + if got, want := w.Code, 404; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := new(errors.Error), errors.ErrNotFound + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) != 0 { + t.Errorf(diff) + } +}