diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 9e81cc90f45..b28ca7852c8 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -26,6 +26,9 @@ import ( "os" "time" + "knative.dev/eventing/pkg/apis/sources" + + filteredFactory "knative.dev/pkg/client/injection/kube/informers/factory/filtered" "knative.dev/pkg/injection/sharedmain" "knative.dev/pkg/signals" @@ -73,6 +76,8 @@ func main() { } }() + ctx = filteredFactory.WithSelectors(ctx, sources.OIDCTokenRoleLabelSelector) + sharedmain.MainWithContext(ctx, "controller", // Messaging channel.NewController, diff --git a/config/core/resources/apiserversource.yaml b/config/core/resources/apiserversource.yaml index a87c4a23fe8..cf0c0ea0033 100644 --- a/config/core/resources/apiserversource.yaml +++ b/config/core/resources/apiserversource.yaml @@ -249,6 +249,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. This only needs to be set if the target is not an Addressable and thus the Audience can't be received from the Addressable itself. If the target is an Addressable and specifies an Audience, the target's Audience takes precedence. + type: string namespaces: description: Namespaces show the namespaces currently watched by the ApiServerSource type: array diff --git a/config/core/roles/controller-clusterroles.yaml b/config/core/roles/controller-clusterroles.yaml index b123b2bf0d0..b76d16ebd3e 100644 --- a/config/core/roles/controller-clusterroles.yaml +++ b/config/core/roles/controller-clusterroles.yaml @@ -31,6 +31,7 @@ rules: - "events" - "serviceaccounts" - "pods" + - "serviceaccounts/token" verbs: &everything - "get" - "list" @@ -60,6 +61,7 @@ rules: - "rbac.authorization.k8s.io" resources: - "rolebindings" + - "roles" verbs: *everything # Our own resources and statuses we care about. diff --git a/pkg/adapter/v2/cloudevents.go b/pkg/adapter/v2/cloudevents.go index bce437e152f..4bad59436b4 100644 --- a/pkg/adapter/v2/cloudevents.go +++ b/pkg/adapter/v2/cloudevents.go @@ -20,10 +20,14 @@ import ( "context" "errors" "fmt" + nethttp "net/http" "net/url" "time" + "k8s.io/apimachinery/pkg/types" + "knative.dev/eventing/pkg/auth" + cloudevents "github.com/cloudevents/sdk-go/v2" ceclient "github.com/cloudevents/sdk-go/v2/client" "github.com/cloudevents/sdk-go/v2/event" @@ -110,6 +114,7 @@ type ClientConfig struct { Reporter source.StatsReporter CrStatusEventClient *crstatusevent.CRStatusEventClient Options []http.Option + TokenProvider *auth.OIDCTokenProvider } type clientConfigKey struct{} @@ -142,6 +147,7 @@ func NewClient(cfg ClientConfig) (Client, error) { if sinkWait := cfg.Env.GetSinktimeout(); sinkWait > 0 { pOpts = append(pOpts, setTimeOut(time.Duration(sinkWait)*time.Second)) } + if eventingtls.IsHttpsSink(cfg.Env.GetSink()) { var err error @@ -161,6 +167,7 @@ func NewClient(cfg ClientConfig) (Client, error) { Propagation: tracecontextb3.TraceContextEgress, } } + if ceOverrides == nil { var err error ceOverrides, err = cfg.Env.GetCloudEventOverrides() @@ -189,13 +196,22 @@ func NewClient(cfg ClientConfig) (Client, error) { if err != nil { return nil, err } - return &client{ + + client := &client{ ceClient: ceClient, closeIdler: transport.Base.(*nethttp.Transport), ceOverrides: ceOverrides, reporter: cfg.Reporter, crStatusEventClient: cfg.CrStatusEventClient, - }, nil + oidcTokenProvider: cfg.TokenProvider, + } + + if cfg.Env != nil { + client.audience = cfg.Env.GetAudience() + client.oidcServiceAccountName = cfg.Env.GetOIDCServiceAccountName() + } + + return client, nil } func setTimeOut(duration time.Duration) http.Option { @@ -217,6 +233,10 @@ type client struct { reporter source.StatsReporter crStatusEventClient *crstatusevent.CRStatusEventClient closeIdler closeIdler + + oidcTokenProvider *auth.OIDCTokenProvider + audience *string + oidcServiceAccountName *types.NamespacedName } func (c *client) CloseIdleConnections() { @@ -228,6 +248,15 @@ var _ cloudevents.Client = (*client)(nil) // Send implements client.Send func (c *client) Send(ctx context.Context, out event.Event) protocol.Result { c.applyOverrides(&out) + var err error + + if c.audience != nil && c.oidcServiceAccountName != nil { + ctx, err = c.withAuthHeader(ctx) + if err != nil { + return err + } + } + res := c.ceClient.Send(ctx, out) c.reportMetrics(ctx, out, res) return res @@ -236,6 +265,15 @@ func (c *client) Send(ctx context.Context, out event.Event) protocol.Result { // Request implements client.Request func (c *client) Request(ctx context.Context, out event.Event) (*event.Event, protocol.Result) { c.applyOverrides(&out) + var err error + + if c.audience != nil && c.oidcServiceAccountName != nil { + ctx, err = c.withAuthHeader(ctx) + if err != nil { + return nil, err + } + } + resp, res := c.ceClient.Request(ctx, out) c.reportMetrics(ctx, out, res) return resp, res @@ -361,3 +399,21 @@ func tracecontextMiddleware(h nethttp.Handler) nethttp.Handler { FormatSpanName: formatSpanName, } } + +// When OIDC is enabled, withAuthHeader will request the JWT token from the tokenProvider and append it to every request +// it has interaction with, if source's OIDC service account (source.Status.Auth.ServiceAccountName) and destination's +// audience are present. +func (c *client) withAuthHeader(ctx context.Context) (context.Context, error) { + // Request the JWT token for the given service account + jwt, err := c.oidcTokenProvider.GetJWT(*c.oidcServiceAccountName, *c.audience) + if err != nil { + return ctx, protocol.NewResult("Failed when appending the Authorization header to the outgoing request %w", err) + } + + // Appending the auth token to the outgoing request + headers := http.HeaderFrom(ctx) + headers.Set("Authorization", fmt.Sprintf("Bearer %s", jwt)) + ctx = http.WithCustomHeader(ctx, headers) + + return ctx, nil +} diff --git a/pkg/adapter/v2/config.go b/pkg/adapter/v2/config.go index d4d54ba76e4..27a4a1eea3c 100644 --- a/pkg/adapter/v2/config.go +++ b/pkg/adapter/v2/config.go @@ -21,6 +21,8 @@ import ( "strconv" "time" + "k8s.io/apimachinery/pkg/types" + "go.uber.org/zap" duckv1 "knative.dev/pkg/apis/duck/v1" @@ -39,6 +41,8 @@ const ( EnvConfigName = "NAME" EnvConfigResourceGroup = "K_RESOURCE_GROUP" EnvConfigSink = "K_SINK" + EnvConfigAudience = "K_AUDIENCE" + EnvConfigOIDCServiceAccount = "K_OIDC_SERVICE_ACCOUNT" EnvConfigCACert = "K_CA_CERTS" EnvConfigCEOverrides = "K_CE_OVERRIDES" EnvConfigMetricsConfig = "K_METRICS_CONFIG" @@ -66,6 +70,12 @@ type EnvConfig struct { // Sink is the URI messages will be sent. Sink string `envconfig:"K_SINK"` + // Audience is the audience of the target sink. + Audience *string `envconfig:"K_AUDIENCE"` + + // OIDCServiceAccount Name is the name of the service account to use for the adapter. + OIDCServiceAccountName *string `envconfig:"K_OIDC_SERVICE_ACCOUNT"` + // CACerts are the Certification Authority (CA) certificates in PEM format // according to https://www.rfc-editor.org/rfc/rfc7468. // +optional @@ -113,6 +123,12 @@ type EnvConfigAccessor interface { // GetCACerts gets the CACerts of the Sink. GetCACerts() *string + // GetAudience gets the audience of the target sink. + GetAudience() *string + + // GetOIDCServiceAccountName gets the service account name to use for the adapter. + GetOIDCServiceAccountName() *types.NamespacedName + // Get the namespace of the adapter. GetNamespace() string @@ -172,10 +188,24 @@ func (e *EnvConfig) GetSink() string { return e.Sink } +func (e *EnvConfig) GetOIDCServiceAccountName() *types.NamespacedName { + if e.OIDCServiceAccountName != nil { + return &types.NamespacedName{ + Namespace: e.Namespace, + Name: *e.OIDCServiceAccountName, + } + } + return nil +} + func (e *EnvConfig) GetCACerts() *string { return e.CACerts } +func (e *EnvConfig) GetAudience() *string { + return e.Audience +} + func (e *EnvConfig) GetNamespace() string { return e.Namespace } diff --git a/pkg/adapter/v2/main.go b/pkg/adapter/v2/main.go index a839cd66896..475afb447c9 100644 --- a/pkg/adapter/v2/main.go +++ b/pkg/adapter/v2/main.go @@ -26,6 +26,8 @@ import ( "sync" "time" + "knative.dev/eventing/pkg/auth" + cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/kelseyhightower/envconfig" "go.uber.org/zap" @@ -218,6 +220,7 @@ func MainWithInformers(ctx context.Context, component string, env EnvConfigAcces Env: env, Reporter: reporter, CrStatusEventClient: crStatusEventClient, + TokenProvider: auth.NewOIDCTokenProvider(ctx), } ctx = withClientConfig(ctx, clientConfig) diff --git a/pkg/adapter/v2/main_test.go b/pkg/adapter/v2/main_test.go index 25394074ab1..de2130f003e 100644 --- a/pkg/adapter/v2/main_test.go +++ b/pkg/adapter/v2/main_test.go @@ -28,6 +28,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + fakekubeclient "knative.dev/pkg/client/injection/kube/client/fake" kubeclient "knative.dev/pkg/client/injection/kube/client/fake" "knative.dev/pkg/configmap" "knative.dev/pkg/leaderelection" @@ -50,7 +51,7 @@ type myAdapter struct { blocking bool } -func TestMainWithNothing(t *testing.T) { +func TestMainWithContext(t *testing.T) { os.Setenv("K_SINK", "http://sink") os.Setenv("NAMESPACE", "ns") os.Setenv("K_METRICS_CONFIG", "error config") @@ -65,7 +66,10 @@ func TestMainWithNothing(t *testing.T) { os.Unsetenv("MODE") }() - Main("mycomponent", + ctx := context.TODO() + ctx, _ = fakekubeclient.With(ctx) + + MainWithContext(ctx, "mycomponent", func() EnvConfigAccessor { return &myEnvConfig{} }, func(ctx context.Context, processed EnvConfigAccessor, client cloudevents.Client) Adapter { env := processed.(*myEnvConfig) @@ -103,6 +107,7 @@ func TestMainWithInformerNoLeaderElection(t *testing.T) { }() ctx, cancel := context.WithCancel(context.TODO()) + ctx, _ = fakekubeclient.With(ctx) env := ConstructEnvOrDie(func() EnvConfigAccessor { return &myEnvConfig{} }) done := make(chan bool) go func() { @@ -161,6 +166,7 @@ func TestMain_MetricsConfig(t *testing.T) { }() ctx, cancel := context.WithCancel(context.TODO()) + ctx, _ = fakekubeclient.With(ctx) env := ConstructEnvOrDie(func() EnvConfigAccessor { return &myEnvConfig{} }) done := make(chan bool) go func() { diff --git a/pkg/apis/sources/register.go b/pkg/apis/sources/register.go index 55b4a748b17..3cd87d78e75 100644 --- a/pkg/apis/sources/register.go +++ b/pkg/apis/sources/register.go @@ -32,6 +32,12 @@ const ( // SourceDuckLabelValue is the label value to indicate // the CRD is a Source duck type. SourceDuckLabelValue = "true" + + //OIDCLabelKey is used to filter out all the informers that related to OIDC work + OIDCLabelKey = "oidc" + + // OIDCTokenRoleLabelSelector is the label selector for the OIDC token creator role and rolebinding informers + OIDCTokenRoleLabelSelector = OIDCLabelKey ) var ( diff --git a/pkg/apis/sources/v1/apiserver_lifecycle.go b/pkg/apis/sources/v1/apiserver_lifecycle.go index 70d0f767493..b4604dfe432 100644 --- a/pkg/apis/sources/v1/apiserver_lifecycle.go +++ b/pkg/apis/sources/v1/apiserver_lifecycle.go @@ -84,6 +84,7 @@ func (s *ApiServerSourceStatus) MarkSink(addr *duckv1.Addressable) { if addr != nil { s.SinkURI = addr.URL s.SinkCACerts = addr.CACerts + s.SinkAudience = addr.Audience apiserverCondSet.Manage(s).MarkTrue(ApiServerConditionSinkProvided) } else { apiserverCondSet.Manage(s).MarkFalse(ApiServerConditionSinkProvided, "SinkEmpty", "Sink has resolved to empty.%s", "") diff --git a/pkg/auth/serviceaccount.go b/pkg/auth/serviceaccount.go index 01e31f4b642..fe308d64247 100644 --- a/pkg/auth/serviceaccount.go +++ b/pkg/auth/serviceaccount.go @@ -21,6 +21,10 @@ import ( "fmt" "strings" + "knative.dev/eventing/pkg/apis/feature" + duckv1 "knative.dev/pkg/apis/duck/v1" + pkgreconciler "knative.dev/pkg/reconciler" + "go.uber.org/zap" v1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" @@ -28,11 +32,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" corev1listers "k8s.io/client-go/listers/core/v1" - "knative.dev/eventing/pkg/apis/feature" - duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/pkg/logging" "knative.dev/pkg/ptr" - pkgreconciler "knative.dev/pkg/reconciler" ) // GetOIDCServiceAccountNameForResource returns the service account name to use diff --git a/pkg/reconciler/apiserversource/apiserversource.go b/pkg/reconciler/apiserversource/apiserversource.go index f96a8e4c9f8..a865d3e232a 100644 --- a/pkg/reconciler/apiserversource/apiserversource.go +++ b/pkg/reconciler/apiserversource/apiserversource.go @@ -22,6 +22,10 @@ import ( "fmt" "sort" + rbacv1listers "k8s.io/client-go/listers/rbac/v1" + + apierrs "k8s.io/apimachinery/pkg/api/errors" + "go.uber.org/zap" appsv1 "k8s.io/api/apps/v1" authorizationv1 "k8s.io/api/authorization/v1" @@ -75,6 +79,8 @@ type Reconciler struct { namespaceLister clientv1.NamespaceLister serviceAccountLister clientv1.ServiceAccountLister + roleLister rbacv1listers.RoleLister + roleBindingLister rbacv1listers.RoleBindingLister } var _ apiserversourcereconciler.Interface = (*Reconciler)(nil) @@ -106,6 +112,23 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, source *v1.ApiServerSour return err } + if featureFlags.IsOIDCAuthentication() { + // Create the role + err := r.createOIDCRole(ctx, source) + + if err != nil { + logging.FromContext(ctx).Errorw("Failed when creating the OIDC Role for ApiServerSource", zap.Error(err)) + return err + } + + // Create the rolebinding + err = r.createOIDCRoleBinding(ctx, source) + if err != nil { + logging.FromContext(ctx).Errorw("Failed when creating the OIDC RoleBinding for ApiServerSource", zap.Error(err)) + return err + } + } + sinkAddr, err := r.sinkResolver.AddressableFromDestinationV1(ctx, *dest, source) if err != nil { source.Status.MarkNoSink("NotFound", "") @@ -134,6 +157,7 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, source *v1.ApiServerSour logging.FromContext(ctx).Errorw("Unable to create the receive adapter", zap.Error(err)) return err } + source.Status.PropagateDeploymentAvailability(ra) cloudEventAttributes, err := r.createCloudEventAttributes(source) @@ -200,10 +224,12 @@ func (r *Reconciler) createReceiveAdapter(ctx context.Context, src *v1.ApiServer Labels: resources.Labels(src.Name), CACerts: sinkAddr.CACerts, SinkURI: sinkAddr.URL.String(), + Audience: sinkAddr.Audience, Configs: r.configs, Namespaces: namespaces, AllNamespaces: allNamespaces, } + expected, err := resources.MakeReceiveAdapter(&adapterArgs) if err != nil { return nil, err @@ -256,6 +282,7 @@ func (r *Reconciler) runAccessCheck(ctx context.Context, src *v1.ApiServerSource return nil } + // Run the basic service account access check (This is not OIDC service account) user := "system:serviceaccount:" + src.Namespace + ":" if src.Spec.ServiceAccountName == "" { user += "default" @@ -338,3 +365,83 @@ func (r *Reconciler) createCloudEventAttributes(src *v1.ApiServerSource) ([]duck } return ceAttributes, nil } + +// createOIDCRole: this function will call resources package to get the role object +// and then pass to kubeclient to make the actual OIDC role +func (r *Reconciler) createOIDCRole(ctx context.Context, source *v1.ApiServerSource) error { + roleName := resources.GetOIDCTokenRoleName(source.Name) + + expected, err := resources.MakeOIDCRole(source) + + if err != nil { + return fmt.Errorf("Cannot create OIDC role for ApiServerSource %s/%s: %w", source.GetName(), source.GetNamespace(), err) + } + + // By querying roleLister to see whether the role exist or not + role, err := r.roleLister.Roles(source.GetNamespace()).Get(roleName) + + if apierrs.IsNotFound(err) { + // If the role does not exist, we will call kubeclient to create it + role = expected + _, err = r.kubeClientSet.RbacV1().Roles(source.GetNamespace()).Create(ctx, role, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("could not create OIDC service account role %s/%s for %s: %w", source.GetName(), source.GetNamespace(), "ApiServerSource", err) + } + } else { + // If the role does exist, we will check whether an update is needed + // By comparing the role's rule + if !equality.Semantic.DeepEqual(role.Rules, expected.Rules) { + // If the role's rules are not equal, we will update the role + role.Rules = expected.Rules + _, err = r.kubeClientSet.RbacV1().Roles(source.GetNamespace()).Update(ctx, role, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update OIDC service account role %s/%s for %s: %w", source.GetName(), source.GetNamespace(), "ApiServerSource", err) + } + } else { + // If the role does exist and no update is needed, we will just return + return nil + } + } + + return nil + +} + +// createOIDCRoleBinding: this function will call resources package to get the rolebinding object +// and then pass to kubeclient to make the actual OIDC rolebinding +func (r *Reconciler) createOIDCRoleBinding(ctx context.Context, source *v1.ApiServerSource) error { + roleBindingName := resources.GetOIDCTokenRoleBindingName(source.Name) + + expected, err := resources.MakeOIDCRoleBinding(source) + if err != nil { + return fmt.Errorf("Cannot create OIDC roleBinding for ApiServerSource %s/%s: %w", source.GetName(), source.GetNamespace(), err) + } + + // By querying roleBindingLister to see whether the roleBinding exist or not + roleBinding, err := r.roleBindingLister.RoleBindings(source.GetNamespace()).Get(roleBindingName) + if apierrs.IsNotFound(err) { + // If the role does not exist, we will call kubeclient to create it + roleBinding = expected + _, err = r.kubeClientSet.RbacV1().RoleBindings(source.GetNamespace()).Create(ctx, roleBinding, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("could not create OIDC service account rolebinding %s/%s for %s: %w", source.GetName(), source.GetNamespace(), "apiserversource", err) + } + } else { + // If the role does exist, we will check whether an update is needed + // By comparing the role's rule + if !equality.Semantic.DeepEqual(roleBinding.RoleRef, expected.RoleRef) || !equality.Semantic.DeepEqual(roleBinding.Subjects, expected.Subjects) { + // If the role's rules are not equal, we will update the role + roleBinding.RoleRef = expected.RoleRef + roleBinding.Subjects = expected.Subjects + _, err = r.kubeClientSet.RbacV1().RoleBindings(source.GetNamespace()).Update(ctx, roleBinding, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update OIDC service account rolebinding %s/%s for %s: %w", source.GetName(), source.GetNamespace(), "apiserversource", err) + } + } else { + // If the role does exist and no update is needed, we will just return + return nil + } + } + + return nil +} diff --git a/pkg/reconciler/apiserversource/apiserversource_test.go b/pkg/reconciler/apiserversource/apiserversource_test.go index f2101a1321e..b6a15fdc0cd 100644 --- a/pkg/reconciler/apiserversource/apiserversource_test.go +++ b/pkg/reconciler/apiserversource/apiserversource_test.go @@ -21,6 +21,12 @@ import ( "fmt" "testing" + "knative.dev/eventing/pkg/apis/sources" + + "knative.dev/pkg/kmeta" + + rbacv1 "k8s.io/api/rbac/v1" + "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -53,6 +59,9 @@ import ( rttesting "knative.dev/eventing/pkg/reconciler/testing" rttestingv1 "knative.dev/eventing/pkg/reconciler/testing/v1" . "knative.dev/pkg/reconciler/testing" + + _ "knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/fake" + _ "knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/fake" ) var ( @@ -83,6 +92,21 @@ var ( Name: &sinkURL.Scheme, URL: sinkURL, } + + sinkAudience = "sink-oidc-audience" + sinkOIDCAddressable = &duckv1.Addressable{ + Name: &sinkURL.Scheme, + URL: sinkURL, + Audience: &sinkAudience, + } + sinkOIDCDest = duckv1.Destination{ + Ref: &duckv1.KReference{ + Name: sinkName, + Kind: "Channel", + APIVersion: "messaging.knative.dev/v1", + }, + Audience: &sinkAudience, + } ) const ( @@ -891,16 +915,18 @@ func TestReconcile(t *testing.T) { APIVersion: "v1", Kind: "Namespace", }}, - SourceSpec: duckv1.SourceSpec{Sink: sinkDest}, + SourceSpec: duckv1.SourceSpec{Sink: sinkOIDCDest}, }), rttestingv1.WithApiServerSourceUID(sourceUID), rttestingv1.WithApiServerSourceObjectMetaGeneration(generation), ), rttestingv1.NewChannel(sinkName, testNS, rttestingv1.WithInitChannelConditions, - rttestingv1.WithChannelAddress(sinkAddressable), + rttestingv1.WithChannelAddress(sinkOIDCAddressable), ), - makeAvailableReceiveAdapter(t), + makeOIDCRole(), + makeOIDCRoleBinding(), + makeAvailableReceiveAdapterWithOIDC(t), }, Key: testNS + "/" + sourceName, WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ @@ -910,14 +936,14 @@ func TestReconcile(t *testing.T) { APIVersion: "v1", Kind: "Namespace", }}, - SourceSpec: duckv1.SourceSpec{Sink: sinkDest}, + SourceSpec: duckv1.SourceSpec{Sink: sinkOIDCDest}, }), rttestingv1.WithApiServerSourceUID(sourceUID), rttestingv1.WithApiServerSourceObjectMetaGeneration(generation), // Status Update: rttestingv1.WithInitApiServerSourceConditions, rttestingv1.WithApiServerSourceDeployed, - rttestingv1.WithApiServerSourceSink(sinkURI), + rttestingv1.WithApiServerSourceSinkAddressable(sinkOIDCAddressable), rttestingv1.WithApiServerSourceSufficientPermissions, rttestingv1.WithApiServerSourceReferenceModeEventTypes(source), rttestingv1.WithApiServerSourceStatusObservedGeneration(generation), @@ -995,6 +1021,70 @@ func TestReconcile(t *testing.T) { WithReactors: []clientgotesting.ReactionFunc{subjectAccessReviewCreateReactor(true)}, SkipNamespaceValidation: true, // SubjectAccessReview objects are cluster-scoped. }, + { + Name: "OIDC: creates role and rolebinding to create OIDC token", + Ctx: feature.ToContext(context.Background(), feature.Flags{ + feature.OIDCAuthentication: feature.Enabled, + }), + Objects: []runtime.Object{ + rttestingv1.NewApiServerSource(sourceName, testNS, + rttestingv1.WithApiServerSourceSpec(sourcesv1.ApiServerSourceSpec{ + Resources: []sourcesv1.APIVersionKindSelector{{ + APIVersion: "v1", + Kind: "Namespace", + }}, + SourceSpec: duckv1.SourceSpec{Sink: sinkOIDCDest}, + }), + rttestingv1.WithApiServerSourceUID(sourceUID), + rttestingv1.WithApiServerSourceObjectMetaGeneration(generation), + ), + rttestingv1.NewChannel(sinkName, testNS, + rttestingv1.WithInitChannelConditions, + rttestingv1.WithChannelAddress(sinkOIDCAddressable), + ), + makeAvailableReceiveAdapterWithOIDC(t), + makeApiServerSourceOIDCServiceAccount(), + }, + Key: testNS + "/" + sourceName, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: rttestingv1.NewApiServerSource(sourceName, testNS, + rttestingv1.WithApiServerSourceSpec(sourcesv1.ApiServerSourceSpec{ + Resources: []sourcesv1.APIVersionKindSelector{{ + APIVersion: "v1", + Kind: "Namespace", + }}, + SourceSpec: duckv1.SourceSpec{Sink: sinkOIDCDest}, + }), + rttestingv1.WithApiServerSourceUID(sourceUID), + rttestingv1.WithApiServerSourceObjectMetaGeneration(generation), + // Status Update: + rttestingv1.WithInitApiServerSourceConditions, + rttestingv1.WithApiServerSourceDeployed, + rttestingv1.WithApiServerSourceSinkAddressable(sinkOIDCAddressable), + rttestingv1.WithApiServerSourceSufficientPermissions, + rttestingv1.WithApiServerSourceReferenceModeEventTypes(source), + rttestingv1.WithApiServerSourceStatusObservedGeneration(generation), + rttestingv1.WithApiServerSourceStatusNamespaces([]string{testNS}), + rttestingv1.WithApiServerSourceOIDCIdentityCreatedSucceeded(), + rttestingv1.WithApiServerSourceOIDCServiceAccountName(makeApiServerSourceOIDCServiceAccount().Name), + ), + }}, + WantCreates: []runtime.Object{ + makeOIDCRole(), + makeOIDCRoleBinding(), + makeSubjectAccessReview("namespaces", "get", "default"), + makeSubjectAccessReview("namespaces", "list", "default"), + makeSubjectAccessReview("namespaces", "watch", "default"), + }, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "FinalizerUpdate", "Updated %q finalizers", sourceName), + }, + WantPatches: []clientgotesting.PatchActionImpl{ + patchFinalizers(sourceName, testNS), + }, + WithReactors: []clientgotesting.ReactionFunc{subjectAccessReviewCreateReactor(true)}, + SkipNamespaceValidation: true, // SubjectAccessReview objects are cluster-scoped. + }, } logger := logtesting.TestLogger(t) @@ -1008,6 +1098,8 @@ func TestReconcile(t *testing.T) { configs: &reconcilersource.EmptyVarsGenerator{}, namespaceLister: listers.GetNamespaceLister(), serviceAccountLister: listers.GetServiceAccountLister(), + roleBindingLister: listers.GetRoleBindingLister(), + roleLister: listers.GetRoleLister(), } return apiserversource.NewReconciler(ctx, logger, fakeeventingclient.Get(ctx), listers.GetApiServerSourceLister(), @@ -1055,6 +1147,50 @@ func makeReceiveAdapterWithName(t *testing.T, sourceName string) *appsv1.Deploym return ra } +func makeReceiveAdapterWithOIDC(t *testing.T) *appsv1.Deployment { + t.Helper() + + src := rttestingv1.NewApiServerSource(sourceName, testNS, + rttestingv1.WithApiServerSourceSpec(sourcesv1.ApiServerSourceSpec{ + Resources: []sourcesv1.APIVersionKindSelector{{ + APIVersion: "v1", + Kind: "Namespace", + }}, + SourceSpec: duckv1.SourceSpec{ + Sink: sinkOIDCDest, + }, + }), + rttestingv1.WithApiServerSourceUID(sourceUID), + // Status Update: + rttestingv1.WithInitApiServerSourceConditions, + rttestingv1.WithApiServerSourceDeployed, + rttestingv1.WithApiServerSourceSink(sinkURI), + rttestingv1.WithApiServerSourceOIDCServiceAccountName(makeApiServerSourceOIDCServiceAccount().Name), + ) + + args := resources.ReceiveAdapterArgs{ + Image: image, + Source: src, + Labels: resources.Labels(sourceName), + SinkURI: sinkURI.String(), + Configs: &reconcilersource.EmptyVarsGenerator{}, + Namespaces: []string{testNS}, + Audience: &sinkAudience, + } + + ra, err := resources.MakeReceiveAdapter(&args) + require.NoError(t, err) + + return ra +} + +func makeAvailableReceiveAdapterWithOIDC(t *testing.T) *appsv1.Deployment { + ra := makeReceiveAdapterWithOIDC(t) + rttesting.WithDeploymentAvailable()(ra) + + return ra +} + func makeAvailableReceiveAdapter(t *testing.T) *appsv1.Deployment { ra := makeReceiveAdapter(t) rttesting.WithDeploymentAvailable()(ra) @@ -1204,6 +1340,69 @@ func makeSubjectAccessReview(resource, verb, sa string) *authorizationv1.Subject return makeNamespacedSubjectAccessReview(resource, verb, sa, testNS) } +func makeOIDCRole() *rbacv1.Role { + src := rttestingv1.NewApiServerSource(sourceName, testNS, + rttestingv1.WithApiServerSourceUID(sourceUID), + ) + return &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: resources.GetOIDCTokenRoleName(sourceName), + Namespace: testNS, + Annotations: map[string]string{ + "description": fmt.Sprintf("Role for OIDC Authentication for ApiServerSource %q", sourceName), + }, + Labels: map[string]string{ + sources.OIDCLabelKey: "", + }, + OwnerReferences: []metav1.OwnerReference{ + *kmeta.NewControllerRef(src), + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + // apiServerSource OIDC service account name, it is in the source.Status, NOT in source.Spec + ResourceNames: []string{makeApiServerSourceOIDCServiceAccount().Name}, + Resources: []string{"serviceaccounts/token"}, + Verbs: []string{"create"}, + }, + }, + } +} + +func makeOIDCRoleBinding() *rbacv1.RoleBinding { + src := rttestingv1.NewApiServerSource(sourceName, testNS, + rttestingv1.WithApiServerSourceUID(sourceUID), + ) + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: resources.GetOIDCTokenRoleBindingName(sourceName), + Namespace: testNS, + Annotations: map[string]string{ + "description": fmt.Sprintf("Role Binding for OIDC Authentication for ApiServerSource %q", sourceName), + }, + Labels: map[string]string{ + sources.OIDCLabelKey: "", + }, + OwnerReferences: []metav1.OwnerReference{ + *kmeta.NewControllerRef(src), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: resources.GetOIDCTokenRoleName(sourceName), + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Namespace: testNS, + Name: "default", + }, + }, + } +} + func subjectAccessReviewCreateReactor(allowed bool) clientgotesting.ReactionFunc { return func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) { if action.GetVerb() == "create" && action.GetResource().Resource == "subjectaccessreviews" { diff --git a/pkg/reconciler/apiserversource/controller.go b/pkg/reconciler/apiserversource/controller.go index f7fb4b1e1f0..ae0e38bd191 100644 --- a/pkg/reconciler/apiserversource/controller.go +++ b/pkg/reconciler/apiserversource/controller.go @@ -19,6 +19,8 @@ package apiserversource import ( "context" + "knative.dev/eventing/pkg/apis/sources" + "knative.dev/eventing/pkg/apis/feature" "github.com/kelseyhightower/envconfig" @@ -38,6 +40,8 @@ import ( apiserversourceinformer "knative.dev/eventing/pkg/client/injection/informers/sources/v1/apiserversource" apiserversourcereconciler "knative.dev/eventing/pkg/client/injection/reconciler/sources/v1/apiserversource" serviceaccountinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount" + roleinformer "knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered" + rolebindinginformer "knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered" ) // envConfig will be used to extract the required environment variables using @@ -59,6 +63,10 @@ func NewController( namespaceInformer := namespace.Get(ctx) serviceaccountInformer := serviceaccountinformer.Get(ctx) + // Create a selector string + roleInformer := roleinformer.Get(ctx, sources.OIDCTokenRoleLabelSelector) + rolebindingInformer := rolebindinginformer.Get(ctx, sources.OIDCTokenRoleLabelSelector) + var globalResync func(obj interface{}) featureStore := feature.NewStore(logging.FromContext(ctx).Named("feature-config-store"), func(name string, value interface{}) { @@ -74,6 +82,8 @@ func NewController( configs: reconcilersource.WatchConfigurations(ctx, component, cmw), namespaceLister: namespaceInformer.Lister(), serviceAccountLister: serviceaccountInformer.Lister(), + roleLister: roleInformer.Lister(), + roleBindingLister: rolebindingInformer.Lister(), } env := &envConfig{} @@ -101,6 +111,16 @@ func NewController( Handler: controller.HandleAll(impl.EnqueueControllerOf), }) + roleInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterController(&v1.ApiServerSource{}), + Handler: controller.HandleAll(impl.EnqueueControllerOf), + }) + + rolebindingInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterController(&v1.ApiServerSource{}), + Handler: controller.HandleAll(impl.EnqueueControllerOf), + }) + cb := func() { logging.FromContext(ctx).Info("Global resync of APIServerSources due to namespaces changing.") impl.GlobalResync(apiServerSourceInformer.Informer()) diff --git a/pkg/reconciler/apiserversource/controller_test.go b/pkg/reconciler/apiserversource/controller_test.go index f0063203323..16d9a5df23c 100644 --- a/pkg/reconciler/apiserversource/controller_test.go +++ b/pkg/reconciler/apiserversource/controller_test.go @@ -17,9 +17,13 @@ limitations under the License. package apiserversource import ( + "context" "os" "testing" + "knative.dev/eventing/pkg/apis/sources" + filteredFactory "knative.dev/pkg/client/injection/kube/informers/factory/filtered" + "knative.dev/eventing/pkg/apis/feature" corev1 "k8s.io/api/core/v1" @@ -37,13 +41,18 @@ import ( _ "knative.dev/pkg/client/injection/kube/informers/apps/v1/deployment/fake" _ "knative.dev/pkg/client/injection/kube/informers/core/v1/namespace/fake" _ "knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount/fake" + _ "knative.dev/pkg/client/injection/kube/informers/factory/filtered/fake" + _ "knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered/fake" + _ "knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered/fake" . "knative.dev/pkg/reconciler/testing" ) func TestNew(t *testing.T) { - ctx, _ := SetupFakeContext(t) + ctx, _ := SetupFakeContext(t, SetUpInformerSelector) + ctx = withCfgHost(ctx, &rest.Config{Host: "unit_test"}) ctx = addressable.WithDuck(ctx) + os.Setenv("METRICS_DOMAIN", "knative.dev/eventing") os.Setenv("APISERVER_RA_IMAGE", "knative.dev/example") c := NewController(ctx, configmap.NewStaticWatcher(&corev1.ConfigMap{ @@ -83,3 +92,8 @@ func TestNew(t *testing.T) { t.Fatal("Expected NewController to return a non-nil value") } } + +func SetUpInformerSelector(ctx context.Context) context.Context { + ctx = filteredFactory.WithSelectors(ctx, sources.OIDCTokenRoleLabelSelector) + return ctx +} diff --git a/pkg/reconciler/apiserversource/resources/oidc_rolebinding.go b/pkg/reconciler/apiserversource/resources/oidc_rolebinding.go new file mode 100644 index 00000000000..0d0ca65dc72 --- /dev/null +++ b/pkg/reconciler/apiserversource/resources/oidc_rolebinding.go @@ -0,0 +1,116 @@ +/* +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 resources + +import ( + "fmt" + + "knative.dev/eventing/pkg/apis/sources" + + "knative.dev/pkg/kmeta" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "knative.dev/eventing/pkg/apis/sources/v1" +) + +// GetOIDCTokenRoleName will return the name of the role for creating the JWT token +func GetOIDCTokenRoleName(sourceName string) string { + return kmeta.ChildName(sourceName, "-create-oidc-token") +} + +// GetOIDCTokenRoleBindingName will return the name of the rolebinding for creating the JWT token +func GetOIDCTokenRoleBindingName(sourceName string) string { + return kmeta.ChildName(sourceName, "-create-oidc-token") +} + +// MakeOIDCRole will return the role object config for generating the JWT token +func MakeOIDCRole(source *v1.ApiServerSource) (*rbacv1.Role, error) { + roleName := GetOIDCTokenRoleName(source.Name) + + if source.Status.Auth == nil || source.Status.Auth.ServiceAccountName == nil { + return nil, fmt.Errorf("Error when making OIDC Role for apiserversource, as the OIDC service account does not exist") + } + + return &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Namespace: source.GetNamespace(), + Annotations: map[string]string{ + "description": fmt.Sprintf("Role for OIDC Authentication for ApiServerSource %q", source.GetName()), + }, + Labels: map[string]string{ + sources.OIDCLabelKey: "", + }, + OwnerReferences: []metav1.OwnerReference{ + *kmeta.NewControllerRef(source), + }, + }, + Rules: []rbacv1.PolicyRule{ + rbacv1.PolicyRule{ + APIGroups: []string{""}, + // apiServerSource OIDC service account name, it is in the source.Status, NOT in source.Spec + ResourceNames: []string{*source.Status.Auth.ServiceAccountName}, + Resources: []string{"serviceaccounts/token"}, + Verbs: []string{"create"}, + }, + }, + }, nil + +} + +// MakeOIDCRoleBinding will return the rolebinding object for generating the JWT token +// So that ApiServerSource's service account have access to create the JWT token for it's OIDC service account and the target audience +// Note: it is in the source.Spec, NOT in source.Auth +func MakeOIDCRoleBinding(source *v1.ApiServerSource) (*rbacv1.RoleBinding, error) { + roleName := GetOIDCTokenRoleName(source.Name) + roleBindingName := GetOIDCTokenRoleBindingName(source.Name) + + if source.Spec.ServiceAccountName == "" { + return nil, fmt.Errorf("Error when making OIDC RoleBinding for apiserversource, as the Spec service account does not exist") + } + + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleBindingName, + Namespace: source.GetNamespace(), + Annotations: map[string]string{ + "description": fmt.Sprintf("Role Binding for OIDC Authentication for ApiServerSource %q", source.GetName()), + }, + Labels: map[string]string{ + sources.OIDCLabelKey: "", + }, + OwnerReferences: []metav1.OwnerReference{ + *kmeta.NewControllerRef(source), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: roleName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Namespace: source.GetNamespace(), + //Note: apiServerSource service account name, it is in the source.Spec, NOT in source.Status.Auth + Name: source.Spec.ServiceAccountName, + }, + }, + }, nil + +} diff --git a/pkg/reconciler/apiserversource/resources/receive_adapter.go b/pkg/reconciler/apiserversource/resources/receive_adapter.go index b7f1bd5363a..4f05730f2ca 100644 --- a/pkg/reconciler/apiserversource/resources/receive_adapter.go +++ b/pkg/reconciler/apiserversource/resources/receive_adapter.go @@ -20,15 +20,13 @@ import ( "encoding/json" "fmt" - "knative.dev/eventing/pkg/adapter/v2" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" - + "knative.dev/eventing/pkg/adapter/v2" "knative.dev/pkg/kmeta" "knative.dev/pkg/ptr" "knative.dev/pkg/system" @@ -44,6 +42,7 @@ type ReceiveAdapterArgs struct { Image string Source *v1.ApiServerSource Labels map[string]string + Audience *string SinkURI string CACerts *string Configs reconcilersource.ConfigAccessor @@ -172,7 +171,8 @@ func makeEnv(args *ReceiveAdapterArgs) ([]corev1.EnvVar, error) { }, { Name: "METRICS_DOMAIN", Value: "knative.dev/eventing", - }} + }, + } if args.CACerts != nil { envs = append(envs, corev1.EnvVar{ @@ -181,6 +181,20 @@ func makeEnv(args *ReceiveAdapterArgs) ([]corev1.EnvVar, error) { }) } + if args.Audience != nil { + envs = append(envs, corev1.EnvVar{ + Name: adapter.EnvConfigAudience, + Value: *args.Audience, + }) + } + + if args.Source.Status.Auth != nil && args.Source.Status.Auth.ServiceAccountName != nil { + envs = append(envs, corev1.EnvVar{ + Name: adapter.EnvConfigOIDCServiceAccount, + Value: *args.Source.Status.Auth.ServiceAccountName, + }) + } + envs = append(envs, args.Configs.ToEnvVars()...) if args.Source.Spec.CloudEventOverrides != nil { diff --git a/pkg/reconciler/testing/v1/apiserversouce.go b/pkg/reconciler/testing/v1/apiserversouce.go index 08d799de74c..cdbe78c8b81 100644 --- a/pkg/reconciler/testing/v1/apiserversouce.go +++ b/pkg/reconciler/testing/v1/apiserversouce.go @@ -72,6 +72,12 @@ func WithApiServerSourceSink(uri *apis.URL) ApiServerSourceOption { } } +func WithApiServerSourceSinkAddressable(sinkAddr *duckv1.Addressable) ApiServerSourceOption { + return func(s *v1.ApiServerSource) { + s.Status.MarkSink(sinkAddr) + } +} + func WithApiServerSourceDeploymentUnavailable(s *v1.ApiServerSource) { // The Deployment uses GenerateName, so its name is empty. name := kmeta.ChildName(fmt.Sprintf("apiserversource-%s-", s.Name), string(s.GetUID())) diff --git a/pkg/reconciler/testing/v1/listers.go b/pkg/reconciler/testing/v1/listers.go index 83f1b599893..788b2fab2cc 100644 --- a/pkg/reconciler/testing/v1/listers.go +++ b/pkg/reconciler/testing/v1/listers.go @@ -180,6 +180,10 @@ func (l *Listers) GetServiceLister() corev1listers.ServiceLister { return corev1listers.NewServiceLister(l.indexerFor(&corev1.Service{})) } +func (l *Listers) GetRoleLister() rbacv1listers.RoleLister { + return rbacv1listers.NewRoleLister(l.indexerFor(&rbacv1.Role{})) +} + func (l *Listers) GetRoleBindingLister() rbacv1listers.RoleBindingLister { return rbacv1listers.NewRoleBindingLister(l.indexerFor(&rbacv1.RoleBinding{})) } diff --git a/test/auth/features/oidc/apiserversource.go b/test/auth/features/oidc/apiserversource.go new file mode 100644 index 00000000000..658449a25e7 --- /dev/null +++ b/test/auth/features/oidc/apiserversource.go @@ -0,0 +1,98 @@ +/* +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 oidc + +import ( + "context" + + "github.com/cloudevents/sdk-go/v2/test" + rbacv1 "k8s.io/api/rbac/v1" + v1 "knative.dev/eventing/pkg/apis/sources/v1" + "knative.dev/eventing/test/rekt/resources/account_role" + "knative.dev/eventing/test/rekt/resources/apiserversource" + "knative.dev/reconciler-test/pkg/eventshub" + eventassert "knative.dev/reconciler-test/pkg/eventshub/assert" + "knative.dev/reconciler-test/pkg/feature" + "knative.dev/reconciler-test/pkg/manifest" + "knative.dev/reconciler-test/pkg/resources/pod" + "knative.dev/reconciler-test/pkg/resources/service" +) + +const ( + exampleImage = "ko://knative.dev/eventing/test/test_images/print" +) + +func ApiserversourceSendEventWithJWT() *feature.Feature { + src := feature.MakeRandomK8sName("apiserversource") + sink := feature.MakeRandomK8sName("sink") + audience := "my-sink-audience" + sacmName := feature.MakeRandomK8sName("apiserversource") + + f := feature.NewFeatureNamed("ApiServerSource send events with OIDC authentication") + + f.Setup("deploy receiver", eventshub.Install(sink, + eventshub.StartReceiver, + eventshub.OIDCReceiverAudience(audience))) + + f.Setup("Create Service Account for ApiServerSource with RBAC for v1.Event resources", + setupAccountAndRoleForApiserversource(sacmName)) + + cfg := []manifest.CfgFn{ + apiserversource.WithServiceAccountName(sacmName), + apiserversource.WithEventMode(v1.ResourceMode), + apiserversource.WithResources(v1.APIVersionKindSelector{ + APIVersion: "v1", + Kind: "Event", + }), + } + + f.Requirement("install ApiServerSource", func(ctx context.Context, t feature.T) { + d := service.AsDestinationRef(sink) + d.Audience = &audience + + cfg = append(cfg, apiserversource.WithSink(d)) + apiserversource.Install(src, cfg...)(ctx, t) + }) + f.Requirement("ApiServerSource goes ready", apiserversource.IsReady(src)) + + examplePodName := feature.MakeRandomK8sName("example") + // create a pod so that ApiServerSource delivers an event to its sink + // event body is similar to this: + // {"kind":"Pod","namespace":"test-wmbcixlv","name":"example-axvlzbvc","apiVersion":"v1"} + f.Requirement("install example pod", pod.Install(examplePodName, exampleImage)) + + f.Stable("ApiServerSource as event source"). + Must("delivers events on sink with ref", + eventassert.OnStore(sink). + Match(eventassert.MatchKind(eventshub.EventReceived)). + MatchEvent(test.HasType("dev.knative.apiserver.resource.update")). + AtLeast(1), + ) + + return f +} + +func setupAccountAndRoleForApiserversource(sacmName string) feature.StepFn { + return account_role.Install(sacmName, + account_role.WithRole(sacmName+"-clusterrole"), + account_role.WithRules(rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"events"}, + Verbs: []string{"get", "list", "watch", "create"}, + }), + ) +} diff --git a/test/auth/oidc_test.go b/test/auth/oidc_test.go index 80e0eb53c1c..5ebe5e6c46b 100644 --- a/test/auth/oidc_test.go +++ b/test/auth/oidc_test.go @@ -145,6 +145,20 @@ func TestSequenceSupportsOIDC(t *testing.T) { env.Test(ctx, t, oidc.SequenceHasAudienceOfInputChannel(name, env.Namespace(), channel_impl.GVR(), channel_impl.GVK().Kind)) } +func TestApiserversourceSendEventWithJWT(t *testing.T) { + t.Parallel() + + ctx, env := global.Environment( + knative.WithKnativeNamespace(system.Namespace()), + knative.WithLoggingConfig, + knative.WithTracingConfig, + k8s.WithEventListener, + environment.Managed(t), + ) + + env.Test(ctx, t, oidc.ApiserversourceSendEventWithJWT()) +} + func TestContainerSourceSendsEventsWithOIDCSupport(t *testing.T) { t.Parallel() diff --git a/test/rekt/resources/apiserversource/apiserversource.go b/test/rekt/resources/apiserversource/apiserversource.go index 5ae71b14154..ef371984e85 100644 --- a/test/rekt/resources/apiserversource/apiserversource.go +++ b/test/rekt/resources/apiserversource/apiserversource.go @@ -100,6 +100,11 @@ func WithSink(d *duckv1.Destination) manifest.CfgFn { if uri != nil { sink["uri"] = uri.String() } + + if d.Audience != nil { + sink["audience"] = *d.Audience + } + if ref != nil { if _, set := sink["ref"]; !set { sink["ref"] = map[string]interface{}{} diff --git a/test/rekt/resources/apiserversource/apiserversource.yaml b/test/rekt/resources/apiserversource/apiserversource.yaml index b0ad6d2ffac..57da274150b 100644 --- a/test/rekt/resources/apiserversource/apiserversource.yaml +++ b/test/rekt/resources/apiserversource/apiserversource.yaml @@ -83,4 +83,7 @@ spec: {{ if .sink.uri }} uri: {{ .sink.uri }} {{ end }} + {{ if .sink.audience }} + audience: {{ .sink.audience }} + {{ end }} {{ end }} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/factory/filtered/fake/fake_filtered_factory.go b/vendor/knative.dev/pkg/client/injection/kube/informers/factory/filtered/fake/fake_filtered_factory.go new file mode 100644 index 00000000000..b5bb695d292 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/factory/filtered/fake/fake_filtered_factory.go @@ -0,0 +1,60 @@ +/* +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 fakeFilteredFactory + +import ( + context "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + informers "k8s.io/client-go/informers" + fake "knative.dev/pkg/client/injection/kube/client/fake" + filtered "knative.dev/pkg/client/injection/kube/informers/factory/filtered" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +var Get = filtered.Get + +func init() { + injection.Fake.RegisterInformerFactory(withInformerFactory) +} + +func withInformerFactory(ctx context.Context) context.Context { + c := fake.Get(ctx) + untyped := ctx.Value(filtered.LabelKey{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch labelkey from context.") + } + labelSelectors := untyped.([]string) + for _, selector := range labelSelectors { + selectorVal := selector + opts := []informers.SharedInformerOption{} + if injection.HasNamespaceScope(ctx) { + opts = append(opts, informers.WithNamespace(injection.GetNamespaceScope(ctx))) + } + opts = append(opts, informers.WithTweakListOptions(func(l *v1.ListOptions) { + l.LabelSelector = selectorVal + })) + ctx = context.WithValue(ctx, filtered.Key{Selector: selectorVal}, + informers.NewSharedInformerFactoryWithOptions(c, controller.GetResyncPeriod(ctx), opts...)) + } + return ctx +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/factory/filtered/filtered_factory.go b/vendor/knative.dev/pkg/client/injection/kube/informers/factory/filtered/filtered_factory.go new file mode 100644 index 00000000000..621a2005373 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/factory/filtered/filtered_factory.go @@ -0,0 +1,78 @@ +/* +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 filteredFactory + +import ( + context "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + informers "k8s.io/client-go/informers" + client "knative.dev/pkg/client/injection/kube/client" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterInformerFactory(withInformerFactory) +} + +// Key is used as the key for associating information with a context.Context. +type Key struct { + Selector string +} + +type LabelKey struct{} + +func WithSelectors(ctx context.Context, selector ...string) context.Context { + return context.WithValue(ctx, LabelKey{}, selector) +} + +func withInformerFactory(ctx context.Context) context.Context { + c := client.Get(ctx) + untyped := ctx.Value(LabelKey{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch labelkey from context.") + } + labelSelectors := untyped.([]string) + for _, selector := range labelSelectors { + selectorVal := selector + opts := []informers.SharedInformerOption{} + if injection.HasNamespaceScope(ctx) { + opts = append(opts, informers.WithNamespace(injection.GetNamespaceScope(ctx))) + } + opts = append(opts, informers.WithTweakListOptions(func(l *v1.ListOptions) { + l.LabelSelector = selectorVal + })) + ctx = context.WithValue(ctx, Key{Selector: selectorVal}, + informers.NewSharedInformerFactoryWithOptions(c, controller.GetResyncPeriod(ctx), opts...)) + } + return ctx +} + +// Get extracts the InformerFactory from the context. +func Get(ctx context.Context, selector string) informers.SharedInformerFactory { + untyped := ctx.Value(Key{Selector: selector}) + if untyped == nil { + logging.FromContext(ctx).Panicf( + "Unable to fetch k8s.io/client-go/informers.SharedInformerFactory with selector %s from context.", selector) + } + return untyped.(informers.SharedInformerFactory) +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/fake/fake.go b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/fake/fake.go new file mode 100644 index 00000000000..e93f76659e2 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/fake/fake.go @@ -0,0 +1,40 @@ +/* +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 fake + +import ( + context "context" + + fake "knative.dev/pkg/client/injection/kube/informers/factory/fake" + role "knative.dev/pkg/client/injection/kube/informers/rbac/v1/role" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" +) + +var Get = role.Get + +func init() { + injection.Fake.RegisterInformer(withInformer) +} + +func withInformer(ctx context.Context) (context.Context, controller.Informer) { + f := fake.Get(ctx) + inf := f.Rbac().V1().Roles() + return context.WithValue(ctx, role.Key{}, inf), inf.Informer() +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered/fake/fake.go b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered/fake/fake.go new file mode 100644 index 00000000000..0223a6b3603 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered/fake/fake.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 fake + +import ( + context "context" + + factoryfiltered "knative.dev/pkg/client/injection/kube/informers/factory/filtered" + filtered "knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +var Get = filtered.Get + +func init() { + injection.Fake.RegisterFilteredInformers(withInformer) +} + +func withInformer(ctx context.Context) (context.Context, []controller.Informer) { + untyped := ctx.Value(factoryfiltered.LabelKey{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch labelkey from context.") + } + labelSelectors := untyped.([]string) + infs := []controller.Informer{} + for _, selector := range labelSelectors { + f := factoryfiltered.Get(ctx, selector) + inf := f.Rbac().V1().Roles() + ctx = context.WithValue(ctx, filtered.Key{Selector: selector}, inf) + infs = append(infs, inf.Informer()) + } + return ctx, infs +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered/role.go b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered/role.go new file mode 100644 index 00000000000..701bb09625f --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered/role.go @@ -0,0 +1,65 @@ +/* +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 filtered + +import ( + context "context" + + v1 "k8s.io/client-go/informers/rbac/v1" + filtered "knative.dev/pkg/client/injection/kube/informers/factory/filtered" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterFilteredInformers(withInformer) +} + +// Key is used for associating the Informer inside the context.Context. +type Key struct { + Selector string +} + +func withInformer(ctx context.Context) (context.Context, []controller.Informer) { + untyped := ctx.Value(filtered.LabelKey{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch labelkey from context.") + } + labelSelectors := untyped.([]string) + infs := []controller.Informer{} + for _, selector := range labelSelectors { + f := filtered.Get(ctx, selector) + inf := f.Rbac().V1().Roles() + ctx = context.WithValue(ctx, Key{Selector: selector}, inf) + infs = append(infs, inf.Informer()) + } + return ctx, infs +} + +// Get extracts the typed informer from the context. +func Get(ctx context.Context, selector string) v1.RoleInformer { + untyped := ctx.Value(Key{Selector: selector}) + if untyped == nil { + logging.FromContext(ctx).Panicf( + "Unable to fetch k8s.io/client-go/informers/rbac/v1.RoleInformer with selector %s from context.", selector) + } + return untyped.(v1.RoleInformer) +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/role.go b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/role.go new file mode 100644 index 00000000000..14651f3f4fc --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/role.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 role + +import ( + context "context" + + v1 "k8s.io/client-go/informers/rbac/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.Rbac().V1().Roles() + return context.WithValue(ctx, Key{}, inf), inf.Informer() +} + +// Get extracts the typed informer from the context. +func Get(ctx context.Context) v1.RoleInformer { + untyped := ctx.Value(Key{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch k8s.io/client-go/informers/rbac/v1.RoleInformer from context.") + } + return untyped.(v1.RoleInformer) +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered/fake/fake.go b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered/fake/fake.go new file mode 100644 index 00000000000..d736866dda9 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered/fake/fake.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 fake + +import ( + context "context" + + factoryfiltered "knative.dev/pkg/client/injection/kube/informers/factory/filtered" + filtered "knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +var Get = filtered.Get + +func init() { + injection.Fake.RegisterFilteredInformers(withInformer) +} + +func withInformer(ctx context.Context) (context.Context, []controller.Informer) { + untyped := ctx.Value(factoryfiltered.LabelKey{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch labelkey from context.") + } + labelSelectors := untyped.([]string) + infs := []controller.Informer{} + for _, selector := range labelSelectors { + f := factoryfiltered.Get(ctx, selector) + inf := f.Rbac().V1().RoleBindings() + ctx = context.WithValue(ctx, filtered.Key{Selector: selector}, inf) + infs = append(infs, inf.Informer()) + } + return ctx, infs +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered/rolebinding.go b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered/rolebinding.go new file mode 100644 index 00000000000..79a6b880f46 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered/rolebinding.go @@ -0,0 +1,65 @@ +/* +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 filtered + +import ( + context "context" + + v1 "k8s.io/client-go/informers/rbac/v1" + filtered "knative.dev/pkg/client/injection/kube/informers/factory/filtered" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterFilteredInformers(withInformer) +} + +// Key is used for associating the Informer inside the context.Context. +type Key struct { + Selector string +} + +func withInformer(ctx context.Context) (context.Context, []controller.Informer) { + untyped := ctx.Value(filtered.LabelKey{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch labelkey from context.") + } + labelSelectors := untyped.([]string) + infs := []controller.Informer{} + for _, selector := range labelSelectors { + f := filtered.Get(ctx, selector) + inf := f.Rbac().V1().RoleBindings() + ctx = context.WithValue(ctx, Key{Selector: selector}, inf) + infs = append(infs, inf.Informer()) + } + return ctx, infs +} + +// Get extracts the typed informer from the context. +func Get(ctx context.Context, selector string) v1.RoleBindingInformer { + untyped := ctx.Value(Key{Selector: selector}) + if untyped == nil { + logging.FromContext(ctx).Panicf( + "Unable to fetch k8s.io/client-go/informers/rbac/v1.RoleBindingInformer with selector %s from context.", selector) + } + return untyped.(v1.RoleBindingInformer) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e40b4491487..c8b92c979cd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1263,8 +1263,16 @@ knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount/fake knative.dev/pkg/client/injection/kube/informers/factory knative.dev/pkg/client/injection/kube/informers/factory/fake +knative.dev/pkg/client/injection/kube/informers/factory/filtered +knative.dev/pkg/client/injection/kube/informers/factory/filtered/fake +knative.dev/pkg/client/injection/kube/informers/rbac/v1/role +knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/fake +knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered +knative.dev/pkg/client/injection/kube/informers/rbac/v1/role/filtered/fake knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/fake +knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered +knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding/filtered/fake knative.dev/pkg/client/injection/kube/reconciler/core/v1/namespace knative.dev/pkg/codegen/cmd/injection-gen knative.dev/pkg/codegen/cmd/injection-gen/args