From 7dc94e459869afc4d85764493030aa62bddc6afa Mon Sep 17 00:00:00 2001 From: Chris Trott <908409+trotttrotttrott@users.noreply.github.com> Date: Sat, 25 Jul 2020 14:16:13 -0600 Subject: [PATCH 1/4] Create additional struct for User collection API endpoints that return a collection of users do not structure user objects the same as those that return a single user. Collections of users intentionally have reduced fields. They are different structs in the Grafana codebase: UserProfileDTO & UserSearchHitDTO. They should be treated as differnt structs here as well. --- user.go | 81 ++++++++++++++++++++++++++++++---------------------- user_test.go | 52 +++++++++++++++++++++------------ 2 files changed, 80 insertions(+), 53 deletions(-) diff --git a/user.go b/user.go index 83a66205..b56149f4 100644 --- a/user.go +++ b/user.go @@ -1,50 +1,63 @@ package gapi import ( + "fmt" "net/url" + "time" ) -// User represents a Grafana user. +// User represents a Grafana user. It is structured after the UserProfileDTO +// struct in the Grafana codebase. type User struct { - Id int64 `json:"id,omitempty"` - Email string `json:"email,omitempty"` - Name string `json:"name,omitempty"` - Login string `json:"login,omitempty"` - Password string `json:"password,omitempty"` - IsAdmin bool `json:"isAdmin,omitempty"` + Id int64 `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Login string `json:"login,omitempty"` + Theme string `json:"theme,omitempty"` + OrgId int64 `json:"orgId,omitempty"` + IsAdmin bool `json:"isGrafanaAdmin,omitempty"` + IsDisabled bool `json:"isDisabled,omitempty"` + IsExternal bool `json:"isExternal,omitempty"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` + AuthLabels []string `json:"authLabels,omitempty"` + AvatarUrl string `json:"avatarUrl,omitempty"` + Password string `json:"password,omitempty"` +} + +// UserSearch represents a Grafana user as returned by API endpoints that +// return a collection of Grafana users. This representation of user has +// reduced and differing fields. It is structured after the UserSearchHitDTO +// struct in the Grafana codebase. +type UserSearch struct { + Id int64 `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Login string `json:"login,omitempty"` + IsAdmin bool `json:"isAdmin,omitempty"` + IsDisabled bool `json:"isDisabled,omitempty"` + LastSeenAt time.Time `json:"lastSeenAt,omitempty"` + LastSeenAtAge string `json:"lastSeenAtAge,omitempty"` + AuthLabels []string `json:"authLabels,omitempty"` + AvatarUrl string `json:"avatarUrl,omitempty"` } // Users fetches and returns Grafana users. -func (c *Client) Users() ([]User, error) { - users := make([]User, 0) - err := c.request("GET", "/api/users", nil, nil, &users) - if err != nil { - return users, err - } +func (c *Client) Users() (users []UserSearch, err error) { + err = c.request("GET", "/api/users", nil, nil, &users) + return +} - return users, err +// User fetches a user by ID. +func (c *Client) User(id int64) (user User, err error) { + err = c.request("GET", fmt.Sprintf("/api/users/%d", id), nil, nil, &user) + return } -// UserByEmail fetches and returns the user whose email matches that passed. -func (c *Client) UserByEmail(email string) (User, error) { - user := User{} +// UserByEmail fetches a user by email address. +func (c *Client) UserByEmail(email string) (user User, err error) { query := url.Values{} query.Add("loginOrEmail", email) - tmp := struct { - Id int64 `json:"id,omitempty"` - Email string `json:"email,omitempty"` - Name string `json:"name,omitempty"` - Login string `json:"login,omitempty"` - Password string `json:"password,omitempty"` - IsAdmin bool `json:"isGrafanaAdmin,omitempty"` - }{} - - err := c.request("GET", "/api/users/lookup", query, nil, &tmp) - if err != nil { - return user, err - } - - user = User(tmp) - - return user, err + err = c.request("GET", "/api/users/lookup", query, nil, &user) + return } diff --git a/user_test.go b/user_test.go index be64dcf7..214e700a 100644 --- a/user_test.go +++ b/user_test.go @@ -7,8 +7,9 @@ import ( ) const ( - getUsersJSON = `[{"id":1,"name":"","login":"admin","email":"admin@localhost","avatarUrl":"/avatar/46d229b033af06a191ff2267bca9ae56","isAdmin":true,"lastSeenAt":"2018-06-28T14:42:24Z","lastSeenAtAge":"\u003c 1m"}]` - getUserByEmailJSON = `{"id":1,"email":"admin@localhost","name":"","login":"admin","theme":"","orgId":1,"isGrafanaAdmin":true}` + getUsersJSON = `[{"id":1,"email":"users@localhost","isAdmin":true}]` + getUserJSON = `{"id":2,"email":"user@localhost","isGrafanaAdmin":false}` + getUserByEmailJSON = `{"id":3,"email":"userByEmail@localhost","isGrafanaAdmin":true}` ) func TestUsers(t *testing.T) { @@ -22,38 +23,51 @@ func TestUsers(t *testing.T) { t.Log(pretty.PrettyFormat(resp)) - user := User{ - Id: 1, - Email: "admin@localhost", - Name: "", - Login: "admin", - IsAdmin: true, + if len(resp) != 1 { + t.Fatal("No users were returned.") } - if len(resp) != 1 || resp[0] != user { + user := resp[0] + + if user.Email != "users@localhost" || + user.Id != 1 || + user.IsAdmin != true { t.Error("Not correctly parsing returned users.") } } +func TestUser(t *testing.T) { + server, client := gapiTestTools(200, getUserJSON) + defer server.Close() + + user, err := client.User(1) + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(user)) + + if user.Email != "user@localhost" || + user.Id != 2 || + user.IsAdmin != false { + t.Error("Not correctly parsing returned user.") + } +} + func TestUserByEmail(t *testing.T) { server, client := gapiTestTools(200, getUserByEmailJSON) defer server.Close() - resp, err := client.UserByEmail("admin@localhost") + user, err := client.UserByEmail("admin@localhost") if err != nil { t.Error(err) } - t.Log(pretty.PrettyFormat(resp)) + t.Log(pretty.PrettyFormat(user)) - user := User{ - Id: 1, - Email: "admin@localhost", - Name: "", - Login: "admin", - IsAdmin: true, - } - if resp != user { + if user.Email != "userByEmail@localhost" || + user.Id != 3 || + user.IsAdmin != true { t.Error("Not correctly parsing returned user.") } } From 243d7e774515473ebd99278c290cf7adc0d77dba Mon Sep 17 00:00:00 2001 From: Chris Trott <908409+trotttrotttrott@users.noreply.github.com> Date: Sat, 25 Jul 2020 18:34:35 -0600 Subject: [PATCH 2/4] UserUpdate function --- user.go | 11 +++++++++++ user_test.go | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/user.go b/user.go index b56149f4..1c88a09a 100644 --- a/user.go +++ b/user.go @@ -1,6 +1,8 @@ package gapi import ( + "bytes" + "encoding/json" "fmt" "net/url" "time" @@ -61,3 +63,12 @@ func (c *Client) UserByEmail(email string) (user User, err error) { err = c.request("GET", "/api/users/lookup", query, nil, &user) return } + +// UserUpdate updates a user by ID. +func (c *Client) UserUpdate(u User) error { + data, err := json.Marshal(u) + if err != nil { + return err + } + return c.request("PUT", fmt.Sprintf("/api/users/%d", u.Id), nil, bytes.NewBuffer(data), nil) +} diff --git a/user_test.go b/user_test.go index 214e700a..f4303eff 100644 --- a/user_test.go +++ b/user_test.go @@ -10,6 +10,7 @@ const ( getUsersJSON = `[{"id":1,"email":"users@localhost","isAdmin":true}]` getUserJSON = `{"id":2,"email":"user@localhost","isGrafanaAdmin":false}` getUserByEmailJSON = `{"id":3,"email":"userByEmail@localhost","isGrafanaAdmin":true}` + getUserUpdateJSON = `{"id":4,"email":"userUpdate@localhost","isGrafanaAdmin":false}` ) func TestUsers(t *testing.T) { @@ -71,3 +72,18 @@ func TestUserByEmail(t *testing.T) { t.Error("Not correctly parsing returned user.") } } + +func TestUserUpdate(t *testing.T) { + server, client := gapiTestTools(200, getUserUpdateJSON) + defer server.Close() + + user, err := client.User(4) + if err != nil { + t.Error(err) + } + user.IsAdmin = true + err = client.UserUpdate(user) + if err != nil { + t.Error(err) + } +} From 96f0664c12dd4504f804987a35c54105921dd765 Mon Sep 17 00:00:00 2001 From: Chris Trott <908409+trotttrotttrott@users.noreply.github.com> Date: Mon, 27 Jul 2020 13:42:42 -0600 Subject: [PATCH 3/4] UpdateUserPassword function --- admin.go | 10 ++++++++++ admin_test.go | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/admin.go b/admin.go index 9e3a4dd5..955773df 100644 --- a/admin.go +++ b/admin.go @@ -38,6 +38,16 @@ func (c *Client) DeleteUser(id int64) error { return c.request("DELETE", fmt.Sprintf("/api/admin/users/%d", id), nil, nil, nil) } +// UpdateUserPassword updates a user password. +func (c *Client) UpdateUserPassword(id int64, password string) error { + body := map[string]string{"password": password} + data, err := json.Marshal(body) + if err != nil { + return err + } + return c.request("PUT", fmt.Sprintf("/api/admin/users/%d/password", id), nil, bytes.NewBuffer(data), nil) +} + // PauseAllAlerts pauses all Grafana alerts. func (c *Client) PauseAllAlerts() (PauseAllAlertsResponse, error) { result := PauseAllAlertsResponse{} diff --git a/admin_test.go b/admin_test.go index 97ce5a61..c5e1d7d5 100644 --- a/admin_test.go +++ b/admin_test.go @@ -8,8 +8,9 @@ import ( ) const ( - createUserJSON = `{"id":1,"message":"User created"}` - deleteUserJSON = `{"message":"User deleted"}` + createUserJSON = `{"id":1,"message":"User created"}` + deleteUserJSON = `{"message":"User deleted"}` + updateUserPasswordJSON = `{"message":"User password updated"}` pauseAllAlertsJSON = `{ "alertsAffected": 1, @@ -47,6 +48,16 @@ func TestDeleteUser(t *testing.T) { } } +func TestUpdateUserPassword(t *testing.T) { + server, client := gapiTestTools(200, updateUserPasswordJSON) + defer server.Close() + + err := client.UpdateUserPassword(int64(1), "new-password") + if err != nil { + t.Error(err) + } +} + func TestPauseAllAlerts(t *testing.T) { server, client := gapiTestTools(200, pauseAllAlertsJSON) defer server.Close() From 8a2ef07969c8c8ea97219e3b809f52a5ef08b6d2 Mon Sep 17 00:00:00 2001 From: Chris Trott <908409+trotttrotttrott@users.noreply.github.com> Date: Mon, 27 Jul 2020 13:57:53 -0600 Subject: [PATCH 4/4] UpdateUserPermissions function --- admin.go | 10 ++++++++++ admin_test.go | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/admin.go b/admin.go index 955773df..f36c3a3d 100644 --- a/admin.go +++ b/admin.go @@ -48,6 +48,16 @@ func (c *Client) UpdateUserPassword(id int64, password string) error { return c.request("PUT", fmt.Sprintf("/api/admin/users/%d/password", id), nil, bytes.NewBuffer(data), nil) } +// UpdateUserPermissions sets a user's admin status. +func (c *Client) UpdateUserPermissions(id int64, isAdmin bool) error { + body := map[string]bool{"isGrafanaAdmin": isAdmin} + data, err := json.Marshal(body) + if err != nil { + return err + } + return c.request("PUT", fmt.Sprintf("/api/admin/users/%d/permissions", id), nil, bytes.NewBuffer(data), nil) +} + // PauseAllAlerts pauses all Grafana alerts. func (c *Client) PauseAllAlerts() (PauseAllAlertsResponse, error) { result := PauseAllAlertsResponse{} diff --git a/admin_test.go b/admin_test.go index c5e1d7d5..b361a0b9 100644 --- a/admin_test.go +++ b/admin_test.go @@ -8,9 +8,10 @@ import ( ) const ( - createUserJSON = `{"id":1,"message":"User created"}` - deleteUserJSON = `{"message":"User deleted"}` - updateUserPasswordJSON = `{"message":"User password updated"}` + createUserJSON = `{"id":1,"message":"User created"}` + deleteUserJSON = `{"message":"User deleted"}` + updateUserPasswordJSON = `{"message":"User password updated"}` + updateUserPermissionsJSON = `{"message":"User permissions updated"}` pauseAllAlertsJSON = `{ "alertsAffected": 1, @@ -58,6 +59,16 @@ func TestUpdateUserPassword(t *testing.T) { } } +func TestUpdateUserPermissions(t *testing.T) { + server, client := gapiTestTools(200, updateUserPermissionsJSON) + defer server.Close() + + err := client.UpdateUserPermissions(int64(1), false) + if err != nil { + t.Error(err) + } +} + func TestPauseAllAlerts(t *testing.T) { server, client := gapiTestTools(200, pauseAllAlertsJSON) defer server.Close()