diff --git a/backend.go b/backend.go index 8787184e..b8c64bcf 100644 --- a/backend.go +++ b/backend.go @@ -12,8 +12,20 @@ import ( ) const ( - configPath string = "config" - rolePrefix string = "role/" + configPath = "config" + rolePrefix = "role/" + + // aliasNameSourceUnset provides backwards compatibility with preexisting roles. + aliasNameSourceUnset = "" + aliasNameSourceSAToken = "sa_token" + aliasNameSourceSAPath = "sa_path" + aliasNameSourceDefault = aliasNameSourceSAToken +) + +var ( + // when adding new alias name sources make sure to update the corresponding FieldSchema description in path_role.go + aliasNameSources = []string{aliasNameSourceSAToken, aliasNameSourceSAPath} + errInvalidAliasNameSource = fmt.Errorf(`invalid alias_name_source, must be one of: %s`, strings.Join(aliasNameSources, ", ")) ) // kubeAuthBackend implements logical.Backend @@ -132,6 +144,15 @@ func (b *kubeAuthBackend) role(ctx context.Context, s logical.Storage, name stri return role, nil } +func validateAliasNameSource(source string) error { + for _, s := range aliasNameSources { + if s == source { + return nil + } + } + return errInvalidAliasNameSource +} + var backendHelp string = ` The Kubernetes Auth Backend allows authentication for Kubernetes service accounts. ` diff --git a/path_login.go b/path_login.go index 213d3ddb..a1eaecea 100644 --- a/path_login.go +++ b/path_login.go @@ -55,14 +55,14 @@ func pathLogin(b *kubeAuthBackend) *framework.Path { // pathLogin is used to authenticate to this backend func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - roleName := data.Get("role").(string) - if len(roleName) == 0 { - return logical.ErrorResponse("missing role"), nil + roleName, resp := b.getFieldValueStr(data, "role") + if resp != nil { + return resp, nil } - jwtStr := data.Get("jwt").(string) - if len(jwtStr) == 0 { - return logical.ErrorResponse("missing jwt"), nil + jwtStr, resp := b.getFieldValueStr(data, "jwt") + if resp != nil { + return resp, nil } b.l.RLock() @@ -73,7 +73,7 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d return nil, err } if role == nil { - return logical.ErrorResponse(fmt.Sprintf("invalid role name \"%s\"", roleName)), nil + return logical.ErrorResponse(fmt.Sprintf("invalid role name %q", roleName)), nil } // Check for a CIDR match. @@ -100,6 +100,11 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d return nil, err } + aliasName, err := b.getAliasName(role, serviceAccount) + if err != nil { + return nil, err + } + // look up the JWT token in the kubernetes API err = serviceAccount.lookup(ctx, jwtStr, b.reviewFactory(config)) if err != nil { @@ -107,11 +112,15 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d return nil, logical.ErrPermissionDenied } + uid, err := serviceAccount.uid() + if err != nil { + return nil, err + } auth := &logical.Auth{ Alias: &logical.Alias{ - Name: serviceAccount.uid(), + Name: aliasName, Metadata: map[string]string{ - "service_account_uid": serviceAccount.uid(), + "service_account_uid": uid, "service_account_name": serviceAccount.name(), "service_account_namespace": serviceAccount.namespace(), "service_account_secret_name": serviceAccount.SecretName, @@ -121,7 +130,7 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d "role": roleName, }, Metadata: map[string]string{ - "service_account_uid": serviceAccount.uid(), + "service_account_uid": uid, "service_account_name": serviceAccount.name(), "service_account_namespace": serviceAccount.namespace(), "service_account_secret_name": serviceAccount.SecretName, @@ -137,12 +146,48 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d }, nil } +func (b *kubeAuthBackend) getFieldValueStr(data *framework.FieldData, param string) (string, *logical.Response) { + val := data.Get(param).(string) + if len(val) == 0 { + return "", logical.ErrorResponse("missing %s", param) + } + return val, nil +} + +func (b *kubeAuthBackend) getAliasName(role *roleStorageEntry, serviceAccount *serviceAccount) (string, error) { + switch role.AliasNameSource { + case aliasNameSourceSAToken, aliasNameSourceUnset: + uid, err := serviceAccount.uid() + if err != nil { + return "", err + } + return uid, nil + case aliasNameSourceSAPath: + return fmt.Sprintf("%s/%s", serviceAccount.Namespace, serviceAccount.Name), nil + default: + return "", fmt.Errorf("unknown alias_name_source %q", role.AliasNameSource) + } +} + // aliasLookahead returns the alias object with the SA UID from the JWT // Claims. -func (b *kubeAuthBackend) aliasLookahead(_ context.Context, _ *logical.Request, data *framework.FieldData) (*logical.Response, error) { - jwtStr := data.Get("jwt").(string) - if len(jwtStr) == 0 { - return logical.ErrorResponse("missing jwt"), nil +func (b *kubeAuthBackend) aliasLookahead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + roleName, resp := b.getFieldValueStr(data, "role") + if resp != nil { + return resp, nil + } + + jwtStr, resp := b.getFieldValueStr(data, "jwt") + if resp != nil { + return resp, nil + } + + role, err := b.role(ctx, req.Storage, roleName) + if err != nil { + return nil, err + } + if role == nil { + return logical.ErrorResponse(fmt.Sprintf("invalid role name %q", roleName)), nil } // Parse into JWT @@ -158,15 +203,15 @@ func (b *kubeAuthBackend) aliasLookahead(_ context.Context, _ *logical.Request, return nil, err } - saUID := sa.uid() - if saUID == "" { - return nil, errors.New("could not parse UID from claims") + aliasName, err := b.getAliasName(role, sa) + if err != nil { + return nil, err } return &logical.Response{ Auth: &logical.Auth{ Alias: &logical.Alias{ - Name: saUID, + Name: aliasName, }, }, }, nil @@ -316,11 +361,17 @@ type serviceAccount struct { // uid returns the UID for the service account, preferring the projected service // account value if found -func (s *serviceAccount) uid() string { +// return an error when the UID is empty. +func (s *serviceAccount) uid() (string, error) { + uid := s.UID if s.Kubernetes != nil && s.Kubernetes.ServiceAccount != nil { - return s.Kubernetes.ServiceAccount.UID + uid = s.Kubernetes.ServiceAccount.UID + } + + if uid == "" { + return "", errors.New("could not parse UID from claims") } - return s.UID + return uid, nil } // name returns the name for the service account, preferring the projected @@ -366,7 +417,11 @@ func (s *serviceAccount) lookup(ctx context.Context, jwtStr string, tr tokenRevi if s.name() != r.Name { return errors.New("JWT names did not match") } - if s.uid() != r.UID { + uid, err := s.uid() + if err != nil { + return err + } + if uid != r.UID { return errors.New("JWT UIDs did not match") } if s.namespace() != r.Namespace { diff --git a/path_login_test.go b/path_login_test.go index 97478187..f049aca0 100644 --- a/path_login_test.go +++ b/path_login_test.go @@ -31,18 +31,28 @@ var ( testNoPEMs = []string{testECCert, testRSACert} ) -func setupBackend(t *testing.T, pems []string, saName string, saNamespace string) (logical.Backend, logical.Storage) { - b, storage := getBackend(t) +type testBackendConfig struct { + pems []string + saName string + saNamespace string + aliasNameSource string +} - // pems := []string{testECCert, testRSACert, testMinikubePubKey} - // pems := []string{testECCert, testRSACert} - // if noPEMs { - // pems = []string{} - // } +func defaultTestBackendConfig() *testBackendConfig { + return &testBackendConfig{ + pems: testDefaultPEMs, + saName: testName, + saNamespace: testNamespace, + aliasNameSource: aliasNameSourceDefault, + } +} + +func setupBackend(t *testing.T, config *testBackendConfig) (logical.Backend, logical.Storage) { + b, storage := getBackend(t) // test no certificate data := map[string]interface{}{ - "pem_keys": pems, + "pem_keys": config.pems, "kubernetes_host": "host", "kubernetes_ca_cert": testCACert, } @@ -60,13 +70,14 @@ func setupBackend(t *testing.T, pems []string, saName string, saNamespace string } data = map[string]interface{}{ - "bound_service_account_names": saName, - "bound_service_account_namespaces": saNamespace, + "bound_service_account_names": config.saName, + "bound_service_account_namespaces": config.saNamespace, "policies": "test", "period": "3s", "ttl": "1s", "num_uses": 12, "max_ttl": "5s", + "alias_name_source": config.aliasNameSource, } req = &logical.Request{ @@ -86,7 +97,7 @@ func setupBackend(t *testing.T, pems []string, saName string, saNamespace string } func TestLogin(t *testing.T) { - b, storage := setupBackend(t, testDefaultPEMs, testName, testNamespace) + b, storage := setupBackend(t, defaultTestBackendConfig()) // Test bad inputs data := map[string]interface{}{ @@ -143,7 +154,7 @@ func TestLogin(t *testing.T) { if resp == nil || !resp.IsError() { t.Fatal("expected error") } - if resp.Error().Error() != "invalid role name \"plugin-test-bad\"" { + if resp.Error().Error() != `invalid role name "plugin-test-bad"` { t.Fatalf("unexpected error: %s", resp.Error()) } @@ -217,7 +228,9 @@ func TestLogin(t *testing.T) { } // test successful login for globbed name - b, storage = setupBackend(t, testDefaultPEMs, testGlobbedName, testNamespace) + config := defaultTestBackendConfig() + config.saName = testGlobbedName + b, storage = setupBackend(t, config) data = map[string]interface{}{ "role": "plugin-test", @@ -240,7 +253,9 @@ func TestLogin(t *testing.T) { } // test successful login for globbed namespace - b, storage = setupBackend(t, testDefaultPEMs, testName, testGlobbedNamespace) + config = defaultTestBackendConfig() + config.saNamespace = testGlobbedNamespace + b, storage = setupBackend(t, config) data = map[string]interface{}{ "role": "plugin-test", @@ -264,7 +279,7 @@ func TestLogin(t *testing.T) { } func TestLogin_ContextError(t *testing.T) { - b, storage := setupBackend(t, testDefaultPEMs, testName, testNamespace) + b, storage := setupBackend(t, defaultTestBackendConfig()) data := map[string]interface{}{ "role": "plugin-test", @@ -291,7 +306,9 @@ func TestLogin_ContextError(t *testing.T) { } func TestLogin_ECDSA_PEM(t *testing.T) { - b, storage := setupBackend(t, testNoPEMs, testName, testNamespace) + config := defaultTestBackendConfig() + config.pems = testNoPEMs + b, storage := setupBackend(t, config) // test no certificate data := map[string]interface{}{ @@ -335,7 +352,9 @@ func TestLogin_ECDSA_PEM(t *testing.T) { } func TestLogin_NoPEMs(t *testing.T) { - b, storage := setupBackend(t, testNoPEMs, testName, testNamespace) + config := defaultTestBackendConfig() + config.pems = testNoPEMs + b, storage := setupBackend(t, config) // test bad jwt service account data := map[string]interface{}{ @@ -383,7 +402,10 @@ func TestLogin_NoPEMs(t *testing.T) { } func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) { - b, storage := setupBackend(t, testDefaultPEMs, "*", "*") + config := defaultTestBackendConfig() + config.saName = "*" + config.saNamespace = "*" + b, storage := setupBackend(t, config) // Test bad inputs data := map[string]interface{}{ @@ -440,7 +462,7 @@ func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) { if resp == nil || !resp.IsError() { t.Fatal("expected error") } - if resp.Error().Error() != "invalid role name \"plugin-test-bad\"" { + if resp.Error().Error() != `invalid role name "plugin-test-bad"` { t.Fatalf("unexpected error: %s", resp.Error()) } @@ -514,7 +536,9 @@ func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) { } // test successful login for globbed name - b, storage = setupBackend(t, testDefaultPEMs, testGlobbedName, testNamespace) + config = defaultTestBackendConfig() + config.saName = testGlobbedName + b, storage = setupBackend(t, config) data = map[string]interface{}{ "role": "plugin-test", @@ -537,7 +561,9 @@ func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) { } // test successful login for globbed namespace - b, storage = setupBackend(t, testDefaultPEMs, testName, testGlobbedNamespace) + config = defaultTestBackendConfig() + config.saNamespace = testGlobbedNamespace + b, storage = setupBackend(t, config) data = map[string]interface{}{ "role": "plugin-test", @@ -561,35 +587,101 @@ func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) { } func TestAliasLookAhead(t *testing.T) { - b, storage := setupBackend(t, testDefaultPEMs, testName, testNamespace) - - // Test bad inputs - data := map[string]interface{}{ - "jwt": jwtData, - } - - req := &logical.Request{ - Operation: logical.AliasLookaheadOperation, - Path: "login", - Storage: storage, - Data: data, - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", + testCases := map[string]struct { + role string + jwt string + config *testBackendConfig + expectedAliasName string + wantErr error + }{ + "default": { + role: "plugin-test", + jwt: jwtData, + config: defaultTestBackendConfig(), + expectedAliasName: testUID, + }, + "no_role": { + jwt: jwtData, + config: defaultTestBackendConfig(), + wantErr: errors.New("missing role"), + }, + "no_jwt": { + role: "plugin-test", + config: defaultTestBackendConfig(), + wantErr: errors.New("missing jwt"), + }, + "sa_token": { + role: "plugin-test", + jwt: jwtData, + config: &testBackendConfig{ + pems: testDefaultPEMs, + saName: testName, + saNamespace: testNamespace, + aliasNameSource: aliasNameSourceSAToken, + }, + expectedAliasName: testUID, + }, + "sa_path": { + role: "plugin-test", + jwt: jwtData, + config: &testBackendConfig{ + pems: testDefaultPEMs, + saName: testName, + saNamespace: testNamespace, + aliasNameSource: aliasNameSourceSAPath, + }, + expectedAliasName: fmt.Sprintf("%s/%s", testNamespace, testName), }, } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + b, storage := setupBackend(t, tc.config) - if resp.Auth.Alias.Name != testUID { - t.Fatalf("Unexpected UID: %s", resp.Auth.Alias.Name) + req := &logical.Request{ + Operation: logical.AliasLookaheadOperation, + Path: "login", + Storage: storage, + Data: map[string]interface{}{ + "jwt": tc.jwt, + "role": tc.role, + }, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + } + + resp, err := b.HandleRequest(context.Background(), req) + if tc.wantErr != nil { + var actual error + if err != nil { + actual = err + } else if resp != nil && resp.IsError() { + actual = resp.Error() + } else { + t.Fatalf("expected error") + } + + if tc.wantErr.Error() != actual.Error() { + t.Fatalf("expected err %q, actual %q", tc.wantErr, actual) + } + } else { + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + if resp.Auth.Alias.Name != tc.expectedAliasName { + t.Fatalf("expected Alias.Name %s, actual %s", tc.expectedAliasName, resp.Auth.Alias.Name) + } + } + }) } } func TestLoginIssValidation(t *testing.T) { - b, storage := setupBackend(t, testNoPEMs, testName, testNamespace) + config := defaultTestBackendConfig() + config.pems = testNoPEMs + b, storage := setupBackend(t, config) // test iss validation enabled with default "kubernetes/serviceaccount" issuer data := map[string]interface{}{ @@ -707,7 +799,7 @@ func TestLoginIssValidation(t *testing.T) { if err == nil { t.Fatal("expected error") } - if err.Error() != "claim \"iss\" is invalid" { + if err.Error() != `claim "iss" is invalid` { t.Fatalf("unexpected error: %s", err) } @@ -768,7 +860,9 @@ Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii -----END PUBLIC KEY-----` func TestLoginProjectedToken(t *testing.T) { - b, storage := setupBackend(t, append(testDefaultPEMs, testMinikubePubKey), testName, testNamespace) + config := defaultTestBackendConfig() + config.pems = append(testDefaultPEMs, testMinikubePubKey) + b, storage := setupBackend(t, config) // update backend to accept "default" bound account name data := map[string]interface{}{ @@ -793,7 +887,7 @@ func TestLoginProjectedToken(t *testing.T) { t.Fatalf("err:%s resp:%#v\n", err, resp) } - var roleNameError = fmt.Errorf("invalid role name \"%s\"", "plugin-test-x") + var roleNameError = fmt.Errorf("invalid role name %q", "plugin-test-x") testCases := map[string]struct { role string @@ -879,10 +973,14 @@ func TestLoginProjectedToken(t *testing.T) { } func TestAliasLookAheadProjectedToken(t *testing.T) { - b, storage := setupBackend(t, append(testDefaultPEMs, testMinikubePubKey), "default", testNamespace) + config := defaultTestBackendConfig() + config.pems = append(testDefaultPEMs, testMinikubePubKey) + config.saName = "default" + b, storage := setupBackend(t, config) data := map[string]interface{}{ - "jwt": jwtProjectedData, + "jwt": jwtProjectedData, + "role": "plugin-test", } req := &logical.Request{ @@ -900,7 +998,7 @@ func TestAliasLookAheadProjectedToken(t *testing.T) { t.Fatalf("err:%s resp:%#v\n", err, resp) } - if resp.Auth.Alias.Name != "77c81ad7-1bea-4d94-9ca5-f5d7f3632331" { + if resp.Auth.Alias.Name != testProjectedUID { t.Fatalf("Unexpected UID: %s", resp.Auth.Alias.Name) } } diff --git a/path_role.go b/path_role.go index 6506b1fe..7f3b3ed5 100644 --- a/path_role.go +++ b/path_role.go @@ -49,6 +49,16 @@ are allowed.`, Type: framework.TypeString, Description: "Optional Audience claim to verify in the jwt.", }, + "alias_name_source": { + Type: framework.TypeString, + Description: fmt.Sprintf(`Source to use when deriving the Alias name. +valid choices: + %q : e.g. 474b11b5-0f20-4f9d-8ca5-65715ab325e0 (most secure choice) + %q : / e.g. vault/vault-agent +default: %q +`, aliasNameSourceSAToken, aliasNameSourceSAPath, aliasNameSourceDefault), + Default: aliasNameSourceDefault, + }, "policies": { Type: framework.TypeCommaStringSlice, Description: tokenutil.DeprecationText("token_policies"), @@ -173,6 +183,8 @@ func (b *kubeAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request d["num_uses"] = role.NumUses } + d["alias_name_source"] = role.AliasNameSource + return &logical.Response{ Data: d, }, nil @@ -276,11 +288,11 @@ func (b *kubeAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical } // Verify names was not empty if len(role.ServiceAccountNames) == 0 { - return logical.ErrorResponse("\"bound_service_account_names\" can not be empty"), nil + return logical.ErrorResponse("%q can not be empty", "bound_service_account_names"), nil } // Verify * was not set with other data if len(role.ServiceAccountNames) > 1 && strutil.StrListContains(role.ServiceAccountNames, "*") { - return logical.ErrorResponse("can not mix \"*\" with values"), nil + return logical.ErrorResponse("can not mix %q with values", "*"), nil } if namespaces, ok := data.GetOk("bound_service_account_namespaces"); ok { @@ -290,11 +302,11 @@ func (b *kubeAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical } // Verify namespaces is not empty if len(role.ServiceAccountNamespaces) == 0 { - return logical.ErrorResponse("\"bound_service_account_namespaces\" can not be empty"), nil + return logical.ErrorResponse("%q can not be empty", "bound_service_account_namespaces"), nil } // Verify * was not set with other data if len(role.ServiceAccountNamespaces) > 1 && strutil.StrListContains(role.ServiceAccountNamespaces, "*") { - return logical.ErrorResponse("can not mix \"*\" with values"), nil + return logical.ErrorResponse("can not mix %q with values", "*"), nil } // optional audience field @@ -302,6 +314,15 @@ func (b *kubeAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical role.Audience = audience.(string) } + if source, ok := data.GetOk("alias_name_source"); ok { + if err := validateAliasNameSource(source.(string)); err != nil { + return logical.ErrorResponse(err.Error()), nil + } + role.AliasNameSource = source.(string) + } else if role.AliasNameSource == aliasNameSourceUnset { + role.AliasNameSource = data.Get("alias_name_source").(string) + } + // Store the entry. entry, err := logical.StorageEntryJSON("role/"+strings.ToLower(roleName), role) if err != nil { @@ -332,6 +353,9 @@ type roleStorageEntry struct { // Audience is an optional jwt claim to verify Audience string `json:"audience" mapstructure:"audience" structs: "audience"` + // AliasNameSource used when deriving the Alias' name. + AliasNameSource string `json:"alias_name_source" mapstructure:"alias_name_source" structs:"alias_name_source"` + // Deprecated by TokenParams Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` NumUses int `json:"num_uses" mapstructure:"num_uses" structs:"num_uses"` diff --git a/path_role_test.go b/path_role_test.go index b54e3774..d67a01e3 100644 --- a/path_role_test.go +++ b/path_role_test.go @@ -2,6 +2,8 @@ package kubeauth import ( "context" + "errors" + "fmt" "testing" "time" @@ -35,139 +37,158 @@ func getBackend(t *testing.T) (logical.Backend, logical.Storage) { } func TestPath_Create(t *testing.T) { - b, storage := getBackend(t) - - data := map[string]interface{}{ - "bound_service_account_names": "name", - "bound_service_account_namespaces": "namespace", - "policies": "test", - "period": "3s", - "ttl": "1s", - "num_uses": 12, - "max_ttl": "5s", - } - - expected := &roleStorageEntry{ - TokenParams: tokenutil.TokenParams{ - TokenPolicies: []string{"test"}, - TokenPeriod: 3 * time.Second, - TokenTTL: 1 * time.Second, - TokenMaxTTL: 5 * time.Second, - TokenNumUses: 12, - TokenBoundCIDRs: nil, + testCases := map[string]struct { + data map[string]interface{} + expected *roleStorageEntry + wantErr error + }{ + "default": { + data: map[string]interface{}{ + "bound_service_account_names": "name", + "bound_service_account_namespaces": "namespace", + "policies": "test", + "period": "3s", + "ttl": "1s", + "num_uses": 12, + "max_ttl": "5s", + "alias_name_source": aliasNameSourceDefault, + }, + expected: &roleStorageEntry{ + TokenParams: tokenutil.TokenParams{ + TokenPolicies: []string{"test"}, + TokenPeriod: 3 * time.Second, + TokenTTL: 1 * time.Second, + TokenMaxTTL: 5 * time.Second, + TokenNumUses: 12, + TokenBoundCIDRs: nil, + }, + Policies: []string{"test"}, + Period: 3 * time.Second, + ServiceAccountNames: []string{"name"}, + ServiceAccountNamespaces: []string{"namespace"}, + TTL: 1 * time.Second, + MaxTTL: 5 * time.Second, + NumUses: 12, + BoundCIDRs: nil, + AliasNameSource: aliasNameSourceDefault, + }, + }, + "alias_name_source_sa_path": { + data: map[string]interface{}{ + "bound_service_account_names": "name", + "bound_service_account_namespaces": "namespace", + "policies": "test", + "period": "3s", + "ttl": "1s", + "num_uses": 12, + "max_ttl": "5s", + "alias_name_source": aliasNameSourceSAPath, + }, + expected: &roleStorageEntry{ + TokenParams: tokenutil.TokenParams{ + TokenPolicies: []string{"test"}, + TokenPeriod: 3 * time.Second, + TokenTTL: 1 * time.Second, + TokenMaxTTL: 5 * time.Second, + TokenNumUses: 12, + TokenBoundCIDRs: nil, + }, + Policies: []string{"test"}, + Period: 3 * time.Second, + ServiceAccountNames: []string{"name"}, + ServiceAccountNamespaces: []string{"namespace"}, + TTL: 1 * time.Second, + MaxTTL: 5 * time.Second, + NumUses: 12, + BoundCIDRs: nil, + AliasNameSource: aliasNameSourceSAPath, + }, + }, + "invalid_alias_name_source": { + data: map[string]interface{}{ + "bound_service_account_names": "name", + "bound_service_account_namespaces": "namespace", + "policies": "test", + "period": "3s", + "ttl": "1s", + "num_uses": 12, + "max_ttl": "5s", + "alias_name_source": "_invalid_", + }, + wantErr: errInvalidAliasNameSource, + }, + "no_service_account_names": { + data: map[string]interface{}{ + "policies": "test", + }, + wantErr: errors.New(`"bound_service_account_names" can not be empty`), + }, + "no_service_account_namespaces": { + data: map[string]interface{}{ + "bound_service_account_names": "name", + "policies": "test", + }, + wantErr: errors.New(`"bound_service_account_namespaces" can not be empty`), + }, + "mixed_splat_values_names": { + data: map[string]interface{}{ + "bound_service_account_names": "*, test", + "bound_service_account_namespaces": "*", + "policies": "test", + }, + wantErr: errors.New(`can not mix "*" with values`), + }, + "mixed_splat_values_namespaces": { + data: map[string]interface{}{ + "bound_service_account_names": "*, test", + "bound_service_account_namespaces": "*", + "policies": "test", + }, + wantErr: errors.New(`can not mix "*" with values`), }, - Policies: []string{"test"}, - Period: 3 * time.Second, - ServiceAccountNames: []string{"name"}, - ServiceAccountNamespaces: []string{"namespace"}, - TTL: 1 * time.Second, - MaxTTL: 5 * time.Second, - NumUses: 12, - BoundCIDRs: nil, - } - - req := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/plugin-test", - Storage: storage, - Data: data, - } - - resp, err := b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - actual, err := b.(*kubeAuthBackend).role(context.Background(), storage, "plugin-test") - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal(expected, actual); diff != nil { - t.Fatal(diff) - } - - // Test no service account info - data = map[string]interface{}{ - "policies": "test", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/test2", - Storage: storage, - Data: data, - } - - resp, err = b.HandleRequest(context.Background(), req) - if resp != nil && !resp.IsError() { - t.Fatalf("expected error") - } - if resp.Error().Error() != "\"bound_service_account_names\" can not be empty" { - t.Fatalf("unexpected err: %v", resp) - } - - // Test no service account info - data = map[string]interface{}{ - "bound_service_account_names": "name", - "policies": "test", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/test2", - Storage: storage, - Data: data, - } - - resp, err = b.HandleRequest(context.Background(), req) - if resp != nil && !resp.IsError() { - t.Fatalf("expected error") - } - if resp.Error().Error() != "\"bound_service_account_namespaces\" can not be empty" { - t.Fatalf("unexpected err: %v", resp) - } - - // Test mixed "*" and values - data = map[string]interface{}{ - "bound_service_account_names": "*, test", - "bound_service_account_namespaces": "*", - "policies": "test", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/test2", - Storage: storage, - Data: data, - } - - resp, err = b.HandleRequest(context.Background(), req) - if resp == nil || !resp.IsError() { - t.Fatalf("expected error") - } - if resp.Error().Error() != "can not mix \"*\" with values" { - t.Fatalf("unexpected err: %v", resp) - } - - data = map[string]interface{}{ - "bound_service_account_names": "*", - "bound_service_account_namespaces": "*, test", - "policies": "test", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/test2", - Storage: storage, - Data: data, } - resp, err = b.HandleRequest(context.Background(), req) - if resp == nil || !resp.IsError() { - t.Fatalf("expected error") - } - if resp.Error().Error() != "can not mix \"*\" with values" { - t.Fatalf("unexpected err: %v", resp) + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + b, storage := getBackend(t) + path := fmt.Sprintf("role/%s", name) + req := &logical.Request{ + Operation: logical.CreateOperation, + Path: path, + Storage: storage, + Data: tc.data, + } + + resp, err := b.HandleRequest(context.Background(), req) + + if tc.wantErr != nil { + var actual error + if err != nil { + actual = err + } else if resp != nil && resp.IsError() { + actual = resp.Error() + } else { + t.Fatalf("expected error") + } + + if tc.wantErr.Error() != actual.Error() { + t.Fatalf("expected err %q, actual %q", tc.wantErr, actual) + } + } else { + if tc.wantErr == nil && (err != nil || (resp != nil && resp.IsError())) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + actual, err := b.(*kubeAuthBackend).role(context.Background(), storage, name) + if err != nil { + t.Fatal(err) + } + + if diff := deep.Equal(tc.expected, actual); diff != nil { + t.Fatal(diff) + } + } + }) } } @@ -201,6 +222,7 @@ func TestPath_Read(t *testing.T) { "token_type": logical.TokenTypeDefault.String(), "token_explicit_max_ttl": int64(0), "token_no_default_policy": false, + "alias_name_source": aliasNameSourceDefault, } req := &logical.Request{