mirror of
https://github.com/rancher/steve.git
synced 2025-08-02 07:12:36 +00:00
Merge 5fa7be534a
into 57ce685118
This commit is contained in:
commit
21c7f870be
@ -7,6 +7,7 @@ import (
|
||||
"github.com/rancher/apiserver/pkg/types"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/resources/virtual/common"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
||||
"github.com/rancher/steve/pkg/stores/proxy"
|
||||
@ -41,15 +42,20 @@ func DefaultTemplateForStore(store types.Store,
|
||||
}
|
||||
|
||||
func selfLink(gvr schema2.GroupVersionResource, meta metav1.Object) (prefix string) {
|
||||
return buildBasePath(gvr, meta.GetNamespace(), meta.GetName())
|
||||
}
|
||||
|
||||
func buildBasePath(gvr schema2.GroupVersionResource, namespace string, includeName string) string {
|
||||
buf := &strings.Builder{}
|
||||
|
||||
if gvr.Group == "management.cattle.io" && gvr.Version == "v3" {
|
||||
buf.WriteString("/v1/")
|
||||
buf.WriteString(gvr.Group)
|
||||
buf.WriteString(".")
|
||||
buf.WriteString(gvr.Resource)
|
||||
if meta.GetNamespace() != "" {
|
||||
if namespace != "" {
|
||||
buf.WriteString("/")
|
||||
buf.WriteString(meta.GetNamespace())
|
||||
buf.WriteString(namespace)
|
||||
}
|
||||
} else {
|
||||
if gvr.Group == "" {
|
||||
@ -61,19 +67,23 @@ func selfLink(gvr schema2.GroupVersionResource, meta metav1.Object) (prefix stri
|
||||
buf.WriteString(gvr.Version)
|
||||
buf.WriteString("/")
|
||||
}
|
||||
if meta.GetNamespace() != "" {
|
||||
if namespace != "" {
|
||||
buf.WriteString("namespaces/")
|
||||
buf.WriteString(meta.GetNamespace())
|
||||
buf.WriteString(namespace)
|
||||
buf.WriteString("/")
|
||||
}
|
||||
buf.WriteString(gvr.Resource)
|
||||
}
|
||||
buf.WriteString("/")
|
||||
buf.WriteString(meta.GetName())
|
||||
|
||||
if includeName != "" {
|
||||
buf.WriteString("/")
|
||||
buf.WriteString(includeName)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func formatter(summarycache *summarycache.SummaryCache, asl accesscontrol.AccessSetLookup) types.Formatter {
|
||||
func formatter(summarycache common.SummaryCache, asl accesscontrol.AccessSetLookup) types.Formatter {
|
||||
return func(request *types.APIRequest, resource *types.RawResource) {
|
||||
if resource.Schema == nil {
|
||||
return
|
||||
@ -146,6 +156,32 @@ func formatter(summarycache *summarycache.SummaryCache, asl accesscontrol.Access
|
||||
excludeValues(request, unstr)
|
||||
}
|
||||
|
||||
if permsQuery := request.Query.Get("checkPermissions"); permsQuery != "" {
|
||||
ns := getNamespaceFromResource(resource.APIObject)
|
||||
permissions := map[string]map[string]string{}
|
||||
|
||||
for _, res := range strings.Split(permsQuery, ",") {
|
||||
s := request.Schemas.LookupSchema(res)
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
gvr := attributes.GVR(s)
|
||||
gr := schema2.GroupResource{Group: gvr.Group, Resource: gvr.Resource}
|
||||
perms := map[string]string{}
|
||||
|
||||
for _, verb := range []string{"create", "update", "delete", "list", "get", "watch", "patch"} {
|
||||
if accessSet.Grants(verb, gr, ns, "") {
|
||||
url := request.URLBuilder.RelativeToRoot(buildBasePath(gvr, ns, ""))
|
||||
perms[verb] = url
|
||||
}
|
||||
}
|
||||
permissions[res] = perms
|
||||
}
|
||||
|
||||
if unstr, ok := resource.APIObject.Object.(*unstructured.Unstructured); ok {
|
||||
data.PutValue(unstr.Object, permissions, "resourcePermissions")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,3 +220,27 @@ func excludeValues(request *types.APIRequest, unstr *unstructured.Unstructured)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNamespaceFromResource(obj types.APIObject) string {
|
||||
unstr, ok := obj.Object.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// If we have a backingNamespace, use that
|
||||
if statusRaw, ok := unstr.Object["status"]; ok {
|
||||
if statusMap, ok := statusRaw.(map[string]interface{}); ok {
|
||||
if backingNamespace, ok := statusMap["backingNamespace"].(string); ok && backingNamespace != "" {
|
||||
return backingNamespace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, if the id has a slash, we will interpret that
|
||||
parts := strings.SplitN(obj.ID, "/", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[0] + "-" + parts[1]
|
||||
}
|
||||
|
||||
return obj.ID
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rancher/apiserver/pkg/types"
|
||||
@ -12,7 +13,9 @@ import (
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/accesscontrol/fake"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/resources/virtual/common"
|
||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
@ -1072,3 +1075,223 @@ func Test_formatterLinks(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatterAddsResourcePermissions(t *testing.T) {
|
||||
const (
|
||||
clusterid = "clusterid"
|
||||
projectid = "projectid"
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
topLevelPermissions []string
|
||||
resourcePermissions map[string][]string
|
||||
schema *types.APISchema
|
||||
apiObject types.APIObject
|
||||
want map[string]map[string]string
|
||||
}{
|
||||
{
|
||||
name: "get update patch on project and get on projectroletemplatebindings",
|
||||
topLevelPermissions: []string{"get", "update", "patch"},
|
||||
resourcePermissions: map[string][]string{
|
||||
"projectroletemplatebindings": {"get", "list", "watch"},
|
||||
},
|
||||
schema: &types.APISchema{
|
||||
Schema: &schemas.Schema{
|
||||
ID: clusterid + "/" + projectid,
|
||||
Attributes: map[string]interface{}{
|
||||
"group": "management.cattle.io",
|
||||
"version": "v1",
|
||||
"resource": "projects",
|
||||
},
|
||||
},
|
||||
},
|
||||
apiObject: types.APIObject{
|
||||
ID: clusterid + "/" + projectid,
|
||||
Object: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
want: map[string]map[string]string{
|
||||
"projectroletemplatebindings": {
|
||||
"get": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/projectroletemplatebindings",
|
||||
"list": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/projectroletemplatebindings",
|
||||
"watch": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/projectroletemplatebindings",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get update patch on project and get on projectroletemplatebindings and pods",
|
||||
topLevelPermissions: []string{"get", "update", "patch"},
|
||||
resourcePermissions: map[string][]string{
|
||||
"projectroletemplatebindings": {"get", "list", "watch"},
|
||||
"pods": {"get", "list", "watch"},
|
||||
},
|
||||
schema: &types.APISchema{
|
||||
Schema: &schemas.Schema{
|
||||
ID: clusterid + "/" + projectid,
|
||||
Attributes: map[string]interface{}{
|
||||
"group": "management.cattle.io",
|
||||
"version": "v1",
|
||||
"resource": "projects",
|
||||
},
|
||||
},
|
||||
},
|
||||
apiObject: types.APIObject{
|
||||
ID: clusterid + "/" + projectid,
|
||||
Object: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
want: map[string]map[string]string{
|
||||
"projectroletemplatebindings": {
|
||||
"get": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/projectroletemplatebindings",
|
||||
"list": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/projectroletemplatebindings",
|
||||
"watch": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/projectroletemplatebindings",
|
||||
},
|
||||
"pods": {
|
||||
"get": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/pods",
|
||||
"list": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/pods",
|
||||
"watch": "/apis/management.cattle.io/v1/namespaces/clusterid-projectid/pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get update remove on project and a checkPermissions on an unknown resource",
|
||||
topLevelPermissions: []string{"get", "update", "patch"},
|
||||
resourcePermissions: map[string][]string{},
|
||||
schema: &types.APISchema{
|
||||
Schema: &schemas.Schema{
|
||||
ID: clusterid + "/" + projectid,
|
||||
Attributes: map[string]interface{}{
|
||||
"group": "management.cattle.io",
|
||||
"version": "v1",
|
||||
"resource": "projects",
|
||||
},
|
||||
},
|
||||
},
|
||||
apiObject: types.APIObject{
|
||||
ID: clusterid + "/" + projectid,
|
||||
Object: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultUserInfo := user.DefaultInfo{}
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
asl := fake.NewMockAccessSetLookup(ctrl)
|
||||
accessSet := accesscontrol.AccessSet{}
|
||||
|
||||
// Se up the AccessSet for the top level resource
|
||||
if len(test.topLevelPermissions) > 0 {
|
||||
gvr := attributes.GVR(test.schema)
|
||||
objMeta, _ := meta.Accessor(test.apiObject.Object)
|
||||
resource := accesscontrol.Access{
|
||||
Namespace: objMeta.GetNamespace(),
|
||||
ResourceName: objMeta.GetName(),
|
||||
}
|
||||
|
||||
for _, verb := range test.topLevelPermissions {
|
||||
accessSet.Add(verb, gvr.GroupResource(), resource)
|
||||
}
|
||||
}
|
||||
|
||||
// Se up the AccessSet for the top nested resources
|
||||
for resource, verbs := range test.resourcePermissions {
|
||||
gvr := schema2.GroupVersionResource{
|
||||
Group: "management.cattle.io",
|
||||
Version: "v1",
|
||||
Resource: resource,
|
||||
}
|
||||
for _, verb := range verbs {
|
||||
accessSet.Add(verb, gvr.GroupResource(), accesscontrol.Access{
|
||||
Namespace: clusterid + "-" + projectid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = request.WithUser(ctx, &defaultUserInfo)
|
||||
httpRequest, _ := http.NewRequestWithContext(ctx, "", "", bytes.NewBuffer([]byte{}))
|
||||
|
||||
var checkPerms []string
|
||||
for res := range test.want {
|
||||
checkPerms = append(checkPerms, res)
|
||||
}
|
||||
|
||||
req := &types.APIRequest{
|
||||
Request: httpRequest,
|
||||
URLBuilder: &urlbuilder.DefaultURLBuilder{},
|
||||
Query: url.Values{
|
||||
"checkPermissions": {strings.Join(checkPerms, ",")},
|
||||
},
|
||||
Schemas: types.EmptyAPISchemas(),
|
||||
}
|
||||
addSchema := func(names ...string) {
|
||||
for _, name := range names {
|
||||
if name == "unknown" {
|
||||
continue
|
||||
}
|
||||
req.Schemas.MustAddSchema(types.APISchema{
|
||||
Schema: &schemas.Schema{
|
||||
ID: name,
|
||||
CollectionMethods: []string{"get"},
|
||||
ResourceMethods: []string{"get"},
|
||||
Attributes: map[string]interface{}{
|
||||
"group": "management.cattle.io",
|
||||
"resource": name,
|
||||
"version": "v1",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for res := range test.want {
|
||||
addSchema(res)
|
||||
}
|
||||
|
||||
resource := &types.RawResource{
|
||||
Schema: test.schema,
|
||||
APIObject: test.apiObject,
|
||||
Links: map[string]string{},
|
||||
}
|
||||
fakeCache := &common.FakeSummaryCache{
|
||||
SummarizedObject: &summary.SummarizedObject{},
|
||||
}
|
||||
|
||||
asl.EXPECT().AccessFor(&defaultUserInfo).Return(&accessSet).AnyTimes()
|
||||
|
||||
formatter := formatter(fakeCache, asl)
|
||||
formatter(req, resource)
|
||||
|
||||
// Extract the resultant resourcePermissions
|
||||
u, ok := resource.APIObject.Object.(*unstructured.Unstructured)
|
||||
require.True(t, ok, "APIObject.Object is not Unstructured")
|
||||
|
||||
if test.want != nil {
|
||||
rawPerms, ok := u.Object["resourcePermissions"]
|
||||
require.True(t, ok, "resourcePermissions field missing")
|
||||
|
||||
permMap, ok := rawPerms.(map[string]map[string]string)
|
||||
require.True(t, ok, "resourcePermissions is not map[string]map[string]string")
|
||||
|
||||
got := map[string]map[string]string{}
|
||||
for res, actionMap := range permMap {
|
||||
got[res] = map[string]string{}
|
||||
for action, boolVal := range actionMap {
|
||||
got[res][action] = boolVal
|
||||
}
|
||||
}
|
||||
require.Equal(t, test.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/rancher/apiserver/pkg/types"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
@ -143,6 +144,15 @@ func (c *Collection) schemasForSubject(access *accesscontrol.AccessSet) (*types.
|
||||
|
||||
s = s.DeepCopy()
|
||||
attributes.SetAccess(s, verbAccess)
|
||||
|
||||
if s.ResourceFields == nil {
|
||||
s.ResourceFields = make(map[string]schemas.Field)
|
||||
}
|
||||
s.ResourceFields["resourcePermissions"] = schemas.Field{
|
||||
Type: "map[json]",
|
||||
Description: "Per-resource access permissions",
|
||||
}
|
||||
|
||||
if verbAccess.AnyVerb("list", "get") {
|
||||
s.ResourceMethods = append(s.ResourceMethods, allowed(http.MethodGet))
|
||||
s.CollectionMethods = append(s.CollectionMethods, allowed(http.MethodGet))
|
||||
|
Loading…
Reference in New Issue
Block a user