Skip to content

Commit ec2c090

Browse files
author
Matt Rogers
committed
Add API events for SA OAuth failures
Collect errors during validation of a service account's OAuth configuration, logging them to a warning event upon a fatal error. The event is limited to 1 per minute, to reduce the amount of etcd writes resulting from incrementing the duplicate event counter. Signed-off-by: Matt Rogers <[email protected]>
1 parent 084f967 commit ec2c090

File tree

6 files changed

+165
-35
lines changed

6 files changed

+165
-35
lines changed

pkg/cmd/server/origin/oauth_apiserver_adapter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func NewOAuthServerConfigFromMasterConfig(masterConfig *MasterConfig) (*oauthapi
5858
// TODO pass a privileged client config through during construction. It is NOT a loopback client.
5959
oauthServerConfig.OpenShiftClient = masterConfig.PrivilegedLoopbackOpenShiftClient
6060
oauthServerConfig.KubeClient = masterConfig.PrivilegedLoopbackKubernetesClientsetInternal
61+
oauthServerConfig.KubeExternalClient = masterConfig.PrivilegedLoopbackKubernetesClientsetExternal
6162

6263
// Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to
6364
if !options.DisabledFeatures.Has(configapi.FeatureWebConsole) {

pkg/cmd/server/origin/storage.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"k8s.io/apimachinery/pkg/runtime/schema"
1111
apirequest "k8s.io/apiserver/pkg/endpoints/request"
1212
"k8s.io/apiserver/pkg/registry/rest"
13+
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
1314
restclient "k8s.io/client-go/rest"
1415
"k8s.io/client-go/util/flowcontrol"
1516
kapi "k8s.io/kubernetes/pkg/api"
@@ -339,7 +340,15 @@ func (c OpenshiftAPIConfig) GetRestStorage() (map[schema.GroupVersion]map[string
339340
saAccountGrantMethod = oauthapi.GrantHandlerType(c.ServiceAccountMethod)
340341
}
341342

342-
combinedOAuthClientGetter := saoauth.NewServiceAccountOAuthClientGetter(c.KubeClientInternal.Core(), c.KubeClientInternal.Core(), c.DeprecatedOpenshiftClient, clientRegistry, saAccountGrantMethod)
343+
combinedOAuthClientGetter := saoauth.NewServiceAccountOAuthClientGetter(
344+
c.KubeClientInternal.Core(),
345+
c.KubeClientInternal.Core(),
346+
corev1.New(c.KubeClientExternal.Core().RESTClient()).Events(""),
347+
c.DeprecatedOpenshiftClient,
348+
clientRegistry,
349+
saAccountGrantMethod,
350+
)
351+
343352
authorizeTokenStorage, err := authorizetokenetcd.NewREST(c.GenericConfig.RESTOptionsGetter, combinedOAuthClientGetter)
344353
if err != nil {
345354
return nil, fmt.Errorf("error building REST storage: %v", err)

pkg/oauth/apiserver/auth.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
x509request "k8s.io/apiserver/pkg/authentication/request/x509"
2525
kuser "k8s.io/apiserver/pkg/authentication/user"
2626
apirequest "k8s.io/apiserver/pkg/endpoints/request"
27+
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
2728
"k8s.io/kubernetes/pkg/client/retry"
2829

2930
"github.com/openshift/origin/pkg/auth/authenticator/challenger/passwordchallenger"
@@ -92,7 +93,15 @@ func (c *OAuthServerConfig) WithOAuth(handler http.Handler) (http.Handler, error
9293
return nil, err
9394
}
9495
clientRegistry := clientregistry.NewRegistry(clientStorage)
95-
combinedOAuthClientGetter := saoauth.NewServiceAccountOAuthClientGetter(c.KubeClient.Core(), c.KubeClient.Core(), c.OpenShiftClient, clientRegistry, oauthapi.GrantHandlerType(c.Options.GrantConfig.ServiceAccountMethod))
96+
combinedOAuthClientGetter := saoauth.NewServiceAccountOAuthClientGetter(
97+
c.KubeClient.Core(),
98+
c.KubeClient.Core(),
99+
// TODO: simplify this construction
100+
corev1.New(c.KubeExternalClient.Core().RESTClient()).Events(""),
101+
c.OpenShiftClient,
102+
clientRegistry,
103+
oauthapi.GrantHandlerType(c.Options.GrantConfig.ServiceAccountMethod),
104+
)
96105

97106
accessTokenStorage, err := accesstokenetcd.NewREST(c.RESTOptionsGetter, combinedOAuthClientGetter, c.EtcdBackends...)
98107
if err != nil {

pkg/oauth/apiserver/oauth_apiserver.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"k8s.io/apiserver/pkg/storage"
1616
"k8s.io/client-go/rest"
1717
kapi "k8s.io/kubernetes/pkg/api"
18+
kclientsetexternal "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
1819
kclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
1920

2021
"github.com/openshift/origin/pkg/auth/server/session"
@@ -36,6 +37,9 @@ type OAuthServerConfig struct {
3637
// KubeClient is kubeclient with enough permission for the auth API
3738
KubeClient kclientset.Interface
3839

40+
// KubeExternalClient is for creating user events
41+
KubeExternalClient kclientsetexternal.Interface
42+
3943
// OpenShiftClient is osclient with enough permission for the auth API
4044
OpenShiftClient osclient.Interface
4145

pkg/serviceaccounts/oauthclient/oauthclientregistry.go

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ import (
1010
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1111
"k8s.io/apimachinery/pkg/runtime"
1212
"k8s.io/apimachinery/pkg/runtime/schema"
13+
"k8s.io/apimachinery/pkg/util/sets"
1314
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
1415
apirequest "k8s.io/apiserver/pkg/endpoints/request"
16+
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
17+
clientv1 "k8s.io/client-go/pkg/api/v1"
18+
"k8s.io/client-go/tools/record"
1519
kapi "k8s.io/kubernetes/pkg/api"
1620
kcoreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
1721
"k8s.io/kubernetes/pkg/serviceaccount"
@@ -21,7 +25,6 @@ import (
2125
oauthapi "github.com/openshift/origin/pkg/oauth/apis/oauth"
2226
"github.com/openshift/origin/pkg/oauth/registry/oauthclient"
2327
routeapi "github.com/openshift/origin/pkg/route/apis/route"
24-
"k8s.io/apimachinery/pkg/util/sets"
2528
)
2629

2730
const (
@@ -47,8 +50,8 @@ var modelPrefixes = []string{
4750
// namesToObjMapperFunc is linked to a given GroupKind.
4851
// Based on the namespace and names provided, it builds a map of resource name to redirect URIs.
4952
// The redirect URIs represent the default values as specified by the resource.
50-
// These values can be overridden by user specified data.
51-
type namesToObjMapperFunc func(namespace string, names sets.String) map[string]redirectURIList
53+
// These values can be overridden by user specified data. Errors returned are informative and non-fatal.
54+
type namesToObjMapperFunc func(namespace string, names sets.String) (map[string]redirectURIList, []error)
5255

5356
var emptyGroupKind = schema.GroupKind{} // Used with static redirect URIs
5457
var routeGroupKind = routeapi.SchemeGroupVersion.WithKind(routeKind).GroupKind()
@@ -58,9 +61,10 @@ var legacyRouteGroupKind = routeapi.LegacySchemeGroupVersion.WithKind(routeKind)
5861
// var ingressGroupKind = routeapi.SchemeGroupVersion.WithKind(IngressKind).GroupKind()
5962

6063
type saOAuthClientAdapter struct {
61-
saClient kcoreclient.ServiceAccountsGetter
62-
secretClient kcoreclient.SecretsGetter
63-
routeClient osclient.RoutesNamespacer
64+
saClient kcoreclient.ServiceAccountsGetter
65+
secretClient kcoreclient.SecretsGetter
66+
eventRecorder record.EventRecorder
67+
routeClient osclient.RoutesNamespacer
6468
// TODO add ingress support
6569
//ingressClient ??
6670

@@ -186,11 +190,36 @@ func (uri *redirectURI) merge(m *model) {
186190

187191
var _ oauthclient.Getter = &saOAuthClientAdapter{}
188192

189-
func NewServiceAccountOAuthClientGetter(saClient kcoreclient.ServiceAccountsGetter, secretClient kcoreclient.SecretsGetter, routeClient osclient.RoutesNamespacer, delegate oauthclient.Getter, grantMethod oauthapi.GrantHandlerType) oauthclient.Getter {
190-
return &saOAuthClientAdapter{saClient: saClient, secretClient: secretClient, routeClient: routeClient, delegate: delegate, grantMethod: grantMethod, decoder: kapi.Codecs.UniversalDecoder()}
193+
func NewServiceAccountOAuthClientGetter(saClient kcoreclient.ServiceAccountsGetter, secretClient kcoreclient.SecretsGetter, eventClient corev1.EventInterface, routeClient osclient.RoutesNamespacer, delegate oauthclient.Getter, grantMethod oauthapi.GrantHandlerType) oauthclient.Getter {
194+
opts := record.NewDefaultEventCorrelatorOptions()
195+
// Limit the OAuth events to 1 new event per minute.
196+
opts.SpamBurst = 1
197+
opts.SpamQPS = 1. / 60.
198+
199+
eventBroadcaster := record.NewBroadcaster()
200+
eventBroadcaster.StartRecordingToSinkWithOptions(&corev1.EventSinkImpl{Interface: eventClient}, opts)
201+
recorder := eventBroadcaster.NewRecorder(kapi.Scheme, clientv1.EventSource{Component: "service-account-oauth-client-getter"})
202+
return &saOAuthClientAdapter{
203+
saClient: saClient,
204+
secretClient: secretClient,
205+
eventRecorder: recorder,
206+
routeClient: routeClient,
207+
delegate: delegate,
208+
grantMethod: grantMethod,
209+
decoder: kapi.Codecs.UniversalDecoder(),
210+
}
211+
}
212+
213+
func joinErrors(errors []error) string {
214+
msg := []string{}
215+
for _, e := range errors {
216+
msg = append(msg, e.Error())
217+
}
218+
return strings.Join(msg, ",")
191219
}
192220

193221
func (a *saOAuthClientAdapter) GetClient(ctx apirequest.Context, name string, options *metav1.GetOptions) (*oauthapi.OAuthClient, error) {
222+
var err error
194223
saNamespace, saName, err := apiserverserviceaccount.SplitUsername(name)
195224
if err != nil {
196225
return a.delegate.GetClient(ctx, name, options)
@@ -201,25 +230,44 @@ func (a *saOAuthClientAdapter) GetClient(ctx apirequest.Context, name string, op
201230
return nil, err
202231
}
203232

233+
var saErrors []error
234+
var failReason string
235+
// Create a warning event combining the collected annotation errors upon failure.
236+
defer func() {
237+
if err != nil && len(saErrors) > 0 && len(failReason) > 0 {
238+
a.eventRecorder.Eventf(sa, kapi.EventTypeWarning, failReason, "%s", joinErrors(saErrors))
239+
}
240+
}()
241+
204242
redirectURIs := []string{}
205-
if modelsMap := parseModelsMap(sa.Annotations, a.decoder); len(modelsMap) > 0 {
206-
if uris := a.extractRedirectURIs(modelsMap, saNamespace); len(uris) > 0 {
243+
modelsMap, saErrors := parseModelsMap(sa.Annotations, a.decoder)
244+
if len(modelsMap) > 0 {
245+
uris, extractErrors := a.extractRedirectURIs(modelsMap, saNamespace)
246+
if len(uris) > 0 {
207247
redirectURIs = append(redirectURIs, uris.extractValidRedirectURIStrings()...)
208248
}
249+
if len(extractErrors) > 0 {
250+
saErrors = append(saErrors, extractErrors...)
251+
}
209252
}
210253
if len(redirectURIs) == 0 {
211-
return nil, fmt.Errorf(
212-
"%v has no redirectURIs; set %v<some-value>=<redirect> or create a dynamic URI using %v<some-value>=<reference>",
254+
err = fmt.Errorf("%v has no redirectURIs; set %v<some-value>=<redirect> or create a dynamic URI using %v<some-value>=<reference>",
213255
name, OAuthRedirectModelAnnotationURIPrefix, OAuthRedirectModelAnnotationReferencePrefix,
214256
)
257+
failReason = "NoSAOAuthRedirectURIs"
258+
saErrors = append(saErrors, err)
259+
return nil, err
215260
}
216261

217262
tokens, err := a.getServiceAccountTokens(sa)
218263
if err != nil {
219264
return nil, err
220265
}
221266
if len(tokens) == 0 {
222-
return nil, fmt.Errorf("%v has no tokens", name)
267+
err = fmt.Errorf("%v has no tokens", name)
268+
failReason = "NoSAOAuthTokens"
269+
saErrors = append(saErrors, err)
270+
return nil, err
223271
}
224272

225273
saWantsChallenges, _ := strconv.ParseBool(sa.Annotations[OAuthWantChallengesAnnotationPrefix])
@@ -242,9 +290,10 @@ func (a *saOAuthClientAdapter) GetClient(ctx apirequest.Context, name string, op
242290

243291
// parseModelsMap builds a map of model name to model using a service account's annotations.
244292
// The model name is only used for building the map (it ties together the uri and reference annotations)
245-
// and serves no functional purpose other than making testing easier.
246-
func parseModelsMap(annotations map[string]string, decoder runtime.Decoder) map[string]model {
293+
// and serves no functional purpose other than making testing easier. Errors returned are informative and non-fatal.
294+
func parseModelsMap(annotations map[string]string, decoder runtime.Decoder) (map[string]model, []error) {
247295
models := map[string]model{}
296+
parseErrors := []error{}
248297
for key, value := range annotations {
249298
prefix, name, ok := parseModelPrefixName(key)
250299
if !ok {
@@ -255,16 +304,20 @@ func parseModelsMap(annotations map[string]string, decoder runtime.Decoder) map[
255304
case OAuthRedirectModelAnnotationURIPrefix:
256305
if u, err := url.Parse(value); err == nil {
257306
m.updateFromURI(u)
307+
} else {
308+
parseErrors = append(parseErrors, err)
258309
}
259310
case OAuthRedirectModelAnnotationReferencePrefix:
260311
r := &oauthapi.OAuthRedirectReference{}
261312
if err := runtime.DecodeInto(decoder, []byte(value), r); err == nil {
262313
m.updateFromReference(&r.Reference)
314+
} else {
315+
parseErrors = append(parseErrors, err)
263316
}
264317
}
265318
models[name] = m
266319
}
267-
return models
320+
return models, parseErrors
268321
}
269322

270323
// parseModelPrefixName determines if the given key is a model prefix.
@@ -279,9 +332,10 @@ func parseModelPrefixName(key string) (string, string, bool) {
279332
}
280333

281334
// extractRedirectURIs builds redirect URIs using the given models and namespace.
282-
// The returned redirect URIs may contain duplicates and invalid entries.
283-
func (a *saOAuthClientAdapter) extractRedirectURIs(modelsMap map[string]model, namespace string) redirectURIList {
335+
// The returned redirect URIs may contain duplicates and invalid entries. Errors returned are informative and non-fatal.
336+
func (a *saOAuthClientAdapter) extractRedirectURIs(modelsMap map[string]model, namespace string) (redirectURIList, []error) {
284337
var data redirectURIList
338+
routeErrors := []error{}
285339
groupKindModelListMapper := map[schema.GroupKind]modelList{} // map of GroupKind to all models belonging to it
286340
groupKindModelToURI := map[schema.GroupKind]namesToObjMapperFunc{
287341
routeGroupKind: a.redirectURIsFromRoutes,
@@ -305,27 +359,37 @@ func (a *saOAuthClientAdapter) extractRedirectURIs(modelsMap map[string]model, n
305359

306360
for gk, models := range groupKindModelListMapper {
307361
if names := models.getNames(); names.Len() > 0 {
308-
if objMapper := groupKindModelToURI[gk](namespace, names); len(objMapper) > 0 {
362+
objMapper, errs := groupKindModelToURI[gk](namespace, names)
363+
if len(objMapper) > 0 {
309364
data = append(data, models.getRedirectURIs(objMapper)...)
310365
}
366+
if len(errs) > 0 {
367+
routeErrors = append(routeErrors, errs...)
368+
}
311369
}
312370
}
313371

314-
return data
372+
return data, routeErrors
315373
}
316374

317375
// redirectURIsFromRoutes is the namesToObjMapperFunc specific to Routes.
318376
// Returns a map of route name to redirect URIs that contain the default data as specified by the route's ingresses.
319-
func (a *saOAuthClientAdapter) redirectURIsFromRoutes(namespace string, osRouteNames sets.String) map[string]redirectURIList {
377+
// Errors returned are informative and non-fatal.
378+
func (a *saOAuthClientAdapter) redirectURIsFromRoutes(namespace string, osRouteNames sets.String) (map[string]redirectURIList, []error) {
320379
var routes []routeapi.Route
380+
routeErrors := []error{}
321381
routeInterface := a.routeClient.Routes(namespace)
322382
if osRouteNames.Len() > 1 {
323383
if r, err := routeInterface.List(metav1.ListOptions{}); err == nil {
324384
routes = r.Items
385+
} else {
386+
routeErrors = append(routeErrors, err)
325387
}
326388
} else {
327389
if r, err := routeInterface.Get(osRouteNames.List()[0], metav1.GetOptions{}); err == nil {
328390
routes = append(routes, *r)
391+
} else {
392+
routeErrors = append(routeErrors, err)
329393
}
330394
}
331395
routeMap := map[string]redirectURIList{}
@@ -334,7 +398,7 @@ func (a *saOAuthClientAdapter) redirectURIsFromRoutes(namespace string, osRouteN
334398
routeMap[route.Name] = redirectURIsFromRoute(&route)
335399
}
336400
}
337-
return routeMap
401+
return routeMap, routeErrors
338402
}
339403

340404
// redirectURIsFromRoute returns a list of redirect URIs that contain the default data as specified by the given route's ingresses.

0 commit comments

Comments
 (0)