Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new role.allow.request field called kubernetes_resources #47173

Merged
merged 2 commits into from
Nov 1, 2024
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
17 changes: 17 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2677,6 +2677,14 @@ message AccessCapabilitiesRequest {
bool FilterRequestableRolesByResource = 6 [(gogoproto.jsontag) = "filter_requestable_roles_by_resource,omitempty"];
}

// RequestKubernetesResource is the Kubernetes resource identifier used
// in access request settings.
// Modeled after existing message KubernetesResource.
message RequestKubernetesResource {
// kind specifies the Kubernetes Resource type.
string kind = 1 [(gogoproto.jsontag) = "kind,omitempty"];
}

// ResourceID is a unique identifier for a teleport resource.
message ResourceID {
// ClusterName is the name of the cluster the resource is in.
Expand Down Expand Up @@ -3418,6 +3426,15 @@ message AccessRequestConditions {
(gogoproto.jsontag) = "max_duration,omitempty",
(gogoproto.casttype) = "Duration"
];

// kubernetes_resources can optionally enforce a requester to request only certain kinds of kube resources.
// Eg: Users can make request to either a resource kind "kube_cluster" or any of its
// subresources like "namespaces". This field can be defined such that it prevents a user
// from requesting "kube_cluster" and enforce requesting any of its subresources.
repeated RequestKubernetesResource kubernetes_resources = 8 [
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "kubernetes_resources,omitempty"
];
}

// AccessReviewConditions is a matcher for allow/deny restrictions on
Expand Down
52 changes: 52 additions & 0 deletions api/types/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ type Role interface {
// SetKubeResources configures the Kubernetes Resources for the RoleConditionType.
SetKubeResources(rct RoleConditionType, pods []KubernetesResource)

// SetRequestKubernetesResources sets the request kubernetes resources.
SetRequestKubernetesResources(rct RoleConditionType, resources []RequestKubernetesResource)

// GetAccessRequestConditions gets allow/deny conditions for access requests.
GetAccessRequestConditions(RoleConditionType) AccessRequestConditions
// SetAccessRequestConditions sets allow/deny conditions for access requests.
Expand Down Expand Up @@ -492,6 +495,18 @@ func (r *RoleV6) SetKubeResources(rct RoleConditionType, pods []KubernetesResour
}
}

// SetRequestKubernetesResources sets the request kubernetes resources.
func (r *RoleV6) SetRequestKubernetesResources(rct RoleConditionType, resources []RequestKubernetesResource) {
roleConditions := &r.Spec.Allow
if rct == Deny {
roleConditions = &r.Spec.Deny
}
if roleConditions.Request == nil {
roleConditions.Request = &AccessRequestConditions{}
}
roleConditions.Request.KubernetesResources = resources
}

// GetKubeUsers returns kubernetes users
func (r *RoleV6) GetKubeUsers(rct RoleConditionType) []string {
if rct == Allow {
Expand Down Expand Up @@ -1135,6 +1150,18 @@ func (r *RoleV6) CheckAndSetDefaults() error {
r.Spec.Deny.Namespaces = []string{defaults.Namespace}
}

// Validate request.kubernetes_resources fields are all valid.
if r.Spec.Allow.Request != nil {
if err := validateRequestKubeResources(r.Version, r.Spec.Allow.Request.KubernetesResources); err != nil {
return trace.Wrap(err)
}
}
if r.Spec.Deny.Request != nil {
if err := validateRequestKubeResources(r.Version, r.Spec.Deny.Request.KubernetesResources); err != nil {
return trace.Wrap(err)
}
}

// Validate that enhanced recording options are all valid.
for _, opt := range r.Spec.Options.BPF {
if opt == constants.EnhancedRecordingCommand ||
Expand Down Expand Up @@ -1791,6 +1818,31 @@ func validateKubeResources(roleVersion string, kubeResources []KubernetesResourc
return nil
}

// validateRequestKubeResources validates each kubeResources entry for `allow.request.kubernetes_resources` field.
// Currently the only supported field for this particular field is:
// - Kind (belonging to KubernetesResourcesKinds)
//
// Mimics types.KubernetesResource data model, but opted to create own type as we don't support other fields yet.
func validateRequestKubeResources(roleVersion string, kubeResources []RequestKubernetesResource) error {
for _, kubeResource := range kubeResources {
if !slices.Contains(KubernetesResourcesKinds, kubeResource.Kind) && kubeResource.Kind != Wildcard {
return trace.BadParameter("request.kubernetes_resource kind %q is invalid or unsupported; Supported: %v", kubeResource.Kind, append([]string{Wildcard}, KubernetesResourcesKinds...))
}

// Only Pod resources are supported in role version <=V6.
// This is mandatory because we must append the other resources to the
// kubernetes resources.
switch roleVersion {
// Teleport does not support role versions < v3.
case V6, V5, V4, V3:
if kubeResource.Kind != KindKubePod {
return trace.BadParameter("request.kubernetes_resources kind %q is not supported in role version %q. Upgrade the role version to %q", kubeResource.Kind, roleVersion, V7)
}
}
}
return nil
}

// ClusterResource returns the resource name in the following format
// <namespace>/<name>.
func (k *KubernetesResource) ClusterResource() string {
Expand Down
206 changes: 206 additions & 0 deletions api/types/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,212 @@ func TestRole_GetKubeResources(t *testing.T) {
}
}

func TestRole_AllowRequestKubernetesResource(t *testing.T) {
type args struct {
version string
resources []RequestKubernetesResource
}
tests := []struct {
name string
args args
want []RequestKubernetesResource
assertErrorCreation require.ErrorAssertionFunc
}{
{
name: "valid single value",
args: args{
version: V7,
resources: []RequestKubernetesResource{
{
Kind: KindKubePod,
},
},
},
assertErrorCreation: require.NoError,
want: []RequestKubernetesResource{
{
Kind: KindKubePod,
},
},
},
{
name: "valid no values",
args: args{
version: V7,
},
assertErrorCreation: require.NoError,
},
{
name: "valid wildcard value",
args: args{
version: V7,
resources: []RequestKubernetesResource{
{
Kind: Wildcard,
},
},
},
assertErrorCreation: require.NoError,
want: []RequestKubernetesResource{
{
Kind: Wildcard,
},
},
},
{
name: "valid multi values",
args: args{
version: V7,
resources: []RequestKubernetesResource{
{
Kind: KindKubeNamespace,
},
{
Kind: KindKubePod,
},
{
Kind: KindKubeSecret,
},
},
},
assertErrorCreation: require.NoError,
want: []RequestKubernetesResource{
{
Kind: KindKubeNamespace,
},
{
Kind: KindKubePod,
},
{
Kind: KindKubeSecret,
},
},
},
{
name: "valid multi values with wildcard",
args: args{
version: V7,
resources: []RequestKubernetesResource{
{
Kind: KindKubeNamespace,
},
{
Kind: Wildcard,
},
},
},
assertErrorCreation: require.NoError,
want: []RequestKubernetesResource{
{
Kind: KindKubeNamespace,
},
{
Kind: Wildcard,
},
},
},
{
name: "invalid kind (kube_cluster is not part of Kubernetes subresources)",
args: args{
version: V7,
resources: []RequestKubernetesResource{
{
Kind: KindKubernetesCluster,
},
},
},
assertErrorCreation: require.Error,
},
{
name: "invalid multi value",
args: args{
version: V7,
resources: []RequestKubernetesResource{
{
Kind: Wildcard,
},
{
Kind: KindKubeNamespace,
},
{
Kind: KindKubernetesCluster,
},
},
},
assertErrorCreation: require.Error,
},
{
name: "invalid kinds not supported for v6",
args: args{
version: V6,
resources: []RequestKubernetesResource{
{
Kind: Wildcard,
},
},
},
assertErrorCreation: require.Error,
},
{
name: "invalid kinds not supported for v5",
args: args{
version: V6,
resources: []RequestKubernetesResource{
{
Kind: Wildcard,
},
},
},
assertErrorCreation: require.Error,
},
{
name: "invalid kinds not supported for v4",
args: args{
version: V6,
resources: []RequestKubernetesResource{
{
Kind: Wildcard,
},
},
},
assertErrorCreation: require.Error,
},
{
name: "invalid kinds not supported for v3",
args: args{
version: V6,
resources: []RequestKubernetesResource{
{
Kind: Wildcard,
},
},
},
assertErrorCreation: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, err := NewRoleWithVersion(
"test",
tt.args.version,
RoleSpecV6{
Allow: RoleConditions{
Request: &AccessRequestConditions{
KubernetesResources: tt.args.resources,
},
},
},
)
tt.assertErrorCreation(t, err)
if err != nil {
return
}
got := r.GetRoleConditions(Allow).Request.KubernetesResources
require.Equal(t, tt.want, got)
})
}
}

func appendV7KubeResources() []KubernetesResource {
resources := []KubernetesResource{}
// append other kubernetes resources
Expand Down
Loading
Loading