diff --git a/api/go.mod b/api/go.mod index 16e2032..cf89ef0 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,6 +3,7 @@ module github.com/fluxcd/image-reflector-controller/api go 1.17 require ( + github.com/fluxcd/pkg/apis/acl v0.0.3 github.com/fluxcd/pkg/apis/meta v0.10.2 k8s.io/apimachinery v0.23.1 sigs.k8s.io/controller-runtime v0.11.0 diff --git a/api/go.sum b/api/go.sum index 826d950..783d483 100644 --- a/api/go.sum +++ b/api/go.sum @@ -121,6 +121,8 @@ github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMi github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/pkg/apis/acl v0.0.3 h1:Lw0ZHdpnO4G7Zy9KjrzwwBmDZQuy4qEjaU/RvA6k1lc= +github.com/fluxcd/pkg/apis/acl v0.0.3/go.mod h1:XPts6lRJ9C9fIF9xVWofmQwftvhY25n1ps7W9xw0XLU= github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI= github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= diff --git a/api/v1beta1/imagerepository_types.go b/api/v1beta1/imagerepository_types.go index cb0648f..582a096 100644 --- a/api/v1beta1/imagerepository_types.go +++ b/api/v1beta1/imagerepository_types.go @@ -21,6 +21,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/fluxcd/pkg/apis/acl" "github.com/fluxcd/pkg/apis/meta" ) @@ -71,15 +72,7 @@ type ImageRepositorySpec struct { // AccessFrom defines an ACL for allowing cross-namespace references // to the ImageRepository object based on the caller's namespace labels. // +optional - AccessFrom *AccessFrom `json:"accessFrom,omitempty"` -} - -type AccessFrom struct { - NamespaceSelectors []NamespaceSelector `json:"namespaceSelectors,omitempty"` -} - -type NamespaceSelector struct { - MatchLabels map[string]string `json:"matchLabels,omitempty"` + AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"` } type ScanResult struct { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 340cf5b..366c010 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -22,33 +22,12 @@ limitations under the License. package v1beta1 import ( + "github.com/fluxcd/pkg/apis/acl" "github.com/fluxcd/pkg/apis/meta" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AccessFrom) DeepCopyInto(out *AccessFrom) { - *out = *in - if in.NamespaceSelectors != nil { - in, out := &in.NamespaceSelectors, &out.NamespaceSelectors - *out = make([]NamespaceSelector, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessFrom. -func (in *AccessFrom) DeepCopy() *AccessFrom { - if in == nil { - return nil - } - out := new(AccessFrom) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AlphabeticalPolicy) DeepCopyInto(out *AlphabeticalPolicy) { *out = *in @@ -277,7 +256,7 @@ func (in *ImageRepositorySpec) DeepCopyInto(out *ImageRepositorySpec) { } if in.AccessFrom != nil { in, out := &in.AccessFrom, &out.AccessFrom - *out = new(AccessFrom) + *out = new(acl.AccessFrom) (*in).DeepCopyInto(*out) } } @@ -320,28 +299,6 @@ func (in *ImageRepositoryStatus) DeepCopy() *ImageRepositoryStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *NamespaceSelector) DeepCopyInto(out *NamespaceSelector) { - *out = *in - if in.MatchLabels != nil { - in, out := &in.MatchLabels, &out.MatchLabels - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceSelector. -func (in *NamespaceSelector) DeepCopy() *NamespaceSelector { - if in == nil { - return nil - } - out := new(NamespaceSelector) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NumericalPolicy) DeepCopyInto(out *NumericalPolicy) { *out = *in diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml index 759b2fc..472d5c3 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml @@ -394,14 +394,27 @@ spec: labels. properties: namespaceSelectors: + description: NamespaceSelectors is the list of namespace selectors + to which this ACL applies. Items in this list are evaluated + using a logical OR operation. items: + description: NamespaceSelector selects the namespaces to which + this ACL applies. An empty map of MatchLabels matches all + namespaces in a cluster. properties: matchLabels: additionalProperties: type: string + description: MatchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. type: object type: object type: array + required: + - namespaceSelectors type: object certSecretRef: description: "CertSecretRef can be given the name of a secret containing diff --git a/controllers/imagepolicy_controller.go b/controllers/imagepolicy_controller.go index 7b39940..85076e3 100644 --- a/controllers/imagepolicy_controller.go +++ b/controllers/imagepolicy_controller.go @@ -21,10 +21,8 @@ import ( "fmt" "time" - v1 "k8s.io/api/core/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" kuberecorder "k8s.io/client-go/tools/record" @@ -36,7 +34,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" + aclapi "github.com/fluxcd/pkg/apis/acl" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/acl" "github.com/fluxcd/pkg/runtime/events" "github.com/fluxcd/pkg/runtime/metrics" @@ -115,11 +115,13 @@ func (r *ImagePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // check if we are allowed to use the referenced ImageRepository - if _, err := r.hasAccessToRepository(ctx, req, pol.Spec.ImageRepositoryRef, repo.Spec.AccessFrom); err != nil { + + aclAuth := acl.NewAuthorization(r.Client) + if err := aclAuth.HasAccessToRef(ctx, &pol, repoNamespacedName, repo.Spec.AccessFrom); err != nil { imagev1.SetImagePolicyReadiness( &pol, metav1.ConditionFalse, - "AccessDenied", + aclapi.AccessDeniedReason, err.Error(), ) if err := r.patchStatus(ctx, req, pol.Status); err != nil { @@ -327,37 +329,3 @@ func (r *ImagePolicyReconciler) patchStatus(ctx context.Context, req ctrl.Reques return r.Status().Patch(ctx, &res, patch) } - -func (r *ImagePolicyReconciler) hasAccessToRepository(ctx context.Context, policy ctrl.Request, repo meta.NamespacedObjectReference, acl *imagev1.AccessFrom) (bool, error) { - // grant access if the policy is in the same namespace as the repository - if repo.Namespace == "" || policy.Namespace == repo.Namespace { - return true, nil - } - - // deny access if the repository has no ACL defined - if acl == nil { - return false, fmt.Errorf("ImageRepository '%s/%s' can't be accessed due to missing access list", - repo.Namespace, repo.Name) - } - - // get the policy namespace labels - var policyNamespace v1.Namespace - if err := r.Get(ctx, types.NamespacedName{Name: policy.Namespace}, &policyNamespace); err != nil { - return false, err - } - policyLabels := policyNamespace.GetLabels() - - // check if the policy namespace labels match any ACL - for _, selector := range acl.NamespaceSelectors { - sel, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: selector.MatchLabels}) - if err != nil { - return false, err - } - if sel.Matches(labels.Set(policyLabels)) { - return true, nil - } - } - - return false, fmt.Errorf("ImageRepository '%s/%s' can't be accessed due to labels mismatch on namespace '%s'", - repo.Namespace, repo.Name, policy.Namespace) -} diff --git a/controllers/policy_test.go b/controllers/policy_test.go index 0369ef4..f05f671 100644 --- a/controllers/policy_test.go +++ b/controllers/policy_test.go @@ -28,6 +28,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "github.com/fluxcd/pkg/apis/acl" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" // +kubebuilder:scaffold:imports ) @@ -480,7 +482,7 @@ var _ = Describe("ImagePolicy controller", func() { Spec: imagev1.ImageRepositorySpec{ Interval: metav1.Duration{Duration: reconciliationInterval}, Image: imgRepo, - AccessFrom: &imagev1.AccessFrom{}, + AccessFrom: nil, }, } repoObjectName := types.NamespacedName{ @@ -532,7 +534,7 @@ var _ = Describe("ImagePolicy controller", func() { _ = r.Get(ctx, polObjectName, &pol) return apimeta.IsStatusConditionFalse(pol.Status.Conditions, meta.ReadyCondition) }, timeout, interval).Should(BeTrue()) - Expect(apimeta.FindStatusCondition(pol.Status.Conditions, meta.ReadyCondition).Reason).To(Equal("AccessDenied")) + Expect(apimeta.FindStatusCondition(pol.Status.Conditions, meta.ReadyCondition).Reason).To(Equal(acl.AccessDeniedReason)) Expect(r.Delete(ctx, &pol)).To(Succeed()) }) @@ -553,8 +555,8 @@ var _ = Describe("ImagePolicy controller", func() { Spec: imagev1.ImageRepositorySpec{ Interval: metav1.Duration{Duration: reconciliationInterval}, Image: imgRepo, - AccessFrom: &imagev1.AccessFrom{ - NamespaceSelectors: []imagev1.NamespaceSelector{ + AccessFrom: &acl.AccessFrom{ + NamespaceSelectors: []acl.NamespaceSelector{ { MatchLabels: make(map[string]string), }, @@ -636,8 +638,8 @@ var _ = Describe("ImagePolicy controller", func() { Spec: imagev1.ImageRepositorySpec{ Interval: metav1.Duration{Duration: reconciliationInterval}, Image: imgRepo, - AccessFrom: &imagev1.AccessFrom{ - NamespaceSelectors: []imagev1.NamespaceSelector{ + AccessFrom: &acl.AccessFrom{ + NamespaceSelectors: []acl.NamespaceSelector{ { MatchLabels: policyNamespace.Labels, }, @@ -737,8 +739,8 @@ var _ = Describe("ImagePolicy controller", func() { Spec: imagev1.ImageRepositorySpec{ Interval: metav1.Duration{Duration: reconciliationInterval}, Image: imgRepo, - AccessFrom: &imagev1.AccessFrom{ - NamespaceSelectors: []imagev1.NamespaceSelector{ + AccessFrom: &acl.AccessFrom{ + NamespaceSelectors: []acl.NamespaceSelector{ { MatchLabels: map[string]string{ "tenant": "b", diff --git a/docs/api/image-reflector.md b/docs/api/image-reflector.md index 2b1639e..606889e 100644 --- a/docs/api/image-reflector.md +++ b/docs/api/image-reflector.md @@ -12,38 +12,6 @@ OCI image repositories into a cluster, so they can be consulted for e.g., automation.

Resource Types: -

AccessFrom -

-

-(Appears on: -ImageRepositorySpec) -

-
-
- - - - - - - - - - - - - -
FieldDescription
-namespaceSelectors
- - -[]NamespaceSelector - - -
-
-
-

AlphabeticalPolicy

@@ -509,9 +477,7 @@ It does not apply to already started scans. Defaults to false.

accessFrom
- -AccessFrom - +github.com/fluxcd/pkg/apis/acl.AccessFrom @@ -655,9 +621,7 @@ It does not apply to already started scans. Defaults to false.

accessFrom
- -AccessFrom - +github.com/fluxcd/pkg/apis/acl.AccessFrom @@ -759,36 +723,6 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus -

NamespaceSelector -

-

-(Appears on: -AccessFrom) -

-
-
- - - - - - - - - - - - - -
FieldDescription
-matchLabels
- -map[string]string - -
-
-
-

NumericalPolicy

diff --git a/go.mod b/go.mod index d8dea0a..6153c53 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,9 @@ require ( github.com/aws/aws-sdk-go v1.42.9 github.com/dgraph-io/badger/v3 v3.2103.2 github.com/fluxcd/image-reflector-controller/api v0.15.0 + github.com/fluxcd/pkg/apis/acl v0.0.3 github.com/fluxcd/pkg/apis/meta v0.10.2 - github.com/fluxcd/pkg/runtime v0.12.3 + github.com/fluxcd/pkg/runtime v0.12.4 github.com/fluxcd/pkg/version v0.1.0 github.com/google/go-containerregistry v0.7.0 github.com/onsi/ginkgo v1.16.5 @@ -25,8 +26,8 @@ require ( ) require ( - github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1 // indirect cloud.google.com/go v0.97.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect @@ -49,7 +50,7 @@ require ( github.com/golang/snappy v0.0.3 // indirect github.com/google/flatbuffers v1.12.1 // indirect github.com/google/go-cmp v0.5.6 // indirect - github.com/google/gofuzz v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.2.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect @@ -77,7 +78,7 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.19.1 // indirect golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect - golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect + golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 // indirect diff --git a/go.sum b/go.sum index d22fca9..7006797 100644 --- a/go.sum +++ b/go.sum @@ -337,10 +337,12 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/pkg/apis/acl v0.0.3 h1:Lw0ZHdpnO4G7Zy9KjrzwwBmDZQuy4qEjaU/RvA6k1lc= +github.com/fluxcd/pkg/apis/acl v0.0.3/go.mod h1:XPts6lRJ9C9fIF9xVWofmQwftvhY25n1ps7W9xw0XLU= github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI= github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I= -github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0= -github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM= +github.com/fluxcd/pkg/runtime v0.12.4 h1:gA27RG/+adN2/7Qe03PB46Iwmye/MusPCpuS4zQ2fW0= +github.com/fluxcd/pkg/runtime v0.12.4/go.mod h1:gspNvhAqodZgSmK1ZhMtvARBf/NGAlxmaZaIOHkJYsc= github.com/fluxcd/pkg/version v0.1.0 h1:v+SmCanmCB5Tj2Cx9TXlj+kNRfPGbAvirkeqsp7ZEAQ= github.com/fluxcd/pkg/version v0.1.0/go.mod h1:V7Z/w8dxLQzv0FHqa5ox5TeyOd2zOd49EeuWFgnwyj4= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -461,8 +463,9 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-containerregistry v0.7.0 h1:u0onUUOcyoCDHEiJoyR1R1gx5er1+r06V5DBhUU5ndk= github.com/google/go-containerregistry v0.7.0/go.mod h1:2zaoelrL0d08gGbpdP3LqyUuBmhWbpD6IOe2s9nLS2k= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -1016,8 +1019,9 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211111160137-58aab5ef257a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 h1:kmreh1vGI63l2FxOAYS3Yv6ATsi7lSTuwNSVbGfJV9I= +golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=