Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ const (
KindCrownJewel = "crown_jewel"
// KindKubernetesCluster is a Kubernetes cluster.
KindKubernetesCluster = "kube_cluster"
// KindKubernetesResource is a Kubernetes resource within a cluster.
KindKubernetesResource = "kube_resource"

// KindKubePod is a Kubernetes Pod resource type.
KindKubePod = "pod"
Expand Down Expand Up @@ -1354,10 +1356,22 @@ const (
var RequestableResourceKinds = []string{
KindNode,
KindKubernetesCluster,
KindKubernetesResource,
KindDatabase,
KindApp,
KindWindowsDesktop,
KindUserGroup,
KindSAMLIdPServiceProvider,
KindIdentityCenterAccount,
KindIdentityCenterAccountAssignment,
KindGitServer,
}

// LegacyRequestableKubeResourceKinds lists all legacy Teleport resource kinds users can request access to.
// Those are the requestable Kubernetes resource kinds that were supported before the introduction of
// custom resource support. We need to keep them to maintain support with older Teleport versions.
// TODO(@creack): DELETE IN v20.0.0.
var LegacyRequestableKubeResourceKinds = []string{
KindKubePod,
KindKubeSecret,
KindKubeConfigmap,
Expand All @@ -1379,12 +1393,18 @@ var RequestableResourceKinds = []string{
KindKubeJob,
KindKubeCertificateSigningRequest,
KindKubeIngress,
KindSAMLIdPServiceProvider,
KindIdentityCenterAccount,
KindIdentityCenterAccountAssignment,
KindGitServer,
}

// Prefix constants to identify kubernetes resources in access requests.
const (
// AccessRequestPrefixKindKube denotes that the resource is a kubernetes one. Used for access requests.
AccessRequestPrefixKindKube = "kube:"
// AccessRequestPrefixKindKubeClusterWide denotes that the kube resource is cluster-wide.
AccessRequestPrefixKindKubeClusterWide = AccessRequestPrefixKindKube + "cw:"
// AccessRequestPrefixKindKubeNamespaced denotes that the kube resource is namespaced.
AccessRequestPrefixKindKubeNamespaced = AccessRequestPrefixKindKube + "ns:"
)

// The list below needs to be kept in sync with `kubernetesResourceKindOptions`
// in `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`. (Keeping
// this comment separate to prevent it from being included in the official
Expand Down Expand Up @@ -1529,6 +1549,7 @@ var KubernetesVerbs = []string{
// KubernetesClusterWideResourceKinds is the list of supported Kubernetes cluster resource kinds
// that are not namespaced.
// Needed to maintain backward compatibility.
// TODO(@creack): Make this a map[string]struct{} to simplify lookups.
var KubernetesClusterWideResourceKinds = []string{
KindKubeNamespace,
KindKubeNode,
Expand Down
51 changes: 31 additions & 20 deletions api/types/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"regexp"
"slices"
"sort"
"strings"
"time"

"github.com/gravitational/trace"
Expand Down Expand Up @@ -547,28 +548,14 @@ func DeduplicateKubeClusters(kubeclusters []KubeCluster) []KubeCluster {

var _ ResourceWithLabels = (*KubernetesResourceV1)(nil)

// NewKubernetesPodV1 creates a new kubernetes resource with kind "pod".
func NewKubernetesPodV1(meta Metadata, spec KubernetesResourceSpecV1) (*KubernetesResourceV1, error) {
pod := &KubernetesResourceV1{
Kind: KindKubePod,
Metadata: meta,
Spec: spec,
}

if err := pod.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return pod, nil
}

// NewKubernetesResourceV1 creates a new kubernetes resource .
func NewKubernetesResourceV1(kind string, meta Metadata, spec KubernetesResourceSpecV1) (*KubernetesResourceV1, error) {
func NewKubernetesResourceV1(kind string, namespaced bool, meta Metadata, spec KubernetesResourceSpecV1) (*KubernetesResourceV1, error) {
resource := &KubernetesResourceV1{
Kind: kind,
Metadata: meta,
Spec: spec,
}
if err := resource.CheckAndSetDefaults(); err != nil {
if err := resource.CheckAndSetDefaults(namespaced); err != nil {
return nil, trace.Wrap(err)
}
return resource, nil
Expand Down Expand Up @@ -631,17 +618,17 @@ func (k *KubernetesResourceV1) SetRevision(rev string) {

// CheckAndSetDefaults validates the Resource and sets any empty fields to
// default values.
func (k *KubernetesResourceV1) CheckAndSetDefaults() error {
func (k *KubernetesResourceV1) CheckAndSetDefaults(namespaced bool) error {
k.setStaticFields()
if !slices.Contains(KubernetesResourcesKinds, k.Kind) {
return trace.BadParameter("invalid kind %q defined; allowed values: %v", k.Kind, KubernetesResourcesKinds)
if !slices.Contains(KubernetesResourcesKinds, k.Kind) && !strings.HasPrefix(k.Kind, AccessRequestPrefixKindKube) {
return trace.BadParameter("invalid kind %q defined; allowed values: %v, %s<kind>", k.Kind, KubernetesResourcesKinds, AccessRequestPrefixKindKube)
}
if err := k.Metadata.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}

// Unless the resource is cluster-wide, it must have a namespace.
if len(k.Spec.Namespace) == 0 && !slices.Contains(KubernetesClusterWideResourceKinds, k.Kind) {
if len(k.Spec.Namespace) == 0 && namespaced {
return trace.BadParameter("missing kubernetes namespace")
}

Expand Down Expand Up @@ -753,3 +740,27 @@ func (k KubeResources) AsResources() ResourcesWithLabels {
}
return resources
}

// KubeResource represents either a KubernetesResource or RequestKubernetesResource.
type KubeResource interface {
GetAPIGroup() string
GetKind() string
GetNamespace() string
SetAPIGroup(string)
SetKind(string)
SetNamespace(string)
}

// Setter/Getter to enable generics.
func (m *RequestKubernetesResource) GetAPIGroup() string { return m.APIGroup }
func (m *KubernetesResource) GetAPIGroup() string { return m.APIGroup }
func (m *RequestKubernetesResource) SetAPIGroup(group string) { m.APIGroup = group }
func (m *KubernetesResource) SetAPIGroup(group string) { m.APIGroup = group }
func (m *RequestKubernetesResource) GetKind() string { return m.Kind }
func (m *KubernetesResource) GetKind() string { return m.Kind }
func (m *RequestKubernetesResource) SetKind(kind string) { m.Kind = kind }
func (m *KubernetesResource) SetKind(kind string) { m.Kind = kind }
func (m *RequestKubernetesResource) GetNamespace() string { return "" }
func (m *KubernetesResource) GetNamespace() string { return m.Namespace }
func (m *RequestKubernetesResource) SetNamespace(ns string) {}
func (m *KubernetesResource) SetNamespace(ns string) { m.Namespace = ns }
43 changes: 29 additions & 14 deletions api/types/resource_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,29 @@ func (id *ResourceID) CheckAndSetDefaults() error {
if len(id.Kind) == 0 {
return trace.BadParameter("ResourceID must include Kind")
}
if !slices.Contains(RequestableResourceKinds, id.Kind) {
return trace.BadParameter("Resource kind %q is invalid or unsupported", id.Kind)
}
if len(id.Name) == 0 {
return trace.BadParameter("ResourceID must include Name")
}

// TODO(@creack): DELETE IN v20.0.0. Here to maintain backwards compatibility with older clients.
if id.Kind != KindKubeNamespace && slices.Contains(KubernetesResourcesKinds, id.Kind) {
apiGroup := KubernetesResourcesV7KindGroups[id.Kind]
if slices.Contains(KubernetesClusterWideResourceKinds, id.Kind) {
id.Kind = AccessRequestPrefixKindKubeClusterWide + KubernetesResourcesKindsPlurals[id.Kind]
} else {
id.Kind = AccessRequestPrefixKindKubeNamespaced + KubernetesResourcesKindsPlurals[id.Kind]
}
if apiGroup != "" {
id.Kind += "." + apiGroup
}
}

if id.Kind != KindKubeNamespace && !slices.Contains(RequestableResourceKinds, id.Kind) && !strings.HasPrefix(id.Kind, AccessRequestPrefixKindKube) {
return trace.BadParameter("Resource kind %q is invalid or unsupported", id.Kind)
}

switch {
case slices.Contains(KubernetesResourcesKinds, id.Kind):
case id.Kind == KindKubeNamespace || strings.HasPrefix(id.Kind, AccessRequestPrefixKindKube):
return trace.Wrap(id.validateK8sSubResource())
case id.SubResourceName != "":
return trace.BadParameter("resource kind %q doesn't allow sub resources", id.Kind)
Expand All @@ -52,17 +66,17 @@ func (id *ResourceID) validateK8sSubResource() error {
if id.SubResourceName == "" {
return trace.BadParameter("resource of kind %q must include a subresource name", id.Kind)
}
isResourceNamespaceScoped := slices.Contains(KubernetesClusterWideResourceKinds, id.Kind)
isResourceClusterwide := id.Kind == KindKubeNamespace || slices.Contains(KubernetesClusterWideResourceKinds, id.Kind) || strings.HasPrefix(id.Kind, AccessRequestPrefixKindKubeClusterWide)
switch split := strings.Split(id.SubResourceName, "/"); {
case isResourceNamespaceScoped && len(split) != 1:
case isResourceClusterwide && len(split) != 1:
return trace.BadParameter("subresource %q must follow the following format: <name>", id.SubResourceName)
case isResourceNamespaceScoped && split[0] == "":
case isResourceClusterwide && split[0] == "":
return trace.BadParameter("subresource %q must include a non-empty name: <name>", id.SubResourceName)
case !isResourceNamespaceScoped && len(split) != 2:
case !isResourceClusterwide && len(split) != 2:
return trace.BadParameter("subresource %q must follow the following format: <namespace>/<name>", id.SubResourceName)
case !isResourceNamespaceScoped && split[0] == "":
case !isResourceClusterwide && split[0] == "":
return trace.BadParameter("subresource %q must include a non-empty namespace: <namespace>/<name>", id.SubResourceName)
case !isResourceNamespaceScoped && split[1] == "":
case !isResourceClusterwide && split[1] == "":
return trace.BadParameter("subresource %q must include a non-empty name: <namespace>/<name>", id.SubResourceName)
}

Expand Down Expand Up @@ -95,9 +109,10 @@ func ResourceIDFromString(raw string) (ResourceID, error) {
Kind: parts[1],
Name: parts[2],
}

switch {
case slices.Contains(KubernetesResourcesKinds, resourceID.Kind):
isResourceNamespaceScoped := slices.Contains(KubernetesClusterWideResourceKinds, resourceID.Kind)
case slices.Contains(KubernetesResourcesKinds, resourceID.Kind) || strings.HasPrefix(resourceID.Kind, AccessRequestPrefixKindKube) || resourceID.Kind == KindKubeNamespace:
isResourceClusterWide := resourceID.Kind == KindKubeNamespace || slices.Contains(KubernetesClusterWideResourceKinds, resourceID.Kind) || strings.HasPrefix(resourceID.Kind, AccessRequestPrefixKindKubeClusterWide)
// Kubernetes forbids slashes "/" in Namespaces and Pod names, so it's safe to
// explode the resourceID.Name and extract the last two entries as namespace
// and name.
Expand All @@ -107,10 +122,10 @@ func ResourceIDFromString(raw string) (ResourceID, error) {
// will fail because, for kind=pod, it's mandatory to present a non-empty
// namespace and name.
splits := strings.Split(resourceID.Name, "/")
if !isResourceNamespaceScoped && len(splits) >= 3 {
if !isResourceClusterWide && len(splits) >= 3 {
resourceID.Name = strings.Join(splits[:len(splits)-2], "/")
resourceID.SubResourceName = strings.Join(splits[len(splits)-2:], "/")
} else if isResourceNamespaceScoped && len(splits) >= 2 {
} else if isResourceClusterWide && len(splits) >= 2 {
resourceID.Name = strings.Join(splits[:len(splits)-1], "/")
resourceID.SubResourceName = strings.Join(splits[len(splits)-1:], "/")
}
Expand Down
Loading
Loading