mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 04:06:03 +00:00
Merge pull request #80234 from tallclair/kubelet-authz
Cleanup kubelet authz tests & make explicit
This commit is contained in:
commit
cff86e7861
@ -82,7 +82,6 @@ go_test(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/httpstream:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/httpstream:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/httpstream/spdy:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/httpstream/spdy:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||||
|
@ -16,7 +16,15 @@ limitations under the License.
|
|||||||
|
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
)
|
||||||
|
|
||||||
func TestIsSubPath(t *testing.T) {
|
func TestIsSubPath(t *testing.T) {
|
||||||
testcases := map[string]struct {
|
testcases := map[string]struct {
|
||||||
@ -51,3 +59,96 @@ func TestIsSubPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetRequestAttributes(t *testing.T) {
|
||||||
|
for _, test := range AuthzTestCases() {
|
||||||
|
t.Run(test.Method+":"+test.Path, func(t *testing.T) {
|
||||||
|
getter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(test.Method, "https://localhost:1234"+test.Path, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
attrs := getter.GetRequestAttributes(AuthzTestUser(), req)
|
||||||
|
|
||||||
|
test.AssertAttributes(t, attrs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
authzTestNodeName = "test"
|
||||||
|
authzTestUserName = "phibby"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthzTestCase struct {
|
||||||
|
Method, Path string
|
||||||
|
|
||||||
|
ExpectedVerb, ExpectedSubresource string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthzTestCase) AssertAttributes(t *testing.T, attrs authorizer.Attributes) {
|
||||||
|
expectedAttributes := authorizer.AttributesRecord{
|
||||||
|
User: AuthzTestUser(),
|
||||||
|
APIGroup: "",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Verb: a.ExpectedVerb,
|
||||||
|
Resource: "nodes",
|
||||||
|
Name: authzTestNodeName,
|
||||||
|
Subresource: a.ExpectedSubresource,
|
||||||
|
ResourceRequest: true,
|
||||||
|
Path: a.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expectedAttributes, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthzTestUser() user.Info {
|
||||||
|
return &user.DefaultInfo{Name: authzTestUserName}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthzTestCases() []AuthzTestCase {
|
||||||
|
// Path -> ExpectedSubresource
|
||||||
|
testPaths := map[string]string{
|
||||||
|
"/attach/{podNamespace}/{podID}/{containerName}": "proxy",
|
||||||
|
"/attach/{podNamespace}/{podID}/{uid}/{containerName}": "proxy",
|
||||||
|
"/configz": "proxy",
|
||||||
|
"/containerLogs/{podNamespace}/{podID}/{containerName}": "proxy",
|
||||||
|
"/cri/": "proxy",
|
||||||
|
"/cri/foo": "proxy",
|
||||||
|
"/debug/flags/v": "proxy",
|
||||||
|
"/debug/pprof/{subpath:*}": "proxy",
|
||||||
|
"/exec/{podNamespace}/{podID}/{containerName}": "proxy",
|
||||||
|
"/exec/{podNamespace}/{podID}/{uid}/{containerName}": "proxy",
|
||||||
|
"/healthz": "proxy",
|
||||||
|
"/healthz/log": "proxy",
|
||||||
|
"/healthz/ping": "proxy",
|
||||||
|
"/healthz/syncloop": "proxy",
|
||||||
|
"/logs/": "log",
|
||||||
|
"/logs/{logpath:*}": "log",
|
||||||
|
"/metrics": "metrics",
|
||||||
|
"/metrics/cadvisor": "metrics",
|
||||||
|
"/metrics/probes": "metrics",
|
||||||
|
"/metrics/resource/v1alpha1": "metrics",
|
||||||
|
"/pods/": "proxy",
|
||||||
|
"/portForward/{podNamespace}/{podID}": "proxy",
|
||||||
|
"/portForward/{podNamespace}/{podID}/{uid}": "proxy",
|
||||||
|
"/run/{podNamespace}/{podID}/{containerName}": "proxy",
|
||||||
|
"/run/{podNamespace}/{podID}/{uid}/{containerName}": "proxy",
|
||||||
|
"/runningpods/": "proxy",
|
||||||
|
"/spec/": "spec",
|
||||||
|
"/stats/": "stats",
|
||||||
|
"/stats/container": "stats",
|
||||||
|
"/stats/summary": "stats",
|
||||||
|
"/stats/{namespace}/{podName}/{uid}/{containerName}": "stats",
|
||||||
|
"/stats/{podName}/{containerName}": "stats",
|
||||||
|
}
|
||||||
|
testCases := []AuthzTestCase{}
|
||||||
|
for path, subresource := range testPaths {
|
||||||
|
testCases = append(testCases,
|
||||||
|
AuthzTestCase{"POST", path, "create", subresource},
|
||||||
|
AuthzTestCase{"GET", path, "get", subresource},
|
||||||
|
AuthzTestCase{"PUT", path, "update", subresource},
|
||||||
|
AuthzTestCase{"PATCH", path, "patch", subresource},
|
||||||
|
AuthzTestCase{"DELETE", path, "delete", subresource})
|
||||||
|
}
|
||||||
|
return testCases
|
||||||
|
}
|
||||||
|
@ -43,7 +43,6 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||||
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
|
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
@ -682,144 +681,90 @@ type authTestCase struct {
|
|||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthFilters(t *testing.T) {
|
// Ensure all registered handlers & services have an associated testcase.
|
||||||
|
func TestAuthzCoverage(t *testing.T) {
|
||||||
fw := newServerTest()
|
fw := newServerTest()
|
||||||
defer fw.testHTTPServer.Close()
|
defer fw.testHTTPServer.Close()
|
||||||
|
|
||||||
testcases := []authTestCase{}
|
// method:path -> has coverage
|
||||||
|
expectedCases := map[string]bool{}
|
||||||
// This is a sanity check that the Handle->HandleWithFilter() delegation is working
|
|
||||||
// Ideally, these would move to registered web services and this list would get shorter
|
|
||||||
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
|
|
||||||
paths := sets.NewString(fw.serverUnderTest.restfulCont.RegisteredHandlePaths()...)
|
|
||||||
for _, expectedPath := range expectedPaths {
|
|
||||||
if !paths.Has(expectedPath) {
|
|
||||||
t.Errorf("Expected registered handle path %s was missing", expectedPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test all the non-web-service handlers
|
// Test all the non-web-service handlers
|
||||||
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
|
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
|
||||||
testcases = append(testcases, authTestCase{"GET", path})
|
expectedCases["GET:"+path] = false
|
||||||
testcases = append(testcases, authTestCase{"POST", path})
|
expectedCases["POST:"+path] = false
|
||||||
// Test subpaths for directory handlers
|
|
||||||
if strings.HasSuffix(path, "/") {
|
|
||||||
testcases = append(testcases, authTestCase{"GET", path + "foo"})
|
|
||||||
testcases = append(testcases, authTestCase{"POST", path + "foo"})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test all the generated web-service paths
|
// Test all the generated web-service paths
|
||||||
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
|
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
|
||||||
for _, r := range ws.Routes() {
|
for _, r := range ws.Routes() {
|
||||||
testcases = append(testcases, authTestCase{r.Method, r.Path})
|
expectedCases[r.Method+":"+r.Path] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
methodToAPIVerb := map[string]string{"GET": "get", "POST": "create", "PUT": "update"}
|
// This is a sanity check that the Handle->HandleWithFilter() delegation is working
|
||||||
pathToSubresource := func(path string) string {
|
// Ideally, these would move to registered web services and this list would get shorter
|
||||||
switch {
|
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
|
||||||
// Cases for subpaths we expect specific subresources for
|
for _, expectedPath := range expectedPaths {
|
||||||
case isSubpath(path, statsPath):
|
if _, expected := expectedCases["GET:"+expectedPath]; !expected {
|
||||||
return "stats"
|
t.Errorf("Expected registered handle path %s was missing", expectedPath)
|
||||||
case isSubpath(path, specPath):
|
|
||||||
return "spec"
|
|
||||||
case isSubpath(path, logsPath):
|
|
||||||
return "log"
|
|
||||||
case isSubpath(path, metricsPath):
|
|
||||||
return "metrics"
|
|
||||||
|
|
||||||
// Cases for subpaths we expect to map to the "proxy" subresource
|
|
||||||
case isSubpath(path, "/attach"),
|
|
||||||
isSubpath(path, "/configz"),
|
|
||||||
isSubpath(path, "/containerLogs"),
|
|
||||||
isSubpath(path, "/debug"),
|
|
||||||
isSubpath(path, "/exec"),
|
|
||||||
isSubpath(path, "/healthz"),
|
|
||||||
isSubpath(path, "/pods"),
|
|
||||||
isSubpath(path, "/portForward"),
|
|
||||||
isSubpath(path, "/run"),
|
|
||||||
isSubpath(path, "/runningpods"),
|
|
||||||
isSubpath(path, "/cri"):
|
|
||||||
return "proxy"
|
|
||||||
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf(`unexpected kubelet API path %s.
|
|
||||||
The kubelet API has likely registered a handler for a new path.
|
|
||||||
If the new path has a use case for partitioned authorization when requested from the kubelet API,
|
|
||||||
add a specific subresource for it in auth.go#GetRequestAttributes() and in TestAuthFilters().
|
|
||||||
Otherwise, add it to the expected list of paths that map to the "proxy" subresource in TestAuthFilters()`, path))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attributesGetter := NewNodeAuthorizerAttributesGetter(types.NodeName("test"))
|
|
||||||
|
|
||||||
for _, tc := range testcases {
|
for _, tc := range AuthzTestCases() {
|
||||||
var (
|
expectedCases[tc.Method+":"+tc.Path] = true
|
||||||
expectedUser = &user.DefaultInfo{Name: "test"}
|
}
|
||||||
expectedAttributes = authorizer.AttributesRecord{
|
|
||||||
User: expectedUser,
|
for tc, found := range expectedCases {
|
||||||
APIGroup: "",
|
if !found {
|
||||||
APIVersion: "v1",
|
t.Errorf("Missing authz test case for %s", tc)
|
||||||
Verb: methodToAPIVerb[tc.Method],
|
}
|
||||||
Resource: "nodes",
|
}
|
||||||
Name: "test",
|
}
|
||||||
Subresource: pathToSubresource(tc.Path),
|
|
||||||
ResourceRequest: true,
|
func TestAuthFilters(t *testing.T) {
|
||||||
Path: tc.Path,
|
fw := newServerTest()
|
||||||
|
defer fw.testHTTPServer.Close()
|
||||||
|
|
||||||
|
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
|
||||||
|
|
||||||
|
for _, tc := range AuthzTestCases() {
|
||||||
|
t.Run(tc.Method+":"+tc.Path, func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
expectedUser = AuthzTestUser()
|
||||||
|
|
||||||
|
calledAuthenticate = false
|
||||||
|
calledAuthorize = false
|
||||||
|
calledAttributes = false
|
||||||
|
)
|
||||||
|
|
||||||
|
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
|
||||||
|
calledAuthenticate = true
|
||||||
|
return &authenticator.Response{User: expectedUser}, true, nil
|
||||||
|
}
|
||||||
|
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) authorizer.Attributes {
|
||||||
|
calledAttributes = true
|
||||||
|
require.Equal(t, expectedUser, u)
|
||||||
|
return attributesGetter.GetRequestAttributes(u, req)
|
||||||
|
}
|
||||||
|
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
||||||
|
calledAuthorize = true
|
||||||
|
tc.AssertAttributes(t, a)
|
||||||
|
return authorizer.DecisionNoOpinion, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
calledAuthenticate = false
|
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
|
||||||
calledAuthorize = false
|
require.NoError(t, err)
|
||||||
calledAttributes = false
|
|
||||||
)
|
|
||||||
|
|
||||||
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
|
resp, err := http.DefaultClient.Do(req)
|
||||||
calledAuthenticate = true
|
require.NoError(t, err)
|
||||||
return &authenticator.Response{User: expectedUser}, true, nil
|
defer resp.Body.Close()
|
||||||
}
|
|
||||||
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) authorizer.Attributes {
|
|
||||||
calledAttributes = true
|
|
||||||
if u != expectedUser {
|
|
||||||
t.Fatalf("%s: expected user %v, got %v", tc.Path, expectedUser, u)
|
|
||||||
}
|
|
||||||
return attributesGetter.GetRequestAttributes(u, req)
|
|
||||||
}
|
|
||||||
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
|
||||||
calledAuthorize = true
|
|
||||||
if a != expectedAttributes {
|
|
||||||
t.Fatalf("%s: expected attributes\n\t%#v\ngot\n\t%#v", tc.Path, expectedAttributes, a)
|
|
||||||
}
|
|
||||||
return authorizer.DecisionNoOpinion, "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||||
if err != nil {
|
assert.True(t, calledAuthenticate, "Authenticate was not called")
|
||||||
t.Errorf("%s: unexpected error: %v", tc.Path, err)
|
assert.True(t, calledAttributes, "Attributes were not called")
|
||||||
continue
|
assert.True(t, calledAuthorize, "Authorize was not called")
|
||||||
}
|
})
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: unexpected error: %v", tc.Path, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusForbidden {
|
|
||||||
t.Errorf("%s: unexpected status code %d", tc.Path, resp.StatusCode)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !calledAuthenticate {
|
|
||||||
t.Errorf("%s: Authenticate was not called", tc.Path)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !calledAttributes {
|
|
||||||
t.Errorf("%s: Attributes were not called", tc.Path)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !calledAuthorize {
|
|
||||||
t.Errorf("%s: Authorize was not called", tc.Path)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user