mirror of
https://github.com/rancher/steve.git
synced 2025-09-05 01:12:09 +00:00
Add vai access-control for ext Tokens and Kubeconfigs (#651)
* Add vai access-control for tokens. * Check for both Token and Kubeconfig resources * Add a unit test for verifying the generated filters for restricted resources. * Remove a TODO comment as Tom points out we no longer need it. * Return error if we can't get userinfo from apiOp.Request.Context * Stop using camelCase for the user ID label. * Add a test for the admin user. * And fold the two user-access tests into a single parameterized test. * Address reviewer comments. * post-rebase merge fixes * WIP - add a comment about determining admin users. --------- Co-authored-by: Peter Matseykanets <peter.matseykanets@suse.com>
This commit is contained in:
@@ -13,34 +13,21 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rancher/steve/pkg/stores/queryhelper"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/rancher/apiserver/pkg/apierror"
|
||||
"github.com/rancher/apiserver/pkg/types"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/sqlcache/informer"
|
||||
"github.com/rancher/steve/pkg/sqlcache/informer/factory"
|
||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||
"github.com/rancher/steve/pkg/sqlcache/sqltypes"
|
||||
"github.com/rancher/steve/pkg/stores/queryhelper"
|
||||
"github.com/rancher/wrangler/v3/pkg/data"
|
||||
"github.com/rancher/wrangler/v3/pkg/kv"
|
||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
||||
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
controllerschema "github.com/rancher/steve/pkg/controllers/schema"
|
||||
@@ -50,6 +37,21 @@ import (
|
||||
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
||||
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
|
||||
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -738,28 +740,50 @@ func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id stri
|
||||
// - the total number of resources (returned list might be a subset depending on pagination options in apiOp)
|
||||
// - a continue token, if there are more pages after the returned one
|
||||
// - an error instead of all of the above if anything went wrong
|
||||
func (s *Store) ListByPartitions(apiOp *types.APIRequest, schema *types.APISchema, partitions []partition.Partition) (*unstructured.UnstructuredList, int, string, error) {
|
||||
func (s *Store) ListByPartitions(apiOp *types.APIRequest, apiSchema *types.APISchema, partitions []partition.Partition) (*unstructured.UnstructuredList, int, string, error) {
|
||||
opts, err := listprocessor.ParseQuery(apiOp, s.namespaceCache)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
// warnings from inside the informer are discarded
|
||||
buffer := WarningBuffer{}
|
||||
client, err := s.clientGetter.TableAdminClient(apiOp, schema, "", &buffer)
|
||||
client, err := s.clientGetter.TableAdminClient(apiOp, apiSchema, "", &buffer)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
gvk := attributes.GVK(schema)
|
||||
fields := getFieldsFromSchema(schema)
|
||||
gvk := attributes.GVK(apiSchema)
|
||||
fields := getFieldsFromSchema(apiSchema)
|
||||
fields = append(fields, getFieldForGVK(gvk)...)
|
||||
transformFunc := s.transformBuilder.GetTransformFunc(gvk)
|
||||
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
||||
attrs := attributes.GVK(schema)
|
||||
ns := attributes.Namespaced(schema)
|
||||
inf, err := s.cacheFactory.CacheFor(s.ctx, fields, transformFunc, tableClient, attrs, ns, controllerschema.IsListWatchable(schema))
|
||||
attrs := attributes.GVK(apiSchema)
|
||||
ns := attributes.Namespaced(apiSchema)
|
||||
inf, err := s.cacheFactory.CacheFor(s.ctx, fields, transformFunc, tableClient, attrs, ns, controllerschema.IsListWatchable(apiSchema))
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
if gvk.Group == "ext.cattle.io" && (gvk.Kind == "Token" || gvk.Kind == "Kubeconfig") {
|
||||
accessSet := accesscontrol.AccessSetFromAPIRequest(apiOp)
|
||||
// See https://github.com/rancher/rancher/blob/7266e5e624f0d610c76ab0af33e30f5b72e11f61/pkg/ext/stores/tokens/tokens.go#L1186C2-L1195C3
|
||||
// for similar code on how we determine if a user is admin
|
||||
if accessSet == nil || !accessSet.Grants("list", schema.GroupResource{
|
||||
Resource: "*",
|
||||
}, "", "") {
|
||||
user, ok := request.UserFrom(apiOp.Request.Context())
|
||||
if !ok {
|
||||
return nil, 0, "", apierror.NewAPIError(validation.MissingRequired, "failed to get user info from the request.Context object")
|
||||
}
|
||||
opts.Filters = append(opts.Filters, sqltypes.OrFilter{
|
||||
Filters: []sqltypes.Filter{
|
||||
{
|
||||
Field: []string{"metadata", "labels", "cattle.io/user-id"},
|
||||
Matches: []string{user.GetName()},
|
||||
Op: sqltypes.Eq,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
list, total, continueToken, err := inf.ListByOptions(apiOp.Context(), &opts, partitions, apiOp.Namespace)
|
||||
if err != nil {
|
||||
|
@@ -9,18 +9,18 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/resources/common"
|
||||
"github.com/rancher/steve/pkg/sqlcache/informer"
|
||||
"github.com/rancher/steve/pkg/sqlcache/informer/factory"
|
||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||
"github.com/rancher/steve/pkg/sqlcache/sqltypes"
|
||||
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
|
||||
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
|
||||
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rancher/apiserver/pkg/apierror"
|
||||
@@ -29,10 +29,14 @@ import (
|
||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
krequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
@@ -621,6 +625,118 @@ func TestListByPartitions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListByPartitionWithUserAccess(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
accessSetSetter func(accessSet *accesscontrol.AccessSet)
|
||||
orFilters []sqltypes.OrFilter
|
||||
}
|
||||
var tests []testCase
|
||||
tests = append(tests, testCase{
|
||||
description: "client ListByPartitions(), with a specified user for a restricted resource should filter for that user",
|
||||
accessSetSetter: func(accessSet *accesscontrol.AccessSet) {},
|
||||
orFilters: []sqltypes.OrFilter{
|
||||
{
|
||||
Filters: []sqltypes.Filter{
|
||||
{
|
||||
Field: []string{"metadata", "labels", "cattle.io/user-id"},
|
||||
Matches: []string{"flip"},
|
||||
Op: sqltypes.Eq,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "client ListByPartitions(), with an admin user for a restricted resource will return all items regardless of user",
|
||||
accessSetSetter: func(accessSet *accesscontrol.AccessSet) {
|
||||
// admins also get this access-set
|
||||
accessSet.Add("list",
|
||||
schema2.GroupResource{Group: accesscontrol.All, Resource: accesscontrol.All},
|
||||
accesscontrol.Access{Namespace: accesscontrol.All, ResourceName: accesscontrol.All},
|
||||
)
|
||||
},
|
||||
orFilters: []sqltypes.OrFilter{},
|
||||
})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
nsi := NewMockCache(gomock.NewController(t))
|
||||
cg := NewMockClientGetter(gomock.NewController(t))
|
||||
cf := NewMockCacheFactory(gomock.NewController(t))
|
||||
ri := NewMockResourceInterface(gomock.NewController(t))
|
||||
bloi := NewMockByOptionsLister(gomock.NewController(t))
|
||||
tb := NewMockTransformBuilder(gomock.NewController(t))
|
||||
inf := &informer.Informer{
|
||||
ByOptionsLister: bloi,
|
||||
}
|
||||
c := factory.Cache{
|
||||
ByOptionsLister: inf,
|
||||
}
|
||||
s := &Store{
|
||||
ctx: context.Background(),
|
||||
namespaceCache: nsi,
|
||||
clientGetter: cg,
|
||||
cacheFactory: cf,
|
||||
transformBuilder: tb,
|
||||
}
|
||||
var partitions []partition.Partition
|
||||
username := "flip"
|
||||
targetGroup := "ext.cattle.io"
|
||||
targetKind := "Token"
|
||||
accessSet := &accesscontrol.AccessSet{ID: username}
|
||||
accessSet.Add("list",
|
||||
schema2.GroupResource{Group: targetGroup, Resource: "token"},
|
||||
accesscontrol.Access{Namespace: accesscontrol.All, ResourceName: "token"},
|
||||
)
|
||||
test.accessSetSetter(accessSet)
|
||||
apiOpSchemas := &types.APISchemas{}
|
||||
accesscontrol.SetAccessSetAttribute(apiOpSchemas, accessSet)
|
||||
theRequest := &http.Request{
|
||||
URL: &url.URL{},
|
||||
}
|
||||
userInfo := user.DefaultInfo{Name: username, UID: "Id"}
|
||||
requestWithContext := krequest.WithUser(context.Background(), &userInfo)
|
||||
theRequest = theRequest.WithContext(requestWithContext)
|
||||
apiOp := &types.APIRequest{
|
||||
Request: theRequest,
|
||||
Schemas: apiOpSchemas,
|
||||
}
|
||||
theSchema := &types.APISchema{
|
||||
Schema: &schemas.Schema{Attributes: map[string]interface{}{
|
||||
"columns": []common.ColumnDefinition{
|
||||
{
|
||||
Field: "some.field",
|
||||
},
|
||||
},
|
||||
"verbs": []string{"list", "watch"},
|
||||
}},
|
||||
}
|
||||
gvk := schema2.GroupVersionKind{
|
||||
Group: targetGroup,
|
||||
Kind: targetKind,
|
||||
}
|
||||
opts := &sqltypes.ListOptions{
|
||||
Filters: test.orFilters,
|
||||
Pagination: sqltypes.Pagination{
|
||||
Page: 1,
|
||||
},
|
||||
}
|
||||
attributes.SetGVK(theSchema, gvk)
|
||||
cg.EXPECT().TableAdminClient(apiOp, theSchema, "", &WarningBuffer{}).Return(ri, nil)
|
||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {"id"}, {"metadata", "state", "name"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(theSchema), attributes.Namespaced(theSchema), true).Return(c, nil)
|
||||
tb.EXPECT().GetTransformFunc(attributes.GVK(theSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||
|
||||
listToReturn := &unstructured.UnstructuredList{
|
||||
Items: make([]unstructured.Unstructured, 0, 0),
|
||||
}
|
||||
bloi.EXPECT().ListByOptions(apiOp.Context(), opts, partitions, "").Return(listToReturn, len(listToReturn.Items), "", nil)
|
||||
_, _, _, err := s.ListByPartitions(apiOp, theSchema, partitions)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReset(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
|
Reference in New Issue
Block a user