Skip to content

Commit

Permalink
Apply request.kubernetes_resources allow/deny settings when querying …
Browse files Browse the repository at this point in the history
…for kube resources (#48196)

* Apply request.kubernetes_resources allow/deny when querying for kube resources

* Address CR
  • Loading branch information
kimlisa committed Nov 5, 2024
1 parent d687fe8 commit 2e1fc23
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 10 deletions.
3 changes: 2 additions & 1 deletion lib/kube/grpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ func (s *Server) ListKubernetesResources(ctx context.Context, req *proto.ListKub
if req.UseSearchAsRoles || req.UsePreviewAsRoles {
var extraRoles []string
if req.UseSearchAsRoles {
extraRoles = append(extraRoles, userContext.Checker.GetAllowedSearchAsRoles()...)
allowedSearchAsRoles := userContext.Checker.GetAllowedSearchAsRolesForKubeResourceKind(req.ResourceType)
extraRoles = append(extraRoles, allowedSearchAsRoles...)
}
if req.UsePreviewAsRoles {
extraRoles = append(extraRoles, userContext.Checker.GetAllowedPreviewAsRoles()...)
Expand Down
127 changes: 122 additions & 5 deletions lib/kube/grpc/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ import (
func TestListKubernetesResources(t *testing.T) {
modules.SetInsecureTestMode(true)
var (
usernameWithFullAccess = "full_user"
usernameNoAccess = "limited_user"
kubeCluster = "test_cluster"
kubeUsers = []string{"kube_user"}
kubeGroups = []string{"kube_user"}
usernameWithFullAccess = "full_user"
usernameNoAccess = "limited_user"
usernameWithEnforceKubePodOrNamespace = "request_kind_enforce_pod_user"
usernameWithEnforceKubeSecret = "request_kind_enforce_secret_user"
kubeCluster = "test_cluster"
kubeUsers = []string{"kube_user"}
kubeGroups = []string{"kube_user"}
)
// kubeMock is a Kubernetes API mock for the session tests.
// Once a new session is created, this mock will write to
Expand Down Expand Up @@ -94,6 +96,45 @@ func TestListKubernetesResources(t *testing.T) {
},
)

userWithEnforceKubePodOrNamespace, _ := testCtx.CreateUserAndRole(
testCtx.Context,
t,
usernameWithEnforceKubePodOrNamespace,
kubeproxy.RoleSpec{
Name: usernameWithEnforceKubePodOrNamespace,
KubeUsers: kubeUsers,
KubeGroups: kubeGroups,
SetupRoleFunc: func(role types.Role) {
// override the role to deny access to all kube resources.
role.SetKubernetesLabels(types.Allow, nil)
// set the role to allow searching as fullAccessRole.
role.SetSearchAsRoles(types.Allow, []string{fullAccessRole.GetName()})
// restrict querying to pods only
role.SetRequestKubernetesResources(types.Allow, []types.RequestKubernetesResource{{Kind: "namespace"}, {Kind: "pod"}})
},
},
)

userWithEnforceKubeSecret, _ := testCtx.CreateUserAndRole(
testCtx.Context,
t,
usernameWithEnforceKubeSecret,
kubeproxy.RoleSpec{
Name: usernameWithEnforceKubeSecret,
KubeUsers: kubeUsers,
KubeGroups: kubeGroups,
SetupRoleFunc: func(role types.Role) {
// override the role to deny access to all kube resources.
role.SetKubernetesLabels(types.Allow, nil)
// set the role to allow searching as fullAccessRole.
role.SetSearchAsRoles(types.Allow, []string{fullAccessRole.GetName()})
// restrict querying to secrets only
role.SetRequestKubernetesResources(types.Allow, []types.RequestKubernetesResource{{Kind: "secret"}})

},
},
)

userNoAccess, _ := testCtx.CreateUserAndRole(
testCtx.Context,
t,
Expand Down Expand Up @@ -357,6 +398,82 @@ func TestListKubernetesResources(t *testing.T) {
},
assertErr: require.NoError,
},
{
name: "user with no access, deny listing dev pod, with role that enforces secret",
args: args{
user: userWithEnforceKubeSecret,
searchAsRoles: true,
namespace: "dev",
resourceKind: types.KindKubePod,
},
assertErr: require.Error,
},
{
name: "user with no access, allow listing dev secret, with role that enforces secret",
args: args{
user: userWithEnforceKubeSecret,
searchAsRoles: true,
namespace: "dev",
resourceKind: types.KindKubeSecret,
},
want: &proto.ListKubernetesResourcesResponse{
Resources: []*types.KubernetesResourceV1{
{
Kind: types.KindKubeSecret,
Version: "v1",
Metadata: types.Metadata{
Name: "secret-1",
},
Spec: types.KubernetesResourceSpecV1{
Namespace: "dev",
},
},
{
Kind: types.KindKubeSecret,
Version: "v1",
Metadata: types.Metadata{
Name: "secret-2",
},
Spec: types.KubernetesResourceSpecV1{
Namespace: "dev",
},
},
},
},
assertErr: require.NoError,
},
{
name: "user with no access, allow listing dev pod, with role that enforces namespace or pods",
args: args{
user: userWithEnforceKubePodOrNamespace,
searchAsRoles: true,
namespace: "dev",
resourceKind: types.KindKubePod,
},
want: &proto.ListKubernetesResourcesResponse{
Resources: []*types.KubernetesResourceV1{
{
Kind: "pod",
Metadata: types.Metadata{
Name: "nginx-1",
},
Spec: types.KubernetesResourceSpecV1{
Namespace: "dev",
},
},
{
Kind: "pod",
Metadata: types.Metadata{
Name: "nginx-2",
},
Spec: types.KubernetesResourceSpecV1{
Namespace: "dev",
},
},
},
},
assertErr: require.NoError,
},
{
name: "user with full access and listing secrets in all namespaces",
args: args{
Expand Down
6 changes: 5 additions & 1 deletion lib/services/access_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ type AccessChecker interface {
CertificateExtensions() []*types.CertExtension

// GetAllowedSearchAsRoles returns all of the allowed SearchAsRoles.
GetAllowedSearchAsRoles() []string
GetAllowedSearchAsRoles(allowFilters ...SearchAsRolesOption) []string

// GetAllowedSearchAsRolesForKubeResourceKind returns all of the allowed SearchAsRoles
// that allowed requesting to the requested Kubernetes resource kind.
GetAllowedSearchAsRolesForKubeResourceKind(requestedKubeResourceKind string) []string

// GetAllowedPreviewAsRoles returns all of the allowed PreviewAsRoles.
GetAllowedPreviewAsRoles() []string
Expand Down
49 changes: 46 additions & 3 deletions lib/services/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -3234,8 +3234,11 @@ func (set RoleSet) ExtractConditionForIdentifier(ctx RuleContext, namespace, res
return &types.WhereExpr{And: types.WhereExpr2{L: denyCond, R: allowCond}}, nil
}

// SearchAsRolesOption is a functional option for filtering SearchAsRoles.
type SearchAsRolesOption func(role types.Role) bool

// GetSearchAsRoles returns all SearchAsRoles for this RoleSet.
func (set RoleSet) GetAllowedSearchAsRoles() []string {
func (set RoleSet) GetAllowedSearchAsRoles(allowFilters ...SearchAsRolesOption) []string {
denied := make(map[string]struct{})
var allowed []string
for _, role := range set {
Expand All @@ -3244,15 +3247,55 @@ func (set RoleSet) GetAllowedSearchAsRoles() []string {
}
}
for _, role := range set {
if slices.ContainsFunc(allowFilters, func(filter SearchAsRolesOption) bool {
return !filter(role)
}) {
// Don't consider this base role if it's filtered out.
continue
}
for _, a := range role.GetSearchAsRoles(types.Allow) {
if _, ok := denied[a]; !ok {
allowed = append(allowed, a)
if _, isDenied := denied[a]; isDenied {
continue
}
allowed = append(allowed, a)
}
}
return apiutils.Deduplicate(allowed)
}

// GetAllowedSearchAsRolesForKubeResourceKind returns all of the allowed SearchAsRoles
// that allowed requesting to the requested Kubernetes resource kind.
func (set RoleSet) GetAllowedSearchAsRolesForKubeResourceKind(requestedKubeResourceKind string) []string {
// Return no results if encountering any denies since its globally matched.
for _, role := range set {
for _, kr := range role.GetAccessRequestConditions(types.Deny).KubernetesResources {
if kr.Kind == types.Wildcard || kr.Kind == requestedKubeResourceKind {
return nil
}
}
}
return set.GetAllowedSearchAsRoles(WithAllowedKubernetesResourceKindFilter(requestedKubeResourceKind))
}

// WithAllowedKubernetesResourceKindFilter returns a SearchAsRolesOption func
// that will check that the requestedKubeResourceKind exists in the allow list
// for the current role.
func WithAllowedKubernetesResourceKindFilter(requestedKubeResourceKind string) SearchAsRolesOption {
return func(role types.Role) bool {
allowed := role.GetAccessRequestConditions(types.Allow).KubernetesResources
// any kind is allowed if nothing was configured.
if len(allowed) == 0 {
return true
}
for _, kr := range role.GetAccessRequestConditions(types.Allow).KubernetesResources {
if kr.Kind == types.Wildcard || kr.Kind == requestedKubeResourceKind {
return true
}
}
return false
}
}

// GetAllowedPreviewAsRoles returns all PreviewAsRoles for this RoleSet.
func (set RoleSet) GetAllowedPreviewAsRoles() []string {
denied := make(map[string]struct{})
Expand Down
85 changes: 85 additions & 0 deletions lib/services/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4541,6 +4541,91 @@ func TestGetAllowedLoginsForResource(t *testing.T) {
}
}

func TestGetAllowedSearchAsRoles_WithAllowedKubernetesResourceKindFilter(t *testing.T) {
newRole := func(
allowRoles []string,
denyRoles []string,
allowedResources []types.RequestKubernetesResource,
deniedResources []types.RequestKubernetesResource,
) *types.RoleV6 {
return &types.RoleV6{
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
SearchAsRoles: allowRoles,
KubernetesResources: allowedResources,
},
},
Deny: types.RoleConditions{
Request: &types.AccessRequestConditions{
SearchAsRoles: denyRoles,
KubernetesResources: deniedResources,
},
},
},
}
}

roleWithNamespace := newRole([]string{"sar1"}, nil, []types.RequestKubernetesResource{{Kind: types.KindNamespace}}, []types.RequestKubernetesResource{})
roleWithSecret := newRole([]string{"sar2"}, nil, []types.RequestKubernetesResource{{Kind: types.KindKubeSecret}}, []types.RequestKubernetesResource{})
roleWithNoConfigure := newRole([]string{"sar3"}, nil, nil, nil)
roleWithDenyRole := newRole([]string{"sar4", "sar5", "sar6", "sar7"}, []string{"sar4", "sar6"}, []types.RequestKubernetesResource{{Kind: types.KindNamespace}, {Kind: types.KindKubePod}}, []types.RequestKubernetesResource{{Kind: types.KindKubePod}})
roleWithDenyWildcard := newRole([]string{"sar10"}, nil, []types.RequestKubernetesResource{{Kind: types.KindNamespace}}, []types.RequestKubernetesResource{{Kind: types.Wildcard}})
roleWithAllowWildcard := newRole([]string{"sar4", "sar5"}, nil, []types.RequestKubernetesResource{{Kind: types.Wildcard}}, nil)

tt := []struct {
name string
roleSet RoleSet
requestType string
expectedAllowedRoles []string
}{
{
name: "single match",
roleSet: NewRoleSet(roleWithNamespace, roleWithSecret),
requestType: types.KindKubeSecret,
expectedAllowedRoles: []string{"sar2"},
},
{
name: "multi match",
roleSet: NewRoleSet(roleWithNamespace, roleWithNoConfigure),
requestType: types.KindNamespace,
expectedAllowedRoles: []string{"sar1", "sar3"},
},
{
name: "wildcard allow",
roleSet: NewRoleSet(roleWithAllowWildcard, roleWithNamespace),
requestType: types.KindNamespace,
expectedAllowedRoles: []string{"sar1", "sar4", "sar5"},
},
{
name: "wildcard deny",
roleSet: NewRoleSet(roleWithAllowWildcard, roleWithDenyWildcard),
requestType: types.KindNamespace,
expectedAllowedRoles: []string{},
},
{
name: "wildcard deny with unconfigured allow",
roleSet: NewRoleSet(roleWithNoConfigure, roleWithDenyWildcard),
requestType: types.KindNamespace,
expectedAllowedRoles: []string{},
},
{
name: "with deny role",
roleSet: NewRoleSet(roleWithDenyRole, roleWithNamespace),
requestType: types.KindNamespace,
expectedAllowedRoles: []string{"sar5", "sar7", "sar1"},
},
}
for _, tc := range tt {
accessChecker := makeAccessCheckerWithRoleSet(tc.roleSet)
t.Run(tc.name, func(t *testing.T) {

allowedRoles := accessChecker.GetAllowedSearchAsRolesForKubeResourceKind(tc.requestType)
require.ElementsMatch(t, tc.expectedAllowedRoles, allowedRoles)
})
}
}

// mustMakeTestServer creates a server with labels and an empty spec.
// It panics in case of an error. Used only for testing
func mustMakeTestServer(labels map[string]string) types.Server {
Expand Down

0 comments on commit 2e1fc23

Please sign in to comment.