From 56b8772f13b06c69eb9c5e6c3c0c47019f25b251 Mon Sep 17 00:00:00 2001 From: John Murphy Date: Wed, 18 Jan 2017 00:43:59 +0800 Subject: [PATCH] Modify User (as a regular user) fails #891 (#959) --- Makefile | 1 + server/kolide/users.go | 6 + server/service/endpoint_appconfig.go | 6 +- server/service/endpoint_appconfig_test.go | 6 +- server/service/endpoint_import_config_test.go | 8 +- server/service/endpoint_middleware.go | 66 ----------- server/service/endpoint_middleware_test.go | 11 -- server/service/endpoint_options_test.go | 6 +- server/service/endpoint_test.go | 23 +++- server/service/endpoint_users.go | 46 ++++++++ server/service/endpoint_users_test.go | 105 ++++++++++++++++++ server/service/handler.go | 12 +- server/service/logging_users.go | 62 +++++++++++ server/service/metrics_users.go | 32 ++++++ server/service/service_users.go | 40 +++++-- server/service/transport_users.go | 26 +++++ 16 files changed, 354 insertions(+), 102 deletions(-) create mode 100644 server/service/endpoint_users_test.go diff --git a/Makefile b/Makefile index f5d9a40b32..65f528ac77 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,7 @@ deps: npm install go get github.com/jteeuwen/go-bindata/... go get github.com/Masterminds/glide + go get github.com/groob/mockimpl glide install distclean: diff --git a/server/kolide/users.go b/server/kolide/users.go index 2337070a00..486639f0dd 100644 --- a/server/kolide/users.go +++ b/server/kolide/users.go @@ -64,6 +64,12 @@ type UserService interface { // ModifyUser updates a user's parameters given a UserPayload. ModifyUser(ctx context.Context, userID uint, p UserPayload) (user *User, err error) + + // ChangeUserAdmin is used to modify the admin state of the user identified by id. + ChangeUserAdmin(ctx context.Context, id uint, isAdmin bool) (*User, error) + + // ChangeUserEnabled is used to enable/disable the user identified by id. + ChangeUserEnabled(ctx context.Context, id uint, isEnabled bool) (*User, error) } // User is the model struct which represents a kolide user diff --git a/server/service/endpoint_appconfig.go b/server/service/endpoint_appconfig.go index fdbb8b253b..5da052f828 100644 --- a/server/service/endpoint_appconfig.go +++ b/server/service/endpoint_appconfig.go @@ -32,6 +32,9 @@ func makeGetAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint { // only admin can see smtp settings if vc.IsAdmin() { smtpSettings = smtpSettingsFromAppConfig(config) + if smtpSettings.SMTPPassword != "" { + smtpSettings.SMTPPassword = "********" + } } response := appConfigResponse{ OrgInfo: &kolide.OrgInfo{ @@ -43,9 +46,6 @@ func makeGetAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint { }, SMTPSettings: smtpSettings, } - if response.SMTPSettings.SMTPPassword != "" { - response.SMTPSettings.SMTPPassword = "********" - } return response, nil } } diff --git a/server/service/endpoint_appconfig_test.go b/server/service/endpoint_appconfig_test.go index 129708b744..6060b17673 100644 --- a/server/service/endpoint_appconfig_test.go +++ b/server/service/endpoint_appconfig_test.go @@ -25,7 +25,7 @@ func testGetAppConfig(t *testing.T, r *testResource) { req, err := http.NewRequest("GET", r.server.URL+"/api/v1/kolide/config", nil) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) @@ -63,7 +63,7 @@ func testModifyAppConfig(t *testing.T, r *testResource) { require.Nil(t, err) req, err := http.NewRequest("PATCH", r.server.URL+"/api/v1/kolide/config", &buffer) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) @@ -92,7 +92,7 @@ func testModifyAppConfigWithValidationFail(t *testing.T, r *testResource) { require.Nil(t, err) req, err := http.NewRequest("PATCH", r.server.URL+"/api/v1/kolide/config", &buffer) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) diff --git a/server/service/endpoint_import_config_test.go b/server/service/endpoint_import_config_test.go index 12f83e047e..2dbc38588c 100644 --- a/server/service/endpoint_import_config_test.go +++ b/server/service/endpoint_import_config_test.go @@ -26,7 +26,7 @@ func testImportConfigWithGlob(t *testing.T, r *testResource) { buff := bytes.NewBufferString(testJSON) req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) @@ -48,7 +48,7 @@ func testImportConfigWithMissingGlob(t *testing.T, r *testResource) { buff := bytes.NewBufferString(testJSON) req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) @@ -73,7 +73,7 @@ func testImportConfig(t *testing.T, r *testResource) { buff := bytes.NewBufferString(testJSON) req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) @@ -93,7 +93,7 @@ func testImportConfigMissingExternal(t *testing.T, r *testResource) { buff := bytes.NewBufferString(testJSON) req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) diff --git a/server/service/endpoint_middleware.go b/server/service/endpoint_middleware.go index d555a82c43..de704bd686 100644 --- a/server/service/endpoint_middleware.go +++ b/server/service/endpoint_middleware.go @@ -180,72 +180,6 @@ const ( admin ) -func validateModifyUserRequest(next endpoint.Endpoint) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - r := request.(modifyUserRequest) - vc, ok := viewer.FromContext(ctx) - if !ok { - return nil, errNoContext - } - uid := requestUserIDFromContext(ctx) - p := r.payload - must := requireRoleForUserModification(p) - - var badArgs []invalidArgument - if !vc.IsAdmin() { - for _, field := range must[admin] { - badArgs = append(badArgs, invalidArgument{name: field, reason: "must be an admin"}) - } - } - if !vc.CanPerformWriteActionOnUser(uid) { - for _, field := range must[self] { - badArgs = append(badArgs, invalidArgument{name: field, reason: "no write permissions on user"}) - } - } - if len(badArgs) != 0 { - return nil, permissionError{badArgs: badArgs} - } - return next(ctx, request) - } -} - -// checks if fields were set in a user payload -// returns a map of updated fields for each role required -func requireRoleForUserModification(p kolide.UserPayload) map[permission][]string { - must := make(map[permission][]string) - adminFields := []string{} - if p.Enabled != nil { - adminFields = append(adminFields, "enabled") - } - if p.Admin != nil { - adminFields = append(adminFields, "admin") - } - if len(adminFields) != 0 { - must[admin] = adminFields - } - - selfFields := []string{} - if p.Username != nil { - selfFields = append(selfFields, "username") - } - if p.GravatarURL != nil { - selfFields = append(selfFields, "gravatar_url") - } - if p.Position != nil { - selfFields = append(selfFields, "position") - } - if p.Email != nil { - selfFields = append(selfFields, "email") - } - if p.Password != nil { - selfFields = append(selfFields, "password") - } - // self is always a must, otherwise - // anyone can edit the field, and we don't have that requirement - must[self] = selfFields - return must -} - func requestUserIDFromContext(ctx context.Context) uint { userID, ok := ctx.Value("request-id").(uint) if !ok { diff --git a/server/service/endpoint_middleware_test.go b/server/service/endpoint_middleware_test.go index 63b236ceb0..fde93e1d88 100644 --- a/server/service/endpoint_middleware_test.go +++ b/server/service/endpoint_middleware_test.go @@ -107,17 +107,6 @@ func TestEndpointPermissions(t *testing.T) { requestID: admin1.ID, wantErr: permissionError{message: "no read permissions on user"}, }, - { - endpoint: validateModifyUserRequest(e), - request: modifyUserRequest{}, - wantErr: errNoContext, - }, - { - endpoint: validateModifyUserRequest(e), - request: modifyUserRequest{payload: kolide.UserPayload{Enabled: boolPtr(true)}}, - vc: &viewer.Viewer{User: user1}, - wantErr: permissionError{message: "unauthorized: must be an admin"}, - }, } for _, tt := range endpointTests { diff --git a/server/service/endpoint_options_test.go b/server/service/endpoint_options_test.go index 399d1db996..da30c380e2 100644 --- a/server/service/endpoint_options_test.go +++ b/server/service/endpoint_options_test.go @@ -15,7 +15,7 @@ func testGetOptions(t *testing.T, r *testResource) { req, err := http.NewRequest("GET", r.server.URL+"/api/v1/kolide/options", nil) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) @@ -34,7 +34,7 @@ func testModifyOptions(t *testing.T, r *testResource) { buff := bytes.NewBufferString(inJson) req, err := http.NewRequest("PATCH", r.server.URL+"/api/v1/kolide/options", buff) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) @@ -55,7 +55,7 @@ func testModifyOptionsValidationFail(t *testing.T, r *testResource) { buff := bytes.NewBufferString(inJson) req, err := http.NewRequest("PATCH", r.server.URL+"/api/v1/kolide/options", buff) require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) client := &http.Client{} resp, err := client.Do(req) require.Nil(t, err) diff --git a/server/service/endpoint_test.go b/server/service/endpoint_test.go index 0cf7c21553..6e3e6edda9 100644 --- a/server/service/endpoint_test.go +++ b/server/service/endpoint_test.go @@ -22,9 +22,10 @@ import ( ) type testResource struct { - server *httptest.Server - userToken string - ds kolide.Datastore + server *httptest.Server + adminToken string + userToken string + ds kolide.Datastore } func setupEndpointTest(t *testing.T) *testResource { @@ -80,7 +81,19 @@ func setupEndpointTest(t *testing.T) *testResource { Err string `json:"error,omitempty"` }{} json.NewDecoder(resp.Body).Decode(&jsn) + test.adminToken = jsn.Token + + // log in non admin user + userParam.Username = "user1" + userParam.Password = testUsers["user1"].PlaintextPassword + marshalledUser, _ = json.Marshal(userParam) + requestBody = &nopCloser{bytes.NewBuffer(marshalledUser)} + resp, err = http.Post(test.server.URL+"/api/v1/kolide/login", "application/json", requestBody) + require.Nil(t, err) + err = json.NewDecoder(resp.Body).Decode(&jsn) + require.Nil(t, err) test.userToken = jsn.Token + return test } @@ -101,6 +114,10 @@ var testFunctions = [...]func(*testing.T, *testResource){ testImportConfigMissingExternal, testImportConfigWithMissingGlob, testImportConfigWithGlob, + testAdminUserSetAdmin, + testNonAdminUserSetAdmin, + testAdminUserSetEnabled, + testNonAdminUserSetEnabled, } func TestEndpoints(t *testing.T) { diff --git a/server/service/endpoint_users.go b/server/service/endpoint_users.go index 234b1370ba..e4ea5a0c16 100644 --- a/server/service/endpoint_users.go +++ b/server/service/endpoint_users.go @@ -60,6 +60,52 @@ func makeGetUserEndpoint(svc kolide.Service) endpoint.Endpoint { } } +type adminUserRequest struct { + ID uint `json:"id"` + Admin bool `json:"admin"` +} + +type adminUserResponse struct { + User *kolide.User `json:"user,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r adminUserResponse) error() error { return r.Err } + +func makeAdminUserEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(adminUserRequest) + user, err := svc.ChangeUserAdmin(ctx, req.ID, req.Admin) + if err != nil { + return adminUserResponse{Err: err}, nil + } + return adminUserResponse{User: user}, nil + } +} + +type enableUserRequest struct { + ID uint `json:"id"` + Enabled bool `json:"enabled"` +} + +type enableUserResponse struct { + User *kolide.User `json:"user,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r enableUserResponse) error() error { return r.Err } + +func makeEnableUserEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(enableUserRequest) + user, err := svc.ChangeUserEnabled(ctx, req.ID, req.Enabled) + if err != nil { + return enableUserResponse{Err: err}, nil + } + return enableUserResponse{User: user}, nil + } +} + func makeGetSessionUserEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { user, err := svc.AuthenticatedUser(ctx) diff --git a/server/service/endpoint_users_test.go b/server/service/endpoint_users_test.go new file mode 100644 index 0000000000..6a4f54eb15 --- /dev/null +++ b/server/service/endpoint_users_test.go @@ -0,0 +1,105 @@ +package service + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testAdminUserSetAdmin(t *testing.T, r *testResource) { + user, err := r.ds.User("user1") + require.Nil(t, err) + assert.False(t, user.Admin) + inJson := `{"admin":true}` + buff := bytes.NewBufferString(inJson) + path := fmt.Sprintf("/api/v1/kolide/users/%d/admin", user.ID) + req, err := http.NewRequest("POST", r.server.URL+path, buff) + require.Nil(t, err) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) + client := &http.Client{} + resp, err := client.Do(req) + require.Nil(t, err) + var actual adminUserResponse + err = json.NewDecoder(resp.Body).Decode(&actual) + require.Nil(t, err) + assert.Nil(t, actual.Err) + require.NotNil(t, actual.User) + assert.True(t, actual.User.Admin) + user, err = r.ds.User("user1") + require.Nil(t, err) + assert.True(t, user.Admin) +} + +func testNonAdminUserSetAdmin(t *testing.T, r *testResource) { + user, err := r.ds.User("user1") + require.Nil(t, err) + assert.False(t, user.Admin) + inJson := `{"admin":true}` + buff := bytes.NewBufferString(inJson) + path := fmt.Sprintf("/api/v1/kolide/users/%d/admin", user.ID) + req, err := http.NewRequest("POST", r.server.URL+path, buff) + require.Nil(t, err) + // user NOT admin + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + client := &http.Client{} + resp, err := client.Do(req) + require.Nil(t, err) + assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) + rb := make([]byte, 500) + resp.Body.Read(rb) + user, err = r.ds.User("user1") + require.Nil(t, err) + assert.False(t, user.Admin) +} + +func testAdminUserSetEnabled(t *testing.T, r *testResource) { + user, err := r.ds.User("user1") + require.Nil(t, err) + assert.True(t, user.Enabled) + inJson := `{"enabled":false}` + buff := bytes.NewBufferString(inJson) + path := fmt.Sprintf("/api/v1/kolide/users/%d/enable", user.ID) + req, err := http.NewRequest("POST", r.server.URL+path, buff) + require.Nil(t, err) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) + client := &http.Client{} + resp, err := client.Do(req) + require.Nil(t, err) + var actual adminUserResponse + err = json.NewDecoder(resp.Body).Decode(&actual) + require.Nil(t, err) + assert.Nil(t, actual.Err) + require.NotNil(t, actual.User) + assert.False(t, actual.User.Enabled) + user, err = r.ds.User("user1") + require.Nil(t, err) + assert.False(t, user.Enabled) +} + +func testNonAdminUserSetEnabled(t *testing.T, r *testResource) { + user, err := r.ds.User("user1") + require.Nil(t, err) + assert.True(t, user.Enabled) + inJson := `{"enabled":false}` + buff := bytes.NewBufferString(inJson) + path := fmt.Sprintf("/api/v1/kolide/users/%d/enable", user.ID) + req, err := http.NewRequest("POST", r.server.URL+path, buff) + require.Nil(t, err) + // user NOT admin + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken)) + client := &http.Client{} + resp, err := client.Do(req) + require.Nil(t, err) + assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) + rb := make([]byte, 500) + resp.Body.Read(rb) + user, err = r.ds.User("user1") + require.Nil(t, err) + // shouldn't change + assert.True(t, user.Enabled) +} diff --git a/server/service/handler.go b/server/service/handler.go index 1742dfe97d..06b1fdc717 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -25,6 +25,8 @@ type KolideEndpoints struct { GetUser endpoint.Endpoint ListUsers endpoint.Endpoint ModifyUser endpoint.Endpoint + AdminUser endpoint.Endpoint + EnableUser endpoint.Endpoint RequirePasswordReset endpoint.Endpoint PerformRequiredPasswordReset endpoint.Endpoint GetSessionsForUserInfo endpoint.Endpoint @@ -94,7 +96,9 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint ChangePassword: authenticatedUser(jwtKey, svc, canPerformActions(makeChangePasswordEndpoint(svc))), GetUser: authenticatedUser(jwtKey, svc, canReadUser(makeGetUserEndpoint(svc))), ListUsers: authenticatedUser(jwtKey, svc, canPerformActions(makeListUsersEndpoint(svc))), - ModifyUser: authenticatedUser(jwtKey, svc, validateModifyUserRequest(makeModifyUserEndpoint(svc))), + ModifyUser: authenticatedUser(jwtKey, svc, canPerformActions(makeModifyUserEndpoint(svc))), + AdminUser: authenticatedUser(jwtKey, svc, mustBeAdmin(makeAdminUserEndpoint(svc))), + EnableUser: authenticatedUser(jwtKey, svc, mustBeAdmin(makeEnableUserEndpoint(svc))), RequirePasswordReset: authenticatedUser(jwtKey, svc, mustBeAdmin(makeRequirePasswordResetEndpoint(svc))), // PerformRequiredPasswordReset needs only to authenticate the // logged in user @@ -158,6 +162,8 @@ type kolideHandlers struct { GetUser http.Handler ListUsers http.Handler ModifyUser http.Handler + AdminUser http.Handler + EnableUser http.Handler RequirePasswordReset http.Handler PerformRequiredPasswordReset http.Handler GetSessionsForUserInfo http.Handler @@ -223,6 +229,8 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt ModifyUser: newServer(e.ModifyUser, decodeModifyUserRequest), RequirePasswordReset: newServer(e.RequirePasswordReset, decodeRequirePasswordResetRequest), PerformRequiredPasswordReset: newServer(e.PerformRequiredPasswordReset, decodePerformRequiredPasswordResetRequest), + EnableUser: newServer(e.EnableUser, decodeEnableUserRequest), + AdminUser: newServer(e.AdminUser, decodeAdminUserRequest), GetSessionsForUserInfo: newServer(e.GetSessionsForUserInfo, decodeGetInfoAboutSessionsForUserRequest), DeleteSessionsForUser: newServer(e.DeleteSessionsForUser, decodeDeleteSessionsForUserRequest), GetSessionInfo: newServer(e.GetSessionInfo, decodeGetInfoAboutSessionRequest), @@ -320,6 +328,8 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/kolide/users", h.CreateUser).Methods("POST").Name("create_user") r.Handle("/api/v1/kolide/users/{id}", h.GetUser).Methods("GET").Name("get_user") r.Handle("/api/v1/kolide/users/{id}", h.ModifyUser).Methods("PATCH").Name("modify_user") + r.Handle("/api/v1/kolide/users/{id}/enable", h.EnableUser).Methods("POST").Name("enable_user") + r.Handle("/api/v1/kolide/users/{id}/admin", h.AdminUser).Methods("POST").Name("admin_user") r.Handle("/api/v1/kolide/users/{id}/require_password_reset", h.RequirePasswordReset).Methods("POST").Name("require_password_reset") r.Handle("/api/v1/kolide/users/{id}/sessions", h.GetSessionsForUserInfo).Methods("GET").Name("get_session_for_user") r.Handle("/api/v1/kolide/users/{id}/sessions", h.DeleteSessionsForUser).Methods("DELETE").Name("delete_session_for_user") diff --git a/server/service/logging_users.go b/server/service/logging_users.go index e3d5a896ff..efed6b0cca 100644 --- a/server/service/logging_users.go +++ b/server/service/logging_users.go @@ -8,6 +8,68 @@ import ( "golang.org/x/net/context" ) +func (mw loggingMiddleware) ChangeUserAdmin(ctx context.Context, id uint, isAdmin bool) (*kolide.User, error) { + var ( + loggedInUser = "unauthenticated" + userName = "none" + err error + user *kolide.User + ) + + vc, ok := viewer.FromContext(ctx) + if ok { + loggedInUser = vc.Username() + } + + defer func(begin time.Time) { + _ = mw.logger.Log( + "method", "ChangeUserAdmin", + "user", userName, + "changed_by", loggedInUser, + "admin", isAdmin, + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + + user, err = mw.Service.ChangeUserAdmin(ctx, id, isAdmin) + if user != nil { + userName = user.Username + } + return user, err +} + +func (mw loggingMiddleware) ChangeUserEnabled(ctx context.Context, id uint, isEnabled bool) (*kolide.User, error) { + var ( + loggedInUser = "unauthenticated" + userName = "none" + err error + user *kolide.User + ) + + vc, ok := viewer.FromContext(ctx) + if ok { + loggedInUser = vc.Username() + } + + defer func(begin time.Time) { + _ = mw.logger.Log( + "method", "ChangeUserEnabled", + "user", userName, + "changed_by", loggedInUser, + "enabled", isEnabled, + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + + user, err = mw.Service.ChangeUserEnabled(ctx, id, isEnabled) + if user != nil { + userName = user.Username + } + return user, err +} + func (mw loggingMiddleware) NewAdminCreatedUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { var ( user *kolide.User diff --git a/server/service/metrics_users.go b/server/service/metrics_users.go index 4e87b2b1bb..e52fa0e36a 100644 --- a/server/service/metrics_users.go +++ b/server/service/metrics_users.go @@ -8,6 +8,38 @@ import ( "golang.org/x/net/context" ) +func (mw metricsMiddleware) ChangeUserAdmin(ctx context.Context, id uint, isAdmin bool) (*kolide.User, error) { + var ( + user *kolide.User + err error + ) + + defer func(begin time.Time) { + lvs := []string{"method", "ChangeUserAdmin", "error", fmt.Sprint(err != nil)} + mw.requestCount.With(lvs...).Add(1) + mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) + }(time.Now()) + + user, err = mw.Service.ChangeUserAdmin(ctx, id, isAdmin) + return user, err +} + +func (mw metricsMiddleware) ChangeUserEnabled(ctx context.Context, id uint, isEnabled bool) (*kolide.User, error) { + var ( + user *kolide.User + err error + ) + + defer func(begin time.Time) { + lvs := []string{"method", "ChangeUserEnabled", "error", fmt.Sprint(err != nil)} + mw.requestCount.With(lvs...).Add(1) + mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) + }(time.Now()) + + user, err = mw.Service.ChangeUserEnabled(ctx, id, isEnabled) + return user, err +} + func (mw metricsMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { var ( user *kolide.User diff --git a/server/service/service_users.go b/server/service/service_users.go index 97463f42c6..616d3cb2f8 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -49,6 +49,30 @@ func (svc service) newUser(p kolide.UserPayload) (*kolide.User, error) { return user, nil } +func (svc service) ChangeUserAdmin(ctx context.Context, id uint, isAdmin bool) (*kolide.User, error) { + user, err := svc.ds.UserByID(id) + if err != nil { + return nil, err + } + user.Admin = isAdmin + if err = svc.saveUser(user); err != nil { + return nil, err + } + return user, nil +} + +func (svc service) ChangeUserEnabled(ctx context.Context, id uint, isEnabled bool) (*kolide.User, error) { + user, err := svc.ds.UserByID(id) + if err != nil { + return nil, err + } + user.Enabled = isEnabled + if err = svc.saveUser(user); err != nil { + return nil, err + } + return user, nil +} + func (svc service) ModifyUser(ctx context.Context, userID uint, p kolide.UserPayload) (*kolide.User, error) { user, err := svc.User(ctx, userID) if err != nil { @@ -57,6 +81,14 @@ func (svc service) ModifyUser(ctx context.Context, userID uint, p kolide.UserPay // the method assumes that the correct authorization // has been validated higher up the stack + if p.Admin != nil { + user.Admin = *p.Admin + } + + if p.Enabled != nil { + user.Enabled = *p.Enabled + } + if p.Username != nil { user.Username = *p.Username } @@ -65,18 +97,10 @@ func (svc service) ModifyUser(ctx context.Context, userID uint, p kolide.UserPay user.Name = *p.Name } - if p.Admin != nil { - user.Admin = *p.Admin - } - if p.Email != nil { user.Email = *p.Email } - if p.Enabled != nil { - user.Enabled = *p.Enabled - } - if p.Position != nil { user.Position = *p.Position } diff --git a/server/service/transport_users.go b/server/service/transport_users.go index 06613e9f1d..526b12c19f 100644 --- a/server/service/transport_users.go +++ b/server/service/transport_users.go @@ -8,6 +8,32 @@ import ( "golang.org/x/net/context" ) +func decodeEnableUserRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + var req enableUserRequest + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + req.ID = id + return req, nil +} + +func decodeAdminUserRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + var req adminUserRequest + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + req.ID = id + return req, nil +} + func decodeCreateUserRequest(ctx context.Context, r *http.Request) (interface{}, error) { var req createUserRequest if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil {