mirror of
https://github.com/rancher/steve.git
synced 2025-08-10 10:41:37 +00:00
Full dynamic RBAC and pagination
This commit is contained in:
parent
12df5d1a3d
commit
e64845dcb9
@ -17,8 +17,8 @@ func NewAccessControl() *AccessControl {
|
|||||||
|
|
||||||
func (a *AccessControl) CanWatch(apiOp *types.APIRequest, schema *types.APISchema) error {
|
func (a *AccessControl) CanWatch(apiOp *types.APIRequest, schema *types.APISchema) error {
|
||||||
access := GetAccessListMap(schema)
|
access := GetAccessListMap(schema)
|
||||||
if !access.Grants("watch", "*", "*") {
|
if _, ok := access["watch"]; ok {
|
||||||
return fmt.Errorf("watch not allowed")
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
return fmt.Errorf("watch not allowed")
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package accesscontrol
|
package accesscontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccessSet struct {
|
type AccessSet struct {
|
||||||
@ -18,6 +21,26 @@ type key struct {
|
|||||||
gr schema.GroupResource
|
gr schema.GroupResource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AccessSet) Namespaces() (result []string) {
|
||||||
|
set := map[string]bool{}
|
||||||
|
for k, as := range a.set {
|
||||||
|
if k.verb != "get" && k.verb != "list" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for access := range as {
|
||||||
|
if access.Namespace == all {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
set[access.Namespace] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k := range set {
|
||||||
|
result = append(result, k)
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AccessSet) Merge(right *AccessSet) {
|
func (a *AccessSet) Merge(right *AccessSet) {
|
||||||
for k, accessMap := range right.set {
|
for k, accessMap := range right.set {
|
||||||
m, ok := a.set[k]
|
m, ok := a.set[k]
|
||||||
@ -36,6 +59,7 @@ func (a *AccessSet) Merge(right *AccessSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AccessSet) AccessListFor(verb string, gr schema.GroupResource) (result AccessList) {
|
func (a AccessSet) AccessListFor(verb string, gr schema.GroupResource) (result AccessList) {
|
||||||
|
dedup := map[Access]bool{}
|
||||||
for _, v := range []string{all, verb} {
|
for _, v := range []string{all, verb} {
|
||||||
for _, g := range []string{all, gr.Group} {
|
for _, g := range []string{all, gr.Group} {
|
||||||
for _, r := range []string{all, gr.Resource} {
|
for _, r := range []string{all, gr.Resource} {
|
||||||
@ -46,12 +70,16 @@ func (a AccessSet) AccessListFor(verb string, gr schema.GroupResource) (result A
|
|||||||
Resource: r,
|
Resource: r,
|
||||||
},
|
},
|
||||||
}] {
|
}] {
|
||||||
result = append(result, k)
|
dedup[k] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k := range dedup {
|
||||||
|
result = append(result, k)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +104,41 @@ func (a AccessListByVerb) Grants(verb, namespace, name string) bool {
|
|||||||
return a[verb].Grants(namespace, name)
|
return a[verb].Grants(namespace, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AccessListByVerb) All(verb string) bool {
|
||||||
|
return a.Grants(verb, all, all)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resources struct {
|
||||||
|
All bool
|
||||||
|
Names sets.String
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AccessListByVerb) Granted(verb string) (result map[string]Resources) {
|
||||||
|
result = map[string]Resources{}
|
||||||
|
|
||||||
|
// if list, we need to check get also
|
||||||
|
verbs := []string{verb}
|
||||||
|
if verb == "list" {
|
||||||
|
verbs = append(verbs, "get")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, verb := range verbs {
|
||||||
|
for _, access := range a[verb] {
|
||||||
|
resources := result[access.Namespace]
|
||||||
|
if access.ResourceName == all {
|
||||||
|
resources.All = true
|
||||||
|
} else {
|
||||||
|
if resources.Names == nil {
|
||||||
|
resources.Names = sets.String{}
|
||||||
|
}
|
||||||
|
resources.Names.Insert(access.ResourceName)
|
||||||
|
}
|
||||||
|
result[access.Namespace] = resources
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (a AccessListByVerb) AnyVerb(verb ...string) bool {
|
func (a AccessListByVerb) AnyVerb(verb ...string) bool {
|
||||||
for _, v := range verb {
|
for _, v := range verb {
|
||||||
if len(a[v]) > 0 {
|
if len(a[v]) > 0 {
|
||||||
|
@ -72,15 +72,23 @@ func (p *Factory) MetadataClient() metadata.Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Factory) Client(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) {
|
func (p *Factory) Client(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) {
|
||||||
return p.newClient(ctx, p.clientCfg, s, namespace)
|
return newClient(ctx, p.clientCfg, s, namespace, p.impersonate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Factory) AdminClient(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) {
|
||||||
|
return newClient(ctx, p.clientCfg, s, namespace, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Factory) ClientForWatch(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) {
|
func (p *Factory) ClientForWatch(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) {
|
||||||
return p.newClient(ctx, p.watchClientCfg, s, namespace)
|
return newClient(ctx, p.watchClientCfg, s, namespace, p.impersonate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Factory) newClient(ctx *types.APIRequest, cfg *rest.Config, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) {
|
func (p *Factory) AdminClientForWatch(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) {
|
||||||
if p.impersonate {
|
return newClient(ctx, p.watchClientCfg, s, namespace, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient(ctx *types.APIRequest, cfg *rest.Config, s *types.APISchema, namespace string, impersonate bool) (dynamic.ResourceInterface, error) {
|
||||||
|
if impersonate {
|
||||||
user, ok := request.UserFrom(ctx.Context())
|
user, ok := request.UserFrom(ctx.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("user not found for impersonation")
|
return nil, fmt.Errorf("user not found for impersonation")
|
||||||
|
@ -72,7 +72,19 @@ func (c *Collection) schemasForSubject(access *accesscontrol.AccessSet) (*types.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(verbAccess) == 0 {
|
if len(verbAccess) == 0 {
|
||||||
continue
|
if gr.Group == "" && gr.Resource == "namespaces" {
|
||||||
|
var accessList accesscontrol.AccessList
|
||||||
|
for _, ns := range access.Namespaces() {
|
||||||
|
accessList = append(accessList, accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: ns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
verbAccess["list"] = accessList
|
||||||
|
verbAccess["watch"] = accessList
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s = s.DeepCopy()
|
s = s.DeepCopy()
|
||||||
|
@ -165,6 +165,8 @@ func (s *Server) handle(apiOp *types.APIRequest, parser parse.Parser) {
|
|||||||
apiOp.WriteResponse(code, obj)
|
apiOp.WriteResponse(code, obj)
|
||||||
} else if list, ok := data.(types.APIObjectList); ok {
|
} else if list, ok := data.(types.APIObjectList); ok {
|
||||||
apiOp.WriteResponseList(code, list)
|
apiOp.WriteResponseList(code, list)
|
||||||
|
} else if code > http.StatusOK {
|
||||||
|
apiOp.Response.WriteHeader(code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,13 @@ type RawResource struct {
|
|||||||
APIObject APIObject `json:"-" yaml:"-"`
|
APIObject APIObject `json:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Pagination struct {
|
||||||
|
Limit int `json:"limit,omitempty"`
|
||||||
|
First string `json:"first,omitempty"`
|
||||||
|
Next string `json:"next,omitempty"`
|
||||||
|
Partial bool `json:"partial,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RawResource) MarshalJSON() ([]byte, error) {
|
func (r *RawResource) MarshalJSON() ([]byte, error) {
|
||||||
type r_ RawResource
|
type r_ RawResource
|
||||||
outer, err := json.Marshal((*r_)(r))
|
outer, err := json.Marshal((*r_)(r))
|
||||||
@ -160,6 +167,7 @@ type URLBuilder interface {
|
|||||||
ResourceLink(schema *APISchema, id string) string
|
ResourceLink(schema *APISchema, id string) string
|
||||||
Link(schema *APISchema, id string, linkName string) string
|
Link(schema *APISchema, id string, linkName string) string
|
||||||
Action(schema *APISchema, id string, action string) string
|
Action(schema *APISchema, id string, action string) string
|
||||||
|
Marker(marker string) string
|
||||||
|
|
||||||
RelativeToRoot(path string) string
|
RelativeToRoot(path string) string
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,13 @@ import (
|
|||||||
"github.com/rancher/wrangler/pkg/schemas"
|
"github.com/rancher/wrangler/pkg/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
ResourceFieldID = "id"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Collection struct {
|
type Collection struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Links map[string]string `json:"links"`
|
Links map[string]string `json:"links"`
|
||||||
CreateTypes map[string]string `json:"createTypes,omitempty"`
|
CreateTypes map[string]string `json:"createTypes,omitempty"`
|
||||||
Actions map[string]string `json:"actions"`
|
Actions map[string]string `json:"actions"`
|
||||||
ResourceType string `json:"resourceType"`
|
ResourceType string `json:"resourceType"`
|
||||||
|
Pagination *Pagination `json:"pagination,omitempty"`
|
||||||
Revision string `json:"revision,omitempty"`
|
Revision string `json:"revision,omitempty"`
|
||||||
Continue string `json:"continue,omitempty"`
|
Continue string `json:"continue,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,15 @@ type DefaultURLBuilder struct {
|
|||||||
query url.Values
|
query url.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DefaultURLBuilder) Marker(marker string) string {
|
||||||
|
newValues := url.Values{}
|
||||||
|
for k, v := range u.query {
|
||||||
|
newValues[k] = v
|
||||||
|
}
|
||||||
|
newValues.Set("continue", marker)
|
||||||
|
return u.Current() + "?" + newValues.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
func (u *DefaultURLBuilder) Link(schema *types.APISchema, id string, linkName string) string {
|
func (u *DefaultURLBuilder) Link(schema *types.APISchema, id string, linkName string) string {
|
||||||
return u.schemaURL(schema, id, linkName)
|
return u.schemaURL(schema, id, linkName)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package writer
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||||
)
|
)
|
||||||
@ -99,6 +100,14 @@ func (j *EncodingResponseWriter) addLinks(schema *types.APISchema, context *type
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLimit(req *http.Request) int {
|
||||||
|
limit, err := strconv.Atoi(req.Header.Get("limit"))
|
||||||
|
if err == nil && limit > 0 {
|
||||||
|
return limit
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func newCollection(apiOp *types.APIRequest, list types.APIObjectList) *types.GenericCollection {
|
func newCollection(apiOp *types.APIRequest, list types.APIObjectList) *types.GenericCollection {
|
||||||
result := &types.GenericCollection{
|
result := &types.GenericCollection{
|
||||||
Collection: types.Collection{
|
Collection: types.Collection{
|
||||||
@ -114,6 +123,18 @@ func newCollection(apiOp *types.APIRequest, list types.APIObjectList) *types.Gen
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial := list.Continue != "" || apiOp.Query.Get("continue") != ""
|
||||||
|
if partial {
|
||||||
|
result.Pagination = &types.Pagination{
|
||||||
|
Limit: getLimit(apiOp.Request),
|
||||||
|
First: apiOp.URLBuilder.Current(),
|
||||||
|
Partial: true,
|
||||||
|
}
|
||||||
|
if list.Continue != "" {
|
||||||
|
result.Pagination.Next = apiOp.URLBuilder.Marker(list.Continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if apiOp.Method == http.MethodGet {
|
if apiOp.Method == http.MethodGet {
|
||||||
if apiOp.AccessControl.CanCreate(apiOp, apiOp.Schema) == nil {
|
if apiOp.AccessControl.CanCreate(apiOp, apiOp.Schema) == nil {
|
||||||
result.CreateTypes[apiOp.Schema.ID] = apiOp.URLBuilder.Collection(apiOp.Schema)
|
result.CreateTypes[apiOp.Schema.ID] = apiOp.URLBuilder.Collection(apiOp.Schema)
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/rancher/steve/pkg/schemaserver/urlbuilder"
|
"github.com/rancher/steve/pkg/schemaserver/urlbuilder"
|
||||||
"github.com/rancher/steve/pkg/server/router"
|
"github.com/rancher/steve/pkg/server/router"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,9 +57,9 @@ type apiServer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *apiServer) common(rw http.ResponseWriter, req *http.Request) (*types.APIRequest, bool) {
|
func (a *apiServer) common(rw http.ResponseWriter, req *http.Request) (*types.APIRequest, bool) {
|
||||||
user := &user.DefaultInfo{
|
user, ok := request.UserFrom(req.Context())
|
||||||
Name: "admin",
|
if !ok {
|
||||||
Groups: []string{"system:masters"},
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
schemas, err := a.sf.Schemas(user)
|
schemas, err := a.sf.Schemas(user)
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
"github.com/rancher/steve/pkg/schema"
|
"github.com/rancher/steve/pkg/schema"
|
||||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultTemplate(clientGetter proxy.ClientGetter) schema.Template {
|
func DefaultTemplate(clientGetter proxy.ClientGetter, asl accesscontrol.AccessSetLookup) schema.Template {
|
||||||
return schema.Template{
|
return schema.Template{
|
||||||
Store: proxy.NewProxyStore(clientGetter),
|
Store: proxy.NewProxyStore(clientGetter, asl),
|
||||||
Formatter: Formatter,
|
Formatter: Formatter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,18 +232,25 @@ func (s *Store) getCount(apiOp *types.APIRequest) Count {
|
|||||||
|
|
||||||
for _, schema := range s.schemasToWatch(apiOp) {
|
for _, schema := range s.schemasToWatch(apiOp) {
|
||||||
gvr := attributes.GVR(schema)
|
gvr := attributes.GVR(schema)
|
||||||
|
access, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
||||||
|
|
||||||
rev := 0
|
rev := 0
|
||||||
itemCount := ItemCount{
|
itemCount := ItemCount{
|
||||||
Namespaces: map[string]int{},
|
Namespaces: map[string]int{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
all := access.Grants("list", "*", "*")
|
||||||
|
|
||||||
for _, obj := range s.ccache.List(gvr) {
|
for _, obj := range s.ccache.List(gvr) {
|
||||||
_, ns, revision, ok := getInfo(obj)
|
name, ns, revision, ok := getInfo(obj)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !all && !access.Grants("list", ns, name) && !access.Grants("get", ns, name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if revision > rev {
|
if revision > rev {
|
||||||
rev = revision
|
rev = revision
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package resources
|
package resources
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
"github.com/rancher/steve/pkg/client"
|
"github.com/rancher/steve/pkg/client"
|
||||||
"github.com/rancher/steve/pkg/clustercache"
|
"github.com/rancher/steve/pkg/clustercache"
|
||||||
"github.com/rancher/steve/pkg/schema"
|
"github.com/rancher/steve/pkg/schema"
|
||||||
@ -21,8 +22,8 @@ func DefaultSchemas(baseSchema *types.APISchemas, discovery discovery.DiscoveryI
|
|||||||
return baseSchema
|
return baseSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultSchemaTemplates(cf *client.Factory) []schema.Template {
|
func DefaultSchemaTemplates(cf *client.Factory, lookup accesscontrol.AccessSetLookup) []schema.Template {
|
||||||
return []schema.Template{
|
return []schema.Template{
|
||||||
common.DefaultTemplate(cf),
|
common.DefaultTemplate(cf, lookup),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,16 +53,16 @@ func setup(ctx context.Context, server *Server) (http.Handler, *schema.Collectio
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ccache := clustercache.NewClusterCache(ctx, cf.MetadataClient())
|
|
||||||
|
|
||||||
server.BaseSchemas = resources.DefaultSchemas(server.BaseSchemas, server.K8s.Discovery(), ccache)
|
|
||||||
server.SchemaTemplates = append(server.SchemaTemplates, resources.DefaultSchemaTemplates(cf)...)
|
|
||||||
|
|
||||||
asl := server.AccessSetLookup
|
asl := server.AccessSetLookup
|
||||||
if asl == nil {
|
if asl == nil {
|
||||||
asl = accesscontrol.NewAccessStore(ctx, true, server.RBAC)
|
asl = accesscontrol.NewAccessStore(ctx, true, server.RBAC)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ccache := clustercache.NewClusterCache(ctx, cf.MetadataClient())
|
||||||
|
|
||||||
|
server.BaseSchemas = resources.DefaultSchemas(server.BaseSchemas, server.K8s.Discovery(), ccache)
|
||||||
|
server.SchemaTemplates = append(server.SchemaTemplates, resources.DefaultSchemaTemplates(cf, asl)...)
|
||||||
|
|
||||||
cols, err := common.NewDynamicColumns(server.RestConfig)
|
cols, err := common.NewDynamicColumns(server.RestConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
204
pkg/server/store/proxy/con_eg.go
Normal file
204
pkg/server/store/proxy/con_eg.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ParallelPartitionLister struct {
|
||||||
|
Lister PartitionLister
|
||||||
|
Concurrency int64
|
||||||
|
Partitions []Partition
|
||||||
|
state *listState
|
||||||
|
revision string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartitionLister func(ctx context.Context, partition Partition, cont string, revision string, limit int) (types.APIObjectList, error)
|
||||||
|
|
||||||
|
func (p *ParallelPartitionLister) Err() error {
|
||||||
|
return p.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParallelPartitionLister) Revision() string {
|
||||||
|
return p.revision
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParallelPartitionLister) Continue() string {
|
||||||
|
if p.state == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
bytes, err := json.Marshal(p.state)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexOrZero(partitions []Partition, namespace string) int {
|
||||||
|
if namespace == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for i, partition := range partitions {
|
||||||
|
if partition.Namespace == namespace {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParallelPartitionLister) List(ctx context.Context, limit int, resume string) (<-chan []types.APIObject, error) {
|
||||||
|
var state listState
|
||||||
|
if resume != "" {
|
||||||
|
bytes, err := base64.StdEncoding.DecodeString(resume)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(bytes, &state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.Limit > 0 {
|
||||||
|
limit = state.Limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(chan []types.APIObject)
|
||||||
|
go p.feeder(ctx, state, limit, result)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type listState struct {
|
||||||
|
Revision string `json:"r,omitempty"`
|
||||||
|
PartitionNamespace string `json:"p,omitempty"`
|
||||||
|
Continue string `json:"c,omitempty"`
|
||||||
|
Offset int `json:"o,omitempty"`
|
||||||
|
Limit int `json:"l,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParallelPartitionLister) feeder(ctx context.Context, state listState, limit int, result chan []types.APIObject) {
|
||||||
|
var (
|
||||||
|
sem = semaphore.NewWeighted(p.Concurrency)
|
||||||
|
capacity = limit
|
||||||
|
last chan struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
defer func() {
|
||||||
|
err := eg.Wait()
|
||||||
|
if p.err == nil {
|
||||||
|
p.err = err
|
||||||
|
}
|
||||||
|
close(result)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := indexOrZero(p.Partitions, state.PartitionNamespace); i < len(p.Partitions); i++ {
|
||||||
|
if capacity <= 0 || isDone(ctx) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
partition = p.Partitions[i]
|
||||||
|
tickets = int64(1)
|
||||||
|
turn = last
|
||||||
|
next = make(chan struct{})
|
||||||
|
)
|
||||||
|
|
||||||
|
// setup a linked list of channel to control insertion order
|
||||||
|
last = next
|
||||||
|
|
||||||
|
if state.Revision == "" {
|
||||||
|
// don't have a revision yet so grab all tickets to set a revision
|
||||||
|
tickets = 3
|
||||||
|
}
|
||||||
|
if err := sem.Acquire(ctx, tickets); err != nil {
|
||||||
|
p.err = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// make state local
|
||||||
|
state := state
|
||||||
|
eg.Go(func() error {
|
||||||
|
defer sem.Release(tickets)
|
||||||
|
defer close(next)
|
||||||
|
|
||||||
|
for {
|
||||||
|
cont := ""
|
||||||
|
if partition.Namespace == state.PartitionNamespace {
|
||||||
|
cont = state.Continue
|
||||||
|
}
|
||||||
|
list, err := p.Lister(ctx, partition, cont, state.Revision, limit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForTurn(ctx, turn)
|
||||||
|
if p.state != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.Revision == "" {
|
||||||
|
state.Revision = list.Revision
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.revision == "" {
|
||||||
|
p.revision = list.Revision
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.PartitionNamespace == partition.Namespace && state.Offset > 0 && state.Offset < len(list.Objects) {
|
||||||
|
list.Objects = list.Objects[state.Offset:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(list.Objects) > capacity {
|
||||||
|
result <- list.Objects[:capacity]
|
||||||
|
// save state to redo this list at this offset
|
||||||
|
p.state = &listState{
|
||||||
|
Revision: list.Revision,
|
||||||
|
PartitionNamespace: partition.Namespace,
|
||||||
|
Continue: cont,
|
||||||
|
Offset: capacity,
|
||||||
|
Limit: limit,
|
||||||
|
}
|
||||||
|
capacity = 0
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
result <- list.Objects
|
||||||
|
capacity -= len(list.Objects)
|
||||||
|
if list.Continue == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// loop again and get more data
|
||||||
|
state.Continue = list.Continue
|
||||||
|
state.PartitionNamespace = partition.Namespace
|
||||||
|
state.Offset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
p.err = eg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForTurn(ctx context.Context, turn chan struct{}) {
|
||||||
|
if turn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-turn:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDone(ctx context.Context) bool {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||||
"github.com/rancher/wrangler/pkg/data"
|
"github.com/rancher/wrangler/pkg/data"
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
apitypes "k8s.io/apimachinery/pkg/types"
|
apitypes "k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
)
|
)
|
||||||
@ -29,17 +31,24 @@ var (
|
|||||||
|
|
||||||
type ClientGetter interface {
|
type ClientGetter interface {
|
||||||
Client(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
Client(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||||
|
AdminClient(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||||
ClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
ClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||||
|
AdminClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
clientGetter ClientGetter
|
clientGetter ClientGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyStore(clientGetter ClientGetter) types.Store {
|
func NewProxyStore(clientGetter ClientGetter, lookup accesscontrol.AccessSetLookup) types.Store {
|
||||||
return &errorStore{
|
return &errorStore{
|
||||||
Store: &Store{
|
Store: &WatchRefresh{
|
||||||
clientGetter: clientGetter,
|
Store: &RBACStore{
|
||||||
|
Store: &Store{
|
||||||
|
clientGetter: clientGetter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
asl: lookup,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,18 +186,43 @@ func tableToObjects(obj map[string]interface{}) []unstructured.Unstructured {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
func (s *Store) ByNames(apiOp *types.APIRequest, schema *types.APISchema, names sets.String) (types.APIObjectList, error) {
|
||||||
k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace)
|
adminClient, err := s.clientGetter.AdminClient(apiOp, schema, apiOp.Namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObjectList{}, err
|
return types.APIObjectList{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
objs, err := s.list(apiOp, schema, adminClient)
|
||||||
|
if err != nil {
|
||||||
|
return types.APIObjectList{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered []types.APIObject
|
||||||
|
for _, obj := range objs.Objects {
|
||||||
|
if names.Has(obj.Name()) {
|
||||||
|
filtered = append(filtered, obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objs.Objects = filtered
|
||||||
|
return objs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||||
|
client, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return types.APIObjectList{}, err
|
||||||
|
}
|
||||||
|
return s.list(apiOp, schema, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) list(apiOp *types.APIRequest, schema *types.APISchema, client dynamic.ResourceInterface) (types.APIObjectList, error) {
|
||||||
opts := metav1.ListOptions{}
|
opts := metav1.ListOptions{}
|
||||||
if err := decodeParams(apiOp, &opts); err != nil {
|
if err := decodeParams(apiOp, &opts); err != nil {
|
||||||
return types.APIObjectList{}, nil
|
return types.APIObjectList{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resultList, err := k8sClient.List(opts)
|
resultList, err := client.List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObjectList{}, err
|
return types.APIObjectList{}, err
|
||||||
}
|
}
|
||||||
@ -255,15 +289,41 @@ func (s *Store) listAndWatch(apiOp *types.APIRequest, k8sClient dynamic.Resource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
func (s *Store) WatchNames(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest, names sets.String) (chan types.APIEvent, error) {
|
||||||
k8sClient, err := s.clientGetter.ClientForWatch(apiOp, schema, apiOp.Namespace)
|
adminClient, err := s.clientGetter.ClientForWatch(apiOp, schema, apiOp.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c, err := s.watch(apiOp, schema, w, adminClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(chan types.APIEvent)
|
result := make(chan types.APIEvent)
|
||||||
go func() {
|
go func() {
|
||||||
s.listAndWatch(apiOp, k8sClient, schema, w, result)
|
defer close(result)
|
||||||
|
for item := range c {
|
||||||
|
if item.Error != nil && names.Has(item.Object.Name()) {
|
||||||
|
result <- item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||||
|
client, err := s.clientGetter.ClientForWatch(apiOp, schema, apiOp.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.watch(apiOp, schema, w, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest, client dynamic.ResourceInterface) (chan types.APIEvent, error) {
|
||||||
|
result := make(chan types.APIEvent)
|
||||||
|
go func() {
|
||||||
|
s.listAndWatch(apiOp, client, schema, w, result)
|
||||||
logrus.Debugf("closing watcher for %s", schema.ID)
|
logrus.Debugf("closing watcher for %s", schema.ID)
|
||||||
close(result)
|
close(result)
|
||||||
}()
|
}()
|
||||||
|
182
pkg/server/store/proxy/rbac_store.go
Normal file
182
pkg/server/store/proxy/rbac_store.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
|
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RBACStore struct {
|
||||||
|
*Store
|
||||||
|
}
|
||||||
|
|
||||||
|
type Partition struct {
|
||||||
|
Namespace string
|
||||||
|
All bool
|
||||||
|
Names sets.String
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPassthrough(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]Partition, bool) {
|
||||||
|
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
||||||
|
if accessListByVerb.All(verb) {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
resources := accessListByVerb.Granted(verb)
|
||||||
|
if apiOp.Namespace != "" {
|
||||||
|
if resources[apiOp.Namespace].All {
|
||||||
|
return nil, true
|
||||||
|
} else {
|
||||||
|
return []Partition{
|
||||||
|
{
|
||||||
|
Namespace: apiOp.Namespace,
|
||||||
|
Names: resources[apiOp.Namespace].Names,
|
||||||
|
},
|
||||||
|
}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []Partition
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RBACStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||||
|
partitions, passthrough := isPassthrough(apiOp, schema, "list")
|
||||||
|
if passthrough {
|
||||||
|
return r.Store.List(apiOp, schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
resume := apiOp.Request.URL.Query().Get("continue")
|
||||||
|
limit := getLimit(apiOp.Request)
|
||||||
|
|
||||||
|
sort.Slice(partitions, func(i, j int) bool {
|
||||||
|
return partitions[i].Namespace < partitions[j].Namespace
|
||||||
|
})
|
||||||
|
|
||||||
|
lister := &ParallelPartitionLister{
|
||||||
|
Lister: func(ctx context.Context, partition Partition, cont string, revision string, limit int) (types.APIObjectList, error) {
|
||||||
|
return r.list(apiOp, schema, partition, cont, revision, limit)
|
||||||
|
},
|
||||||
|
Concurrency: 3,
|
||||||
|
Partitions: partitions,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := types.APIObjectList{}
|
||||||
|
items, err := lister.List(apiOp.Context(), limit, resume)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for item := range items {
|
||||||
|
result.Objects = append(result.Objects, item...)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Continue = lister.Continue()
|
||||||
|
result.Revision = lister.Revision()
|
||||||
|
return result, lister.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLimit(req *http.Request) int {
|
||||||
|
limitString := req.URL.Query().Get("limit")
|
||||||
|
limit, err := strconv.Atoi(limitString)
|
||||||
|
if err != nil {
|
||||||
|
limit = 0
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 100000
|
||||||
|
}
|
||||||
|
return limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RBACStore) list(apiOp *types.APIRequest, schema *types.APISchema, partition Partition, cont, revision string, limit int) (types.APIObjectList, error) {
|
||||||
|
req := *apiOp
|
||||||
|
req.Namespace = partition.Namespace
|
||||||
|
req.Request = req.Request.Clone(apiOp.Context())
|
||||||
|
|
||||||
|
values := req.Request.URL.Query()
|
||||||
|
values.Set("continue", cont)
|
||||||
|
values.Set("revision", revision)
|
||||||
|
if limit > 0 {
|
||||||
|
values.Set("limit", strconv.Itoa(limit))
|
||||||
|
} else {
|
||||||
|
values.Del("limit")
|
||||||
|
}
|
||||||
|
req.Request.URL.RawQuery = values.Encode()
|
||||||
|
|
||||||
|
if partition.All {
|
||||||
|
return r.Store.List(&req, schema)
|
||||||
|
}
|
||||||
|
return r.Store.ByNames(&req, schema, partition.Names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RBACStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||||
|
partitions, passthrough := isPassthrough(apiOp, schema, "watch")
|
||||||
|
if passthrough {
|
||||||
|
return r.Store.Watch(apiOp, schema, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(apiOp.Context())
|
||||||
|
apiOp = apiOp.WithContext(ctx)
|
||||||
|
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
response := make(chan types.APIEvent)
|
||||||
|
for _, partition := range partitions {
|
||||||
|
partition := partition
|
||||||
|
eg.Go(func() error {
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
c chan types.APIEvent
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
req := *apiOp
|
||||||
|
req.Namespace = partition.Namespace
|
||||||
|
if partition.All {
|
||||||
|
c, err = r.Store.Watch(&req, schema, w)
|
||||||
|
} else {
|
||||||
|
c, err = r.Store.WatchNames(&req, schema, w, partition.Names)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range c {
|
||||||
|
response <- i
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(response)
|
||||||
|
<-ctx.Done()
|
||||||
|
eg.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
45
pkg/server/store/proxy/watch_refresh.go
Normal file
45
pkg/server/store/proxy/watch_refresh.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
|
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WatchRefresh struct {
|
||||||
|
types.Store
|
||||||
|
asl accesscontrol.AccessSetLookup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WatchRefresh) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
|
||||||
|
user, ok := request.UserFrom(apiOp.Context())
|
||||||
|
if !ok {
|
||||||
|
return w.Store.Watch(apiOp, schema, wr)
|
||||||
|
}
|
||||||
|
|
||||||
|
as := w.asl.AccessFor(user)
|
||||||
|
ctx, cancel := context.WithCancel(apiOp.Context())
|
||||||
|
apiOp = apiOp.WithContext(ctx)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(30 * time.Second):
|
||||||
|
}
|
||||||
|
|
||||||
|
newAs := w.asl.AccessFor(user)
|
||||||
|
if as.ID != newAs.ID {
|
||||||
|
// RBAC changed
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return w.Store.Watch(apiOp, schema, wr)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user