446 lines
10 KiB
Go
446 lines
10 KiB
Go
package enterprise
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/influxdata/chronograf"
|
|
)
|
|
|
|
// MetaClient represents a Meta node in an Influx Enterprise cluster
|
|
type MetaClient struct {
|
|
URL *url.URL
|
|
client interface {
|
|
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
|
}
|
|
}
|
|
|
|
// NewMetaClient represents a meta node in an Influx Enterprise cluster
|
|
func NewMetaClient(url *url.URL) *MetaClient {
|
|
return &MetaClient{
|
|
URL: url,
|
|
client: &defaultClient{},
|
|
}
|
|
}
|
|
|
|
// ShowCluster returns the cluster configuration (not health)
|
|
func (m *MetaClient) ShowCluster(ctx context.Context) (*Cluster, error) {
|
|
res, err := m.Do(ctx, "GET", "/show-cluster", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
dec := json.NewDecoder(res.Body)
|
|
out := &Cluster{}
|
|
err = dec.Decode(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Users gets all the users. If name is not nil it filters for a single user
|
|
func (m *MetaClient) Users(ctx context.Context, name *string) (*Users, error) {
|
|
params := map[string]string{}
|
|
if name != nil {
|
|
params["name"] = *name
|
|
}
|
|
res, err := m.Do(ctx, "GET", "/user", params, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
dec := json.NewDecoder(res.Body)
|
|
users := &Users{}
|
|
err = dec.Decode(users)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return users, nil
|
|
}
|
|
|
|
// User returns a single Influx Enterprise user
|
|
func (m *MetaClient) User(ctx context.Context, name string) (*User, error) {
|
|
users, err := m.Users(ctx, &name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, user := range users.Users {
|
|
return &user, nil
|
|
}
|
|
return nil, fmt.Errorf("No user found")
|
|
}
|
|
|
|
// CreateUser adds a user to Influx Enterprise
|
|
func (m *MetaClient) CreateUser(ctx context.Context, name, passwd string) error {
|
|
return m.CreateUpdateUser(ctx, "create", name, passwd)
|
|
}
|
|
|
|
// ChangePassword updates a user's password in Influx Enterprise
|
|
func (m *MetaClient) ChangePassword(ctx context.Context, name, passwd string) error {
|
|
return m.CreateUpdateUser(ctx, "change-password", name, passwd)
|
|
}
|
|
|
|
// CreateUpdateUser is a helper function to POST to the /user Influx Enterprise endpoint
|
|
func (m *MetaClient) CreateUpdateUser(ctx context.Context, action, name, passwd string) error {
|
|
a := &UserAction{
|
|
Action: action,
|
|
User: &User{
|
|
Name: name,
|
|
Password: passwd,
|
|
},
|
|
}
|
|
return m.Post(ctx, "/user", a, nil)
|
|
}
|
|
|
|
// DeleteUser removes a user from Influx Enterprise
|
|
func (m *MetaClient) DeleteUser(ctx context.Context, name string) error {
|
|
a := &UserAction{
|
|
Action: "delete",
|
|
User: &User{
|
|
Name: name,
|
|
},
|
|
}
|
|
|
|
return m.Post(ctx, "/user", a, nil)
|
|
}
|
|
|
|
// RemoveAllUserPerms revokes all permissions for a user in Influx Enterprise
|
|
func (m *MetaClient) RemoveAllUserPerms(ctx context.Context, name string) error {
|
|
user, err := m.User(ctx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// No permissions to remove
|
|
if len(user.Permissions) == 0 {
|
|
return nil
|
|
}
|
|
|
|
a := &UserAction{
|
|
Action: "remove-permissions",
|
|
User: user,
|
|
}
|
|
return m.Post(ctx, "/user", a, nil)
|
|
}
|
|
|
|
// SetUserPerms removes all permissions and then adds the requested perms
|
|
func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permissions) error {
|
|
err := m.RemoveAllUserPerms(ctx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// No permissions to add, so, user is in the right state
|
|
if len(perms) == 0 {
|
|
return nil
|
|
}
|
|
|
|
a := &UserAction{
|
|
Action: "add-permissions",
|
|
User: &User{
|
|
Name: name,
|
|
Permissions: perms,
|
|
},
|
|
}
|
|
return m.Post(ctx, "/user", a, nil)
|
|
}
|
|
|
|
// UserRoles returns a map of users to all of their current roles
|
|
func (m *MetaClient) UserRoles(ctx context.Context) (map[string]Roles, error) {
|
|
res, err := m.Roles(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userRoles := make(map[string]Roles)
|
|
for _, role := range res.Roles {
|
|
for _, u := range role.Users {
|
|
ur, ok := userRoles[u]
|
|
if !ok {
|
|
ur = Roles{}
|
|
}
|
|
ur.Roles = append(ur.Roles, role)
|
|
userRoles[u] = ur
|
|
}
|
|
}
|
|
return userRoles, nil
|
|
}
|
|
|
|
// Roles gets all the roles. If name is not nil it filters for a single role
|
|
func (m *MetaClient) Roles(ctx context.Context, name *string) (*Roles, error) {
|
|
params := map[string]string{}
|
|
if name != nil {
|
|
params["name"] = *name
|
|
}
|
|
res, err := m.Do(ctx, "GET", "/role", params, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
dec := json.NewDecoder(res.Body)
|
|
roles := &Roles{}
|
|
err = dec.Decode(roles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return roles, nil
|
|
}
|
|
|
|
// Role returns a single named role
|
|
func (m *MetaClient) Role(ctx context.Context, name string) (*Role, error) {
|
|
roles, err := m.Roles(ctx, &name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, role := range roles.Roles {
|
|
return &role, nil
|
|
}
|
|
return nil, fmt.Errorf("No role found")
|
|
}
|
|
|
|
// CreateRole adds a role to Influx Enterprise
|
|
func (m *MetaClient) CreateRole(ctx context.Context, name string) error {
|
|
a := &RoleAction{
|
|
Action: "create",
|
|
Role: &Role{
|
|
Name: name,
|
|
},
|
|
}
|
|
return m.Post(ctx, "/role", a, nil)
|
|
}
|
|
|
|
// DeleteRole removes a role from Influx Enterprise
|
|
func (m *MetaClient) DeleteRole(ctx context.Context, name string) error {
|
|
a := &RoleAction{
|
|
Action: "delete",
|
|
Role: &Role{
|
|
Name: name,
|
|
},
|
|
}
|
|
return m.Post(ctx, "/role", a, nil)
|
|
}
|
|
|
|
// RemoveAllRolePerms removes all permissions from a role
|
|
func (m *MetaClient) RemoveAllRolePerms(ctx context.Context, name string) error {
|
|
role, err := m.Role(ctx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// No permissions to remove
|
|
if len(role.Permissions) == 0 {
|
|
return nil
|
|
}
|
|
|
|
a := &RoleAction{
|
|
Action: "remove-permissions",
|
|
Role: role,
|
|
}
|
|
return m.Post(ctx, "/role", a, nil)
|
|
}
|
|
|
|
// SetRolePerms removes all permissions and then adds the requested perms to role
|
|
func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permissions) error {
|
|
err := m.RemoveAllRolePerms(ctx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// No permissions to add, so, role is in the right state
|
|
if len(perms) == 0 {
|
|
return nil
|
|
}
|
|
|
|
a := &RoleAction{
|
|
Action: "add-permissions",
|
|
Role: &Role{
|
|
Name: name,
|
|
Permissions: perms,
|
|
},
|
|
}
|
|
return m.Post(ctx, "/role", a, nil)
|
|
}
|
|
|
|
// SetRoleUsers removes all users and then adds the requested users to role
|
|
func (m *MetaClient) SetRoleUsers(ctx context.Context, name string, users []string) error {
|
|
role, err := m.Role(ctx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
revoke, add := Difference(users, role.Users)
|
|
if err := m.RemoveRoleUsers(ctx, name, revoke); err != nil {
|
|
return err
|
|
}
|
|
|
|
return m.AddRoleUsers(ctx, name, add)
|
|
}
|
|
|
|
// Difference compares two sets and returns a set to be removed and a set to be added
|
|
func Difference(wants []string, haves []string) (revoke []string, add []string) {
|
|
for _, want := range wants {
|
|
found := false
|
|
for _, got := range haves {
|
|
if want != got {
|
|
continue
|
|
}
|
|
found = true
|
|
}
|
|
if !found {
|
|
add = append(add, want)
|
|
}
|
|
}
|
|
for _, got := range haves {
|
|
found := false
|
|
for _, want := range wants {
|
|
if want != got {
|
|
continue
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
if !found {
|
|
revoke = append(revoke, got)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// AddRoleUsers updates a role to have additional users.
|
|
func (m *MetaClient) AddRoleUsers(ctx context.Context, name string, users []string) error {
|
|
// No permissions to add, so, role is in the right state
|
|
if len(users) == 0 {
|
|
return nil
|
|
}
|
|
|
|
a := &RoleAction{
|
|
Action: "add-users",
|
|
Role: &Role{
|
|
Name: name,
|
|
Users: users,
|
|
},
|
|
}
|
|
return m.Post(ctx, "/role", a, nil)
|
|
}
|
|
|
|
// RemoveRoleUsers updates a role to remove some users.
|
|
func (m *MetaClient) RemoveRoleUsers(ctx context.Context, name string, users []string) error {
|
|
// No permissions to add, so, role is in the right state
|
|
if len(users) == 0 {
|
|
return nil
|
|
}
|
|
|
|
a := &RoleAction{
|
|
Action: "remove-users",
|
|
Role: &Role{
|
|
Name: name,
|
|
Users: users,
|
|
},
|
|
}
|
|
return m.Post(ctx, "/role", a, nil)
|
|
}
|
|
|
|
// Post is a helper function to POST to Influx Enterprise
|
|
func (m *MetaClient) Post(ctx context.Context, path string, action interface{}, params map[string]string) error {
|
|
b, err := json.Marshal(action)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := bytes.NewReader(b)
|
|
_, err = m.Do(ctx, "POST", path, params, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type defaultClient struct{}
|
|
|
|
// Do is a helper function to interface with Influx Enterprise's Meta API
|
|
func (d *defaultClient) Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error) {
|
|
p := url.Values{}
|
|
for k, v := range params {
|
|
p.Add(k, v)
|
|
}
|
|
|
|
URL.Path = path
|
|
URL.RawQuery = p.Encode()
|
|
|
|
req, err := http.NewRequest(method, URL.String(), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
// Meta servers will redirect (307) to leader. We need
|
|
// special handling to preserve authentication headers.
|
|
client := &http.Client{
|
|
CheckRedirect: AuthedCheckRedirect,
|
|
}
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
defer res.Body.Close()
|
|
dec := json.NewDecoder(res.Body)
|
|
out := &Error{}
|
|
err = dec.Decode(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, errors.New(out.Error)
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
// AuthedCheckRedirect tries to follow the Influx Enterprise pattern of
|
|
// redirecting to the leader but preserving authentication headers.
|
|
func AuthedCheckRedirect(req *http.Request, via []*http.Request) error {
|
|
if len(via) >= 10 {
|
|
return errors.New("too many redirects")
|
|
} else if len(via) == 0 {
|
|
return nil
|
|
}
|
|
preserve := "Authorization"
|
|
if auth, ok := via[0].Header[preserve]; ok {
|
|
req.Header[preserve] = auth
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Do is a cancelable function to interface with Influx Enterprise's Meta API
|
|
func (m *MetaClient) Do(ctx context.Context, method, path string, params map[string]string, body io.Reader) (*http.Response, error) {
|
|
type result struct {
|
|
Response *http.Response
|
|
Err error
|
|
}
|
|
resps := make(chan (result))
|
|
go func() {
|
|
resp, err := m.client.Do(m.URL, path, method, params, body)
|
|
resps <- result{resp, err}
|
|
}()
|
|
|
|
select {
|
|
case resp := <-resps:
|
|
return resp.Response, resp.Err
|
|
case <-ctx.Done():
|
|
return nil, chronograf.ErrUpstreamTimeout
|
|
}
|
|
}
|