Skip to content
This repository was archived by the owner on Mar 15, 2024. It is now read-only.

WIP: Add support for creating ephemeral users for multi-node Splunk deployments #17

Merged
merged 4 commits into from
Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func newBackend() logical.Backend {
b.pathRolesList(),
b.pathRoles(),
b.pathCredsCreate(),
b.pathCredsCreateMulti(),
},
Secrets: []*framework.Secret{
b.pathSecretCreds(),
Expand All @@ -53,7 +54,7 @@ func newBackend() logical.Backend {
return &b
}

func (b *backend) ensureConnection(ctx context.Context, name string, config *splunkConfig) (*splunk.API, error) {
func (b *backend) ensureConnection(ctx context.Context, config *splunkConfig) (*splunk.API, error) {
if conn, ok := b.conn.Load(config.ID); ok {
return conn.(*splunk.API), nil
}
Expand Down
19 changes: 19 additions & 0 deletions clients/splunk/deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package splunk

// DeploymentService encapsulates the Deployment portion of the Splunk API
type DeploymentService struct {
client *Client
}

func newDeploymentService(client *Client) *DeploymentService {
return &DeploymentService{
client: client,
}
}

// GetSearchPeers returns information about all search peers
func (d *DeploymentService) GetSearchPeers() ([]ServerInfoEntry, *Response, error) {
info := make([]ServerInfoEntry, 0)
resp, err := Receive(d.client.New().Get("search/distributed/peers"), &info)
return info, resp, err
}
6 changes: 3 additions & 3 deletions clients/splunk/properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ type Entry struct {
// stringResponseDecoder decodes http response string
// Properties API operates on particular key in the configuration file.
// CRUD for properties API returns JSON/XML encoded response for error cases and returns a string response for success
type stringResponseDecoder struct{
type stringResponseDecoder struct {
}

func getPropertiesUri(file string, stanza string, key string) (string) {
func getPropertiesUri(file string, stanza string, key string) string {
return fmt.Sprintf("properties/%s/%s/%s", url.PathEscape(file), url.PathEscape(stanza), url.PathEscape(key))
}

Expand Down Expand Up @@ -74,4 +74,4 @@ func (p *PropertiesService) GetKey(file string, stanza string, key string) (*str
return nil, resp, relevantError(err, apiError)
}
return &output.Value, resp, relevantError(err, apiError)
}
}
3 changes: 1 addition & 2 deletions clients/splunk/properties_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestPropertiesService_GetKey(t *testing.T) {
_, response, err := propertiesSvc.GetKey("foo", "bar", "key")
assert.ErrorContains(t, err, "splunk: foo does not exist")
assert.Equal(t, response.StatusCode, 404)
_, response, err = propertiesSvc.GetKey("b/a/z","b-ar", "k-ey")
_, response, err = propertiesSvc.GetKey("b/a/z", "b-ar", "k-ey")
assert.ErrorContains(t, err, "ERROR splunk: Directory traversal risk in /nobody/system/b/a/z at segment \"b/a/z\"")
assert.Equal(t, response.StatusCode, 403)
_, response, err = propertiesSvc.GetKey("foo-bar", "b/a/z", "k-ey")
Expand All @@ -22,7 +22,6 @@ func TestPropertiesService_GetKey(t *testing.T) {
assert.ErrorContains(t, err, "splunk: bar does not exist")
assert.Equal(t, response.StatusCode, 404)


_, response, _ = propertiesSvc.GetKey("server", "general", "pass4SymmKey")
assert.Equal(t, response.StatusCode, 200)

Expand Down
2 changes: 2 additions & 0 deletions clients/splunk/splunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type API struct {
Introspection *IntrospectionService
AccessControl *AccessControlService
Properties *PropertiesService
Deployment *DeploymentService
// XXX ...
}

Expand All @@ -39,6 +40,7 @@ func (params *APIParams) NewAPI(ctx context.Context) *API {
Introspection: newIntrospectionService(client.New()),
AccessControl: newAccessControlService(client.New()),
Properties: newPropertiesService(client.New()),
Deployment: newDeploymentService(client.New()),
}
}

Expand Down
1 change: 1 addition & 0 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type splunkConfig struct {
Username string `json:"username" structs:"username"`
Password string `json:"password" structs:"password"`
URL string `json:"url" structs:"url"`
IsStandalone bool `json:"is_standalone" structs:"is_standalone"`
AllowedRoles []string `json:"allowed_roles" structs:"allowed_roles"`
Verify bool `json:"verify" structs:"verify"`
InsecureTLS bool `json:"insecure_tls" structs:"insecure_tls"`
Expand Down
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ require (
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.1.2
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v0.1.1 // indirect
Expand Down
13 changes: 11 additions & 2 deletions path_config_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ func (b *backend) pathConfigConnection() *framework.Path {
Type: framework.TypeString,
Description: "Splunk server URL.",
},
"is_standalone": &framework.FieldSchema{
Type: framework.TypeBool,
Description: `Whether this is a standalone or multi-node deployment. Default: false`,
Default: false,
},
"allowed_roles": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: trimIndent(`
Expand All @@ -59,7 +64,7 @@ func (b *backend) pathConfigConnection() *framework.Path {
Default: "tls12",
Description: trimIndent(`
Minimum TLS version to use. Accepted values are "tls10", "tls11" or
"tls12". Defaults to "tls12".`),
"tls12". Default: "tls12".`),
},
"pem_bundle": &framework.FieldSchema{
Type: framework.TypeString,
Expand All @@ -82,7 +87,7 @@ func (b *backend) pathConfigConnection() *framework.Path {
"connect_timeout": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: "30s",
Description: `The connection timeout to use. Default: 30s.`,
Description: `The connection timeout to use. Default: 30s.`,
},
},

Expand Down Expand Up @@ -165,6 +170,10 @@ func (b *backend) connectionWriteHandler(ctx context.Context, req *logical.Reque
if config.URL == "" {
return logical.ErrorResponse("empty URL"), nil
}
if isStandalone, ok := getValue(data, req.Operation, "is_standalone"); ok {
config.IsStandalone = isStandalone.(bool)
}

if verifyRaw, ok := getValue(data, req.Operation, "verify"); ok {
config.Verify = verifyRaw.(bool)
}
Expand Down
157 changes: 154 additions & 3 deletions path_creds_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ import (
"github.com/splunk/vault-plugin-splunk/clients/splunk"
)

const (
SEARCHHEAD = "search_head"
INDEXER = "indexer"
)

func (b *backend) pathCredsCreate() *framework.Path {
return &framework.Path{
Pattern: "creds/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the role",
},
Expand All @@ -30,7 +35,30 @@ func (b *backend) pathCredsCreate() *framework.Path {
}
}

func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
func (b *backend) pathCredsCreateMulti() *framework.Path {
return &framework.Path{
Pattern: "creds/" + framework.GenericNameRegex("name") + "/" + framework.GenericNameRegex("node_fqdn"),
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the role",
},
"node_fqdn": {
Type: framework.TypeString,
Description: "FQDN for the Splunk Stack node",
},
},

Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.credsReadHandler,
},

HelpSynopsis: pathCredsCreateHelpSyn,
HelpDescription: pathCredsCreateHelpDesc,
}
}

func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
role, err := roleConfigLoad(ctx, req.Storage, name)
if err != nil {
Expand All @@ -50,7 +78,7 @@ func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d
return nil, fmt.Errorf("%q is not an allowed role for connection %q", name, role.Connection)
}

conn, err := b.ensureConnection(ctx, role.Connection, config)
conn, err := b.ensureConnection(ctx, config)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -100,6 +128,129 @@ func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d
return resp, nil
}

func findNode(nodeFQDN string, hosts []splunk.ServerInfoEntry) (bool, error) {
for _, host := range hosts {
// check if node_fqdn is in either of HostFQDN or Host. User might not always the FQDN on the cli input
if host.Content.HostFQDN == nodeFQDN || host.Content.Host == nodeFQDN {
// Return true if the requested node is a search head
for _, role := range host.Content.Roles {
if role == SEARCHHEAD {
return true, nil
}
}
return false, fmt.Errorf("host: %s isn't search head; creating ephemeral creds is only supported for search heads", nodeFQDN)
}
}
return false, fmt.Errorf("host: %s not found", nodeFQDN)
}

func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
node, _ := d.GetOk("node_fqdn")
nodeFQDN := node.(string)
role, err := roleConfigLoad(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse(fmt.Sprintf("role not found: %q", name)), nil
}

config, err := connectionConfigLoad(ctx, req.Storage, role.Connection)
if err != nil {
return nil, err
}
// Check if isStandalone is set
if config.IsStandalone {
return nil, fmt.Errorf("expected is_standalone to be unset for connection: %q", role.Connection)
}

// If role name isn't in allowed roles, send back a permission denied.
if !strutil.StrListContains(config.AllowedRoles, "*") && !strutil.StrListContainsGlob(config.AllowedRoles, name) {
return nil, fmt.Errorf("%q is not an allowed role for connection %q", name, role.Connection)
}

conn, err := b.ensureConnection(ctx, config)
if err != nil {
return nil, err
}

nodes, _, err := conn.Deployment.GetSearchPeers()
if err != nil {
b.Logger().Error("Error while reading SearchPeers from cluster master", err)
return nil, fmt.Errorf("unable to read searchpeers from cluster master")
}
_, err = findNode(nodeFQDN, nodes)
if err != nil {
return nil, err
}

// Re-create connection for node
config.URL = "https://" + nodeFQDN + ":8089"
// XXX config.ID = ""
conn, err = config.newConnection(ctx) // XXX cache
if err != nil {
return nil, err
}
// Generate credentials
userUUID, err := uuid.GenerateUUID()
if err != nil {
return nil, err
}
userPrefix := role.UserPrefix
if role.UserPrefix == defaultUserPrefix {
userPrefix = fmt.Sprintf("%s_%s", role.UserPrefix, req.DisplayName)
}
username := fmt.Sprintf("%s_%s", userPrefix, userUUID)
passwd, err := uuid.GenerateUUID()
if err != nil {
return nil, errwrap.Wrapf("error generating new password {{err}}", err)
}
conn.Params().BaseURL = nodeFQDN
opts := splunk.CreateUserOptions{
Name: username,
Password: passwd,
Roles: role.Roles,
DefaultApp: role.DefaultApp,
Email: role.Email,
TZ: role.TZ,
}
if _, _, err := conn.AccessControl.Authentication.Users.Create(&opts); err != nil {
return nil, err
}

resp := b.Secret(secretCredsType).Response(map[string]interface{}{
// return to user
"username": username,
"password": passwd,
"roles": role.Roles,
"connection": role.Connection,
"url": conn.Params().BaseURL,
}, map[string]interface{}{
// store (with lease)
"username": username,
"role": name,
"connection": role.Connection,
"node_fqdn": nodeFQDN,
})
resp.Secret.TTL = role.DefaultTTL
resp.Secret.MaxTTL = role.MaxTTL

return resp, nil
}

func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
node_fqdn, present := d.GetOk("node_fqdn")
// if node_fqdn is specified then the treat the request for a multi-node deployment
if present {
b.Logger().Debug(fmt.Sprintf("node_fqdn: [%s] specified for role: [%s]. using clustered mode getting temporary creds", node_fqdn.(string), name))
return b.credsReadHandlerMulti(ctx, req, d)
}
b.Logger().Debug(fmt.Sprintf("node_fqdn not specified for role: [%s]. using standalone mode getting temporary creds", name))
return b.credsReadHandlerStandalone(ctx, req, d)
}

const pathCredsCreateHelpSyn = `
Request Splunk credentials for a certain role.
`
Expand Down
2 changes: 1 addition & 1 deletion path_rotate_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (b *backend) rotateRootUpdateHandler(ctx context.Context, req *logical.Requ
if err != nil {
return nil, err
}
conn, err := b.ensureConnection(ctx, name, oldConfig)
conn, err := b.ensureConnection(ctx, oldConfig)
if err != nil {
return nil, err
}
Expand Down
Loading