github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/resource_ids.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 types
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"slices"
    23  	"strings"
    24  
    25  	"github.com/gravitational/trace"
    26  )
    27  
    28  func (id *ResourceID) CheckAndSetDefaults() error {
    29  	if len(id.ClusterName) == 0 {
    30  		return trace.BadParameter("ResourceID must include ClusterName")
    31  	}
    32  	if len(id.Kind) == 0 {
    33  		return trace.BadParameter("ResourceID must include Kind")
    34  	}
    35  	if !slices.Contains(RequestableResourceKinds, id.Kind) {
    36  		return trace.BadParameter("Resource kind %q is invalid or unsupported", id.Kind)
    37  	}
    38  	if len(id.Name) == 0 {
    39  		return trace.BadParameter("ResourceID must include Name")
    40  	}
    41  
    42  	switch {
    43  	case slices.Contains(KubernetesResourcesKinds, id.Kind):
    44  		return trace.Wrap(id.validateK8sSubResource())
    45  	case id.SubResourceName != "":
    46  		return trace.BadParameter("resource kind %q doesn't allow sub resources", id.Kind)
    47  	}
    48  	return nil
    49  }
    50  
    51  func (id *ResourceID) validateK8sSubResource() error {
    52  	if id.SubResourceName == "" {
    53  		return trace.BadParameter("resource of kind %q must include a subresource name", id.Kind)
    54  	}
    55  	isResourceNamespaceScoped := slices.Contains(KubernetesClusterWideResourceKinds, id.Kind)
    56  	switch split := strings.Split(id.SubResourceName, "/"); {
    57  	case isResourceNamespaceScoped && len(split) != 1:
    58  		return trace.BadParameter("subresource %q must follow the following format: <name>", id.SubResourceName)
    59  	case isResourceNamespaceScoped && split[0] == "":
    60  		return trace.BadParameter("subresource %q must include a non-empty name: <name>", id.SubResourceName)
    61  	case !isResourceNamespaceScoped && len(split) != 2:
    62  		return trace.BadParameter("subresource %q must follow the following format: <namespace>/<name>", id.SubResourceName)
    63  	case !isResourceNamespaceScoped && split[0] == "":
    64  		return trace.BadParameter("subresource %q must include a non-empty namespace: <namespace>/<name>", id.SubResourceName)
    65  	case !isResourceNamespaceScoped && split[1] == "":
    66  		return trace.BadParameter("subresource %q must include a non-empty name: <namespace>/<name>", id.SubResourceName)
    67  	}
    68  
    69  	return nil
    70  }
    71  
    72  // ResourceIDToString marshals a ResourceID to a string.
    73  func ResourceIDToString(id ResourceID) string {
    74  	if id.SubResourceName == "" {
    75  		return fmt.Sprintf("/%s/%s/%s", id.ClusterName, id.Kind, id.Name)
    76  	}
    77  	return fmt.Sprintf("/%s/%s/%s/%s", id.ClusterName, id.Kind, id.Name, id.SubResourceName)
    78  }
    79  
    80  // ResourceIDFromString parses a ResourceID from a string. The string should
    81  // have been obtained from ResourceIDToString.
    82  func ResourceIDFromString(raw string) (ResourceID, error) {
    83  	if len(raw) < 1 || raw[0] != '/' {
    84  		return ResourceID{}, trace.BadParameter("%s is not a valid ResourceID string", raw)
    85  	}
    86  	raw = raw[1:]
    87  	// Should be safe for any Name as long as the ClusterName and Kind don't
    88  	// contain slashes, which should never happen.
    89  	parts := strings.SplitN(raw, "/", 3)
    90  	if len(parts) != 3 {
    91  		return ResourceID{}, trace.BadParameter("/%s is not a valid ResourceID string", raw)
    92  	}
    93  	resourceID := ResourceID{
    94  		ClusterName: parts[0],
    95  		Kind:        parts[1],
    96  		Name:        parts[2],
    97  	}
    98  	switch {
    99  	case slices.Contains(KubernetesResourcesKinds, resourceID.Kind):
   100  		isResourceNamespaceScoped := slices.Contains(KubernetesClusterWideResourceKinds, resourceID.Kind)
   101  		// Kubernetes forbids slashes "/" in Namespaces and Pod names, so it's safe to
   102  		// explode the resourceID.Name and extract the last two entries as namespace
   103  		// and name.
   104  		// Teleport allows the resource names to contain slashes, so we need to join
   105  		// splits[:len(splits)-2] to reconstruct the resource name that contains slashes.
   106  		// If splits slice does not have the correct size, resourceID.CheckAndSetDefaults()
   107  		// will fail because, for kind=pod, it's mandatory to present a non-empty
   108  		// namespace and name.
   109  		splits := strings.Split(resourceID.Name, "/")
   110  		if !isResourceNamespaceScoped && len(splits) >= 3 {
   111  			resourceID.Name = strings.Join(splits[:len(splits)-2], "/")
   112  			resourceID.SubResourceName = strings.Join(splits[len(splits)-2:], "/")
   113  		} else if isResourceNamespaceScoped && len(splits) >= 2 {
   114  			resourceID.Name = strings.Join(splits[:len(splits)-1], "/")
   115  			resourceID.SubResourceName = strings.Join(splits[len(splits)-1:], "/")
   116  		}
   117  	}
   118  
   119  	return resourceID, trace.Wrap(resourceID.CheckAndSetDefaults())
   120  }
   121  
   122  // ResourceIDsFromStrings parses a list of ResourceIDs from a list of strings.
   123  // Each string should have been obtained from ResourceIDToString.
   124  func ResourceIDsFromStrings(resourceIDStrs []string) ([]ResourceID, error) {
   125  	resourceIDs := make([]ResourceID, len(resourceIDStrs))
   126  	var err error
   127  	for i, resourceIDStr := range resourceIDStrs {
   128  		resourceIDs[i], err = ResourceIDFromString(resourceIDStr)
   129  		if err != nil {
   130  			return nil, trace.Wrap(err)
   131  		}
   132  	}
   133  	return resourceIDs, nil
   134  }
   135  
   136  // ResourceIDsToString marshals a list of ResourceIDs to a string.
   137  func ResourceIDsToString(ids []ResourceID) (string, error) {
   138  	if len(ids) == 0 {
   139  		return "", nil
   140  	}
   141  	// Marshal each ID to a string using the custom helper.
   142  	var idStrings []string
   143  	for _, id := range ids {
   144  		idStrings = append(idStrings, ResourceIDToString(id))
   145  	}
   146  	// Marshal the entire list of strings as JSON (should properly handle any
   147  	// IDs containing commas or quotes).
   148  	bytes, err := json.Marshal(idStrings)
   149  	if err != nil {
   150  		return "", trace.BadParameter("failed to marshal resource IDs to JSON: %v", err)
   151  	}
   152  	return string(bytes), nil
   153  }
   154  
   155  // ResourceIDsFromString parses a list of resource IDs from a single string.
   156  // The string should have been obtained from ResourceIDsToString.
   157  func ResourceIDsFromString(raw string) ([]ResourceID, error) {
   158  	if raw == "" {
   159  		return nil, nil
   160  	}
   161  	// Parse the full list of strings.
   162  	var idStrings []string
   163  	if err := json.Unmarshal([]byte(raw), &idStrings); err != nil {
   164  		return nil, trace.BadParameter("failed to parse resource IDs from JSON: %v", err)
   165  	}
   166  	// Parse each ID using the custom helper.
   167  	resourceIDs := make([]ResourceID, 0, len(idStrings))
   168  	for _, idString := range idStrings {
   169  		id, err := ResourceIDFromString(idString)
   170  		if err != nil {
   171  			return nil, trace.Wrap(err)
   172  		}
   173  		resourceIDs = append(resourceIDs, id)
   174  	}
   175  	return resourceIDs, nil
   176  }