mirror of
https://github.com/rancher/steve.git
synced 2025-07-07 03:49:01 +00:00
This implements the Imperative API that is served at /ext with Steve. The imperative API is compatible with Kubernetes' API server and will be used as an extension API server.
345 lines
9.9 KiB
Go
345 lines
9.9 KiB
Go
package ext
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rancher/lasso/pkg/controller"
|
|
"github.com/rancher/steve/pkg/accesscontrol"
|
|
wrbacv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
|
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
"k8s.io/apiserver/pkg/endpoints/request"
|
|
"k8s.io/apiserver/pkg/server/options"
|
|
)
|
|
|
|
type authzTestStore struct {
|
|
*testStore
|
|
}
|
|
|
|
func (t *authzTestStore) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestType, error) {
|
|
if name == "not-found" {
|
|
return nil, apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
|
|
}
|
|
return t.testStore.Get(ctx, name, opts)
|
|
}
|
|
|
|
func (t *authzTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
|
|
if ctx.User.GetName() == "read-only-error" {
|
|
decision, _, err := ctx.Authorizer.Authorize(ctx, authorizer.AttributesRecord{
|
|
User: ctx.User,
|
|
Verb: "customverb",
|
|
Resource: "testtypes",
|
|
ResourceRequest: true,
|
|
APIGroup: "ext.cattle.io",
|
|
})
|
|
if err != nil || decision != authorizer.DecisionAllow {
|
|
if err == nil {
|
|
err = fmt.Errorf("not allowed")
|
|
}
|
|
forbidden := apierrors.NewForbidden(ctx.GroupVersionResource.GroupResource(), "Forbidden", err)
|
|
forbidden.ErrStatus.Kind = "Status"
|
|
forbidden.ErrStatus.APIVersion = "v1"
|
|
return nil, forbidden
|
|
}
|
|
}
|
|
return &testTypeListFixture, nil
|
|
}
|
|
|
|
func (s *ExtensionAPIServerSuite) TestAuthorization() {
|
|
t := s.T()
|
|
|
|
scheme := runtime.NewScheme()
|
|
AddToScheme(scheme)
|
|
rbacv1.AddToScheme(scheme)
|
|
codecs := serializer.NewCodecFactory(scheme)
|
|
|
|
controllerFactory, err := controller.NewSharedControllerFactoryFromConfigWithOptions(s.restConfig, scheme, &controller.SharedControllerFactoryOptions{})
|
|
require.NoError(t, err)
|
|
|
|
rbacController := wrbacv1.New(controllerFactory)
|
|
|
|
accessStore := accesscontrol.NewAccessStore(s.ctx, false, rbacController)
|
|
authz := NewAccessSetAuthorizer(accessStore)
|
|
|
|
err = controllerFactory.Start(s.ctx, 2)
|
|
require.NoError(t, err)
|
|
|
|
ln, _, err := options.CreateListener("", ":0", net.ListenConfig{})
|
|
require.NoError(t, err)
|
|
|
|
store := &authzTestStore{
|
|
testStore: &testStore{},
|
|
}
|
|
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
|
|
opts.Listener = ln
|
|
opts.Authorizer = authz
|
|
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
|
|
user, ok := request.UserFrom(req.Context())
|
|
if !ok {
|
|
return nil, false, nil
|
|
}
|
|
return &authenticator.Response{
|
|
User: user,
|
|
}, true, nil
|
|
})
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
defer cleanup()
|
|
|
|
rbacBytes, err := os.ReadFile(filepath.Join("testdata", "rbac.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader(rbacBytes), 4096)
|
|
for {
|
|
var rawObj runtime.RawExtension
|
|
if err = decoder.Decode(&rawObj); err != nil {
|
|
break
|
|
}
|
|
|
|
obj, _, err := codecs.UniversalDecoder(rbacv1.SchemeGroupVersion).Decode(rawObj.Raw, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
switch obj := obj.(type) {
|
|
case *rbacv1.ClusterRole:
|
|
_, err = s.client.RbacV1().ClusterRoles().Create(s.ctx, obj, metav1.CreateOptions{})
|
|
defer func(name string) {
|
|
s.client.RbacV1().ClusterRoles().Delete(s.ctx, obj.GetName(), metav1.DeleteOptions{})
|
|
}(obj.GetName())
|
|
case *rbacv1.ClusterRoleBinding:
|
|
_, err = s.client.RbacV1().ClusterRoleBindings().Create(s.ctx, obj, metav1.CreateOptions{})
|
|
defer func(name string) {
|
|
s.client.RbacV1().ClusterRoleBindings().Delete(s.ctx, obj.GetName(), metav1.DeleteOptions{})
|
|
}(obj.GetName())
|
|
}
|
|
require.NoError(t, err, "creating")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
user *user.DefaultInfo
|
|
createRequest func() *http.Request
|
|
|
|
expectedStatusCode int
|
|
expectedStatus apierrors.APIStatus
|
|
}{
|
|
{
|
|
name: "authorized get read-only not found",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-only",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/not-found", nil)
|
|
},
|
|
expectedStatusCode: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "authorized get read-only",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-only",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/foo", nil)
|
|
},
|
|
expectedStatusCode: http.StatusOK,
|
|
},
|
|
{
|
|
name: "authorized list read-only",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-only",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil)
|
|
},
|
|
expectedStatusCode: http.StatusOK,
|
|
},
|
|
{
|
|
name: "unauthorized create from read-only",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-only",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodPost, "/apis/ext.cattle.io/v1/testtypes", nil)
|
|
},
|
|
expectedStatusCode: http.StatusForbidden,
|
|
},
|
|
{
|
|
name: "unauthorized update from read-only",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-only",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodPut, "/apis/ext.cattle.io/v1/testtypes/foo", nil)
|
|
},
|
|
expectedStatusCode: http.StatusForbidden,
|
|
},
|
|
{
|
|
name: "unauthorized delete from read-only",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-only",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodDelete, "/apis/ext.cattle.io/v1/testtypes/foo", nil)
|
|
},
|
|
expectedStatusCode: http.StatusForbidden,
|
|
},
|
|
{
|
|
name: "unauthorized create-on-update",
|
|
user: &user.DefaultInfo{
|
|
Name: "update-not-create",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
var buf bytes.Buffer
|
|
json.NewEncoder(&buf).Encode(&TestType{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "TestType",
|
|
APIVersion: testTypeGV.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "not-found",
|
|
},
|
|
})
|
|
return httptest.NewRequest(http.MethodPut, "/apis/ext.cattle.io/v1/testtypes/not-found", &buf)
|
|
},
|
|
expectedStatusCode: http.StatusForbidden,
|
|
},
|
|
{
|
|
name: "authorized read-only-error with custom store authorization",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-only-error",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil)
|
|
},
|
|
expectedStatusCode: http.StatusForbidden,
|
|
},
|
|
{
|
|
name: "authorized get read-write not found",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-write",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/not-found", nil)
|
|
},
|
|
expectedStatusCode: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "authorized get read-write",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-write",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/foo", nil)
|
|
},
|
|
expectedStatusCode: http.StatusOK,
|
|
},
|
|
{
|
|
name: "authorized list read-write",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-write",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil)
|
|
},
|
|
expectedStatusCode: http.StatusOK,
|
|
},
|
|
{
|
|
name: "authorized create from read-write",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-write",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
var buf bytes.Buffer
|
|
json.NewEncoder(&buf).Encode(&TestType{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "TestType",
|
|
APIVersion: testTypeGV.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
},
|
|
})
|
|
return httptest.NewRequest(http.MethodPost, "/apis/ext.cattle.io/v1/testtypes", &buf)
|
|
},
|
|
expectedStatusCode: http.StatusCreated,
|
|
},
|
|
{
|
|
name: "authorized update from read-write",
|
|
user: &user.DefaultInfo{
|
|
Name: "read-write",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
var buf bytes.Buffer
|
|
json.NewEncoder(&buf).Encode(&TestType{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "TestType",
|
|
APIVersion: testTypeGV.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
},
|
|
})
|
|
return httptest.NewRequest(http.MethodPut, "/apis/ext.cattle.io/v1/testtypes/foo", &buf)
|
|
},
|
|
expectedStatusCode: http.StatusOK,
|
|
},
|
|
{
|
|
name: "unauthorized user",
|
|
user: &user.DefaultInfo{
|
|
Name: "unknown-user",
|
|
},
|
|
createRequest: func() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil)
|
|
},
|
|
expectedStatusCode: http.StatusForbidden,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
req := test.createRequest()
|
|
w := httptest.NewRecorder()
|
|
|
|
if test.user != nil {
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
accessSet := accessStore.AccessFor(test.user)
|
|
assert.NotNil(c, accessSet)
|
|
}, time.Second*5, 100*time.Millisecond)
|
|
|
|
ctx := request.WithUser(req.Context(), test.user)
|
|
req = req.WithContext(ctx)
|
|
}
|
|
|
|
extensionAPIServer.ServeHTTP(w, req)
|
|
resp := w.Result()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
responseStatus := metav1.Status{}
|
|
json.Unmarshal(body, &responseStatus)
|
|
|
|
require.Equal(t, test.expectedStatusCode, resp.StatusCode)
|
|
if test.expectedStatus != nil {
|
|
require.Equal(t, test.expectedStatus.Status(), responseStatus, "for request "+req.URL.String())
|
|
}
|
|
})
|
|
}
|
|
}
|