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
// 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 != '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 {
// Provider of the object storage bucket.
// 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
// Bucket provider.
//
// This field is only supported for the `aws` provider.
// This field is only supported for the `aws` and `generic` providers.
// +optional
STS *BucketSTSSpec `json:"sts,omitempty"`
@ -153,7 +156,7 @@ type BucketSpec struct {
// provider.
type BucketSTSSpec struct {
// Provider of the Security Token Service.
// +kubebuilder:validation:Enum=aws
// +kubebuilder:validation:Enum=aws;ldap
// +required
Provider string `json:"provider"`
@ -162,6 +165,29 @@ type BucketSTSSpec struct {
// +required
// +kubebuilder:validation:Pattern="^(http|https)://.*$"
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.

View File

@ -20,4 +20,7 @@ const (
// STSProviderAmazon represents the AWS provider for Security Token Service.
// Provides support for fetching temporary credentials from an AWS STS endpoint.
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.
func (in *BucketSTSSpec) DeepCopyInto(out *BucketSTSSpec) {
*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.
@ -136,7 +146,7 @@ func (in *BucketSpec) DeepCopyInto(out *BucketSpec) {
if in.STS != nil {
in, out := &in.STS, &out.STS
*out = new(BucketSTSSpec)
**out = **in
(*in).DeepCopyInto(*out)
}
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef

View File

@ -424,8 +424,34 @@ spec:
Bucket provider.
This field is only supported for the `aws` provider.
This field is only supported for the `aws` and `generic` providers.
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:
description: |-
Endpoint is the HTTP/S endpoint of the Security Token Service from
@ -436,7 +462,20 @@ spec:
description: Provider of the Security Token Service.
enum:
- aws
- ldap
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:
- endpoint
- provider
@ -457,12 +496,21 @@ spec:
- interval
type: object
x-kubernetes-validations:
- message: STS configuration is only supported for the 'aws' Bucket provider
rule: self.provider == 'aws' || !has(self.sts)
- message: STS configuration is only supported for the 'aws' and 'generic'
Bucket providers
rule: self.provider == 'aws' || self.provider == 'generic' || !has(self.sts)
- message: '''aws'' is the only supported STS provider for the ''aws''
Bucket provider'
rule: self.provider != 'aws' || !has(self.sts) || self.sts.provider
== '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:
default:
observedGeneration: -1

View File

@ -126,7 +126,7 @@ BucketSTSSpec
<p>STS specifies the required configuration to use a Security Token
Service for fetching temporary credentials to authenticate in a
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>
</tr>
<tr>
@ -1497,6 +1497,48 @@ string
where temporary credentials will be fetched.</p>
</td>
</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>
</table>
</div>
@ -1569,7 +1611,7 @@ BucketSTSSpec
<p>STS specifies the required configuration to use a Security Token
Service for fetching temporary credentials to authenticate in a
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>
</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
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:
- `.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
the case of AWS, this can be `https://sts.amazonaws.com`, or a Regional STS
Endpoint, or an Interface Endpoint created inside a VPC.
the case of `aws` this can be `https://sts.amazonaws.com`, or a Regional STS
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

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)
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 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)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", 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)
return sreconcile.ResultEmpty, e
}
}
tlsConfig, err := r.getTLSConfig(ctx, obj)
if err != nil {
e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e
if err := minio.ValidateSTSSecret(sts.Provider, stsSecret); err != nil {
e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e
}
}
var opts []minio.Option
if secret != nil {
@ -512,6 +530,12 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.Serial
if proxyURL != nil {
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 {
e := serror.NewGeneric(err, "ClientError")
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
}
func (r *BucketReconciler) getTLSConfig(ctx context.Context, obj *bucketv1.Bucket) (*stdtls.Config, error) {
certSecret, err := r.getSecret(ctx, obj.Spec.CertSecretRef, obj.GetNamespace())
// getTLSConfig attempts to fetch a TLS configuration from the given
// 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 {
return nil, err
}
tlsConfig, _, err := tls.KubeTLSClientConfigFromSecret(*certSecret, obj.Spec.Endpoint)
tlsConfig, _, err := tls.KubeTLSClientConfigFromSecret(*certSecret, endpoint)
if err != nil {
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
}
// 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) {
namespace := obj.GetNamespace()
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
}
// 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.
//
// 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"),
},
},
{
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",
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",
beforeFunc: func(obj *bucketv1.Bucket) {
obj.Spec.Provider = "generic"
@ -622,18 +710,18 @@ func TestBucketReconciler_reconcileSource_generic(t *testing.T) {
wantErr: true,
assertIndex: index.NewDigester(),
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.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
},
},
{
name: "Observes invalid STS endpoint",
name: "Observes invalid sts.endpoint",
bucketName: "dummy",
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{
Provider: "aws", // TODO: change to ldap when ldap STS provider is implemented
Provider: "ldap",
Endpoint: "something\t",
}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
@ -1863,7 +1951,7 @@ func TestBucketReconciler_APIServerValidation_STS(t *testing.T) {
Provider: "aws",
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",
@ -1872,16 +1960,7 @@ func TestBucketReconciler_APIServerValidation_STS(t *testing.T) {
Provider: "aws",
Endpoint: "http://test",
},
err: "STS configuration is only supported for the 'aws' Bucket provider",
},
{
name: "generic unsupported",
bucketProvider: "generic",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "aws",
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: "aws supported",
@ -1916,16 +1995,70 @@ func TestBucketReconciler_APIServerValidation_STS(t *testing.T) {
name: "aws can be created without STS config",
bucketProvider: "aws",
},
// Can't be tested at present with only one allowed sts provider.
// {
// name: "ldap unsupported for aws",
// bucketProvider: "aws",
// stsConfig: &bucketv1.BucketSTSSpec{
// Provider: "ldap",
// Endpoint: "http://test",
// },
// err: "'aws' is the only supported STS provider for the 'aws' Bucket provider",
// },
{
name: "ldap unsupported for aws",
bucketProvider: "aws",
stsConfig: &bucketv1.BucketSTSSpec{
Provider: "ldap",
Endpoint: "http://test",
},
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 {

View File

@ -23,6 +23,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
@ -40,9 +41,11 @@ type MinioClient struct {
// options holds the configuration for the Minio client.
type options struct {
secret *corev1.Secret
tlsConfig *tls.Config
proxyURL *url.URL
secret *corev1.Secret
stsSecret *corev1.Secret
tlsConfig *tls.Config
stsTLSConfig *tls.Config
proxyURL *url.URL
}
// 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.
func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
var o options
@ -89,6 +106,8 @@ func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
minioOpts.Creds = newCredsFromSecret(o.secret)
case bucketProvider == sourcev1.AmazonBucketProvider:
minioOpts.Creds = newAWSCreds(bucket, o.proxyURL)
case bucketProvider == sourcev1.GenericBucketProvider:
minioOpts.Creds = newGenericCreds(bucket, &o)
}
var transportOpts []func(*http.Transport)
@ -159,6 +178,43 @@ func newAWSCreds(bucket *sourcev1.Bucket, proxyURL *url.URL) *credentials.Creden
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
// be nil.
func ValidateSecret(secret *corev1.Secret) error {
@ -176,14 +232,31 @@ func ValidateSecret(secret *corev1.Secret) error {
}
// 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",
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 {
case sourcev1.AmazonBucketProvider:
switch stsProvider {
switch sts.Provider {
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
default:
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)
}
// 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
// writes it to targetPath.
// It returns the etag of the successfully fetched file, or any error.

View File

@ -21,6 +21,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"log"
@ -70,6 +71,11 @@ var (
testMinioClient *MinioClient
// testTLSConfig is the TLS configuration used to connect to the Minio server.
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 (
@ -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
// a client TLS configuration to connect to the Minio server.
var serverCert, serverKey string
serverCert, serverKey, testTLSConfig, err = loadServerCertAndClientTLSConfig()
testServerCert, testServerKey, testTLSConfig, err = loadServerCertAndClientTLSConfig()
if err != nil {
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"},
Mounts: []string{
fmt.Sprintf("%s:/root/.minio/certs/public.crt", serverCert),
fmt.Sprintf("%s:/root/.minio/certs/private.key", serverKey),
fmt.Sprintf("%s:/root/.minio/certs/public.crt", testServerCert),
fmt.Sprintf("%s:/root/.minio/certs/private.key", testServerKey),
},
}, func(config *docker.HostConfig) {
config.AutoRemove = true
@ -247,24 +252,24 @@ func TestFGetObject(t *testing.T) {
}
func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) {
// start a mock STS server
stsListener, stsAddr, stsPort := testlistener.New(t)
stsEndpoint := fmt.Sprintf("http://%s", stsAddr)
stsHandler := http.NewServeMux()
stsHandler.HandleFunc("PUT "+credentials.TokenPath,
// start a mock AWS STS server
awsSTSListener, awsSTSAddr, awsSTSPort := testlistener.New(t)
awsSTSEndpoint := fmt.Sprintf("http://%s", awsSTSAddr)
awsSTSHandler := http.NewServeMux()
awsSTSHandler.HandleFunc("PUT "+credentials.TokenPath,
func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("mock-token"))
assert.NilError(t, err)
})
stsHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath,
awsSTSHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath,
func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get(credentials.TokenRequestHeader)
assert.Equal(t, token, "mock-token")
_, err := w.Write([]byte("mock-role"))
assert.NilError(t, err)
})
var roleCredsRetrieved bool
stsHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath+"mock-role",
var credsRetrieved bool
awsSTSHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath+"mock-role",
func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get(credentials.TokenRequestHeader)
assert.Equal(t, token, "mock-token")
@ -274,81 +279,187 @@ func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) {
"SecretAccessKey": testMinioRootPassword,
})
assert.NilError(t, err)
roleCredsRetrieved = true
credsRetrieved = true
})
stsServer := &http.Server{
Addr: stsAddr,
Handler: stsHandler,
awsSTSServer := &http.Server{
Addr: awsSTSAddr,
Handler: awsSTSHandler,
}
go stsServer.Serve(stsListener)
defer stsServer.Shutdown(context.Background())
go awsSTSServer.Serve(awsSTSListener)
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
proxyAddr, proxyPort := testproxy.New(t)
tests := []struct {
name string
provider string
stsSpec *sourcev1.BucketSTSSpec
opts []Option
err string
name string
provider string
stsSpec *sourcev1.BucketSTSSpec
opts []Option
ldapUsername string
ldapPassword string
err string
}{
{
name: "with correct endpoint",
name: "with correct aws endpoint",
provider: "aws",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "aws",
Endpoint: stsEndpoint,
Endpoint: awsSTSEndpoint,
},
},
{
name: "with incorrect endpoint",
name: "with incorrect aws endpoint",
provider: "aws",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "aws",
Endpoint: fmt.Sprintf("http://localhost:%d", stsPort+1),
Endpoint: fmt.Sprintf("http://localhost:%d", awsSTSPort+1),
},
err: "connection refused",
},
{
name: "with correct endpoint and proxy",
name: "with correct aws endpoint and proxy",
provider: "aws",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "aws",
Endpoint: stsEndpoint,
Endpoint: awsSTSEndpoint,
},
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",
stsSpec: &sourcev1.BucketSTSSpec{
Provider: "aws",
Endpoint: stsEndpoint,
Endpoint: awsSTSEndpoint,
},
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)})},
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 {
t.Run(tt.name, func(t *testing.T) {
roleCredsRetrieved = false
credsRetrieved = false
ldapUsername = tt.ldapUsername
ldapPassword = tt.ldapPassword
bucket := bucketStub(bucket, testMinioAddress)
bucket.Spec.Provider = tt.provider
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.Assert(t, minioClient != nil)
ctx := context.Background()
tempDir := t.TempDir()
path := filepath.Join(tempDir, sourceignore.IgnoreFile)
path := filepath.Join(t.TempDir(), sourceignore.IgnoreFile)
_, err = minioClient.FGetObject(ctx, bucketName, objectName, path)
if tt.err != "" {
assert.ErrorContains(t, err, tt.err)
} else {
assert.NilError(t, err)
assert.Assert(t, roleCredsRetrieved)
assert.Assert(t, credsRetrieved)
}
})
}
@ -477,6 +588,8 @@ func TestValidateSTSProvider(t *testing.T) {
name string
bucketProvider string
stsProvider string
withSecret bool
withCertSecret bool
err string
}{
{
@ -485,15 +598,52 @@ func TestValidateSTSProvider(t *testing.T) {
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",
stsProvider: "ldap",
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",
bucketProvider: "gcp",
stsProvider: "gcp",
stsProvider: "ldap",
err: "STS configuration is not supported for 'gcp' bucket provider",
},
}
@ -501,7 +651,102 @@ func TestValidateSTSProvider(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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 != "" {
assert.Error(t, err, tt.err)
} else {