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
pull/3279/head
FSJ 2022-10-26 13:13:01 +01:00 committed by GitHub
parent 5792a1bd86
commit c7587fd3f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 183 additions and 0 deletions

View File

@ -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))
})

View File

@ -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)
}
}

View File

@ -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)
}
}