diff --git a/backend_test.go b/backend_test.go index 4a5bd56..1834338 100644 --- a/backend_test.go +++ b/backend_test.go @@ -20,20 +20,29 @@ func TestBackend_basic(t *testing.T) { t.Fatal(err) } - roleConfig := roleConfig{ - Connection: "testconn", - Roles: []string{"admin"}, - UserPrefix: defaultUserPrefix, + schemes := []string{ + userIDSchemeUUID4_v0_5_0, + userIDSchemeUUID4, + userIDSchemeBase58_64, + userIDSchemeBase58_128, } + for _, scheme := range schemes { + roleConfig := roleConfig{ + Connection: "testconn", + Roles: []string{"admin"}, + UserPrefix: defaultUserPrefix, + UserIDScheme: scheme, + } - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t), - testAccStepRole(t, "test", roleConfig), - testAccStepCredsRead(t, "test"), - }, - }) + logicaltest.Test(t, logicaltest.TestCase{ + LogicalBackend: b, + Steps: []logicaltest.TestStep{ + testAccStepConfig(t), + testAccStepRole(t, "test", roleConfig), + testAccStepCredsRead(t, "test"), + }, + }) + } } func TestBackend_RotateRoot(t *testing.T) { @@ -92,6 +101,7 @@ func TestBackend_RoleCRUD(t *testing.T) { AllowedServerRoles: []string{"*"}, PasswordSpec: DefaultPasswordSpec(), UserPrefix: "my-custom-prefix", + UserIDScheme: userIDSchemeUUID4, } logicaltest.Test(t, logicaltest.TestCase{ @@ -105,17 +115,23 @@ func TestBackend_RoleCRUD(t *testing.T) { testAccStepRoleDelete(t, "test"), }, }) - emptyUserPrefixConfig := roleConfig{ - Connection: "testconn", - Roles: []string{"admin"}, - UserPrefix: "", - } + emptyUserPrefixConfig := testRoleConfig + emptyUserPrefixConfig.UserPrefix = "" logicaltest.Test(t, logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ testEmptyUserPrefix(t, "test", emptyUserPrefixConfig), }, }) + + userIDSchemeConfig := testRoleConfig + userIDSchemeConfig.UserIDScheme = "-invalid-" + logicaltest.Test(t, logicaltest.TestCase{ + LogicalBackend: b, + Steps: []logicaltest.TestStep{ + testUserIDScheme(t, "test", "-invalid-", userIDSchemeConfig), + }, + }) } // Test steps @@ -219,6 +235,22 @@ func testEmptyUserPrefix(t *testing.T, role string, config roleConfig) logicalte } } +func testUserIDScheme(t *testing.T, role, idScheme string, config roleConfig) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.CreateOperation, + Path: rolesPrefix + role, + Data: config.toResponseData(), + ErrorOk: true, + Check: func(resp *logical.Response) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + assert.Error(t, resp.Error(), fmt.Sprintf("invalid user_id_scheme: %q", idScheme)) + return nil + }, + } +} + func testAccStepCredsRead(t *testing.T, role string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, @@ -235,8 +267,11 @@ func testAccStepCredsRead(t *testing.T, role string) logicaltest.TestStep { if err := mapstructure.Decode(resp.Data, &d); err != nil { return err } - t.Logf("[WARN] Generated credentials: %+v", d) - // XXXX check that generated user can login + // check that generated user can login + conn := splunk.NewTestSplunkClient(d.URL, d.Username, d.Password) + _, _, err := conn.Introspection.ServerInfo() + assert.NilError(t, err) + // XXXX check that generated user is deleted if lease expires return nil }, diff --git a/go.mod b/go.mod index 441516d..ebc8a68 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ 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/mr-tron/base58 v1.1.3 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 diff --git a/go.sum b/go.sum index 8f5a9b5..57369db 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= diff --git a/path_creds_create.go b/path_creds_create.go index fad51c6..f14f28a 100644 --- a/path_creds_create.go +++ b/path_creds_create.go @@ -252,7 +252,18 @@ func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d } func generateUserID(roleConfig *roleConfig) (string, error) { - return uuid.GenerateUUID() + switch roleConfig.UserIDScheme { + case userIDSchemeUUID4_v0_5_0: + fallthrough + case userIDSchemeUUID4: + return uuid.GenerateUUID() + case userIDSchemeBase58_64: + return GenerateShortUUID(8) + case userIDSchemeBase58_128: + return GenerateShortUUID(16) + default: + return "", fmt.Errorf("invalid user_id_scheme: %q", roleConfig.UserIDScheme) + } } func generateUserPassword(roleConfig *roleConfig) (string, error) { diff --git a/path_roles.go b/path_roles.go index 456e551..d51168b 100644 --- a/path_roles.go +++ b/path_roles.go @@ -2,14 +2,22 @@ package splunk import ( "context" + "fmt" "time" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) -const rolesPrefix = "roles/" -const defaultUserPrefix = "vault" +const ( + rolesPrefix = "roles/" + defaultUserPrefix = "vault" + + userIDSchemeUUID4_v0_5_0 = "" + userIDSchemeUUID4 = "uuid4" + userIDSchemeBase58_64 = "base58-64" + userIDSchemeBase58_128 = "base58-128" +) func (b *backend) pathRoles() *framework.Path { return &framework.Path{ @@ -58,9 +66,15 @@ func (b *backend) pathRoles() *framework.Path { }, "user_prefix": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Prefix for creating new users", + Description: "Prefix for creating new users.", Default: defaultUserPrefix, }, + "user_id_scheme": &framework.FieldSchema{ + Type: framework.TypeLowerCaseString, + Description: fmt.Sprintf("ID generation scheme (%s, %s, %s). Default: %s", + userIDSchemeUUID4, userIDSchemeBase58_64, userIDSchemeBase58_128, userIDSchemeBase58_64), + Default: userIDSchemeBase58_64, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.rolesReadHandler, @@ -149,6 +163,18 @@ func (b *backend) rolesWriteHandler(ctx context.Context, req *logical.Request, d return logical.ErrorResponse("user_prefix can't be set to empty string"), nil } + if userIDSchemeRaw, ok := getValue(data, req.Operation, "user_id_scheme"); ok { + role.UserIDScheme = userIDSchemeRaw.(string) + } + switch role.UserIDScheme { + case userIDSchemeUUID4_v0_5_0: + case userIDSchemeUUID4: + case userIDSchemeBase58_64: + case userIDSchemeBase58_128: + default: + return logical.ErrorResponse("invalid user_id_scheme: %q", role.UserIDScheme), nil + } + if err := role.store(ctx, req.Storage, name); err != nil { return nil, err } diff --git a/role.go b/role.go index db96d80..25eab46 100644 --- a/role.go +++ b/role.go @@ -18,11 +18,12 @@ type roleConfig struct { PasswordSpec *PasswordSpec `json:"password_spec" structs:"password_spec"` // Splunk user attributes - Roles []string `json:"roles" structs:"roles"` - DefaultApp string `json:"default_app,omitempty" structs:"default_app"` - Email string `json:"email,omitempty" structs:"email"` - TZ string `json:"tz,omitempty" structs:"tz"` - UserPrefix string `json:"user_prefix,omitempty" structs:"user_prefix"` + Roles []string `json:"roles" structs:"roles"` + DefaultApp string `json:"default_app,omitempty" structs:"default_app"` + Email string `json:"email,omitempty" structs:"email"` + TZ string `json:"tz,omitempty" structs:"tz"` + UserPrefix string `json:"user_prefix,omitempty" structs:"user_prefix"` + UserIDScheme string `json:"user_id_scheme,omitempty" structs:"user_id_scheme"` } // Role returns nil if role named `name` does not exist in `storage`, otherwise diff --git a/uuid.go b/uuid.go new file mode 100644 index 0000000..dc4d5ac --- /dev/null +++ b/uuid.go @@ -0,0 +1,18 @@ +package splunk + +import ( + "github.com/hashicorp/go-uuid" + "github.com/mr-tron/base58" +) + +func GenerateShortUUID(size int) (string, error) { + bytes, err := uuid.GenerateRandomBytes(size) + if err != nil { + return "", err + } + return FormatShortUUID(bytes), nil +} + +func FormatShortUUID(bytes []byte) string { + return base58.Encode(bytes) +} diff --git a/uuid_test.go b/uuid_test.go new file mode 100644 index 0000000..7b642f8 --- /dev/null +++ b/uuid_test.go @@ -0,0 +1,60 @@ +package splunk + +import ( + "fmt" + "testing" + + "github.com/mr-tron/base58" + "gotest.tools/assert" +) + +func TestGenerateShortUUID(t *testing.T) { + for _, size := range []int{8, 16} { + uuid, err := GenerateShortUUID(size) + assert.NilError(t, err) + fmt.Println(uuid) + bytes, err := base58.Decode(uuid) + assert.NilError(t, err) + assert.Equal(t, size, len(bytes)) + } +} + +func TestFormatShortUUID(t *testing.T) { + type args struct { + bytes []byte + } + tests := []struct { + name string + args args + want string + }{ + { + name: "0_x_8", + args: args{[]byte{0, 0, 0, 0, 0, 0, 0, 0}}, + want: "11111111", + }, + { + name: "1_x_8", + args: args{[]byte{0, 0, 0, 0, 0, 0, 0, 1}}, + want: "11111112", + }, + { + name: "255_x_8", + args: args{[]byte{255, 255, 255, 255, 255, 255, 255, 255}}, + want: "jpXCZedGfVQ", + }, + { + name: "uuid4", + args: args{[]byte{0xb3, 0x3b, 0x6b, 0x76, 0xcb, 0x1f, 0xbe, 0x28, 0xbd, 0x5b, 0x86, 0xca, 0x76, 0x23, 0x72, 0x72}}, + want: "P8gD5AcMf2n6FkGz9nydEZ", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FormatShortUUID(tt.args.bytes); got != tt.want { + t.Errorf("FormatShortUUID() = %v, want %v", got, tt.want) + } + }) + } +}