diff --git a/pkg/stores/sqlproxy/proxy_store.go b/pkg/stores/sqlproxy/proxy_store.go index b63c0cee..41ecc956 100644 --- a/pkg/stores/sqlproxy/proxy_store.go +++ b/pkg/stores/sqlproxy/proxy_store.go @@ -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 { diff --git a/pkg/stores/sqlproxy/proxy_store_test.go b/pkg/stores/sqlproxy/proxy_store_test.go index f7c49a43..7507604f 100644 --- a/pkg/stores/sqlproxy/proxy_store_test.go +++ b/pkg/stores/sqlproxy/proxy_store_test.go @@ -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