github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/aws/identifiers.go (about)

     1  /*
     2  Copyright 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 aws
    18  
    19  import (
    20  	"regexp"
    21  	"slices"
    22  	"strings"
    23  
    24  	"github.com/gravitational/trace"
    25  )
    26  
    27  // IsValidAccountID checks whether the accountID is a valid AWS Account ID
    28  //
    29  // https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-identifiers.html
    30  func IsValidAccountID(accountID string) error {
    31  	if len(accountID) != 12 {
    32  		return trace.BadParameter("must be 12-digit")
    33  	}
    34  	for _, d := range accountID {
    35  		if d < '0' || d > '9' {
    36  			return trace.BadParameter("must be 12-digit")
    37  		}
    38  	}
    39  
    40  	return nil
    41  }
    42  
    43  // IsValidIAMRoleName checks whether the role name is a valid AWS IAM Role identifier.
    44  //
    45  // > Length Constraints: Minimum length of 1. Maximum length of 64.
    46  // > Pattern: [\w+=,.@-]+
    47  // https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateRole.html
    48  func IsValidIAMRoleName(roleName string) error {
    49  	if len(roleName) == 0 || len(roleName) > 64 || !matchRoleName(roleName) {
    50  		return trace.BadParameter("role is invalid")
    51  	}
    52  
    53  	return nil
    54  }
    55  
    56  // IsValidIAMPolicyName checks whether the policy name is a valid AWS IAM Policy
    57  // identifier.
    58  //
    59  // > Length Constraints: Minimum length of 1. Maximum length of 128.
    60  // > Pattern: [\w+=,.@-]+
    61  // https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicy.html
    62  func IsValidIAMPolicyName(policyName string) error {
    63  	// The same regex is used for role and policy names.
    64  	if len(policyName) == 0 || len(policyName) > 128 || !matchRoleName(policyName) {
    65  		return trace.BadParameter("policy name is invalid")
    66  	}
    67  	return nil
    68  }
    69  
    70  // IsValidRegion ensures the region looks to be valid.
    71  // It does not do a full validation, because AWS doesn't provide documentation for that.
    72  // However, they usually only have the following chars: [a-z0-9\-]
    73  func IsValidRegion(region string) error {
    74  	if matchRegion.MatchString(region) {
    75  		return nil
    76  	}
    77  	return trace.BadParameter("region %q is invalid", region)
    78  }
    79  
    80  // IsValidPartition checks if partition is a valid AWS partition
    81  func IsValidPartition(partition string) error {
    82  	if slices.Contains(validPartitions, partition) {
    83  		return nil
    84  	}
    85  	return trace.BadParameter("partition %q is invalid", partition)
    86  }
    87  
    88  // IsValidAthenaWorkgroupName checks whether the name is a valid AWS Athena
    89  // workgroup name.
    90  func IsValidAthenaWorkgroupName(workgroup string) error {
    91  	if matchAthenaWorkgroupName(workgroup) {
    92  		return nil
    93  	}
    94  	return trace.BadParameter("athena workgroup name %q is invalid", workgroup)
    95  }
    96  
    97  // IsValidGlueResourceName check whether the name is valid for an AWS Glue
    98  // database or table used with AWS Athena
    99  func IsValidGlueResourceName(name string) error {
   100  	if matchGlueName(name) {
   101  		return nil
   102  	}
   103  	return trace.BadParameter("glue resource name %q is invalid", name)
   104  }
   105  
   106  const (
   107  	arnDelimiter    = ":"
   108  	arnPrefix       = "arn:"
   109  	arnSections     = 6
   110  	sectionService  = 2 // arn:<partition>:<service>:...
   111  	sectionAccount  = 4 // arn:<partition>:<service>:<region>:<accountid>:...
   112  	sectionResource = 5 // arn:<partition>:<service>:<region>:<accountid>:<resource>
   113  	iamServiceName  = "iam"
   114  )
   115  
   116  // CheckRoleARN returns whether a string is a valid IAM Role ARN.
   117  // Example role ARN: arn:aws:iam::123456789012:role/some-role-name
   118  func CheckRoleARN(arn string) error {
   119  	if !strings.HasPrefix(arn, arnPrefix) {
   120  		return trace.BadParameter("arn: invalid prefix: %q", arn)
   121  	}
   122  
   123  	sections := strings.SplitN(arn, arnDelimiter, arnSections)
   124  	if len(sections) != arnSections {
   125  		return trace.BadParameter("arn: not enough sections: %q", arn)
   126  	}
   127  
   128  	resourceParts := strings.SplitN(sections[sectionResource], "/", 2)
   129  
   130  	if resourceParts[0] != "role" || sections[sectionService] != iamServiceName {
   131  		return trace.BadParameter("%q is not an AWS IAM role ARN", arn)
   132  	}
   133  
   134  	if len(resourceParts) < 2 || resourceParts[1] == "" {
   135  		return trace.BadParameter("%q is missing AWS IAM role name", arn)
   136  	}
   137  
   138  	if err := IsValidAccountID(sections[sectionAccount]); err != nil {
   139  		return trace.BadParameter("%q invalid account ID: %v", arn, err)
   140  	}
   141  
   142  	return nil
   143  }
   144  
   145  var (
   146  	// matchRoleName is a regex that matches against AWS IAM Role Names.
   147  	matchRoleName = regexp.MustCompile(`^[\w+=,.@-]+$`).MatchString
   148  
   149  	// matchRegion is a regex that defines the format of AWS regions.
   150  	//
   151  	// The regex matches the following from left to right:
   152  	// - starts with 2 lower case letters that represents a geo region like a
   153  	//   country code
   154  	// - optional -gov, -iso, -isob for corresponding partitions
   155  	// - a word that should be a direction like "east", "west", etc.
   156  	// - a number counter
   157  	//
   158  	// Reference:
   159  	// https://github.com/aws/aws-sdk-go-v2/blob/main/codegen/smithy-aws-go-codegen/src/main/resources/software/amazon/smithy/aws/go/codegen/endpoints.json
   160  	matchRegion = regexp.MustCompile(`^[a-z]{2}(-gov|-iso|-isob)?-\w+-\d+$`)
   161  
   162  	// https://docs.aws.amazon.com/athena/latest/APIReference/API_CreateWorkGroup.html
   163  	matchAthenaWorkgroupName = regexp.MustCompile(`^[a-zA-Z0-9._-]{1,128}$`).MatchString
   164  
   165  	// https://docs.aws.amazon.com/athena/latest/ug/tables-databases-columns-names.html
   166  	// More strict than strictly necessary, but a good baseline
   167  	// > database, table, and column names must be 255 characters or fewer
   168  	// > Athena accepts mixed case in DDL and DML queries, but lower cases the names when it executes the query
   169  	// > avoid using mixed case for table or column names
   170  	// > special characters other than underscore (_) are not supported
   171  	matchGlueName = regexp.MustCompile(`^[a-z0-9_]{1,255}$`).MatchString
   172  
   173  	// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html
   174  	validPartitions = []string{"aws", "aws-cn", "aws-us-gov"}
   175  )