diff --git a/cmd/kubeadm/app/apis/kubeadm/bootstraptokenhelpers.go b/cmd/kubeadm/app/apis/kubeadm/bootstraptokenhelpers.go index cd2977ed643..48d89b0ce28 100644 --- a/cmd/kubeadm/app/apis/kubeadm/bootstraptokenhelpers.go +++ b/cmd/kubeadm/app/apis/kubeadm/bootstraptokenhelpers.go @@ -27,6 +27,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" bootstrapapi "k8s.io/cluster-bootstrap/token/api" bootstraputil "k8s.io/cluster-bootstrap/token/util" + bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets" ) // ToSecret converts the given BootstrapToken object to its Secret representation that @@ -55,7 +56,7 @@ func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]by } // If for some strange reason both token.TTL and token.Expires would be set - // (they are mutually exlusive in validation so this shouldn't be the case), + // (they are mutually exclusive in validation so this shouldn't be the case), // token.Expires has higher priority, as can be seen in the logic here. if token.Expires != nil { // Format the expiration date accordingly @@ -83,7 +84,7 @@ func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]by // BootstrapTokenFromSecret returns a BootstrapToken object from the given Secret func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { // Get the Token ID field from the Secret data - tokenID := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) + tokenID := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey) if len(tokenID) == 0 { return nil, errors.Errorf("bootstrap Token Secret has no token-id data: %s", secret.Name) } @@ -94,7 +95,7 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID)) } - tokenSecret := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) + tokenSecret := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey) if len(tokenSecret) == 0 { return nil, errors.Errorf("bootstrap Token Secret has no token-secret data: %s", secret.Name) } @@ -106,11 +107,11 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { } // Get the description (if any) from the Secret - description := getSecretString(secret, bootstrapapi.BootstrapTokenDescriptionKey) + description := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenDescriptionKey) // Expiration time is optional, if not specified this implies the token // never expires. - secretExpiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey) + secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey) var expires *metav1.Time if len(secretExpiration) > 0 { expTime, err := time.Parse(time.RFC3339, secretExpiration) @@ -142,7 +143,7 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { // It's done this way to make .Groups be nil in case there is no items, rather than an // empty slice or an empty slice with a "" string only var groups []string - groupsString := getSecretString(secret, bootstrapapi.BootstrapTokenExtraGroupsKey) + groupsString := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExtraGroupsKey) g := strings.Split(groupsString, ",") if len(g) > 0 && len(g[0]) > 0 { groups = g @@ -156,14 +157,3 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { Groups: groups, }, nil } - -// getSecretString returns the string value for the given key in the specified Secret -func getSecretString(secret *v1.Secret, key string) string { - if secret.Data == nil { - return "" - } - if val, ok := secret.Data[key]; ok { - return string(val) - } - return "" -} diff --git a/cmd/kubeadm/app/apis/kubeadm/bootstraptokenhelpers_test.go b/cmd/kubeadm/app/apis/kubeadm/bootstraptokenhelpers_test.go index b867876e888..58eac205142 100644 --- a/cmd/kubeadm/app/apis/kubeadm/bootstraptokenhelpers_test.go +++ b/cmd/kubeadm/app/apis/kubeadm/bootstraptokenhelpers_test.go @@ -454,55 +454,3 @@ func jsonMarshal(bt *BootstrapToken) string { b, _ := json.Marshal(*bt) return string(b) } - -func TestGetSecretString(t *testing.T) { - var tests = []struct { - name string - secret *v1.Secret - key string - expectedVal string - }{ - { - name: "existing key", - secret: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Data: map[string][]byte{ - "foo": []byte("bar"), - }, - }, - key: "foo", - expectedVal: "bar", - }, - { - name: "non-existing key", - secret: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Data: map[string][]byte{ - "foo": []byte("bar"), - }, - }, - key: "baz", - expectedVal: "", - }, - { - name: "no data", - secret: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - }, - key: "foo", - expectedVal: "", - }, - } - for _, rt := range tests { - t.Run(rt.name, func(t *testing.T) { - actual := getSecretString(rt.secret, rt.key) - if actual != rt.expectedVal { - t.Errorf( - "failed getSecretString:\n\texpected: %s\n\t actual: %s", - rt.expectedVal, - actual, - ) - } - }) - } -} diff --git a/pkg/controller/bootstrap/tokencleaner.go b/pkg/controller/bootstrap/tokencleaner.go index adaee9b58d5..c8e517cc599 100644 --- a/pkg/controller/bootstrap/tokencleaner.go +++ b/pkg/controller/bootstrap/tokencleaner.go @@ -31,6 +31,7 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" bootstrapapi "k8s.io/cluster-bootstrap/token/api" + bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets" "k8s.io/klog" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/controller" @@ -187,7 +188,7 @@ func (tc *TokenCleaner) syncFunc(key string) error { func (tc *TokenCleaner) evalSecret(o interface{}) { secret := o.(*v1.Secret) - if isSecretExpired(secret) { + if bootstrapsecretutil.HasExpired(secret, time.Now()) { klog.V(3).Infof("Deleting expired secret %s/%s", secret.Namespace, secret.Name) var options *metav1.DeleteOptions if len(secret.UID) > 0 { diff --git a/pkg/controller/bootstrap/util.go b/pkg/controller/bootstrap/util.go index 44d024af41a..f61bc73f40c 100644 --- a/pkg/controller/bootstrap/util.go +++ b/pkg/controller/bootstrap/util.go @@ -17,46 +17,23 @@ limitations under the License. package bootstrap import ( - "regexp" "time" "k8s.io/klog" "k8s.io/api/core/v1" bootstrapapi "k8s.io/cluster-bootstrap/token/api" + bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets" ) -var namePattern = `^` + regexp.QuoteMeta(bootstrapapi.BootstrapTokenSecretPrefix) + `([a-z0-9]{6})$` -var nameRegExp = regexp.MustCompile(namePattern) - -// getSecretString gets a string value from a secret. If there is an error or -// if the key doesn't exist, an empty string is returned. -func getSecretString(secret *v1.Secret, key string) string { - data, ok := secret.Data[key] - if !ok { - return "" - } - - return string(data) -} - -// parseSecretName parses the name of the secret to extract the secret ID. -func parseSecretName(name string) (secretID string, ok bool) { - r := nameRegExp.FindStringSubmatch(name) - if r == nil { - return "", false - } - return r[1], true -} - func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, ok bool) { - nameTokenID, ok := parseSecretName(secret.Name) + nameTokenID, ok := bootstrapsecretutil.ParseName(secret.Name) if !ok { klog.V(3).Infof("Invalid secret name: %s. Must be of form %s.", secret.Name, bootstrapapi.BootstrapTokenSecretPrefix) return "", "", false } - tokenID = getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) + tokenID = bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey) if len(tokenID) == 0 { klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenIDKey, secret.Namespace, secret.Name) return "", "", false @@ -67,7 +44,7 @@ func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, o return "", "", false } - tokenSecret = getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) + tokenSecret = bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey) if len(tokenSecret) == 0 { klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenSecretKey, secret.Namespace, secret.Name) return "", "", false @@ -76,34 +53,15 @@ func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, o // Ensure this secret hasn't expired. The TokenCleaner should remove this // but if that isn't working or it hasn't gotten there yet we should check // here. - if isSecretExpired(secret) { + if bootstrapsecretutil.HasExpired(secret, time.Now()) { return "", "", false } // Make sure this secret can be used for signing - okToSign := getSecretString(secret, bootstrapapi.BootstrapTokenUsageSigningKey) + okToSign := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenUsageSigningKey) if okToSign != "true" { return "", "", false } return tokenID, tokenSecret, true } - -// isSecretExpired returns true if the Secret is expired. -func isSecretExpired(secret *v1.Secret) bool { - expiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey) - if len(expiration) > 0 { - expTime, err2 := time.Parse(time.RFC3339, expiration) - if err2 != nil { - klog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.", - expiration, secret.Namespace, secret.Name, err2) - return true - } - if time.Now().After(expTime) { - klog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v", - secret.Namespace, secret.Name, expiration) - return true - } - } - return false -} diff --git a/pkg/controller/bootstrap/util_test.go b/pkg/controller/bootstrap/util_test.go index 9a7617a3dd0..ca17c99785f 100644 --- a/pkg/controller/bootstrap/util_test.go +++ b/pkg/controller/bootstrap/util_test.go @@ -20,8 +20,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" bootstrapapi "k8s.io/cluster-bootstrap/token/api" @@ -180,27 +178,3 @@ func TestMismatchSecretName(t *testing.T) { t.Errorf("Token validation should fail with mismatched name") } } - -func TestParseSecretName(t *testing.T) { - tokenID, ok := parseSecretName("bootstrap-token-abc123") - assert.True(t, ok, "parseSecretName should accept valid name") - assert.Equal(t, "abc123", tokenID, "parseSecretName should return token ID") - - _, ok = parseSecretName("") - assert.False(t, ok, "parseSecretName should reject blank name") - - _, ok = parseSecretName("abc123") - assert.False(t, ok, "parseSecretName should reject with no prefix") - - _, ok = parseSecretName("bootstrap-token-") - assert.False(t, ok, "parseSecretName should reject no token ID") - - _, ok = parseSecretName("bootstrap-token-abc") - assert.False(t, ok, "parseSecretName should reject short token ID") - - _, ok = parseSecretName("bootstrap-token-abc123ghi") - assert.False(t, ok, "parseSecretName should reject long token ID") - - _, ok = parseSecretName("bootstrap-token-ABC123") - assert.False(t, ok, "parseSecretName should reject invalid token ID") -} diff --git a/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go b/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go index 44596156653..bdafe1cc693 100644 --- a/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go +++ b/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go @@ -23,20 +23,18 @@ import ( "context" "crypto/subtle" "fmt" - "regexp" - "strings" "time" "k8s.io/klog" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" corev1listers "k8s.io/client-go/listers/core/v1" bootstrapapi "k8s.io/cluster-bootstrap/token/api" - bootstraputil "k8s.io/cluster-bootstrap/token/util" + bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets" + bootstraptokenutil "k8s.io/cluster-bootstrap/util/tokens" ) // TODO: A few methods in this package is copied from other sources. Either @@ -92,7 +90,7 @@ func tokenErrorf(s *corev1.Secret, format string, i ...interface{}) { // ( token-id ).( token-secret ) // func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { - tokenID, tokenSecret, err := parseToken(token) + tokenID, tokenSecret, err := bootstraptokenutil.ParseToken(token) if err != nil { // Token isn't of the correct form, ignore it. return nil, false, nil @@ -118,29 +116,29 @@ func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string return nil, false, nil } - ts := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) + ts := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey) if subtle.ConstantTimeCompare([]byte(ts), []byte(tokenSecret)) != 1 { tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenSecretKey, tokenSecret) return nil, false, nil } - id := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) + id := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey) if id != tokenID { tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenIDKey, tokenID) return nil, false, nil } - if isSecretExpired(secret) { + if bootstrapsecretutil.HasExpired(secret, time.Now()) { // logging done in isSecretExpired method. return nil, false, nil } - if getSecretString(secret, bootstrapapi.BootstrapTokenUsageAuthentication) != "true" { + if bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenUsageAuthentication) != "true" { tokenErrorf(secret, "not marked %s=true.", bootstrapapi.BootstrapTokenUsageAuthentication) return nil, false, nil } - groups, err := getGroups(secret) + groups, err := bootstrapsecretutil.GetGroups(secret) if err != nil { tokenErrorf(secret, "has invalid value for key %s: %v.", bootstrapapi.BootstrapTokenExtraGroupsKey, err) return nil, false, nil @@ -153,76 +151,3 @@ func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string }, }, true, nil } - -// Copied from k8s.io/cluster-bootstrap/token/api -func getSecretString(secret *corev1.Secret, key string) string { - data, ok := secret.Data[key] - if !ok { - return "" - } - - return string(data) -} - -// Copied from k8s.io/cluster-bootstrap/token/api -func isSecretExpired(secret *corev1.Secret) bool { - expiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey) - if len(expiration) > 0 { - expTime, err2 := time.Parse(time.RFC3339, expiration) - if err2 != nil { - klog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.", - expiration, secret.Namespace, secret.Name, err2) - return true - } - if time.Now().After(expTime) { - klog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v", - secret.Namespace, secret.Name, expiration) - return true - } - } - return false -} - -// Copied from kubernetes/cmd/kubeadm/app/util/token - -var ( - // tokenRegexpString defines id.secret regular expression pattern - tokenRegexpString = "^([a-z0-9]{6})\\.([a-z0-9]{16})$" - // tokenRegexp is a compiled regular expression of TokenRegexpString - tokenRegexp = regexp.MustCompile(tokenRegexpString) -) - -// parseToken tries and parse a valid token from a string. -// A token ID and token secret are returned in case of success, an error otherwise. -func parseToken(s string) (string, string, error) { - split := tokenRegexp.FindStringSubmatch(s) - if len(split) != 3 { - return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, tokenRegexpString) - } - return split[1], split[2], nil -} - -// getGroups loads and validates the bootstrapapi.BootstrapTokenExtraGroupsKey -// key from the bootstrap token secret, returning a list of group names or an -// error if any of the group names are invalid. -func getGroups(secret *corev1.Secret) ([]string, error) { - // always include the default group - groups := sets.NewString(bootstrapapi.BootstrapDefaultGroup) - - // grab any extra groups and if there are none, return just the default - extraGroupsString := getSecretString(secret, bootstrapapi.BootstrapTokenExtraGroupsKey) - if extraGroupsString == "" { - return groups.List(), nil - } - - // validate the names of the extra groups - for _, group := range strings.Split(extraGroupsString, ",") { - if err := bootstraputil.ValidateBootstrapGroupName(group); err != nil { - return nil, err - } - groups.Insert(group) - } - - // return the result as a deduplicated, sorted list - return groups.List(), nil -} diff --git a/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap_test.go b/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap_test.go index 0613e1587a4..e0e23fa0588 100644 --- a/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap_test.go +++ b/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap_test.go @@ -288,72 +288,3 @@ func TestTokenAuthenticator(t *testing.T) { }() } } - -func TestGetGroups(t *testing.T) { - tests := []struct { - name string - secret *corev1.Secret - expectResult []string - expectError bool - }{ - { - name: "not set", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Data: map[string][]byte{}, - }, - expectResult: []string{"system:bootstrappers"}, - }, - { - name: "set to empty value", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Data: map[string][]byte{ - bootstrapapi.BootstrapTokenExtraGroupsKey: []byte(""), - }, - }, - expectResult: []string{"system:bootstrappers"}, - }, - { - name: "invalid prefix", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Data: map[string][]byte{ - bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("foo"), - }, - }, - expectError: true, - }, - { - name: "valid", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Data: map[string][]byte{ - bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("system:bootstrappers:foo,system:bootstrappers:bar,system:bootstrappers:bar"), - }, - }, - // expect the results in deduplicated, sorted order - expectResult: []string{ - "system:bootstrappers", - "system:bootstrappers:bar", - "system:bootstrappers:foo", - }, - }, - } - for _, test := range tests { - result, err := getGroups(test.secret) - if test.expectError { - if err == nil { - t.Errorf("test %q expected an error, but didn't get one (result: %#v)", test.name, result) - } - continue - } - if err != nil { - t.Errorf("test %q return an unexpected error: %v", test.name, err) - continue - } - if !reflect.DeepEqual(result, test.expectResult) { - t.Errorf("test %q expected %#v, got %#v", test.name, test.expectResult, result) - } - } -} diff --git a/staging/src/k8s.io/cluster-bootstrap/token/util/helpers.go b/staging/src/k8s.io/cluster-bootstrap/token/util/helpers.go index 5565cb26d8a..f9ea35b5ee3 100644 --- a/staging/src/k8s.io/cluster-bootstrap/token/util/helpers.go +++ b/staging/src/k8s.io/cluster-bootstrap/token/util/helpers.go @@ -27,6 +27,8 @@ import ( "k8s.io/cluster-bootstrap/token/api" ) +// TODO(dixudx): refactor this to util/secrets and util/tokens + // validBootstrapTokenChars defines the characters a bootstrap token can consist of const validBootstrapTokenChars = "0123456789abcdefghijklmnopqrstuvwxyz" @@ -110,6 +112,7 @@ func BootstrapTokenSecretName(tokenID string) string { // ValidateBootstrapGroupName checks if the provided group name is a valid // bootstrap group name. Returns nil if valid or a validation error if invalid. +// TODO(dixudx): should be moved to util/secrets func ValidateBootstrapGroupName(name string) error { if BootstrapGroupRegexp.Match([]byte(name)) { return nil diff --git a/staging/src/k8s.io/cluster-bootstrap/util/secrets/secrets.go b/staging/src/k8s.io/cluster-bootstrap/util/secrets/secrets.go new file mode 100644 index 00000000000..07b4a6e9740 --- /dev/null +++ b/staging/src/k8s.io/cluster-bootstrap/util/secrets/secrets.go @@ -0,0 +1,100 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package secrets + +import ( + "regexp" + "strings" + "time" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/cluster-bootstrap/token/api" + legacyutil "k8s.io/cluster-bootstrap/token/util" + "k8s.io/klog" +) + +// GetData returns the string value for the given key in the specified Secret +// If there is an error or if the key doesn't exist, an empty string is returned. +func GetData(secret *v1.Secret, key string) string { + if secret.Data == nil { + return "" + } + if val, ok := secret.Data[key]; ok { + return string(val) + } + return "" +} + +// HasExpired will identify whether the secret expires +func HasExpired(secret *v1.Secret, currentTime time.Time) bool { + expiration := GetData(secret, api.BootstrapTokenExpirationKey) + if len(expiration) > 0 { + expTime, err2 := time.Parse(time.RFC3339, expiration) + if err2 != nil { + klog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.", + expiration, secret.Namespace, secret.Name, err2) + return true + } + if currentTime.After(expTime) { + klog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v", + secret.Namespace, secret.Name, expiration) + return true + } + } + return false +} + +// ParseName parses the name of the secret to extract the secret ID. +func ParseName(name string) (secretID string, ok bool) { + namePattern := `^` + regexp.QuoteMeta(api.BootstrapTokenSecretPrefix) + `([a-z0-9]{6})$` + nameRegExp, err := regexp.Compile(namePattern) + if err != nil { + klog.Errorf("error compiling bootstrap regex %q: %v", namePattern, err) + return "", false + } + r := nameRegExp.FindStringSubmatch(name) + if r == nil { + return "", false + } + return r[1], true +} + +// GetGroups loads and validates the bootstrapapi.BootstrapTokenExtraGroupsKey +// key from the bootstrap token secret, returning a list of group names or an +// error if any of the group names are invalid. +func GetGroups(secret *v1.Secret) ([]string, error) { + // always include the default group + groups := sets.NewString(api.BootstrapDefaultGroup) + + // grab any extra groups and if there are none, return just the default + extraGroupsString := GetData(secret, api.BootstrapTokenExtraGroupsKey) + if extraGroupsString == "" { + return groups.List(), nil + } + + // validate the names of the extra groups + for _, group := range strings.Split(extraGroupsString, ",") { + if err := legacyutil.ValidateBootstrapGroupName(group); err != nil { + return nil, err + } + groups.Insert(group) + } + + // return the result as a deduplicated, sorted list + return groups.List(), nil +} diff --git a/staging/src/k8s.io/cluster-bootstrap/util/secrets/secrets_test.go b/staging/src/k8s.io/cluster-bootstrap/util/secrets/secrets_test.go new file mode 100644 index 00000000000..20dcba93419 --- /dev/null +++ b/staging/src/k8s.io/cluster-bootstrap/util/secrets/secrets_test.go @@ -0,0 +1,187 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package secrets + +import ( + "reflect" + "testing" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + bootstrapapi "k8s.io/cluster-bootstrap/token/api" +) + +func TestGetSecretString(t *testing.T) { + var tests = []struct { + name string + secret *v1.Secret + key string + expectedVal string + }{ + { + name: "existing key", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Data: map[string][]byte{ + "foo": []byte("bar"), + }, + }, + key: "foo", + expectedVal: "bar", + }, + { + name: "non-existing key", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Data: map[string][]byte{ + "foo": []byte("bar"), + }, + }, + key: "baz", + expectedVal: "", + }, + { + name: "no data", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + }, + key: "foo", + expectedVal: "", + }, + } + for _, rt := range tests { + t.Run(rt.name, func(t *testing.T) { + actual := GetData(rt.secret, rt.key) + if actual != rt.expectedVal { + t.Errorf( + "failed getSecretString:\n\texpected: %s\n\t actual: %s", + rt.expectedVal, + actual, + ) + } + }) + } +} + +func TestParseSecretName(t *testing.T) { + tokenID, ok := ParseName("bootstrap-token-abc123") + if !ok { + t.Error("ParseName should accept valid name") + } + if tokenID != "abc123" { + t.Error("ParseName should return token ID") + } + + _, ok = ParseName("") + if ok { + t.Error("ParseName should reject blank name") + } + + _, ok = ParseName("abc123") + if ok { + t.Error("ParseName should reject with no prefix") + } + + _, ok = ParseName("bootstrap-token-") + if ok { + t.Error("ParseName should reject no token ID") + } + + _, ok = ParseName("bootstrap-token-abc") + if ok { + t.Error("ParseName should reject short token ID") + } + + _, ok = ParseName("bootstrap-token-abc123ghi") + if ok { + t.Error("ParseName should reject long token ID") + } + + _, ok = ParseName("bootstrap-token-ABC123") + if ok { + t.Error("ParseName should reject invalid token ID") + } +} + +func TestGetGroups(t *testing.T) { + tests := []struct { + name string + secret *v1.Secret + expectResult []string + expectError bool + }{ + { + name: "not set", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Data: map[string][]byte{}, + }, + expectResult: []string{"system:bootstrappers"}, + }, + { + name: "set to empty value", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenExtraGroupsKey: []byte(""), + }, + }, + expectResult: []string{"system:bootstrappers"}, + }, + { + name: "invalid prefix", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("foo"), + }, + }, + expectError: true, + }, + { + name: "valid", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("system:bootstrappers:foo,system:bootstrappers:bar,system:bootstrappers:bar"), + }, + }, + // expect the results in deduplicated, sorted order + expectResult: []string{ + "system:bootstrappers", + "system:bootstrappers:bar", + "system:bootstrappers:foo", + }, + }, + } + for _, test := range tests { + result, err := GetGroups(test.secret) + if test.expectError { + if err == nil { + t.Errorf("test %q expected an error, but didn't get one (result: %#v)", test.name, result) + } + continue + } + if err != nil { + t.Errorf("test %q return an unexpected error: %v", test.name, err) + continue + } + if !reflect.DeepEqual(result, test.expectResult) { + t.Errorf("test %q expected %#v, got %#v", test.name, test.expectResult, result) + } + } +} diff --git a/staging/src/k8s.io/cluster-bootstrap/util/tokens/tokens.go b/staging/src/k8s.io/cluster-bootstrap/util/tokens/tokens.go new file mode 100644 index 00000000000..ad8fb3b4732 --- /dev/null +++ b/staging/src/k8s.io/cluster-bootstrap/util/tokens/tokens.go @@ -0,0 +1,39 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tokens + +import ( + "fmt" + "regexp" + + "k8s.io/cluster-bootstrap/token/api" +) + +// ParseToken tries and parse a valid token from a string. +// A token ID and token secret are returned in case of success, an error otherwise. +func ParseToken(s string) (tokenID, tokenSecret string, err error) { + bootstrapTokenRegexp, err := regexp.Compile(api.BootstrapTokenPattern) + if err != nil { + return "", "", fmt.Errorf("error compiling bootstrap regex %q: %v", api.BootstrapTokenPattern, err) + } + + split := bootstrapTokenRegexp.FindStringSubmatch(s) + if len(split) != 3 { + return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, api.BootstrapTokenPattern) + } + return split[1], split[2], nil +}