2020-02-10 17:18:20 +00:00
package proxy
import (
2020-06-05 20:30:33 +00:00
"fmt"
2020-02-10 17:18:20 +00:00
"sort"
2020-06-12 04:50:59 +00:00
"github.com/rancher/apiserver/pkg/types"
2020-02-10 17:18:20 +00:00
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/attributes"
2020-06-12 04:50:59 +00:00
"github.com/rancher/steve/pkg/stores/partition"
2024-06-04 18:52:48 +00:00
"github.com/rancher/wrangler/v3/pkg/kv"
2024-10-29 13:27:12 +00:00
"github.com/sirupsen/logrus"
2022-10-21 23:41:08 +00:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2020-02-10 17:18:20 +00:00
"k8s.io/apimachinery/pkg/util/sets"
2022-10-21 23:41:08 +00:00
"k8s.io/apimachinery/pkg/watch"
2020-02-10 17:18:20 +00:00
)
2020-06-05 20:30:33 +00:00
var (
passthroughPartitions = [ ] partition . Partition {
Partition { Passthrough : true } ,
}
)
2022-10-14 23:17:32 +00:00
// Partition is an implementation of the partition.Partition interface that uses RBAC to determine how a set of resources should be segregated and accessed.
2020-06-05 20:30:33 +00:00
type Partition struct {
Namespace string
All bool
Passthrough bool
Names sets . String
}
2022-10-14 23:17:32 +00:00
// Name returns the name of the partition, which for this type is the namespace.
2020-06-05 20:30:33 +00:00
func ( p Partition ) Name ( ) string {
return p . Namespace
}
2022-10-14 23:17:32 +00:00
// rbacPartitioner is an implementation of the partition.Partioner interface.
2020-06-05 20:30:33 +00:00
type rbacPartitioner struct {
proxyStore * Store
}
2022-10-14 23:17:32 +00:00
// Lookup returns the default passthrough partition which is used only for retrieving single resources.
// Listing or watching resources require custom partitions.
2020-06-05 20:30:33 +00:00
func ( p * rbacPartitioner ) Lookup ( apiOp * types . APIRequest , schema * types . APISchema , verb , id string ) ( partition . Partition , error ) {
switch verb {
2020-06-12 21:18:13 +00:00
case "create" :
fallthrough
2020-06-05 20:30:33 +00:00
case "get" :
fallthrough
case "update" :
fallthrough
case "delete" :
return passthroughPartitions [ 0 ] , nil
default :
2020-06-12 21:18:13 +00:00
return nil , fmt . Errorf ( "partition list: invalid verb %s" , verb )
2020-06-05 20:30:33 +00:00
}
}
2022-10-14 23:17:32 +00:00
// 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.
2020-06-22 16:42:18 +00:00
func ( p * rbacPartitioner ) All ( apiOp * types . APIRequest , schema * types . APISchema , verb , id string ) ( [ ] partition . Partition , error ) {
2020-06-05 20:30:33 +00:00
switch verb {
case "list" :
fallthrough
case "watch" :
2020-06-22 16:42:18 +00:00
if id != "" {
2024-10-29 13:27:12 +00:00
partitions := generatePartitionsByID ( apiOp , schema , verb , id )
return partitions , nil
2020-06-22 16:42:18 +00:00
}
2024-10-29 13:27:12 +00:00
partitions , passthrough := generateAggregatePartitions ( apiOp , schema , verb )
2020-06-05 20:30:33 +00:00
if passthrough {
return passthroughPartitions , nil
}
sort . Slice ( partitions , func ( i , j int ) bool {
return partitions [ i ] . ( Partition ) . Namespace < partitions [ j ] . ( Partition ) . Namespace
} )
return partitions , nil
default :
2020-06-12 21:18:13 +00:00
return nil , fmt . Errorf ( "parition all: invalid verb %s" , verb )
2020-06-05 20:30:33 +00:00
}
}
2022-10-21 23:41:08 +00:00
// Store returns an UnstructuredStore suited to listing and watching resources by partition.
func ( p * rbacPartitioner ) Store ( apiOp * types . APIRequest , partition partition . Partition ) ( partition . UnstructuredStore , error ) {
2020-06-05 20:30:33 +00:00
return & byNameOrNamespaceStore {
Store : p . proxyStore ,
partition : partition . ( Partition ) ,
} , nil
}
type byNameOrNamespaceStore struct {
2020-02-10 17:18:20 +00:00
* Store
2020-06-05 20:30:33 +00:00
partition Partition
2020-02-10 17:18:20 +00:00
}
2022-10-14 23:17:32 +00:00
// List returns a list of resources by partition.
2022-12-16 16:12:57 +00:00
func ( b * byNameOrNamespaceStore ) List ( apiOp * types . APIRequest , schema * types . APISchema ) ( * unstructured . UnstructuredList , [ ] types . Warning , error ) {
2020-06-05 20:30:33 +00:00
if b . partition . Passthrough {
return b . Store . List ( apiOp , schema )
}
apiOp . Namespace = b . partition . Namespace
if b . partition . All {
return b . Store . List ( apiOp , schema )
}
return b . Store . ByNames ( apiOp , schema , b . partition . Names )
2020-02-10 17:18:20 +00:00
}
2022-10-14 23:17:32 +00:00
// Watch returns a channel of resources by partition.
2022-10-21 23:41:08 +00:00
func ( b * byNameOrNamespaceStore ) Watch ( apiOp * types . APIRequest , schema * types . APISchema , wr types . WatchRequest ) ( chan watch . Event , error ) {
2020-06-05 20:30:33 +00:00
if b . partition . Passthrough {
return b . Store . Watch ( apiOp , schema , wr )
}
apiOp . Namespace = b . partition . Namespace
if b . partition . All {
return b . Store . Watch ( apiOp , schema , wr )
}
return b . Store . WatchNames ( apiOp , schema , wr , b . partition . Names )
}
2024-10-29 13:27:12 +00:00
// 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 {
Partition {
Namespace : effectiveNamespace ,
All : false ,
Passthrough : false ,
Names : sets . NewString ( name ) ,
} ,
}
}
if effectiveNamespace != "" {
if resources [ effectiveNamespace ] . All {
return [ ] partition . Partition {
Partition {
Namespace : effectiveNamespace ,
All : false ,
Passthrough : false ,
Names : sets . NewString ( name ) ,
} ,
}
}
}
// For cluster-scoped resources, we will have parsed a "" out
// of the ID field from RSplit, but accessListByVerb specifies "*" for
// the namespace, so correct that here
resourceNamespace := effectiveNamespace
if resourceNamespace == "" {
resourceNamespace = accesscontrol . All
}
nameset , ok := resources [ resourceNamespace ]
if ok && nameset . Names . Has ( name ) {
return [ ] partition . Partition {
Partition {
Namespace : effectiveNamespace ,
All : false ,
Passthrough : false ,
Names : sets . NewString ( name ) ,
} ,
}
}
return nil
}
// generateAggregatePartitions determines whether a request can be passed through directly to the underlying store
2022-10-11 19:28:51 +00:00
// or if the results need to be partitioned by namespace and name based on the requester's access.
2024-10-29 13:27:12 +00:00
func generateAggregatePartitions ( apiOp * types . APIRequest , schema * types . APISchema , verb string ) ( [ ] partition . Partition , bool ) {
2020-02-10 17:18:20 +00:00
accessListByVerb , _ := attributes . Access ( schema ) . ( accesscontrol . AccessListByVerb )
2024-10-29 13:27:12 +00:00
resources := accessListByVerb . Granted ( verb )
2020-02-10 17:18:20 +00:00
if accessListByVerb . All ( verb ) {
return nil , true
}
if apiOp . Namespace != "" {
if resources [ apiOp . Namespace ] . All {
return nil , true
}
2022-10-14 20:21:17 +00:00
return [ ] partition . Partition {
Partition {
Namespace : apiOp . Namespace ,
Names : resources [ apiOp . Namespace ] . Names ,
} ,
} , false
2020-02-10 17:18:20 +00:00
}
2020-06-05 20:30:33 +00:00
var result [ ] partition . Partition
2020-02-10 17:18:20 +00:00
if attributes . Namespaced ( schema ) {
for k , v := range resources {
result = append ( result , Partition {
Namespace : k ,
All : v . All ,
Names : v . Names ,
} )
}
} else {
for _ , v := range resources {
result = append ( result , Partition {
All : v . All ,
Names : v . Names ,
} )
}
}
return result , false
}