mirror of
https://github.com/rancher/steve.git
synced 2025-07-01 01:02:08 +00:00
* Copy pkg/cache/sql from lasso to pkg/sqlcache * Rename import from github.com/rancher/lasso/pkg/cache/sql to github.com/rancher/steve/pkg/sqlcache * Fix filter.Match -> filter.Matches * go mod tidy * Fix lint errors * Remove lasso SQL cache mentions * Fix more CI lint errors * fix goimports Signed-off-by: Silvio Moioli <silvio@moioli.net> * fix tests (Match -> Matches) Signed-off-by: Silvio Moioli <silvio@moioli.net> * Fix Sort order --------- Signed-off-by: Silvio Moioli <silvio@moioli.net> Co-authored-by: Silvio Moioli <silvio@moioli.net>
190 lines
6.4 KiB
Go
190 lines
6.4 KiB
Go
package sqlpartition
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/rancher/apiserver/pkg/types"
|
|
"github.com/rancher/steve/pkg/accesscontrol"
|
|
"github.com/rancher/steve/pkg/attributes"
|
|
"github.com/rancher/steve/pkg/sqlcache/partition"
|
|
"github.com/rancher/wrangler/v3/pkg/kv"
|
|
"github.com/sirupsen/logrus"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
)
|
|
|
|
var (
|
|
passthroughPartitions = []partition.Partition{
|
|
{Passthrough: true},
|
|
}
|
|
)
|
|
|
|
// UnstructuredStore is like types.Store but deals in k8s unstructured objects instead of apiserver types.
|
|
// This interface exists in order for store to be mocked in tests
|
|
type UnstructuredStore interface {
|
|
ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error)
|
|
Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (*unstructured.Unstructured, []types.Warning, error)
|
|
Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (*unstructured.Unstructured, []types.Warning, error)
|
|
Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error)
|
|
|
|
ListByPartitions(apiOp *types.APIRequest, schema *types.APISchema, partitions []partition.Partition) ([]unstructured.Unstructured, int, string, error)
|
|
WatchByPartitions(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest, partitions []partition.Partition) (chan watch.Event, error)
|
|
}
|
|
|
|
// rbacPartitioner is an implementation of the sqlpartition.Partitioner interface.
|
|
type rbacPartitioner struct {
|
|
proxyStore UnstructuredStore
|
|
}
|
|
|
|
// All returns a slice of partitions applicable to the API schema and the user's access level.
|
|
// For watching individual resources or for blanket access permissions, it returns the passthrough partition.
|
|
// For more granular permissions, it returns a slice of partitions matching an allowed namespace or resource names.
|
|
func (p *rbacPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) ([]partition.Partition, error) {
|
|
switch verb {
|
|
case "list":
|
|
fallthrough
|
|
case "watch":
|
|
if id != "" {
|
|
partitions := generatePartitionsByID(apiOp, schema, verb, id)
|
|
return partitions, nil
|
|
}
|
|
partitions, passthrough := generateAggregatePartitions(apiOp, schema, verb)
|
|
if passthrough {
|
|
return passthroughPartitions, nil
|
|
}
|
|
sort.Slice(partitions, func(i, j int) bool {
|
|
return partitions[i].Namespace < partitions[j].Namespace
|
|
})
|
|
return partitions, nil
|
|
default:
|
|
return nil, fmt.Errorf("parition all: invalid verb %s", verb)
|
|
}
|
|
}
|
|
|
|
// Store returns an Store suited to listing and watching resources by partition.
|
|
func (p *rbacPartitioner) Store() UnstructuredStore {
|
|
return p.proxyStore
|
|
}
|
|
|
|
// generatePartitionsById determines whether a requester can access a particular resource
|
|
// and if so, returns the corresponding partitions
|
|
func generatePartitionsByID(apiOp *types.APIRequest, schema *types.APISchema, verb string, id string) []partition.Partition {
|
|
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
|
resources := accessListByVerb.Granted(verb)
|
|
|
|
idNamespace, name := kv.RSplit(id, "/")
|
|
apiNamespace := apiOp.Namespace
|
|
effectiveNamespace := idNamespace
|
|
|
|
// If a non-empty namespace was provided, be sure to select that for filtering and permissions checks
|
|
if idNamespace == "" && apiNamespace != "" {
|
|
effectiveNamespace = apiNamespace
|
|
}
|
|
|
|
// The external API is flexible, and permits specifying a namespace as a separate key or embedded
|
|
// within the ID of the object. Both of these cases should be valid:
|
|
// {"namespace": "n1", "id": "r1"}
|
|
// {"id": "n1/r1"}
|
|
// however, the following conflicting request is not valid, but was previously accepted:
|
|
// {"namespace": "n1", "id": "n2/r1"}
|
|
// To avoid breaking UI plugins that may inadvertently rely on the feature, we issue a deprecation
|
|
// warning for now. We still need to pick one of the namespaces for permission verification purposes.
|
|
if idNamespace != "" && apiNamespace != "" && idNamespace != apiNamespace {
|
|
logrus.Warningf("DEPRECATION: Conflicting namespaces '%v' and '%v' requested. "+
|
|
"Selecting '%v' as the effective namespace. Future steve versions will reject this request.",
|
|
idNamespace, apiNamespace, effectiveNamespace)
|
|
}
|
|
|
|
if accessListByVerb.All(verb) {
|
|
return []partition.Partition{
|
|
{
|
|
Namespace: effectiveNamespace,
|
|
All: false,
|
|
Passthrough: false,
|
|
Names: sets.New(name),
|
|
},
|
|
}
|
|
}
|
|
|
|
if effectiveNamespace != "" {
|
|
if resources[effectiveNamespace].All {
|
|
return []partition.Partition{
|
|
{
|
|
Namespace: effectiveNamespace,
|
|
All: false,
|
|
Passthrough: false,
|
|
Names: sets.New(name),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// For cluster-scoped resources, we will have parsed a "" out
|
|
// of the ID field from RSplit, but accessListByVerb specifies "*" for
|
|
// the nameset, so correct that here
|
|
resourceNamespace := effectiveNamespace
|
|
if resourceNamespace == "" {
|
|
resourceNamespace = accesscontrol.All
|
|
}
|
|
|
|
nameset, ok := resources[resourceNamespace]
|
|
if ok && nameset.Names.Has(name) {
|
|
return []partition.Partition{
|
|
{
|
|
Namespace: effectiveNamespace,
|
|
All: false,
|
|
Passthrough: false,
|
|
Names: sets.New(name),
|
|
},
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateAggregatePartitions determines whether a request can be passed through directly to the underlying store
|
|
// or if the results need to be partitioned by namespace and name based on the requester's access.
|
|
func generateAggregatePartitions(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
|
|
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
|
resources := accessListByVerb.Granted(verb)
|
|
|
|
if accessListByVerb.All(verb) {
|
|
return nil, true
|
|
}
|
|
|
|
if apiOp.Namespace != "" {
|
|
if resources[apiOp.Namespace].All {
|
|
return nil, true
|
|
}
|
|
return []partition.Partition{
|
|
{
|
|
Namespace: apiOp.Namespace,
|
|
Names: sets.Set[string](resources[apiOp.Namespace].Names),
|
|
},
|
|
}, false
|
|
}
|
|
|
|
var result []partition.Partition
|
|
|
|
if attributes.Namespaced(schema) {
|
|
for k, v := range resources {
|
|
result = append(result, partition.Partition{
|
|
Namespace: k,
|
|
All: v.All,
|
|
Names: sets.Set[string](v.Names),
|
|
})
|
|
}
|
|
} else {
|
|
for _, v := range resources {
|
|
result = append(result, partition.Partition{
|
|
All: v.All,
|
|
Names: sets.Set[string](v.Names),
|
|
})
|
|
}
|
|
}
|
|
|
|
return result, false
|
|
}
|