Skip to content

Commit

Permalink
Check request modes while pruning search as roles
Browse files Browse the repository at this point in the history
  • Loading branch information
kimlisa committed Oct 16, 2024
1 parent b80caae commit 1a055b0
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 123 deletions.
8 changes: 8 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)

// SetRequestMode sets the access request mode.
SetRequestMode(requestMode *AccessRequestMode)

// 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,11 @@ func (r *RoleV6) SetKubeResources(rct RoleConditionType, pods []KubernetesResour
}
}

// SetRequestMode sets the access request mode.
func (r *RoleV6) SetRequestMode(requestMode *AccessRequestMode) {
r.Spec.Options.RequestMode = requestMode
}

// GetKubeUsers returns kubernetes users
func (r *RoleV6) GetKubeUsers(rct RoleConditionType) []string {
if rct == Allow {
Expand Down
176 changes: 109 additions & 67 deletions lib/services/access_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package services
import (
"context"
"log/slog"
"maps"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -59,7 +58,7 @@ const (
// the access request can be reviewed. Defaults to 1 week.
requestTTL = 7 * day

InvalidKubernetesKindAccessRequest = "Not allowed to request Kubernetes resource kind"
InvalidKubernetesKindAccessRequest = `Your Teleport roles "request_mode" option field restricts you from requesting kinds`
)

// ValidateAccessRequest validates the AccessRequest and sets default values
Expand Down Expand Up @@ -273,7 +272,12 @@ func CalculateAccessCapabilities(ctx context.Context, clock clockwork.Clock, clt
caps.RequireReason = v.requireReason
caps.RequestPrompt = v.prompt
caps.AutoRequest = v.autoRequest
caps.RequestMode = &types.AccessRequestMode{KubernetesResources: v.requestMode.kubernetesResources}

protoRequestModeLookup := make(map[string]*types.RequestModeKubernetesResourceValues)
for searchAsRole, requestModes := range v.kubeRequestModeLookup {
protoRequestModeLookup[searchAsRole] = &types.RequestModeKubernetesResourceValues{Values: requestModes}
}
caps.KubeRequestModeLookup = protoRequestModeLookup

return &caps, nil
}
Expand Down Expand Up @@ -1031,12 +1035,15 @@ type RequestValidator struct {
getter RequestValidatorGetter
userState UserState
requireReason bool
requestMode struct {
kubernetesResources []types.RequestModeKubernetesResource
}
autoRequest bool
prompt string
opts struct {
// kubeRequestModeLookup is a map of search_as_role to a list
// of collected request modes found from each static role.
// Used to enforce that the request mode found in the static
// role that defined the search_as_role, is respected.
// An empty map or list means no request modes were specified.
kubeRequestModeLookup map[string][]types.RequestModeKubernetesResource
autoRequest bool
prompt string
opts struct {
expandVars bool
}
Roles struct {
Expand Down Expand Up @@ -1071,10 +1078,11 @@ func NewRequestValidator(ctx context.Context, clock clockwork.Clock, getter Requ
}

m := RequestValidator{
clock: clock,
getter: getter,
userState: uls,
logger: slog.With(teleport.ComponentKey, "request.validator"),
clock: clock,
getter: getter,
userState: uls,
logger: slog.With(teleport.ComponentKey, "request.validator"),
kubeRequestModeLookup: make(map[string][]types.RequestModeKubernetesResource),
}
for _, opt := range opts {
opt(&m)
Expand Down Expand Up @@ -1264,49 +1272,6 @@ func (m *RequestValidator) Validate(ctx context.Context, req types.AccessRequest
return trace.Wrap(err)
}
}

// Validate kube request kinds.
// If request mode is defined, then any request for kube_cluster will be rejected.
isResourceRequest := len(req.GetRequestedResourceIDs()) > 0
restrictKubeRequestKinds := len(m.requestMode.kubernetesResources) > 0
if isResourceRequest && restrictKubeRequestKinds {
if err := enforceKubernetesRequestModes(req.GetRequestedResourceIDs(), m.requestMode.kubernetesResources); err != nil {
return trace.Wrap(err)
}
}
}

return nil
}

func enforceKubernetesRequestModes(requestedResourceIDs []types.ResourceID, requestModeKubeResources []types.RequestModeKubernetesResource) error {
allowedKindsLookup := make(map[string]string, len(types.KubernetesResourcesKinds))
isWildcard := false

for _, kr := range requestModeKubeResources {
if kr.Kind == types.Wildcard {
isWildcard = true
break
}
allowedKindsLookup[kr.Kind] = kr.Kind
}

if isWildcard {
for _, kind := range types.KubernetesResourcesKinds {
allowedKindsLookup[kind] = kind
}
}

for _, id := range requestedResourceIDs {
if id.Kind == types.KindKubernetesCluster {
return trace.BadParameter("%s %q. Allowed kinds: %v.", InvalidKubernetesKindAccessRequest, types.KindKubernetesCluster, slices.Collect(maps.Keys(allowedKindsLookup)))
}
// Filter for kube resources.
if slices.Contains(types.KubernetesResourcesKinds, id.Kind) {
if _, found := allowedKindsLookup[id.Kind]; !found {
return trace.BadParameter("%s %q. Allowed kinds: %v.", InvalidKubernetesKindAccessRequest, id.Kind, slices.Collect(maps.Keys(allowedKindsLookup)))
}
}
}

return nil
Expand Down Expand Up @@ -1546,12 +1511,21 @@ func (m *RequestValidator) push(ctx context.Context, role types.Role) error {
m.prompt = role.GetOptions().RequestPrompt
}

if role.GetOptions().RequestMode != nil {
m.requestMode.kubernetesResources = append(m.requestMode.kubernetesResources, role.GetOptions().RequestMode.KubernetesResources...)
}

allow, deny := role.GetAccessRequestConditions(types.Allow), role.GetAccessRequestConditions(types.Deny)

// Collect all the request modes for the search as roles found for this role.
if len(allow.SearchAsRoles) > 0 && role.GetOptions().RequestMode != nil {
for _, allowedSearchAsRole := range allow.SearchAsRoles {
kubeRequestModes := role.GetOptions().RequestMode.KubernetesResources
// If for some reason, the same search_as_role name got defined in another static role,
// merge the request modes.
if _, exists := m.kubeRequestModeLookup[allowedSearchAsRole]; exists {
kubeRequestModes = append(kubeRequestModes, m.kubeRequestModeLookup[allowedSearchAsRole]...)
}
m.kubeRequestModeLookup[allowedSearchAsRole] = kubeRequestModes
}
}

m.Roles.DenyRequest, err = appendRoleMatchers(m.Roles.DenyRequest, deny.Roles, deny.ClaimsToRoles, m.userState.GetTraits())
if err != nil {
return trace.Wrap(err)
Expand Down Expand Up @@ -1995,10 +1969,13 @@ func (m *RequestValidator) pruneResourceRequestRoles(
necessaryRoles := make(map[string]struct{})
for _, resource := range resources {
var (
rolesForResource []types.Role
resourceMatcher *KubeResourcesMatcher
rolesForResource []types.Role
resourceMatcher *KubeResourcesMatcher
rejectedKubeResourceKinds []string
allowedKubeResourceKinds []string
canRequestKubeCluster bool
)
kubernetesResources, err := getKubeResourcesFromResourceIDs(resourceIDs, resource.GetName())
kubernetesResources, hasKubeClusterKindRequest, err := getKubeResourcesFromResourceIDs(resourceIDs, resource.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -2015,8 +1992,37 @@ func (m *RequestValidator) pruneResourceRequestRoles(
// unless it allows access to another resource.
continue
}

// Check kube subresource request mode restrictions for the current role.
if requestModes, exists := m.kubeRequestModeLookup[role.GetName()]; exists && len(requestModes) > 0 {
rejectedKinds, allowedKinds, allValidResources := kubeResourcesMeetsRequestModes(requestModes, kubernetesResources)
rejectedKubeResourceKinds = append(rejectedKubeResourceKinds, rejectedKinds...)
allowedKubeResourceKinds = append(allowedKubeResourceKinds, allowedKinds...)
if !allValidResources {
// Pruning this role because some kube resources did not meet
// the request_mode settings.
continue
}
} else {
// If this role was not a part of the lookup map, it meant
// no request mode options were found, which means this role
// allows "any kind".
if hasKubeClusterKindRequest {
canRequestKubeCluster = true
}
}

rolesForResource = append(rolesForResource, role)
}

if len(rolesForResource) == 0 && len(rejectedKubeResourceKinds) > 0 {
return nil, trace.BadParameter("%s %v for Kubernetes cluster %q. Allowed kinds: %v", InvalidKubernetesKindAccessRequest, apiutils.Deduplicate(rejectedKubeResourceKinds), resource.GetName(), apiutils.Deduplicate(allowedKubeResourceKinds))
}

if hasKubeClusterKindRequest && !canRequestKubeCluster {
return nil, trace.BadParameter("%s %v for Kubernetes cluster %q. Allowed kinds: %v", InvalidKubernetesKindAccessRequest, []string{types.KindKubernetesCluster}, resource.GetName(), apiutils.Deduplicate(allowedKubeResourceKinds))
}

// If any of the requested resources didn't match with the provided roles,
// we deny the request because the user is trying to request more access
// than what is allowed by its search_as_roles.
Expand Down Expand Up @@ -2086,6 +2092,36 @@ func countAllowedLogins(role types.Role) int {
return len(allowed)
}

// kubeResourcesMeetsRequestModes goes through each request modes, and return true if all the
// kube resources matched with the request modes. It returns false along with what kube resource
// kinds got rejected.
//
// Wildcard among the requestModes will be interpreted as "allow any kube resource kind" and
// will return early and takes precedence over other modes in the list.
func kubeResourcesMeetsRequestModes(requestModes []types.RequestModeKubernetesResource, kubernetesResources []types.KubernetesResource) ([]string, []string, bool) {
allowedRequestKinds := make([]string, 0, len(requestModes))
for _, requestMode := range requestModes {
if requestMode.Kind == types.Wildcard {
return nil, nil, true
}
allowedRequestKinds = append(allowedRequestKinds, requestMode.Kind)
}

// Collect all the rejected kinds to display to user if on error.
rejectedKubeResourceKinds := make([]string, 0, len(kubernetesResources))
for _, kubeResource := range kubernetesResources {
if !slices.Contains(allowedRequestKinds, kubeResource.Kind) {
rejectedKubeResourceKinds = append(rejectedKubeResourceKinds, kubeResource.Kind)
}
}

if len(rejectedKubeResourceKinds) > 0 {
return apiutils.Deduplicate(rejectedKubeResourceKinds), apiutils.Deduplicate(allowedRequestKinds), false
}

return nil, nil, true
}

func (m *RequestValidator) roleAllowsResource(
ctx context.Context,
role types.Role,
Expand Down Expand Up @@ -2146,11 +2182,17 @@ func (m *RequestValidator) getUnderlyingResourcesByResourceIDs(ctx context.Conte
}

// getKubeResourcesFromResourceIDs returns the Kubernetes Resources requested for
// the configured cluster.
func getKubeResourcesFromResourceIDs(resourceIDs []types.ResourceID, clusterName string) ([]types.KubernetesResource, error) {
// the configured cluster. Also does a check if among the resourceIDs, there
// exists a Kubernetes cluster request and is returned as a boolean.
func getKubeResourcesFromResourceIDs(resourceIDs []types.ResourceID, clusterName string) ([]types.KubernetesResource, bool, error) {
kubernetesResources := make([]types.KubernetesResource, 0, len(resourceIDs))
hasKubeClusterKindRequest := false

for _, resourceID := range resourceIDs {
if resourceID.Kind == types.KindKubernetesCluster && resourceID.Name == clusterName {
hasKubeClusterKindRequest = true
continue
}
if slices.Contains(types.KubernetesResourcesKinds, resourceID.Kind) && resourceID.Name == clusterName {
switch {
case slices.Contains(types.KubernetesClusterWideResourceKinds, resourceID.Kind):
Expand All @@ -2161,7 +2203,7 @@ func getKubeResourcesFromResourceIDs(resourceIDs []types.ResourceID, clusterName
default:
splits := strings.Split(resourceID.SubResourceName, "/")
if len(splits) != 2 {
return nil, trace.BadParameter("subresource name %q does not follow <namespace>/<name> format", resourceID.SubResourceName)
return nil, false, trace.BadParameter("subresource name %q does not follow <namespace>/<name> format", resourceID.SubResourceName)
}
kubernetesResources = append(kubernetesResources, types.KubernetesResource{
Kind: resourceID.Kind,
Expand All @@ -2171,7 +2213,7 @@ func getKubeResourcesFromResourceIDs(resourceIDs []types.ResourceID, clusterName
}
}
}
return kubernetesResources, nil
return kubernetesResources, hasKubeClusterKindRequest, nil
}

func newReviewPermissionParser() (*typical.Parser[reviewPermissionContext, bool], error) {
Expand Down
Loading

0 comments on commit 1a055b0

Please sign in to comment.