k8s.io/kubernetes@v1.29.3/pkg/registry/core/serviceaccount/storage/token.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package storage
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"time"
    23  
    24  	authenticationapiv1 "k8s.io/api/authentication/v1"
    25  	"k8s.io/apimachinery/pkg/api/errors"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"k8s.io/apimachinery/pkg/util/validation/field"
    32  	"k8s.io/apiserver/pkg/audit"
    33  	"k8s.io/apiserver/pkg/authentication/authenticator"
    34  	"k8s.io/apiserver/pkg/authentication/serviceaccount"
    35  	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
    36  	"k8s.io/apiserver/pkg/registry/rest"
    37  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    38  	"k8s.io/apiserver/pkg/warning"
    39  	"k8s.io/klog/v2"
    40  	authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
    41  	authenticationvalidation "k8s.io/kubernetes/pkg/apis/authentication/validation"
    42  	api "k8s.io/kubernetes/pkg/apis/core"
    43  	"k8s.io/kubernetes/pkg/features"
    44  	token "k8s.io/kubernetes/pkg/serviceaccount"
    45  )
    46  
    47  func (r *TokenREST) New() runtime.Object {
    48  	return &authenticationapi.TokenRequest{}
    49  }
    50  
    51  // Destroy cleans up resources on shutdown.
    52  func (r *TokenREST) Destroy() {
    53  	// Given no underlying store, we don't destroy anything
    54  	// here explicitly.
    55  }
    56  
    57  type TokenREST struct {
    58  	svcaccts             rest.Getter
    59  	pods                 rest.Getter
    60  	secrets              rest.Getter
    61  	nodes                rest.Getter
    62  	issuer               token.TokenGenerator
    63  	auds                 authenticator.Audiences
    64  	audsSet              sets.String
    65  	maxExpirationSeconds int64
    66  	extendExpiration     bool
    67  }
    68  
    69  var _ = rest.NamedCreater(&TokenREST{})
    70  var _ = rest.GroupVersionKindProvider(&TokenREST{})
    71  
    72  var gvk = schema.GroupVersionKind{
    73  	Group:   authenticationapiv1.SchemeGroupVersion.Group,
    74  	Version: authenticationapiv1.SchemeGroupVersion.Version,
    75  	Kind:    "TokenRequest",
    76  }
    77  
    78  func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
    79  	req := obj.(*authenticationapi.TokenRequest)
    80  
    81  	// Get the namespace from the context (populated from the URL).
    82  	namespace, ok := genericapirequest.NamespaceFrom(ctx)
    83  	if !ok {
    84  		return nil, errors.NewBadRequest("namespace is required")
    85  	}
    86  
    87  	// require name/namespace in the body to match URL if specified
    88  	if len(req.Name) > 0 && req.Name != name {
    89  		errs := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("name"), req.Name, "must match the service account name if specified")}
    90  		return nil, errors.NewInvalid(gvk.GroupKind(), name, errs)
    91  	}
    92  	if len(req.Namespace) > 0 && req.Namespace != namespace {
    93  		errs := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("namespace"), req.Namespace, "must match the service account namespace if specified")}
    94  		return nil, errors.NewInvalid(gvk.GroupKind(), name, errs)
    95  	}
    96  
    97  	// Lookup service account
    98  	svcacctObj, err := r.svcaccts.Get(ctx, name, &metav1.GetOptions{})
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	svcacct := svcacctObj.(*api.ServiceAccount)
   103  
   104  	// Default unset spec audiences to API server audiences based on server config
   105  	if len(req.Spec.Audiences) == 0 {
   106  		req.Spec.Audiences = r.auds
   107  	}
   108  	// Populate metadata fields if not set
   109  	if len(req.Name) == 0 {
   110  		req.Name = svcacct.Name
   111  	}
   112  	if len(req.Namespace) == 0 {
   113  		req.Namespace = svcacct.Namespace
   114  	}
   115  
   116  	// Save current time before building the token, to make sure the expiration
   117  	// returned in TokenRequestStatus would be <= the exp field in token.
   118  	nowTime := time.Now()
   119  	req.CreationTimestamp = metav1.NewTime(nowTime)
   120  
   121  	// Clear status
   122  	req.Status = authenticationapi.TokenRequestStatus{}
   123  
   124  	// call static validation, then validating admission
   125  	if errs := authenticationvalidation.ValidateTokenRequest(req); len(errs) != 0 {
   126  		return nil, errors.NewInvalid(gvk.GroupKind(), "", errs)
   127  	}
   128  	if createValidation != nil {
   129  		if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
   130  			return nil, err
   131  		}
   132  	}
   133  
   134  	var (
   135  		pod    *api.Pod
   136  		node   *api.Node
   137  		secret *api.Secret
   138  	)
   139  
   140  	if ref := req.Spec.BoundObjectRef; ref != nil {
   141  		var uid types.UID
   142  
   143  		gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)
   144  		switch {
   145  		case gvk.Group == "" && gvk.Kind == "Pod":
   146  			newCtx := newContext(ctx, "pods", ref.Name, namespace, gvk)
   147  			podObj, err := r.pods.Get(newCtx, ref.Name, &metav1.GetOptions{})
   148  			if err != nil {
   149  				return nil, err
   150  			}
   151  			pod = podObj.(*api.Pod)
   152  			if name != pod.Spec.ServiceAccountName {
   153  				return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token for serviceaccount %q to pod running with different serviceaccount name.", name))
   154  			}
   155  			uid = pod.UID
   156  			if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
   157  				if nodeName := pod.Spec.NodeName; nodeName != "" {
   158  					newCtx := newContext(ctx, "nodes", nodeName, "", api.SchemeGroupVersion.WithKind("Node"))
   159  					// set ResourceVersion=0 to allow this to be read/served from the apiservers watch cache
   160  					nodeObj, err := r.nodes.Get(newCtx, nodeName, &metav1.GetOptions{ResourceVersion: "0"})
   161  					if err != nil {
   162  						nodeObj, err = r.nodes.Get(newCtx, nodeName, &metav1.GetOptions{}) // fallback to a live lookup on any error
   163  					}
   164  					switch {
   165  					case errors.IsNotFound(err):
   166  						// if the referenced Node object does not exist, we still embed just the pod name into the
   167  						// claims so that clients still have some indication of what node a pod is assigned to when
   168  						// inspecting a token (even if the UID is not present).
   169  						klog.V(4).ErrorS(err, "failed fetching node for pod", "pod", klog.KObj(pod), "podUID", pod.UID, "nodeName", nodeName)
   170  						node = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName}}
   171  					case err != nil:
   172  						return nil, errors.NewInternalError(err)
   173  					default:
   174  						node = nodeObj.(*api.Node)
   175  					}
   176  				}
   177  			}
   178  		case gvk.Group == "" && gvk.Kind == "Node":
   179  			if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBinding) {
   180  				return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token to a Node object as the %q feature-gate is disabled", features.ServiceAccountTokenNodeBinding))
   181  			}
   182  			newCtx := newContext(ctx, "nodes", ref.Name, "", gvk)
   183  			nodeObj, err := r.nodes.Get(newCtx, ref.Name, &metav1.GetOptions{})
   184  			if err != nil {
   185  				return nil, err
   186  			}
   187  			node = nodeObj.(*api.Node)
   188  			uid = node.UID
   189  		case gvk.Group == "" && gvk.Kind == "Secret":
   190  			newCtx := newContext(ctx, "secrets", ref.Name, namespace, gvk)
   191  			secretObj, err := r.secrets.Get(newCtx, ref.Name, &metav1.GetOptions{})
   192  			if err != nil {
   193  				return nil, err
   194  			}
   195  			secret = secretObj.(*api.Secret)
   196  			uid = secret.UID
   197  		default:
   198  			return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token to object of type %s", gvk.String()))
   199  		}
   200  		if ref.UID != "" && uid != ref.UID {
   201  			return nil, errors.NewConflict(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, ref.Name, fmt.Errorf("the UID in the bound object reference (%s) does not match the UID in record. The object might have been deleted and then recreated", ref.UID))
   202  		}
   203  	}
   204  
   205  	if r.maxExpirationSeconds > 0 && req.Spec.ExpirationSeconds > r.maxExpirationSeconds {
   206  		//only positive value is valid
   207  		warning.AddWarning(ctx, "", fmt.Sprintf("requested expiration of %d seconds shortened to %d seconds", req.Spec.ExpirationSeconds, r.maxExpirationSeconds))
   208  		req.Spec.ExpirationSeconds = r.maxExpirationSeconds
   209  	}
   210  
   211  	// Tweak expiration for safe transition of projected service account token.
   212  	// Warn (instead of fail) after requested expiration time.
   213  	// Fail after hard-coded extended expiration time.
   214  	// Only perform the extension when token is pod-bound.
   215  	var warnAfter int64
   216  	exp := req.Spec.ExpirationSeconds
   217  	if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) {
   218  		warnAfter = exp
   219  		exp = token.ExpirationExtensionSeconds
   220  	}
   221  
   222  	sc, pc, err := token.Claims(*svcacct, pod, secret, node, exp, warnAfter, req.Spec.Audiences)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	tokdata, err := r.issuer.GenerateToken(sc, pc)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("failed to generate token: %v", err)
   229  	}
   230  
   231  	// populate status
   232  	out := req.DeepCopy()
   233  	out.Status = authenticationapi.TokenRequestStatus{
   234  		Token:               tokdata,
   235  		ExpirationTimestamp: metav1.Time{Time: nowTime.Add(time.Duration(out.Spec.ExpirationSeconds) * time.Second)},
   236  	}
   237  	if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) && len(sc.ID) > 0 {
   238  		audit.AddAuditAnnotation(ctx, serviceaccount.CredentialIDKey, serviceaccount.CredentialIDForJTI(sc.ID))
   239  	}
   240  	return out, nil
   241  }
   242  
   243  func (r *TokenREST) GroupVersionKind(schema.GroupVersion) schema.GroupVersionKind {
   244  	return gvk
   245  }
   246  
   247  // newContext return a copy of ctx in which new RequestInfo is set
   248  func newContext(ctx context.Context, resource, name, namespace string, gvk schema.GroupVersionKind) context.Context {
   249  	newInfo := genericapirequest.RequestInfo{
   250  		IsResourceRequest: true,
   251  		Verb:              "get",
   252  		Namespace:         namespace,
   253  		Resource:          resource,
   254  		Name:              name,
   255  		Parts:             []string{resource, name},
   256  		APIGroup:          gvk.Group,
   257  		APIVersion:        gvk.Version,
   258  	}
   259  	return genericapirequest.WithRequestInfo(ctx, &newInfo)
   260  }
   261  
   262  // isKubeAudiences returns true if the tokenaudiences is a strict subset of apiserver audiences.
   263  func (r *TokenREST) isKubeAudiences(tokenAudience []string) bool {
   264  	// tokenAudiences must be a strict subset of apiserver audiences
   265  	return r.audsSet.HasAll(tokenAudience...)
   266  }