Skip to content

Commit

Permalink
Prune search as roles not meeting request mode when querying kube res…
Browse files Browse the repository at this point in the history
…ources
  • Loading branch information
kimlisa committed Oct 16, 2024
1 parent 042d282 commit d27f5f6
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 7 deletions.
2 changes: 1 addition & 1 deletion lib/kube/grpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ 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()...)
extraRoles = append(extraRoles, userContext.Checker.GetAllowedSearchAsRolesMeetingKubeRequestModes(req.ResourceType)...)
}
if req.UsePreviewAsRoles {
extraRoles = append(extraRoles, userContext.Checker.GetAllowedPreviewAsRoles()...)
Expand Down
96 changes: 91 additions & 5 deletions lib/kube/grpc/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,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"
usernameWithRequestModePod = "request_mode_pod_user"
usernameWithRequestModeSecret = "request_mode_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 @@ -95,6 +97,48 @@ func TestListKubernetesResources(t *testing.T) {
},
)

userWithRequestModePod, _ := testCtx.CreateUserAndRole(
testCtx.Context,
t,
usernameWithRequestModePod,
kubeproxy.RoleSpec{
Name: usernameWithRequestModePod,
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.SetRequestMode(&types.AccessRequestMode{
KubernetesResources: []types.RequestModeKubernetesResource{{Kind: "namespace"}, {Kind: "pod"}},
})
},
},
)

userWithRequestModeSecret, _ := testCtx.CreateUserAndRole(
testCtx.Context,
t,
usernameWithRequestModeSecret,
kubeproxy.RoleSpec{
Name: usernameWithRequestModeSecret,
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.SetRequestMode(&types.AccessRequestMode{
KubernetesResources: []types.RequestModeKubernetesResource{{Kind: "secret"}},
})
},
},
)

userNoAccess, _ := testCtx.CreateUserAndRole(
testCtx.Context,
t,
Expand Down Expand Up @@ -292,6 +336,48 @@ func TestListKubernetesResources(t *testing.T) {
},
assertErr: require.NoError,
},
{
name: "user with no access, listing dev namespace using search as roles with request mode secret, request type pod",
args: args{
user: userWithRequestModeSecret,
searchAsRoles: true,
namespace: "dev",
resourceKind: types.KindKubePod,
},
assertErr: require.Error,
},
{
name: "user with no access listing dev namespace using search as roles with request mode pod, request type pod",
args: args{
user: userWithRequestModePod,
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 no access listing dev namespace using search as roles and sort",
args: args{
Expand Down
4 changes: 4 additions & 0 deletions lib/services/access_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ type AccessChecker interface {
// GetAllowedSearchAsRoles returns all of the allowed SearchAsRoles.
GetAllowedSearchAsRoles() []string

// GetAllowedSearchAsRolesMeetingKubeRequestModes returns all of the allowed SearchAsRoles that
// also passes the test where requestType matches the requestMode found for allowed role.
GetAllowedSearchAsRolesMeetingKubeRequestModes(requestType string) []string

// GetAllowedPreviewAsRoles returns all of the allowed PreviewAsRoles.
GetAllowedPreviewAsRoles() []string

Expand Down
46 changes: 46 additions & 0 deletions lib/services/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -3293,6 +3293,52 @@ func (set RoleSet) GetAllowedSearchAsRoles() []string {
return apiutils.Deduplicate(allowed)
}

// GetAllowedSearchAsRolesMeetingKubeRequestModes returns all of the allowed SearchAsRoles that
// also passes the test where requestType matches the requestMode found for allowed role.
func (set RoleSet) GetAllowedSearchAsRolesMeetingKubeRequestModes(requestType string) []string {
denied := make(map[string]struct{})
for _, role := range set {
for _, d := range role.GetSearchAsRoles(types.Deny) {
denied[d] = struct{}{}
}
}

searchAsRolesLookup := make(map[string][]types.RequestModeKubernetesResource)

for _, role := range set {
hasRequestMode := role.GetOptions().RequestMode != nil && len(role.GetOptions().RequestMode.KubernetesResources) > 0

for _, allowedRole := range role.GetSearchAsRoles(types.Allow) {
if _, denied := denied[allowedRole]; !denied {
if hasRequestMode {
kubeRequestModes := role.GetOptions().RequestMode.KubernetesResources
if _, exists := searchAsRolesLookup[allowedRole]; exists {
kubeRequestModes = append(kubeRequestModes, searchAsRolesLookup[allowedRole]...)
}
searchAsRolesLookup[allowedRole] = kubeRequestModes
} else {
searchAsRolesLookup[allowedRole] = nil
}
}
}
}

var allowedRoleNames []string
for allowedRole, kubeResources := range searchAsRolesLookup {
if len(kubeResources) == 0 {
allowedRoleNames = append(allowedRoleNames, allowedRole)
continue
}
for _, kubeResource := range kubeResources {
if kubeResource.Kind == requestType || kubeResource.Kind == types.Wildcard {
allowedRoleNames = append(allowedRoleNames, allowedRole)
}
}
}

return apiutils.Deduplicate(allowedRoleNames)
}

// GetAllowedPreviewAsRoles returns all PreviewAsRoles for this RoleSet.
func (set RoleSet) GetAllowedPreviewAsRoles() []string {
denied := make(map[string]struct{})
Expand Down
142 changes: 141 additions & 1 deletion lib/services/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,40 @@ func TestRoleParse(t *testing.T) {
error: trace.BadParameter(""),
matchMessage: "KubernetesResource must include Namespace",
},
{
name: "validation error, invalid request mode kube resource kind",
in: `{
"kind": "role",
"version": "v6",
"metadata": {"name": "name1"},
"spec": {
"options": {
"request_mode": {
"kubernetes_resources": [{"kind":"abcd"}]
}
}
}
}`,
error: trace.BadParameter(""),
matchMessage: "invalid or unsupported",
},
{
name: "validation error, request mode namespace not supported in v6",
in: `{
"kind": "role",
"version": "v6",
"metadata": {"name": "name1"},
"spec": {
"options": {
"request_mode": {
"kubernetes_resources": [{"kind":"namespace"}]
}
}
}
}`,
error: trace.BadParameter(""),
matchMessage: "not supported in role version \"v6\"",
},
{
name: "validation error, missing podname in pod names",
in: `{
Expand Down Expand Up @@ -331,7 +365,10 @@ func TestRoleParse(t *testing.T) {
"enhanced_recording": ["command", "network"],
"desktop_clipboard": true,
"desktop_directory_sharing": true,
"ssh_file_copy" : false
"ssh_file_copy" : false,
"request_mode": {
"kubernetes_resources": [{"kind":"pod"}]
}
},
"allow": {
"node_labels": {"a": "b", "c-d": "e"},
Expand Down Expand Up @@ -368,6 +405,11 @@ func TestRoleParse(t *testing.T) {
},
Spec: types.RoleSpecV6{
Options: types.RoleOptions{
RequestMode: &types.AccessRequestMode{
KubernetesResources: []types.RequestModeKubernetesResource{
{Kind: types.KindKubePod},
},
},
CertificateFormat: constants.CertificateFormatStandard,
MaxSessionTTL: types.NewDuration(20 * time.Hour),
PortForwarding: types.NewBoolOption(true),
Expand Down Expand Up @@ -4686,6 +4728,104 @@ func TestGetAllowedLoginsForResource(t *testing.T) {
}
}

func TestGetAllowedSearchAsRolesMeetingKubeRequestModes(t *testing.T) {
newRole := func(
allowRoles []string,
denyRoles []string,
requestModes []types.RequestModeKubernetesResource,
) *types.RoleV6 {
return &types.RoleV6{
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
SearchAsRoles: allowRoles,
},
},
Deny: types.RoleConditions{
Request: &types.AccessRequestConditions{
SearchAsRoles: denyRoles,
},
},
Options: types.RoleOptions{
RequestMode: &types.AccessRequestMode{
KubernetesResources: requestModes,
},
},
},
}
}

withoutRequestModes := newRole([]string{"role1", "role2"}, []string{"role3"}, []types.RequestModeKubernetesResource{})
withRequestModesNamespaceAndPod := newRole([]string{"role2", "role3", "role10"}, []string{"role3"}, []types.RequestModeKubernetesResource{
{Kind: types.KindNamespace},
{Kind: types.KindKubePod},
})
withRequestModeWildcard := newRole([]string{"role4", "role5"}, []string{"role3"}, []types.RequestModeKubernetesResource{
{Kind: types.KindNamespace},
{Kind: types.KindKubePod},
{Kind: types.Wildcard},
})
withRequestModeSecret := newRole([]string{"role5", "role6"}, []string{"role3"}, []types.RequestModeKubernetesResource{
{Kind: types.KindKubeSecret},
})

tt := []struct {
name string
labels map[string]string
roleSet RoleSet
requestType string
expectedAllowedRoles []string
}{
{
name: "return all allowed roles that doesn't have request mode defined since type doesn't match",
roleSet: NewRoleSet(withRequestModeSecret, withoutRequestModes),
requestType: types.KindNamespace,
// only roles from "withoutRequestModes"
expectedAllowedRoles: []string{"role1", "role2"},
},
{
name: "return all allowed roles with wildcard",
roleSet: NewRoleSet(withRequestModeSecret, withRequestModeWildcard),
requestType: types.KindKubeNamespace,
// only roles from "withRequestModeWildcard"
expectedAllowedRoles: []string{"role4", "role5"},
},
{
name: "return all allowed roles with matching type and wildcard",
roleSet: NewRoleSet(withRequestModeSecret, withRequestModeWildcard),
requestType: types.KindKubeSecret,
// roles from both "withRequestModeWildcard" & "withRequestModeSecret"
expectedAllowedRoles: []string{"role4", "role5", "role6"},
},
{
name: "return empty if there were no matching types",
roleSet: NewRoleSet(withRequestModeSecret, withRequestModesNamespaceAndPod),
requestType: types.KindKubeDeployment,
},
{
name: "return all allowed roles only matching types",
roleSet: NewRoleSet(withRequestModeSecret, withRequestModesNamespaceAndPod),
requestType: types.KindKubePod,
// roles from "withRequestModesNamespaceAndPod"
expectedAllowedRoles: []string{"role2", "role10"},
},
{
name: "return all allowed roles with multiple rolesets",
roleSet: NewRoleSet(withoutRequestModes, withRequestModesNamespaceAndPod, withRequestModeWildcard, withRequestModeSecret),
requestType: types.KindKubeSecret,
expectedAllowedRoles: []string{"role1", "role4", "role5", "role6"},
},
}
for _, tc := range tt {
accessChecker := makeAccessCheckerWithRoleSet(tc.roleSet)
t.Run(tc.name, func(t *testing.T) {

allowedRoles := accessChecker.GetAllowedSearchAsRolesMeetingKubeRequestModes(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 d27f5f6

Please sign in to comment.