diff --git a/pkg/resources/common/formatter.go b/pkg/resources/common/formatter.go index 415ab6a9..7cc6da09 100644 --- a/pkg/resources/common/formatter.go +++ b/pkg/resources/common/formatter.go @@ -98,6 +98,7 @@ func formatter(summarycache *summarycache.SummaryCache, asl accesscontrol.Access } hasUpdate := accessSet.Grants("update", gvr.GroupResource(), resource.APIObject.Namespace(), resource.APIObject.Name()) hasDelete := accessSet.Grants("delete", gvr.GroupResource(), resource.APIObject.Namespace(), resource.APIObject.Name()) + hasPatch := accessSet.Grants("patch", gvr.GroupResource(), resource.APIObject.Namespace(), resource.APIObject.Name()) selfLink := selfLink(gvr, meta) @@ -118,6 +119,13 @@ func formatter(summarycache *summarycache.SummaryCache, asl accesscontrol.Access } else { delete(resource.Links, "remove") } + if hasPatch { + if attributes.DisallowMethods(resource.Schema)[http.MethodPatch] { + resource.Links["patch"] = "blocked" + } + } else { + delete(resource.Links, "patch") + } if unstr, ok := resource.APIObject.Object.(*unstructured.Unstructured); ok { // with the sql cache, these were already added by the indexer. However, the sql cache diff --git a/pkg/resources/common/formatter_test.go b/pkg/resources/common/formatter_test.go index 83027f29..be46fa0a 100644 --- a/pkg/resources/common/formatter_test.go +++ b/pkg/resources/common/formatter_test.go @@ -634,6 +634,7 @@ func Test_formatterLinks(t *testing.T) { hasGet bool hasUpdate bool hasRemove bool + hasPatch bool } tests := []struct { name string @@ -935,6 +936,81 @@ func Test_formatterLinks(t *testing.T) { "view": "/api/v1/namespaces/example-ns/pods/example-pod", }, }, + { + name: "patch permissions", + hasUser: true, + permissions: &permissions{ + hasPatch: true, + }, + schema: &types.APISchema{ + Schema: &schemas.Schema{ + ID: "example", + Attributes: map[string]interface{}{ + "group": "apps", + "version": "v1", + "resource": "deployments", + }, + }, + }, + apiObject: types.APIObject{ + ID: "example", + Object: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-deployment", + Namespace: "example-ns", + }, + }, + }, + currentLinks: map[string]string{ + "default": "defaultVal", + "patch": "/v1/apps.deployments/example-ns/example-deployment", + "view": "/apis/apps/v1/namespaces/example-ns/deployments/example-deployment", + }, + wantLinks: map[string]string{ + "default": "defaultVal", + "patch": "/v1/apps.deployments/example-ns/example-deployment", + "view": "/apis/apps/v1/namespaces/example-ns/deployments/example-deployment", + }, + }, + { + name: "patch permissions, but blocked", + hasUser: true, + permissions: &permissions{ + hasPatch: true, + }, + schema: &types.APISchema{ + Schema: &schemas.Schema{ + ID: "example", + Attributes: map[string]interface{}{ + "group": "apps", + "version": "v1", + "resource": "deployments", + "disallowMethods": map[string]bool{ + http.MethodPatch: true, + }, + }, + }, + }, + apiObject: types.APIObject{ + ID: "example", + Object: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-deployment", + Namespace: "example-ns", + }, + }, + }, + currentLinks: map[string]string{ + "default": "defaultVal", + "patch": "/v1/apps.deployments/example-ns/example-deployment", + "view": "/apis/apps/v1/namespaces/example-ns/deployments/example-deployment", + }, + wantLinks: map[string]string{ + "default": "defaultVal", + "patch": "blocked", + "view": "/apis/apps/v1/namespaces/example-ns/deployments/example-deployment", + }, + }, } for _, test := range tests { @@ -964,6 +1040,12 @@ func Test_formatterLinks(t *testing.T) { ResourceName: meta.GetName(), }) } + if test.permissions.hasPatch { + accessSet.Add("patch", gvr.GroupResource(), accesscontrol.Access{ + Namespace: meta.GetNamespace(), + ResourceName: meta.GetName(), + }) + } asl.EXPECT().AccessFor(&defaultUserInfo).Return(&accessSet) } else { asl.EXPECT().AccessFor(&defaultUserInfo).Return(nil).AnyTimes() diff --git a/pkg/schema/factory.go b/pkg/schema/factory.go index b3fbe4a4..53c35173 100644 --- a/pkg/schema/factory.go +++ b/pkg/schema/factory.go @@ -157,6 +157,9 @@ func (c *Collection) schemasForSubject(access *accesscontrol.AccessSet) (*types. if verbAccess.AnyVerb("create") { s.CollectionMethods = append(s.CollectionMethods, allowed(http.MethodPost)) } + if verbAccess.AnyVerb("patch") { + s.ResourceMethods = append(s.ResourceMethods, allowed(http.MethodPatch)) + } if len(s.CollectionMethods) == 0 && len(s.ResourceMethods) == 0 { continue diff --git a/pkg/schema/factory_test.go b/pkg/schema/factory_test.go index 44dd0eaa..45853bee 100644 --- a/pkg/schema/factory_test.go +++ b/pkg/schema/factory_test.go @@ -38,6 +38,15 @@ func TestSchemas(t *testing.T) { errDesired: false, }, }, + { + name: "basic patch schema test", + config: schemaTestConfig{ + permissionVerbs: []string{"patch"}, + desiredResourceVerbs: []string{"PATCH"}, + desiredCollectionVerbs: []string{}, + errDesired: false, + }, + }, } for _, test := range tests { test := test @@ -162,7 +171,7 @@ func makeSchema(resourceType string) *types.APISchema { "group": testGroup, "version": testVersion, "resource": resourceType, - "verbs": []string{"get", "list", "watch", "delete", "update", "create"}, + "verbs": []string{"get", "list", "watch", "delete", "update", "create", "patch"}, }, }, }