Update user/role responses to return empty arrays
parent
0c9db03bef
commit
44aa0526ed
|
@ -21,7 +21,8 @@ func (c *UserStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.Us
|
|||
if err := c.Ctrl.SetUserPerms(ctx, u.Name, perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
|
||||
return c.Get(ctx, u.Name)
|
||||
}
|
||||
|
||||
// Delete the User from Influx Enterprise
|
||||
|
|
|
@ -36,6 +36,22 @@ func TestClient_Add(t *testing.T) {
|
|||
setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
|
||||
return nil
|
||||
},
|
||||
user: func(ctx context.Context, name string) (*enterprise.User, error) {
|
||||
return &enterprise.User{
|
||||
Name: "marty",
|
||||
Password: "johnny be good",
|
||||
Permissions: map[string][]string{
|
||||
"": {
|
||||
"ViewChronograf",
|
||||
"ReadData",
|
||||
"WriteData",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) {
|
||||
return map[string]enterprise.Roles{}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
|
@ -46,8 +62,14 @@ func TestClient_Add(t *testing.T) {
|
|||
},
|
||||
},
|
||||
want: &chronograf.User{
|
||||
Name: "marty",
|
||||
Passwd: "johnny be good",
|
||||
Name: "marty",
|
||||
Permissions: chronograf.Permissions{
|
||||
{
|
||||
Scope: chronograf.AllScope,
|
||||
Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"},
|
||||
},
|
||||
},
|
||||
Roles: []chronograf.Role{},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -17,7 +17,7 @@ func (c *Client) Add(ctx context.Context, u *chronograf.User) (*chronograf.User,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
return c.Get(ctx, u.Name)
|
||||
}
|
||||
|
||||
// Delete the User from InfluxDB
|
||||
|
|
|
@ -97,12 +97,12 @@ func TestClient_Add(t *testing.T) {
|
|||
u *chronograf.User
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
status int
|
||||
want *chronograf.User
|
||||
wantQuery string
|
||||
wantErr bool
|
||||
name string
|
||||
args args
|
||||
status int
|
||||
want *chronograf.User
|
||||
wantQueries []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Create User",
|
||||
|
@ -114,10 +114,22 @@ func TestClient_Add(t *testing.T) {
|
|||
Passwd: "Dont Need Roads",
|
||||
},
|
||||
},
|
||||
wantQuery: `CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
|
||||
wantQueries: []string{
|
||||
`CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
|
||||
`SHOW USERS`,
|
||||
`SHOW GRANTS FOR "docbrown"`,
|
||||
},
|
||||
want: &chronograf.User{
|
||||
Name: "docbrown",
|
||||
Passwd: "Dont Need Roads",
|
||||
Name: "docbrown",
|
||||
Permissions: chronograf.Permissions{
|
||||
chronograf.Permission{
|
||||
Scope: chronograf.AllScope,
|
||||
Allowed: chronograf.Allowances{
|
||||
"WRITE",
|
||||
"READ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -130,19 +142,19 @@ func TestClient_Add(t *testing.T) {
|
|||
Passwd: "Dont Need Roads",
|
||||
},
|
||||
},
|
||||
wantQuery: `CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
|
||||
wantErr: true,
|
||||
wantQueries: []string{`CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
query := ""
|
||||
queries := []string{}
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if path := r.URL.Path; path != "/query" {
|
||||
t.Error("Expected the path to contain `/query` but was", path)
|
||||
}
|
||||
query = r.URL.Query().Get("q")
|
||||
queries = append(queries, r.URL.Query().Get("q"))
|
||||
rw.WriteHeader(tt.status)
|
||||
rw.Write([]byte(`{"results":[{}]}`))
|
||||
rw.Write([]byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`))
|
||||
}))
|
||||
u, _ := url.Parse(ts.URL)
|
||||
c := &Client{
|
||||
|
@ -155,9 +167,16 @@ func TestClient_Add(t *testing.T) {
|
|||
t.Errorf("%q. Client.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if tt.wantQuery != query {
|
||||
t.Errorf("%q. Client.Add() query = %v, want %v", tt.name, query, tt.wantQuery)
|
||||
if len(tt.wantQueries) != len(queries) {
|
||||
t.Errorf("%q. Client.Add() queries = %v, want %v", tt.name, queries, tt.wantQueries)
|
||||
continue
|
||||
}
|
||||
for i := range tt.wantQueries {
|
||||
if tt.wantQueries[i] != queries[i] {
|
||||
t.Errorf("%q. Client.Add() query = %v, want %v", tt.name, queries[i], tt.wantQueries[i])
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. Client.Add() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
|
|
545
server/admin.go
545
server/admin.go
|
@ -1,545 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func validPermissions(perms *chronograf.Permissions) error {
|
||||
if perms == nil {
|
||||
return nil
|
||||
}
|
||||
for _, perm := range *perms {
|
||||
if perm.Scope != chronograf.AllScope && perm.Scope != chronograf.DBScope {
|
||||
return fmt.Errorf("Invalid permission scope")
|
||||
}
|
||||
if perm.Scope == chronograf.DBScope && perm.Name == "" {
|
||||
return fmt.Errorf("Database scoped permission requires a name")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type sourceUserRequest struct {
|
||||
Username string `json:"name,omitempty"` // Username for new account
|
||||
Password string `json:"password,omitempty"` // Password for new account
|
||||
Permissions chronograf.Permissions `json:"permissions,omitempty"` // Optional permissions
|
||||
}
|
||||
|
||||
func (r *sourceUserRequest) ValidCreate() error {
|
||||
if r.Username == "" {
|
||||
return fmt.Errorf("Username required")
|
||||
}
|
||||
if r.Password == "" {
|
||||
return fmt.Errorf("Password required")
|
||||
}
|
||||
return validPermissions(&r.Permissions)
|
||||
}
|
||||
|
||||
func (r *sourceUserRequest) ValidUpdate() error {
|
||||
if r.Password == "" && len(r.Permissions) == 0 {
|
||||
return fmt.Errorf("No fields to update")
|
||||
}
|
||||
return validPermissions(&r.Permissions)
|
||||
}
|
||||
|
||||
type sourceUser struct {
|
||||
Username string `json:"name,omitempty"` // Username for new account
|
||||
Permissions chronograf.Permissions `json:"permissions,omitempty"` // Account's permissions
|
||||
Roles []roleResponse `json:"roles,omitempty"` // Roles if source uses them
|
||||
Links selfLinks `json:"links"` // Links are URI locations related to user
|
||||
}
|
||||
|
||||
type selfLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
}
|
||||
|
||||
func newSelfLinks(id int, parent, resource string) selfLinks {
|
||||
httpAPISrcs := "/chronograf/v1/sources"
|
||||
u := &url.URL{Path: resource}
|
||||
encodedResource := u.String()
|
||||
return selfLinks{
|
||||
Self: fmt.Sprintf("%s/%d/%s/%s", httpAPISrcs, id, parent, encodedResource),
|
||||
}
|
||||
}
|
||||
|
||||
// NewSourceUser adds user to source
|
||||
func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req sourceUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
if err := req.ValidCreate(); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
srcID, store, err := h.sourceUsersStore(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user := &chronograf.User{
|
||||
Name: req.Username,
|
||||
Passwd: req.Password,
|
||||
}
|
||||
|
||||
res, err := store.Add(ctx, user)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
su := sourceUser{
|
||||
Username: res.Name,
|
||||
Permissions: req.Permissions,
|
||||
Links: newSelfLinks(srcID, "users", res.Name),
|
||||
}
|
||||
w.Header().Add("Location", su.Links.Self)
|
||||
encodeJSON(w, http.StatusCreated, su, h.Logger)
|
||||
}
|
||||
|
||||
type sourceUsers struct {
|
||||
Users []sourceUser `json:"users"`
|
||||
}
|
||||
|
||||
// SourceUsers retrieves all users from source.
|
||||
func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
srcID, store, err := h.sourceUsersStore(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
users, err := store.All(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
su := []sourceUser{}
|
||||
for _, u := range users {
|
||||
res := sourceUser{
|
||||
Username: u.Name,
|
||||
Permissions: u.Permissions,
|
||||
Links: newSelfLinks(srcID, "users", u.Name),
|
||||
}
|
||||
if len(u.Roles) > 0 {
|
||||
rr := make([]roleResponse, len(u.Roles))
|
||||
for i, role := range u.Roles {
|
||||
rr[i] = newRoleResponse(srcID, &role)
|
||||
}
|
||||
res.Roles = rr
|
||||
}
|
||||
su = append(su, res)
|
||||
}
|
||||
|
||||
res := sourceUsers{
|
||||
Users: su,
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
// SourceUserID retrieves a user with ID from store.
|
||||
func (h *Service) SourceUserID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
uid := httprouter.GetParamFromContext(ctx, "uid")
|
||||
|
||||
srcID, store, err := h.sourceUsersStore(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := store.Get(ctx, uid)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := sourceUser{
|
||||
Username: u.Name,
|
||||
Permissions: u.Permissions,
|
||||
Links: newSelfLinks(srcID, "users", u.Name),
|
||||
}
|
||||
if len(u.Roles) > 0 {
|
||||
rr := make([]roleResponse, len(u.Roles))
|
||||
for i, role := range u.Roles {
|
||||
rr[i] = newRoleResponse(srcID, &role)
|
||||
}
|
||||
res.Roles = rr
|
||||
}
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
// RemoveSourceUser removes the user from the InfluxDB source
|
||||
func (h *Service) RemoveSourceUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
uid := httprouter.GetParamFromContext(ctx, "uid")
|
||||
|
||||
_, store, err := h.sourceUsersStore(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.Delete(ctx, &chronograf.User{Name: uid}); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UpdateSourceUser changes the password or permissions of a source user
|
||||
func (h *Service) UpdateSourceUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req sourceUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
if err := req.ValidUpdate(); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
uid := httprouter.GetParamFromContext(ctx, "uid")
|
||||
srcID, store, err := h.sourceUsersStore(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user := &chronograf.User{
|
||||
Name: uid,
|
||||
Passwd: req.Password,
|
||||
Permissions: req.Permissions,
|
||||
}
|
||||
|
||||
if err := store.Update(ctx, user); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
su := sourceUser{
|
||||
Username: user.Name,
|
||||
Permissions: user.Permissions,
|
||||
Links: newSelfLinks(srcID, "users", user.Name),
|
||||
}
|
||||
w.Header().Add("Location", su.Links.Self)
|
||||
encodeJSON(w, http.StatusOK, su, h.Logger)
|
||||
}
|
||||
|
||||
func (h *Service) sourcesSeries(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.TimeSeries, error) {
|
||||
srcID, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||
if err != nil {
|
||||
notFound(w, srcID, h.Logger)
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
ts, err := h.TimeSeries(src)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if err = ts.Connect(ctx, &src); err != nil {
|
||||
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||
return 0, nil, err
|
||||
}
|
||||
return srcID, ts, nil
|
||||
}
|
||||
|
||||
func (h *Service) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.UsersStore, error) {
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
store := ts.Users(ctx)
|
||||
return srcID, store, nil
|
||||
}
|
||||
|
||||
// hasRoles checks if the influx source has roles or not
|
||||
func (h *Service) hasRoles(ctx context.Context, ts chronograf.TimeSeries) (chronograf.RolesStore, bool) {
|
||||
store, err := ts.Roles(ctx)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return store, true
|
||||
}
|
||||
|
||||
// Permissions returns all possible permissions for this source.
|
||||
func (h *Service) Permissions(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||
if err != nil {
|
||||
notFound(w, srcID, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ts, err := h.TimeSeries(src)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ts.Connect(ctx, &src); err != nil {
|
||||
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
perms := ts.Permissions(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
httpAPISrcs := "/chronograf/v1/sources"
|
||||
res := struct {
|
||||
Permissions chronograf.Permissions `json:"permissions"`
|
||||
Links map[string]string `json:"links"` // Links are URI locations related to user
|
||||
}{
|
||||
Permissions: perms,
|
||||
Links: map[string]string{
|
||||
"self": fmt.Sprintf("%s/%d/permissions", httpAPISrcs, srcID),
|
||||
"source": fmt.Sprintf("%s/%d", httpAPISrcs, srcID),
|
||||
},
|
||||
}
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
type sourceRoleRequest struct {
|
||||
chronograf.Role
|
||||
}
|
||||
|
||||
func (r *sourceRoleRequest) ValidCreate() error {
|
||||
if r.Name == "" || len(r.Name) > 254 {
|
||||
return fmt.Errorf("Name is required for a role")
|
||||
}
|
||||
for _, user := range r.Users {
|
||||
if user.Name == "" {
|
||||
return fmt.Errorf("Username required")
|
||||
}
|
||||
}
|
||||
return validPermissions(&r.Permissions)
|
||||
}
|
||||
|
||||
func (r *sourceRoleRequest) ValidUpdate() error {
|
||||
if len(r.Name) > 254 {
|
||||
return fmt.Errorf("Username too long; must be less than 254 characters")
|
||||
}
|
||||
for _, user := range r.Users {
|
||||
if user.Name == "" {
|
||||
return fmt.Errorf("Username required")
|
||||
}
|
||||
}
|
||||
return validPermissions(&r.Permissions)
|
||||
}
|
||||
|
||||
type roleResponse struct {
|
||||
Users []sourceUser `json:"users,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Permissions chronograf.Permissions `json:"permissions"`
|
||||
Links selfLinks `json:"links"`
|
||||
}
|
||||
|
||||
func newRoleResponse(srcID int, res *chronograf.Role) roleResponse {
|
||||
su := make([]sourceUser, len(res.Users))
|
||||
for i := range res.Users {
|
||||
name := res.Users[i].Name
|
||||
su[i] = sourceUser{
|
||||
Username: name,
|
||||
Links: newSelfLinks(srcID, "users", name),
|
||||
}
|
||||
}
|
||||
|
||||
if res.Permissions == nil {
|
||||
res.Permissions = make(chronograf.Permissions, 0)
|
||||
}
|
||||
return roleResponse{
|
||||
Name: res.Name,
|
||||
Permissions: res.Permissions,
|
||||
Users: su,
|
||||
Links: newSelfLinks(srcID, "roles", res.Name),
|
||||
}
|
||||
}
|
||||
|
||||
// NewRole adds role to source
|
||||
func (h *Service) NewRole(w http.ResponseWriter, r *http.Request) {
|
||||
var req sourceRoleRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.ValidCreate(); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := roles.Add(ctx, &req.Role)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rr := newRoleResponse(srcID, res)
|
||||
w.Header().Add("Location", rr.Links.Self)
|
||||
encodeJSON(w, http.StatusCreated, rr, h.Logger)
|
||||
}
|
||||
|
||||
// UpdateRole changes the permissions or users of a role
|
||||
func (h *Service) UpdateRole(w http.ResponseWriter, r *http.Request) {
|
||||
var req sourceRoleRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
if err := req.ValidUpdate(); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rid := httprouter.GetParamFromContext(ctx, "rid")
|
||||
req.Name = rid
|
||||
|
||||
if err := roles.Update(ctx, &req.Role); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
role, err := roles.Get(ctx, req.Name)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
rr := newRoleResponse(srcID, role)
|
||||
w.Header().Add("Location", rr.Links.Self)
|
||||
encodeJSON(w, http.StatusOK, rr, h.Logger)
|
||||
}
|
||||
|
||||
// RoleID retrieves a role with ID from store.
|
||||
func (h *Service) RoleID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rid := httprouter.GetParamFromContext(ctx, "rid")
|
||||
role, err := roles.Get(ctx, rid)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
rr := newRoleResponse(srcID, role)
|
||||
encodeJSON(w, http.StatusOK, rr, h.Logger)
|
||||
}
|
||||
|
||||
// Roles retrieves all roles from the store
|
||||
func (h *Service) Roles(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
store, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
roles, err := store.All(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rr := make([]roleResponse, len(roles))
|
||||
for i, role := range roles {
|
||||
rr[i] = newRoleResponse(srcID, &role)
|
||||
}
|
||||
|
||||
res := struct {
|
||||
Roles []roleResponse `json:"roles"`
|
||||
}{rr}
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
// RemoveRole removes role from data source.
|
||||
func (h *Service) RemoveRole(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rid := httprouter.GetParamFromContext(ctx, "rid")
|
||||
if err := roles.Delete(ctx, &chronograf.Role{Name: rid}); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
1476
server/admin_test.go
1476
server/admin_test.go
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,99 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/oauth2"
|
||||
)
|
||||
|
||||
type meLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
}
|
||||
|
||||
type meResponse struct {
|
||||
*chronograf.User
|
||||
Links meLinks `json:"links"`
|
||||
}
|
||||
|
||||
// If new user response is nil, return an empty meResponse because it
|
||||
// indicates authentication is not needed
|
||||
func newMeResponse(usr *chronograf.User) meResponse {
|
||||
base := "/chronograf/v1/users"
|
||||
name := "me"
|
||||
if usr != nil {
|
||||
// TODO: Change to urls.PathEscape for go 1.8
|
||||
u := &url.URL{Path: usr.Name}
|
||||
name = u.String()
|
||||
}
|
||||
|
||||
return meResponse{
|
||||
User: usr,
|
||||
Links: meLinks{
|
||||
Self: fmt.Sprintf("%s/%s", base, name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getEmail(ctx context.Context) (string, error) {
|
||||
principal, err := getPrincipal(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if principal.Subject == "" {
|
||||
return "", fmt.Errorf("Token not found")
|
||||
}
|
||||
return principal.Subject, nil
|
||||
}
|
||||
|
||||
func getPrincipal(ctx context.Context) (oauth2.Principal, error) {
|
||||
principal, ok := ctx.Value(oauth2.PrincipalKey).(oauth2.Principal)
|
||||
if !ok {
|
||||
return oauth2.Principal{}, fmt.Errorf("Token not found")
|
||||
}
|
||||
|
||||
return principal, nil
|
||||
}
|
||||
|
||||
// Me does a findOrCreate based on the email in the context
|
||||
func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !h.UseAuth {
|
||||
// If there's no authentication, return an empty user
|
||||
res := newMeResponse(nil)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
email, err := getEmail(ctx)
|
||||
if err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
usr, err := h.UsersStore.Get(ctx, email)
|
||||
if err == nil {
|
||||
res := newMeResponse(usr)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// Because we didnt find a user, making a new one
|
||||
user := &chronograf.User{
|
||||
Name: email,
|
||||
}
|
||||
|
||||
newUser, err := h.UsersStore.Add(ctx, user)
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("error storing user %s: %v", user.Name, err)
|
||||
unknownErrorWithMessage(w, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newMeResponse(newUser)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
"github.com/influxdata/chronograf/oauth2"
|
||||
)
|
||||
|
||||
type MockUsers struct{}
|
||||
|
||||
func TestService_Me(t *testing.T) {
|
||||
type fields struct {
|
||||
UsersStore chronograf.UsersStore
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
principal oauth2.Principal
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "Existing user",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
UsersStore: &mocks.UsersStore{
|
||||
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
Name: "me",
|
||||
Passwd: "hunter2",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "me",
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","password":"hunter2","links":{"self":"/chronograf/v1/users/me"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "New user",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
UsersStore: &mocks.UsersStore{
|
||||
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
|
||||
return nil, fmt.Errorf("Unknown User")
|
||||
},
|
||||
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
|
||||
return u, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "secret",
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"secret","password":"","links":{"self":"/chronograf/v1/users/secret"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Error adding user",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
UsersStore: &mocks.UsersStore{
|
||||
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
|
||||
return nil, fmt.Errorf("Unknown User")
|
||||
},
|
||||
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
|
||||
return nil, fmt.Errorf("Why Heavy?")
|
||||
},
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "secret",
|
||||
},
|
||||
wantStatus: http.StatusInternalServerError,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":500,"message":"Unknown error: error storing user secret: Why Heavy?"}`,
|
||||
},
|
||||
{
|
||||
name: "No Auth",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: false,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/users/me"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Empty Principal",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
wantStatus: http.StatusUnprocessableEntity,
|
||||
principal: oauth2.Principal{
|
||||
Subject: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt.args.r = tt.args.r.WithContext(context.WithValue(context.Background(), oauth2.PrincipalKey, tt.principal))
|
||||
h := &Service{
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
Logger: tt.fields.Logger,
|
||||
UseAuth: tt.fields.UseAuth,
|
||||
}
|
||||
|
||||
h.Me(tt.args.w, tt.args.r)
|
||||
|
||||
resp := tt.args.w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wantStatus {
|
||||
t.Errorf("%q. Me() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
|
||||
}
|
||||
if tt.wantContentType != "" && content != tt.wantContentType {
|
||||
t.Errorf("%q. Me() = %v, want %v", tt.name, content, tt.wantContentType)
|
||||
}
|
||||
if tt.wantBody != "" && string(body) != tt.wantBody {
|
||||
t.Errorf("%q. Me() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// Permissions returns all possible permissions for this source.
|
||||
func (h *Service) Permissions(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||
if err != nil {
|
||||
notFound(w, srcID, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ts, err := h.TimeSeries(src)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ts.Connect(ctx, &src); err != nil {
|
||||
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
perms := ts.Permissions(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
httpAPISrcs := "/chronograf/v1/sources"
|
||||
res := struct {
|
||||
Permissions chronograf.Permissions `json:"permissions"`
|
||||
Links map[string]string `json:"links"` // Links are URI locations related to user
|
||||
}{
|
||||
Permissions: perms,
|
||||
Links: map[string]string{
|
||||
"self": fmt.Sprintf("%s/%d/permissions", httpAPISrcs, srcID),
|
||||
"source": fmt.Sprintf("%s/%d", httpAPISrcs, srcID),
|
||||
},
|
||||
}
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
func validPermissions(perms *chronograf.Permissions) error {
|
||||
if perms == nil {
|
||||
return nil
|
||||
}
|
||||
for _, perm := range *perms {
|
||||
if perm.Scope != chronograf.AllScope && perm.Scope != chronograf.DBScope {
|
||||
return fmt.Errorf("Invalid permission scope")
|
||||
}
|
||||
if perm.Scope == chronograf.DBScope && perm.Name == "" {
|
||||
return fmt.Errorf("Database scoped permission requires a name")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
func TestService_Permissions(t *testing.T) {
|
||||
type fields struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
TimeSeries TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
ID string
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "New user for data source",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://server.local/chronograf/v1/sources/1",
|
||||
ioutil.NopCloser(
|
||||
bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
|
||||
return chronograf.Source{
|
||||
ID: 1,
|
||||
Name: "muh source",
|
||||
Username: "name",
|
||||
Password: "hunter2",
|
||||
URL: "http://localhost:8086",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
TimeSeries: &mocks.TimeSeries{
|
||||
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
|
||||
return nil
|
||||
},
|
||||
PermissionsF: func(ctx context.Context) chronograf.Permissions {
|
||||
return chronograf.Permissions{
|
||||
{
|
||||
Scope: chronograf.AllScope,
|
||||
Allowed: chronograf.Allowances{"READ", "WRITE"},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
ID: "1",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"permissions":[{"scope":"all","allowed":["READ","WRITE"]}],"links":{"self":"/chronograf/v1/sources/1/permissions","source":"/chronograf/v1/sources/1"}}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.ID,
|
||||
},
|
||||
}))
|
||||
h := &Service{
|
||||
SourcesStore: tt.fields.SourcesStore,
|
||||
TimeSeriesClient: tt.fields.TimeSeries,
|
||||
Logger: tt.fields.Logger,
|
||||
UseAuth: tt.fields.UseAuth,
|
||||
}
|
||||
h.Permissions(tt.args.w, tt.args.r)
|
||||
resp := tt.args.w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wantStatus {
|
||||
t.Errorf("%q. Permissions() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
|
||||
}
|
||||
if tt.wantContentType != "" && content != tt.wantContentType {
|
||||
t.Errorf("%q. Permissions() = %v, want %v", tt.name, content, tt.wantContentType)
|
||||
}
|
||||
if tt.wantBody != "" && string(body) != tt.wantBody {
|
||||
t.Errorf("%q. Permissions() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// NewRole adds role to source
|
||||
func (h *Service) NewRole(w http.ResponseWriter, r *http.Request) {
|
||||
var req sourceRoleRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.ValidCreate(); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := roles.Add(ctx, &req.Role)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rr := newRoleResponse(srcID, res)
|
||||
w.Header().Add("Location", rr.Links.Self)
|
||||
encodeJSON(w, http.StatusCreated, rr, h.Logger)
|
||||
}
|
||||
|
||||
// UpdateRole changes the permissions or users of a role
|
||||
func (h *Service) UpdateRole(w http.ResponseWriter, r *http.Request) {
|
||||
var req sourceRoleRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
if err := req.ValidUpdate(); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rid := httprouter.GetParamFromContext(ctx, "rid")
|
||||
req.Name = rid
|
||||
|
||||
if err := roles.Update(ctx, &req.Role); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
role, err := roles.Get(ctx, req.Name)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
rr := newRoleResponse(srcID, role)
|
||||
w.Header().Add("Location", rr.Links.Self)
|
||||
encodeJSON(w, http.StatusOK, rr, h.Logger)
|
||||
}
|
||||
|
||||
// RoleID retrieves a role with ID from store.
|
||||
func (h *Service) RoleID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rid := httprouter.GetParamFromContext(ctx, "rid")
|
||||
role, err := roles.Get(ctx, rid)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
rr := newRoleResponse(srcID, role)
|
||||
encodeJSON(w, http.StatusOK, rr, h.Logger)
|
||||
}
|
||||
|
||||
// Roles retrieves all roles from the store
|
||||
func (h *Service) Roles(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
store, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
roles, err := store.All(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rr := make([]roleResponse, len(roles))
|
||||
for i, role := range roles {
|
||||
rr[i] = newRoleResponse(srcID, &role)
|
||||
}
|
||||
|
||||
res := struct {
|
||||
Roles []roleResponse `json:"roles"`
|
||||
}{rr}
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
// RemoveRole removes role from data source.
|
||||
func (h *Service) RemoveRole(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, ok := h.hasRoles(ctx, ts)
|
||||
if !ok {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rid := httprouter.GetParamFromContext(ctx, "rid")
|
||||
if err := roles.Delete(ctx, &chronograf.Role{Name: rid}); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// sourceRoleRequest is the format used for both creating and updating roles
|
||||
type sourceRoleRequest struct {
|
||||
chronograf.Role
|
||||
}
|
||||
|
||||
func (r *sourceRoleRequest) ValidCreate() error {
|
||||
if r.Name == "" || len(r.Name) > 254 {
|
||||
return fmt.Errorf("Name is required for a role")
|
||||
}
|
||||
for _, user := range r.Users {
|
||||
if user.Name == "" {
|
||||
return fmt.Errorf("Username required")
|
||||
}
|
||||
}
|
||||
return validPermissions(&r.Permissions)
|
||||
}
|
||||
|
||||
func (r *sourceRoleRequest) ValidUpdate() error {
|
||||
if len(r.Name) > 254 {
|
||||
return fmt.Errorf("Username too long; must be less than 254 characters")
|
||||
}
|
||||
for _, user := range r.Users {
|
||||
if user.Name == "" {
|
||||
return fmt.Errorf("Username required")
|
||||
}
|
||||
}
|
||||
return validPermissions(&r.Permissions)
|
||||
}
|
||||
|
||||
type roleResponse struct {
|
||||
Users []*userResponse `json:"users"`
|
||||
Name string `json:"name"`
|
||||
Permissions chronograf.Permissions `json:"permissions"`
|
||||
Links selfLinks `json:"links"`
|
||||
}
|
||||
|
||||
func newRoleResponse(srcID int, res *chronograf.Role) roleResponse {
|
||||
su := make([]*userResponse, len(res.Users))
|
||||
for i := range res.Users {
|
||||
name := res.Users[i].Name
|
||||
su[i] = newUserResponse(srcID, name)
|
||||
}
|
||||
|
||||
if res.Permissions == nil {
|
||||
res.Permissions = make(chronograf.Permissions, 0)
|
||||
}
|
||||
return roleResponse{
|
||||
Name: res.Name,
|
||||
Permissions: res.Permissions,
|
||||
Users: su,
|
||||
Links: newSelfLinks(srcID, "roles", res.Name),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,691 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
func TestService_NewSourceRole(t *testing.T) {
|
||||
type fields struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
TimeSeries TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
ID string
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "Bad JSON",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://server.local/chronograf/v1/sources/1/roles",
|
||||
ioutil.NopCloser(
|
||||
bytes.NewReader([]byte(`{BAD}`)))),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":400,"message":"Unparsable JSON"}`,
|
||||
},
|
||||
{
|
||||
name: "Invalid request",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://server.local/chronograf/v1/sources/1/roles",
|
||||
ioutil.NopCloser(
|
||||
bytes.NewReader([]byte(`{"name": ""}`)))),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
ID: "1",
|
||||
wantStatus: http.StatusUnprocessableEntity,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":422,"message":"Name is required for a role"}`,
|
||||
},
|
||||
{
|
||||
name: "Invalid source ID",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://server.local/chronograf/v1/sources/1/roles",
|
||||
ioutil.NopCloser(
|
||||
bytes.NewReader([]byte(`{"name": "newrole"}`)))),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
ID: "BADROLE",
|
||||
wantStatus: http.StatusUnprocessableEntity,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":422,"message":"Error converting ID BADROLE"}`,
|
||||
},
|
||||
{
|
||||
name: "Source doesn't support roles",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://server.local/chronograf/v1/sources/1/roles",
|
||||
ioutil.NopCloser(
|
||||
bytes.NewReader([]byte(`{"name": "role"}`)))),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
|
||||
return chronograf.Source{
|
||||
ID: 1,
|
||||
Name: "muh source",
|
||||
Username: "name",
|
||||
Password: "hunter2",
|
||||
URL: "http://localhost:8086",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
TimeSeries: &mocks.TimeSeries{
|
||||
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
|
||||
return nil
|
||||
},
|
||||
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
|
||||
return nil, fmt.Errorf("roles not supported")
|
||||
},
|
||||
},
|
||||
},
|
||||
ID: "1",
|
||||
wantStatus: http.StatusNotFound,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":404,"message":"Source 1 does not have role capability"}`,
|
||||
},
|
||||
{
|
||||
name: "Unable to add role to server",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://server.local/chronograf/v1/sources/1/roles",
|
||||
ioutil.NopCloser(
|
||||
bytes.NewReader([]byte(`{"name": "role"}`)))),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
|
||||
return chronograf.Source{
|
||||
ID: 1,
|
||||
Name: "muh source",
|
||||
Username: "name",
|
||||
Password: "hunter2",
|
||||
URL: "http://localhost:8086",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
TimeSeries: &mocks.TimeSeries{
|
||||
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
|
||||
return nil
|
||||
},
|
||||
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
|
||||
return &mocks.RolesStore{
|
||||
AddF: func(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) {
|
||||
return nil, fmt.Errorf("server had and issue")
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
ID: "1",
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":400,"message":"server had and issue"}`,
|
||||
},
|
||||
{
|
||||
name: "New role for data source",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://server.local/chronograf/v1/sources/1/roles",
|
||||
ioutil.NopCloser(
|
||||
bytes.NewReader([]byte(`{"name": "biffsgang","users": [{"name": "match"},{"name": "skinhead"},{"name": "3-d"}]}`)))),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
|
||||
return chronograf.Source{
|
||||
ID: 1,
|
||||
Name: "muh source",
|
||||
Username: "name",
|
||||
Password: "hunter2",
|
||||
URL: "http://localhost:8086",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
TimeSeries: &mocks.TimeSeries{
|
||||
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
|
||||
return nil
|
||||
},
|
||||
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
|
||||
return &mocks.RolesStore{
|
||||
AddF: func(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) {
|
||||
return u, nil
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
ID: "1",
|
||||
wantStatus: http.StatusCreated,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/match"},"name":"match"},{"links":{"self":"/chronograf/v1/sources/1/users/skinhead"},"name":"skinhead"},{"links":{"self":"/chronograf/v1/sources/1/users/3-d"},"name":"3-d"}],"name":"biffsgang","permissions":[],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
h := &Service{
|
||||
SourcesStore: tt.fields.SourcesStore,
|
||||
TimeSeriesClient: tt.fields.TimeSeries,
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.ID,
|
||||
},
|
||||
}))
|
||||
|
||||
h.NewRole(tt.args.w, tt.args.r)
|
||||
|
||||
resp := tt.args.w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wantStatus {
|
||||
t.Errorf("%q. NewRole() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
|
||||
}
|
||||
if tt.wantContentType != "" && content != tt.wantContentType {
|
||||
t.Errorf("%q. NewRole() = %v, want %v", tt.name, content, tt.wantContentType)
|
||||
}
|
||||
if tt.wantBody != "" && string(body) != tt.wantBody {
|
||||
t.Errorf("%q. NewRole() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UpdateRole(t *testing.T) {
|
||||
type fields struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
TimeSeries TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
ID string
|
||||
RoleID string
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "Update role for data source",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://server.local/chronograf/v1/sources/1/roles",
|
||||
ioutil.NopCloser(
|
||||
bytes.NewReader([]byte(`{"name": "biffsgang","users": [{"name": "match"},{"name": "skinhead"},{"name": "3-d"}]}`)))),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
|
||||
return chronograf.Source{
|
||||
ID: 1,
|
||||
Name: "muh source",
|
||||
Username: "name",
|
||||
Password: "hunter2",
|
||||
URL: "http://localhost:8086",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
TimeSeries: &mocks.TimeSeries{
|
||||
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
|
||||
return nil
|
||||
},
|
||||
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
|
||||
return &mocks.RolesStore{
|
||||
UpdateF: func(ctx context.Context, u *chronograf.Role) error {
|
||||
return nil
|
||||
},
|
||||
GetF: func(ctx context.Context, name string) (*chronograf.Role, error) {
|
||||
return &chronograf.Role{
|
||||
Name: "biffsgang",
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
Name: "match",
|
||||
},
|
||||
{
|
||||
Name: "skinhead",
|
||||
},
|
||||
{
|
||||
Name: "3-d",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
ID: "1",
|
||||
RoleID: "biffsgang",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/match"},"name":"match"},{"links":{"self":"/chronograf/v1/sources/1/users/skinhead"},"name":"skinhead"},{"links":{"self":"/chronograf/v1/sources/1/users/3-d"},"name":"3-d"}],"name":"biffsgang","permissions":[],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
h := &Service{
|
||||
SourcesStore: tt.fields.SourcesStore,
|
||||
TimeSeriesClient: tt.fields.TimeSeries,
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
|
||||
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.ID,
|
||||
},
|
||||
{
|
||||
Key: "rid",
|
||||
Value: tt.RoleID,
|
||||
},
|
||||
}))
|
||||
|
||||
h.UpdateRole(tt.args.w, tt.args.r)
|
||||
|
||||
resp := tt.args.w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wantStatus {
|
||||
t.Errorf("%q. NewRole() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
|
||||
}
|
||||
if tt.wantContentType != "" && content != tt.wantContentType {
|
||||
t.Errorf("%q. NewRole() = %v, want %v", tt.name, content, tt.wantContentType)
|
||||
}
|
||||
if tt.wantBody != "" && string(body) != tt.wantBody {
|
||||
t.Errorf("%q. NewRole() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_RoleID(t *testing.T) {
|
||||
type fields struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
TimeSeries TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
ID string
|
||||
RoleID string
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "Get role for data source",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"GET",
|
||||
"http://server.local/chronograf/v1/sources/1/roles/biffsgang",
|
||||
nil),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
|
||||
return chronograf.Source{
|
||||
ID: 1,
|
||||
Name: "muh source",
|
||||
Username: "name",
|
||||
Password: "hunter2",
|
||||
URL: "http://localhost:8086",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
TimeSeries: &mocks.TimeSeries{
|
||||
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
|
||||
return nil
|
||||
},
|
||||
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
|
||||
return &mocks.RolesStore{
|
||||
GetF: func(ctx context.Context, name string) (*chronograf.Role, error) {
|
||||
return &chronograf.Role{
|
||||
Name: "biffsgang",
|
||||
Permissions: chronograf.Permissions{
|
||||
{
|
||||
Name: "grays_sports_almanac",
|
||||
Scope: "DBScope",
|
||||
Allowed: chronograf.Allowances{
|
||||
"ReadData",
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
Name: "match",
|
||||
},
|
||||
{
|
||||
Name: "skinhead",
|
||||
},
|
||||
{
|
||||
Name: "3-d",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
ID: "1",
|
||||
RoleID: "biffsgang",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/match"},"name":"match"},{"links":{"self":"/chronograf/v1/sources/1/users/skinhead"},"name":"skinhead"},{"links":{"self":"/chronograf/v1/sources/1/users/3-d"},"name":"3-d"}],"name":"biffsgang","permissions":[{"scope":"DBScope","name":"grays_sports_almanac","allowed":["ReadData"]}],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
h := &Service{
|
||||
SourcesStore: tt.fields.SourcesStore,
|
||||
TimeSeriesClient: tt.fields.TimeSeries,
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
|
||||
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.ID,
|
||||
},
|
||||
{
|
||||
Key: "rid",
|
||||
Value: tt.RoleID,
|
||||
},
|
||||
}))
|
||||
|
||||
h.RoleID(tt.args.w, tt.args.r)
|
||||
|
||||
resp := tt.args.w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wantStatus {
|
||||
t.Errorf("%q. RoleID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
|
||||
}
|
||||
if tt.wantContentType != "" && content != tt.wantContentType {
|
||||
t.Errorf("%q. RoleID() = %v, want %v", tt.name, content, tt.wantContentType)
|
||||
}
|
||||
if tt.wantBody != "" && string(body) != tt.wantBody {
|
||||
t.Errorf("%q. RoleID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_RemoveRole(t *testing.T) {
|
||||
type fields struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
TimeSeries TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
ID string
|
||||
RoleID string
|
||||
wantStatus int
|
||||
}{
|
||||
{
|
||||
name: "remove role for data source",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"GET",
|
||||
"http://server.local/chronograf/v1/sources/1/roles/biffsgang",
|
||||
nil),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
|
||||
return chronograf.Source{
|
||||
ID: 1,
|
||||
Name: "muh source",
|
||||
Username: "name",
|
||||
Password: "hunter2",
|
||||
URL: "http://localhost:8086",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
TimeSeries: &mocks.TimeSeries{
|
||||
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
|
||||
return nil
|
||||
},
|
||||
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
|
||||
return &mocks.RolesStore{
|
||||
DeleteF: func(context.Context, *chronograf.Role) error {
|
||||
return nil
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
ID: "1",
|
||||
RoleID: "biffsgang",
|
||||
wantStatus: http.StatusNoContent,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
h := &Service{
|
||||
SourcesStore: tt.fields.SourcesStore,
|
||||
TimeSeriesClient: tt.fields.TimeSeries,
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
|
||||
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.ID,
|
||||
},
|
||||
{
|
||||
Key: "rid",
|
||||
Value: tt.RoleID,
|
||||
},
|
||||
}))
|
||||
|
||||
h.RemoveRole(tt.args.w, tt.args.r)
|
||||
|
||||
resp := tt.args.w.Result()
|
||||
if resp.StatusCode != tt.wantStatus {
|
||||
t.Errorf("%q. RemoveRole() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Roles(t *testing.T) {
|
||||
type fields struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
TimeSeries TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
ID string
|
||||
RoleID string
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "Get roles for data source",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"GET",
|
||||
"http://server.local/chronograf/v1/sources/1/roles",
|
||||
nil),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
|
||||
return chronograf.Source{
|
||||
ID: 1,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
TimeSeries: &mocks.TimeSeries{
|
||||
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
|
||||
return nil
|
||||
},
|
||||
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
|
||||
return &mocks.RolesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Role, error) {
|
||||
return []chronograf.Role{
|
||||
chronograf.Role{
|
||||
Name: "biffsgang",
|
||||
Permissions: chronograf.Permissions{
|
||||
{
|
||||
Name: "grays_sports_almanac",
|
||||
Scope: "DBScope",
|
||||
Allowed: chronograf.Allowances{
|
||||
"ReadData",
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
Name: "match",
|
||||
},
|
||||
{
|
||||
Name: "skinhead",
|
||||
},
|
||||
{
|
||||
Name: "3-d",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
ID: "1",
|
||||
RoleID: "biffsgang",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"roles":[{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/match"},"name":"match"},{"links":{"self":"/chronograf/v1/sources/1/users/skinhead"},"name":"skinhead"},{"links":{"self":"/chronograf/v1/sources/1/users/3-d"},"name":"3-d"}],"name":"biffsgang","permissions":[{"scope":"DBScope","name":"grays_sports_almanac","allowed":["ReadData"]}],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}]}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
h := &Service{
|
||||
SourcesStore: tt.fields.SourcesStore,
|
||||
TimeSeriesClient: tt.fields.TimeSeries,
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
|
||||
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.ID,
|
||||
},
|
||||
{
|
||||
Key: "rid",
|
||||
Value: tt.RoleID,
|
||||
},
|
||||
}))
|
||||
|
||||
h.Roles(tt.args.w, tt.args.r)
|
||||
|
||||
resp := tt.args.w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wantStatus {
|
||||
t.Errorf("%q. RoleID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
|
||||
}
|
||||
if tt.wantContentType != "" && content != tt.wantContentType {
|
||||
t.Errorf("%q. RoleID() = %v, want %v", tt.name, content, tt.wantContentType)
|
||||
}
|
||||
if tt.wantBody != "" && string(body) != tt.wantBody {
|
||||
t.Errorf("%q. RoleID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
|
||||
}
|
||||
}
|
||||
}
|
366
server/users.go
366
server/users.go
|
@ -1,99 +1,313 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/oauth2"
|
||||
)
|
||||
|
||||
type userLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
}
|
||||
|
||||
type userResponse struct {
|
||||
*chronograf.User
|
||||
Links userLinks `json:"links"`
|
||||
}
|
||||
|
||||
// If new user response is nil, return an empty userResponse because it
|
||||
// indicates authentication is not needed
|
||||
func newUserResponse(usr *chronograf.User) userResponse {
|
||||
base := "/chronograf/v1/users"
|
||||
name := "me"
|
||||
if usr != nil {
|
||||
// TODO: Change to usrl.PathEscape for go 1.8
|
||||
u := &url.URL{Path: usr.Name}
|
||||
name = u.String()
|
||||
}
|
||||
|
||||
return userResponse{
|
||||
User: usr,
|
||||
Links: userLinks{
|
||||
Self: fmt.Sprintf("%s/%s", base, name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getEmail(ctx context.Context) (string, error) {
|
||||
principal, err := getPrincipal(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if principal.Subject == "" {
|
||||
return "", fmt.Errorf("Token not found")
|
||||
}
|
||||
return principal.Subject, nil
|
||||
}
|
||||
|
||||
func getPrincipal(ctx context.Context) (oauth2.Principal, error) {
|
||||
principal, ok := ctx.Value(oauth2.PrincipalKey).(oauth2.Principal)
|
||||
if !ok {
|
||||
return oauth2.Principal{}, fmt.Errorf("Token not found")
|
||||
}
|
||||
|
||||
return principal, nil
|
||||
}
|
||||
|
||||
// Me does a findOrCreate based on the email in the context
|
||||
func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !h.UseAuth {
|
||||
// If there's no authentication, return an empty user
|
||||
res := newUserResponse(nil)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
// NewSourceUser adds user to source
|
||||
func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req userRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
email, err := getEmail(ctx)
|
||||
if err != nil {
|
||||
if err := req.ValidCreate(); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
usr, err := h.UsersStore.Get(ctx, email)
|
||||
if err == nil {
|
||||
res := newUserResponse(usr)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// Because we didnt find a user, making a new one
|
||||
user := &chronograf.User{
|
||||
Name: email,
|
||||
}
|
||||
|
||||
newUser, err := h.UsersStore.Add(ctx, user)
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("error storing user %s: %v", user.Name, err)
|
||||
unknownErrorWithMessage(w, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newUserResponse(newUser)
|
||||
store := ts.Users(ctx)
|
||||
user := &chronograf.User{
|
||||
Name: req.Username,
|
||||
Passwd: req.Password,
|
||||
}
|
||||
|
||||
res, err := store.Add(ctx, user)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
su := newUserResponse(srcID, res.Name).WithPermissions(res.Permissions)
|
||||
if _, hasRoles := h.hasRoles(ctx, ts); hasRoles {
|
||||
su.WithRoles(srcID, res.Roles)
|
||||
}
|
||||
w.Header().Add("Location", su.Links.Self)
|
||||
encodeJSON(w, http.StatusCreated, su, h.Logger)
|
||||
}
|
||||
|
||||
// SourceUsers retrieves all users from source.
|
||||
func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
store := ts.Users(ctx)
|
||||
users, err := store.All(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, hasRoles := h.hasRoles(ctx, ts)
|
||||
ur := make([]userResponse, len(users))
|
||||
for i, u := range users {
|
||||
usr := newUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
|
||||
if hasRoles {
|
||||
usr.WithRoles(srcID, u.Roles)
|
||||
}
|
||||
ur[i] = *usr
|
||||
}
|
||||
|
||||
res := usersResponse{
|
||||
Users: ur,
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
// SourceUserID retrieves a user with ID from store.
|
||||
func (h *Service) SourceUserID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
uid := httprouter.GetParamFromContext(ctx, "uid")
|
||||
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
store := ts.Users(ctx)
|
||||
u, err := store.Get(ctx, uid)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
|
||||
if _, hasRoles := h.hasRoles(ctx, ts); hasRoles {
|
||||
res.WithRoles(srcID, u.Roles)
|
||||
}
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
// RemoveSourceUser removes the user from the InfluxDB source
|
||||
func (h *Service) RemoveSourceUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
uid := httprouter.GetParamFromContext(ctx, "uid")
|
||||
|
||||
_, store, err := h.sourceUsersStore(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.Delete(ctx, &chronograf.User{Name: uid}); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UpdateSourceUser changes the password or permissions of a source user
|
||||
func (h *Service) UpdateSourceUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req userRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
if err := req.ValidUpdate(); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
uid := httprouter.GetParamFromContext(ctx, "uid")
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user := &chronograf.User{
|
||||
Name: uid,
|
||||
Passwd: req.Password,
|
||||
Permissions: req.Permissions,
|
||||
}
|
||||
store := ts.Users(ctx)
|
||||
|
||||
if err := store.Update(ctx, user); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := store.Get(ctx, uid)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
|
||||
if _, hasRoles := h.hasRoles(ctx, ts); hasRoles {
|
||||
res.WithRoles(srcID, u.Roles)
|
||||
}
|
||||
w.Header().Add("Location", res.Links.Self)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
func (h *Service) sourcesSeries(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.TimeSeries, error) {
|
||||
srcID, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||
if err != nil {
|
||||
notFound(w, srcID, h.Logger)
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
ts, err := h.TimeSeries(src)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if err = ts.Connect(ctx, &src); err != nil {
|
||||
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||
return 0, nil, err
|
||||
}
|
||||
return srcID, ts, nil
|
||||
}
|
||||
|
||||
func (h *Service) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.UsersStore, error) {
|
||||
srcID, ts, err := h.sourcesSeries(ctx, w, r)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
store := ts.Users(ctx)
|
||||
return srcID, store, nil
|
||||
}
|
||||
|
||||
// hasRoles checks if the influx source has roles or not
|
||||
func (h *Service) hasRoles(ctx context.Context, ts chronograf.TimeSeries) (chronograf.RolesStore, bool) {
|
||||
store, err := ts.Roles(ctx)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return store, true
|
||||
}
|
||||
|
||||
type userRequest struct {
|
||||
Username string `json:"name,omitempty"` // Username for new account
|
||||
Password string `json:"password,omitempty"` // Password for new account
|
||||
Permissions chronograf.Permissions `json:"permissions,omitempty"` // Optional permissions
|
||||
}
|
||||
|
||||
func (r *userRequest) ValidCreate() error {
|
||||
if r.Username == "" {
|
||||
return fmt.Errorf("Username required")
|
||||
}
|
||||
if r.Password == "" {
|
||||
return fmt.Errorf("Password required")
|
||||
}
|
||||
return validPermissions(&r.Permissions)
|
||||
}
|
||||
|
||||
type usersResponse struct {
|
||||
Users []userResponse `json:"users"`
|
||||
}
|
||||
|
||||
func (r *userRequest) ValidUpdate() error {
|
||||
if r.Password == "" && len(r.Permissions) == 0 {
|
||||
return fmt.Errorf("No fields to update")
|
||||
}
|
||||
return validPermissions(&r.Permissions)
|
||||
}
|
||||
|
||||
type userResponse struct {
|
||||
Name string // Username for new account
|
||||
Permissions chronograf.Permissions // Account's permissions
|
||||
Roles []roleResponse // Roles if source uses them
|
||||
Links selfLinks // Links are URI locations related to user
|
||||
hasPermissions bool
|
||||
hasRoles bool
|
||||
}
|
||||
|
||||
func (u *userResponse) MarshalJSON() ([]byte, error) {
|
||||
res := map[string]interface{}{
|
||||
"name": u.Name,
|
||||
"links": u.Links,
|
||||
}
|
||||
if u.hasRoles {
|
||||
res["roles"] = u.Roles
|
||||
}
|
||||
if u.hasPermissions {
|
||||
res["permissions"] = u.Permissions
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
|
||||
// newUserResponse creates an HTTP JSON response for a user w/o roles
|
||||
func newUserResponse(srcID int, name string) *userResponse {
|
||||
self := newSelfLinks(srcID, "users", name)
|
||||
return &userResponse{
|
||||
Name: name,
|
||||
Links: self,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *userResponse) WithPermissions(perms chronograf.Permissions) *userResponse {
|
||||
u.hasPermissions = true
|
||||
if perms == nil {
|
||||
perms = make(chronograf.Permissions, 0)
|
||||
}
|
||||
u.Permissions = perms
|
||||
return u
|
||||
}
|
||||
|
||||
// WithRoles adds roles to the HTTP JSON response for a user
|
||||
func (u *userResponse) WithRoles(srcID int, roles []chronograf.Role) *userResponse {
|
||||
u.hasRoles = true
|
||||
rr := make([]roleResponse, len(roles))
|
||||
for i, role := range roles {
|
||||
rr[i] = newRoleResponse(srcID, &role)
|
||||
}
|
||||
u.Roles = rr
|
||||
return u
|
||||
}
|
||||
|
||||
type selfLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
}
|
||||
|
||||
func newSelfLinks(id int, parent, resource string) selfLinks {
|
||||
httpAPISrcs := "/chronograf/v1/sources"
|
||||
u := &url.URL{Path: resource}
|
||||
encodedResource := u.String()
|
||||
return selfLinks{
|
||||
Self: fmt.Sprintf("%s/%d/%s/%s", httpAPISrcs, id, parent, encodedResource),
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue