mirror of
https://github.com/rancher/steve.git
synced 2025-09-04 08:55:55 +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"
|
"sync"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"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/apierror"
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
"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"
|
||||||
"github.com/rancher/steve/pkg/sqlcache/informer/factory"
|
"github.com/rancher/steve/pkg/sqlcache/informer/factory"
|
||||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||||
"github.com/rancher/steve/pkg/sqlcache/sqltypes"
|
"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/data"
|
||||||
"github.com/rancher/wrangler/v3/pkg/kv"
|
"github.com/rancher/wrangler/v3/pkg/kv"
|
||||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||||
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
||||||
"github.com/rancher/wrangler/v3/pkg/summary"
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
controllerschema "github.com/rancher/steve/pkg/controllers/schema"
|
controllerschema "github.com/rancher/steve/pkg/controllers/schema"
|
||||||
@@ -50,6 +37,21 @@ import (
|
|||||||
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
||||||
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
|
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
|
||||||
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
|
"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 (
|
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)
|
// - 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
|
// - a continue token, if there are more pages after the returned one
|
||||||
// - an error instead of all of the above if anything went wrong
|
// - 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)
|
opts, err := listprocessor.ParseQuery(apiOp, s.namespaceCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, "", err
|
return nil, 0, "", err
|
||||||
}
|
}
|
||||||
// warnings from inside the informer are discarded
|
// warnings from inside the informer are discarded
|
||||||
buffer := WarningBuffer{}
|
buffer := WarningBuffer{}
|
||||||
client, err := s.clientGetter.TableAdminClient(apiOp, schema, "", &buffer)
|
client, err := s.clientGetter.TableAdminClient(apiOp, apiSchema, "", &buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, "", err
|
return nil, 0, "", err
|
||||||
}
|
}
|
||||||
gvk := attributes.GVK(schema)
|
gvk := attributes.GVK(apiSchema)
|
||||||
fields := getFieldsFromSchema(schema)
|
fields := getFieldsFromSchema(apiSchema)
|
||||||
fields = append(fields, getFieldForGVK(gvk)...)
|
fields = append(fields, getFieldForGVK(gvk)...)
|
||||||
transformFunc := s.transformBuilder.GetTransformFunc(gvk)
|
transformFunc := s.transformBuilder.GetTransformFunc(gvk)
|
||||||
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
||||||
attrs := attributes.GVK(schema)
|
attrs := attributes.GVK(apiSchema)
|
||||||
ns := attributes.Namespaced(schema)
|
ns := attributes.Namespaced(apiSchema)
|
||||||
inf, err := s.cacheFactory.CacheFor(s.ctx, fields, transformFunc, tableClient, attrs, ns, controllerschema.IsListWatchable(schema))
|
inf, err := s.cacheFactory.CacheFor(s.ctx, fields, transformFunc, tableClient, attrs, ns, controllerschema.IsListWatchable(apiSchema))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, "", err
|
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)
|
list, total, continueToken, err := inf.ListByOptions(apiOp.Context(), &opts, partitions, apiOp.Namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -9,18 +9,18 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
|
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
"github.com/rancher/steve/pkg/resources/common"
|
"github.com/rancher/steve/pkg/resources/common"
|
||||||
"github.com/rancher/steve/pkg/sqlcache/informer"
|
"github.com/rancher/steve/pkg/sqlcache/informer"
|
||||||
"github.com/rancher/steve/pkg/sqlcache/informer/factory"
|
"github.com/rancher/steve/pkg/sqlcache/informer/factory"
|
||||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
"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/sqlpartition/listprocessor"
|
||||||
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
|
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
||||||
|
|
||||||
"go.uber.org/mock/gomock"
|
"go.uber.org/mock/gomock"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rancher/apiserver/pkg/apierror"
|
"github.com/rancher/apiserver/pkg/apierror"
|
||||||
@@ -29,10 +29,14 @@ import (
|
|||||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"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"
|
||||||
"k8s.io/client-go/dynamic/fake"
|
"k8s.io/client-go/dynamic/fake"
|
||||||
"k8s.io/client-go/rest"
|
"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) {
|
func TestReset(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
description string
|
description string
|
||||||
|
Reference in New Issue
Block a user