Skip to content

Commit

Permalink
Fix gap in APIExport virtual workspace queues
Browse files Browse the repository at this point in the history
Fix a gap in the APIExport virtual workspace queueing where it was
possible to incorrectly return a "not found" error when trying to list a
claimed resource. The gap was that we weren't queueing the dependent
APIExports; i.e., if APIExport A gets updated, and APIExport B has a
permission claim on A's identity, we need to make sure to queue B in
this situation.

Signed-off-by: Andy Goldstein <andy.goldstein@redhat.com>
  • Loading branch information
ncdc committed Jan 31, 2023
1 parent 6124312 commit c359931
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 17 deletions.
29 changes: 18 additions & 11 deletions pkg/indexers/apiexport.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ limitations under the License.
package indexers

import (
"fmt"

kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache"
"github.com/kcp-dev/logicalcluster/v3"

"k8s.io/apimachinery/pkg/util/sets"

apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1"
)

Expand All @@ -30,25 +30,21 @@ const (
APIExportByIdentity = "APIExportByIdentity"
// APIExportBySecret is the indexer name for retrieving APIExports by secret.
APIExportBySecret = "APIExportSecret"
// APIExportByClaimedIdentities is the indexer name for retrieving APIExports that have a permission claim for a
// particular identity hash.
APIExportByClaimedIdentities = "APIExportByClaimedIdentities"
)

// IndexAPIExportByIdentity is an index function that indexes an APIExport by its identity hash.
func IndexAPIExportByIdentity(obj interface{}) ([]string, error) {
apiExport, ok := obj.(*apisv1alpha1.APIExport)
if !ok {
return []string{}, fmt.Errorf("obj %T is not an APIExport", obj)
}

apiExport := obj.(*apisv1alpha1.APIExport)
return []string{apiExport.Status.IdentityHash}, nil
}

// IndexAPIExportBySecret is an index function that indexes an APIExport by its identity secret references. Index values
// are of the form <cluster name>|<secret reference namespace>/<secret reference name> (cache keys).
func IndexAPIExportBySecret(obj interface{}) ([]string, error) {
apiExport, ok := obj.(*apisv1alpha1.APIExport)
if !ok {
return []string{}, fmt.Errorf("obj %T is not an APIExport", obj)
}
apiExport := obj.(*apisv1alpha1.APIExport)

if apiExport.Spec.Identity == nil {
return []string{}, nil
Expand All @@ -65,3 +61,14 @@ func IndexAPIExportBySecret(obj interface{}) ([]string, error) {

return []string{kcpcache.ToClusterAwareKey(logicalcluster.From(apiExport).String(), ref.Namespace, ref.Name)}, nil
}

// IndexAPIExportByClaimedIdentities is an index function that indexes an APIExport by its permission claims' identity
// hashes.
func IndexAPIExportByClaimedIdentities(obj interface{}) ([]string, error) {
apiExport := obj.(*apisv1alpha1.APIExport)
claimedIdentities := sets.NewString()
for _, claim := range apiExport.Spec.PermissionClaims {
claimedIdentities.Insert(claim.IdentityHash)
}
return claimedIdentities.List(), nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ func NewAPIReconciler(
indexers.AddIfNotPresentOrDie(
apiExportInformer.Informer().GetIndexer(),
cache.Indexers{
indexers.APIExportByIdentity: indexers.IndexAPIExportByIdentity,
indexers.APIExportByIdentity: indexers.IndexAPIExportByIdentity,
indexers.APIExportByClaimedIdentities: indexers.IndexAPIExportByClaimedIdentities,
},
)

Expand Down Expand Up @@ -174,6 +175,25 @@ func (c *APIReconciler) enqueueAPIExport(apiExport *apisv1alpha1.APIExport, logg
}
logging.WithQueueKey(logger, key).V(2).Info("queueing APIExport")
c.queue.Add(key)

if apiExport.Status.IdentityHash != "" {
logger.V(4).Info("looking for APIExports to queue that have claims against this identity", "identity", apiExport.Status.IdentityHash)
others, err := indexers.ByIndex[*apisv1alpha1.APIExport](c.apiExportIndexer, indexers.APIExportByClaimedIdentities, apiExport.Status.IdentityHash)
if err != nil {
logger.Error(err, "error getting APIExports for claimed identity", "identity", apiExport.Status.IdentityHash)
return
}
logger.V(4).Info("got APIExports", "identity", apiExport.Status.IdentityHash, "count", len(others))
for _, other := range others {
key, err := kcpcache.MetaClusterNamespaceKeyFunc(other)
if err != nil {
logger.Error(err, "error getting key!")
continue
}
logging.WithQueueKey(logger, key).V(2).Info("queueing APIExport for claim")
c.queue.Add(key)
}
}
}

func (c *APIReconciler) startWorker(ctx context.Context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1"
"github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1/permissionclaims"
"github.com/kcp-dev/kcp/pkg/indexers"
"github.com/kcp-dev/kcp/pkg/logging"
"github.com/kcp-dev/kcp/pkg/virtual/apiexport/schemas"
apiexportbuiltin "github.com/kcp-dev/kcp/pkg/virtual/apiexport/schemas/builtin"
"github.com/kcp-dev/kcp/pkg/virtual/framework/dynamic/apidefinition"
Expand Down Expand Up @@ -82,6 +83,9 @@ func (c *APIReconciler) reconcile(ctx context.Context, apiExport *apisv1alpha1.A
claims := map[schema.GroupResource]apisv1alpha1.PermissionClaim{}
claimsAPIBindings := false
for _, pc := range apiExport.Spec.PermissionClaims {
logger := logger.WithValues("claim", pc.String())
logger.V(4).Info("evaluating claim")

// APIExport resources have priority over claimed resources
gr := schema.GroupResource{Group: pc.Group, Resource: pc.Resource}
if _, found := apiResourceSchemas[gr]; found {
Expand Down Expand Up @@ -131,11 +135,16 @@ func (c *APIReconciler) reconcile(ctx context.Context, apiExport *apisv1alpha1.A
continue
}

logger = logger.WithValues("identity", pc.IdentityHash)

logger.V(4).Info("getting APIExports by identity")
exports, err := c.apiExportIndexer.ByIndex(indexers.APIExportByIdentity, pc.IdentityHash)
if err != nil {
return err
}

logger.V(4).Info("got APIExports", "count", len(exports))

// there might be multiple exports with the same identity hash all exporting the same GR.
// This is fine. Same identity means same owner. They have to ensure the schemas are compatible.
// The kcp server resource handlers will make sure the right structural schemas are applied. Here,
Expand All @@ -148,14 +157,22 @@ func (c *APIReconciler) reconcile(ctx context.Context, apiExport *apisv1alpha1.A

for _, obj := range exports {
export := obj.(*apisv1alpha1.APIExport)
logger := logger.WithValues(logging.FromPrefix("candidateAPIExport", export)...)
logger.V(4).Info("getting APIResourceSchemas for candidate APIExport")
candidates, err := c.getSchemasFromAPIExport(ctx, export)
if err != nil {
return err
}
logger.V(4).Info("got APIResourceSchemas for candidate APIExport", "count", len(candidates))
for _, apiResourceSchema := range candidates {
logger := logger.WithValues(logging.FromPrefix("candidateAPIResourceSchema", apiResourceSchema)...)
logger = logger.WithValues("candidateGroup", apiResourceSchema.Spec.Group, "candidateResource", apiResourceSchema.Spec.Names.Plural)
logger.V(4).Info("evaluating candidate APIResourceSchema")
if apiResourceSchema.Spec.Group != pc.Group || apiResourceSchema.Spec.Names.Plural != pc.Resource {
logger.V(4).Info("not a match")
continue
}
logger.V(4).Info("got a match!")
apiResourceSchemas[gr] = apiResourceSchema
identities[gr] = pc.IdentityHash
claims[gr] = pc
Expand Down
12 changes: 7 additions & 5 deletions test/e2e/virtual/apiexport/authorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,13 @@ metadata:
require.NoError(t, err)

t.Logf("verify that service-provider-2-admin cannot list sherrifs resources via virtual apiexport apiserver because we have no local maximal permissions yet granted")
_, err = user2DynamicVWClient.Resource(schema.GroupVersionResource{Version: "v1", Resource: "sheriffs", Group: "wild.wild.west"}).List(ctx, metav1.ListOptions{})
require.ErrorContains(
t, err,
`sheriffs.wild.wild.west is forbidden: User "service-provider-2-admin" cannot list resource "sheriffs" in API group "wild.wild.west" at the cluster scope: access denied`,
"service-provider-2-admin must not be allowed to list sheriff resources")
framework.Eventually(t, func() (success bool, reason string) {
_, err = user2DynamicVWClient.Resource(schema.GroupVersionResource{Version: "v1", Resource: "sheriffs", Group: "wild.wild.west"}).List(ctx, metav1.ListOptions{})
if err == nil {
return false, "expected an error but got nil"
}
return strings.Contains(err.Error(), `sheriffs.wild.wild.west is forbidden: User "service-provider-2-admin" cannot list resource "sheriffs" in API group "wild.wild.west" at the cluster scope: access denied`), fmt.Sprintf("unexpected error: %v", err)
}, wait.ForeverTestTimeout, 100*time.Millisecond, "service-provider-2-admin must not be allowed to list sheriff resources")

_, err = user2DynamicVWClient.Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}).List(ctx, metav1.ListOptions{})
require.NoError(t, err, "service-provider-2-admin must be allowed to list native types")
Expand Down

0 comments on commit c359931

Please sign in to comment.