From 9710f6ae7548a73bcfd5cff06e4e7fe9ca7e22ef Mon Sep 17 00:00:00 2001 From: Eneman Donatien Date: Fri, 13 Sep 2024 15:06:15 +0200 Subject: [PATCH] immutable fields + allowedNamespaces --- api/v1alpha1/bucket_types.go | 1 + api/v1alpha1/path_types.go | 1 + api/v1alpha1/policy_types.go | 1 + api/v1alpha1/s3instance_types.go | 8 +- api/v1alpha1/s3user_types.go | 1 + api/v1alpha1/zz_generated.deepcopy.go | 4 +- config/crd/bases/s3.onyxia.sh_buckets.yaml | 3 + config/crd/bases/s3.onyxia.sh_paths.yaml | 3 + config/crd/bases/s3.onyxia.sh_policies.yaml | 3 + .../crd/bases/s3.onyxia.sh_s3instances.yaml | 9 ++- config/crd/bases/s3.onyxia.sh_s3users.yaml | 3 + controllers/bucket_controller.go | 8 +- controllers/path_controller.go | 10 ++- controllers/policy_controller.go | 9 ++- controllers/s3instance_controller.go | 75 +++++++++++++++++-- controllers/user_controller.go | 9 ++- go.mod | 2 + go.sum | 4 + internal/s3/factory/interface.go | 5 +- internal/utils/glob/glob.go | 41 ++++++++++ internal/utils/regex/regex.go | 17 +++++ internal/utils/utils.go | 5 ++ main.go | 68 +++++++++-------- 23 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 internal/utils/glob/glob.go create mode 100644 internal/utils/regex/regex.go diff --git a/api/v1alpha1/bucket_types.go b/api/v1alpha1/bucket_types.go index 7f15adb..b258bb3 100644 --- a/api/v1alpha1/bucket_types.go +++ b/api/v1alpha1/bucket_types.go @@ -38,6 +38,7 @@ type BucketSpec struct { // s3InstanceRef where create the bucket // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3InstanceRef is immutable" S3InstanceRef string `json:"s3InstanceRef,omitempty"` // Quota to apply to the bucket diff --git a/api/v1alpha1/path_types.go b/api/v1alpha1/path_types.go index 45c7ce9..019d7ef 100644 --- a/api/v1alpha1/path_types.go +++ b/api/v1alpha1/path_types.go @@ -38,6 +38,7 @@ type PathSpec struct { // s3InstanceRef where create the Paths // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3InstanceRef is immutable" S3InstanceRef string `json:"s3InstanceRef,omitempty"` } diff --git a/api/v1alpha1/policy_types.go b/api/v1alpha1/policy_types.go index ac7a07c..2825b45 100644 --- a/api/v1alpha1/policy_types.go +++ b/api/v1alpha1/policy_types.go @@ -38,6 +38,7 @@ type PolicySpec struct { // s3InstanceRef where create the Policy // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3InstanceRef is immutable" S3InstanceRef string `json:"s3InstanceRef,omitempty"` } diff --git a/api/v1alpha1/s3instance_types.go b/api/v1alpha1/s3instance_types.go index eb03eff..2edcaa5 100644 --- a/api/v1alpha1/s3instance_types.go +++ b/api/v1alpha1/s3instance_types.go @@ -46,9 +46,13 @@ type S3InstanceSpec struct { // +kubebuilder:validation:Optional UseSSL bool `json:"useSSL,omitempty"` - // CaCertificatesBase64 associated to the S3InstanceUrl + // Secret containing key ca.crt with the certificate associated to the S3InstanceUrl // +kubebuilder:validation:Optional - CaCertificatesBase64 []string `json:"caCertificateBase64,omitempty"` + CaCertSecretRef string `json:"caCertSecretRef,omitempty"` + + // AllowedNamespaces to use this S3InstanceUrl if empty only the namespace of this instance url is allowed to use it + // +kubebuilder:validation:Optional + AllowedNamespaces []string `json:"allowedNamespaces,omitempty"` } // S3InstanceStatus defines the observed state of S3Instance diff --git a/api/v1alpha1/s3user_types.go b/api/v1alpha1/s3user_types.go index e116a92..be69923 100644 --- a/api/v1alpha1/s3user_types.go +++ b/api/v1alpha1/s3user_types.go @@ -40,6 +40,7 @@ type S3UserSpec struct { // s3InstanceRef where create the user // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3InstanceRef is immutable" S3InstanceRef string `json:"s3InstanceRef,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 46c7fe1..f9ca52d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -402,8 +402,8 @@ func (in *S3InstanceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *S3InstanceSpec) DeepCopyInto(out *S3InstanceSpec) { *out = *in - if in.CaCertificatesBase64 != nil { - in, out := &in.CaCertificatesBase64, &out.CaCertificatesBase64 + if in.AllowedNamespaces != nil { + in, out := &in.AllowedNamespaces, &out.AllowedNamespaces *out = make([]string, len(*in)) copy(*out, *in) } diff --git a/config/crd/bases/s3.onyxia.sh_buckets.yaml b/config/crd/bases/s3.onyxia.sh_buckets.yaml index 098120e..9973ea4 100644 --- a/config/crd/bases/s3.onyxia.sh_buckets.yaml +++ b/config/crd/bases/s3.onyxia.sh_buckets.yaml @@ -60,6 +60,9 @@ spec: s3InstanceRef: description: s3InstanceRef where create the bucket type: string + x-kubernetes-validations: + - message: S3InstanceRef is immutable + rule: self == oldSelf required: - name - quota diff --git a/config/crd/bases/s3.onyxia.sh_paths.yaml b/config/crd/bases/s3.onyxia.sh_paths.yaml index c124fd0..44778b0 100644 --- a/config/crd/bases/s3.onyxia.sh_paths.yaml +++ b/config/crd/bases/s3.onyxia.sh_paths.yaml @@ -46,6 +46,9 @@ spec: s3InstanceRef: description: s3InstanceRef where create the Paths type: string + x-kubernetes-validations: + - message: S3InstanceRef is immutable + rule: self == oldSelf required: - bucketName type: object diff --git a/config/crd/bases/s3.onyxia.sh_policies.yaml b/config/crd/bases/s3.onyxia.sh_policies.yaml index aaa69a1..6c422ef 100644 --- a/config/crd/bases/s3.onyxia.sh_policies.yaml +++ b/config/crd/bases/s3.onyxia.sh_policies.yaml @@ -44,6 +44,9 @@ spec: s3InstanceRef: description: s3InstanceRef where create the Policy type: string + x-kubernetes-validations: + - message: S3InstanceRef is immutable + rule: self == oldSelf required: - name - policyContent diff --git a/config/crd/bases/s3.onyxia.sh_s3instances.yaml b/config/crd/bases/s3.onyxia.sh_s3instances.yaml index e1654f5..5cf716b 100644 --- a/config/crd/bases/s3.onyxia.sh_s3instances.yaml +++ b/config/crd/bases/s3.onyxia.sh_s3instances.yaml @@ -35,11 +35,16 @@ spec: spec: description: S3InstanceSpec defines the desired state of S3Instance properties: - caCertificateBase64: - description: CaCertificatesBase64 associated to the S3InstanceUrl + allowedNamespaces: + description: AllowedNamespaces to use this S3InstanceUrl if empty + only the namespace of this instance url is allowed to use it items: type: string type: array + caCertSecretRef: + description: Secret containing key ca.crt with the certificate associated + to the S3InstanceUrl + type: string region: description: region associated to the S3Instance type: string diff --git a/config/crd/bases/s3.onyxia.sh_s3users.yaml b/config/crd/bases/s3.onyxia.sh_s3users.yaml index 17b96cf..b3e42d3 100644 --- a/config/crd/bases/s3.onyxia.sh_s3users.yaml +++ b/config/crd/bases/s3.onyxia.sh_s3users.yaml @@ -46,6 +46,9 @@ spec: s3InstanceRef: description: s3InstanceRef where create the user type: string + x-kubernetes-validations: + - message: S3InstanceRef is immutable + rule: self == oldSelf secretName: description: SecretName associated to the S3User type: string diff --git a/controllers/bucket_controller.go b/controllers/bucket_controller.go index 5e88562..9577fc1 100644 --- a/controllers/bucket_controller.go +++ b/controllers/bucket_controller.go @@ -315,6 +315,12 @@ func (r *BucketReconciler) getS3InstanceForObject(ctx context.Context, bucketRes logger.Error(err, "No client was found") return nil, err } - return s3Client, nil + logger.Info(fmt.Sprintf("Check if this bucketRessource can use this S3Instance")) + if utils.IsAllowedNamespaces(bucketResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", bucketResource.Spec.S3InstanceRef)} + return nil, err + } } } diff --git a/controllers/path_controller.go b/controllers/path_controller.go index f05549a..e448073 100644 --- a/controllers/path_controller.go +++ b/controllers/path_controller.go @@ -274,13 +274,19 @@ func (r *PathReconciler) getS3InstanceForObject(ctx context.Context, pathResourc return s3Client, nil } else { - logger.Info(fmt.Sprintf("Bucket resource doesn't refer to s3Instance: %s, search instance in cache", pathResource.Spec.S3InstanceRef)) + logger.Info(fmt.Sprintf("Path resource doesn't refer to s3Instance: %s, search instance in cache", pathResource.Spec.S3InstanceRef)) s3Client, found := r.S3ClientCache.Get(pathResource.Spec.S3InstanceRef) if !found { err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", pathResource.Spec.S3InstanceRef)} logger.Error(err, "No client was found") return nil, err } - return s3Client, nil + logger.Info(fmt.Sprintf("Check if this PathRessource can use this S3Instance")) + if utils.IsAllowedNamespaces(pathResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", pathResource.Spec.S3InstanceRef)} + return nil, err + } } } diff --git a/controllers/policy_controller.go b/controllers/policy_controller.go index 37c3ee8..120d92c 100644 --- a/controllers/policy_controller.go +++ b/controllers/policy_controller.go @@ -281,13 +281,20 @@ func (r *PolicyReconciler) getS3InstanceForObject(ctx context.Context, policyRes } return s3Client, nil } else { - logger.Info(fmt.Sprintf("Bucket resource doesn't refer to s3Instance: %s, search instance in cache", policyResource.Spec.S3InstanceRef)) + logger.Info(fmt.Sprintf("Policy resource doesn't refer to s3Instance: %s, search instance in cache", policyResource.Spec.S3InstanceRef)) s3Client, found := r.S3ClientCache.Get(policyResource.Spec.S3InstanceRef) if !found { err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", policyResource.Spec.S3InstanceRef)} logger.Error(err, "No client was found") return nil, err } + logger.Info(fmt.Sprintf("Check if this PolicyRessource can use this S3Instance")) + if utils.IsAllowedNamespaces(policyResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", policyResource.Spec.S3InstanceRef)} + return nil, err + } return s3Client, nil } } diff --git a/controllers/s3instance_controller.go b/controllers/s3instance_controller.go index c46f1b5..eac2280 100644 --- a/controllers/s3instance_controller.go +++ b/controllers/s3instance_controller.go @@ -136,15 +136,23 @@ func (r *S3InstanceReconciler) handleS3InstanceUpdate(ctx context.Context, s3Ins // Get S3_ACCESS_KEY and S3_SECRET_KEY related to this s3Instance - s3InstanceSecretSecretExpected, err := r.getS3InstanceSecret(ctx, s3InstanceResource) + s3InstanceSecretSecretExpected, err := r.getS3InstanceAccessSecret(ctx, s3InstanceResource) if err != nil { logger.Error(err, "Could not get s3InstanceSecret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceUpdateFailed", fmt.Sprintf("Updating secret of S3Instance %s has failed", s3InstanceResource.Name), err) } + s3InstanceCaCertSecretExpected, err := r.getS3InstanceCaCertSecret(ctx, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get s3InstanceSecret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) + return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceCreationFailed", + fmt.Sprintf("Getting secret of S3s3Instance %s has failed", s3InstanceResource.Name), err) + + } + // if s3Provider have change recreate totaly One Differ instance will be deleted and recreated - if s3Config.S3Provider != s3InstanceResource.Spec.S3Provider || s3Config.S3UrlEndpoint != s3InstanceResource.Spec.UrlEndpoint || s3Config.UseSsl != s3InstanceResource.Spec.UseSSL || s3Config.Region != s3InstanceResource.Spec.Region || !reflect.DeepEqual(s3Config.CaCertificatesBase64, s3InstanceResource.Spec.CaCertificatesBase64) || s3Config.AccessKey != string(s3InstanceSecretSecretExpected.Data["S3_ACCESS_KEY"]) || s3Config.SecretKey != string(s3InstanceSecretSecretExpected.Data["S3_SECRET_KEY"]) { + if s3Config.S3Provider != s3InstanceResource.Spec.S3Provider || s3Config.S3UrlEndpoint != s3InstanceResource.Spec.UrlEndpoint || s3Config.UseSsl != s3InstanceResource.Spec.UseSSL || s3Config.Region != s3InstanceResource.Spec.Region || !reflect.DeepEqual(s3Config.AllowedNamespaces, s3InstanceResource.Spec.AllowedNamespaces) || !reflect.DeepEqual(s3Config.CaCertificatesBase64, []string{string(s3InstanceCaCertSecretExpected.Data["ca.crt"])}) || s3Config.AccessKey != string(s3InstanceSecretSecretExpected.Data["S3_ACCESS_KEY"]) || s3Config.SecretKey != string(s3InstanceSecretSecretExpected.Data["S3_SECRET_KEY"]) { logger.Info("Instance in cache not equal to expected , cache will be prune and instance recreate", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) r.S3ClientCache.Remove(s3InstanceResource.Name) return r.handleS3InstanceCreation(ctx, s3InstanceResource) @@ -157,7 +165,7 @@ func (r *S3InstanceReconciler) handleS3InstanceUpdate(ctx context.Context, s3Ins func (r *S3InstanceReconciler) handleS3InstanceCreation(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { logger := log.FromContext(ctx) - s3InstanceSecretSecret, err := r.getS3InstanceSecret(ctx, s3InstanceResource) + s3InstanceSecretSecret, err := r.getS3InstanceAccessSecret(ctx, s3InstanceResource) if err != nil { logger.Error(err, "Could not get s3InstanceSecret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceCreationFailed", @@ -165,7 +173,14 @@ func (r *S3InstanceReconciler) handleS3InstanceCreation(ctx context.Context, s3I } - s3Config := &s3Factory.S3Config{S3Provider: s3InstanceResource.Spec.S3Provider, AccessKey: string(s3InstanceSecretSecret.Data["S3_ACCESS_KEY"]), SecretKey: string(s3InstanceSecretSecret.Data["S3_SECRET_KEY"]), S3UrlEndpoint: s3InstanceResource.Spec.UrlEndpoint, Region: s3InstanceResource.Spec.Region, UseSsl: s3InstanceResource.Spec.UseSSL, CaCertificatesBase64: s3InstanceResource.Spec.CaCertificatesBase64} + s3InstanceCaCertSecret, err := r.getS3InstanceCaCertSecret(ctx, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get s3InstanceSecret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) + return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceCreationFailed", + fmt.Sprintf("Getting secret of S3s3Instance %s has failed", s3InstanceResource.Name), err) + } + + s3Config := &s3Factory.S3Config{S3Provider: s3InstanceResource.Spec.S3Provider, AccessKey: string(s3InstanceSecretSecret.Data["S3_ACCESS_KEY"]), SecretKey: string(s3InstanceSecretSecret.Data["S3_SECRET_KEY"]), S3UrlEndpoint: s3InstanceResource.Spec.UrlEndpoint, Region: s3InstanceResource.Spec.Region, UseSsl: s3InstanceResource.Spec.UseSSL, AllowedNamespaces: s3InstanceResource.Spec.AllowedNamespaces, CaCertificatesBase64: []string{string(s3InstanceCaCertSecret.Data["ca.crt"])}} s3Client, err := s3Factory.GenerateS3Client(s3Config.S3Provider, s3Config) if err != nil { @@ -285,11 +300,12 @@ func (r *S3InstanceReconciler) finalizeS3Instance(ctx context.Context, s3Instanc return nil } -func (r *S3InstanceReconciler) getS3InstanceSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { +func (r *S3InstanceReconciler) getS3InstanceAccessSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { logger := log.FromContext(ctx) secretsList := &corev1.SecretList{} s3InstanceSecret := corev1.Secret{} + secretFound := false err := r.List(ctx, secretsList, client.InNamespace(s3InstanceResource.Namespace)) if err != nil { @@ -299,7 +315,7 @@ func (r *S3InstanceReconciler) getS3InstanceSecret(ctx context.Context, s3Instan if len(secretsList.Items) == 0 { logger.Info("The s3instance's namespace doesn't appear to contain any secret") - return s3InstanceSecret, nil + return s3InstanceSecret, fmt.Errorf("No secret found in namespace") } // In all the secrets inside the s3instance's namespace, one should have a name equal to // the S3InstanceSecretRefName field. @@ -309,9 +325,54 @@ func (r *S3InstanceReconciler) getS3InstanceSecret(ctx context.Context, s3Instan for _, secret := range secretsList.Items { if secret.Name == s3InstanceSecretName { s3InstanceSecret = secret + secretFound = true + break + } + } + if secretFound { + return s3InstanceSecret, nil + } else { + return s3InstanceSecret, fmt.Errorf("Secret not found in namespace") + } +} + +func (r *S3InstanceReconciler) getS3InstanceCaCertSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { + logger := log.FromContext(ctx) + + secretsList := &corev1.SecretList{} + s3InstanceCaCertSecret := corev1.Secret{} + secretFound := false + + if s3InstanceResource.Spec.CaCertSecretRef == "" { + return s3InstanceCaCertSecret, nil + } + + err := r.List(ctx, secretsList, client.InNamespace(s3InstanceResource.Namespace)) + if err != nil { + logger.Error(err, "An error occurred while listing the secrets in s3instance's namespace") + return s3InstanceCaCertSecret, fmt.Errorf("SecretListingFailed") + } + + if len(secretsList.Items) == 0 { + logger.Info("The s3instance's namespace doesn't appear to contain any secret") + return s3InstanceCaCertSecret, nil + } + // In all the secrets inside the s3instance's namespace, one should have a name equal to + // the S3InstanceSecretRefName field. + s3InstanceCaCertSecretRef := s3InstanceResource.Spec.CaCertSecretRef + + // cmp.Or takes the first non "zero" value, see https://pkg.go.dev/cmp#Or + for _, secret := range secretsList.Items { + if secret.Name == s3InstanceCaCertSecretRef { + s3InstanceCaCertSecret = secret break } } - return s3InstanceSecret, nil + if secretFound { + return s3InstanceCaCertSecret, nil + } else { + return s3InstanceCaCertSecret, fmt.Errorf("Secret not found in namespace") + } + } diff --git a/controllers/user_controller.go b/controllers/user_controller.go index c5bd4d8..f6b88ec 100644 --- a/controllers/user_controller.go +++ b/controllers/user_controller.go @@ -619,13 +619,20 @@ func (r *S3UserReconciler) getS3InstanceForObject(ctx context.Context, userResou } return s3Client, nil } else { - logger.Info(fmt.Sprintf("Bucket resource doesn't refer to s3Instance: %s, search instance in cache", userResource.Spec.S3InstanceRef)) + logger.Info(fmt.Sprintf("User resource doesn't refer to s3Instance: %s, search instance in cache", userResource.Spec.S3InstanceRef)) s3Client, found := r.S3ClientCache.Get(userResource.Spec.S3InstanceRef) if !found { err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", userResource.Spec.S3InstanceRef)} logger.Error(err, "No client was found") return nil, err } + logger.Info(fmt.Sprintf("Check if this PathRessource can use this S3Instance")) + if utils.IsAllowedNamespaces(userResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", userResource.Spec.S3InstanceRef)} + return nil, err + } return s3Client, nil } } diff --git a/go.mod b/go.mod index e781b36..85e66b8 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.4 github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect @@ -29,6 +30,7 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/glob v0.2.3 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index 9790a39..adaf635 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -37,6 +39,8 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= diff --git a/internal/s3/factory/interface.go b/internal/s3/factory/interface.go index dbd14b2..5af0405 100644 --- a/internal/s3/factory/interface.go +++ b/internal/s3/factory/interface.go @@ -48,6 +48,7 @@ type S3Config struct { UseSsl bool CaCertificatesBase64 []string CaBundlePath string + AllowedNamespaces []string } func GenerateS3Client(s3Provider string, S3Config *S3Config) (S3Client, error) { @@ -60,7 +61,7 @@ func GenerateS3Client(s3Provider string, S3Config *S3Config) (S3Client, error) { return nil, fmt.Errorf("s3 provider " + s3Provider + "not supported") } -func GenerateDefaultS3Client(s3Provider string, s3UrlEndpoint string, accessKey string, secretKey string, region string, useSsl bool, caCertificatesBase64 []string, caBundlePath string) (S3Client, error) { +func GenerateDefaultS3Client(s3Provider string, s3UrlEndpoint string, accessKey string, secretKey string, region string, useSsl bool, caCertificatesBase64 []string, caBundlePath string, allowedNamespaces []string) (S3Client, error) { // For S3 access key and secret key, we first try to read the values from environment variables. // Only if these are not defined do we use the respective flags. @@ -82,7 +83,7 @@ func GenerateDefaultS3Client(s3Provider string, s3UrlEndpoint string, accessKey return newMockedS3Client(), nil } if s3Provider == "minio" { - S3Config := &S3Config{S3Provider: s3Provider, S3UrlEndpoint: s3UrlEndpoint, Region: region, AccessKey: accessKeyFromEnvIfAvailable, SecretKey: secretKeyFromEnvIfAvailable, UseSsl: useSsl, CaCertificatesBase64: caCertificatesBase64, CaBundlePath: caBundlePath} + S3Config := &S3Config{S3Provider: s3Provider, S3UrlEndpoint: s3UrlEndpoint, Region: region, AccessKey: accessKeyFromEnvIfAvailable, SecretKey: secretKeyFromEnvIfAvailable, UseSsl: useSsl, CaCertificatesBase64: caCertificatesBase64, CaBundlePath: caBundlePath, AllowedNamespaces: allowedNamespaces} return newMinioS3Client(S3Config), nil } return nil, fmt.Errorf("s3 provider " + s3Provider + "not supported") diff --git a/internal/utils/glob/glob.go b/internal/utils/glob/glob.go new file mode 100644 index 0000000..f4f67d5 --- /dev/null +++ b/internal/utils/glob/glob.go @@ -0,0 +1,41 @@ +package glob + +import ( + "strings" + + "github.com/InseeFrLab/s3-operator/internal/utils/regex" + "github.com/gobwas/glob" +) + +const ( + EXACT = "exact" + GLOB = "glob" + REGEXP = "regexp" +) + +func Match(pattern, text string, separators ...rune) bool { + compiledGlob, err := glob.Compile(pattern, separators...) + if err != nil { + return false + } + return compiledGlob.Match(text) +} + +// MatchStringInList will return true if item is contained in list. +// patternMatch; can be set to exact, glob, regexp. +// If patternMatch; is set to exact, the item must be an exact match. +// If patternMatch; is set to glob, the item must match a glob pattern. +// If patternMatch; is set to regexp, the item must match a regular expression or glob. +func MatchStringInList(list []string, item string, patternMatch string) bool { + for _, ll := range list { + // If string is wrapped in "/", assume it is a regular expression. + if patternMatch == REGEXP && strings.HasPrefix(ll, "/") && strings.HasSuffix(ll, "/") && regex.Match(ll[1:len(ll)-1], item) { + return true + } else if (patternMatch == REGEXP || patternMatch == GLOB) && Match(ll, item) { + return true + } else if patternMatch == EXACT && item == ll { + return true + } + } + return false +} diff --git a/internal/utils/regex/regex.go b/internal/utils/regex/regex.go new file mode 100644 index 0000000..cda0051 --- /dev/null +++ b/internal/utils/regex/regex.go @@ -0,0 +1,17 @@ +package regex + +import ( + "github.com/dlclark/regexp2" +) + +func Match(pattern, text string) bool { + compiledRegex, err := regexp2.Compile(pattern, 0) + if err != nil { + return false + } + regexMatch, err := compiledRegex.MatchString(text) + if err != nil { + return false + } + return regexMatch +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 4f69b74..648573d 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "time" + glob "github.com/InseeFrLab/s3-operator/internal/utils/glob" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -27,3 +28,7 @@ func UpdateConditions(existingConditions []metav1.Condition, newCondition metav1 return append([]metav1.Condition{newCondition}, existingConditions...) } + +func IsAllowedNamespaces(namespace string, namespaces []string) bool { + return glob.MatchStringInList(namespaces, namespace, glob.REGEXP) +} diff --git a/main.go b/main.go index e48edab..599e65a 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "flag" "fmt" "os" + "strings" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -29,7 +30,6 @@ import ( controllers "github.com/InseeFrLab/s3-operator/controllers" s3ClientCache "github.com/InseeFrLab/s3-operator/internal/s3" "github.com/InseeFrLab/s3-operator/internal/s3/factory" - "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -85,6 +85,7 @@ func main() { var pathDeletion bool var s3userDeletion bool var s3LabelSelector string + var allowedNamespaces string //K8S related variable var overrideExistingSecret bool @@ -110,6 +111,7 @@ func main() { flag.BoolVar(&pathDeletion, "path-deletion", false, "Trigger path deletion on the S3 backend upon CR deletion. Limited to deleting the `.keep` files used by the operator.") flag.BoolVar(&s3userDeletion, "s3user-deletion", false, "Trigger S3 deletion on the S3 backend upon CR deletion") flag.BoolVar(&overrideExistingSecret, "override-existing-secret", false, "Override existing secret associated to user in case of the secret already exist") + flag.StringVar(&allowedNamespaces, "allowed-namespaces", "*", "namespace that are allowed to use default s3instance") opts := zap.Options{ Development: true, @@ -157,7 +159,7 @@ func main() { s3ClientCache := s3ClientCache.New() // Creation of the default S3 client - s3DefaultClient, err := factory.GenerateDefaultS3Client(s3Provider, s3EndpointUrl, accessKey, secretKey, region, useSsl, caCertificatesBase64, caCertificatesBundlePath) + s3DefaultClient, err := factory.GenerateDefaultS3Client(s3Provider, s3EndpointUrl, accessKey, secretKey, region, useSsl, caCertificatesBase64, caCertificatesBundlePath, strings.Split(allowedNamespaces, ",")) if err != nil { // setupLog.Log.Error(err, err.Error()) @@ -181,37 +183,37 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Bucket") os.Exit(1) } - // if err = (&controllers.PathReconciler{ - // Client: mgr.GetClient(), - // Scheme: mgr.GetScheme(), - // S3ClientCache: s3ClientCache, - // PathDeletion: pathDeletion, - // S3LabelSelectorValue: s3LabelSelector, - // }).SetupWithManager(mgr); err != nil { - // setupLog.Error(err, "unable to create controller", "controller", "Path") - // os.Exit(1) - // } - // if err = (&controllers.PolicyReconciler{ - // Client: mgr.GetClient(), - // Scheme: mgr.GetScheme(), - // S3ClientCache: s3ClientCache, - // PolicyDeletion: policyDeletion, - // S3LabelSelectorValue: s3LabelSelector, - // }).SetupWithManager(mgr); err != nil { - // setupLog.Error(err, "unable to create controller", "controller", "Policy") - // os.Exit(1) - // } - // if err = (&controllers.S3UserReconciler{ - // Client: mgr.GetClient(), - // Scheme: mgr.GetScheme(), - // S3ClientCache: s3ClientCache, - // S3UserDeletion: s3userDeletion, - // OverrideExistingSecret: overrideExistingSecret, - // S3LabelSelectorValue: s3LabelSelector, - // }).SetupWithManager(mgr); err != nil { - // setupLog.Error(err, "unable to create controller", "controller", "S3User") - // os.Exit(1) - // } + if err = (&controllers.PathReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + PathDeletion: pathDeletion, + S3LabelSelectorValue: s3LabelSelector, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Path") + os.Exit(1) + } + if err = (&controllers.PolicyReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + PolicyDeletion: policyDeletion, + S3LabelSelectorValue: s3LabelSelector, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Policy") + os.Exit(1) + } + if err = (&controllers.S3UserReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + S3UserDeletion: s3userDeletion, + OverrideExistingSecret: overrideExistingSecret, + S3LabelSelectorValue: s3LabelSelector, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "S3User") + os.Exit(1) + } if err = (&controllers.S3InstanceReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(),