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

Commit

Permalink
Merge pull request #17 from splunk/ephemeral_creds_1
Browse files Browse the repository at this point in the history
WIP: Add support for creating ephemeral users for multi-node Splunk deployments
  • Loading branch information
michaelw authored Jan 21, 2020
2 parents 4d55d1c + 02fbc21 commit 3ba890f
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 16 deletions.
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

0 comments on commit 3ba890f

Please sign in to comment.