diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index fcb08c35ecd..006de160878 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -194,7 +194,7 @@ func NewConfigValidationController(ctx context.Context, _ configmap.Watcher) *co func NewSinkBindingWebhook(opts ...psbinding.ReconcilerOption) injection.ControllerConstructor { return func(ctx context.Context, cmw configmap.Watcher) *controller.Impl { - sbresolver := sinkbinding.WithContextFactory(ctx, func(types.NamespacedName) {}) + withContext := sinkbinding.WithContextFactory(ctx, func(types.NamespacedName) {}) return psbinding.NewAdmissionController(ctx, @@ -208,7 +208,7 @@ func NewSinkBindingWebhook(opts ...psbinding.ReconcilerOption) injection.Control sinkbinding.ListAll, // How to setup the context prior to invoking Do/Undo. - sbresolver, + withContext, opts..., ) } diff --git a/config/core/resources/containersource.yaml b/config/core/resources/containersource.yaml index 50971891a1f..15a369ea509 100644 --- a/config/core/resources/containersource.yaml +++ b/config/core/resources/containersource.yaml @@ -143,6 +143,9 @@ spec: sinkCACerts: description: CACerts is the Certification Authority (CA) certificates in PEM format that the source trusts when sending events to the sink. type: string + sinkAudience: + description: Audience is the OIDC audience of the sink. + type: string additionalPrinterColumns: - name: Sink type: string diff --git a/config/core/resources/sinkbindings.yaml b/config/core/resources/sinkbindings.yaml index 5097493e7ef..a8d609604df 100644 --- a/config/core/resources/sinkbindings.yaml +++ b/config/core/resources/sinkbindings.yaml @@ -185,6 +185,12 @@ spec: sinkCACerts: description: CACerts is the Certification Authority (CA) certificates in PEM format that the source trusts when sending events to the sink. type: string + sinkAudience: + description: Audience is the OIDC audience of the sink. + type: string + oidcTokenSecretName: + description: Name of the secret with the OIDC token for the sink. + type: string additionalPrinterColumns: - name: Sink type: string diff --git a/config/core/roles/webhook-clusterrole.yaml b/config/core/roles/webhook-clusterrole.yaml index 91b6e330712..5fe8cf1ce88 100644 --- a/config/core/roles/webhook-clusterrole.yaml +++ b/config/core/roles/webhook-clusterrole.yaml @@ -109,12 +109,25 @@ rules: - "create" - "patch" -# For the SinkBinding reconciler adding the OIDC identity service accounts + # For the SinkBinding reconciler adding the OIDC identity service accounts - apiGroups: - "" resources: - "serviceaccounts" verbs: *everything + # For the SinkBinding reconciler creating the sinkbinding token secret + - apiGroups: + - "" + resources: + - "serviceaccounts/token" + verbs: + - "create" + - apiGroups: + - "" + resources: + - "secrets" + verbs: *everything + # Necessary for conversion webhook. These are copied from the serving # TODO: Do we really need all these permissions? - apiGroups: ["apiextensions.k8s.io"] diff --git a/docs/eventing-api.md b/docs/eventing-api.md index f8f61a64a2e..16a4343c97a 100644 --- a/docs/eventing-api.md +++ b/docs/eventing-api.md @@ -5958,6 +5958,18 @@ state. Source.

+ + +oidcTokenSecretName
+ +string + + + +

OIDCTokenSecretName is the name of the secret containing the token for +this SinkBindings OIDC authentication

+ +
diff --git a/go.mod b/go.mod index 868ad667d38..09214cc5cc8 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/cloudevents/sdk-go/sql/v2 v2.13.0 github.com/cloudevents/sdk-go/v2 v2.13.0 github.com/coreos/go-oidc/v3 v3.6.0 + github.com/go-jose/go-jose/v3 v3.0.0 github.com/golang/protobuf v1.5.3 github.com/google/go-cmp v0.6.0 github.com/google/gofuzz v1.2.0 @@ -71,7 +72,6 @@ require ( github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.7.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.2.4 // indirect diff --git a/pkg/apis/sources/v1/sinkbinding_lifecycle.go b/pkg/apis/sources/v1/sinkbinding_lifecycle.go index 5a8d1003554..45fe0725538 100644 --- a/pkg/apis/sources/v1/sinkbinding_lifecycle.go +++ b/pkg/apis/sources/v1/sinkbinding_lifecycle.go @@ -33,9 +33,14 @@ import ( "knative.dev/pkg/tracker" ) +const ( + oidcTokenVolumeName = "oidc-token" +) + var sbCondSet = apis.NewLivingConditionSet( SinkBindingConditionSinkProvided, SinkBindingConditionOIDCIdentityCreated, + SinkBindingConditionOIDCTokenSecretCreated, ) // GetConditionSet retrieves the condition set for this resource. Implements the KRShaped interface. @@ -90,6 +95,7 @@ func (sbs *SinkBindingStatus) MarkSink(addr *duckv1.Addressable) { if addr != nil { sbs.SinkURI = addr.URL sbs.SinkCACerts = addr.CACerts + sbs.SinkAudience = addr.Audience sbCondSet.Manage(sbs).MarkTrue(SinkBindingConditionSinkProvided) } else { sbCondSet.Manage(sbs).MarkFalse(SinkBindingConditionSinkProvided, "SinkEmpty", "Sink has resolved to empty.%s", "") @@ -112,6 +118,22 @@ func (sbs *SinkBindingStatus) MarkOIDCIdentityCreatedUnknown(reason, messageForm sbCondSet.Manage(sbs).MarkUnknown(SinkBindingConditionOIDCIdentityCreated, reason, messageFormat, messageA...) } +func (sbs *SinkBindingStatus) MarkOIDCTokenSecretCreatedSuccceeded() { + sbCondSet.Manage(sbs).MarkTrue(SinkBindingConditionOIDCTokenSecretCreated) +} + +func (sbs *SinkBindingStatus) MarkOIDCTokenSecretCreatedSuccceededWithReason(reason, messageFormat string, messageA ...interface{}) { + sbCondSet.Manage(sbs).MarkTrueWithReason(SinkBindingConditionOIDCTokenSecretCreated, reason, messageFormat, messageA...) +} + +func (sbs *SinkBindingStatus) MarkOIDCTokenSecretCreatedFailed(reason, messageFormat string, messageA ...interface{}) { + sbCondSet.Manage(sbs).MarkFalse(SinkBindingConditionOIDCTokenSecretCreated, reason, messageFormat, messageA...) +} + +func (sbs *SinkBindingStatus) MarkOIDCTokenSecretCreatedUnknown(reason, messageFormat string, messageA ...interface{}) { + sbCondSet.Manage(sbs).MarkUnknown(SinkBindingConditionOIDCTokenSecretCreated, reason, messageFormat, messageA...) +} + // Do implements psbinding.Bindable func (sb *SinkBinding) Do(ctx context.Context, ps *duckv1.WithPod) { // First undo so that we can just unconditionally append below. @@ -171,6 +193,38 @@ func (sb *SinkBinding) Do(ctx context.Context, ps *duckv1.WithPod) { Value: ceOverrides, }) } + + if sb.Status.OIDCTokenSecretName != nil { + ps.Spec.Template.Spec.Volumes = append(ps.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: oidcTokenVolumeName, + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: *sb.Status.OIDCTokenSecretName, + }, + }, + }, + }, + }, + }, + }) + + for i := range spec.Containers { + spec.Containers[i].VolumeMounts = append(spec.Containers[i].VolumeMounts, corev1.VolumeMount{ + Name: oidcTokenVolumeName, + MountPath: "/oidc", + }) + } + for i := range spec.InitContainers { + spec.InitContainers[i].VolumeMounts = append(spec.InitContainers[i].VolumeMounts, corev1.VolumeMount{ + Name: oidcTokenVolumeName, + MountPath: "/oidc", + }) + } + } } func (sb *SinkBinding) Undo(ctx context.Context, ps *duckv1.WithPod) { @@ -189,6 +243,17 @@ func (sb *SinkBinding) Undo(ctx context.Context, ps *duckv1.WithPod) { } } spec.InitContainers[i].Env = env + + if len(spec.InitContainers[i].VolumeMounts) > 0 { + volumeMounts := make([]corev1.VolumeMount, 0, len(spec.InitContainers[i].VolumeMounts)) + for j, vol := range c.VolumeMounts { + if vol.Name == oidcTokenVolumeName { + continue + } + volumeMounts = append(volumeMounts, spec.InitContainers[i].VolumeMounts[j]) + } + spec.InitContainers[i].VolumeMounts = volumeMounts + } } for i, c := range spec.Containers { if len(c.Env) == 0 { @@ -204,5 +269,27 @@ func (sb *SinkBinding) Undo(ctx context.Context, ps *duckv1.WithPod) { } } spec.Containers[i].Env = env + + if len(spec.Containers[i].VolumeMounts) > 0 { + volumeMounts := make([]corev1.VolumeMount, 0, len(spec.Containers[i].VolumeMounts)) + for j, vol := range c.VolumeMounts { + if vol.Name == oidcTokenVolumeName { + continue + } + volumeMounts = append(volumeMounts, spec.Containers[i].VolumeMounts[j]) + } + spec.Containers[i].VolumeMounts = volumeMounts + } + } + + if len(spec.Volumes) > 0 { + volumes := make([]corev1.Volume, 0, len(spec.Volumes)) + for i, vol := range spec.Volumes { + if vol.Name == oidcTokenVolumeName { + continue + } + volumes = append(volumes, spec.Volumes[i]) + } + ps.Spec.Template.Spec.Volumes = volumes } } diff --git a/pkg/apis/sources/v1/sinkbinding_lifecycle_test.go b/pkg/apis/sources/v1/sinkbinding_lifecycle_test.go index 6795e4e84ec..9dcc80c250c 100644 --- a/pkg/apis/sources/v1/sinkbinding_lifecycle_test.go +++ b/pkg/apis/sources/v1/sinkbinding_lifecycle_test.go @@ -172,6 +172,7 @@ func TestSinkBindingStatusIsReady(t *testing.T) { s.MarkSink(sink) s.MarkBindingAvailable() s.MarkOIDCIdentityCreatedSucceeded() + s.MarkOIDCTokenSecretCreatedSuccceeded() return s }(), want: true, @@ -183,6 +184,7 @@ func TestSinkBindingStatusIsReady(t *testing.T) { s.MarkSink(sink) s.MarkBindingAvailable() s.MarkOIDCIdentityCreatedSucceeded() + s.MarkOIDCTokenSecretCreatedSuccceeded() return s }(), want: true, @@ -194,6 +196,7 @@ func TestSinkBindingStatusIsReady(t *testing.T) { s.MarkSink(sink) s.MarkBindingAvailable() s.MarkOIDCIdentityCreatedSucceededWithReason("TheReason", "feature is disabled") + s.MarkOIDCTokenSecretCreatedSuccceeded() return s }(), want: true, @@ -205,6 +208,31 @@ func TestSinkBindingStatusIsReady(t *testing.T) { s.MarkSink(sink) s.MarkBindingAvailable() s.MarkOIDCIdentityCreatedFailed("TheReason", "this is a message") + s.MarkOIDCTokenSecretCreatedSuccceeded() + return s + }(), + want: false, + }, { + name: "mark OIDC token secret created", + s: func() *SinkBindingStatus { + s := &SinkBindingStatus{} + s.InitializeConditions() + s.MarkSink(sink) + s.MarkBindingAvailable() + s.MarkOIDCIdentityCreatedSucceeded() + s.MarkOIDCTokenSecretCreatedSuccceeded() + return s + }(), + want: true, + }, { + name: "mark OIDC token secret failed", + s: func() *SinkBindingStatus { + s := &SinkBindingStatus{} + s.InitializeConditions() + s.MarkSink(sink) + s.MarkBindingAvailable() + s.MarkOIDCIdentityCreatedSucceeded() + s.MarkOIDCTokenSecretCreatedFailed("Some", "reason") return s }(), want: false, @@ -276,6 +304,11 @@ func TestSinkBindingUndo(t *testing.T) { Name: "K_CE_OVERRIDES", Value: `{"extensions":{"foo":"bar"}}`, }}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "foo", + }, { + Name: oidcTokenVolumeName, + }}, }}, Containers: []corev1.Container{{ Name: "blah", @@ -296,6 +329,11 @@ func TestSinkBindingUndo(t *testing.T) { Name: "K_CE_OVERRIDES", Value: `{"extensions":{"foo":"bar"}}`, }}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "foo", + }, { + Name: oidcTokenVolumeName, + }}, }, { Name: "sidecar", Image: "busybox", @@ -312,6 +350,16 @@ func TestSinkBindingUndo(t *testing.T) { Name: "K_CE_OVERRIDES", Value: `{"extensions":{"foo":"bar"}}`, }}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "foo", + }, { + Name: oidcTokenVolumeName, + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "foo", + }, { + Name: oidcTokenVolumeName, }}, }, }, @@ -331,6 +379,9 @@ func TestSinkBindingUndo(t *testing.T) { Name: "BAZ", Value: "INGA", }}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "foo", + }}, }}, Containers: []corev1.Container{{ Name: "blah", @@ -342,6 +393,9 @@ func TestSinkBindingUndo(t *testing.T) { Name: "BAZ", Value: "INGA", }}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "foo", + }}, }, { Name: "sidecar", Image: "busybox", @@ -349,6 +403,12 @@ func TestSinkBindingUndo(t *testing.T) { Name: "BAZ", Value: "INGA", }}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "foo", + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "foo", }}, }, }, @@ -382,9 +442,11 @@ func TestSinkBindingDo(t *testing.T) { overrides := duckv1.CloudEventOverrides{Extensions: map[string]string{"foo": "bar"}} tests := []struct { - name string - in *duckv1.WithPod - want *duckv1.WithPod + name string + in *duckv1.WithPod + sbStatus *SinkBindingStatus + want *duckv1.WithPod + ctx context.Context }{{ name: "nothing to add", in: &duckv1.WithPod{ @@ -567,22 +629,132 @@ func TestSinkBindingDo(t *testing.T) { }, }, }, + }, { + name: "adds OIDC token volume", + in: &duckv1.WithPod{ + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "init", + Image: "busybox", + Env: []corev1.EnvVar{{ + Name: "K_SINK", + Value: destination.URI.String(), + }, { + Name: "K_CA_CERTS", + Value: caCert, + }, { + Name: "K_CE_OVERRIDES", + Value: `{"extensions":{"foo":"bar"}}`, + }}, + }}, + Containers: []corev1.Container{{ + Name: "blah", + Image: "busybox", + Env: []corev1.EnvVar{{ + Name: "K_SINK", + Value: destination.URI.String(), + }, { + Name: "K_CA_CERTS", + Value: caCert, + }, { + Name: "K_CE_OVERRIDES", + Value: `{"extensions":{"foo":"bar"}}`, + }}, + }}, + }, + }, + }, + }, + sbStatus: &SinkBindingStatus{ + OIDCTokenSecretName: pointer.String("oidc-token"), + }, + want: &duckv1.WithPod{ + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "init", + Image: "busybox", + Env: []corev1.EnvVar{{ + Name: "K_SINK", + Value: destination.URI.String(), + }, { + Name: "K_CA_CERTS", + Value: caCert, + }, { + Name: "K_CE_OVERRIDES", + Value: `{"extensions":{"foo":"bar"}}`, + }}, + VolumeMounts: []corev1.VolumeMount{{ + Name: oidcTokenVolumeName, + MountPath: "/oidc", + }}, + }}, + Containers: []corev1.Container{{ + Name: "blah", + Image: "busybox", + Env: []corev1.EnvVar{{ + Name: "K_SINK", + Value: destination.URI.String(), + }, { + Name: "K_CA_CERTS", + Value: caCert, + }, { + Name: "K_CE_OVERRIDES", + Value: `{"extensions":{"foo":"bar"}}`, + }}, + VolumeMounts: []corev1.VolumeMount{{ + Name: oidcTokenVolumeName, + MountPath: "/oidc", + }}, + }}, + Volumes: []corev1.Volume{{ + Name: oidcTokenVolumeName, + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{{ + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "oidc-token", + }, + }, + }}, + }, + }, + }}, + }, + }, + }, + }, }} for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := test.in - ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, got) - ctx = addressable.WithDuck(ctx) - r := resolver.NewURIResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0)) - ctx = WithURIResolver(context.Background(), r) + applicationContext, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, got) + applicationContext = addressable.WithDuck(applicationContext) + r := resolver.NewURIResolverFromTracker(applicationContext, tracker.New(func(types.NamespacedName) {}, 0)) + + ctx := context.Background() + if test.ctx != nil { + ctx = test.ctx + } + ctx = WithURIResolver(ctx, r) + + sb := &SinkBinding{ + Spec: SinkBindingSpec{ + SourceSpec: duckv1.SourceSpec{ + Sink: destination, + CloudEventOverrides: &overrides, + }}, + } + + if test.sbStatus != nil { + sb.Status = *test.sbStatus + } - sb := &SinkBinding{Spec: SinkBindingSpec{ - SourceSpec: duckv1.SourceSpec{ - Sink: destination, - CloudEventOverrides: &overrides, - }, - }} sb.Do(ctx, got) if !cmp.Equal(got, test.want) { diff --git a/pkg/apis/sources/v1/sinkbinding_types.go b/pkg/apis/sources/v1/sinkbinding_types.go index 512d687d31a..e8afbc1451d 100644 --- a/pkg/apis/sources/v1/sinkbinding_types.go +++ b/pkg/apis/sources/v1/sinkbinding_types.go @@ -81,6 +81,10 @@ const ( // SinkBindingConditionOIDCIdentityCreated is configured to indicate whether // the OIDC identity has been created for the sink. SinkBindingConditionOIDCIdentityCreated apis.ConditionType = "OIDCIdentityCreated" + + // SinkBindingConditionOIDCTokenSecretCreated is configured to indicate whether + // the secret containing the OIDC token has been created for the sink. + SinkBindingConditionOIDCTokenSecretCreated apis.ConditionType = "OIDCTokenSecretCreated" ) // SinkBindingStatus communicates the observed state of the SinkBinding (from the controller). @@ -93,6 +97,10 @@ type SinkBindingStatus struct { // * SinkURI - the current active sink URI that has been configured for the // Source. duckv1.SourceStatus `json:",inline"` + + // OIDCTokenSecretName is the name of the secret containing the token for + // this SinkBindings OIDC authentication + OIDCTokenSecretName *string `json:"oidcTokenSecretName,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/sources/v1/zz_generated.deepcopy.go b/pkg/apis/sources/v1/zz_generated.deepcopy.go index 551322eab40..6d175e3c960 100644 --- a/pkg/apis/sources/v1/zz_generated.deepcopy.go +++ b/pkg/apis/sources/v1/zz_generated.deepcopy.go @@ -454,6 +454,11 @@ func (in *SinkBindingSpec) DeepCopy() *SinkBindingSpec { func (in *SinkBindingStatus) DeepCopyInto(out *SinkBindingStatus) { *out = *in in.SourceStatus.DeepCopyInto(&out.SourceStatus) + if in.OIDCTokenSecretName != nil { + in, out := &in.OIDCTokenSecretName, &out.OIDCTokenSecretName + *out = new(string) + **out = **in + } return } diff --git a/pkg/auth/token_provider.go b/pkg/auth/token_provider.go index d35a6e29f71..6fb115f9d4e 100644 --- a/pkg/auth/token_provider.go +++ b/pkg/auth/token_provider.go @@ -27,11 +27,13 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/cache" "k8s.io/client-go/kubernetes" + "k8s.io/utils/pointer" kubeclient "knative.dev/pkg/client/injection/kube/client" "knative.dev/pkg/logging" ) const ( + TokenExpirationTime = time.Hour expirationBufferTime = 5 * time.Minute ) @@ -58,9 +60,16 @@ func (c *OIDCTokenProvider) GetJWT(serviceAccount types.NamespacedName, audience } // if not found in cache: request new token + return c.GetNewJWT(serviceAccount, audience) +} + +// GetNewJWT returns a new JWT from the given service account for the given audience without using the token cache. +func (c *OIDCTokenProvider) GetNewJWT(serviceAccount types.NamespacedName, audience string) (string, error) { + // request new token tokenRequest := authv1.TokenRequest{ Spec: authv1.TokenRequestSpec{ - Audiences: []string{audience}, + Audiences: []string{audience}, + ExpirationSeconds: pointer.Int64(int64(TokenExpirationTime.Seconds())), }, } diff --git a/pkg/auth/utils.go b/pkg/auth/utils.go index 0f52c34364f..81e7145a8f2 100644 --- a/pkg/auth/utils.go +++ b/pkg/auth/utils.go @@ -20,6 +20,9 @@ import ( "fmt" "net/http" "strings" + "time" + + "github.com/go-jose/go-jose/v3/jwt" ) const ( @@ -40,3 +43,22 @@ func GetJWTFromHeader(header http.Header) string { func SetAuthHeader(jwt string, header http.Header) { header.Set(AuthHeaderKey, fmt.Sprintf("Bearer %s", jwt)) } + +// GetJWTExpiry returns the expiry time of the token in UTC +func GetJWTExpiry(token string) (time.Time, error) { + t, err := jwt.ParseSigned(token) + if err != nil { + return time.Time{}, err + } + + var claims jwt.Claims + if err := t.UnsafeClaimsWithoutVerification(&claims); err != nil { + return time.Time{}, err + } + + if claims.Expiry == nil { + return time.Time{}, fmt.Errorf("no expiry set in JWT") + } + + return claims.Expiry.Time(), nil +} diff --git a/pkg/auth/utils_test.go b/pkg/auth/utils_test.go new file mode 100644 index 00000000000..fa6eaf852b5 --- /dev/null +++ b/pkg/auth/utils_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2023 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "reflect" + "testing" + "time" +) + +func TestGetJWTExpiry(t *testing.T) { + tests := []struct { + name string + token string + want time.Time + wantErr bool + }{ + { + name: "Valid JWT", + token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJteS1pc3N1ZXIiLCJpYXQiOjE1Nzc5MzA2NDUsImV4cCI6MTU3NzkzNDI0NSwiYXVkIjoibXktYXVkaWVuY2UiLCJzdWIiOiJzdWJqZWN0QGV4YW1wbGUuY29tIn0.Hl8n6Ipt0X0gI46QLPZtpESRtc7cQ75AqXNal0sQ2a4", + want: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + wantErr: false, + }, { + name: "No valid JWT", + token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJteS1pc3N1ZXIiLCJpYXQiOjE1Nzc5MzA2NDUsImV4cCI6MTU3NzkzNDI0NSwiYXVkIjoibXktYXVkaWVuY2UiLCJzdWIiOiJzdWJqZWN0QGV4YW1wbGUuY29.Hl8n6Ipt0X0gI46QLPZtpESRtc7cQ75AqXNal0sQ2a4", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetJWTExpiry(tt.token) + if (err != nil) != tt.wantErr { + t.Errorf("GetJWTExpiry() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got.UTC(), tt.want.UTC()) { + t.Errorf("GetJWTExpiry() = %v, want %v", got.UTC(), tt.want.UTC()) + } + }) + } +} diff --git a/pkg/reconciler/sinkbinding/controller.go b/pkg/reconciler/sinkbinding/controller.go index dab1680c477..2113fb2a07e 100644 --- a/pkg/reconciler/sinkbinding/controller.go +++ b/pkg/reconciler/sinkbinding/controller.go @@ -18,8 +18,7 @@ package sinkbinding import ( "context" - "errors" - "fmt" + "time" "knative.dev/eventing/pkg/auth" sbinformer "knative.dev/eventing/pkg/client/injection/informers/sources/v1/sinkbinding" @@ -32,17 +31,15 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" - corev1listers "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "knative.dev/eventing/pkg/apis/feature" v1 "knative.dev/eventing/pkg/apis/sources/v1" "knative.dev/pkg/apis/duck" - duckv1 "knative.dev/pkg/apis/duck/v1" kubeclient "knative.dev/pkg/client/injection/kube/client" + secretinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/secret" serviceaccountinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount" "knative.dev/pkg/configmap" "knative.dev/pkg/controller" @@ -54,15 +51,14 @@ import ( const ( controllerAgentName = "sinkbinding-controller" -) -type SinkBindingSubResourcesReconciler struct { - res *resolver.URIResolver - tracker tracker.Interface - serviceAccountLister corev1listers.ServiceAccountLister - kubeclient kubernetes.Interface - featureStore *feature.Store -} + // resyncPeriod defines the period in which SinkBindings will be reenqued + // (e.g. to check the validity of their OIDC token secret) + resyncPeriod = auth.TokenExpirationTime / 2 + // tokenExpiryBuffer defines an additional buffer for the expiry of OIDC + // token secrets + tokenExpiryBuffer = 5 * time.Minute +) // NewController returns a new SinkBinding reconciler. func NewController( @@ -76,6 +72,17 @@ func NewController( psInformerFactory := podspecable.Get(ctx) namespaceInformer := namespace.Get(ctx) serviceaccountInformer := serviceaccountinformer.Get(ctx) + secretInformer := secretinformer.Get(ctx) + + var globalResync func() + featureStore := feature.NewStore(logging.FromContext(ctx).Named("feature-config-store"), func(name string, value interface{}) { + logger.Infof("feature config changed. name: %s, value: %v", name, value) + + if globalResync != nil { + globalResync() + } + }) + featureStore.WatchConfigs(cmw) c := &psbinding.BaseReconciler{ LeaderAwareFuncs: reconciler.LeaderAwareFuncs{ @@ -106,11 +113,9 @@ func NewController( Logger: logger, }) - featureStore := feature.NewStore(logging.FromContext(ctx).Named("feature-config-store"), func(name string, value interface{}) { + globalResync = func() { impl.GlobalResync(sbInformer.Informer()) - }) - - featureStore.WatchConfigs(cmw) + } sbInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) namespaceInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) @@ -121,7 +126,9 @@ func NewController( tracker: impl.Tracker, kubeclient: kubeclient.Get(ctx), serviceAccountLister: serviceaccountInformer.Lister(), + secretLister: secretInformer.Lister(), featureStore: featureStore, + tokenProvider: auth.NewOIDCTokenProvider(ctx), } c.WithContext = func(ctx context.Context, b psbinding.Bindable) (context.Context, error) { @@ -141,9 +148,29 @@ func NewController( Handler: controller.HandleAll(impl.EnqueueControllerOf), }) + // do a periodic reync of all sinkbindings to renew the token secrets eventually + go periodicResync(ctx, globalResync) + return impl } +func periodicResync(ctx context.Context, globalResyncFunc func()) { + ticker := time.NewTicker(resyncPeriod) + logger := logging.FromContext(ctx) + + logger.Infof("Starting global resync of SinkBindings every %s", resyncPeriod) + for { + select { + case <-ticker.C: + logger.Debug("Triggering global resync of SinkBindings") + globalResyncFunc() + case <-ctx.Done(): + logger.Debug("Context finished. Stopping periodic resync of SinkBindings") + return + } + } +} + func ListAll(ctx context.Context, handler cache.ResourceEventHandler) psbinding.ListAll { fbInformer := sbinformer.Get(ctx) @@ -172,55 +199,6 @@ func WithContextFactory(ctx context.Context, handler func(types.NamespacedName)) } } -func (s *SinkBindingSubResourcesReconciler) Reconcile(ctx context.Context, b psbinding.Bindable) error { - sb := b.(*v1.SinkBinding) - if s.res == nil { - err := errors.New("Resolver is nil") - logging.FromContext(ctx).Errorf("%w", err) - sb.Status.MarkBindingUnavailable("NoResolver", "No Resolver associated with context for sink") - return err - } - if sb.Spec.Sink.Ref != nil { - s.tracker.TrackReference(tracker.Reference{ - APIVersion: sb.Spec.Sink.Ref.APIVersion, - Kind: sb.Spec.Sink.Ref.Kind, - Namespace: sb.Spec.Sink.Ref.Namespace, - Name: sb.Spec.Sink.Ref.Name, - }, b) - } - - featureFlags := s.featureStore.Load() - if featureFlags.IsOIDCAuthentication() { - saName := auth.GetOIDCServiceAccountNameForResource(v1.SchemeGroupVersion.WithKind("SinkBinding"), sb.ObjectMeta) - sb.Status.Auth = &duckv1.AuthStatus{ - ServiceAccountName: &saName, - } - - if err := auth.EnsureOIDCServiceAccountExistsForResource(ctx, s.serviceAccountLister, s.kubeclient, v1.SchemeGroupVersion.WithKind("SinkBinding"), sb.ObjectMeta); err != nil { - sb.Status.MarkOIDCIdentityCreatedFailed("Unable to resolve service account for OIDC authentication", "%v", err) - return err - } - sb.Status.MarkOIDCIdentityCreatedSucceeded() - } else { - sb.Status.Auth = nil - sb.Status.MarkOIDCIdentityCreatedSucceededWithReason(fmt.Sprintf("%s feature disabled", feature.OIDCAuthentication), "") - } - - addr, err := s.res.AddressableFromDestinationV1(ctx, sb.Spec.Sink, sb) - if err != nil { - logging.FromContext(ctx).Errorf("Failed to get Addressable from Destination: %w", err) - sb.Status.MarkBindingUnavailable("NoAddressable", "Addressable could not be extracted from destination") - return err - } - sb.Status.MarkSink(addr) - return nil -} - -// I'm just here so I won't get fined -func (*SinkBindingSubResourcesReconciler) ReconcileDeletion(ctx context.Context, b psbinding.Bindable) error { - return nil -} - func createRecorder(ctx context.Context, agentName string) record.EventRecorder { logger := logging.FromContext(ctx) diff --git a/pkg/reconciler/sinkbinding/sinkbinding.go b/pkg/reconciler/sinkbinding/sinkbinding.go new file mode 100644 index 00000000000..43645cf625c --- /dev/null +++ b/pkg/reconciler/sinkbinding/sinkbinding.go @@ -0,0 +1,220 @@ +/* +Copyright 2020 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sinkbinding + +import ( + "context" + "errors" + "fmt" + "time" + + "knative.dev/eventing/pkg/auth" + "knative.dev/pkg/kmeta" + "knative.dev/pkg/resolver" + + corev1 "k8s.io/api/core/v1" + apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + applyconfigurationcorev1 "k8s.io/client-go/applyconfigurations/core/v1" + applyconfigurationmetav1 "k8s.io/client-go/applyconfigurations/meta/v1" + "k8s.io/client-go/kubernetes" + corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/utils/pointer" + "knative.dev/eventing/pkg/apis/feature" + v1 "knative.dev/eventing/pkg/apis/sources/v1" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/logging" + "knative.dev/pkg/tracker" + "knative.dev/pkg/webhook/psbinding" +) + +type SinkBindingSubResourcesReconciler struct { + res *resolver.URIResolver + tracker tracker.Interface + serviceAccountLister corev1listers.ServiceAccountLister + secretLister corev1listers.SecretLister + kubeclient kubernetes.Interface + featureStore *feature.Store + tokenProvider *auth.OIDCTokenProvider +} + +func (s *SinkBindingSubResourcesReconciler) Reconcile(ctx context.Context, b psbinding.Bindable) error { + sb := b.(*v1.SinkBinding) + if s.res == nil { + err := errors.New("Resolver is nil") + logging.FromContext(ctx).Errorf("%w", err) + sb.Status.MarkBindingUnavailable("NoResolver", "No Resolver associated with context for sink") + return err + } + if sb.Spec.Sink.Ref != nil { + s.tracker.TrackReference(tracker.Reference{ + APIVersion: sb.Spec.Sink.Ref.APIVersion, + Kind: sb.Spec.Sink.Ref.Kind, + Namespace: sb.Spec.Sink.Ref.Namespace, + Name: sb.Spec.Sink.Ref.Name, + }, b) + } + + addr, err := s.res.AddressableFromDestinationV1(ctx, sb.Spec.Sink, sb) + if err != nil { + logging.FromContext(ctx).Errorf("Failed to get Addressable from Destination: %w", err) + sb.Status.MarkBindingUnavailable("NoAddressable", "Addressable could not be extracted from destination") + return err + } + sb.Status.MarkSink(addr) + + featureFlags := s.featureStore.Load() + if featureFlags.IsOIDCAuthentication() { + saName := auth.GetOIDCServiceAccountNameForResource(v1.SchemeGroupVersion.WithKind("SinkBinding"), sb.ObjectMeta) + sb.Status.Auth = &duckv1.AuthStatus{ + ServiceAccountName: &saName, + } + + if err := auth.EnsureOIDCServiceAccountExistsForResource(ctx, s.serviceAccountLister, s.kubeclient, v1.SchemeGroupVersion.WithKind("SinkBinding"), sb.ObjectMeta); err != nil { + sb.Status.MarkOIDCIdentityCreatedFailed("Unable to resolve service account for OIDC authentication", "%v", err) + return err + } + sb.Status.MarkOIDCIdentityCreatedSucceeded() + + err := s.reconcileOIDCTokenSecret(ctx, sb) + if err != nil { + sb.Status.MarkOIDCTokenSecretCreatedFailed("Unable to reconcile OIDC token secret", "%v", err) + return err + } + sb.Status.MarkOIDCTokenSecretCreatedSuccceeded() + + } else { + sb.Status.Auth = nil + sb.Status.MarkOIDCIdentityCreatedSucceededWithReason(fmt.Sprintf("%s feature disabled", feature.OIDCAuthentication), "") + sb.Status.MarkOIDCTokenSecretCreatedSuccceededWithReason(fmt.Sprintf("%s feature disabled", feature.OIDCAuthentication), "") + + if err := s.removeOIDCTokenSecretEventually(ctx, sb); err != nil { + return err + } + sb.Status.OIDCTokenSecretName = nil + } + + return nil +} + +// I'm just here so I won't get fined +func (*SinkBindingSubResourcesReconciler) ReconcileDeletion(ctx context.Context, b psbinding.Bindable) error { + return nil +} + +func (s *SinkBindingSubResourcesReconciler) reconcileOIDCTokenSecret(ctx context.Context, sb *v1.SinkBinding) error { + logger := logging.FromContext(ctx) + secretName := s.oidcTokenSecretName(sb) + + if sb.Status.SinkAudience == nil { + return fmt.Errorf("sinkAudience must be set on %s/%s to generate a OIDC token secret", sb.Name, sb.Namespace) + } + + secret, err := s.secretLister.Secrets(sb.Namespace).Get(secretName) + if err != nil { + if apierrs.IsNotFound(err) { + // create new secret + logger.Debugf("No OIDC token secret found for %s/%s sinkbinding. Will create a new secret", sb.Name, sb.Namespace) + + return s.renewOIDCTokenSecret(ctx, sb) + } + + return fmt.Errorf("could not check if secret %q exists already: %w", secretName, err) + } + + // check if token needs to be renewed + expiry, err := auth.GetJWTExpiry(string(secret.Data["token"])) + if err != nil { + logger.Warnf("Could not get expiry date of OIDC token secret: %s. Will renew token.", err) + + return s.renewOIDCTokenSecret(ctx, sb) + } + + resyncAndBufferDuration := resyncPeriod + tokenExpiryBuffer + if expiry.After(time.Now().Add(resyncAndBufferDuration)) { + logger.Debugf("OIDC token secret for %s/%s sinkbinding still valid for > %s (expires %s). Will not update secret", sb.Name, sb.Namespace, resyncAndBufferDuration, expiry) + // token is still valid for resync period + buffer --> we're fine + + return nil + } + + logger.Debugf("OIDC token secret for %s/%s sinkbinding is valid for less than %s (expires %s). Will update secret", sb.Name, sb.Namespace, resyncAndBufferDuration, expiry) + + return s.renewOIDCTokenSecret(ctx, sb) +} + +func (s *SinkBindingSubResourcesReconciler) renewOIDCTokenSecret(ctx context.Context, sb *v1.SinkBinding) error { + logger := logging.FromContext(ctx) + secretName := s.oidcTokenSecretName(sb) + + token, err := s.tokenProvider.GetNewJWT(types.NamespacedName{ + Namespace: sb.Namespace, + Name: *sb.Status.Auth.ServiceAccountName, + }, *sb.Status.SinkAudience) + + if err != nil { + return fmt.Errorf("could not create token for SinkBinding %s/%s: %w", sb.Name, sb.Namespace, err) + } + + apiVersion := fmt.Sprintf("%s/%s", v1.SchemeGroupVersion.Group, v1.SchemeGroupVersion.Version) + applyConfig := new(applyconfigurationcorev1.SecretApplyConfiguration). + WithName(secretName). + WithNamespace(sb.Namespace). + WithType(corev1.SecretTypeOpaque). + WithKind("Secret"). + WithAPIVersion("v1"). + WithOwnerReferences(&applyconfigurationmetav1.OwnerReferenceApplyConfiguration{ + APIVersion: &apiVersion, + Kind: pointer.String("SinkBinding"), + Name: &sb.Name, + UID: &sb.UID, + Controller: pointer.Bool(true), + BlockOwnerDeletion: pointer.Bool(false), + }). + WithStringData(map[string]string{ + "token": token, + }) + + _, err = s.kubeclient.CoreV1().Secrets(sb.Namespace).Apply(ctx, applyConfig, metav1.ApplyOptions{FieldManager: controllerAgentName}) + if err != nil { + return fmt.Errorf("could not create or update OIDC token secret for SinkBinding %s/%s: %w", sb.Name, sb.Namespace, err) + } + + logger.Debugf("Created/Updated OIDC token secret for %s/%s sinkbinding with new token.", sb.Name, sb.Namespace) + + sb.Status.OIDCTokenSecretName = &secretName + + return nil +} + +func (s *SinkBindingSubResourcesReconciler) oidcTokenSecretName(sb *v1.SinkBinding) string { + return kmeta.ChildName(sb.Name, "-oidc-token") +} + +func (s *SinkBindingSubResourcesReconciler) removeOIDCTokenSecretEventually(ctx context.Context, sb *v1.SinkBinding) error { + if sb.Status.OIDCTokenSecretName == nil { + return nil + } + + _, err := s.secretLister.Secrets(sb.Namespace).Get(*sb.Status.OIDCTokenSecretName) + if apierrs.IsNotFound(err) { + return nil + } + + return s.kubeclient.CoreV1().Secrets(sb.Namespace).Delete(ctx, *sb.Status.OIDCTokenSecretName, metav1.DeleteOptions{}) +} diff --git a/vendor/github.com/go-jose/go-jose/v3/jwt/builder.go b/vendor/github.com/go-jose/go-jose/v3/jwt/builder.go new file mode 100644 index 00000000000..7df270cc39d --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v3/jwt/builder.go @@ -0,0 +1,334 @@ +/*- + * Copyright 2016 Zbigniew Mandziejewicz + * Copyright 2016 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jwt + +import ( + "bytes" + "reflect" + + "github.com/go-jose/go-jose/v3/json" + + "github.com/go-jose/go-jose/v3" +) + +// Builder is a utility for making JSON Web Tokens. Calls can be chained, and +// errors are accumulated until the final call to CompactSerialize/FullSerialize. +type Builder interface { + // Claims encodes claims into JWE/JWS form. Multiple calls will merge claims + // into single JSON object. If you are passing private claims, make sure to set + // struct field tags to specify the name for the JSON key to be used when + // serializing. + Claims(i interface{}) Builder + // Token builds a JSONWebToken from provided data. + Token() (*JSONWebToken, error) + // FullSerialize serializes a token using the JWS/JWE JSON Serialization format. + FullSerialize() (string, error) + // CompactSerialize serializes a token using the compact serialization format. + CompactSerialize() (string, error) +} + +// NestedBuilder is a utility for making Signed-Then-Encrypted JSON Web Tokens. +// Calls can be chained, and errors are accumulated until final call to +// CompactSerialize/FullSerialize. +type NestedBuilder interface { + // Claims encodes claims into JWE/JWS form. Multiple calls will merge claims + // into single JSON object. If you are passing private claims, make sure to set + // struct field tags to specify the name for the JSON key to be used when + // serializing. + Claims(i interface{}) NestedBuilder + // Token builds a NestedJSONWebToken from provided data. + Token() (*NestedJSONWebToken, error) + // FullSerialize serializes a token using the JSON Serialization format. + FullSerialize() (string, error) + // CompactSerialize serializes a token using the compact serialization format. + CompactSerialize() (string, error) +} + +type builder struct { + payload map[string]interface{} + err error +} + +type signedBuilder struct { + builder + sig jose.Signer +} + +type encryptedBuilder struct { + builder + enc jose.Encrypter +} + +type nestedBuilder struct { + builder + sig jose.Signer + enc jose.Encrypter +} + +// Signed creates builder for signed tokens. +func Signed(sig jose.Signer) Builder { + return &signedBuilder{ + sig: sig, + } +} + +// Encrypted creates builder for encrypted tokens. +func Encrypted(enc jose.Encrypter) Builder { + return &encryptedBuilder{ + enc: enc, + } +} + +// SignedAndEncrypted creates builder for signed-then-encrypted tokens. +// ErrInvalidContentType will be returned if encrypter doesn't have JWT content type. +func SignedAndEncrypted(sig jose.Signer, enc jose.Encrypter) NestedBuilder { + if contentType, _ := enc.Options().ExtraHeaders[jose.HeaderContentType].(jose.ContentType); contentType != "JWT" { + return &nestedBuilder{ + builder: builder{ + err: ErrInvalidContentType, + }, + } + } + return &nestedBuilder{ + sig: sig, + enc: enc, + } +} + +func (b builder) claims(i interface{}) builder { + if b.err != nil { + return b + } + + m, ok := i.(map[string]interface{}) + switch { + case ok: + return b.merge(m) + case reflect.Indirect(reflect.ValueOf(i)).Kind() == reflect.Struct: + m, err := normalize(i) + if err != nil { + return builder{ + err: err, + } + } + return b.merge(m) + default: + return builder{ + err: ErrInvalidClaims, + } + } +} + +func normalize(i interface{}) (map[string]interface{}, error) { + m := make(map[string]interface{}) + + raw, err := json.Marshal(i) + if err != nil { + return nil, err + } + + d := json.NewDecoder(bytes.NewReader(raw)) + d.SetNumberType(json.UnmarshalJSONNumber) + + if err := d.Decode(&m); err != nil { + return nil, err + } + + return m, nil +} + +func (b *builder) merge(m map[string]interface{}) builder { + p := make(map[string]interface{}) + for k, v := range b.payload { + p[k] = v + } + for k, v := range m { + p[k] = v + } + + return builder{ + payload: p, + } +} + +func (b *builder) token(p func(interface{}) ([]byte, error), h []jose.Header) (*JSONWebToken, error) { + return &JSONWebToken{ + payload: p, + Headers: h, + }, nil +} + +func (b *signedBuilder) Claims(i interface{}) Builder { + return &signedBuilder{ + builder: b.builder.claims(i), + sig: b.sig, + } +} + +func (b *signedBuilder) Token() (*JSONWebToken, error) { + sig, err := b.sign() + if err != nil { + return nil, err + } + + h := make([]jose.Header, len(sig.Signatures)) + for i, v := range sig.Signatures { + h[i] = v.Header + } + + return b.builder.token(sig.Verify, h) +} + +func (b *signedBuilder) CompactSerialize() (string, error) { + sig, err := b.sign() + if err != nil { + return "", err + } + + return sig.CompactSerialize() +} + +func (b *signedBuilder) FullSerialize() (string, error) { + sig, err := b.sign() + if err != nil { + return "", err + } + + return sig.FullSerialize(), nil +} + +func (b *signedBuilder) sign() (*jose.JSONWebSignature, error) { + if b.err != nil { + return nil, b.err + } + + p, err := json.Marshal(b.payload) + if err != nil { + return nil, err + } + + return b.sig.Sign(p) +} + +func (b *encryptedBuilder) Claims(i interface{}) Builder { + return &encryptedBuilder{ + builder: b.builder.claims(i), + enc: b.enc, + } +} + +func (b *encryptedBuilder) CompactSerialize() (string, error) { + enc, err := b.encrypt() + if err != nil { + return "", err + } + + return enc.CompactSerialize() +} + +func (b *encryptedBuilder) FullSerialize() (string, error) { + enc, err := b.encrypt() + if err != nil { + return "", err + } + + return enc.FullSerialize(), nil +} + +func (b *encryptedBuilder) Token() (*JSONWebToken, error) { + enc, err := b.encrypt() + if err != nil { + return nil, err + } + + return b.builder.token(enc.Decrypt, []jose.Header{enc.Header}) +} + +func (b *encryptedBuilder) encrypt() (*jose.JSONWebEncryption, error) { + if b.err != nil { + return nil, b.err + } + + p, err := json.Marshal(b.payload) + if err != nil { + return nil, err + } + + return b.enc.Encrypt(p) +} + +func (b *nestedBuilder) Claims(i interface{}) NestedBuilder { + return &nestedBuilder{ + builder: b.builder.claims(i), + sig: b.sig, + enc: b.enc, + } +} + +func (b *nestedBuilder) Token() (*NestedJSONWebToken, error) { + enc, err := b.signAndEncrypt() + if err != nil { + return nil, err + } + + return &NestedJSONWebToken{ + enc: enc, + Headers: []jose.Header{enc.Header}, + }, nil +} + +func (b *nestedBuilder) CompactSerialize() (string, error) { + enc, err := b.signAndEncrypt() + if err != nil { + return "", err + } + + return enc.CompactSerialize() +} + +func (b *nestedBuilder) FullSerialize() (string, error) { + enc, err := b.signAndEncrypt() + if err != nil { + return "", err + } + + return enc.FullSerialize(), nil +} + +func (b *nestedBuilder) signAndEncrypt() (*jose.JSONWebEncryption, error) { + if b.err != nil { + return nil, b.err + } + + p, err := json.Marshal(b.payload) + if err != nil { + return nil, err + } + + sig, err := b.sig.Sign(p) + if err != nil { + return nil, err + } + + p2, err := sig.CompactSerialize() + if err != nil { + return nil, err + } + + return b.enc.Encrypt([]byte(p2)) +} diff --git a/vendor/github.com/go-jose/go-jose/v3/jwt/claims.go b/vendor/github.com/go-jose/go-jose/v3/jwt/claims.go new file mode 100644 index 00000000000..286be1d2fe9 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v3/jwt/claims.go @@ -0,0 +1,130 @@ +/*- + * Copyright 2016 Zbigniew Mandziejewicz + * Copyright 2016 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jwt + +import ( + "strconv" + "time" + + "github.com/go-jose/go-jose/v3/json" +) + +// Claims represents public claim values (as specified in RFC 7519). +type Claims struct { + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Audience Audience `json:"aud,omitempty"` + Expiry *NumericDate `json:"exp,omitempty"` + NotBefore *NumericDate `json:"nbf,omitempty"` + IssuedAt *NumericDate `json:"iat,omitempty"` + ID string `json:"jti,omitempty"` +} + +// NumericDate represents date and time as the number of seconds since the +// epoch, ignoring leap seconds. Non-integer values can be represented +// in the serialized format, but we round to the nearest second. +// See RFC7519 Section 2: https://tools.ietf.org/html/rfc7519#section-2 +type NumericDate int64 + +// NewNumericDate constructs NumericDate from time.Time value. +func NewNumericDate(t time.Time) *NumericDate { + if t.IsZero() { + return nil + } + + // While RFC 7519 technically states that NumericDate values may be + // non-integer values, we don't bother serializing timestamps in + // claims with sub-second accurancy and just round to the nearest + // second instead. Not convined sub-second accuracy is useful here. + out := NumericDate(t.Unix()) + return &out +} + +// MarshalJSON serializes the given NumericDate into its JSON representation. +func (n NumericDate) MarshalJSON() ([]byte, error) { + return []byte(strconv.FormatInt(int64(n), 10)), nil +} + +// UnmarshalJSON reads a date from its JSON representation. +func (n *NumericDate) UnmarshalJSON(b []byte) error { + s := string(b) + + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return ErrUnmarshalNumericDate + } + + *n = NumericDate(f) + return nil +} + +// Time returns time.Time representation of NumericDate. +func (n *NumericDate) Time() time.Time { + if n == nil { + return time.Time{} + } + return time.Unix(int64(*n), 0) +} + +// Audience represents the recipients that the token is intended for. +type Audience []string + +// UnmarshalJSON reads an audience from its JSON representation. +func (s *Audience) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + switch v := v.(type) { + case string: + *s = []string{v} + case []interface{}: + a := make([]string, len(v)) + for i, e := range v { + s, ok := e.(string) + if !ok { + return ErrUnmarshalAudience + } + a[i] = s + } + *s = a + default: + return ErrUnmarshalAudience + } + + return nil +} + +// MarshalJSON converts audience to json representation. +func (s Audience) MarshalJSON() ([]byte, error) { + if len(s) == 1 { + return json.Marshal(s[0]) + } + return json.Marshal([]string(s)) +} + +//Contains checks whether a given string is included in the Audience +func (s Audience) Contains(v string) bool { + for _, a := range s { + if a == v { + return true + } + } + return false +} diff --git a/vendor/github.com/go-jose/go-jose/v3/jwt/doc.go b/vendor/github.com/go-jose/go-jose/v3/jwt/doc.go new file mode 100644 index 00000000000..4cf97b54e78 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v3/jwt/doc.go @@ -0,0 +1,22 @@ +/*- + * Copyright 2017 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + +Package jwt provides an implementation of the JSON Web Token standard. + +*/ +package jwt diff --git a/vendor/github.com/go-jose/go-jose/v3/jwt/errors.go b/vendor/github.com/go-jose/go-jose/v3/jwt/errors.go new file mode 100644 index 00000000000..27388e5449a --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v3/jwt/errors.go @@ -0,0 +1,53 @@ +/*- + * Copyright 2016 Zbigniew Mandziejewicz + * Copyright 2016 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jwt + +import "errors" + +// ErrUnmarshalAudience indicates that aud claim could not be unmarshalled. +var ErrUnmarshalAudience = errors.New("go-jose/go-jose/jwt: expected string or array value to unmarshal to Audience") + +// ErrUnmarshalNumericDate indicates that JWT NumericDate could not be unmarshalled. +var ErrUnmarshalNumericDate = errors.New("go-jose/go-jose/jwt: expected number value to unmarshal NumericDate") + +// ErrInvalidClaims indicates that given claims have invalid type. +var ErrInvalidClaims = errors.New("go-jose/go-jose/jwt: expected claims to be value convertible into JSON object") + +// ErrInvalidIssuer indicates invalid iss claim. +var ErrInvalidIssuer = errors.New("go-jose/go-jose/jwt: validation failed, invalid issuer claim (iss)") + +// ErrInvalidSubject indicates invalid sub claim. +var ErrInvalidSubject = errors.New("go-jose/go-jose/jwt: validation failed, invalid subject claim (sub)") + +// ErrInvalidAudience indicated invalid aud claim. +var ErrInvalidAudience = errors.New("go-jose/go-jose/jwt: validation failed, invalid audience claim (aud)") + +// ErrInvalidID indicates invalid jti claim. +var ErrInvalidID = errors.New("go-jose/go-jose/jwt: validation failed, invalid ID claim (jti)") + +// ErrNotValidYet indicates that token is used before time indicated in nbf claim. +var ErrNotValidYet = errors.New("go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)") + +// ErrExpired indicates that token is used after expiry time indicated in exp claim. +var ErrExpired = errors.New("go-jose/go-jose/jwt: validation failed, token is expired (exp)") + +// ErrIssuedInTheFuture indicates that the iat field is in the future. +var ErrIssuedInTheFuture = errors.New("go-jose/go-jose/jwt: validation field, token issued in the future (iat)") + +// ErrInvalidContentType indicates that token requires JWT cty header. +var ErrInvalidContentType = errors.New("go-jose/go-jose/jwt: expected content type to be JWT (cty header)") diff --git a/vendor/github.com/go-jose/go-jose/v3/jwt/jwt.go b/vendor/github.com/go-jose/go-jose/v3/jwt/jwt.go new file mode 100644 index 00000000000..8553fc50b08 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v3/jwt/jwt.go @@ -0,0 +1,133 @@ +/*- + * Copyright 2016 Zbigniew Mandziejewicz + * Copyright 2016 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jwt + +import ( + "fmt" + "strings" + + jose "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/json" +) + +// JSONWebToken represents a JSON Web Token (as specified in RFC7519). +type JSONWebToken struct { + payload func(k interface{}) ([]byte, error) + unverifiedPayload func() []byte + Headers []jose.Header +} + +type NestedJSONWebToken struct { + enc *jose.JSONWebEncryption + Headers []jose.Header +} + +// Claims deserializes a JSONWebToken into dest using the provided key. +func (t *JSONWebToken) Claims(key interface{}, dest ...interface{}) error { + b, err := t.payload(key) + if err != nil { + return err + } + + for _, d := range dest { + if err := json.Unmarshal(b, d); err != nil { + return err + } + } + + return nil +} + +// UnsafeClaimsWithoutVerification deserializes the claims of a +// JSONWebToken into the dests. For signed JWTs, the claims are not +// verified. This function won't work for encrypted JWTs. +func (t *JSONWebToken) UnsafeClaimsWithoutVerification(dest ...interface{}) error { + if t.unverifiedPayload == nil { + return fmt.Errorf("go-jose/go-jose: Cannot get unverified claims") + } + claims := t.unverifiedPayload() + for _, d := range dest { + if err := json.Unmarshal(claims, d); err != nil { + return err + } + } + return nil +} + +func (t *NestedJSONWebToken) Decrypt(decryptionKey interface{}) (*JSONWebToken, error) { + b, err := t.enc.Decrypt(decryptionKey) + if err != nil { + return nil, err + } + + sig, err := ParseSigned(string(b)) + if err != nil { + return nil, err + } + + return sig, nil +} + +// ParseSigned parses token from JWS form. +func ParseSigned(s string) (*JSONWebToken, error) { + sig, err := jose.ParseSigned(s) + if err != nil { + return nil, err + } + headers := make([]jose.Header, len(sig.Signatures)) + for i, signature := range sig.Signatures { + headers[i] = signature.Header + } + + return &JSONWebToken{ + payload: sig.Verify, + unverifiedPayload: sig.UnsafePayloadWithoutVerification, + Headers: headers, + }, nil +} + +// ParseEncrypted parses token from JWE form. +func ParseEncrypted(s string) (*JSONWebToken, error) { + enc, err := jose.ParseEncrypted(s) + if err != nil { + return nil, err + } + + return &JSONWebToken{ + payload: enc.Decrypt, + Headers: []jose.Header{enc.Header}, + }, nil +} + +// ParseSignedAndEncrypted parses signed-then-encrypted token from JWE form. +func ParseSignedAndEncrypted(s string) (*NestedJSONWebToken, error) { + enc, err := jose.ParseEncrypted(s) + if err != nil { + return nil, err + } + + contentType, _ := enc.Header.ExtraHeaders[jose.HeaderContentType].(string) + if strings.ToUpper(contentType) != "JWT" { + return nil, ErrInvalidContentType + } + + return &NestedJSONWebToken{ + enc: enc, + Headers: []jose.Header{enc.Header}, + }, nil +} diff --git a/vendor/github.com/go-jose/go-jose/v3/jwt/validation.go b/vendor/github.com/go-jose/go-jose/v3/jwt/validation.go new file mode 100644 index 00000000000..09d8541f4ce --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v3/jwt/validation.go @@ -0,0 +1,120 @@ +/*- + * Copyright 2016 Zbigniew Mandziejewicz + * Copyright 2016 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jwt + +import "time" + +const ( + // DefaultLeeway defines the default leeway for matching NotBefore/Expiry claims. + DefaultLeeway = 1.0 * time.Minute +) + +// Expected defines values used for protected claims validation. +// If field has zero value then validation is skipped, with the exception of +// Time, where the zero value means "now." To skip validating them, set the +// corresponding field in the Claims struct to nil. +type Expected struct { + // Issuer matches the "iss" claim exactly. + Issuer string + // Subject matches the "sub" claim exactly. + Subject string + // Audience matches the values in "aud" claim, regardless of their order. + Audience Audience + // ID matches the "jti" claim exactly. + ID string + // Time matches the "exp", "nbf" and "iat" claims with leeway. + Time time.Time +} + +// WithTime copies expectations with new time. +func (e Expected) WithTime(t time.Time) Expected { + e.Time = t + return e +} + +// Validate checks claims in a token against expected values. +// A default leeway value of one minute is used to compare time values. +// +// The default leeway will cause the token to be deemed valid until one +// minute after the expiration time. If you're a server application that +// wants to give an extra minute to client tokens, use this +// function. If you're a client application wondering if the server +// will accept your token, use ValidateWithLeeway with a leeway <=0, +// otherwise this function might make you think a token is valid when +// it is not. +func (c Claims) Validate(e Expected) error { + return c.ValidateWithLeeway(e, DefaultLeeway) +} + +// ValidateWithLeeway checks claims in a token against expected values. A +// custom leeway may be specified for comparing time values. You may pass a +// zero value to check time values with no leeway, but you should note that +// numeric date values are rounded to the nearest second and sub-second +// precision is not supported. +// +// The leeway gives some extra time to the token from the server's +// point of view. That is, if the token is expired, ValidateWithLeeway +// will still accept the token for 'leeway' amount of time. This fails +// if you're using this function to check if a server will accept your +// token, because it will think the token is valid even after it +// expires. So if you're a client validating if the token is valid to +// be submitted to a server, use leeway <=0, if you're a server +// validation a token, use leeway >=0. +func (c Claims) ValidateWithLeeway(e Expected, leeway time.Duration) error { + if e.Issuer != "" && e.Issuer != c.Issuer { + return ErrInvalidIssuer + } + + if e.Subject != "" && e.Subject != c.Subject { + return ErrInvalidSubject + } + + if e.ID != "" && e.ID != c.ID { + return ErrInvalidID + } + + if len(e.Audience) != 0 { + for _, v := range e.Audience { + if !c.Audience.Contains(v) { + return ErrInvalidAudience + } + } + } + + // validate using the e.Time, or time.Now if not provided + validationTime := e.Time + if validationTime.IsZero() { + validationTime = time.Now() + } + + if c.NotBefore != nil && validationTime.Add(leeway).Before(c.NotBefore.Time()) { + return ErrNotValidYet + } + + if c.Expiry != nil && validationTime.Add(-leeway).After(c.Expiry.Time()) { + return ErrExpired + } + + // IssuedAt is optional but cannot be in the future. This is not required by the RFC, but + // something is misconfigured if this happens and we should not trust it. + if c.IssuedAt != nil && validationTime.Add(leeway).Before(c.IssuedAt.Time()) { + return ErrIssuedInTheFuture + } + + return nil +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/core/v1/secret/secret.go b/vendor/knative.dev/pkg/client/injection/kube/informers/core/v1/secret/secret.go new file mode 100644 index 00000000000..22ddeb56426 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/core/v1/secret/secret.go @@ -0,0 +1,52 @@ +/* +Copyright 2022 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package secret + +import ( + context "context" + + v1 "k8s.io/client-go/informers/core/v1" + factory "knative.dev/pkg/client/injection/kube/informers/factory" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterInformer(withInformer) +} + +// Key is used for associating the Informer inside the context.Context. +type Key struct{} + +func withInformer(ctx context.Context) (context.Context, controller.Informer) { + f := factory.Get(ctx) + inf := f.Core().V1().Secrets() + return context.WithValue(ctx, Key{}, inf), inf.Informer() +} + +// Get extracts the typed informer from the context. +func Get(ctx context.Context) v1.SecretInformer { + untyped := ctx.Value(Key{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch k8s.io/client-go/informers/core/v1.SecretInformer from context.") + } + return untyped.(v1.SecretInformer) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b4a5382e50b..217cdf53599 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -114,6 +114,7 @@ github.com/evanphx/json-patch/v5 github.com/go-jose/go-jose/v3 github.com/go-jose/go-jose/v3/cipher github.com/go-jose/go-jose/v3/json +github.com/go-jose/go-jose/v3/jwt # github.com/go-kit/log v0.2.1 ## explicit; go 1.17 github.com/go-kit/log @@ -1256,6 +1257,7 @@ knative.dev/pkg/client/injection/kube/informers/core/v1/endpoints/fake knative.dev/pkg/client/injection/kube/informers/core/v1/namespace knative.dev/pkg/client/injection/kube/informers/core/v1/namespace/fake knative.dev/pkg/client/injection/kube/informers/core/v1/pod +knative.dev/pkg/client/injection/kube/informers/core/v1/secret knative.dev/pkg/client/injection/kube/informers/core/v1/service knative.dev/pkg/client/injection/kube/informers/core/v1/service/fake knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount