Skip to content

Commit 3817fa0

Browse files
committed
Implement support for verbs in PermissionClaims
Signed-off-by: Marko Mudrinić <[email protected]> On-behalf-of: @SAP [email protected]
1 parent d1c4645 commit 3817fa0

24 files changed

+1205
-252
lines changed

cli/pkg/workspace/plugin/use.go

+9-1
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"
@@ -442,20 +443,27 @@ func getAPIBindings(ctx context.Context, kcpClusterClient kcpclientset.ClusterIn
442443
func findUnresolvedPermissionClaims(out io.Writer, apiBindings []apisv1alpha2.APIBinding) error {
443444
for _, binding := range apiBindings {
444445
for _, exportedClaim := range binding.Status.ExportPermissionClaims {
445-
var found, ack bool
446+
var found, ack, verbsMatch bool
447+
var verbsExpected, verbsActual sets.Set[string]
446448
for _, specClaim := range binding.Spec.PermissionClaims {
447449
if !exportedClaim.Equal(specClaim.PermissionClaim) {
448450
continue
449451
}
450452
found = true
451453
ack = (specClaim.State == apisv1alpha2.ClaimAccepted) || specClaim.State == apisv1alpha2.ClaimRejected
454+
verbsExpected = sets.New(exportedClaim.Verbs...)
455+
verbsActual = sets.New(specClaim.Verbs...)
456+
verbsMatch = verbsActual.Difference(verbsExpected).Len() == 0 && verbsExpected.Difference(verbsActual).Len() == 0
452457
}
453458
if !found {
454459
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)
455460
}
456461
if !ack {
457462
fmt.Fprintf(out, "Warning: claim for %s specified on APIBinding %s but not accepted or rejected.\n", exportedClaim.String(), binding.Name)
458463
}
464+
if !verbsMatch {
465+
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(), ","))
466+
}
459467
}
460468
}
461469
return nil

config/crds/apis.kcp.io_apibindings.yaml

+33
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

+11
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/openapi/zz_generated.openapi.go

+42-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/virtual/apiexport/authorizer/binding.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import (
2222
"slices"
2323
"strings"
2424

25+
kerrors "k8s.io/apimachinery/pkg/api/errors"
2526
"k8s.io/apimachinery/pkg/labels"
27+
"k8s.io/apimachinery/pkg/util/sets"
2628
"k8s.io/apiserver/pkg/authorization/authorizer"
2729
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
2830

@@ -35,18 +37,25 @@ import (
3537
)
3638

3739
type boundAPIAuthorizer struct {
40+
getAPIExport func(clusterName, apiExportName string) (*apisv1alpha2.APIExport, error)
3841
getAPIBindingByExport func(clusterName, apiExportName, apiExportCluster string) (*apisv1alpha2.APIBinding, error)
3942

4043
delegate authorizer.Authorizer
4144
}
4245

46+
const wildcardVerb = "*"
47+
4348
var readOnlyVerbs = []string{"get", "list", "watch"}
4449

45-
func NewBoundAPIAuthorizer(delegate authorizer.Authorizer, apiBindingInformer apisv1alpha2informers.APIBindingClusterInformer, kubeClusterClient kcpkubernetesclientset.ClusterInterface) authorizer.Authorizer {
50+
func NewBoundAPIAuthorizer(delegate authorizer.Authorizer, apiBindingInformer apisv1alpha2informers.APIBindingClusterInformer, apiExportInformer apisv1alpha2informers.APIExportClusterInformer, kubeClusterClient kcpkubernetesclientset.ClusterInterface) authorizer.Authorizer {
51+
apiExportLister := apiExportInformer.Lister()
4652
apiBindingLister := apiBindingInformer.Lister()
4753

4854
return &boundAPIAuthorizer{
4955
delegate: delegate,
56+
getAPIExport: func(clusterName, apiExportName string) (*apisv1alpha2.APIExport, error) {
57+
return apiExportLister.Cluster(logicalcluster.Name(clusterName)).Get(apiExportName)
58+
},
5059
getAPIBindingByExport: func(clusterName, apiExportName, apiExportCluster string) (*apisv1alpha2.APIBinding, error) {
5160
bindings, err := apiBindingLister.Cluster(logicalcluster.Name(clusterName)).List(labels.Everything())
5261
if err != nil {
@@ -87,6 +96,14 @@ func (a *boundAPIAuthorizer) Authorize(ctx context.Context, attr authorizer.Attr
8796
}
8897
apiExportCluster, apiExportName := parts[0], parts[1]
8998

99+
apiExport, err := a.getAPIExport(apiExportCluster, apiExportName)
100+
if kerrors.IsNotFound(err) {
101+
return authorizer.DecisionNoOpinion, "", fmt.Errorf("API export not found: %w", err)
102+
}
103+
if err != nil {
104+
return authorizer.DecisionNoOpinion, "", err
105+
}
106+
90107
apiBinding, err := a.getAPIBindingByExport(targetCluster.Name.String(), apiExportName, apiExportCluster)
91108
if err != nil {
92109
return authorizer.DecisionDeny, "could not find suitable APIBinding in target logical cluster", nil //nolint:nilerr // this is on purpose, we want to deny, not return a server error
@@ -99,14 +116,32 @@ func (a *boundAPIAuthorizer) Authorize(ctx context.Context, attr authorizer.Attr
99116
}
100117
}
101118

102-
// check if a resource claim for this resource has been accepted.
119+
// check if a resource claim for this resource has been accepted and has correct verbs.
103120
for _, permissionClaim := range apiBinding.Spec.PermissionClaims {
104121
if permissionClaim.State != apisv1alpha2.ClaimAccepted {
105122
// if the claim is not accepted it cannot be used.
106123
continue
107124
}
108125

109126
if permissionClaim.Group == attr.GetAPIGroup() && permissionClaim.Resource == attr.GetResource() {
127+
apiBindingVerbs := sets.New(permissionClaim.Verbs...)
128+
apiExportVerbs := sets.New[string]()
129+
130+
for _, exportPermpermissionClaim := range apiExport.Spec.PermissionClaims {
131+
if exportPermpermissionClaim.Equal(permissionClaim.PermissionClaim) {
132+
apiExportVerbs.Insert(exportPermpermissionClaim.Verbs...)
133+
134+
break
135+
}
136+
}
137+
138+
allowedVerbs := apiBindingVerbs.Intersection(apiExportVerbs)
139+
140+
if !allowedVerbs.HasAny(attr.GetVerb(), wildcardVerb) {
141+
// if the requested verb is not found, the claim cannot be used.
142+
continue
143+
}
144+
110145
return a.delegate.Authorize(ctx, attr)
111146
}
112147
}

0 commit comments

Comments
 (0)