Skip to content

Commit

Permalink
impr: add base construct for selector definition in roles
Browse files Browse the repository at this point in the history
Implements: hashicorp#155
  • Loading branch information
f4z3r committed Sep 4, 2023
1 parent b846de8 commit 1814f45
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 54 deletions.
5 changes: 1 addition & 4 deletions backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func Test_kubeAuthBackend_updateTLSConfig(t *testing.T) {
for idx, config := range tt.configs {
t.Run(fmt.Sprintf("config-%d", idx), func(t *testing.T) {
if config.localCACert != "" {
if err := os.WriteFile(localFile, []byte(config.localCACert), 0600); err != nil {
if err := os.WriteFile(localFile, []byte(config.localCACert), 0o600); err != nil {
t.Fatalf("failed to write local file %q", localFile)
}
t.Cleanup(func() {
Expand Down Expand Up @@ -324,7 +324,6 @@ func Test_kubeAuthBackend_initialize(t *testing.T) {
err := b.initialize(ctx, tt.req)
if tt.wantErr && err == nil {
t.Errorf("initialize() error = %v, wantErr %v", err, tt.wantErr)

}

if !reflect.DeepEqual(err, tt.expectErr) {
Expand Down Expand Up @@ -442,7 +441,6 @@ func Test_kubeAuthBackend_runTLSConfigUpdater(t *testing.T) {
err := b.runTLSConfigUpdater(ctx, tt.storage, tt.horizon)
if tt.wantErr && err == nil {
t.Errorf("runTLSConfigUpdater() error = %v, wantErr %v", err, tt.wantErr)

}

if !reflect.DeepEqual(err, tt.expectErr) {
Expand Down Expand Up @@ -506,7 +504,6 @@ func assertTLSConfigEquals(t *testing.T, actual, expected *tls.Config) {
t.Errorf("updateTLSConfig() actual MinVersion = %v, expected MinVersion %v",
actual.MinVersion, expected.MinVersion)
}

}

func assertValidTransport(t *testing.T, b *kubeAuthBackend, expected *tls.Config) {
Expand Down
28 changes: 28 additions & 0 deletions namespace_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kubeauth

import (
"context"
"net/http"
)

// This exists so we can use a mock namespace validation when running tests
type namespaceValidator interface {
ValidateLabels(context.Context, *http.Client, string, map[string]string) (bool, error)
}

type namespaceValidatorFactory func(*kubeConfig) namespaceValidator

// This is the real implementation that calls the kubernetes API
type namespaceValidatorAPI struct {
config *kubeConfig
}

func namespaceValidatorAPIFactory(config *kubeConfig) namespaceValidator {
return &namespaceValidatorAPI{
config: config,
}
}

func (t *namespaceValidatorAPI) ValidateLabels(ctx context.Context, client *http.Client, name string, labels map[string]string) (bool, error) {
return true, nil
}
34 changes: 27 additions & 7 deletions path_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ are allowed.`,
Type: framework.TypeCommaStringSlice,
Description: `List of namespaces allowed to access this role. If set to "*" all namespaces
are allowed.`,
},
"bound_service_account_namespace_selector": {
Type: framework.TypeString,
Description: `A label selector for Kubernetes namspaces which are allowed to access this role.
Accepts either a JSON or YAML object. If set with bound_service_account_namespaces,
the conditions are conjuncted.`,
},
"audience": {
Type: framework.TypeString,
Expand Down Expand Up @@ -161,8 +167,15 @@ func (b *kubeAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request

// Create a map of data to be returned
d := map[string]interface{}{
"bound_service_account_names": role.ServiceAccountNames,
"bound_service_account_namespaces": role.ServiceAccountNamespaces,
"bound_service_account_names": role.ServiceAccountNames,
}

if len(role.ServiceAccountNamespaces) > 0 {
d["bound_service_account_namespaces"] = role.ServiceAccountNamespaces
}

if role.ServiceAccountNamespaceSelector != "" {
d["bound_service_account_namespace_selector"] = role.ServiceAccountNamespaceSelector
}

if role.Audience != "" {
Expand Down Expand Up @@ -304,12 +317,15 @@ func (b *kubeAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical

if namespaces, ok := data.GetOk("bound_service_account_namespaces"); ok {
role.ServiceAccountNamespaces = namespaces.([]string)
} else if req.Operation == logical.CreateOperation {
role.ServiceAccountNamespaces = data.Get("bound_service_account_namespaces").([]string)
}
// Verify namespaces is not empty
if len(role.ServiceAccountNamespaces) == 0 {
return logical.ErrorResponse("%q can not be empty", "bound_service_account_namespaces"), nil

if namespaceSelector, ok := data.GetOk("bound_service_account_namespace_selector"); ok {
role.ServiceAccountNamespaceSelector = namespaceSelector.(string)
}

// Verify namespaces is not empty unless selector is set
if len(role.ServiceAccountNamespaces) == 0 && role.ServiceAccountNamespaceSelector == "" {
return logical.ErrorResponse("%q can not be empty if %q is not set", "bound_service_account_namespaces", "bound_service_account_namespace_selector"), nil
}
// Verify * was not set with other data
if len(role.ServiceAccountNamespaces) > 1 && strutil.StrListContains(role.ServiceAccountNamespaces, "*") {
Expand Down Expand Up @@ -364,6 +380,10 @@ type roleStorageEntry struct {
// role.
ServiceAccountNamespaces []string `json:"bound_service_account_namespaces" mapstructure:"bound_service_account_namespaces" structs:"bound_service_account_namespaces"`

// ServiceAccountNamespaceSelector is the label selector string of the
// namespaces able to access this role.
ServiceAccountNamespaceSelector string `json:"bound_service_account_namespace_selector" mapstructure:"bound_service_account_namespace_selector" structs:"bound_service_account_namespace_selector"`

// Audience is an optional jwt claim to verify
Audience string `json:"audience" mapstructure:"audience" structs:"audience"`

Expand Down
179 changes: 136 additions & 43 deletions path_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ import (
"github.com/hashicorp/vault/sdk/logical"
)

const (
goodJSONSelector = `{
"matchLabels": {
"stage": "prod",
"app": "vault"
}
}`

badJSONSelector = `{
"matchLabels":
"stage": "prod",
"app": "vault"
}`

goodYAMLSelector = `matchLabels:
stage: prod
app: vault
`
badYAMLSelector = `matchLabels:
- stage: prod
- app: vault
`
)

func getBackend(t *testing.T) (logical.Backend, logical.Storage) {
defaultLeaseTTLVal := time.Hour * 12
maxLeaseTTLVal := time.Hour * 24
Expand Down Expand Up @@ -68,15 +92,16 @@ func TestPath_Create(t *testing.T) {
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,
Policies: []string{"test"},
Period: 3 * time.Second,
ServiceAccountNames: []string{"name"},
ServiceAccountNamespaces: []string{"namespace"},
ServiceAccountNamespaceSelector: "",
TTL: 1 * time.Second,
MaxTTL: 5 * time.Second,
NumUses: 12,
BoundCIDRs: nil,
AliasNameSource: aliasNameSourceDefault,
},
},
"alias_name_source_serviceaccount_name": {
Expand All @@ -99,15 +124,81 @@ func TestPath_Create(t *testing.T) {
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: aliasNameSourceSAName,
Policies: []string{"test"},
Period: 3 * time.Second,
ServiceAccountNames: []string{"name"},
ServiceAccountNamespaces: []string{"namespace"},
ServiceAccountNamespaceSelector: "",
TTL: 1 * time.Second,
MaxTTL: 5 * time.Second,
NumUses: 12,
BoundCIDRs: nil,
AliasNameSource: aliasNameSourceSAName,
},
},
"namespace_selector": {
data: map[string]interface{}{
"bound_service_account_names": "name",
"bound_service_account_namespace_selector": goodJSONSelector,
"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(nil),
ServiceAccountNamespaceSelector: goodJSONSelector,
TTL: 1 * time.Second,
MaxTTL: 5 * time.Second,
NumUses: 12,
BoundCIDRs: nil,
AliasNameSource: aliasNameSourceDefault,
},
},
"namespace_selector_with_namespaces": {
data: map[string]interface{}{
"bound_service_account_names": "name",
"bound_service_account_namespaces": "namespace1,namespace2",
"bound_service_account_namespace_selector": goodYAMLSelector,
"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{"namespace1", "namespace2"},
ServiceAccountNamespaceSelector: goodYAMLSelector,
TTL: 1 * time.Second,
MaxTTL: 5 * time.Second,
NumUses: 12,
BoundCIDRs: nil,
AliasNameSource: aliasNameSourceDefault,
},
},
"invalid_alias_name_source": {
Expand All @@ -134,7 +225,7 @@ func TestPath_Create(t *testing.T) {
"bound_service_account_names": "name",
"policies": "test",
},
wantErr: errors.New(`"bound_service_account_namespaces" can not be empty`),
wantErr: errors.New(`"bound_service_account_namespaces" can not be empty if "bound_service_account_namespace_selector" is not set`),
},
"mixed_splat_values_names": {
data: map[string]interface{}{
Expand Down Expand Up @@ -202,33 +293,35 @@ func TestPath_Read(t *testing.T) {
b, storage := getBackend(t)

configData := 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",
"bound_service_account_names": "name",
"bound_service_account_namespaces": "namespace",
"bound_service_account_namespace_selector": goodJSONSelector,
"policies": "test",
"period": "3s",
"ttl": "1s",
"num_uses": 12,
"max_ttl": "5s",
}

expected := map[string]interface{}{
"bound_service_account_names": []string{"name"},
"bound_service_account_namespaces": []string{"namespace"},
"token_policies": []string{"test"},
"policies": []string{"test"},
"token_period": int64(3),
"period": int64(3),
"token_ttl": int64(1),
"ttl": int64(1),
"token_num_uses": 12,
"num_uses": 12,
"token_max_ttl": int64(5),
"max_ttl": int64(5),
"token_bound_cidrs": []string{},
"token_type": logical.TokenTypeDefault.String(),
"token_explicit_max_ttl": int64(0),
"token_no_default_policy": false,
"alias_name_source": aliasNameSourceDefault,
"bound_service_account_names": []string{"name"},
"bound_service_account_namespaces": []string{"namespace"},
"bound_service_account_namespace_selector": goodJSONSelector,
"token_policies": []string{"test"},
"policies": []string{"test"},
"token_period": int64(3),
"period": int64(3),
"token_ttl": int64(1),
"ttl": int64(1),
"token_num_uses": 12,
"num_uses": 12,
"token_max_ttl": int64(5),
"max_ttl": int64(5),
"token_bound_cidrs": []string{},
"token_type": logical.TokenTypeDefault.String(),
"token_explicit_max_ttl": int64(0),
"token_no_default_policy": false,
"alias_name_source": aliasNameSourceDefault,
}

req := &logical.Request{
Expand Down

0 comments on commit 1814f45

Please sign in to comment.