2023-04-03 21:16:02 +00:00
|
|
|
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
2021-06-09 23:00:54 +00:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package integration
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
authorizationv1 "k8s.io/api/authorization/v1"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
|
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
|
|
v1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
|
|
|
"k8s.io/client-go/rest"
|
|
|
|
|
2021-06-22 15:23:19 +00:00
|
|
|
"go.pinniped.dev/test/testlib"
|
2021-06-09 23:00:54 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestServiceAccountPermissions(t *testing.T) {
|
|
|
|
// TODO: update this test to check the permissions of all service accounts
|
|
|
|
// For now it just checks the permissions of the impersonation proxy SA
|
|
|
|
|
2021-06-22 15:23:19 +00:00
|
|
|
env := testlib.IntegrationEnv(t)
|
2021-06-09 23:00:54 +00:00
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// impersonate the SA since it is easier than fetching a token and lets us control the group memberships
|
2021-06-22 15:23:19 +00:00
|
|
|
config := rest.CopyConfig(testlib.NewClientConfig(t))
|
2021-06-09 23:00:54 +00:00
|
|
|
config.Impersonate = rest.ImpersonationConfig{
|
|
|
|
UserName: serviceaccount.MakeUsername(env.ConciergeNamespace, env.ConciergeAppName+"-impersonation-proxy"),
|
|
|
|
// avoid permissions assigned to system:serviceaccounts by explicitly impersonating system:serviceaccounts:<namespace>
|
|
|
|
// as not all clusters will have the system:service-account-issuer-discovery binding
|
|
|
|
// system:authenticated is required for us to create selfsubjectrulesreviews
|
|
|
|
// TODO remove this once we stop supporting Kube clusters before v1.19
|
|
|
|
Groups: []string{serviceaccount.MakeNamespaceGroupName(env.ConciergeNamespace), user.AllAuthenticated},
|
|
|
|
}
|
|
|
|
|
2021-06-22 15:23:19 +00:00
|
|
|
ssrrClient := testlib.NewKubeclient(t, config).Kubernetes.AuthorizationV1().SelfSubjectRulesReviews()
|
2021-06-09 23:00:54 +00:00
|
|
|
|
|
|
|
// the impersonation proxy SA has the same permissions for all checks because it should only be authorized via cluster role bindings
|
|
|
|
|
|
|
|
expectedResourceRules := []authorizationv1.ResourceRule{
|
|
|
|
// the expected impersonation permissions
|
|
|
|
{Verbs: []string{"impersonate"}, APIGroups: []string{""}, Resources: []string{"users", "groups", "serviceaccounts"}},
|
|
|
|
{Verbs: []string{"impersonate"}, APIGroups: []string{"authentication.k8s.io"}, Resources: []string{"*"}},
|
|
|
|
|
|
|
|
// we bind these to system:authenticated
|
|
|
|
{Verbs: []string{"create", "list"}, APIGroups: []string{"login.concierge." + env.APIGroupSuffix}, Resources: []string{"tokencredentialrequests"}},
|
|
|
|
{Verbs: []string{"create", "list"}, APIGroups: []string{"identity.concierge." + env.APIGroupSuffix}, Resources: []string{"whoamirequests"}},
|
|
|
|
}
|
|
|
|
|
2023-04-03 21:16:02 +00:00
|
|
|
// system:basic-user is bound to system:authenticated by default, so the SA gets these permissions too.
|
|
|
|
// See https://kubernetes.io/docs/reference/access-authn-authz/rbac/#discovery-roles.
|
|
|
|
// Note that this list previously only included "selfsubjectaccessreviews" and "selfsubjectrulesreviews",
|
|
|
|
// but later was updated in Kubernetes to also include "selfsubjectreviews".
|
|
|
|
// Rather than explicitly listing them all as expectations, dynamically append them here, so this test
|
|
|
|
// can pass against all versions of Kubernetes.
|
|
|
|
basicUserClusterRole, err := testlib.NewKubernetesClientset(t).RbacV1().ClusterRoles().Get(ctx, "system:basic-user", metav1.GetOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
for _, policyRule := range basicUserClusterRole.Rules {
|
|
|
|
expectedResourceRules = append(expectedResourceRules, authorizationv1.ResourceRule{
|
|
|
|
Verbs: policyRule.Verbs,
|
|
|
|
APIGroups: policyRule.APIGroups,
|
|
|
|
Resources: policyRule.Resources,
|
|
|
|
ResourceNames: policyRule.ResourceNames,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-06-09 23:00:54 +00:00
|
|
|
if otherPinnipedGroupSuffix := getOtherPinnipedGroupSuffix(t); len(otherPinnipedGroupSuffix) > 0 {
|
|
|
|
expectedResourceRules = append(expectedResourceRules,
|
|
|
|
// we bind these to system:authenticated in the other instance of pinniped
|
|
|
|
authorizationv1.ResourceRule{Verbs: []string{"create", "list"}, APIGroups: []string{"login.concierge." + otherPinnipedGroupSuffix}, Resources: []string{"tokencredentialrequests"}},
|
|
|
|
authorizationv1.ResourceRule{Verbs: []string{"create", "list"}, APIGroups: []string{"identity.concierge." + otherPinnipedGroupSuffix}, Resources: []string{"whoamirequests"}},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-06-22 15:23:19 +00:00
|
|
|
crbs, err := testlib.NewKubernetesClientset(t).RbacV1().ClusterRoleBindings().List(ctx, metav1.ListOptions{LabelSelector: "eks.amazonaws.com/component=pod-security-policy"})
|
2021-06-15 15:17:59 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
if len(crbs.Items) > 0 {
|
|
|
|
expectedResourceRules = append(expectedResourceRules,
|
|
|
|
// EKS binds these to system:authenticated
|
|
|
|
authorizationv1.ResourceRule{Verbs: []string{"use"}, APIGroups: []string{"policy"}, Resources: []string{"podsecuritypolicies"}, ResourceNames: []string{"eks.privileged"}},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-06-09 23:00:54 +00:00
|
|
|
expectedNonResourceRules := []authorizationv1.NonResourceRule{
|
|
|
|
// system:public-info-viewer is bound to system:authenticated and system:unauthenticated by default
|
|
|
|
{Verbs: []string{"get"}, NonResourceURLs: []string{"/healthz", "/livez", "/readyz", "/version", "/version/"}},
|
|
|
|
|
|
|
|
// system:discovery is bound to system:authenticated by default
|
|
|
|
{Verbs: []string{"get"}, NonResourceURLs: []string{"/api", "/api/*", "/apis", "/apis/*",
|
|
|
|
"/healthz", "/livez", "/openapi", "/openapi/*", "/readyz", "/version", "/version/",
|
|
|
|
}},
|
|
|
|
}
|
|
|
|
|
|
|
|
// check permissions in concierge namespace
|
|
|
|
testPermissionsInNamespace(ctx, t, ssrrClient, env.ConciergeNamespace, expectedResourceRules, expectedNonResourceRules)
|
|
|
|
|
|
|
|
// check permissions in supervisor namespace
|
|
|
|
testPermissionsInNamespace(ctx, t, ssrrClient, env.SupervisorNamespace, expectedResourceRules, expectedNonResourceRules)
|
|
|
|
|
|
|
|
// check permissions in kube-system namespace
|
|
|
|
testPermissionsInNamespace(ctx, t, ssrrClient, metav1.NamespaceSystem, expectedResourceRules, expectedNonResourceRules)
|
|
|
|
|
|
|
|
// check permissions in kube-public namespace
|
|
|
|
testPermissionsInNamespace(ctx, t, ssrrClient, metav1.NamespacePublic, expectedResourceRules, expectedNonResourceRules)
|
|
|
|
|
|
|
|
// check permissions in default namespace
|
|
|
|
testPermissionsInNamespace(ctx, t, ssrrClient, metav1.NamespaceDefault, expectedResourceRules, expectedNonResourceRules)
|
|
|
|
|
|
|
|
// we fake a cluster scoped selfsubjectrulesreviews check by picking a nonsense namespace
|
|
|
|
testPermissionsInNamespace(ctx, t, ssrrClient, "some-namespace-invalid-name||pandas-are-the-best", expectedResourceRules, expectedNonResourceRules)
|
|
|
|
}
|
|
|
|
|
|
|
|
func testPermissionsInNamespace(ctx context.Context, t *testing.T, ssrrClient v1.SelfSubjectRulesReviewInterface, namespace string,
|
|
|
|
expectedResourceRules []authorizationv1.ResourceRule, expectedNonResourceRules []authorizationv1.NonResourceRule) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
ssrr, err := ssrrClient.Create(ctx, &authorizationv1.SelfSubjectRulesReview{
|
|
|
|
Spec: authorizationv1.SelfSubjectRulesReviewSpec{Namespace: namespace},
|
|
|
|
}, metav1.CreateOptions{})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
assert.ElementsMatch(t, expectedResourceRules, ssrr.Status.ResourceRules)
|
|
|
|
assert.ElementsMatch(t, expectedNonResourceRules, ssrr.Status.NonResourceRules)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getOtherPinnipedGroupSuffix(t *testing.T) string {
|
|
|
|
t.Helper()
|
|
|
|
|
2021-06-22 15:23:19 +00:00
|
|
|
env := testlib.IntegrationEnv(t)
|
2021-06-09 23:00:54 +00:00
|
|
|
|
|
|
|
var resources []*metav1.APIResourceList
|
|
|
|
|
2021-06-22 15:23:19 +00:00
|
|
|
testlib.RequireEventuallyWithoutError(t, func() (bool, error) {
|
2021-06-09 23:00:54 +00:00
|
|
|
// we need a complete discovery listing for the check we are trying to make below
|
|
|
|
// loop since tests like TestAPIServingCertificateAutoCreationAndRotation can break discovery
|
2021-06-22 15:23:19 +00:00
|
|
|
_, r, err := testlib.NewKubernetesClientset(t).Discovery().ServerGroupsAndResources()
|
2021-06-09 23:00:54 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Logf("retrying due to partial discovery failure: %v", err)
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
resources = r
|
|
|
|
return true, nil
|
|
|
|
}, 3*time.Minute, time.Second)
|
|
|
|
|
|
|
|
var otherPinnipedGroupSuffix string
|
|
|
|
|
|
|
|
for _, resource := range resources {
|
|
|
|
gv, err := schema.ParseGroupVersion(resource.GroupVersion)
|
|
|
|
require.NoError(t, err)
|
|
|
|
for _, apiResource := range resource.APIResources {
|
|
|
|
if apiResource.Name == "tokencredentialrequests" && gv.Group != "login.concierge."+env.APIGroupSuffix {
|
|
|
|
require.Empty(t, otherPinnipedGroupSuffix, "only expected at most one other instance of pinniped")
|
|
|
|
otherPinnipedGroupSuffix = strings.TrimPrefix(gv.Group, "login.concierge.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return otherPinnipedGroupSuffix
|
|
|
|
}
|