Skip to content

Commit 7814220

Browse files
authored
Merge pull request #3402 from xmudrii/verbs-support
Implement support for verbs in PermissionClaims
2 parents ef6b425 + 000910b commit 7814220

28 files changed

+1902
-85
lines changed

cli/pkg/workspace/plugin/use.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
apierrors "k8s.io/apimachinery/pkg/api/errors"
3232
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/util/sets"
3334
"k8s.io/cli-runtime/pkg/genericclioptions"
3435
"k8s.io/client-go/tools/clientcmd"
3536
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
@@ -424,20 +425,27 @@ func getAPIBindings(ctx context.Context, kcpClusterClient kcpclientset.ClusterIn
424425
func findUnresolvedPermissionClaims(out io.Writer, apiBindings []apisv1alpha2.APIBinding) error {
425426
for _, binding := range apiBindings {
426427
for _, exportedClaim := range binding.Status.ExportPermissionClaims {
427-
var found, ack bool
428+
var found, ack, verbsMatch bool
429+
var verbsExpected, verbsActual sets.Set[string]
428430
for _, specClaim := range binding.Spec.PermissionClaims {
429431
if !exportedClaim.Equal(specClaim.PermissionClaim) {
430432
continue
431433
}
432434
found = true
433435
ack = (specClaim.State == apisv1alpha2.ClaimAccepted) || specClaim.State == apisv1alpha2.ClaimRejected
436+
verbsExpected = sets.New(exportedClaim.Verbs...)
437+
verbsActual = sets.New(specClaim.Verbs...)
438+
verbsMatch = verbsActual.Difference(verbsExpected).Len() == 0 && verbsExpected.Difference(verbsActual).Len() == 0
434439
}
435440
if !found {
436441
fmt.Fprintf(out, "Warning: claim for %s exported but not specified on APIBinding %s\nAdd this claim to the APIBinding's Spec.\n", exportedClaim.String(), binding.Name)
437442
}
438443
if !ack {
439444
fmt.Fprintf(out, "Warning: claim for %s specified on APIBinding %s but not accepted or rejected.\n", exportedClaim.String(), binding.Name)
440445
}
446+
if !verbsMatch {
447+
fmt.Fprintf(out, "Warning: allowed verbs (%s) on claim for %s on APIBinding %s do not match expected verbs (%s).\n", strings.Join(verbsActual.UnsortedList(), ","), exportedClaim.String(), binding.Name, strings.Join(verbsExpected.UnsortedList(), ","))
448+
}
441449
}
442450
}
443451
return nil

config/crds/apis.kcp.io_apibindings.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,9 +535,20 @@ spec:
535535
- Accepted
536536
- Rejected
537537
type: string
538+
verbs:
539+
description: |-
540+
verbs is a list of supported API operation types (this includes
541+
but is not limited to get, list, watch, create, update, patch,
542+
delete, deletecollection, and proxy).
543+
items:
544+
type: string
545+
minItems: 1
546+
type: array
547+
x-kubernetes-list-type: set
538548
required:
539549
- resource
540550
- state
551+
- verbs
541552
type: object
542553
x-kubernetes-validations:
543554
- message: either "all" or "resourceSelector" must be set
@@ -641,8 +652,19 @@ spec:
641652
- message: at least one field must be set
642653
rule: has(self.__namespace__) || has(self.name)
643654
type: array
655+
verbs:
656+
description: |-
657+
verbs is a list of supported API operation types (this includes
658+
but is not limited to get, list, watch, create, update, patch,
659+
delete, deletecollection, and proxy).
660+
items:
661+
type: string
662+
minItems: 1
663+
type: array
664+
x-kubernetes-list-type: set
644665
required:
645666
- resource
667+
- verbs
646668
type: object
647669
x-kubernetes-validations:
648670
- message: either "all" or "resourceSelector" must be set
@@ -821,8 +843,19 @@ spec:
821843
- message: at least one field must be set
822844
rule: has(self.__namespace__) || has(self.name)
823845
type: array
846+
verbs:
847+
description: |-
848+
verbs is a list of supported API operation types (this includes
849+
but is not limited to get, list, watch, create, update, patch,
850+
delete, deletecollection, and proxy).
851+
items:
852+
type: string
853+
minItems: 1
854+
type: array
855+
x-kubernetes-list-type: set
824856
required:
825857
- resource
858+
- verbs
826859
type: object
827860
x-kubernetes-validations:
828861
- message: either "all" or "resourceSelector" must be set

config/crds/apis.kcp.io_apiexports.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,19 @@ spec:
443443
- message: at least one field must be set
444444
rule: has(self.__namespace__) || has(self.name)
445445
type: array
446+
verbs:
447+
description: |-
448+
verbs is a list of supported API operation types (this includes
449+
but is not limited to get, list, watch, create, update, patch,
450+
delete, deletecollection, and proxy).
451+
items:
452+
type: string
453+
minItems: 1
454+
type: array
455+
x-kubernetes-list-type: set
446456
required:
447457
- resource
458+
- verbs
448459
type: object
449460
x-kubernetes-validations:
450461
- message: either "all" or "resourceSelector" must be set

pkg/admission/apibinding/apibinding_admission.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package apibinding
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"errors"
2223
"fmt"
2324
"io"
@@ -92,6 +93,13 @@ func (o *apiBindingAdmission) Admit(ctx context.Context, a admission.Attributes,
9293
return apierrors.NewInternalError(err)
9394
}
9495

96+
if a.GetResource().GroupResource() == apisv1alpha1.Resource("apibindings") {
97+
ab := &apisv1alpha1.APIBinding{}
98+
if err := validateOverhangingPermissionClaims(ctx, a, ab); err != nil {
99+
return admission.NewForbidden(a, err)
100+
}
101+
}
102+
95103
if a.GetResource().GroupResource() != apisv1alpha2.Resource("apibindings") {
96104
return nil
97105
}
@@ -312,3 +320,43 @@ func (o *apiBindingAdmission) SetKcpInformers(local, global kcpinformers.SharedI
312320
indexers.ByLogicalClusterPathAndName: indexers.IndexByLogicalClusterPathAndName,
313321
})
314322
}
323+
324+
func validateOverhangingPermissionClaims(_ context.Context, _ admission.Attributes, ab *apisv1alpha1.APIBinding) error {
325+
// TODO(xmudrii): Remove this once we are sure that all APIExport objects are
326+
// converted to v1alpha2.
327+
if _, ok := ab.Annotations[apisv1alpha2.PermissionClaimsAnnotation]; ok {
328+
// validate if we can decode overhanging permission claims. If not, we will fail.
329+
var overhanging []apisv1alpha2.PermissionClaim
330+
if err := json.Unmarshal([]byte(ab.Annotations[apisv1alpha2.PermissionClaimsAnnotation]), &overhanging); err != nil {
331+
return field.Invalid(field.NewPath("metadata").Child("annotations").Key(apisv1alpha2.PermissionClaimsAnnotation), ab.Annotations[apisv1alpha2.PermissionClaimsAnnotation], "failed to decode overhanging permission claims")
332+
}
333+
334+
// validate mismatches. We could have mismatches between the spec and the annotation
335+
// (e.g. a resource present in the annotation, but not in the spec).
336+
// We convert to v2 to check for mismatches.
337+
v2Claims := make([]apisv1alpha2.PermissionClaim, len(ab.Spec.PermissionClaims))
338+
for i, v1pc := range ab.Spec.PermissionClaims {
339+
var v2pc apisv1alpha2.PermissionClaim
340+
err := apisv1alpha2.Convert_v1alpha1_PermissionClaim_To_v1alpha2_PermissionClaim(&v1pc.PermissionClaim, &v2pc, nil)
341+
if err != nil {
342+
return field.Invalid(field.NewPath("spec").Child("permissionClaims").Index(i), ab.Spec.PermissionClaims, "failed to convert spec.PermissionClaims")
343+
}
344+
v2Claims = append(v2Claims, v2pc)
345+
}
346+
347+
for _, o := range overhanging {
348+
var found bool
349+
for _, pc := range v2Claims {
350+
if pc.Equal(o) {
351+
found = true
352+
353+
break
354+
}
355+
}
356+
if !found {
357+
return field.Invalid(field.NewPath("metadata").Child("annotations").Key(apisv1alpha2.PermissionClaimsAnnotation), ab.Annotations[apisv1alpha2.PermissionClaimsAnnotation], "permission claims defined in annotation do not match permission claims defined in spec")
358+
}
359+
}
360+
}
361+
return nil
362+
}

pkg/admission/apibinding/apibinding_admission_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package apibinding
1919
import (
2020
"context"
2121
"crypto/sha256"
22+
"encoding/json"
2223
"errors"
2324
"math/big"
2425
"strings"
@@ -555,3 +556,154 @@ func newExport(path logicalcluster.Path, name string) apiExportBuilder {
555556
},
556557
}}
557558
}
559+
560+
func TestValidateOverhangingPermissionClaims(t *testing.T) {
561+
tests := map[string]struct {
562+
annotations func() map[string]string
563+
permissionClaims []apisv1alpha1.AcceptablePermissionClaim
564+
expectedError string
565+
}{
566+
"NoAnnotations": {
567+
annotations: func() map[string]string { return nil },
568+
permissionClaims: nil,
569+
expectedError: "",
570+
},
571+
"EmptyJSON": {
572+
annotations: func() map[string]string {
573+
pc := apisv1alpha2.PermissionClaim{}
574+
data, err := json.Marshal(pc)
575+
if err != nil {
576+
t.Fatalf("failed to marshal: %v", err)
577+
}
578+
return map[string]string{
579+
apisv1alpha2.PermissionClaimsAnnotation: string(data),
580+
}
581+
},
582+
permissionClaims: nil,
583+
expectedError: "failed to decode overhanging permission claims",
584+
},
585+
"EmptyPermissionClaimsAndAnnotation": {
586+
annotations: func() map[string]string {
587+
return map[string]string{
588+
apisv1alpha2.PermissionClaimsAnnotation: "[]",
589+
}
590+
},
591+
permissionClaims: []apisv1alpha1.AcceptablePermissionClaim{},
592+
expectedError: "",
593+
},
594+
"ValidJSON": {
595+
annotations: func() map[string]string {
596+
s := []apisv1alpha2.PermissionClaim{{
597+
GroupResource: apisv1alpha2.GroupResource{
598+
Group: "foo",
599+
Resource: "bar",
600+
},
601+
All: true,
602+
IdentityHash: "baz",
603+
Verbs: []string{"get", "list"},
604+
}}
605+
data, err := json.Marshal(s)
606+
if err != nil {
607+
t.Fatalf("failed to marshal: %v", err)
608+
}
609+
return map[string]string{
610+
apisv1alpha2.ResourceSchemasAnnotation: string(data),
611+
}
612+
},
613+
permissionClaims: []apisv1alpha1.AcceptablePermissionClaim{
614+
{
615+
PermissionClaim: apisv1alpha1.PermissionClaim{
616+
GroupResource: apisv1alpha1.GroupResource{
617+
Group: "foo",
618+
Resource: "bar",
619+
},
620+
All: true,
621+
IdentityHash: "baz",
622+
},
623+
},
624+
},
625+
expectedError: "",
626+
},
627+
"MismatchInAnnotations": {
628+
annotations: func() map[string]string {
629+
s := []apisv1alpha2.PermissionClaim{
630+
{
631+
GroupResource: apisv1alpha2.GroupResource{
632+
Group: "foo",
633+
Resource: "bar",
634+
},
635+
All: true,
636+
IdentityHash: "baz",
637+
Verbs: []string{"get", "list"},
638+
},
639+
{
640+
GroupResource: apisv1alpha2.GroupResource{
641+
Group: "foo",
642+
Resource: "baz",
643+
},
644+
All: true,
645+
IdentityHash: "bar",
646+
Verbs: []string{"get"},
647+
},
648+
}
649+
data, err := json.Marshal(s)
650+
if err != nil {
651+
t.Fatalf("failed to marshal: %v", err)
652+
}
653+
return map[string]string{
654+
apisv1alpha2.PermissionClaimsAnnotation: string(data),
655+
}
656+
},
657+
permissionClaims: []apisv1alpha1.AcceptablePermissionClaim{
658+
{
659+
PermissionClaim: apisv1alpha1.PermissionClaim{
660+
GroupResource: apisv1alpha1.GroupResource{
661+
Group: "foo",
662+
Resource: "bar",
663+
},
664+
All: true,
665+
IdentityHash: "baz",
666+
},
667+
},
668+
{
669+
PermissionClaim: apisv1alpha1.PermissionClaim{
670+
GroupResource: apisv1alpha1.GroupResource{
671+
Group: "test",
672+
Resource: "schema",
673+
},
674+
All: true,
675+
IdentityHash: "random",
676+
},
677+
},
678+
},
679+
expectedError: "permission claims defined in annotation do not match permission claims defined in spec",
680+
},
681+
"InvalidJSON": {
682+
annotations: func() map[string]string {
683+
return map[string]string{
684+
apisv1alpha2.PermissionClaimsAnnotation: "invalid json",
685+
}
686+
},
687+
permissionClaims: nil,
688+
expectedError: "failed to decode overhanging permission claims",
689+
},
690+
}
691+
for name, tc := range tests {
692+
t.Run(name, func(t *testing.T) {
693+
ae := &apisv1alpha1.APIBinding{
694+
ObjectMeta: metav1.ObjectMeta{
695+
Annotations: tc.annotations(),
696+
},
697+
Spec: apisv1alpha1.APIBindingSpec{
698+
PermissionClaims: tc.permissionClaims,
699+
},
700+
}
701+
err := validateOverhangingPermissionClaims(context.TODO(), nil, ae)
702+
if tc.expectedError == "" {
703+
require.NoError(t, err)
704+
} else {
705+
require.Contains(t, err.Error(), tc.expectedError)
706+
}
707+
})
708+
}
709+
}

0 commit comments

Comments
 (0)