Merge pull request #1585 from fluxcd/bucket-sts-endpoint-ldap

Add LDAP provider for Bucket STS API
This commit is contained in:
Matheus Pimenta 2024-08-22 08:50:09 -03:00 committed by GitHub
commit 74e82d2467
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 811 additions and 94 deletions

View File

@ -49,8 +49,11 @@ const (
// BucketSpec specifies the required configuration to produce an Artifact for // BucketSpec specifies the required configuration to produce an Artifact for
// an object storage bucket. // an object storage bucket.
// +kubebuilder:validation:XValidation:rule="self.provider == 'aws' || !has(self.sts)", message="STS configuration is only supported for the 'aws' Bucket provider" // +kubebuilder:validation:XValidation:rule="self.provider == 'aws' || self.provider == 'generic' || !has(self.sts)", message="STS configuration is only supported for the 'aws' and 'generic' Bucket providers"
// +kubebuilder:validation:XValidation:rule="self.provider != 'aws' || !has(self.sts) || self.sts.provider == 'aws'", message="'aws' is the only supported STS provider for the 'aws' Bucket provider" // +kubebuilder:validation:XValidation:rule="self.provider != 'aws' || !has(self.sts) || self.sts.provider == 'aws'", message="'aws' is the only supported STS provider for the 'aws' Bucket provider"
// +kubebuilder:validation:XValidation:rule="self.provider != 'generic' || !has(self.sts) || self.sts.provider == 'ldap'", message="'ldap' is the only supported STS provider for the 'generic' Bucket provider"
// +kubebuilder:validation:XValidation:rule="!has(self.sts) || self.sts.provider != 'aws' || !has(self.sts.secretRef)", message="spec.sts.secretRef is not required for the 'aws' STS provider"
// +kubebuilder:validation:XValidation:rule="!has(self.sts) || self.sts.provider != 'aws' || !has(self.sts.certSecretRef)", message="spec.sts.certSecretRef is not required for the 'aws' STS provider"
type BucketSpec struct { type BucketSpec struct {
// Provider of the object storage bucket. // Provider of the object storage bucket.
// Defaults to 'generic', which expects an S3 (API) compatible object // Defaults to 'generic', which expects an S3 (API) compatible object
@ -72,7 +75,7 @@ type BucketSpec struct {
// Service for fetching temporary credentials to authenticate in a // Service for fetching temporary credentials to authenticate in a
// Bucket provider. // Bucket provider.
// //
// This field is only supported for the `aws` provider. // This field is only supported for the `aws` and `generic` providers.
// +optional // +optional
STS *BucketSTSSpec `json:"sts,omitempty"` STS *BucketSTSSpec `json:"sts,omitempty"`
@ -153,7 +156,7 @@ type BucketSpec struct {
// provider. // provider.
type BucketSTSSpec struct { type BucketSTSSpec struct {
// Provider of the Security Token Service. // Provider of the Security Token Service.
// +kubebuilder:validation:Enum=aws // +kubebuilder:validation:Enum=aws;ldap
// +required // +required
Provider string `json:"provider"` Provider string `json:"provider"`
@ -162,6 +165,29 @@ type BucketSTSSpec struct {
// +required // +required
// +kubebuilder:validation:Pattern="^(http|https)://.*$" // +kubebuilder:validation:Pattern="^(http|https)://.*$"
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
// SecretRef specifies the Secret containing authentication credentials
// for the STS endpoint. This Secret must contain the fields `username`
// and `password` and is supported only for the `ldap` provider.
// +optional
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
// CertSecretRef can be given the name of a Secret containing
// either or both of
//
// - a PEM-encoded client certificate (`tls.crt`) and private
// key (`tls.key`);
// - a PEM-encoded CA certificate (`ca.crt`)
//
// and whichever are supplied, will be used for connecting to the
// STS endpoint. The client cert and key are useful if you are
// authenticating with a certificate; the CA cert is useful if
// you are using a self-signed server certificate. The Secret must
// be of type `Opaque` or `kubernetes.io/tls`.
//
// This field is only supported for the `ldap` provider.
// +optional
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
} }
// BucketStatus records the observed state of a Bucket. // BucketStatus records the observed state of a Bucket.

View File

@ -20,4 +20,7 @@ const (
// STSProviderAmazon represents the AWS provider for Security Token Service. // STSProviderAmazon represents the AWS provider for Security Token Service.
// Provides support for fetching temporary credentials from an AWS STS endpoint. // Provides support for fetching temporary credentials from an AWS STS endpoint.
STSProviderAmazon string = "aws" STSProviderAmazon string = "aws"
// STSProviderLDAP represents the LDAP provider for Security Token Service.
// Provides support for fetching temporary credentials from an LDAP endpoint.
STSProviderLDAP string = "ldap"
) )

View File

@ -118,6 +118,16 @@ func (in *BucketList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BucketSTSSpec) DeepCopyInto(out *BucketSTSSpec) { func (in *BucketSTSSpec) DeepCopyInto(out *BucketSTSSpec) {
*out = *in *out = *in
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(meta.LocalObjectReference)
**out = **in
}
if in.CertSecretRef != nil {
in, out := &in.CertSecretRef, &out.CertSecretRef
*out = new(meta.LocalObjectReference)
**out = **in
}
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BucketSTSSpec. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BucketSTSSpec.
@ -136,7 +146,7 @@ func (in *BucketSpec) DeepCopyInto(out *BucketSpec) {
if in.STS != nil { if in.STS != nil {
in, out := &in.STS, &out.STS in, out := &in.STS, &out.STS
*out = new(BucketSTSSpec) *out = new(BucketSTSSpec)
**out = **in (*in).DeepCopyInto(*out)
} }
if in.SecretRef != nil { if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef in, out := &in.SecretRef, &out.SecretRef

View File

@ -424,8 +424,34 @@ spec:
Bucket provider. Bucket provider.
This field is only supported for the `aws` provider. This field is only supported for the `aws` and `generic` providers.
properties: properties:
certSecretRef:
description: |-
CertSecretRef can be given the name of a Secret containing
either or both of
- a PEM-encoded client certificate (`tls.crt`) and private
key (`tls.key`);
- a PEM-encoded CA certificate (`ca.crt`)
and whichever are supplied, will be used for connecting to the
STS endpoint. The client cert and key are useful if you are
authenticating with a certificate; the CA cert is useful if
you are using a self-signed server certificate. The Secret must
be of type `Opaque` or `kubernetes.io/tls`.
This field is only supported for the `ldap` provider.
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
endpoint: endpoint:
description: |- description: |-
Endpoint is the HTTP/S endpoint of the Security Token Service from Endpoint is the HTTP/S endpoint of the Security Token Service from
@ -436,7 +462,20 @@ spec:
description: Provider of the Security Token Service. description: Provider of the Security Token Service.
enum: enum:
- aws - aws
- ldap
type: string type: string
secretRef:
description: |-
SecretRef specifies the Secret containing authentication credentials
for the STS endpoint. This Secret must contain the fields `username`
and `password` and is supported only for the `ldap` provider.
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
required: required:
- endpoint - endpoint
- provider - provider
@ -457,12 +496,21 @@ spec:
- interval - interval
type: object type: object
x-kubernetes-validations: x-kubernetes-validations:
- message: STS configuration is only supported for the 'aws' Bucket provider - message: STS configuration is only supported for the 'aws' and 'generic'
rule: self.provider == 'aws' || !has(self.sts) Bucket providers
rule: self.provider == 'aws' || self.provider == 'generic' || !has(self.sts)
- message: '''aws'' is the only supported STS provider for the ''aws'' - message: '''aws'' is the only supported STS provider for the ''aws''
Bucket provider' Bucket provider'
rule: self.provider != 'aws' || !has(self.sts) || self.sts.provider rule: self.provider != 'aws' || !has(self.sts) || self.sts.provider
== 'aws' == 'aws'
- message: '''ldap'' is the only supported STS provider for the ''generic''
Bucket provider'
rule: self.provider != 'generic' || !has(self.sts) || self.sts.provider
== 'ldap'
- message: spec.sts.secretRef is not required for the 'aws' STS provider
rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.secretRef)'
- message: spec.sts.certSecretRef is not required for the 'aws' STS provider
rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.certSecretRef)'
status: status:
default: default:
observedGeneration: -1 observedGeneration: -1

View File

@ -126,7 +126,7 @@ BucketSTSSpec
<p>STS specifies the required configuration to use a Security Token <p>STS specifies the required configuration to use a Security Token
Service for fetching temporary credentials to authenticate in a Service for fetching temporary credentials to authenticate in a
Bucket provider.</p> Bucket provider.</p>
<p>This field is only supported for the <code>aws</code> provider.</p> <p>This field is only supported for the <code>aws</code> and <code>generic</code> providers.</p>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -1497,6 +1497,48 @@ string
where temporary credentials will be fetched.</p> where temporary credentials will be fetched.</p>
</td> </td>
</tr> </tr>
<tr>
<td>
<code>secretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>SecretRef specifies the Secret containing authentication credentials
for the STS endpoint. This Secret must contain the fields <code>username</code>
and <code>password</code> and is supported only for the <code>ldap</code> provider.</p>
</td>
</tr>
<tr>
<td>
<code>certSecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>CertSecretRef can be given the name of a Secret containing
either or both of</p>
<ul>
<li>a PEM-encoded client certificate (<code>tls.crt</code>) and private
key (<code>tls.key</code>);</li>
<li>a PEM-encoded CA certificate (<code>ca.crt</code>)</li>
</ul>
<p>and whichever are supplied, will be used for connecting to the
STS endpoint. The client cert and key are useful if you are
authenticating with a certificate; the CA cert is useful if
you are using a self-signed server certificate. The Secret must
be of type <code>Opaque</code> or <code>kubernetes.io/tls</code>.</p>
<p>This field is only supported for the <code>ldap</code> provider.</p>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -1569,7 +1611,7 @@ BucketSTSSpec
<p>STS specifies the required configuration to use a Security Token <p>STS specifies the required configuration to use a Security Token
Service for fetching temporary credentials to authenticate in a Service for fetching temporary credentials to authenticate in a
Bucket provider.</p> Bucket provider.</p>
<p>This field is only supported for the <code>aws</code> provider.</p> <p>This field is only supported for the <code>aws</code> and <code>generic</code> providers.</p>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -756,15 +756,75 @@ configuration. A Security Token Service (STS) is a web service that issues
temporary security credentials. By adding this field, one may specify the temporary security credentials. By adding this field, one may specify the
STS endpoint from where temporary credentials will be fetched. STS endpoint from where temporary credentials will be fetched.
This field is only supported for the `aws` and `generic` bucket [providers](#provider).
If using `.spec.sts`, the following fields are required: If using `.spec.sts`, the following fields are required:
- `.spec.sts.provider`, the Security Token Service provider. The only supported - `.spec.sts.provider`, the Security Token Service provider. The only supported
option is `aws`. option for the `generic` bucket provider is `ldap`. The only supported option
for the `aws` bucket provider is `aws`.
- `.spec.sts.endpoint`, the HTTP/S endpoint of the Security Token Service. In - `.spec.sts.endpoint`, the HTTP/S endpoint of the Security Token Service. In
the case of AWS, this can be `https://sts.amazonaws.com`, or a Regional STS the case of `aws` this can be `https://sts.amazonaws.com`, or a Regional STS
Endpoint, or an Interface Endpoint created inside a VPC. Endpoint, or an Interface Endpoint created inside a VPC. In the case of
`ldap` this must be the LDAP server endpoint.
This field is only supported for the `aws` bucket provider. When using the `ldap` provider, the following fields may also be specified:
- `.spec.sts.secretRef.name`, the name of the Secret containing the LDAP
credentials. The Secret must contain the following keys:
- `username`, the username to authenticate with.
- `password`, the password to authenticate with.
- `.spec.sts.certSecretRef.name`, the name of the Secret containing the
TLS configuration for communicating with the STS endpoint. The contents
of this Secret must follow the same structure of
[`.spec.certSecretRef.name`](#cert-secret-reference).
If [`.spec.proxySecretRef.name`](#proxy-secret-reference) is specified,
the proxy configuration will be used for commucating with the STS endpoint.
Example for the `ldap` provider:
```yaml
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: Bucket
metadata:
name: example
namespace: example
spec:
interval: 5m
bucketName: example
provider: generic
endpoint: minio.example.com
sts:
provider: ldap
endpoint: https://ldap.example.com
secretRef:
name: ldap-credentials
certSecretRef:
name: ldap-tls
---
apiVersion: v1
kind: Secret
metadata:
name: ldap-credentials
namespace: example
type: Opaque
stringData:
username: <username>
password: <password>
---
apiVersion: v1
kind: Secret
metadata:
name: ldap-tls
namespace: example
type: kubernetes.io/tls # or Opaque
stringData:
tls.crt: <PEM-encoded cert>
tls.key: <PEM-encoded key>
ca.crt: <PEM-encoded cert>
```
### Bucket name ### Bucket name

View File

@ -483,8 +483,27 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.Serial
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
tlsConfig, err := r.getTLSConfig(ctx, obj.Spec.CertSecretRef, obj.GetNamespace(), obj.Spec.Endpoint)
if err != nil {
e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e
}
stsSecret, err := r.getSTSSecret(ctx, obj)
if err != nil {
e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e
}
stsTLSConfig, err := r.getSTSTLSConfig(ctx, obj)
if err != nil {
err := fmt.Errorf("failed to get STS TLS config: %w", err)
e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e
}
if sts := obj.Spec.STS; sts != nil { if sts := obj.Spec.STS; sts != nil {
if err := minio.ValidateSTSProvider(obj.Spec.Provider, sts.Provider); err != nil { if err := minio.ValidateSTSProvider(obj.Spec.Provider, sts); err != nil {
e := serror.NewStalling(err, sourcev1.InvalidSTSConfigurationReason) e := serror.NewStalling(err, sourcev1.InvalidSTSConfigurationReason)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
@ -495,12 +514,11 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.Serial
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
} if err := minio.ValidateSTSSecret(sts.Provider, stsSecret); err != nil {
tlsConfig, err := r.getTLSConfig(ctx, obj) e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason)
if err != nil { conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason) return sreconcile.ResultEmpty, e
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) }
return sreconcile.ResultEmpty, e
} }
var opts []minio.Option var opts []minio.Option
if secret != nil { if secret != nil {
@ -512,6 +530,12 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.Serial
if proxyURL != nil { if proxyURL != nil {
opts = append(opts, minio.WithProxyURL(proxyURL)) opts = append(opts, minio.WithProxyURL(proxyURL))
} }
if stsSecret != nil {
opts = append(opts, minio.WithSTSSecret(stsSecret))
}
if stsTLSConfig != nil {
opts = append(opts, minio.WithSTSTLSConfig(stsTLSConfig))
}
if provider, err = minio.NewClient(obj, opts...); err != nil { if provider, err = minio.NewClient(obj, opts...); err != nil {
e := serror.NewGeneric(err, "ClientError") e := serror.NewGeneric(err, "ClientError")
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
@ -732,12 +756,15 @@ func (r *BucketReconciler) getSecret(ctx context.Context, secretRef *meta.LocalO
return secret, nil return secret, nil
} }
func (r *BucketReconciler) getTLSConfig(ctx context.Context, obj *bucketv1.Bucket) (*stdtls.Config, error) { // getTLSConfig attempts to fetch a TLS configuration from the given
certSecret, err := r.getSecret(ctx, obj.Spec.CertSecretRef, obj.GetNamespace()) // Secret reference, namespace and endpoint.
func (r *BucketReconciler) getTLSConfig(ctx context.Context,
secretRef *meta.LocalObjectReference, namespace, endpoint string) (*stdtls.Config, error) {
certSecret, err := r.getSecret(ctx, secretRef, namespace)
if err != nil || certSecret == nil { if err != nil || certSecret == nil {
return nil, err return nil, err
} }
tlsConfig, _, err := tls.KubeTLSClientConfigFromSecret(*certSecret, obj.Spec.Endpoint) tlsConfig, _, err := tls.KubeTLSClientConfigFromSecret(*certSecret, endpoint)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create TLS config: %w", err) return nil, fmt.Errorf("failed to create TLS config: %w", err)
} }
@ -747,6 +774,8 @@ func (r *BucketReconciler) getTLSConfig(ctx context.Context, obj *bucketv1.Bucke
return tlsConfig, nil return tlsConfig, nil
} }
// getProxyURL attempts to fetch a proxy URL from the object's proxy secret
// reference.
func (r *BucketReconciler) getProxyURL(ctx context.Context, obj *bucketv1.Bucket) (*url.URL, error) { func (r *BucketReconciler) getProxyURL(ctx context.Context, obj *bucketv1.Bucket) (*url.URL, error) {
namespace := obj.GetNamespace() namespace := obj.GetNamespace()
proxySecret, err := r.getSecret(ctx, obj.Spec.ProxySecretRef, namespace) proxySecret, err := r.getSecret(ctx, obj.Spec.ProxySecretRef, namespace)
@ -771,6 +800,24 @@ func (r *BucketReconciler) getProxyURL(ctx context.Context, obj *bucketv1.Bucket
return proxyURL, nil return proxyURL, nil
} }
// getSTSSecret attempts to fetch the secret from the object's STS secret
// reference.
func (r *BucketReconciler) getSTSSecret(ctx context.Context, obj *bucketv1.Bucket) (*corev1.Secret, error) {
if obj.Spec.STS == nil {
return nil, nil
}
return r.getSecret(ctx, obj.Spec.STS.SecretRef, obj.GetNamespace())
}
// getSTSTLSConfig attempts to fetch the certificate secret from the object's
// STS configuration.
func (r *BucketReconciler) getSTSTLSConfig(ctx context.Context, obj *bucketv1.Bucket) (*stdtls.Config, error) {
if obj.Spec.STS == nil {
return nil, nil
}
return r.getTLSConfig(ctx, obj.Spec.STS.CertSecretRef, obj.GetNamespace(), obj.Spec.STS.Endpoint)
}
// eventLogf records events, and logs at the same time. // eventLogf records events, and logs at the same time.
// //
// This log is different from the debug log in the EventRecorder, in the sense // This log is different from the debug log in the EventRecorder, in the sense

View File

@ -592,6 +592,94 @@ func TestBucketReconciler_reconcileSource_generic(t *testing.T) {
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "invalid proxy secret '/dummy': key 'address' is missing"), *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "invalid proxy secret '/dummy': key 'address' is missing"),
}, },
}, },
{
name: "Observes non-existing sts.secretRef",
bucketName: "dummy",
beforeFunc: func(obj *bucketv1.Bucket) {
obj.Spec.STS = &bucketv1.BucketSTSSpec{
SecretRef: &meta.LocalObjectReference{Name: "dummy"},
}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
},
wantErr: true,
assertIndex: index.NewDigester(),
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret '/dummy': secrets \"dummy\" not found"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
},
},
{
name: "Observes invalid sts.secretRef",
bucketName: "dummy",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "dummy",
},
},
beforeFunc: func(obj *bucketv1.Bucket) {
obj.Spec.Provider = "generic"
obj.Spec.STS = &bucketv1.BucketSTSSpec{
Provider: "ldap",
Endpoint: "https://something",
SecretRef: &meta.LocalObjectReference{Name: "dummy"},
}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
},
wantErr: true,
assertIndex: index.NewDigester(),
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "invalid 'dummy' secret data for 'ldap' STS provider: required fields username, password"),
},
},
{
name: "Observes non-existing sts.certSecretRef",
bucketName: "dummy",
beforeFunc: func(obj *bucketv1.Bucket) {
obj.Spec.STS = &bucketv1.BucketSTSSpec{
CertSecretRef: &meta.LocalObjectReference{Name: "dummy"},
}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
},
wantErr: true,
assertIndex: index.NewDigester(),
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret '/dummy': secrets \"dummy\" not found"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
},
},
{
name: "Observes invalid sts.certSecretRef",
bucketName: "dummy",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "dummy",
},
},
beforeFunc: func(obj *bucketv1.Bucket) {
obj.Spec.Provider = "generic"
obj.Spec.STS = &bucketv1.BucketSTSSpec{
Provider: "ldap",
Endpoint: "https://something",
CertSecretRef: &meta.LocalObjectReference{Name: "dummy"},
}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
},
wantErr: true,
assertIndex: index.NewDigester(),
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get STS TLS config: certificate secret does not contain any TLS configuration"),
},
},
{ {
name: "Observes non-existing bucket name", name: "Observes non-existing bucket name",
bucketName: "dummy", bucketName: "dummy",
@ -609,7 +697,7 @@ func TestBucketReconciler_reconcileSource_generic(t *testing.T) {
}, },
}, },
{ {
name: "Observes incompatible STS provider", name: "Observes incompatible sts.provider",
bucketName: "dummy", bucketName: "dummy",
beforeFunc: func(obj *bucketv1.Bucket) { beforeFunc: func(obj *bucketv1.Bucket) {
obj.Spec.Provider = "generic" obj.Spec.Provider = "generic"
@ -622,18 +710,18 @@ func TestBucketReconciler_reconcileSource_generic(t *testing.T) {
wantErr: true, wantErr: true,
assertIndex: index.NewDigester(), assertIndex: index.NewDigester(),
assertConditions: []metav1.Condition{ assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.InvalidSTSConfigurationReason, "STS configuration is not supported for 'generic' bucket provider"), *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.InvalidSTSConfigurationReason, "STS provider 'aws' is not supported for 'generic' bucket provider"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"), *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"), *conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
}, },
}, },
{ {
name: "Observes invalid STS endpoint", name: "Observes invalid sts.endpoint",
bucketName: "dummy", bucketName: "dummy",
beforeFunc: func(obj *bucketv1.Bucket) { beforeFunc: func(obj *bucketv1.Bucket) {
obj.Spec.Provider = "aws" // TODO: change to generic when ldap STS provider is implemented obj.Spec.Provider = "generic"
obj.Spec.STS = &bucketv1.BucketSTSSpec{ obj.Spec.STS = &bucketv1.BucketSTSSpec{
Provider: "aws", // TODO: change to ldap when ldap STS provider is implemented Provider: "ldap",
Endpoint: "something\t", Endpoint: "something\t",
} }
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo") conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
@ -1863,7 +1951,7 @@ func TestBucketReconciler_APIServerValidation_STS(t *testing.T) {
Provider: "aws", Provider: "aws",
Endpoint: "http://test", Endpoint: "http://test",
}, },
err: "STS configuration is only supported for the 'aws' Bucket provider", err: "STS configuration is only supported for the 'aws' and 'generic' Bucket providers",
}, },
{ {
name: "azure unsupported", name: "azure unsupported",
@ -1872,16 +1960,7 @@ func TestBucketReconciler_APIServerValidation_STS(t *testing.T) {
Provider: "aws", Provider: "aws",
Endpoint: "http://test", Endpoint: "http://test",
}, },
err: "STS configuration is only supported for the 'aws' Bucket provider", err: "STS configuration is only supported for the 'aws' and 'generic' Bucket providers",
},
{
name: "generic unsupported",
bucketProvider: "generic",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "aws",
Endpoint: "http://test",
},
err: "STS configuration is only supported for the 'aws' Bucket provider",
}, },
{ {
name: "aws supported", name: "aws supported",
@ -1916,16 +1995,70 @@ func TestBucketReconciler_APIServerValidation_STS(t *testing.T) {
name: "aws can be created without STS config", name: "aws can be created without STS config",
bucketProvider: "aws", bucketProvider: "aws",
}, },
// Can't be tested at present with only one allowed sts provider. {
// { name: "ldap unsupported for aws",
// name: "ldap unsupported for aws", bucketProvider: "aws",
// bucketProvider: "aws", stsConfig: &bucketv1.BucketSTSSpec{
// stsConfig: &bucketv1.BucketSTSSpec{ Provider: "ldap",
// Provider: "ldap", Endpoint: "http://test",
// Endpoint: "http://test", },
// }, err: "'aws' is the only supported STS provider for the 'aws' Bucket provider",
// err: "'aws' is the only supported STS provider for the 'aws' Bucket provider", },
// }, {
name: "aws unsupported for generic",
bucketProvider: "generic",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "aws",
Endpoint: "http://test",
},
err: "'ldap' is the only supported STS provider for the 'generic' Bucket provider",
},
{
name: "aws does not require a secret",
bucketProvider: "aws",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "aws",
Endpoint: "http://test",
SecretRef: &meta.LocalObjectReference{},
},
err: "spec.sts.secretRef is not required for the 'aws' STS provider",
},
{
name: "aws does not require a cert secret",
bucketProvider: "aws",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "aws",
Endpoint: "http://test",
CertSecretRef: &meta.LocalObjectReference{},
},
err: "spec.sts.certSecretRef is not required for the 'aws' STS provider",
},
{
name: "ldap may use a secret",
bucketProvider: "generic",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "ldap",
Endpoint: "http://test",
SecretRef: &meta.LocalObjectReference{},
},
},
{
name: "ldap may use a cert secret",
bucketProvider: "generic",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "ldap",
Endpoint: "http://test",
CertSecretRef: &meta.LocalObjectReference{},
},
},
{
name: "ldap may not use a secret or cert secret",
bucketProvider: "generic",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "ldap",
Endpoint: "http://test",
},
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
@ -40,9 +41,11 @@ type MinioClient struct {
// options holds the configuration for the Minio client. // options holds the configuration for the Minio client.
type options struct { type options struct {
secret *corev1.Secret secret *corev1.Secret
tlsConfig *tls.Config stsSecret *corev1.Secret
proxyURL *url.URL tlsConfig *tls.Config
stsTLSConfig *tls.Config
proxyURL *url.URL
} }
// Option is a function that configures the Minio client. // Option is a function that configures the Minio client.
@ -69,6 +72,20 @@ func WithProxyURL(proxyURL *url.URL) Option {
} }
} }
// WithSTSSecret sets the STS secret for the Minio client.
func WithSTSSecret(secret *corev1.Secret) Option {
return func(o *options) {
o.stsSecret = secret
}
}
// WithSTSTLSConfig sets the STS TLS configuration for the Minio client.
func WithSTSTLSConfig(tlsConfig *tls.Config) Option {
return func(o *options) {
o.stsTLSConfig = tlsConfig
}
}
// NewClient creates a new Minio storage client. // NewClient creates a new Minio storage client.
func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) { func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
var o options var o options
@ -89,6 +106,8 @@ func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
minioOpts.Creds = newCredsFromSecret(o.secret) minioOpts.Creds = newCredsFromSecret(o.secret)
case bucketProvider == sourcev1.AmazonBucketProvider: case bucketProvider == sourcev1.AmazonBucketProvider:
minioOpts.Creds = newAWSCreds(bucket, o.proxyURL) minioOpts.Creds = newAWSCreds(bucket, o.proxyURL)
case bucketProvider == sourcev1.GenericBucketProvider:
minioOpts.Creds = newGenericCreds(bucket, &o)
} }
var transportOpts []func(*http.Transport) var transportOpts []func(*http.Transport)
@ -159,6 +178,43 @@ func newAWSCreds(bucket *sourcev1.Bucket, proxyURL *url.URL) *credentials.Creden
return creds return creds
} }
// newGenericCreds creates a new Minio credentials object for the `generic` bucket provider.
func newGenericCreds(bucket *sourcev1.Bucket, o *options) *credentials.Credentials {
sts := bucket.Spec.STS
if sts == nil {
return nil
}
switch sts.Provider {
case sourcev1.STSProviderLDAP:
client := &http.Client{Transport: http.DefaultTransport}
if o.proxyURL != nil || o.stsTLSConfig != nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
if o.proxyURL != nil {
transport.Proxy = http.ProxyURL(o.proxyURL)
}
if o.stsTLSConfig != nil {
transport.TLSClientConfig = o.stsTLSConfig.Clone()
}
client = &http.Client{Transport: transport}
}
var username, password string
if o.stsSecret != nil {
username = string(o.stsSecret.Data["username"])
password = string(o.stsSecret.Data["password"])
}
return credentials.New(&credentials.LDAPIdentity{
Client: client,
STSEndpoint: sts.Endpoint,
LDAPUsername: username,
LDAPPassword: password,
})
}
return nil
}
// ValidateSecret validates the credential secret. The provided Secret may // ValidateSecret validates the credential secret. The provided Secret may
// be nil. // be nil.
func ValidateSecret(secret *corev1.Secret) error { func ValidateSecret(secret *corev1.Secret) error {
@ -176,14 +232,31 @@ func ValidateSecret(secret *corev1.Secret) error {
} }
// ValidateSTSProvider validates the STS provider. // ValidateSTSProvider validates the STS provider.
func ValidateSTSProvider(bucketProvider, stsProvider string) error { func ValidateSTSProvider(bucketProvider string, sts *sourcev1.BucketSTSSpec) error {
errProviderIncompatbility := fmt.Errorf("STS provider '%s' is not supported for '%s' bucket provider", errProviderIncompatbility := fmt.Errorf("STS provider '%s' is not supported for '%s' bucket provider",
stsProvider, bucketProvider) sts.Provider, bucketProvider)
errSecretNotRequired := fmt.Errorf("spec.sts.secretRef is not required for the '%s' STS provider",
sts.Provider)
errCertSecretNotRequired := fmt.Errorf("spec.sts.certSecretRef is not required for the '%s' STS provider",
sts.Provider)
switch bucketProvider { switch bucketProvider {
case sourcev1.AmazonBucketProvider: case sourcev1.AmazonBucketProvider:
switch stsProvider { switch sts.Provider {
case sourcev1.STSProviderAmazon: case sourcev1.STSProviderAmazon:
if sts.SecretRef != nil {
return errSecretNotRequired
}
if sts.CertSecretRef != nil {
return errCertSecretNotRequired
}
return nil
default:
return errProviderIncompatbility
}
case sourcev1.GenericBucketProvider:
switch sts.Provider {
case sourcev1.STSProviderLDAP:
return nil return nil
default: default:
return errProviderIncompatbility return errProviderIncompatbility
@ -193,6 +266,36 @@ func ValidateSTSProvider(bucketProvider, stsProvider string) error {
return fmt.Errorf("STS configuration is not supported for '%s' bucket provider", bucketProvider) return fmt.Errorf("STS configuration is not supported for '%s' bucket provider", bucketProvider)
} }
// ValidateSTSSecret validates the STS secret. The provided Secret may be nil.
func ValidateSTSSecret(stsProvider string, secret *corev1.Secret) error {
switch stsProvider {
case sourcev1.STSProviderLDAP:
return validateSTSSecretForProvider(stsProvider, secret, "username", "password")
default:
return nil
}
}
// validateSTSSecretForProvider validates the STS secret for each provider.
// The provided Secret may be nil.
func validateSTSSecretForProvider(stsProvider string, secret *corev1.Secret, keys ...string) error {
if secret == nil {
return nil
}
err := fmt.Errorf("invalid '%s' secret data for '%s' STS provider: required fields %s",
secret.Name, stsProvider, strings.Join(keys, ", "))
if len(secret.Data) == 0 {
return err
}
for _, key := range keys {
value, ok := secret.Data[key]
if !ok || len(value) == 0 {
return err
}
}
return nil
}
// FGetObject gets the object from the provided object storage bucket, and // FGetObject gets the object from the provided object storage bucket, and
// writes it to targetPath. // writes it to targetPath.
// It returns the etag of the successfully fetched file, or any error. // It returns the etag of the successfully fetched file, or any error.

View File

@ -21,6 +21,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@ -70,6 +71,11 @@ var (
testMinioClient *MinioClient testMinioClient *MinioClient
// testTLSConfig is the TLS configuration used to connect to the Minio server. // testTLSConfig is the TLS configuration used to connect to the Minio server.
testTLSConfig *tls.Config testTLSConfig *tls.Config
// testServerCert is the path to the server certificate used to start the Minio
// and STS servers.
testServerCert string
// testServerKey is the path to the server key used to start the Minio and STS servers.
testServerKey string
) )
var ( var (
@ -128,8 +134,7 @@ func TestMain(m *testing.M) {
// Load a private key and certificate from a self-signed CA for the Minio server and // Load a private key and certificate from a self-signed CA for the Minio server and
// a client TLS configuration to connect to the Minio server. // a client TLS configuration to connect to the Minio server.
var serverCert, serverKey string testServerCert, testServerKey, testTLSConfig, err = loadServerCertAndClientTLSConfig()
serverCert, serverKey, testTLSConfig, err = loadServerCertAndClientTLSConfig()
if err != nil { if err != nil {
log.Fatalf("could not load server cert and client TLS config: %s", err) log.Fatalf("could not load server cert and client TLS config: %s", err)
} }
@ -148,8 +153,8 @@ func TestMain(m *testing.M) {
}, },
Cmd: []string{"server", "/data", "--console-address", ":9001"}, Cmd: []string{"server", "/data", "--console-address", ":9001"},
Mounts: []string{ Mounts: []string{
fmt.Sprintf("%s:/root/.minio/certs/public.crt", serverCert), fmt.Sprintf("%s:/root/.minio/certs/public.crt", testServerCert),
fmt.Sprintf("%s:/root/.minio/certs/private.key", serverKey), fmt.Sprintf("%s:/root/.minio/certs/private.key", testServerKey),
}, },
}, func(config *docker.HostConfig) { }, func(config *docker.HostConfig) {
config.AutoRemove = true config.AutoRemove = true
@ -247,24 +252,24 @@ func TestFGetObject(t *testing.T) {
} }
func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) { func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) {
// start a mock STS server // start a mock AWS STS server
stsListener, stsAddr, stsPort := testlistener.New(t) awsSTSListener, awsSTSAddr, awsSTSPort := testlistener.New(t)
stsEndpoint := fmt.Sprintf("http://%s", stsAddr) awsSTSEndpoint := fmt.Sprintf("http://%s", awsSTSAddr)
stsHandler := http.NewServeMux() awsSTSHandler := http.NewServeMux()
stsHandler.HandleFunc("PUT "+credentials.TokenPath, awsSTSHandler.HandleFunc("PUT "+credentials.TokenPath,
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("mock-token")) _, err := w.Write([]byte("mock-token"))
assert.NilError(t, err) assert.NilError(t, err)
}) })
stsHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath, awsSTSHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath,
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get(credentials.TokenRequestHeader) token := r.Header.Get(credentials.TokenRequestHeader)
assert.Equal(t, token, "mock-token") assert.Equal(t, token, "mock-token")
_, err := w.Write([]byte("mock-role")) _, err := w.Write([]byte("mock-role"))
assert.NilError(t, err) assert.NilError(t, err)
}) })
var roleCredsRetrieved bool var credsRetrieved bool
stsHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath+"mock-role", awsSTSHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath+"mock-role",
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get(credentials.TokenRequestHeader) token := r.Header.Get(credentials.TokenRequestHeader)
assert.Equal(t, token, "mock-token") assert.Equal(t, token, "mock-token")
@ -274,81 +279,187 @@ func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) {
"SecretAccessKey": testMinioRootPassword, "SecretAccessKey": testMinioRootPassword,
}) })
assert.NilError(t, err) assert.NilError(t, err)
roleCredsRetrieved = true credsRetrieved = true
}) })
stsServer := &http.Server{ awsSTSServer := &http.Server{
Addr: stsAddr, Addr: awsSTSAddr,
Handler: stsHandler, Handler: awsSTSHandler,
} }
go stsServer.Serve(stsListener) go awsSTSServer.Serve(awsSTSListener)
defer stsServer.Shutdown(context.Background()) defer awsSTSServer.Shutdown(context.Background())
// start a mock LDAP STS server
ldapSTSListener, ldapSTSAddr, ldapSTSPort := testlistener.New(t)
ldapSTSEndpoint := fmt.Sprintf("https://%s", ldapSTSAddr)
ldapSTSHandler := http.NewServeMux()
var ldapUsername, ldapPassword string
ldapSTSHandler.HandleFunc("POST /",
func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
assert.NilError(t, err)
username := r.Form.Get("LDAPUsername")
password := r.Form.Get("LDAPPassword")
assert.Equal(t, username, ldapUsername)
assert.Equal(t, password, ldapPassword)
var result credentials.LDAPIdentityResult
result.Credentials.AccessKey = testMinioRootUser
result.Credentials.SecretKey = testMinioRootPassword
err = xml.NewEncoder(w).Encode(credentials.AssumeRoleWithLDAPResponse{Result: result})
assert.NilError(t, err)
credsRetrieved = true
})
ldapSTSServer := &http.Server{
Addr: ldapSTSAddr,
Handler: ldapSTSHandler,
}
go ldapSTSServer.ServeTLS(ldapSTSListener, testServerCert, testServerKey)
defer ldapSTSServer.Shutdown(context.Background())
// start proxy // start proxy
proxyAddr, proxyPort := testproxy.New(t) proxyAddr, proxyPort := testproxy.New(t)
tests := []struct { tests := []struct {
name string name string
provider string provider string
stsSpec *sourcev1.BucketSTSSpec stsSpec *sourcev1.BucketSTSSpec
opts []Option opts []Option
err string ldapUsername string
ldapPassword string
err string
}{ }{
{ {
name: "with correct endpoint", name: "with correct aws endpoint",
provider: "aws", provider: "aws",
stsSpec: &sourcev1.BucketSTSSpec{ stsSpec: &sourcev1.BucketSTSSpec{
Provider: "aws", Provider: "aws",
Endpoint: stsEndpoint, Endpoint: awsSTSEndpoint,
}, },
}, },
{ {
name: "with incorrect endpoint", name: "with incorrect aws endpoint",
provider: "aws", provider: "aws",
stsSpec: &sourcev1.BucketSTSSpec{ stsSpec: &sourcev1.BucketSTSSpec{
Provider: "aws", Provider: "aws",
Endpoint: fmt.Sprintf("http://localhost:%d", stsPort+1), Endpoint: fmt.Sprintf("http://localhost:%d", awsSTSPort+1),
}, },
err: "connection refused", err: "connection refused",
}, },
{ {
name: "with correct endpoint and proxy", name: "with correct aws endpoint and proxy",
provider: "aws", provider: "aws",
stsSpec: &sourcev1.BucketSTSSpec{ stsSpec: &sourcev1.BucketSTSSpec{
Provider: "aws", Provider: "aws",
Endpoint: stsEndpoint, Endpoint: awsSTSEndpoint,
}, },
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: proxyAddr})}, opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: proxyAddr})},
}, },
{ {
name: "with correct endpoint and incorrect proxy", name: "with correct aws endpoint and incorrect proxy",
provider: "aws", provider: "aws",
stsSpec: &sourcev1.BucketSTSSpec{ stsSpec: &sourcev1.BucketSTSSpec{
Provider: "aws", Provider: "aws",
Endpoint: stsEndpoint, Endpoint: awsSTSEndpoint,
}, },
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)})}, opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)})},
err: "connection refused", err: "connection refused",
}, },
{
name: "with correct ldap endpoint",
provider: "generic",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "ldap",
Endpoint: ldapSTSEndpoint,
},
opts: []Option{WithSTSTLSConfig(testTLSConfig)},
},
{
name: "with incorrect ldap endpoint",
provider: "generic",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "ldap",
Endpoint: fmt.Sprintf("http://localhost:%d", ldapSTSPort+1),
},
err: "connection refused",
},
{
name: "with correct ldap endpoint and secret",
provider: "generic",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "ldap",
Endpoint: ldapSTSEndpoint,
},
opts: []Option{
WithSTSTLSConfig(testTLSConfig),
WithSTSSecret(&corev1.Secret{
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte("password"),
},
}),
},
ldapUsername: "user",
ldapPassword: "password",
},
{
name: "with correct ldap endpoint and proxy",
provider: "generic",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "ldap",
Endpoint: ldapSTSEndpoint,
},
opts: []Option{
WithProxyURL(&url.URL{Scheme: "http", Host: proxyAddr}),
WithSTSTLSConfig(testTLSConfig),
},
},
{
name: "with correct ldap endpoint and incorrect proxy",
provider: "generic",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "ldap",
Endpoint: ldapSTSEndpoint,
},
opts: []Option{
WithProxyURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)}),
},
err: "connection refused",
},
{
name: "with correct ldap endpoint and without client tls config",
provider: "generic",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "ldap",
Endpoint: ldapSTSEndpoint,
},
err: "tls: failed to verify certificate: x509: certificate signed by unknown authority",
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
roleCredsRetrieved = false credsRetrieved = false
ldapUsername = tt.ldapUsername
ldapPassword = tt.ldapPassword
bucket := bucketStub(bucket, testMinioAddress) bucket := bucketStub(bucket, testMinioAddress)
bucket.Spec.Provider = tt.provider bucket.Spec.Provider = tt.provider
bucket.Spec.STS = tt.stsSpec bucket.Spec.STS = tt.stsSpec
minioClient, err := NewClient(bucket, append(tt.opts, WithTLSConfig(testTLSConfig))...)
opts := tt.opts
opts = append(opts, WithTLSConfig(testTLSConfig))
minioClient, err := NewClient(bucket, opts...)
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, minioClient != nil) assert.Assert(t, minioClient != nil)
ctx := context.Background() ctx := context.Background()
tempDir := t.TempDir() path := filepath.Join(t.TempDir(), sourceignore.IgnoreFile)
path := filepath.Join(tempDir, sourceignore.IgnoreFile)
_, err = minioClient.FGetObject(ctx, bucketName, objectName, path) _, err = minioClient.FGetObject(ctx, bucketName, objectName, path)
if tt.err != "" { if tt.err != "" {
assert.ErrorContains(t, err, tt.err) assert.ErrorContains(t, err, tt.err)
} else { } else {
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, roleCredsRetrieved) assert.Assert(t, credsRetrieved)
} }
}) })
} }
@ -477,6 +588,8 @@ func TestValidateSTSProvider(t *testing.T) {
name string name string
bucketProvider string bucketProvider string
stsProvider string stsProvider string
withSecret bool
withCertSecret bool
err string err string
}{ }{
{ {
@ -485,15 +598,52 @@ func TestValidateSTSProvider(t *testing.T) {
stsProvider: "aws", stsProvider: "aws",
}, },
{ {
name: "unsupported for aws", name: "aws does not require a secret",
bucketProvider: "aws",
stsProvider: "aws",
withSecret: true,
err: "spec.sts.secretRef is not required for the 'aws' STS provider",
},
{
name: "aws does not require a cert secret",
bucketProvider: "aws",
stsProvider: "aws",
withCertSecret: true,
err: "spec.sts.certSecretRef is not required for the 'aws' STS provider",
},
{
name: "ldap",
bucketProvider: "generic",
stsProvider: "ldap",
},
{
name: "ldap may use a secret",
bucketProvider: "generic",
stsProvider: "ldap",
withSecret: true,
},
{
name: "ldap may use a cert secret",
bucketProvider: "generic",
stsProvider: "ldap",
withCertSecret: true,
},
{
name: "ldap sts provider unsupported for aws bucket provider",
bucketProvider: "aws", bucketProvider: "aws",
stsProvider: "ldap", stsProvider: "ldap",
err: "STS provider 'ldap' is not supported for 'aws' bucket provider", err: "STS provider 'ldap' is not supported for 'aws' bucket provider",
}, },
{
name: "aws sts provider unsupported for generic bucket provider",
bucketProvider: "generic",
stsProvider: "aws",
err: "STS provider 'aws' is not supported for 'generic' bucket provider",
},
{ {
name: "unsupported bucket provider", name: "unsupported bucket provider",
bucketProvider: "gcp", bucketProvider: "gcp",
stsProvider: "gcp", stsProvider: "ldap",
err: "STS configuration is not supported for 'gcp' bucket provider", err: "STS configuration is not supported for 'gcp' bucket provider",
}, },
} }
@ -501,7 +651,102 @@ func TestValidateSTSProvider(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
err := ValidateSTSProvider(tt.bucketProvider, tt.stsProvider) sts := &sourcev1.BucketSTSSpec{
Provider: tt.stsProvider,
}
if tt.withSecret {
sts.SecretRef = &meta.LocalObjectReference{}
}
if tt.withCertSecret {
sts.CertSecretRef = &meta.LocalObjectReference{}
}
err := ValidateSTSProvider(tt.bucketProvider, sts)
if tt.err != "" {
assert.Error(t, err, tt.err)
} else {
assert.NilError(t, err)
}
})
}
}
func TestValidateSTSSecret(t *testing.T) {
t.Parallel()
tests := []struct {
name string
provider string
secret *corev1.Secret
err string
}{
{
name: "ldap provider does not require a secret",
provider: "ldap",
},
{
name: "valid ldap secret",
provider: "ldap",
secret: &corev1.Secret{
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte("pass"),
},
},
},
{
name: "empty ldap secret",
provider: "ldap",
secret: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Name: "ldap-secret"}},
err: "invalid 'ldap-secret' secret data for 'ldap' STS provider: required fields username, password",
},
{
name: "ldap secret missing password",
provider: "ldap",
secret: &corev1.Secret{
Data: map[string][]byte{
"username": []byte("user"),
},
},
err: "invalid '' secret data for 'ldap' STS provider: required fields username, password",
},
{
name: "ldap secret missing username",
provider: "ldap",
secret: &corev1.Secret{
Data: map[string][]byte{
"password": []byte("pass"),
},
},
err: "invalid '' secret data for 'ldap' STS provider: required fields username, password",
},
{
name: "ldap secret with empty username",
provider: "ldap",
secret: &corev1.Secret{
Data: map[string][]byte{
"username": []byte(""),
"password": []byte("pass"),
},
},
err: "invalid '' secret data for 'ldap' STS provider: required fields username, password",
},
{
name: "ldap secret with empty password",
provider: "ldap",
secret: &corev1.Secret{
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte(""),
},
},
err: "invalid '' secret data for 'ldap' STS provider: required fields username, password",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := ValidateSTSSecret(tt.provider, tt.secret)
if tt.err != "" { if tt.err != "" {
assert.Error(t, err, tt.err) assert.Error(t, err, tt.err)
} else { } else {