github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/provisioning.go (about)

     1  /*
     2  Copyright 2020-2022 Gravitational, Inc.
     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 types
    18  
    19  import (
    20  	"crypto/x509"
    21  	"encoding/pem"
    22  	"fmt"
    23  	"slices"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/gravitational/trace"
    28  
    29  	"github.com/gravitational/teleport/api/defaults"
    30  	apiutils "github.com/gravitational/teleport/api/utils"
    31  )
    32  
    33  // JoinMethod is the method used for new nodes to join the cluster.
    34  type JoinMethod string
    35  
    36  const (
    37  	JoinMethodUnspecified JoinMethod = ""
    38  	// JoinMethodToken is the default join method, nodes join the cluster by
    39  	// presenting a secret token.
    40  	JoinMethodToken JoinMethod = "token"
    41  	// JoinMethodEC2 indicates that the node will join with the EC2 join method.
    42  	JoinMethodEC2 JoinMethod = "ec2"
    43  	// JoinMethodIAM indicates that the node will join with the IAM join method.
    44  	JoinMethodIAM JoinMethod = "iam"
    45  	// JoinMethodGitHub indicates that the node will join with the GitHub join
    46  	// method. Documentation regarding the implementation of this can be found
    47  	// in lib/githubactions
    48  	JoinMethodGitHub JoinMethod = "github"
    49  	// JoinMethodCircleCI indicates that the node will join with the CircleCI\
    50  	// join method. Documentation regarding the implementation of this can be
    51  	// found in lib/circleci
    52  	JoinMethodCircleCI JoinMethod = "circleci"
    53  	// JoinMethodKubernetes indicates that the node will join with the
    54  	// Kubernetes join method. Documentation regarding implementation can be
    55  	// found in lib/kubernetestoken
    56  	JoinMethodKubernetes JoinMethod = "kubernetes"
    57  	// JoinMethodAzure indicates that the node will join with the Azure join
    58  	// method.
    59  	JoinMethodAzure JoinMethod = "azure"
    60  	// JoinMethodGitLab indicates that the node will join with the GitLab
    61  	// join method. Documentation regarding implementation of this
    62  	// can be found in lib/gitlab
    63  	JoinMethodGitLab JoinMethod = "gitlab"
    64  	// JoinMethodGCP indicates that the node will join with the GCP join method.
    65  	// Documentation regarding implementation of this can be found in lib/gcp.
    66  	JoinMethodGCP JoinMethod = "gcp"
    67  	// JoinMethodSpacelift indicates the node will join with the SpaceLift join
    68  	// method. Documentation regarding implementation of this can be found in
    69  	// lib/spacelift.
    70  	JoinMethodSpacelift JoinMethod = "spacelift"
    71  	// JoinMethodTPM indicates that the node will join with the TPM join method.
    72  	// The core implementation of this join method can be found in lib/tpm.
    73  	JoinMethodTPM JoinMethod = "tpm"
    74  )
    75  
    76  var JoinMethods = []JoinMethod{
    77  	JoinMethodAzure,
    78  	JoinMethodCircleCI,
    79  	JoinMethodEC2,
    80  	JoinMethodGCP,
    81  	JoinMethodGitHub,
    82  	JoinMethodGitLab,
    83  	JoinMethodIAM,
    84  	JoinMethodKubernetes,
    85  	JoinMethodSpacelift,
    86  	JoinMethodToken,
    87  	JoinMethodTPM,
    88  }
    89  
    90  func ValidateJoinMethod(method JoinMethod) error {
    91  	hasJoinMethod := slices.Contains(JoinMethods, method)
    92  	if !hasJoinMethod {
    93  		return trace.BadParameter("join method must be one of %s", apiutils.JoinStrings(JoinMethods, ", "))
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  type KubernetesJoinType string
   100  
   101  var (
   102  	KubernetesJoinTypeUnspecified KubernetesJoinType = ""
   103  	KubernetesJoinTypeInCluster   KubernetesJoinType = "in_cluster"
   104  	KubernetesJoinTypeStaticJWKS  KubernetesJoinType = "static_jwks"
   105  )
   106  
   107  // ProvisionToken is a provisioning token
   108  type ProvisionToken interface {
   109  	ResourceWithOrigin
   110  	// SetMetadata sets resource metatada
   111  	SetMetadata(meta Metadata)
   112  	// GetRoles returns a list of teleport roles
   113  	// that will be granted to the user of the token
   114  	// in the crendentials
   115  	GetRoles() SystemRoles
   116  	// SetRoles sets teleport roles
   117  	SetRoles(SystemRoles)
   118  	// SetLabels sets the tokens labels
   119  	SetLabels(map[string]string)
   120  	// GetAllowRules returns the list of allow rules
   121  	GetAllowRules() []*TokenRule
   122  	// SetAllowRules sets the allow rules
   123  	SetAllowRules([]*TokenRule)
   124  	// GetAWSIIDTTL returns the TTL of EC2 IIDs
   125  	GetAWSIIDTTL() Duration
   126  	// GetJoinMethod returns joining method that must be used with this token.
   127  	GetJoinMethod() JoinMethod
   128  	// GetBotName returns the BotName field which must be set for joining bots.
   129  	GetBotName() string
   130  
   131  	// GetSuggestedLabels returns the set of labels that the resource should add when adding itself to the cluster
   132  	GetSuggestedLabels() Labels
   133  
   134  	// GetSuggestedAgentMatcherLabels returns the set of labels that should be watched when an agent/service uses this token.
   135  	// An example of this is the Database Agent.
   136  	// When using the install-database.sh script, the script will add those labels as part of the `teleport.yaml` configuration.
   137  	// They are added to `db_service.resources.0.labels`.
   138  	GetSuggestedAgentMatcherLabels() Labels
   139  
   140  	// V1 returns V1 version of the resource
   141  	V1() *ProvisionTokenV1
   142  	// String returns user friendly representation of the resource
   143  	String() string
   144  
   145  	// GetSafeName returns the name of the token, sanitized appropriately for
   146  	// join methods where the name is secret. This should be used when logging
   147  	// the token name.
   148  	GetSafeName() string
   149  }
   150  
   151  // NewProvisionToken returns a new provision token with the given roles.
   152  func NewProvisionToken(token string, roles SystemRoles, expires time.Time) (ProvisionToken, error) {
   153  	return NewProvisionTokenFromSpec(token, expires, ProvisionTokenSpecV2{
   154  		Roles: roles,
   155  	})
   156  }
   157  
   158  // NewProvisionTokenFromSpec returns a new provision token with the given spec.
   159  func NewProvisionTokenFromSpec(token string, expires time.Time, spec ProvisionTokenSpecV2) (ProvisionToken, error) {
   160  	t := &ProvisionTokenV2{
   161  		Metadata: Metadata{
   162  			Name:    token,
   163  			Expires: &expires,
   164  		},
   165  		Spec: spec,
   166  	}
   167  	if err := t.CheckAndSetDefaults(); err != nil {
   168  		return nil, trace.Wrap(err)
   169  	}
   170  	return t, nil
   171  }
   172  
   173  // MustCreateProvisionToken returns a new valid provision token
   174  // or panics, used in tests
   175  func MustCreateProvisionToken(token string, roles SystemRoles, expires time.Time) ProvisionToken {
   176  	t, err := NewProvisionToken(token, roles, expires)
   177  	if err != nil {
   178  		panic(err)
   179  	}
   180  	return t
   181  }
   182  
   183  // setStaticFields sets static resource header and metadata fields.
   184  func (p *ProvisionTokenV2) setStaticFields() {
   185  	p.Kind = KindToken
   186  	p.Version = V2
   187  }
   188  
   189  // CheckAndSetDefaults checks and set default values for any missing fields.
   190  func (p *ProvisionTokenV2) CheckAndSetDefaults() error {
   191  	p.setStaticFields()
   192  	if err := p.Metadata.CheckAndSetDefaults(); err != nil {
   193  		return trace.Wrap(err)
   194  	}
   195  
   196  	if len(p.Spec.Roles) == 0 {
   197  		return trace.BadParameter("provisioning token is missing roles")
   198  	}
   199  	roles, err := NewTeleportRoles(SystemRoles(p.Spec.Roles).StringSlice())
   200  	if err != nil {
   201  		return trace.Wrap(err)
   202  	}
   203  	p.Spec.Roles = roles
   204  
   205  	if roles.Include(RoleBot) && p.Spec.BotName == "" {
   206  		return trace.BadParameter("token with role %q must set bot_name", RoleBot)
   207  	}
   208  
   209  	if p.Spec.BotName != "" && !roles.Include(RoleBot) {
   210  		return trace.BadParameter("can only set bot_name on token with role %q", RoleBot)
   211  	}
   212  
   213  	hasAllowRules := len(p.Spec.Allow) > 0
   214  	if p.Spec.JoinMethod == JoinMethodUnspecified {
   215  		// Default to the ec2 join method if any allow rules were specified,
   216  		// else default to the token method. These defaults are necessary for
   217  		// backwards compatibility.
   218  		if hasAllowRules {
   219  			p.Spec.JoinMethod = JoinMethodEC2
   220  		} else {
   221  			p.Spec.JoinMethod = JoinMethodToken
   222  		}
   223  	}
   224  	switch p.Spec.JoinMethod {
   225  	case JoinMethodToken:
   226  		if hasAllowRules {
   227  			return trace.BadParameter("allow rules are not compatible with the %q join method", JoinMethodToken)
   228  		}
   229  	case JoinMethodEC2:
   230  		if !hasAllowRules {
   231  			return trace.BadParameter("the %q join method requires defined token allow rules", JoinMethodEC2)
   232  		}
   233  		for _, allowRule := range p.Spec.Allow {
   234  			if allowRule.AWSARN != "" {
   235  				return trace.BadParameter(`the %q join method does not support the "aws_arn" parameter`, JoinMethodEC2)
   236  			}
   237  			if allowRule.AWSAccount == "" && allowRule.AWSRole == "" {
   238  				return trace.BadParameter(`allow rule for %q join method must set "aws_account" or "aws_role"`, JoinMethodEC2)
   239  			}
   240  		}
   241  		if p.Spec.AWSIIDTTL == 0 {
   242  			// default to 5 minute ttl if unspecified
   243  			p.Spec.AWSIIDTTL = Duration(5 * time.Minute)
   244  		}
   245  	case JoinMethodIAM:
   246  		if !hasAllowRules {
   247  			return trace.BadParameter("the %q join method requires defined token allow rules", JoinMethodIAM)
   248  		}
   249  		for _, allowRule := range p.Spec.Allow {
   250  			if allowRule.AWSRole != "" {
   251  				return trace.BadParameter(`the %q join method does not support the "aws_role" parameter`, JoinMethodIAM)
   252  			}
   253  			if len(allowRule.AWSRegions) != 0 {
   254  				return trace.BadParameter(`the %q join method does not support the "aws_regions" parameter`, JoinMethodIAM)
   255  			}
   256  			if allowRule.AWSAccount == "" && allowRule.AWSARN == "" {
   257  				return trace.BadParameter(`allow rule for %q join method must set "aws_account" or "aws_arn"`, JoinMethodEC2)
   258  			}
   259  		}
   260  	case JoinMethodGitHub:
   261  		providerCfg := p.Spec.GitHub
   262  		if providerCfg == nil {
   263  			return trace.BadParameter(
   264  				`"github" configuration must be provided for join method %q`,
   265  				JoinMethodGitHub,
   266  			)
   267  		}
   268  		if err := providerCfg.checkAndSetDefaults(); err != nil {
   269  			return trace.Wrap(err)
   270  		}
   271  	case JoinMethodCircleCI:
   272  		providerCfg := p.Spec.CircleCI
   273  		if providerCfg == nil {
   274  			return trace.BadParameter(
   275  				`"cirleci" configuration must be provided for join method %q`,
   276  				JoinMethodCircleCI,
   277  			)
   278  		}
   279  		if err := providerCfg.checkAndSetDefaults(); err != nil {
   280  			return trace.Wrap(err)
   281  		}
   282  	case JoinMethodKubernetes:
   283  		providerCfg := p.Spec.Kubernetes
   284  		if providerCfg == nil {
   285  			return trace.BadParameter(
   286  				`"kubernetes" configuration must be provided for the join method %q`,
   287  				JoinMethodKubernetes,
   288  			)
   289  		}
   290  		if err := providerCfg.checkAndSetDefaults(); err != nil {
   291  			return trace.Wrap(err, "spec.kubernetes:")
   292  		}
   293  	case JoinMethodAzure:
   294  		providerCfg := p.Spec.Azure
   295  		if providerCfg == nil {
   296  			return trace.BadParameter(
   297  				`"azure" configuration must be provided for the join method %q`,
   298  				JoinMethodAzure,
   299  			)
   300  		}
   301  		if err := providerCfg.checkAndSetDefaults(); err != nil {
   302  			return trace.Wrap(err)
   303  		}
   304  	case JoinMethodGitLab:
   305  		providerCfg := p.Spec.GitLab
   306  		if providerCfg == nil {
   307  			return trace.BadParameter(
   308  				`"gitlab" configuration must be provided for the join method %q`,
   309  				JoinMethodGitLab,
   310  			)
   311  		}
   312  		if err := providerCfg.checkAndSetDefaults(); err != nil {
   313  			return trace.Wrap(err)
   314  		}
   315  	case JoinMethodGCP:
   316  		providerCfg := p.Spec.GCP
   317  		if providerCfg == nil {
   318  			return trace.BadParameter(
   319  				`"gcp" configuration must be provided for the join method %q`,
   320  				JoinMethodGCP,
   321  			)
   322  		}
   323  		if err := providerCfg.checkAndSetDefaults(); err != nil {
   324  			return trace.Wrap(err)
   325  		}
   326  	case JoinMethodSpacelift:
   327  		providerCfg := p.Spec.Spacelift
   328  		if providerCfg == nil {
   329  			return trace.BadParameter(
   330  				`spec.spacelift: must be configured for the join method %q`,
   331  				JoinMethodSpacelift,
   332  			)
   333  		}
   334  		if err := providerCfg.checkAndSetDefaults(); err != nil {
   335  			return trace.Wrap(err, "spec.spacelift: failed validation")
   336  		}
   337  	case JoinMethodTPM:
   338  		providerCfg := p.Spec.TPM
   339  		if providerCfg == nil {
   340  			return trace.BadParameter(
   341  				`spec.tpm: must be configured for the join method %q`,
   342  				JoinMethodTPM,
   343  			)
   344  		}
   345  		if err := providerCfg.validate(); err != nil {
   346  			return trace.Wrap(err, "spec.tpm: failed validation")
   347  		}
   348  	default:
   349  		return trace.BadParameter("unknown join method %q", p.Spec.JoinMethod)
   350  	}
   351  
   352  	return nil
   353  }
   354  
   355  // GetVersion returns resource version
   356  func (p *ProvisionTokenV2) GetVersion() string {
   357  	return p.Version
   358  }
   359  
   360  // GetRoles returns a list of teleport roles
   361  // that will be granted to the user of the token
   362  // in the crendentials
   363  func (p *ProvisionTokenV2) GetRoles() SystemRoles {
   364  	// Ensure that roles are case-insensitive.
   365  	return normalizedSystemRoles(SystemRoles(p.Spec.Roles).StringSlice())
   366  }
   367  
   368  // SetRoles sets teleport roles
   369  func (p *ProvisionTokenV2) SetRoles(r SystemRoles) {
   370  	p.Spec.Roles = r
   371  }
   372  
   373  func (p *ProvisionTokenV2) SetLabels(l map[string]string) {
   374  	p.Metadata.Labels = l
   375  }
   376  
   377  // GetAllowRules returns the list of allow rules
   378  func (p *ProvisionTokenV2) GetAllowRules() []*TokenRule {
   379  	return p.Spec.Allow
   380  }
   381  
   382  // SetAllowRules sets the allow rules.
   383  func (p *ProvisionTokenV2) SetAllowRules(rules []*TokenRule) {
   384  	p.Spec.Allow = rules
   385  }
   386  
   387  // GetAWSIIDTTL returns the TTL of EC2 IIDs
   388  func (p *ProvisionTokenV2) GetAWSIIDTTL() Duration {
   389  	return p.Spec.AWSIIDTTL
   390  }
   391  
   392  // GetJoinMethod returns joining method that must be used with this token.
   393  func (p *ProvisionTokenV2) GetJoinMethod() JoinMethod {
   394  	return p.Spec.JoinMethod
   395  }
   396  
   397  // GetBotName returns the BotName field which must be set for joining bots.
   398  func (p *ProvisionTokenV2) GetBotName() string {
   399  	return p.Spec.BotName
   400  }
   401  
   402  // GetKind returns resource kind
   403  func (p *ProvisionTokenV2) GetKind() string {
   404  	return p.Kind
   405  }
   406  
   407  // GetSubKind returns resource sub kind
   408  func (p *ProvisionTokenV2) GetSubKind() string {
   409  	return p.SubKind
   410  }
   411  
   412  // SetSubKind sets resource subkind
   413  func (p *ProvisionTokenV2) SetSubKind(s string) {
   414  	p.SubKind = s
   415  }
   416  
   417  // GetResourceID returns resource ID
   418  func (p *ProvisionTokenV2) GetResourceID() int64 {
   419  	return p.Metadata.ID
   420  }
   421  
   422  // SetResourceID sets resource ID
   423  func (p *ProvisionTokenV2) SetResourceID(id int64) {
   424  	p.Metadata.ID = id
   425  }
   426  
   427  // GetRevision returns the revision
   428  func (p *ProvisionTokenV2) GetRevision() string {
   429  	return p.Metadata.GetRevision()
   430  }
   431  
   432  // SetRevision sets the revision
   433  func (p *ProvisionTokenV2) SetRevision(rev string) {
   434  	p.Metadata.SetRevision(rev)
   435  }
   436  
   437  // GetMetadata returns metadata
   438  func (p *ProvisionTokenV2) GetMetadata() Metadata {
   439  	return p.Metadata
   440  }
   441  
   442  // SetMetadata sets resource metatada
   443  func (p *ProvisionTokenV2) SetMetadata(meta Metadata) {
   444  	p.Metadata = meta
   445  }
   446  
   447  // Origin returns the origin value of the resource.
   448  func (p *ProvisionTokenV2) Origin() string {
   449  	return p.Metadata.Origin()
   450  }
   451  
   452  // SetOrigin sets the origin value of the resource.
   453  func (p *ProvisionTokenV2) SetOrigin(origin string) {
   454  	p.Metadata.SetOrigin(origin)
   455  }
   456  
   457  // GetSuggestedLabels returns the labels the resource should set when using this token
   458  func (p *ProvisionTokenV2) GetSuggestedLabels() Labels {
   459  	return p.Spec.SuggestedLabels
   460  }
   461  
   462  // GetAgentMatcherLabels returns the set of labels that should be watched when an agent/service uses this token.
   463  // An example of this is the Database Agent.
   464  // When using the install-database.sh script, the script will add those labels as part of the `teleport.yaml` configuration.
   465  // They are added to `db_service.resources.0.labels`.
   466  func (p *ProvisionTokenV2) GetSuggestedAgentMatcherLabels() Labels {
   467  	return p.Spec.SuggestedAgentMatcherLabels
   468  }
   469  
   470  // V1 returns V1 version of the resource
   471  func (p *ProvisionTokenV2) V1() *ProvisionTokenV1 {
   472  	return &ProvisionTokenV1{
   473  		Roles:   p.Spec.Roles,
   474  		Expires: p.Metadata.Expiry(),
   475  		Token:   p.Metadata.Name,
   476  	}
   477  }
   478  
   479  // V2 returns V2 version of the resource
   480  func (p *ProvisionTokenV2) V2() *ProvisionTokenV2 {
   481  	return p
   482  }
   483  
   484  // SetExpiry sets expiry time for the object
   485  func (p *ProvisionTokenV2) SetExpiry(expires time.Time) {
   486  	p.Metadata.SetExpiry(expires)
   487  }
   488  
   489  // Expiry returns object expiry setting
   490  func (p *ProvisionTokenV2) Expiry() time.Time {
   491  	return p.Metadata.Expiry()
   492  }
   493  
   494  // GetName returns the name of the provision token. This value can be secret!
   495  // Use GetSafeName where the name may be logged.
   496  func (p *ProvisionTokenV2) GetName() string {
   497  	return p.Metadata.Name
   498  }
   499  
   500  // SetName sets the name of the provision token.
   501  func (p *ProvisionTokenV2) SetName(e string) {
   502  	p.Metadata.Name = e
   503  }
   504  
   505  // GetSafeName returns the name of the token, sanitized appropriately for
   506  // join methods where the name is secret. This should be used when logging
   507  // the token name.
   508  func (p *ProvisionTokenV2) GetSafeName() string {
   509  	name := p.GetName()
   510  	if p.GetJoinMethod() != JoinMethodToken {
   511  		return name
   512  	}
   513  
   514  	// If the token name is short, we just blank the whole thing.
   515  	if len(name) < 16 {
   516  		return strings.Repeat("*", len(name))
   517  	}
   518  
   519  	// If the token name is longer, we can show the last 25% of it to help
   520  	// the operator identify it.
   521  	hiddenBefore := int(0.75 * float64(len(name)))
   522  	name = name[hiddenBefore:]
   523  	name = strings.Repeat("*", hiddenBefore) + name
   524  	return name
   525  }
   526  
   527  // String returns the human readable representation of a provisioning token.
   528  func (p ProvisionTokenV2) String() string {
   529  	expires := "never"
   530  	if !p.Expiry().IsZero() {
   531  		expires = p.Expiry().String()
   532  	}
   533  	return fmt.Sprintf("ProvisionToken(Roles=%v, Expires=%v)", p.Spec.Roles, expires)
   534  }
   535  
   536  // ProvisionTokensToV1 converts provision tokens to V1 list
   537  func ProvisionTokensToV1(in []ProvisionToken) []ProvisionTokenV1 {
   538  	if in == nil {
   539  		return nil
   540  	}
   541  	out := make([]ProvisionTokenV1, len(in))
   542  	for i := range in {
   543  		out[i] = *in[i].V1()
   544  	}
   545  	return out
   546  }
   547  
   548  // ProvisionTokensFromV1 converts V1 provision tokens to resource list
   549  func ProvisionTokensFromV1(in []ProvisionTokenV1) []ProvisionToken {
   550  	if in == nil {
   551  		return nil
   552  	}
   553  	out := make([]ProvisionToken, len(in))
   554  	for i := range in {
   555  		out[i] = in[i].V2()
   556  	}
   557  	return out
   558  }
   559  
   560  // V1 returns V1 version of the resource
   561  func (p *ProvisionTokenV1) V1() *ProvisionTokenV1 {
   562  	return p
   563  }
   564  
   565  // V2 returns V2 version of the resource
   566  func (p *ProvisionTokenV1) V2() *ProvisionTokenV2 {
   567  	t := &ProvisionTokenV2{
   568  		Kind:    KindToken,
   569  		Version: V2,
   570  		Metadata: Metadata{
   571  			Name:      p.Token,
   572  			Namespace: defaults.Namespace,
   573  		},
   574  		Spec: ProvisionTokenSpecV2{
   575  			Roles: p.Roles,
   576  		},
   577  	}
   578  	if !p.Expires.IsZero() {
   579  		t.SetExpiry(p.Expires)
   580  	}
   581  	t.CheckAndSetDefaults()
   582  	return t
   583  }
   584  
   585  // String returns the human readable representation of a provisioning token.
   586  func (p ProvisionTokenV1) String() string {
   587  	expires := "never"
   588  	if p.Expires.Unix() != 0 {
   589  		expires = p.Expires.String()
   590  	}
   591  	return fmt.Sprintf("ProvisionToken(Roles=%v, Expires=%v)",
   592  		p.Roles, expires)
   593  }
   594  
   595  func (a *ProvisionTokenSpecV2GitHub) checkAndSetDefaults() error {
   596  	if len(a.Allow) == 0 {
   597  		return trace.BadParameter("the %q join method requires at least one token allow rule", JoinMethodGitHub)
   598  	}
   599  	for _, rule := range a.Allow {
   600  		repoSet := rule.Repository != ""
   601  		ownerSet := rule.RepositoryOwner != ""
   602  		subSet := rule.Sub != ""
   603  		if !(subSet || ownerSet || repoSet) {
   604  			return trace.BadParameter(
   605  				`allow rule for %q must include at least one of "repository", "repository_owner" or "sub"`,
   606  				JoinMethodGitHub,
   607  			)
   608  		}
   609  	}
   610  	if strings.Contains(a.EnterpriseServerHost, "/") {
   611  		return trace.BadParameter("'spec.github.enterprise_server_host' should not contain the scheme or path")
   612  	}
   613  	if a.EnterpriseServerHost != "" && a.EnterpriseSlug != "" {
   614  		return trace.BadParameter("'spec.github.enterprise_server_host' and `spec.github.enterprise_slug` cannot both be set")
   615  	}
   616  	return nil
   617  }
   618  
   619  func (a *ProvisionTokenSpecV2CircleCI) checkAndSetDefaults() error {
   620  	if len(a.Allow) == 0 {
   621  		return trace.BadParameter("the %q join method requires at least one token allow rule", JoinMethodCircleCI)
   622  	}
   623  	if a.OrganizationID == "" {
   624  		return trace.BadParameter("the %q join method requires 'organization_id' to be set", JoinMethodCircleCI)
   625  	}
   626  	for _, rule := range a.Allow {
   627  		projectSet := rule.ProjectID != ""
   628  		contextSet := rule.ContextID != ""
   629  		if !projectSet && !contextSet {
   630  			return trace.BadParameter(
   631  				`allow rule for %q must include at least "project_id" or "context_id"`,
   632  				JoinMethodCircleCI,
   633  			)
   634  		}
   635  	}
   636  	return nil
   637  }
   638  
   639  func (a *ProvisionTokenSpecV2Kubernetes) checkAndSetDefaults() error {
   640  	if len(a.Allow) == 0 {
   641  		return trace.BadParameter("allow: at least one rule must be set")
   642  	}
   643  	for i, allowRule := range a.Allow {
   644  		if allowRule.ServiceAccount == "" {
   645  			return trace.BadParameter(
   646  				"allow[%d].service_account: name of service account must be set",
   647  				i,
   648  			)
   649  		}
   650  		if len(strings.Split(allowRule.ServiceAccount, ":")) != 2 {
   651  			return trace.BadParameter(
   652  				`allow[%d].service_account: name of service account should be in format "namespace:service_account", got %q instead`,
   653  				i,
   654  				allowRule.ServiceAccount,
   655  			)
   656  		}
   657  	}
   658  
   659  	if a.Type == KubernetesJoinTypeUnspecified {
   660  		// For compatibility with older resources which did not have a Type
   661  		// field we default to "in_cluster".
   662  		a.Type = KubernetesJoinTypeInCluster
   663  	}
   664  	switch a.Type {
   665  	case KubernetesJoinTypeInCluster:
   666  		if a.StaticJWKS != nil {
   667  			return trace.BadParameter("static_jwks: must not be set when type is %q", KubernetesJoinTypeInCluster)
   668  		}
   669  	case KubernetesJoinTypeStaticJWKS:
   670  		if a.StaticJWKS == nil {
   671  			return trace.BadParameter("static_jwks: must be set when type is %q", KubernetesJoinTypeStaticJWKS)
   672  		}
   673  		if a.StaticJWKS.JWKS == "" {
   674  			return trace.BadParameter("static_jwks.jwks: must be set when type is %q", KubernetesJoinTypeStaticJWKS)
   675  		}
   676  	default:
   677  		return trace.BadParameter(
   678  			"type: must be one of (%s), got %q",
   679  			apiutils.JoinStrings(JoinMethods, ", "),
   680  			a.Type,
   681  		)
   682  	}
   683  
   684  	return nil
   685  }
   686  
   687  func (a *ProvisionTokenSpecV2Azure) checkAndSetDefaults() error {
   688  	if len(a.Allow) == 0 {
   689  		return trace.BadParameter(
   690  			"the %q join method requires defined azure allow rules",
   691  			JoinMethodAzure,
   692  		)
   693  	}
   694  	for _, allowRule := range a.Allow {
   695  		if allowRule.Subscription == "" {
   696  			return trace.BadParameter(
   697  				"the %q join method requires azure allow rules with non-empty subscription",
   698  				JoinMethodAzure,
   699  			)
   700  		}
   701  	}
   702  	return nil
   703  }
   704  
   705  const defaultGitLabDomain = "gitlab.com"
   706  
   707  func (a *ProvisionTokenSpecV2GitLab) checkAndSetDefaults() error {
   708  	if len(a.Allow) == 0 {
   709  		return trace.BadParameter(
   710  			"the %q join method requires defined gitlab allow rules",
   711  			JoinMethodGitLab,
   712  		)
   713  	}
   714  	for _, allowRule := range a.Allow {
   715  		if allowRule.Sub == "" && allowRule.NamespacePath == "" && allowRule.ProjectPath == "" && allowRule.CIConfigRefURI == "" {
   716  			return trace.BadParameter(
   717  				"the %q join method requires allow rules with at least one of ['sub', 'project_path', 'namespace_path', 'ci_config_ref_uri'] to ensure security.",
   718  				JoinMethodGitLab,
   719  			)
   720  		}
   721  	}
   722  
   723  	if a.Domain == "" {
   724  		a.Domain = defaultGitLabDomain
   725  	} else {
   726  		if strings.Contains(a.Domain, "/") {
   727  			return trace.BadParameter(
   728  				"'spec.gitlab.domain' should not contain the scheme or path",
   729  			)
   730  		}
   731  	}
   732  	return nil
   733  }
   734  
   735  func (a *ProvisionTokenSpecV2GCP) checkAndSetDefaults() error {
   736  	if len(a.Allow) == 0 {
   737  		return trace.BadParameter("the %q join method requires at least one token allow rule", JoinMethodGCP)
   738  	}
   739  	for _, allowRule := range a.Allow {
   740  		if len(allowRule.ProjectIDs) == 0 {
   741  			return trace.BadParameter(
   742  				"the %q join method requires gcp allow rules with at least one project ID",
   743  				JoinMethodGCP,
   744  			)
   745  		}
   746  	}
   747  	return nil
   748  }
   749  
   750  func (a *ProvisionTokenSpecV2Spacelift) checkAndSetDefaults() error {
   751  	if a.Hostname == "" {
   752  		return trace.BadParameter(
   753  			"hostname: should be set to the hostname of the spacelift tenant",
   754  		)
   755  	}
   756  	if strings.Contains(a.Hostname, "/") {
   757  		return trace.BadParameter(
   758  			"hostname: should not contain the scheme or path",
   759  		)
   760  	}
   761  	if len(a.Allow) == 0 {
   762  		return trace.BadParameter("allow: at least one rule must be set")
   763  	}
   764  	for i, allowRule := range a.Allow {
   765  		if allowRule.SpaceID == "" && allowRule.CallerID == "" {
   766  			return trace.BadParameter(
   767  				"allow[%d]: at least one of ['space_id', 'caller_id'] must be set",
   768  				i,
   769  			)
   770  		}
   771  	}
   772  	return nil
   773  }
   774  
   775  func (a *ProvisionTokenSpecV2TPM) validate() error {
   776  	for i, caData := range a.EKCertAllowedCAs {
   777  		p, _ := pem.Decode([]byte(caData))
   778  		if p == nil {
   779  			return trace.BadParameter(
   780  				"ekcert_allowed_cas[%d]: no pem block found",
   781  				i,
   782  			)
   783  		}
   784  		if p.Type != "CERTIFICATE" {
   785  			return trace.BadParameter(
   786  				"ekcert_allowed_cas[%d]: pem block is not 'CERTIFICATE' type",
   787  				i,
   788  			)
   789  		}
   790  		if _, err := x509.ParseCertificate(p.Bytes); err != nil {
   791  			return trace.Wrap(
   792  				err,
   793  				"ekcert_allowed_cas[%d]: parsing certificate",
   794  				i,
   795  			)
   796  
   797  		}
   798  	}
   799  
   800  	if len(a.Allow) == 0 {
   801  		return trace.BadParameter(
   802  			"allow: at least one rule must be set",
   803  		)
   804  	}
   805  	for i, allowRule := range a.Allow {
   806  		if len(allowRule.EKPublicHash) == 0 && len(allowRule.EKCertificateSerial) == 0 {
   807  			return trace.BadParameter(
   808  				"allow[%d]: at least one of ['ek_public_hash', 'ek_certificate_serial'] must be set",
   809  				i,
   810  			)
   811  		}
   812  	}
   813  	return nil
   814  }