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 }