Skip to content

Commit

Permalink
Add a new role.allow.request field called kubernetes_resources (#…
Browse files Browse the repository at this point in the history
…47173)

* Add a new role.allow.request field called kubernetes_resources

* Fix lint: update terraform docs
  • Loading branch information
kimlisa authored Nov 1, 2024
1 parent c7765fa commit a132be0
Show file tree
Hide file tree
Showing 21 changed files with 4,067 additions and 2,136 deletions.
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

0 comments on commit a132be0

Please sign in to comment.