From 947081390ac0236f360624afbf8c495688fa1dc5 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Tue, 10 Mar 2026 12:13:14 +0100 Subject: [PATCH 1/9] feat: introduce apiexportpolicy resource On-behalf-of: SAP aleh.yarshou@sap.com --- .../v1alpha1/apiexportpolicy_types.go | 62 ++++ .../v1alpha1/groupversion_info.go | 20 ++ .../v1alpha1/zz_generated.deepcopy.go | 132 ++++++++ cmd/model_generator.go | 2 +- cmd/operator.go | 175 ++++++---- ...on.platform-mesh.io_apiexportpolicies.yaml | 126 +++++++ ...export-authorization.platform-mesh.io.yaml | 13 + ...licies.authorization.platform-mesh.io.yaml | 123 +++++++ ...uthorization_v1alpha1_apiexportpolicy.yaml | 18 + internal/config/config.go | 57 ++-- internal/config/config_test.go | 2 +- .../controller/apiexportpolicy_controller.go | 44 +++ internal/subroutine/apiexportpolicy.go | 310 ++++++++++++++++++ 13 files changed, 993 insertions(+), 91 deletions(-) create mode 100644 api/authorization/v1alpha1/apiexportpolicy_types.go create mode 100644 api/authorization/v1alpha1/groupversion_info.go create mode 100644 api/authorization/v1alpha1/zz_generated.deepcopy.go create mode 100644 config/crd/bases/authorization.platform-mesh.io_apiexportpolicies.yaml create mode 100644 config/resources/apiexport-authorization.platform-mesh.io.yaml create mode 100644 config/resources/apiresourceschema-apiexportpolicies.authorization.platform-mesh.io.yaml create mode 100644 config/samples/authorization_v1alpha1_apiexportpolicy.yaml create mode 100644 internal/controller/apiexportpolicy_controller.go create mode 100644 internal/subroutine/apiexportpolicy.go diff --git a/api/authorization/v1alpha1/apiexportpolicy_types.go b/api/authorization/v1alpha1/apiexportpolicy_types.go new file mode 100644 index 00000000..1272dd74 --- /dev/null +++ b/api/authorization/v1alpha1/apiexportpolicy_types.go @@ -0,0 +1,62 @@ +package v1alpha1 + +import ( + lifecycleapi "github.com/platform-mesh/golang-commons/controller/lifecycle/api" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type APIExportRef struct { + Name string `json:"name"` + ClusterPath string `json:"clusterName"` +} + +type APIExportPolicySpec struct { + APIExportRef APIExportRef `json:"apiExportRef"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + AllowPathExpressions []string `json:"allowPathExpressions"` +} + +type APIExportPolicyStatus struct { + Conditions []metav1.Condition `json:"conditions,omitempty"` + ManagedAllowExpressions []string `json:"managedAllowExpressions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster + +type APIExportPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec APIExportPolicySpec `json:"spec,omitempty"` + Status APIExportPolicyStatus `json:"status,omitempty"` +} + +// GetConditions implements lifecycle.RuntimeObjectConditions. +func (in *APIExportPolicy) GetConditions() []metav1.Condition { + return in.Status.Conditions +} + +// SetConditions implements lifecycle.RuntimeObjectConditions. +func (in *APIExportPolicy) SetConditions(conditions []metav1.Condition) { + in.Status.Conditions = conditions +} + +var _ lifecycleapi.RuntimeObjectConditions = &APIExportPolicy{} + +// +kubebuilder:object:root=true + +// APIExportPolicyList contains a list of APIExportPolicy. +type APIExportPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []APIExportPolicy `json:"items"` +} + +func init() { + SchemeBuilder.Register(&APIExportPolicy{}, &APIExportPolicyList{}) +} diff --git a/api/authorization/v1alpha1/groupversion_info.go b/api/authorization/v1alpha1/groupversion_info.go new file mode 100644 index 00000000..cad71591 --- /dev/null +++ b/api/authorization/v1alpha1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1alpha1 contains API Schema definitions for the authorization v1alpha1 API group. +// +kubebuilder:object:generate=true +// +groupName=authorization.platform-mesh.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "authorization.platform-mesh.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/authorization/v1alpha1/zz_generated.deepcopy.go b/api/authorization/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..0a4d531f --- /dev/null +++ b/api/authorization/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,132 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportPolicy) DeepCopyInto(out *APIExportPolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicy. +func (in *APIExportPolicy) DeepCopy() *APIExportPolicy { + if in == nil { + return nil + } + out := new(APIExportPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIExportPolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportPolicyList) DeepCopyInto(out *APIExportPolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]APIExportPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicyList. +func (in *APIExportPolicyList) DeepCopy() *APIExportPolicyList { + if in == nil { + return nil + } + out := new(APIExportPolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIExportPolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportPolicySpec) DeepCopyInto(out *APIExportPolicySpec) { + *out = *in + out.APIExportRef = in.APIExportRef + if in.AllowPathExpressions != nil { + in, out := &in.AllowPathExpressions, &out.AllowPathExpressions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicySpec. +func (in *APIExportPolicySpec) DeepCopy() *APIExportPolicySpec { + if in == nil { + return nil + } + out := new(APIExportPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportPolicyStatus) DeepCopyInto(out *APIExportPolicyStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ManagedAllowExpressions != nil { + in, out := &in.ManagedAllowExpressions, &out.ManagedAllowExpressions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicyStatus. +func (in *APIExportPolicyStatus) DeepCopy() *APIExportPolicyStatus { + if in == nil { + return nil + } + out := new(APIExportPolicyStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportRef) DeepCopyInto(out *APIExportRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportRef. +func (in *APIExportRef) DeepCopy() *APIExportRef { + if in == nil { + return nil + } + out := new(APIExportRef) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/model_generator.go b/cmd/model_generator.go index 7dd54c38..0bef3266 100644 --- a/cmd/model_generator.go +++ b/cmd/model_generator.go @@ -70,7 +70,7 @@ var modelGeneratorCmd = &cobra.Command{ return fmt.Errorf("scheme should not be nil") } - provider, err := apiexport.New(restCfg, operatorCfg.APIExportEndpointSliceName, apiexport.Options{ + provider, err := apiexport.New(restCfg, operatorCfg.CoreAPIExportEndpointSliceName, apiexport.Options{ Scheme: mgrOpts.Scheme, }) if err != nil { diff --git a/cmd/operator.go b/cmd/operator.go index ee1c0405..03508d2b 100644 --- a/cmd/operator.go +++ b/cmd/operator.go @@ -11,11 +11,13 @@ import ( platformeshcontext "github.com/platform-mesh/golang-commons/context" "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/golang-commons/sentry" + authorizationv1alpha1 "github.com/platform-mesh/security-operator/api/authorization/v1alpha1" corev1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/controller" internalwebhook "github.com/platform-mesh/security-operator/internal/webhook" "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ctrl "sigs.k8s.io/controller-runtime" @@ -32,6 +34,7 @@ import ( "github.com/kcp-dev/logicalcluster/v3" "github.com/kcp-dev/multicluster-provider/apiexport" + pathaware "github.com/kcp-dev/multicluster-provider/path-aware" kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" kcpapisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" @@ -99,59 +102,15 @@ var operatorCmd = &cobra.Command{ defer platformeshcontext.Recover(log) } - webhookServer := webhook.NewServer(webhook.Options{ - TLSOpts: []func(*tls.Config){ - func(c *tls.Config) { - log.Info().Msg("disabling http/2") - c.NextProtos = []string{"http/1.1"} - }, - }, - CertDir: operatorCfg.Webhooks.CertDir, - Port: operatorCfg.Webhooks.Port, - }) - - mgrOpts := ctrl.Options{ - Scheme: scheme, - Metrics: metricsserver.Options{ - BindAddress: defaultCfg.Metrics.BindAddress, - TLSOpts: []func(*tls.Config){ - func(c *tls.Config) { - log.Info().Msg("disabling http/2") - c.NextProtos = []string{"http/1.1"} - }, - }, - }, - HealthProbeBindAddress: defaultCfg.HealthProbeBindAddress, - LeaderElection: defaultCfg.LeaderElectionEnabled, - LeaderElectionID: "security-operator.platform-mesh.io", - BaseContext: func() context.Context { return ctx }, - WebhookServer: webhookServer, - } - if defaultCfg.LeaderElectionEnabled { - inClusterCfg, err := rest.InClusterConfig() - if err != nil { - log.Error().Err(err).Msg("unable to create in-cluster config") - return err - } - mgrOpts.LeaderElectionConfig = inClusterCfg - } - - if mgrOpts.Scheme == nil { - log.Error().Err(fmt.Errorf("scheme should not be nil")).Msg("scheme should not be nil") - return fmt.Errorf("scheme should not be nil") - } - - provider, err := apiexport.New(restCfg, operatorCfg.APIExportEndpointSliceName, apiexport.Options{ - Scheme: mgrOpts.Scheme, - }) + coreMgr, err := setupCorePlatformMeshManager(ctx, restCfg) if err != nil { - setupLog.Error(err, "unable to construct cluster provider") + log.Error().Err(err).Msg("unable to setup main manager") return err } - mgr, err := mcmanager.New(restCfg, provider, mgrOpts) + authorizationMgr, err := setupAuthorizationPlatformMeshManager(ctx, restCfg) if err != nil { - setupLog.Error(err, "Failed to create manager") + log.Error().Err(err).Msg("unable to setup path-aware manager") return err } @@ -161,7 +120,7 @@ var operatorCmd = &cobra.Command{ return err } - orgClient, err := logicalClusterClientFromKey(mgr.GetLocalManager().GetConfig(), log)(logicalcluster.Name("root:orgs")) + orgClient, err := logicalClusterClientFromKey(coreMgr.GetLocalManager().GetConfig(), log)(logicalcluster.Name("root:orgs")) if err != nil { log.Error().Err(err).Msg("Failed to create org client") return err @@ -169,51 +128,67 @@ var operatorCmd = &cobra.Command{ fga := openfgav1.NewOpenFGAServiceClient(conn) - if err = controller.NewStoreReconciler(ctx, log, fga, mgr). - SetupWithManager(mgr, defaultCfg); err != nil { + if err = controller.NewStoreReconciler(ctx, log, fga, coreMgr). + SetupWithManager(coreMgr, defaultCfg); err != nil { log.Error().Err(err).Str("controller", "store").Msg("unable to create controller") return err } if err = controller. - NewAuthorizationModelReconciler(log, fga, mgr). - SetupWithManager(mgr, defaultCfg); err != nil { + NewAuthorizationModelReconciler(log, fga, coreMgr). + SetupWithManager(coreMgr, defaultCfg); err != nil { log.Error().Err(err).Str("controller", "authorizationmodel").Msg("unable to create controller") return err } - if err = controller.NewIdentityProviderConfigurationReconciler(ctx, mgr, orgClient, &operatorCfg, log).SetupWithManager(mgr, defaultCfg, log); err != nil { + if err = controller.NewIdentityProviderConfigurationReconciler(ctx, coreMgr, orgClient, &operatorCfg, log).SetupWithManager(coreMgr, defaultCfg, log); err != nil { log.Error().Err(err).Str("controller", "identityprovider").Msg("unable to create controller") return err } - if err = controller.NewInviteReconciler(ctx, mgr, &operatorCfg, log).SetupWithManager(mgr, defaultCfg, log); err != nil { + if err = controller.NewInviteReconciler(ctx, coreMgr, &operatorCfg, log).SetupWithManager(coreMgr, defaultCfg, log); err != nil { log.Error().Err(err).Str("controller", "invite").Msg("unable to create controller") return err } - if err = controller.NewAccountInfoReconciler(log, mgr).SetupWithManager(mgr, defaultCfg); err != nil { + if err = controller.NewAccountInfoReconciler(log, coreMgr).SetupWithManager(coreMgr, defaultCfg); err != nil { log.Error().Err(err).Str("controller", "accountinfo").Msg("unable to create controller") return err } + if err = controller.NewAPIExportPolicyReconciler(log, fga, authorizationMgr).SetupWithManager(authorizationMgr, defaultCfg); err != nil { + log.Error().Err(err).Str("controller", "apiexportpolicy").Msg("unable to create controller") + return err + } + if operatorCfg.Webhooks.Enabled { log.Info().Msg("validating webhooks are enabled") - if err := internalwebhook.SetupIdentityProviderConfigurationValidatingWebhookWithManager(ctx, mgr.GetLocalManager(), &operatorCfg); err != nil { + if err := internalwebhook.SetupIdentityProviderConfigurationValidatingWebhookWithManager(ctx, coreMgr.GetLocalManager(), &operatorCfg); err != nil { log.Error().Err(err).Str("webhook", "IdentityProviderConfiguration").Msg("unable to create webhook") return err } } // +kubebuilder:scaffold:builder - if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + if err := coreMgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { log.Error().Err(err).Msg("unable to set up health check") return err } - if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + if err := coreMgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { log.Error().Err(err).Msg("unable to set up ready check") return err } - setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { - log.Error().Err(err).Msg("problem running manager") + g, gctx := errgroup.WithContext(ctx) + + g.Go(func() error { + setupLog.Info("starting core manager") + return coreMgr.Start(gctx) + }) + + g.Go(func() error { + setupLog.Info("starting authorization manager") + return authorizationMgr.Start(gctx) + }) + + if err := g.Wait(); err != nil { + log.Error().Err(err).Msg("failed to run managers") return err } return nil @@ -266,9 +241,85 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(kcptenancyv1alpha1.AddToScheme(scheme)) utilruntime.Must(corev1alpha1.AddToScheme(scheme)) + utilruntime.Must(authorizationv1alpha1.AddToScheme(scheme)) utilruntime.Must(kcpapisv1alpha1.AddToScheme(scheme)) utilruntime.Must(kcpapisv1alpha2.AddToScheme(scheme)) utilruntime.Must(kcpcorev1alpha1.AddToScheme(scheme)) utilruntime.Must(accountsv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } + +func setupCorePlatformMeshManager(ctx context.Context, restCfg *rest.Config) (mcmanager.Manager, error) { + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: []func(*tls.Config){ + func(c *tls.Config) { + c.NextProtos = []string{"http/1.1"} + }, + }, + CertDir: operatorCfg.Webhooks.CertDir, + Port: operatorCfg.Webhooks.Port, + }) + + opts := ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: defaultCfg.Metrics.BindAddress, + TLSOpts: []func(*tls.Config){ + func(c *tls.Config) { + c.NextProtos = []string{"http/1.1"} + }, + }, + }, + HealthProbeBindAddress: defaultCfg.HealthProbeBindAddress, + LeaderElection: defaultCfg.LeaderElectionEnabled, + LeaderElectionID: "security-operator.platform-mesh.io", + BaseContext: func() context.Context { return ctx }, + WebhookServer: webhookServer, + } + + if defaultCfg.LeaderElectionEnabled { + inClusterCfg, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("getting in-cluster config for leader election: %w", err) + } + opts.LeaderElectionConfig = inClusterCfg + } + + provider, err := apiexport.New(restCfg, operatorCfg.CoreAPIExportEndpointSliceName, apiexport.Options{ + Scheme: opts.Scheme, + }) + if err != nil { + return nil, fmt.Errorf("creating apiexport provider: %w", err) + } + + mgr, err := mcmanager.New(restCfg, provider, opts) + if err != nil { + return nil, fmt.Errorf("creating main manager: %w", err) + } + + return mgr, nil +} + +func setupAuthorizationPlatformMeshManager(ctx context.Context, restCfg *rest.Config) (mcmanager.Manager, error) { + provider, err := pathaware.New(restCfg, operatorCfg.AuthorizationAPIExportEndpointSliceName, apiexport.Options{ + Scheme: scheme, + }) + if err != nil { + return nil, fmt.Errorf("creating path-aware provider: %w", err) + } + + opts := ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{BindAddress: "0"}, + HealthProbeBindAddress: "0", + LeaderElection: false, + BaseContext: func() context.Context { return ctx }, + } + + mgr, err := mcmanager.New(restCfg, provider, opts) + if err != nil { + return nil, fmt.Errorf("creating path-aware manager: %w", err) + } + + return mgr, nil +} diff --git a/config/crd/bases/authorization.platform-mesh.io_apiexportpolicies.yaml b/config/crd/bases/authorization.platform-mesh.io_apiexportpolicies.yaml new file mode 100644 index 00000000..09f0fe70 --- /dev/null +++ b/config/crd/bases/authorization.platform-mesh.io_apiexportpolicies.yaml @@ -0,0 +1,126 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: apiexportpolicies.authorization.platform-mesh.io +spec: + group: authorization.platform-mesh.io + names: + kind: APIExportPolicy + listKind: APIExportPolicyList + plural: apiexportpolicies + singular: apiexportpolicy + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + allowPathExpressions: + items: + type: string + minItems: 1 + type: array + apiExportRef: + properties: + clusterName: + type: string + name: + type: string + required: + - clusterName + - name + type: object + required: + - allowPathExpressions + - apiExportRef + type: object + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + managedAllowExpressions: + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/resources/apiexport-authorization.platform-mesh.io.yaml b/config/resources/apiexport-authorization.platform-mesh.io.yaml new file mode 100644 index 00000000..54aa9a22 --- /dev/null +++ b/config/resources/apiexport-authorization.platform-mesh.io.yaml @@ -0,0 +1,13 @@ +apiVersion: apis.kcp.io/v1alpha2 +kind: APIExport +metadata: + creationTimestamp: null + name: authorization.platform-mesh.io +spec: + resources: + - group: authorization.platform-mesh.io + name: apiexportpolicies + schema: v260310-c5ff50b.apiexportpolicies.authorization.platform-mesh.io + storage: + crd: {} +status: {} diff --git a/config/resources/apiresourceschema-apiexportpolicies.authorization.platform-mesh.io.yaml b/config/resources/apiresourceschema-apiexportpolicies.authorization.platform-mesh.io.yaml new file mode 100644 index 00000000..e29fc7d8 --- /dev/null +++ b/config/resources/apiresourceschema-apiexportpolicies.authorization.platform-mesh.io.yaml @@ -0,0 +1,123 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v260310-c5ff50b.apiexportpolicies.authorization.platform-mesh.io +spec: + group: authorization.platform-mesh.io + names: + kind: APIExportPolicy + listKind: APIExportPolicyList + plural: apiexportpolicies + singular: apiexportpolicy + scope: Cluster + versions: + - name: v1alpha1 + schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + allowPathExpressions: + items: + type: string + minItems: 1 + type: array + apiExportRef: + properties: + clusterName: + type: string + name: + type: string + required: + - clusterName + - name + type: object + required: + - allowPathExpressions + - apiExportRef + type: object + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + managedAllowExpressions: + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/samples/authorization_v1alpha1_apiexportpolicy.yaml b/config/samples/authorization_v1alpha1_apiexportpolicy.yaml new file mode 100644 index 00000000..5d592f80 --- /dev/null +++ b/config/samples/authorization_v1alpha1_apiexportpolicy.yaml @@ -0,0 +1,18 @@ +apiVersion: authorization.platform-mesh.io/v1alpha1 +kind: APIExportPolicy +metadata: + name: orchestrate-platform-mesh-io + namespace: platform-mesh-system +spec: + apiExportRef: + name: orchestrate.platform-mesh.io + clusterName: root:providers:httpbin + + allowPathExpressions: + # Allow binding only for specific account and its child accounts + - :root:orgs:default:abc + - :root:orgs:default:cde + # Allow binding for all accounts in all organizations + - :root:orgs:* + # Allow binding for all accounts within demo organization + - :root:orgs:demo:* \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index 823449de..28ea2980 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,25 +58,26 @@ type IDPConfig struct { // Config struct to hold the app config type Config struct { - FGA FGAConfig - KCP KCPConfig - APIExportEndpointSliceName string - CoreModulePath string - BaseDomain string - GroupClaim string - UserClaim string - DevelopmentAllowUnverifiedEmails bool - WorkspacePath string - WorkspaceTypeName string - DomainCALookup bool - MigrateAuthorizationModels bool - HttpClientTimeoutSeconds int - SetDefaultPassword bool - AllowMemberTuplesEnabled bool - IDP IDPConfig - Keycloak KeycloakConfig - Initializer InitializerConfig - Webhooks WebhooksConfig + FGA FGAConfig + KCP KCPConfig + CoreAPIExportEndpointSliceName string + AuthorizationAPIExportEndpointSliceName string + CoreModulePath string + BaseDomain string + GroupClaim string + UserClaim string + DevelopmentAllowUnverifiedEmails bool + WorkspacePath string + WorkspaceTypeName string + DomainCALookup bool + MigrateAuthorizationModels bool + HttpClientTimeoutSeconds int + SetDefaultPassword bool + AllowMemberTuplesEnabled bool + IDP IDPConfig + Keycloak KeycloakConfig + Initializer InitializerConfig + Webhooks WebhooksConfig } func NewConfig() Config { @@ -89,13 +90,14 @@ func NewConfig() Config { KCP: KCPConfig{ Kubeconfig: "/api-kubeconfig/kubeconfig", }, - APIExportEndpointSliceName: "core.platform-mesh.io", - BaseDomain: "portal.dev.local:8443", - GroupClaim: "groups", - UserClaim: "email", - WorkspacePath: "root", - WorkspaceTypeName: "security", - HttpClientTimeoutSeconds: 30, + CoreAPIExportEndpointSliceName: "core.platform-mesh.io", + AuthorizationAPIExportEndpointSliceName: "authorization.platform-mesh.io", + BaseDomain: "portal.dev.local:8443", + GroupClaim: "groups", + UserClaim: "email", + WorkspacePath: "root", + WorkspaceTypeName: "security", + HttpClientTimeoutSeconds: 30, IDP: IDPConfig{ KubectlClientRedirectURLs: []string{"http://localhost:8000", "http://localhost:18000"}, AccessTokenLifespan: 28800, @@ -123,7 +125,8 @@ func (c *Config) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&c.FGA.ParentRelation, "fga-parent-relation", c.FGA.ParentRelation, "Set the OpenFGA parent relation name") fs.StringVar(&c.FGA.CreatorRelation, "fga-creator-relation", c.FGA.CreatorRelation, "Set the OpenFGA creator relation name") fs.StringVar(&c.KCP.Kubeconfig, "kcp-kubeconfig", c.KCP.Kubeconfig, "Set the KCP kubeconfig path") - fs.StringVar(&c.APIExportEndpointSliceName, "api-export-endpoint-slice-name", c.APIExportEndpointSliceName, "Set the APIExportEndpointSlice name") + fs.StringVar(&c.CoreAPIExportEndpointSliceName, "core-api-export-endpoint-slice-name", c.CoreAPIExportEndpointSliceName, "Set the APIExportEndpointSlice name") + fs.StringVar(&c.AuthorizationAPIExportEndpointSliceName, "authorization-api-export-endpoint-slice-name", c.AuthorizationAPIExportEndpointSliceName, "Set the Authorization APIExportEndpointSlice name") fs.StringVar(&c.CoreModulePath, "core-module-path", c.CoreModulePath, "Set the path to the core module FGA model file") fs.StringVar(&c.BaseDomain, "base-domain", c.BaseDomain, "Set the base domain used to construct issuer URLs") fs.StringVar(&c.GroupClaim, "group-claim", c.GroupClaim, "Set the ID token group claim") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ff07b7b2..ca18bd8d 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -12,7 +12,7 @@ func TestNewConfig(t *testing.T) { assert.Equal(t, "core_platform-mesh_io_account", cfg.FGA.ObjectType) assert.Equal(t, "/api-kubeconfig/kubeconfig", cfg.KCP.Kubeconfig) - assert.Equal(t, "core.platform-mesh.io", cfg.APIExportEndpointSliceName) + assert.Equal(t, "core.platform-mesh.io", cfg.CoreAPIExportEndpointSliceName) assert.Equal(t, "security-operator", cfg.Keycloak.ClientID) assert.Equal(t, 9443, cfg.Webhooks.Port) assert.Equal(t, []string{"http://localhost:8000", "http://localhost:18000"}, cfg.IDP.KubectlClientRedirectURLs) diff --git a/internal/controller/apiexportpolicy_controller.go b/internal/controller/apiexportpolicy_controller.go new file mode 100644 index 00000000..05cb5bc1 --- /dev/null +++ b/internal/controller/apiexportpolicy_controller.go @@ -0,0 +1,44 @@ +package controller + +import ( + "context" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + platformeshconfig "github.com/platform-mesh/golang-commons/config" + "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" + "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" + lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/logger" + authorizationv1alpha1 "github.com/platform-mesh/security-operator/api/authorization/v1alpha1" + "github.com/platform-mesh/security-operator/internal/subroutine" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/predicate" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" +) + +type APIExportPolicyReconciler struct { + log *logger.Logger + mclifecycle *multicluster.LifecycleManager +} + +func NewAPIExportPolicyReconciler(log *logger.Logger, fga openfgav1.OpenFGAServiceClient, mcMgr mcmanager.Manager) *APIExportPolicyReconciler { + return &APIExportPolicyReconciler{ + log: log, + mclifecycle: builder.NewBuilder("apiexportpolicy", "APIExportPolicyReconciler", []lifecyclesubroutine.Subroutine{ + subroutine.NewAPIExportPolicySubroutine(fga, mcMgr), + }, log). + WithConditionManagement(). + BuildMultiCluster(mcMgr), + } +} + +func (r *APIExportPolicyReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { + ctxWithCluster := mccontext.WithCluster(ctx, req.ClusterName) + return r.mclifecycle.Reconcile(ctxWithCluster, req, &authorizationv1alpha1.APIExportPolicy{}) +} + +func (r *APIExportPolicyReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platformeshconfig.CommonServiceConfig, evp ...predicate.Predicate) error { + return r.mclifecycle.SetupWithManager(mgr, cfg.MaxConcurrentReconciles, "apiexportpolicy", &authorizationv1alpha1.APIExportPolicy{}, cfg.DebugLabelValue, r, r.log, evp...) +} diff --git a/internal/subroutine/apiexportpolicy.go b/internal/subroutine/apiexportpolicy.go new file mode 100644 index 00000000..e576e298 --- /dev/null +++ b/internal/subroutine/apiexportpolicy.go @@ -0,0 +1,310 @@ +package subroutine + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/kcp-dev/logicalcluster/v3" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" + openfgav1 "github.com/openfga/api/proto/openfga/v1" + accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" + lifecyclecontrollerruntime "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/errors" + "github.com/platform-mesh/golang-commons/logger" + authorizationv1alpha1 "github.com/platform-mesh/security-operator/api/authorization/v1alpha1" + corev1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" + iclient "github.com/platform-mesh/security-operator/internal/client" + "github.com/platform-mesh/security-operator/pkg/fga" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" +) + +const ( + APIExportPolicyFinalizer = "authorization.platform-mesh.io/apiexportpolicy-finalizer" + orgsWorkspacePath = "root:orgs" + bindRelation = "bind" + bindInheritedRelation = "bind_inherited" +) + +type APIExportPolicySubroutine struct { + fga openfgav1.OpenFGAServiceClient + mgr mcmanager.Manager +} + +func NewAPIExportPolicySubroutine(fga openfgav1.OpenFGAServiceClient, mgr mcmanager.Manager) *APIExportPolicySubroutine { + return &APIExportPolicySubroutine{ + fga: fga, + mgr: mgr, + } +} + +var _ lifecyclesubroutine.Subroutine = &APIExportPolicySubroutine{} + +func (a *APIExportPolicySubroutine) GetName() string { + return "APIExportPolicySubroutine" +} + +func (a *APIExportPolicySubroutine) Finalizers(_ lifecyclecontrollerruntime.RuntimeObject) []string { + return []string{APIExportPolicyFinalizer} +} + +// Process reconciles the APIExportPolicy resource. +func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecyclecontrollerruntime.RuntimeObject) (ctrl.Result, errors.OperatorError) { + log := logger.LoadLoggerFromContext(ctx) + policy := instance.(*authorizationv1alpha1.APIExportPolicy) + + providerClusterID, err := a.getClusterID(ctx, policy.Spec.APIExportRef.ClusterPath) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("getting provider cluster ID for %s: %w", policy.Spec.APIExportRef.ClusterPath, err), + true, true) + } + + // Delete tuples for expressions that were removed from the spec + if err := a.deleteRemovedExpressions(ctx, policy); err != nil { + log.Error().Err(err).Msg("Failed to delete some removed expressions, continuing") + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("removing tuples for policy %s: %w", policy.Name, err), + true, true) + } + + for _, expression := range policy.Spec.AllowPathExpressions { + workspacePath, relation, err := parseAllowExpression(expression) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("parsing allow expression %s: %w", expression, err), + true, true) + } + + if workspacePath == orgsWorkspacePath { + allclient, err := iclient.NewForAllPlatformMeshResources(ctx, a.mgr.GetLocalManager().GetConfig(), a.mgr.GetLocalManager().GetScheme()) + if err != nil { + log.Fatal().Err(err).Msg("unable to create all client") + } + + var accountInfoList accountsv1alpha1.AccountInfoList + if err := allclient.List(ctx, &accountInfoList); err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("listing AccountInfo resources: %w", err), + true, true) + } + + for _, ai := range accountInfoList.Items { + if ai.Spec.Account.Type != accountsv1alpha1.AccountTypeOrg { + continue + } + + if ai.Spec.FGA.Store.Id == "" { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store id is empty in account info %w", err), true, true) + } + + tuple := corev1alpha1.Tuple{ + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + Relation: relation, + User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, policy.Spec.APIExportRef.Name), + } + + tm := fga.NewTupleManager(a.fga, ai.Spec.FGA.Store.Id, fga.AuthorizationModelIDLatest, log) + if err := tm.Apply(ctx, []corev1alpha1.Tuple{tuple}); err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("applying tuple for expression %s: %w", expression, err), + true, true) + } + } + continue + } + + lcClient, err := iclient.NewForLogicalCluster(a.mgr.GetLocalManager().GetConfig(), a.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(workspacePath)) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) + } + + var ai accountsv1alpha1.AccountInfo + if err := lcClient.Get(ctx, client.ObjectKey{Name: "account"}, &ai); err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("getting AccountInfo for workspace %s: %w", workspacePath, err), + true, true) + } + + tuple := corev1alpha1.Tuple{ + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.GeneratedClusterId, ai.Spec.Account.Name), + Relation: relation, + User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, policy.Spec.APIExportRef.Name), + } + + tm := fga.NewTupleManager(a.fga, ai.Spec.FGA.Store.Id, fga.AuthorizationModelIDLatest, log) + if err := tm.Apply(ctx, []corev1alpha1.Tuple{tuple}); err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("applying tuple for expression %s: %w", expression, err), + true, true) + } + } + + cluster, err := a.mgr.ClusterFromContext(ctx) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get cluster from context %w", err), true, true) + } + + // Update status with managed expressions + original := policy.DeepCopy() + policy.Status.ManagedAllowExpressions = policy.Spec.AllowPathExpressions + + if err := cluster.GetClient().Status().Patch(ctx, policy, client.MergeFrom(original)); err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("failed to patch APIExportPolicy status: %w", err), + true, true) + } + + log.Info().Msg("Successfully processed APIExportPolicy") + return ctrl.Result{}, nil +} + +// Finalize cleans up resources when the APIExportPolicy is deleted. +func (a *APIExportPolicySubroutine) Finalize(ctx context.Context, instance lifecyclecontrollerruntime.RuntimeObject) (ctrl.Result, errors.OperatorError) { + log := logger.LoadLoggerFromContext(ctx) + policy := instance.(*authorizationv1alpha1.APIExportPolicy) + + providerClusterID, err := a.getClusterID(ctx, policy.Spec.APIExportRef.ClusterPath) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("getting provider cluster ID for %s: %w", policy.Spec.APIExportRef.ClusterPath, err), + true, true) + } + + for _, expression := range policy.Spec.AllowPathExpressions { + err := a.deleteTuplesForExpression(ctx, expression, providerClusterID, policy.Spec.APIExportRef.Name) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError( + fmt.Errorf("deleting tuples for expression %s: %w", expression, err), + true, true) + } + } + + log.Info().Msg("Finalized APIExportPolicy") + return ctrl.Result{}, nil +} + +func (a *APIExportPolicySubroutine) getClusterID(ctx context.Context, clusterPath string) (string, error) { + // clusterPath is :root:orgs:A:B + // to get client path-aware manager might be used + // or direct client creation + cluster, err := a.mgr.GetCluster(ctx, clusterPath) + if err != nil { + return "", fmt.Errorf("getting cluster for pattern %s: %w", clusterPath, err) + } + + var lc kcpcorev1alpha1.LogicalCluster + if err := cluster.GetClient().Get(ctx, client.ObjectKey{Name: "cluster"}, &lc); err != nil { + return "", fmt.Errorf("getting logical cluster for path %s: %w", clusterPath, err) + } + + clusterID, ok := lc.Annotations["kcp.io/cluster"] + if !ok { + return "", fmt.Errorf("kcp.io/cluster annotation not found on logical cluster %s", clusterPath) + } + return clusterID, nil +} + +func parseAllowExpression(expr string) (workspacePath string, relation string, err error) { + expr = strings.TrimPrefix(expr, ":") + + if !strings.HasPrefix(expr, "root:orgs:") { + return "", "", fmt.Errorf("invalid path expression: must start with root:orgs") + } + + if strings.HasSuffix(expr, ":*") { + // Wildcard pattern, use bind_inherited relation + // Remove the trailing :* + workspacePath = strings.TrimSuffix(expr, ":*") + relation = bindInheritedRelation + return workspacePath, relation, nil + } + return expr, bindRelation, nil +} + +func (a *APIExportPolicySubroutine) deleteRemovedExpressions(ctx context.Context, policy *authorizationv1alpha1.APIExportPolicy) error { + providerClusterID, err := a.getClusterID(ctx, policy.Spec.APIExportRef.ClusterPath) + if err != nil { + return fmt.Errorf("getting provider cluster ID for %s: %w", policy.Spec.APIExportRef.ClusterPath, err) + } + + for _, managedExpr := range policy.Status.ManagedAllowExpressions { + exists := slices.Contains(policy.Spec.AllowPathExpressions, managedExpr) + if exists { + continue + } + + err := a.deleteTuplesForExpression(ctx, managedExpr, providerClusterID, policy.Spec.APIExportRef.Name) + if err != nil { + return fmt.Errorf("removing tuples for expression %s: %w", managedExpr, err) + } + } + return nil + +} + +func (a *APIExportPolicySubroutine) deleteTuplesForExpression(ctx context.Context, expression string, providerClusterID string, apiExportName string) error { + log := logger.LoadLoggerFromContext(ctx) + + workspacePath, relation, err := parseAllowExpression(expression) + if err != nil { + return fmt.Errorf("parsing expression %s: %w", expression, err) + } + + if workspacePath == orgsWorkspacePath { + allclient, err := iclient.NewForAllPlatformMeshResources(ctx, a.mgr.GetLocalManager().GetConfig(), a.mgr.GetLocalManager().GetScheme()) + if err != nil { + return fmt.Errorf("creating all-resources client: %w", err) + } + + var accountInfoList accountsv1alpha1.AccountInfoList + if err := allclient.List(ctx, &accountInfoList); err != nil { + return fmt.Errorf("listing AccountInfo resources for %s: %w", expression, err) + } + + for _, ai := range accountInfoList.Items { + if ai.Spec.FGA.Store.Id == "" { + return fmt.Errorf("empty store id in AccountInfo resources %w", err) + } + + tupleToDelete := corev1alpha1.Tuple{ + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.GeneratedClusterId, ai.Spec.Account.Name), + Relation: relation, + User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, apiExportName), + } + + tm := fga.NewTupleManager(a.fga, ai.Spec.FGA.Store.Id, fga.AuthorizationModelIDLatest, log) + if err := tm.Delete(ctx, []corev1alpha1.Tuple{tupleToDelete}); err != nil { + return fmt.Errorf("removing tuple in openFGA: %w", err) + } + } + return nil + } + + lcClient, err := iclient.NewForLogicalCluster(a.mgr.GetLocalManager().GetConfig(), a.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(workspacePath)) + if err != nil { + return fmt.Errorf("getting client for workspace %s: %w", workspacePath, err) + } + + var ai accountsv1alpha1.AccountInfo + if err := lcClient.Get(ctx, client.ObjectKey{Name: "account"}, &ai); err != nil { + return fmt.Errorf("getting AccountInfo for workspace %s: %w", workspacePath, err) + } + + tupleToDelete := corev1alpha1.Tuple{ + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.GeneratedClusterId, ai.Spec.Account.Name), + Relation: relation, + User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, apiExportName), + } + + tm := fga.NewTupleManager(a.fga, ai.Spec.FGA.Store.Id, fga.AuthorizationModelIDLatest, log) + if err := tm.Delete(ctx, []corev1alpha1.Tuple{tupleToDelete}); err != nil { + return fmt.Errorf("removing tuples: %w", err) + } + + return nil +} From 695ba5ddfdfb2a04b5f27df07352e8c920aad4c8 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Tue, 10 Mar 2026 12:54:23 +0100 Subject: [PATCH 2/9] chore: little refactoring On-behalf-of: SAP aleh.yarshou@sap.com --- .../v1alpha1/apiexportpolicy_types.go | 2 +- api/authorization/v1alpha1/groupversion_info.go | 3 ++- cmd/operator.go | 8 ++++---- internal/subroutine/apiexportpolicy.go | 14 ++++++-------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/api/authorization/v1alpha1/apiexportpolicy_types.go b/api/authorization/v1alpha1/apiexportpolicy_types.go index 1272dd74..4e14dde3 100644 --- a/api/authorization/v1alpha1/apiexportpolicy_types.go +++ b/api/authorization/v1alpha1/apiexportpolicy_types.go @@ -7,7 +7,7 @@ import ( ) type APIExportRef struct { - Name string `json:"name"` + Name string `json:"name"` ClusterPath string `json:"clusterName"` } diff --git a/api/authorization/v1alpha1/groupversion_info.go b/api/authorization/v1alpha1/groupversion_info.go index cad71591..e60af069 100644 --- a/api/authorization/v1alpha1/groupversion_info.go +++ b/api/authorization/v1alpha1/groupversion_info.go @@ -4,8 +4,9 @@ package v1alpha1 import ( - "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" + + "k8s.io/apimachinery/pkg/runtime/schema" ) var ( diff --git a/cmd/operator.go b/cmd/operator.go index 03508d2b..12f1512f 100644 --- a/cmd/operator.go +++ b/cmd/operator.go @@ -104,13 +104,13 @@ var operatorCmd = &cobra.Command{ coreMgr, err := setupCorePlatformMeshManager(ctx, restCfg) if err != nil { - log.Error().Err(err).Msg("unable to setup main manager") + setupLog.Error(err, "unable to setup core manager") return err } authorizationMgr, err := setupAuthorizationPlatformMeshManager(ctx, restCfg) if err != nil { - log.Error().Err(err).Msg("unable to setup path-aware manager") + setupLog.Error(err, "unable to setup authorization manager") return err } @@ -294,7 +294,7 @@ func setupCorePlatformMeshManager(ctx context.Context, restCfg *rest.Config) (mc mgr, err := mcmanager.New(restCfg, provider, opts) if err != nil { - return nil, fmt.Errorf("creating main manager: %w", err) + return nil, fmt.Errorf("creating core.platform-mesh.io manager: %w", err) } return mgr, nil @@ -318,7 +318,7 @@ func setupAuthorizationPlatformMeshManager(ctx context.Context, restCfg *rest.Co mgr, err := mcmanager.New(restCfg, provider, opts) if err != nil { - return nil, fmt.Errorf("creating path-aware manager: %w", err) + return nil, fmt.Errorf("creating authorization.platform-mesh.io manager: %w", err) } return mgr, nil diff --git a/internal/subroutine/apiexportpolicy.go b/internal/subroutine/apiexportpolicy.go index e576e298..009d3e2a 100644 --- a/internal/subroutine/apiexportpolicy.go +++ b/internal/subroutine/apiexportpolicy.go @@ -6,8 +6,6 @@ import ( "slices" "strings" - "github.com/kcp-dev/logicalcluster/v3" - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" openfgav1 "github.com/openfga/api/proto/openfga/v1" accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" lifecyclecontrollerruntime "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" @@ -21,6 +19,9 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "github.com/kcp-dev/logicalcluster/v3" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) const ( @@ -52,7 +53,6 @@ func (a *APIExportPolicySubroutine) Finalizers(_ lifecyclecontrollerruntime.Runt return []string{APIExportPolicyFinalizer} } -// Process reconciles the APIExportPolicy resource. func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecyclecontrollerruntime.RuntimeObject) (ctrl.Result, errors.OperatorError) { log := logger.LoadLoggerFromContext(ctx) policy := instance.(*authorizationv1alpha1.APIExportPolicy) @@ -80,6 +80,8 @@ func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecy true, true) } + // for orgs workspace we need to write 1 tuple in every store + // for this we need to get cluster id for every org's workspace if workspacePath == orgsWorkspacePath { allclient, err := iclient.NewForAllPlatformMeshResources(ctx, a.mgr.GetLocalManager().GetConfig(), a.mgr.GetLocalManager().GetScheme()) if err != nil { @@ -103,7 +105,7 @@ func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecy } tuple := corev1alpha1.Tuple{ - Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.GeneratedClusterId, ai.Spec.Account.Name), Relation: relation, User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, policy.Spec.APIExportRef.Name), } @@ -163,7 +165,6 @@ func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecy return ctrl.Result{}, nil } -// Finalize cleans up resources when the APIExportPolicy is deleted. func (a *APIExportPolicySubroutine) Finalize(ctx context.Context, instance lifecyclecontrollerruntime.RuntimeObject) (ctrl.Result, errors.OperatorError) { log := logger.LoadLoggerFromContext(ctx) policy := instance.(*authorizationv1alpha1.APIExportPolicy) @@ -189,9 +190,6 @@ func (a *APIExportPolicySubroutine) Finalize(ctx context.Context, instance lifec } func (a *APIExportPolicySubroutine) getClusterID(ctx context.Context, clusterPath string) (string, error) { - // clusterPath is :root:orgs:A:B - // to get client path-aware manager might be used - // or direct client creation cluster, err := a.mgr.GetCluster(ctx, clusterPath) if err != nil { return "", fmt.Errorf("getting cluster for pattern %s: %w", clusterPath, err) From dba9d8d10dc42cb32d4b209421e2137004ddc6c7 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Wed, 11 Mar 2026 14:40:02 +0100 Subject: [PATCH 3/9] fix: use origin cluster id from account info On-behalf-of: SAP aleh.yarshou@sap.com --- internal/subroutine/apiexportpolicy.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/subroutine/apiexportpolicy.go b/internal/subroutine/apiexportpolicy.go index 009d3e2a..a4c66f98 100644 --- a/internal/subroutine/apiexportpolicy.go +++ b/internal/subroutine/apiexportpolicy.go @@ -105,7 +105,7 @@ func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecy } tuple := corev1alpha1.Tuple{ - Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.GeneratedClusterId, ai.Spec.Account.Name), + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), Relation: relation, User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, policy.Spec.APIExportRef.Name), } @@ -133,7 +133,7 @@ func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecy } tuple := corev1alpha1.Tuple{ - Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.GeneratedClusterId, ai.Spec.Account.Name), + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), Relation: relation, User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, policy.Spec.APIExportRef.Name), } @@ -190,13 +190,13 @@ func (a *APIExportPolicySubroutine) Finalize(ctx context.Context, instance lifec } func (a *APIExportPolicySubroutine) getClusterID(ctx context.Context, clusterPath string) (string, error) { - cluster, err := a.mgr.GetCluster(ctx, clusterPath) + lcClient, err := iclient.NewForLogicalCluster(a.mgr.GetLocalManager().GetConfig(), a.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(clusterPath)) if err != nil { - return "", fmt.Errorf("getting cluster for pattern %s: %w", clusterPath, err) + return "", fmt.Errorf("getting client for workspace %s: %w", clusterPath, err) } var lc kcpcorev1alpha1.LogicalCluster - if err := cluster.GetClient().Get(ctx, client.ObjectKey{Name: "cluster"}, &lc); err != nil { + if err := lcClient.Get(ctx, client.ObjectKey{Name: "cluster"}, &lc); err != nil { return "", fmt.Errorf("getting logical cluster for path %s: %w", clusterPath, err) } @@ -270,7 +270,7 @@ func (a *APIExportPolicySubroutine) deleteTuplesForExpression(ctx context.Contex } tupleToDelete := corev1alpha1.Tuple{ - Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.GeneratedClusterId, ai.Spec.Account.Name), + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), Relation: relation, User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, apiExportName), } @@ -294,7 +294,7 @@ func (a *APIExportPolicySubroutine) deleteTuplesForExpression(ctx context.Contex } tupleToDelete := corev1alpha1.Tuple{ - Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.GeneratedClusterId, ai.Spec.Account.Name), + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), Relation: relation, User: fmt.Sprintf("apis_kcp_io_apiexport:%s/%s", providerClusterID, apiExportName), } From 0ce13a13cce5fda6d4bce353eb7962a439b87134 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Thu, 12 Mar 2026 15:18:16 +0100 Subject: [PATCH 4/9] fix: mode apiexportpolicy from authorization group to core On-behalf-of: SAP aleh.yarshou@sap.com --- .../v1alpha1/groupversion_info.go | 21 --- .../v1alpha1/zz_generated.deepcopy.go | 132 ------------------ .../v1alpha1/apiexportpolicy_types.go | 0 api/v1alpha1/zz_generated.deepcopy.go | 122 ++++++++++++++++ cmd/operator.go | 2 - ...re.platform-mesh.io_apiexportpolicies.yaml | 126 +++++++++++++++++ .../apiexport-core.platform-mesh.io.yaml | 5 + ...iexportpolicies.core.platform-mesh.io.yaml | 123 ++++++++++++++++ .../controller/apiexportpolicy_controller.go | 6 +- internal/subroutine/apiexportpolicy.go | 7 +- 10 files changed, 382 insertions(+), 162 deletions(-) delete mode 100644 api/authorization/v1alpha1/groupversion_info.go delete mode 100644 api/authorization/v1alpha1/zz_generated.deepcopy.go rename api/{authorization => }/v1alpha1/apiexportpolicy_types.go (100%) create mode 100644 config/crd/bases/core.platform-mesh.io_apiexportpolicies.yaml create mode 100644 config/resources/apiresourceschema-apiexportpolicies.core.platform-mesh.io.yaml diff --git a/api/authorization/v1alpha1/groupversion_info.go b/api/authorization/v1alpha1/groupversion_info.go deleted file mode 100644 index e60af069..00000000 --- a/api/authorization/v1alpha1/groupversion_info.go +++ /dev/null @@ -1,21 +0,0 @@ -// Package v1alpha1 contains API Schema definitions for the authorization v1alpha1 API group. -// +kubebuilder:object:generate=true -// +groupName=authorization.platform-mesh.io -package v1alpha1 - -import ( - "sigs.k8s.io/controller-runtime/pkg/scheme" - - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - // GroupVersion is group version used to register these objects. - GroupVersion = schema.GroupVersion{Group: "authorization.platform-mesh.io", Version: "v1alpha1"} - - // SchemeBuilder is used to add go types to the GroupVersionKind scheme. - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} - - // AddToScheme adds the types in this group-version to the given scheme. - AddToScheme = SchemeBuilder.AddToScheme -) diff --git a/api/authorization/v1alpha1/zz_generated.deepcopy.go b/api/authorization/v1alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 0a4d531f..00000000 --- a/api/authorization/v1alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,132 +0,0 @@ -//go:build !ignore_autogenerated - -// Code generated by controller-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIExportPolicy) DeepCopyInto(out *APIExportPolicy) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicy. -func (in *APIExportPolicy) DeepCopy() *APIExportPolicy { - if in == nil { - return nil - } - out := new(APIExportPolicy) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *APIExportPolicy) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIExportPolicyList) DeepCopyInto(out *APIExportPolicyList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]APIExportPolicy, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicyList. -func (in *APIExportPolicyList) DeepCopy() *APIExportPolicyList { - if in == nil { - return nil - } - out := new(APIExportPolicyList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *APIExportPolicyList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIExportPolicySpec) DeepCopyInto(out *APIExportPolicySpec) { - *out = *in - out.APIExportRef = in.APIExportRef - if in.AllowPathExpressions != nil { - in, out := &in.AllowPathExpressions, &out.AllowPathExpressions - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicySpec. -func (in *APIExportPolicySpec) DeepCopy() *APIExportPolicySpec { - if in == nil { - return nil - } - out := new(APIExportPolicySpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIExportPolicyStatus) DeepCopyInto(out *APIExportPolicyStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.ManagedAllowExpressions != nil { - in, out := &in.ManagedAllowExpressions, &out.ManagedAllowExpressions - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicyStatus. -func (in *APIExportPolicyStatus) DeepCopy() *APIExportPolicyStatus { - if in == nil { - return nil - } - out := new(APIExportPolicyStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIExportRef) DeepCopyInto(out *APIExportRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportRef. -func (in *APIExportRef) DeepCopy() *APIExportRef { - if in == nil { - return nil - } - out := new(APIExportRef) - in.DeepCopyInto(out) - return out -} diff --git a/api/authorization/v1alpha1/apiexportpolicy_types.go b/api/v1alpha1/apiexportpolicy_types.go similarity index 100% rename from api/authorization/v1alpha1/apiexportpolicy_types.go rename to api/v1alpha1/apiexportpolicy_types.go diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 500f4a8d..7fb7acb3 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -9,6 +9,128 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportPolicy) DeepCopyInto(out *APIExportPolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicy. +func (in *APIExportPolicy) DeepCopy() *APIExportPolicy { + if in == nil { + return nil + } + out := new(APIExportPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIExportPolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportPolicyList) DeepCopyInto(out *APIExportPolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]APIExportPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicyList. +func (in *APIExportPolicyList) DeepCopy() *APIExportPolicyList { + if in == nil { + return nil + } + out := new(APIExportPolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIExportPolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportPolicySpec) DeepCopyInto(out *APIExportPolicySpec) { + *out = *in + out.APIExportRef = in.APIExportRef + if in.AllowPathExpressions != nil { + in, out := &in.AllowPathExpressions, &out.AllowPathExpressions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicySpec. +func (in *APIExportPolicySpec) DeepCopy() *APIExportPolicySpec { + if in == nil { + return nil + } + out := new(APIExportPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportPolicyStatus) DeepCopyInto(out *APIExportPolicyStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ManagedAllowExpressions != nil { + in, out := &in.ManagedAllowExpressions, &out.ManagedAllowExpressions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportPolicyStatus. +func (in *APIExportPolicyStatus) DeepCopy() *APIExportPolicyStatus { + if in == nil { + return nil + } + out := new(APIExportPolicyStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIExportRef) DeepCopyInto(out *APIExportRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIExportRef. +func (in *APIExportRef) DeepCopy() *APIExportRef { + if in == nil { + return nil + } + out := new(APIExportRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthorizationModel) DeepCopyInto(out *AuthorizationModel) { *out = *in diff --git a/cmd/operator.go b/cmd/operator.go index 12f1512f..4b51b109 100644 --- a/cmd/operator.go +++ b/cmd/operator.go @@ -11,7 +11,6 @@ import ( platformeshcontext "github.com/platform-mesh/golang-commons/context" "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/golang-commons/sentry" - authorizationv1alpha1 "github.com/platform-mesh/security-operator/api/authorization/v1alpha1" corev1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/controller" @@ -241,7 +240,6 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(kcptenancyv1alpha1.AddToScheme(scheme)) utilruntime.Must(corev1alpha1.AddToScheme(scheme)) - utilruntime.Must(authorizationv1alpha1.AddToScheme(scheme)) utilruntime.Must(kcpapisv1alpha1.AddToScheme(scheme)) utilruntime.Must(kcpapisv1alpha2.AddToScheme(scheme)) utilruntime.Must(kcpcorev1alpha1.AddToScheme(scheme)) diff --git a/config/crd/bases/core.platform-mesh.io_apiexportpolicies.yaml b/config/crd/bases/core.platform-mesh.io_apiexportpolicies.yaml new file mode 100644 index 00000000..4d2d0bd0 --- /dev/null +++ b/config/crd/bases/core.platform-mesh.io_apiexportpolicies.yaml @@ -0,0 +1,126 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: apiexportpolicies.core.platform-mesh.io +spec: + group: core.platform-mesh.io + names: + kind: APIExportPolicy + listKind: APIExportPolicyList + plural: apiexportpolicies + singular: apiexportpolicy + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + allowPathExpressions: + items: + type: string + minItems: 1 + type: array + apiExportRef: + properties: + clusterName: + type: string + name: + type: string + required: + - clusterName + - name + type: object + required: + - allowPathExpressions + - apiExportRef + type: object + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + managedAllowExpressions: + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/resources/apiexport-core.platform-mesh.io.yaml b/config/resources/apiexport-core.platform-mesh.io.yaml index a6002c20..b0f3f03b 100644 --- a/config/resources/apiexport-core.platform-mesh.io.yaml +++ b/config/resources/apiexport-core.platform-mesh.io.yaml @@ -5,6 +5,11 @@ metadata: name: core.platform-mesh.io spec: resources: + - group: core.platform-mesh.io + name: apiexportpolicies + schema: v260312-dba9d8d.apiexportpolicies.core.platform-mesh.io + storage: + crd: {} - group: core.platform-mesh.io name: authorizationmodels schema: v260112-5925c7e.authorizationmodels.core.platform-mesh.io diff --git a/config/resources/apiresourceschema-apiexportpolicies.core.platform-mesh.io.yaml b/config/resources/apiresourceschema-apiexportpolicies.core.platform-mesh.io.yaml new file mode 100644 index 00000000..d2c2dd03 --- /dev/null +++ b/config/resources/apiresourceschema-apiexportpolicies.core.platform-mesh.io.yaml @@ -0,0 +1,123 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v260312-dba9d8d.apiexportpolicies.core.platform-mesh.io +spec: + group: core.platform-mesh.io + names: + kind: APIExportPolicy + listKind: APIExportPolicyList + plural: apiexportpolicies + singular: apiexportpolicy + scope: Cluster + versions: + - name: v1alpha1 + schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + allowPathExpressions: + items: + type: string + minItems: 1 + type: array + apiExportRef: + properties: + clusterName: + type: string + name: + type: string + required: + - clusterName + - name + type: object + required: + - allowPathExpressions + - apiExportRef + type: object + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + managedAllowExpressions: + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/controller/apiexportpolicy_controller.go b/internal/controller/apiexportpolicy_controller.go index 05cb5bc1..3de0c5b3 100644 --- a/internal/controller/apiexportpolicy_controller.go +++ b/internal/controller/apiexportpolicy_controller.go @@ -9,7 +9,7 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/logger" - authorizationv1alpha1 "github.com/platform-mesh/security-operator/api/authorization/v1alpha1" + corev1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" "github.com/platform-mesh/security-operator/internal/subroutine" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -36,9 +36,9 @@ func NewAPIExportPolicyReconciler(log *logger.Logger, fga openfgav1.OpenFGAServi func (r *APIExportPolicyReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { ctxWithCluster := mccontext.WithCluster(ctx, req.ClusterName) - return r.mclifecycle.Reconcile(ctxWithCluster, req, &authorizationv1alpha1.APIExportPolicy{}) + return r.mclifecycle.Reconcile(ctxWithCluster, req, &corev1alpha1.APIExportPolicy{}) } func (r *APIExportPolicyReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platformeshconfig.CommonServiceConfig, evp ...predicate.Predicate) error { - return r.mclifecycle.SetupWithManager(mgr, cfg.MaxConcurrentReconciles, "apiexportpolicy", &authorizationv1alpha1.APIExportPolicy{}, cfg.DebugLabelValue, r, r.log, evp...) + return r.mclifecycle.SetupWithManager(mgr, cfg.MaxConcurrentReconciles, "apiexportpolicy", &corev1alpha1.APIExportPolicy{}, cfg.DebugLabelValue, r, r.log, evp...) } diff --git a/internal/subroutine/apiexportpolicy.go b/internal/subroutine/apiexportpolicy.go index a4c66f98..0876d444 100644 --- a/internal/subroutine/apiexportpolicy.go +++ b/internal/subroutine/apiexportpolicy.go @@ -12,7 +12,6 @@ import ( lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" - authorizationv1alpha1 "github.com/platform-mesh/security-operator/api/authorization/v1alpha1" corev1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/pkg/fga" @@ -55,7 +54,7 @@ func (a *APIExportPolicySubroutine) Finalizers(_ lifecyclecontrollerruntime.Runt func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecyclecontrollerruntime.RuntimeObject) (ctrl.Result, errors.OperatorError) { log := logger.LoadLoggerFromContext(ctx) - policy := instance.(*authorizationv1alpha1.APIExportPolicy) + policy := instance.(*corev1alpha1.APIExportPolicy) providerClusterID, err := a.getClusterID(ctx, policy.Spec.APIExportRef.ClusterPath) if err != nil { @@ -167,7 +166,7 @@ func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecy func (a *APIExportPolicySubroutine) Finalize(ctx context.Context, instance lifecyclecontrollerruntime.RuntimeObject) (ctrl.Result, errors.OperatorError) { log := logger.LoadLoggerFromContext(ctx) - policy := instance.(*authorizationv1alpha1.APIExportPolicy) + policy := instance.(*corev1alpha1.APIExportPolicy) providerClusterID, err := a.getClusterID(ctx, policy.Spec.APIExportRef.ClusterPath) if err != nil { @@ -224,7 +223,7 @@ func parseAllowExpression(expr string) (workspacePath string, relation string, e return expr, bindRelation, nil } -func (a *APIExportPolicySubroutine) deleteRemovedExpressions(ctx context.Context, policy *authorizationv1alpha1.APIExportPolicy) error { +func (a *APIExportPolicySubroutine) deleteRemovedExpressions(ctx context.Context, policy *corev1alpha1.APIExportPolicy) error { providerClusterID, err := a.getClusterID(ctx, policy.Spec.APIExportRef.ClusterPath) if err != nil { return fmt.Errorf("getting provider cluster ID for %s: %w", policy.Spec.APIExportRef.ClusterPath, err) From 78f76ee31dd9c1d6654f05b72fcd108fd9d2f186 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Thu, 12 Mar 2026 15:22:26 +0100 Subject: [PATCH 5/9] chore: remove leftover from authorization group On-behalf-of: SAP aleh.yarshou@sap.com --- ...on.platform-mesh.io_apiexportpolicies.yaml | 126 ------------------ ...export-authorization.platform-mesh.io.yaml | 13 -- ...licies.authorization.platform-mesh.io.yaml | 123 ----------------- ...aml => core_v1alpha1_apiexportpolicy.yaml} | 0 4 files changed, 262 deletions(-) delete mode 100644 config/crd/bases/authorization.platform-mesh.io_apiexportpolicies.yaml delete mode 100644 config/resources/apiexport-authorization.platform-mesh.io.yaml delete mode 100644 config/resources/apiresourceschema-apiexportpolicies.authorization.platform-mesh.io.yaml rename config/samples/{authorization_v1alpha1_apiexportpolicy.yaml => core_v1alpha1_apiexportpolicy.yaml} (100%) diff --git a/config/crd/bases/authorization.platform-mesh.io_apiexportpolicies.yaml b/config/crd/bases/authorization.platform-mesh.io_apiexportpolicies.yaml deleted file mode 100644 index 09f0fe70..00000000 --- a/config/crd/bases/authorization.platform-mesh.io_apiexportpolicies.yaml +++ /dev/null @@ -1,126 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.4 - name: apiexportpolicies.authorization.platform-mesh.io -spec: - group: authorization.platform-mesh.io - names: - kind: APIExportPolicy - listKind: APIExportPolicyList - plural: apiexportpolicies - singular: apiexportpolicy - scope: Cluster - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - properties: - allowPathExpressions: - items: - type: string - minItems: 1 - type: array - apiExportRef: - properties: - clusterName: - type: string - name: - type: string - required: - - clusterName - - name - type: object - required: - - allowPathExpressions - - apiExportRef - type: object - status: - properties: - conditions: - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - managedAllowExpressions: - items: - type: string - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/resources/apiexport-authorization.platform-mesh.io.yaml b/config/resources/apiexport-authorization.platform-mesh.io.yaml deleted file mode 100644 index 54aa9a22..00000000 --- a/config/resources/apiexport-authorization.platform-mesh.io.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: apis.kcp.io/v1alpha2 -kind: APIExport -metadata: - creationTimestamp: null - name: authorization.platform-mesh.io -spec: - resources: - - group: authorization.platform-mesh.io - name: apiexportpolicies - schema: v260310-c5ff50b.apiexportpolicies.authorization.platform-mesh.io - storage: - crd: {} -status: {} diff --git a/config/resources/apiresourceschema-apiexportpolicies.authorization.platform-mesh.io.yaml b/config/resources/apiresourceschema-apiexportpolicies.authorization.platform-mesh.io.yaml deleted file mode 100644 index e29fc7d8..00000000 --- a/config/resources/apiresourceschema-apiexportpolicies.authorization.platform-mesh.io.yaml +++ /dev/null @@ -1,123 +0,0 @@ -apiVersion: apis.kcp.io/v1alpha1 -kind: APIResourceSchema -metadata: - creationTimestamp: null - name: v260310-c5ff50b.apiexportpolicies.authorization.platform-mesh.io -spec: - group: authorization.platform-mesh.io - names: - kind: APIExportPolicy - listKind: APIExportPolicyList - plural: apiexportpolicies - singular: apiexportpolicy - scope: Cluster - versions: - - name: v1alpha1 - schema: - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - properties: - allowPathExpressions: - items: - type: string - minItems: 1 - type: array - apiExportRef: - properties: - clusterName: - type: string - name: - type: string - required: - - clusterName - - name - type: object - required: - - allowPathExpressions - - apiExportRef - type: object - status: - properties: - conditions: - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - managedAllowExpressions: - items: - type: string - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/samples/authorization_v1alpha1_apiexportpolicy.yaml b/config/samples/core_v1alpha1_apiexportpolicy.yaml similarity index 100% rename from config/samples/authorization_v1alpha1_apiexportpolicy.yaml rename to config/samples/core_v1alpha1_apiexportpolicy.yaml From 545b3efbc03e59bd9f52b8fe437b7e890df969de Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Thu, 12 Mar 2026 15:36:57 +0100 Subject: [PATCH 6/9] fix: move authorization manager into a separate cobra command On-behalf-of: SAP aleh.yarshou@sap.com --- cmd/authorization.go | 104 ++++++++++++++++++++++++++++++ cmd/operator.go | 146 +++++++++++++------------------------------ cmd/root.go | 18 +++--- 3 files changed, 159 insertions(+), 109 deletions(-) create mode 100644 cmd/authorization.go diff --git a/cmd/authorization.go b/cmd/authorization.go new file mode 100644 index 00000000..0e758214 --- /dev/null +++ b/cmd/authorization.go @@ -0,0 +1,104 @@ +package cmd + +import ( + "context" + "crypto/tls" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + platformeshcontext "github.com/platform-mesh/golang-commons/context" + "github.com/platform-mesh/security-operator/internal/controller" + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "k8s.io/client-go/rest" + + "github.com/kcp-dev/multicluster-provider/apiexport" + pathaware "github.com/kcp-dev/multicluster-provider/path-aware" +) + +var apiExportPolicyCmd = &cobra.Command{ + Use: "authorization", + Short: "Controllers for authorization.platform-mesh.io APIExport", + RunE: func(cmd *cobra.Command, args []string) error { + ctrl.SetLogger(log.ComponentLogger("controller-runtime").Logr()) + + ctx, _, shutdown := platformeshcontext.StartContext(log, defaultCfg, defaultCfg.ShutdownTimeout) + defer shutdown() + + restCfg, err := getKubeconfigFromPath(authorizationCfg.KCP.Kubeconfig) + if err != nil { + log.Error().Err(err).Msg("unable to get KCP kubeconfig") + return err + } + + opts := ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: defaultCfg.Metrics.BindAddress, + TLSOpts: []func(*tls.Config){ + func(c *tls.Config) { + c.NextProtos = []string{"http/1.1"} + }, + }, + }, + HealthProbeBindAddress: defaultCfg.HealthProbeBindAddress, + LeaderElection: defaultCfg.LeaderElectionEnabled, + LeaderElectionID: "security-operator-authorization.platform-mesh.io", + BaseContext: func() context.Context { return ctx }, + } + + if defaultCfg.LeaderElectionEnabled { + inClusterCfg, err := rest.InClusterConfig() + if err != nil { + setupLog.Error(err, "unable to get in-cluster config for leader election") + return err + } + opts.LeaderElectionConfig = inClusterCfg + } + + provider, err := pathaware.New(restCfg, authorizationCfg.AuthorizationAPIExportEndpointSliceName, apiexport.Options{ + Scheme: scheme, + }) + if err != nil { + setupLog.Error(err, "unable to create path-aware provider") + return err + } + + mgr, err := mcmanager.New(restCfg, provider, opts) + if err != nil { + setupLog.Error(err, "unable to create apiexportpolicy manager") + return err + } + + conn, err := grpc.NewClient(authorizationCfg.FGA.Target, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error().Err(err).Msg("unable to create grpc client") + return err + } + + fga := openfgav1.NewOpenFGAServiceClient(conn) + + if err = controller.NewAPIExportPolicyReconciler(log, fga, mgr).SetupWithManager(mgr, defaultCfg); err != nil { + log.Error().Err(err).Str("controller", "apiexportpolicy").Msg("unable to create controller") + return err + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + log.Error().Err(err).Msg("unable to set up health check") + return err + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + log.Error().Err(err).Msg("unable to set up ready check") + return err + } + + setupLog.Info("starting apiexportpolicy manager") + + return mgr.Start(ctx) + }, +} diff --git a/cmd/operator.go b/cmd/operator.go index 4b51b109..6f9bbffc 100644 --- a/cmd/operator.go +++ b/cmd/operator.go @@ -16,7 +16,6 @@ import ( "github.com/platform-mesh/security-operator/internal/controller" internalwebhook "github.com/platform-mesh/security-operator/internal/webhook" "github.com/spf13/cobra" - "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ctrl "sigs.k8s.io/controller-runtime" @@ -33,7 +32,6 @@ import ( "github.com/kcp-dev/logicalcluster/v3" "github.com/kcp-dev/multicluster-provider/apiexport" - pathaware "github.com/kcp-dev/multicluster-provider/path-aware" kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" kcpapisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" @@ -101,15 +99,53 @@ var operatorCmd = &cobra.Command{ defer platformeshcontext.Recover(log) } - coreMgr, err := setupCorePlatformMeshManager(ctx, restCfg) + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: []func(*tls.Config){ + func(c *tls.Config) { + c.NextProtos = []string{"http/1.1"} + }, + }, + CertDir: operatorCfg.Webhooks.CertDir, + Port: operatorCfg.Webhooks.Port, + }) + + opts := ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: defaultCfg.Metrics.BindAddress, + TLSOpts: []func(*tls.Config){ + func(c *tls.Config) { + c.NextProtos = []string{"http/1.1"} + }, + }, + }, + HealthProbeBindAddress: defaultCfg.HealthProbeBindAddress, + LeaderElection: defaultCfg.LeaderElectionEnabled, + LeaderElectionID: "security-operator.platform-mesh.io", + BaseContext: func() context.Context { return ctx }, + WebhookServer: webhookServer, + } + + if defaultCfg.LeaderElectionEnabled { + inClusterCfg, err := rest.InClusterConfig() + if err != nil { + setupLog.Error(err, "unable to get in-cluster config for leader election") + return err + } + opts.LeaderElectionConfig = inClusterCfg + } + + provider, err := apiexport.New(restCfg, operatorCfg.CoreAPIExportEndpointSliceName, apiexport.Options{ + Scheme: opts.Scheme, + }) if err != nil { - setupLog.Error(err, "unable to setup core manager") + setupLog.Error(err, "unable to create apiexport provider") return err } - authorizationMgr, err := setupAuthorizationPlatformMeshManager(ctx, restCfg) + coreMgr, err := mcmanager.New(restCfg, provider, opts) if err != nil { - setupLog.Error(err, "unable to setup authorization manager") + setupLog.Error(err, "unable to create core manager") return err } @@ -151,11 +187,6 @@ var operatorCmd = &cobra.Command{ return err } - if err = controller.NewAPIExportPolicyReconciler(log, fga, authorizationMgr).SetupWithManager(authorizationMgr, defaultCfg); err != nil { - log.Error().Err(err).Str("controller", "apiexportpolicy").Msg("unable to create controller") - return err - } - if operatorCfg.Webhooks.Enabled { log.Info().Msg("validating webhooks are enabled") if err := internalwebhook.SetupIdentityProviderConfigurationValidatingWebhookWithManager(ctx, coreMgr.GetLocalManager(), &operatorCfg); err != nil { @@ -174,23 +205,9 @@ var operatorCmd = &cobra.Command{ return err } - g, gctx := errgroup.WithContext(ctx) - - g.Go(func() error { - setupLog.Info("starting core manager") - return coreMgr.Start(gctx) - }) - - g.Go(func() error { - setupLog.Info("starting authorization manager") - return authorizationMgr.Start(gctx) - }) + setupLog.Info("starting core manager") - if err := g.Wait(); err != nil { - log.Error().Err(err).Msg("failed to run managers") - return err - } - return nil + return coreMgr.Start(ctx) }, } @@ -246,78 +263,3 @@ func init() { utilruntime.Must(accountsv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } - -func setupCorePlatformMeshManager(ctx context.Context, restCfg *rest.Config) (mcmanager.Manager, error) { - webhookServer := webhook.NewServer(webhook.Options{ - TLSOpts: []func(*tls.Config){ - func(c *tls.Config) { - c.NextProtos = []string{"http/1.1"} - }, - }, - CertDir: operatorCfg.Webhooks.CertDir, - Port: operatorCfg.Webhooks.Port, - }) - - opts := ctrl.Options{ - Scheme: scheme, - Metrics: metricsserver.Options{ - BindAddress: defaultCfg.Metrics.BindAddress, - TLSOpts: []func(*tls.Config){ - func(c *tls.Config) { - c.NextProtos = []string{"http/1.1"} - }, - }, - }, - HealthProbeBindAddress: defaultCfg.HealthProbeBindAddress, - LeaderElection: defaultCfg.LeaderElectionEnabled, - LeaderElectionID: "security-operator.platform-mesh.io", - BaseContext: func() context.Context { return ctx }, - WebhookServer: webhookServer, - } - - if defaultCfg.LeaderElectionEnabled { - inClusterCfg, err := rest.InClusterConfig() - if err != nil { - return nil, fmt.Errorf("getting in-cluster config for leader election: %w", err) - } - opts.LeaderElectionConfig = inClusterCfg - } - - provider, err := apiexport.New(restCfg, operatorCfg.CoreAPIExportEndpointSliceName, apiexport.Options{ - Scheme: opts.Scheme, - }) - if err != nil { - return nil, fmt.Errorf("creating apiexport provider: %w", err) - } - - mgr, err := mcmanager.New(restCfg, provider, opts) - if err != nil { - return nil, fmt.Errorf("creating core.platform-mesh.io manager: %w", err) - } - - return mgr, nil -} - -func setupAuthorizationPlatformMeshManager(ctx context.Context, restCfg *rest.Config) (mcmanager.Manager, error) { - provider, err := pathaware.New(restCfg, operatorCfg.AuthorizationAPIExportEndpointSliceName, apiexport.Options{ - Scheme: scheme, - }) - if err != nil { - return nil, fmt.Errorf("creating path-aware provider: %w", err) - } - - opts := ctrl.Options{ - Scheme: scheme, - Metrics: metricsserver.Options{BindAddress: "0"}, - HealthProbeBindAddress: "0", - LeaderElection: false, - BaseContext: func() context.Context { return ctx }, - } - - mgr, err := mcmanager.New(restCfg, provider, opts) - if err != nil { - return nil, fmt.Errorf("creating authorization.platform-mesh.io manager: %w", err) - } - - return mgr, nil -} diff --git a/cmd/root.go b/cmd/root.go index ff5567ea..a7bbacd9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,13 +15,14 @@ import ( ) var ( - defaultCfg *platformeshconfig.CommonServiceConfig - initializerCfg config.Config - terminatorCfg config.Config - operatorCfg config.Config - generatorCfg config.Config - log *logger.Logger - setupLog logr.Logger + defaultCfg *platformeshconfig.CommonServiceConfig + initializerCfg config.Config + terminatorCfg config.Config + operatorCfg config.Config + generatorCfg config.Config + authorizationCfg config.Config + log *logger.Logger + setupLog logr.Logger ) var rootCmd = &cobra.Command{ @@ -34,12 +35,14 @@ func init() { rootCmd.AddCommand(operatorCmd) rootCmd.AddCommand(modelGeneratorCmd) rootCmd.AddCommand(initContainerCmd) + rootCmd.AddCommand(apiExportPolicyCmd) defaultCfg = platformeshconfig.NewDefaultConfig() operatorCfg = config.NewConfig() generatorCfg = config.NewConfig() initializerCfg = config.NewConfig() terminatorCfg = config.NewConfig() + authorizationCfg = config.NewConfig() initContainerCfg = config.NewInitContainerConfig() defaultCfg.AddFlags(rootCmd.PersistentFlags()) @@ -47,6 +50,7 @@ func init() { generatorCfg.AddFlags(modelGeneratorCmd.Flags()) initializerCfg.AddFlags(initializerCmd.Flags()) terminatorCfg.AddFlags(terminatorCmd.Flags()) + authorizationCfg.AddFlags(apiExportPolicyCmd.Flags()) initContainerCfg.AddFlags(initContainerCmd.Flags()) cobra.OnInitialize(initLog) From c8b198d5a4c2fd470ee222483d69a80a8a79f3f4 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Thu, 12 Mar 2026 17:06:07 +0100 Subject: [PATCH 7/9] chore: refactor operator setup On-behalf-of: SAP aleh.yarshou@sap.com --- cmd/authorization.go | 2 +- cmd/operator.go | 53 ++++++++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/cmd/authorization.go b/cmd/authorization.go index 0e758214..300c5ebb 100644 --- a/cmd/authorization.go +++ b/cmd/authorization.go @@ -97,7 +97,7 @@ var apiExportPolicyCmd = &cobra.Command{ return err } - setupLog.Info("starting apiexportpolicy manager") + setupLog.Info("starting authorization manager") return mgr.Start(ctx) }, diff --git a/cmd/operator.go b/cmd/operator.go index 6f9bbffc..75a1501d 100644 --- a/cmd/operator.go +++ b/cmd/operator.go @@ -102,6 +102,7 @@ var operatorCmd = &cobra.Command{ webhookServer := webhook.NewServer(webhook.Options{ TLSOpts: []func(*tls.Config){ func(c *tls.Config) { + log.Info().Msg("disabling http/2") c.NextProtos = []string{"http/1.1"} }, }, @@ -109,12 +110,13 @@ var operatorCmd = &cobra.Command{ Port: operatorCfg.Webhooks.Port, }) - opts := ctrl.Options{ + mgrOpts := ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{ BindAddress: defaultCfg.Metrics.BindAddress, TLSOpts: []func(*tls.Config){ func(c *tls.Config) { + log.Info().Msg("disabling http/2") c.NextProtos = []string{"http/1.1"} }, }, @@ -125,27 +127,31 @@ var operatorCmd = &cobra.Command{ BaseContext: func() context.Context { return ctx }, WebhookServer: webhookServer, } - if defaultCfg.LeaderElectionEnabled { inClusterCfg, err := rest.InClusterConfig() if err != nil { - setupLog.Error(err, "unable to get in-cluster config for leader election") + log.Error().Err(err).Msg("unable to create in-cluster config") return err } - opts.LeaderElectionConfig = inClusterCfg + mgrOpts.LeaderElectionConfig = inClusterCfg + } + + if mgrOpts.Scheme == nil { + log.Error().Err(fmt.Errorf("scheme should not be nil")).Msg("scheme should not be nil") + return fmt.Errorf("scheme should not be nil") } provider, err := apiexport.New(restCfg, operatorCfg.CoreAPIExportEndpointSliceName, apiexport.Options{ - Scheme: opts.Scheme, + Scheme: mgrOpts.Scheme, }) if err != nil { - setupLog.Error(err, "unable to create apiexport provider") + setupLog.Error(err, "unable to construct cluster provider") return err } - coreMgr, err := mcmanager.New(restCfg, provider, opts) + mgr, err := mcmanager.New(restCfg, provider, mgrOpts) if err != nil { - setupLog.Error(err, "unable to create core manager") + setupLog.Error(err, "Failed to create manager") return err } @@ -155,7 +161,7 @@ var operatorCmd = &cobra.Command{ return err } - orgClient, err := logicalClusterClientFromKey(coreMgr.GetLocalManager().GetConfig(), log)(logicalcluster.Name("root:orgs")) + orgClient, err := logicalClusterClientFromKey(mgr.GetLocalManager().GetConfig(), log)(logicalcluster.Name("root:orgs")) if err != nil { log.Error().Err(err).Msg("Failed to create org client") return err @@ -163,51 +169,54 @@ var operatorCmd = &cobra.Command{ fga := openfgav1.NewOpenFGAServiceClient(conn) - if err = controller.NewStoreReconciler(ctx, log, fga, coreMgr). - SetupWithManager(coreMgr, defaultCfg); err != nil { + if err = controller.NewStoreReconciler(ctx, log, fga, mgr). + SetupWithManager(mgr, defaultCfg); err != nil { log.Error().Err(err).Str("controller", "store").Msg("unable to create controller") return err } if err = controller. - NewAuthorizationModelReconciler(log, fga, coreMgr). - SetupWithManager(coreMgr, defaultCfg); err != nil { + NewAuthorizationModelReconciler(log, fga, mgr). + SetupWithManager(mgr, defaultCfg); err != nil { log.Error().Err(err).Str("controller", "authorizationmodel").Msg("unable to create controller") return err } - if err = controller.NewIdentityProviderConfigurationReconciler(ctx, coreMgr, orgClient, &operatorCfg, log).SetupWithManager(coreMgr, defaultCfg, log); err != nil { + if err = controller.NewIdentityProviderConfigurationReconciler(ctx, mgr, orgClient, &operatorCfg, log).SetupWithManager(mgr, defaultCfg, log); err != nil { log.Error().Err(err).Str("controller", "identityprovider").Msg("unable to create controller") return err } - if err = controller.NewInviteReconciler(ctx, coreMgr, &operatorCfg, log).SetupWithManager(coreMgr, defaultCfg, log); err != nil { + if err = controller.NewInviteReconciler(ctx, mgr, &operatorCfg, log).SetupWithManager(mgr, defaultCfg, log); err != nil { log.Error().Err(err).Str("controller", "invite").Msg("unable to create controller") return err } - if err = controller.NewAccountInfoReconciler(log, coreMgr).SetupWithManager(coreMgr, defaultCfg); err != nil { + if err = controller.NewAccountInfoReconciler(log, mgr).SetupWithManager(mgr, defaultCfg); err != nil { log.Error().Err(err).Str("controller", "accountinfo").Msg("unable to create controller") return err } if operatorCfg.Webhooks.Enabled { log.Info().Msg("validating webhooks are enabled") - if err := internalwebhook.SetupIdentityProviderConfigurationValidatingWebhookWithManager(ctx, coreMgr.GetLocalManager(), &operatorCfg); err != nil { + if err := internalwebhook.SetupIdentityProviderConfigurationValidatingWebhookWithManager(ctx, mgr.GetLocalManager(), &operatorCfg); err != nil { log.Error().Err(err).Str("webhook", "IdentityProviderConfiguration").Msg("unable to create webhook") return err } } // +kubebuilder:scaffold:builder - if err := coreMgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { log.Error().Err(err).Msg("unable to set up health check") return err } - if err := coreMgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { log.Error().Err(err).Msg("unable to set up ready check") return err } - setupLog.Info("starting core manager") - - return coreMgr.Start(ctx) + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + log.Error().Err(err).Msg("problem running manager") + return err + } + return nil }, } From 97e0a69c497dfc5401985e9f7b8b843537aff6fd Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Fri, 13 Mar 2026 10:43:56 +0100 Subject: [PATCH 8/9] chore: little refactoring On-behalf-of: SAP aleh.yarshou@sap.com --- api/v1alpha1/apiexportpolicy_types.go | 2 +- cmd/authorization.go | 3 +-- .../core.platform-mesh.io_apiexportpolicies.yaml | 4 ++-- .../resources/apiexport-core.platform-mesh.io.yaml | 2 +- ...hema-apiexportpolicies.core.platform-mesh.io.yaml | 6 +++--- internal/subroutine/apiexportpolicy.go | 12 +++++------- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/api/v1alpha1/apiexportpolicy_types.go b/api/v1alpha1/apiexportpolicy_types.go index 4e14dde3..455bd649 100644 --- a/api/v1alpha1/apiexportpolicy_types.go +++ b/api/v1alpha1/apiexportpolicy_types.go @@ -8,7 +8,7 @@ import ( type APIExportRef struct { Name string `json:"name"` - ClusterPath string `json:"clusterName"` + ClusterPath string `json:"clusterPath"` } type APIExportPolicySpec struct { diff --git a/cmd/authorization.go b/cmd/authorization.go index 300c5ebb..f87d1040 100644 --- a/cmd/authorization.go +++ b/cmd/authorization.go @@ -18,7 +18,6 @@ import ( "k8s.io/client-go/rest" "github.com/kcp-dev/multicluster-provider/apiexport" - pathaware "github.com/kcp-dev/multicluster-provider/path-aware" ) var apiExportPolicyCmd = &cobra.Command{ @@ -61,7 +60,7 @@ var apiExportPolicyCmd = &cobra.Command{ opts.LeaderElectionConfig = inClusterCfg } - provider, err := pathaware.New(restCfg, authorizationCfg.AuthorizationAPIExportEndpointSliceName, apiexport.Options{ + provider, err := apiexport.New(restCfg, authorizationCfg.AuthorizationAPIExportEndpointSliceName, apiexport.Options{ Scheme: scheme, }) if err != nil { diff --git a/config/crd/bases/core.platform-mesh.io_apiexportpolicies.yaml b/config/crd/bases/core.platform-mesh.io_apiexportpolicies.yaml index 4d2d0bd0..4cbeb9a6 100644 --- a/config/crd/bases/core.platform-mesh.io_apiexportpolicies.yaml +++ b/config/crd/bases/core.platform-mesh.io_apiexportpolicies.yaml @@ -44,12 +44,12 @@ spec: type: array apiExportRef: properties: - clusterName: + clusterPath: type: string name: type: string required: - - clusterName + - clusterPath - name type: object required: diff --git a/config/resources/apiexport-core.platform-mesh.io.yaml b/config/resources/apiexport-core.platform-mesh.io.yaml index b0f3f03b..062e48f3 100644 --- a/config/resources/apiexport-core.platform-mesh.io.yaml +++ b/config/resources/apiexport-core.platform-mesh.io.yaml @@ -7,7 +7,7 @@ spec: resources: - group: core.platform-mesh.io name: apiexportpolicies - schema: v260312-dba9d8d.apiexportpolicies.core.platform-mesh.io + schema: v260313-c8b198d.apiexportpolicies.core.platform-mesh.io storage: crd: {} - group: core.platform-mesh.io diff --git a/config/resources/apiresourceschema-apiexportpolicies.core.platform-mesh.io.yaml b/config/resources/apiresourceschema-apiexportpolicies.core.platform-mesh.io.yaml index d2c2dd03..970b0705 100644 --- a/config/resources/apiresourceschema-apiexportpolicies.core.platform-mesh.io.yaml +++ b/config/resources/apiresourceschema-apiexportpolicies.core.platform-mesh.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v260312-dba9d8d.apiexportpolicies.core.platform-mesh.io + name: v260313-c8b198d.apiexportpolicies.core.platform-mesh.io spec: group: core.platform-mesh.io names: @@ -41,12 +41,12 @@ spec: type: array apiExportRef: properties: - clusterName: + clusterPath: type: string name: type: string required: - - clusterName + - clusterPath - name type: object required: diff --git a/internal/subroutine/apiexportpolicy.go b/internal/subroutine/apiexportpolicy.go index 0876d444..32b15b99 100644 --- a/internal/subroutine/apiexportpolicy.go +++ b/internal/subroutine/apiexportpolicy.go @@ -24,10 +24,9 @@ import ( ) const ( - APIExportPolicyFinalizer = "authorization.platform-mesh.io/apiexportpolicy-finalizer" - orgsWorkspacePath = "root:orgs" - bindRelation = "bind" - bindInheritedRelation = "bind_inherited" + orgsWorkspacePath = "root:orgs" + bindRelation = "bind" + bindInheritedRelation = "bind_inherited" ) type APIExportPolicySubroutine struct { @@ -49,7 +48,7 @@ func (a *APIExportPolicySubroutine) GetName() string { } func (a *APIExportPolicySubroutine) Finalizers(_ lifecyclecontrollerruntime.RuntimeObject) []string { - return []string{APIExportPolicyFinalizer} + return []string{"authorization.platform-mesh.io/apiexportpolicy-finalizer"} } func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecyclecontrollerruntime.RuntimeObject) (ctrl.Result, errors.OperatorError) { @@ -65,7 +64,6 @@ func (a *APIExportPolicySubroutine) Process(ctx context.Context, instance lifecy // Delete tuples for expressions that were removed from the spec if err := a.deleteRemovedExpressions(ctx, policy); err != nil { - log.Error().Err(err).Msg("Failed to delete some removed expressions, continuing") return ctrl.Result{}, errors.NewOperatorError( fmt.Errorf("removing tuples for policy %s: %w", policy.Name, err), true, true) @@ -255,7 +253,7 @@ func (a *APIExportPolicySubroutine) deleteTuplesForExpression(ctx context.Contex if workspacePath == orgsWorkspacePath { allclient, err := iclient.NewForAllPlatformMeshResources(ctx, a.mgr.GetLocalManager().GetConfig(), a.mgr.GetLocalManager().GetScheme()) if err != nil { - return fmt.Errorf("creating all-resources client: %w", err) + return fmt.Errorf("creating all client: %w", err) } var accountInfoList accountsv1alpha1.AccountInfoList From 858127e342e7a34cf907f93dd463f4fcc1ed395f Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Fri, 13 Mar 2026 16:32:55 +0100 Subject: [PATCH 9/9] feat: add direct enqueue when a new org appears On-behalf-of: SAP aleh.yarshou@sap.com --- .../controller/apiexportpolicy_controller.go | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/internal/controller/apiexportpolicy_controller.go b/internal/controller/apiexportpolicy_controller.go index 3de0c5b3..d4cf5050 100644 --- a/internal/controller/apiexportpolicy_controller.go +++ b/internal/controller/apiexportpolicy_controller.go @@ -2,6 +2,7 @@ package controller import ( "context" + "strings" openfgav1 "github.com/openfga/api/proto/openfga/v1" platformeshconfig "github.com/platform-mesh/golang-commons/config" @@ -10,12 +11,21 @@ import ( lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/logger" corev1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" + iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/subroutine" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" + ctrhandler "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + "sigs.k8s.io/multicluster-runtime/pkg/handler" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + "github.com/kcp-dev/logicalcluster/v3" + "k8s.io/apimachinery/pkg/types" ) type APIExportPolicyReconciler struct { @@ -40,5 +50,59 @@ func (r *APIExportPolicyReconciler) Reconcile(ctx context.Context, req mcreconci } func (r *APIExportPolicyReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platformeshconfig.CommonServiceConfig, evp ...predicate.Predicate) error { - return r.mclifecycle.SetupWithManager(mgr, cfg.MaxConcurrentReconciles, "apiexportpolicy", &corev1alpha1.APIExportPolicy{}, cfg.DebugLabelValue, r, r.log, evp...) + bld, err := r.mclifecycle.SetupWithManagerBuilder(mgr, cfg.MaxConcurrentReconciles, "apiexportpolicy", &corev1alpha1.APIExportPolicy{}, cfg.DebugLabelValue, r.log, evp...) + if err != nil { + return err + } + return bld. + Watches( + &corev1alpha1.Store{}, + func(clusterName string, c cluster.Cluster) ctrhandler.TypedEventHandler[client.Object, mcreconcile.Request] { + return handler.TypedEnqueueRequestsFromMapFuncWithClusterPreservation(func(ctx context.Context, obj client.Object) []mcreconcile.Request { + _, ok := obj.(*corev1alpha1.Store) + if !ok { + return nil + } + + // List all APIExportPolicy resources and enqueue those with root:orgs:* expression + return r.enqueueAllAPIExportPolicies(ctx, mgr) + }) + }, + ).Complete(r) +} + +func (r *APIExportPolicyReconciler) enqueueAllAPIExportPolicies(ctx context.Context, mgr mcmanager.Manager) []mcreconcile.Request { + allClient, err := iclient.NewForAllPlatformMeshResources(ctx, mgr.GetLocalManager().GetConfig(), mgr.GetLocalManager().GetScheme()) + if err != nil { + r.log.Error().Err(err).Msg("failed to create all-cluster client for APIExportPolicy listing") + return nil + } + + var policies corev1alpha1.APIExportPolicyList + if err := allClient.List(ctx, &policies); err != nil { + r.log.Error().Err(err).Msg("failed to list APIExportPolicy resources") + return nil + } + + var requests []mcreconcile.Request + for _, policy := range policies.Items { + // Check if policy has root:orgs:* expression + for _, expr := range policy.Spec.AllowPathExpressions { + trimmedExpr := strings.TrimPrefix(expr, ":") + + if trimmedExpr == "root:orgs:*" { + clusterName := logicalcluster.From(&policy) + requests = append(requests, mcreconcile.Request{ + Request: reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: policy.Name, + }, + }, + ClusterName: clusterName.String(), + }) + break + } + } + } + return requests }