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

Commit 3ba890f

Browse files
authored
Merge pull request #17 from splunk/ephemeral_creds_1
WIP: Add support for creating ephemeral users for multi-node Splunk deployments
2 parents 4d55d1c + 02fbc21 commit 3ba890f

11 files changed

+220
-16
lines changed

backend.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func newBackend() logical.Backend {
4141
b.pathRolesList(),
4242
b.pathRoles(),
4343
b.pathCredsCreate(),
44+
b.pathCredsCreateMulti(),
4445
},
4546
Secrets: []*framework.Secret{
4647
b.pathSecretCreds(),
@@ -53,7 +54,7 @@ func newBackend() logical.Backend {
5354
return &b
5455
}
5556

56-
func (b *backend) ensureConnection(ctx context.Context, name string, config *splunkConfig) (*splunk.API, error) {
57+
func (b *backend) ensureConnection(ctx context.Context, config *splunkConfig) (*splunk.API, error) {
5758
if conn, ok := b.conn.Load(config.ID); ok {
5859
return conn.(*splunk.API), nil
5960
}

clients/splunk/deployment.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package splunk
2+
3+
// DeploymentService encapsulates the Deployment portion of the Splunk API
4+
type DeploymentService struct {
5+
client *Client
6+
}
7+
8+
func newDeploymentService(client *Client) *DeploymentService {
9+
return &DeploymentService{
10+
client: client,
11+
}
12+
}
13+
14+
// GetSearchPeers returns information about all search peers
15+
func (d *DeploymentService) GetSearchPeers() ([]ServerInfoEntry, *Response, error) {
16+
info := make([]ServerInfoEntry, 0)
17+
resp, err := Receive(d.client.New().Get("search/distributed/peers"), &info)
18+
return info, resp, err
19+
}

clients/splunk/properties.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ type Entry struct {
2929
// stringResponseDecoder decodes http response string
3030
// Properties API operates on particular key in the configuration file.
3131
// CRUD for properties API returns JSON/XML encoded response for error cases and returns a string response for success
32-
type stringResponseDecoder struct{
32+
type stringResponseDecoder struct {
3333
}
3434

35-
func getPropertiesUri(file string, stanza string, key string) (string) {
35+
func getPropertiesUri(file string, stanza string, key string) string {
3636
return fmt.Sprintf("properties/%s/%s/%s", url.PathEscape(file), url.PathEscape(stanza), url.PathEscape(key))
3737
}
3838

@@ -74,4 +74,4 @@ func (p *PropertiesService) GetKey(file string, stanza string, key string) (*str
7474
return nil, resp, relevantError(err, apiError)
7575
}
7676
return &output.Value, resp, relevantError(err, apiError)
77-
}
77+
}

clients/splunk/properties_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func TestPropertiesService_GetKey(t *testing.T) {
1212
_, response, err := propertiesSvc.GetKey("foo", "bar", "key")
1313
assert.ErrorContains(t, err, "splunk: foo does not exist")
1414
assert.Equal(t, response.StatusCode, 404)
15-
_, response, err = propertiesSvc.GetKey("b/a/z","b-ar", "k-ey")
15+
_, response, err = propertiesSvc.GetKey("b/a/z", "b-ar", "k-ey")
1616
assert.ErrorContains(t, err, "ERROR splunk: Directory traversal risk in /nobody/system/b/a/z at segment \"b/a/z\"")
1717
assert.Equal(t, response.StatusCode, 403)
1818
_, response, err = propertiesSvc.GetKey("foo-bar", "b/a/z", "k-ey")
@@ -22,7 +22,6 @@ func TestPropertiesService_GetKey(t *testing.T) {
2222
assert.ErrorContains(t, err, "splunk: bar does not exist")
2323
assert.Equal(t, response.StatusCode, 404)
2424

25-
2625
_, response, _ = propertiesSvc.GetKey("server", "general", "pass4SymmKey")
2726
assert.Equal(t, response.StatusCode, 200)
2827

clients/splunk/splunk.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type API struct {
2020
Introspection *IntrospectionService
2121
AccessControl *AccessControlService
2222
Properties *PropertiesService
23+
Deployment *DeploymentService
2324
// XXX ...
2425
}
2526

@@ -39,6 +40,7 @@ func (params *APIParams) NewAPI(ctx context.Context) *API {
3940
Introspection: newIntrospectionService(client.New()),
4041
AccessControl: newAccessControlService(client.New()),
4142
Properties: newPropertiesService(client.New()),
43+
Deployment: newDeploymentService(client.New()),
4244
}
4345
}
4446

conn.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type splunkConfig struct {
3030
Username string `json:"username" structs:"username"`
3131
Password string `json:"password" structs:"password"`
3232
URL string `json:"url" structs:"url"`
33+
IsStandalone bool `json:"is_standalone" structs:"is_standalone"`
3334
AllowedRoles []string `json:"allowed_roles" structs:"allowed_roles"`
3435
Verify bool `json:"verify" structs:"verify"`
3536
InsecureTLS bool `json:"insecure_tls" structs:"insecure_tls"`

go.mod

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ require (
4040
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
4141
github.com/mitchellh/mapstructure v1.1.2
4242
github.com/mitchellh/reflectwalk v1.0.1 // indirect
43-
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
44-
github.com/modern-go/reflect2 v1.0.1 // indirect
4543
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
4644
github.com/opencontainers/image-spec v1.0.1 // indirect
4745
github.com/opencontainers/runc v0.1.1 // indirect

path_config_connection.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ func (b *backend) pathConfigConnection() *framework.Path {
3333
Type: framework.TypeString,
3434
Description: "Splunk server URL.",
3535
},
36+
"is_standalone": &framework.FieldSchema{
37+
Type: framework.TypeBool,
38+
Description: `Whether this is a standalone or multi-node deployment. Default: false`,
39+
Default: false,
40+
},
3641
"allowed_roles": &framework.FieldSchema{
3742
Type: framework.TypeCommaStringSlice,
3843
Description: trimIndent(`
@@ -59,7 +64,7 @@ func (b *backend) pathConfigConnection() *framework.Path {
5964
Default: "tls12",
6065
Description: trimIndent(`
6166
Minimum TLS version to use. Accepted values are "tls10", "tls11" or
62-
"tls12". Defaults to "tls12".`),
67+
"tls12". Default: "tls12".`),
6368
},
6469
"pem_bundle": &framework.FieldSchema{
6570
Type: framework.TypeString,
@@ -82,7 +87,7 @@ func (b *backend) pathConfigConnection() *framework.Path {
8287
"connect_timeout": &framework.FieldSchema{
8388
Type: framework.TypeDurationSecond,
8489
Default: "30s",
85-
Description: `The connection timeout to use. Default: 30s.`,
90+
Description: `The connection timeout to use. Default: 30s.`,
8691
},
8792
},
8893

@@ -165,6 +170,10 @@ func (b *backend) connectionWriteHandler(ctx context.Context, req *logical.Reque
165170
if config.URL == "" {
166171
return logical.ErrorResponse("empty URL"), nil
167172
}
173+
if isStandalone, ok := getValue(data, req.Operation, "is_standalone"); ok {
174+
config.IsStandalone = isStandalone.(bool)
175+
}
176+
168177
if verifyRaw, ok := getValue(data, req.Operation, "verify"); ok {
169178
config.Verify = verifyRaw.(bool)
170179
}

path_creds_create.go

Lines changed: 154 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@ import (
1111
"github.com/splunk/vault-plugin-splunk/clients/splunk"
1212
)
1313

14+
const (
15+
SEARCHHEAD = "search_head"
16+
INDEXER = "indexer"
17+
)
18+
1419
func (b *backend) pathCredsCreate() *framework.Path {
1520
return &framework.Path{
1621
Pattern: "creds/" + framework.GenericNameRegex("name"),
1722
Fields: map[string]*framework.FieldSchema{
18-
"name": &framework.FieldSchema{
23+
"name": {
1924
Type: framework.TypeString,
2025
Description: "Name of the role",
2126
},
@@ -30,7 +35,30 @@ func (b *backend) pathCredsCreate() *framework.Path {
3035
}
3136
}
3237

33-
func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
38+
func (b *backend) pathCredsCreateMulti() *framework.Path {
39+
return &framework.Path{
40+
Pattern: "creds/" + framework.GenericNameRegex("name") + "/" + framework.GenericNameRegex("node_fqdn"),
41+
Fields: map[string]*framework.FieldSchema{
42+
"name": {
43+
Type: framework.TypeString,
44+
Description: "Name of the role",
45+
},
46+
"node_fqdn": {
47+
Type: framework.TypeString,
48+
Description: "FQDN for the Splunk Stack node",
49+
},
50+
},
51+
52+
Callbacks: map[logical.Operation]framework.OperationFunc{
53+
logical.ReadOperation: b.credsReadHandler,
54+
},
55+
56+
HelpSynopsis: pathCredsCreateHelpSyn,
57+
HelpDescription: pathCredsCreateHelpDesc,
58+
}
59+
}
60+
61+
func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
3462
name := d.Get("name").(string)
3563
role, err := roleConfigLoad(ctx, req.Storage, name)
3664
if err != nil {
@@ -50,7 +78,7 @@ func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d
5078
return nil, fmt.Errorf("%q is not an allowed role for connection %q", name, role.Connection)
5179
}
5280

53-
conn, err := b.ensureConnection(ctx, role.Connection, config)
81+
conn, err := b.ensureConnection(ctx, config)
5482
if err != nil {
5583
return nil, err
5684
}
@@ -100,6 +128,129 @@ func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d
100128
return resp, nil
101129
}
102130

131+
func findNode(nodeFQDN string, hosts []splunk.ServerInfoEntry) (bool, error) {
132+
for _, host := range hosts {
133+
// check if node_fqdn is in either of HostFQDN or Host. User might not always the FQDN on the cli input
134+
if host.Content.HostFQDN == nodeFQDN || host.Content.Host == nodeFQDN {
135+
// Return true if the requested node is a search head
136+
for _, role := range host.Content.Roles {
137+
if role == SEARCHHEAD {
138+
return true, nil
139+
}
140+
}
141+
return false, fmt.Errorf("host: %s isn't search head; creating ephemeral creds is only supported for search heads", nodeFQDN)
142+
}
143+
}
144+
return false, fmt.Errorf("host: %s not found", nodeFQDN)
145+
}
146+
147+
func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
148+
name := d.Get("name").(string)
149+
node, _ := d.GetOk("node_fqdn")
150+
nodeFQDN := node.(string)
151+
role, err := roleConfigLoad(ctx, req.Storage, name)
152+
if err != nil {
153+
return nil, err
154+
}
155+
if role == nil {
156+
return logical.ErrorResponse(fmt.Sprintf("role not found: %q", name)), nil
157+
}
158+
159+
config, err := connectionConfigLoad(ctx, req.Storage, role.Connection)
160+
if err != nil {
161+
return nil, err
162+
}
163+
// Check if isStandalone is set
164+
if config.IsStandalone {
165+
return nil, fmt.Errorf("expected is_standalone to be unset for connection: %q", role.Connection)
166+
}
167+
168+
// If role name isn't in allowed roles, send back a permission denied.
169+
if !strutil.StrListContains(config.AllowedRoles, "*") && !strutil.StrListContainsGlob(config.AllowedRoles, name) {
170+
return nil, fmt.Errorf("%q is not an allowed role for connection %q", name, role.Connection)
171+
}
172+
173+
conn, err := b.ensureConnection(ctx, config)
174+
if err != nil {
175+
return nil, err
176+
}
177+
178+
nodes, _, err := conn.Deployment.GetSearchPeers()
179+
if err != nil {
180+
b.Logger().Error("Error while reading SearchPeers from cluster master", err)
181+
return nil, fmt.Errorf("unable to read searchpeers from cluster master")
182+
}
183+
_, err = findNode(nodeFQDN, nodes)
184+
if err != nil {
185+
return nil, err
186+
}
187+
188+
// Re-create connection for node
189+
config.URL = "https://" + nodeFQDN + ":8089"
190+
// XXX config.ID = ""
191+
conn, err = config.newConnection(ctx) // XXX cache
192+
if err != nil {
193+
return nil, err
194+
}
195+
// Generate credentials
196+
userUUID, err := uuid.GenerateUUID()
197+
if err != nil {
198+
return nil, err
199+
}
200+
userPrefix := role.UserPrefix
201+
if role.UserPrefix == defaultUserPrefix {
202+
userPrefix = fmt.Sprintf("%s_%s", role.UserPrefix, req.DisplayName)
203+
}
204+
username := fmt.Sprintf("%s_%s", userPrefix, userUUID)
205+
passwd, err := uuid.GenerateUUID()
206+
if err != nil {
207+
return nil, errwrap.Wrapf("error generating new password {{err}}", err)
208+
}
209+
conn.Params().BaseURL = nodeFQDN
210+
opts := splunk.CreateUserOptions{
211+
Name: username,
212+
Password: passwd,
213+
Roles: role.Roles,
214+
DefaultApp: role.DefaultApp,
215+
Email: role.Email,
216+
TZ: role.TZ,
217+
}
218+
if _, _, err := conn.AccessControl.Authentication.Users.Create(&opts); err != nil {
219+
return nil, err
220+
}
221+
222+
resp := b.Secret(secretCredsType).Response(map[string]interface{}{
223+
// return to user
224+
"username": username,
225+
"password": passwd,
226+
"roles": role.Roles,
227+
"connection": role.Connection,
228+
"url": conn.Params().BaseURL,
229+
}, map[string]interface{}{
230+
// store (with lease)
231+
"username": username,
232+
"role": name,
233+
"connection": role.Connection,
234+
"node_fqdn": nodeFQDN,
235+
})
236+
resp.Secret.TTL = role.DefaultTTL
237+
resp.Secret.MaxTTL = role.MaxTTL
238+
239+
return resp, nil
240+
}
241+
242+
func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
243+
name := d.Get("name").(string)
244+
node_fqdn, present := d.GetOk("node_fqdn")
245+
// if node_fqdn is specified then the treat the request for a multi-node deployment
246+
if present {
247+
b.Logger().Debug(fmt.Sprintf("node_fqdn: [%s] specified for role: [%s]. using clustered mode getting temporary creds", node_fqdn.(string), name))
248+
return b.credsReadHandlerMulti(ctx, req, d)
249+
}
250+
b.Logger().Debug(fmt.Sprintf("node_fqdn not specified for role: [%s]. using standalone mode getting temporary creds", name))
251+
return b.credsReadHandlerStandalone(ctx, req, d)
252+
}
253+
103254
const pathCredsCreateHelpSyn = `
104255
Request Splunk credentials for a certain role.
105256
`

path_rotate_root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (b *backend) rotateRootUpdateHandler(ctx context.Context, req *logical.Requ
3434
if err != nil {
3535
return nil, err
3636
}
37-
conn, err := b.ensureConnection(ctx, name, oldConfig)
37+
conn, err := b.ensureConnection(ctx, oldConfig)
3838
if err != nil {
3939
return nil, err
4040
}

0 commit comments

Comments
 (0)