diff --git a/hack/import-restrictions.json b/hack/import-restrictions.json index f2ae6e1e8d27..ae66dad58cc9 100644 --- a/hack/import-restrictions.json +++ b/hack/import-restrictions.json @@ -147,6 +147,9 @@ "ignoredSubTrees": [ "github.com/openshift/origin/pkg/oauthserver", + "github.com/openshift/origin/pkg/apiserver/authentication/oauth", + "github.com/openshift/origin/pkg/oauth/apis/oauth/validation", + "github.com/openshift/origin/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver", "github.com/openshift/origin/pkg/cmd/server/origin", "github.com/openshift/origin/pkg/cmd/server/apis/config/validation", diff --git a/pkg/apiserver/authentication/oauth/bootstrapauthenticator.go b/pkg/apiserver/authentication/oauth/bootstrapauthenticator.go new file mode 100644 index 000000000000..c03a7ae010b1 --- /dev/null +++ b/pkg/apiserver/authentication/oauth/bootstrapauthenticator.go @@ -0,0 +1,62 @@ +package oauth + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + kauthenticator "k8s.io/apiserver/pkg/authentication/authenticator" + kuser "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/client-go/kubernetes/typed/core/v1" + + userapi "github.com/openshift/api/user/v1" + oauthclient "github.com/openshift/client-go/oauth/clientset/versioned/typed/oauth/v1" + "github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap" +) + +type bootstrapAuthenticator struct { + tokens oauthclient.OAuthAccessTokenInterface + secrets v1.SecretInterface + validator OAuthTokenValidator +} + +func NewBootstrapAuthenticator(tokens oauthclient.OAuthAccessTokenInterface, secrets v1.SecretsGetter, validators ...OAuthTokenValidator) kauthenticator.Token { + return &bootstrapAuthenticator{ + tokens: tokens, + secrets: secrets.Secrets(metav1.NamespaceSystem), + validator: OAuthTokenValidators(validators), + } +} + +func (a *bootstrapAuthenticator) AuthenticateToken(name string) (kuser.Info, bool, error) { + token, err := a.tokens.Get(name, metav1.GetOptions{}) + if err != nil { + return nil, false, errLookup // mask the error so we do not leak token data in logs + } + + if token.UserName != bootstrap.BootstrapUser { + return nil, false, nil + } + + _, uid, ok, err := bootstrap.HashAndUID(a.secrets) + if err != nil || !ok { + return nil, ok, err + } + + // this allows us to reuse existing validators + // since the uid is based on the secret, if the secret changes, all + // tokens issued for the bootstrap user before that change stop working + fakeUser := &userapi.User{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(uid), + }, + } + + if err := a.validator.Validate(token, fakeUser); err != nil { + return nil, false, err + } + + // we explicitly do not set UID as we do not want to leak any derivative of the password + return &kuser.DefaultInfo{ + Name: bootstrap.BootstrapUser, + Groups: []string{kuser.SystemPrivilegedGroup}, // authorized to do everything + }, true, nil +} diff --git a/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver/patch_authenticator.go b/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver/patch_authenticator.go index 4cd7a643fcb4..a1b71e54676b 100644 --- a/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver/patch_authenticator.go +++ b/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver/patch_authenticator.go @@ -18,6 +18,7 @@ import ( genericapiserver "k8s.io/apiserver/pkg/server" webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" kclientsetexternal "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" "k8s.io/client-go/util/cert" sacontroller "k8s.io/kubernetes/pkg/controller/serviceaccount" @@ -79,10 +80,22 @@ func NewAuthenticator( userClient.User().Users(), apiClientCAs, usercache.NewGroupCache(groupInformer), + kubeExternalClient.CoreV1(), ) } -func newAuthenticator(serviceAccountPublicKeyFiles []string, oauthConfig *osinv1.OAuthConfig, authConfig kubecontrolplanev1.MasterAuthConfig, accessTokenGetter oauthclient.OAuthAccessTokenInterface, oauthClientLister oauthclientlister.OAuthClientLister, tokenGetter serviceaccount.ServiceAccountTokenGetter, userGetter usertypedclient.UserInterface, apiClientCAs *x509.CertPool, groupMapper oauth.UserToGroupMapper) (authenticator.Request, map[string]genericapiserver.PostStartHookFunc, error) { +func newAuthenticator( + serviceAccountPublicKeyFiles []string, + oauthConfig *osinv1.OAuthConfig, + authConfig kubecontrolplanev1.MasterAuthConfig, + accessTokenGetter oauthclient.OAuthAccessTokenInterface, + oauthClientLister oauthclientlister.OAuthClientLister, + tokenGetter serviceaccount.ServiceAccountTokenGetter, + userGetter usertypedclient.UserInterface, + apiClientCAs *x509.CertPool, + groupMapper oauth.UserToGroupMapper, + secretsGetter v1.SecretsGetter, +) (authenticator.Request, map[string]genericapiserver.PostStartHookFunc, error) { postStartHooks := map[string]genericapiserver.PostStartHookFunc{} authenticators := []authenticator.Request{} tokenAuthenticators := []authenticator.Token{} @@ -121,6 +134,12 @@ func newAuthenticator(serviceAccountPublicKeyFiles []string, oauthConfig *osinv1 tokenAuthenticators = append(tokenAuthenticators, // if you have an OAuth bearer token, you're a human (usually) group.NewTokenGroupAdder(oauthTokenAuthenticator, []string{bootstrappolicy.AuthenticatedOAuthGroup})) + + if oauthConfig.SessionConfig != nil { + tokenAuthenticators = append(tokenAuthenticators, + // bootstrap oauth user that can do anything, backed by a secret + oauth.NewBootstrapAuthenticator(accessTokenGetter, secretsGetter, validators...)) + } } for _, wta := range authConfig.WebhookTokenAuthenticators { diff --git a/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver/patch_oauthserver.go b/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver/patch_oauthserver.go index 802df3423435..e98e1949abdd 100644 --- a/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver/patch_oauthserver.go +++ b/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver/patch_oauthserver.go @@ -4,10 +4,8 @@ import ( "net/http" osinv1 "github.com/openshift/api/osin/v1" - routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1" "github.com/openshift/origin/pkg/oauthserver/oauthserver" genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/client-go/kubernetes" ) // TODO this is taking a very large config for a small piece of it. The information must be broken up at some point so that @@ -23,18 +21,6 @@ func NewOAuthServerConfigFromMasterConfig(genericConfig *genericapiserver.Config oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker - routeClient, err := routeclient.NewForConfig(genericConfig.LoopbackClientConfig) - if err != nil { - return nil, err - } - kubeClient, err := kubernetes.NewForConfig(genericConfig.LoopbackClientConfig) - if err != nil { - return nil, err - } - - oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient - oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient - // Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL} diff --git a/pkg/cmd/openshift-osinserver/server.go b/pkg/cmd/openshift-osinserver/server.go index 9b368f54b347..08fc777d7377 100644 --- a/pkg/cmd/openshift-osinserver/server.go +++ b/pkg/cmd/openshift-osinserver/server.go @@ -1,10 +1,8 @@ package openshift_osinserver import ( - "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1" configapi "github.com/openshift/origin/pkg/cmd/server/apis/config" "github.com/openshift/origin/pkg/oauthserver/oauthserver" genericapiserver "k8s.io/apiserver/pkg/server" @@ -25,18 +23,6 @@ func RunOpenShiftOsinServer(oauthConfig configapi.OAuthConfig, kubeClientConfig //oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend //oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker - routeClient, err := routeclient.NewForConfig(kubeClientConfig) - if err != nil { - return err - } - kubeClient, err := kubernetes.NewForConfig(kubeClientConfig) - if err != nil { - return err - } - - oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient - oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient - // Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL} diff --git a/pkg/cmd/server/apis/config/bootstrapidp.go b/pkg/cmd/server/apis/config/bootstrapidp.go new file mode 100644 index 000000000000..357e0ce40ada --- /dev/null +++ b/pkg/cmd/server/apis/config/bootstrapidp.go @@ -0,0 +1,11 @@ +package config + +import "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BootstrapIdentityProvider serves as a marker for an "IDP" that is backed by osin +// this allows us to reuse most of the logic from existing identity providers +type BootstrapIdentityProvider struct { + v1.TypeMeta +} diff --git a/pkg/cmd/server/apis/config/helpers.go b/pkg/cmd/server/apis/config/helpers.go index b00302307801..439a1f5c6b48 100644 --- a/pkg/cmd/server/apis/config/helpers.go +++ b/pkg/cmd/server/apis/config/helpers.go @@ -344,7 +344,11 @@ func IsPasswordAuthenticator(provider IdentityProvider) bool { *DenyAllPasswordIdentityProvider, *HTPasswdPasswordIdentityProvider, *LDAPPasswordIdentityProvider, - *KeystonePasswordIdentityProvider: + *KeystonePasswordIdentityProvider, + // we explicitly only include the bootstrap type in this function + // but not IsIdentityProviderType as this is not a real IDP + // it is an implementation detail that is not surfaced to users + *BootstrapIdentityProvider: return true } diff --git a/pkg/cmd/server/apis/config/zz_generated.deepcopy.go b/pkg/cmd/server/apis/config/zz_generated.deepcopy.go index 22b485823d0b..7c7276ccde86 100644 --- a/pkg/cmd/server/apis/config/zz_generated.deepcopy.go +++ b/pkg/cmd/server/apis/config/zz_generated.deepcopy.go @@ -233,6 +233,31 @@ func (in *BasicAuthPasswordIdentityProvider) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BootstrapIdentityProvider) DeepCopyInto(out *BootstrapIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapIdentityProvider. +func (in *BootstrapIdentityProvider) DeepCopy() *BootstrapIdentityProvider { + if in == nil { + return nil + } + out := new(BootstrapIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BootstrapIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BuildControllerConfig) DeepCopyInto(out *BuildControllerConfig) { *out = *in diff --git a/pkg/cmd/server/origin/legacyconfigprocessing/patch_oauthserver.go b/pkg/cmd/server/origin/legacyconfigprocessing/patch_oauthserver.go index d0e2ac52ce54..e9278202643e 100644 --- a/pkg/cmd/server/origin/legacyconfigprocessing/patch_oauthserver.go +++ b/pkg/cmd/server/origin/legacyconfigprocessing/patch_oauthserver.go @@ -5,9 +5,7 @@ import ( "github.com/openshift/origin/pkg/oauthserver/oauthserver" genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/client-go/kubernetes" - routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1" configapi "github.com/openshift/origin/pkg/cmd/server/apis/config" ) @@ -24,18 +22,6 @@ func NewOAuthServerConfigFromMasterConfig(genericConfig *genericapiserver.Config oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker - routeClient, err := routeclient.NewForConfig(genericConfig.LoopbackClientConfig) - if err != nil { - return nil, err - } - kubeClient, err := kubernetes.NewForConfig(genericConfig.LoopbackClientConfig) - if err != nil { - return nil, err - } - - oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient - oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient - // Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL} diff --git a/pkg/oauth/apis/oauth/validation/validation.go b/pkg/oauth/apis/oauth/validation/validation.go index 3472b2104581..3b596f245034 100644 --- a/pkg/oauth/apis/oauth/validation/validation.go +++ b/pkg/oauth/apis/oauth/validation/validation.go @@ -13,6 +13,7 @@ import ( authorizerscopes "github.com/openshift/origin/pkg/authorization/authorizer/scope" oauthapi "github.com/openshift/origin/pkg/oauth/apis/oauth" + "github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap" uservalidation "github.com/openshift/origin/pkg/user/apis/user/validation" ) @@ -312,7 +313,13 @@ func ValidateClientNameField(value string, fldPath *field.Path) field.ErrorList func ValidateUserNameField(value string, fldPath *field.Path) field.ErrorList { if len(value) == 0 { return field.ErrorList{field.Required(fldPath, "")} - } else if reasons := uservalidation.ValidateUserName(value, false); len(reasons) != 0 { + } + // we explicitly allow the bootstrap user in the username field + // note that we still do not allow the user API objects to have such a name + if value == bootstrap.BootstrapUser { + return field.ErrorList{} + } + if reasons := uservalidation.ValidateUserName(value, false); len(reasons) != 0 { return field.ErrorList{field.Invalid(fldPath, value, strings.Join(reasons, ", "))} } return field.ErrorList{} diff --git a/pkg/oauth/apiserver/registry/oauthclientauthorization/strategy.go b/pkg/oauth/apiserver/registry/oauthclientauthorization/strategy.go index 24929ee4152e..3ea298b2f0bf 100644 --- a/pkg/oauth/apiserver/registry/oauthclientauthorization/strategy.go +++ b/pkg/oauth/apiserver/registry/oauthclientauthorization/strategy.go @@ -36,6 +36,7 @@ func (strategy) DefaultGarbageCollectionPolicy(ctx context.Context) rest.Garbage func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { auth := obj.(*oauthapi.OAuthClientAuthorization) + // this is not as easy to break apart in the face of the bootstrap user auth.Name = fmt.Sprintf("%s:%s", auth.UserName, auth.ClientName) } @@ -50,6 +51,7 @@ func (strategy) GenerateName(base string) string { func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { auth := obj.(*oauthapi.OAuthClientAuthorization) + // this is not as easy to break apart in the face of the bootstrap user auth.Name = fmt.Sprintf("%s:%s", auth.UserName, auth.ClientName) } diff --git a/pkg/oauthserver/authenticator/password/bootstrap/bootstrap.go b/pkg/oauthserver/authenticator/password/bootstrap/bootstrap.go new file mode 100644 index 000000000000..20a8be11cce1 --- /dev/null +++ b/pkg/oauthserver/authenticator/password/bootstrap/bootstrap.go @@ -0,0 +1,84 @@ +package bootstrap + +import ( + "crypto/sha512" + "encoding/base64" + + "golang.org/x/crypto/bcrypt" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/client-go/kubernetes/typed/core/v1" +) + +const ( + // BootstrapUser is the magic bootstrap OAuth user that can perform any action + BootstrapUser = "kube:admin" + // support basic auth which does not allow : in username + bootstrapUserBasicAuth = "kubeadmin" +) + +func New(secrets v1.SecretsGetter) authenticator.Password { + return &bootstrapPassword{ + secrets: secrets.Secrets(metav1.NamespaceSystem), + names: sets.NewString(BootstrapUser, bootstrapUserBasicAuth), + } +} + +type bootstrapPassword struct { + secrets v1.SecretInterface + names sets.String +} + +func (b *bootstrapPassword) AuthenticatePassword(username, password string) (user.Info, bool, error) { + if !b.names.Has(username) { + return nil, false, nil + } + + hashedPassword, uid, ok, err := HashAndUID(b.secrets) + if err != nil || !ok { + return nil, ok, err + } + + if err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + return nil, false, nil + } + return nil, false, err + } + + // do not set other fields, see identitymapper.userToInfo func + return &user.DefaultInfo{ + Name: BootstrapUser, + UID: uid, // uid ties this authentication to the current state of the secret + }, true, nil +} + +func HashAndUID(secrets v1.SecretInterface) ([]byte, string, bool, error) { + secret, err := secrets.Get(bootstrapUserBasicAuth, metav1.GetOptions{}) + if errors.IsNotFound(err) { + return nil, "", false, nil + } + if err != nil { + return nil, "", false, err + } + + hashedPassword := secret.Data[bootstrapUserBasicAuth] + + // make sure the value is a valid bcrypt hash + if _, err := bcrypt.Cost(hashedPassword); err != nil { + return nil, "", false, err + } + + exactSecret := string(secret.UID) + secret.ResourceVersion + both := append([]byte(exactSecret), hashedPassword...) + + // use a hash to avoid leaking any derivative of the password + // this makes it easy for us to tell if the secret changed + uidBytes := sha512.Sum512(both) + + return hashedPassword, base64.RawURLEncoding.EncodeToString(uidBytes[:]), true, nil +} diff --git a/pkg/oauthserver/oauth/handlers/default_auth_handler.go b/pkg/oauthserver/oauth/handlers/default_auth_handler.go index 7d269035255a..b68ece64ab1e 100644 --- a/pkg/oauthserver/oauth/handlers/default_auth_handler.go +++ b/pkg/oauthserver/oauth/handlers/default_auth_handler.go @@ -33,7 +33,7 @@ func NewUnionAuthenticationHandler(passedChallengers map[string]AuthenticationCh redirectors = new(AuthenticationRedirectors) } - return &unionAuthenticationHandler{challengers, redirectors, errorHandler, selectionHandler} + return &unionAuthenticationHandler{challengers: challengers, redirectors: redirectors, errorHandler: errorHandler, selectionHandler: selectionHandler} } const ( diff --git a/pkg/oauthserver/oauth/registry/grantchecker.go b/pkg/oauthserver/oauth/registry/grantchecker.go index b86ea39fc716..6a1819ee5f7d 100644 --- a/pkg/oauthserver/oauth/registry/grantchecker.go +++ b/pkg/oauthserver/oauth/registry/grantchecker.go @@ -34,6 +34,7 @@ func (c *ClientAuthorizationGrantChecker) HasAuthorizedClient(user kuser.Info, g return false, errEmptyUID } + // this is not as easy to break apart in the face of the bootstrap user id := user.GetName() + ":" + grant.Client.GetId() var authorization *oauth.OAuthClientAuthorization diff --git a/pkg/oauthserver/oauthserver/auth.go b/pkg/oauthserver/oauthserver/auth.go index 8306eb7e63e1..a3a99443ead4 100644 --- a/pkg/oauthserver/oauthserver/auth.go +++ b/pkg/oauthserver/oauthserver/auth.go @@ -35,6 +35,7 @@ import ( "github.com/openshift/origin/pkg/oauthserver/authenticator/challenger/placeholderchallenger" "github.com/openshift/origin/pkg/oauthserver/authenticator/password/allowanypassword" "github.com/openshift/origin/pkg/oauthserver/authenticator/password/basicauthpassword" + "github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap" "github.com/openshift/origin/pkg/oauthserver/authenticator/password/denypassword" "github.com/openshift/origin/pkg/oauthserver/authenticator/password/htpasswd" "github.com/openshift/origin/pkg/oauthserver/authenticator/password/keystonepassword" @@ -290,7 +291,13 @@ func (c *OAuthServerConfig) getAuthenticationFinalizer() osinserver.AuthorizeHan if c.ExtraOAuthConfig.SessionAuth != nil { // The session needs to know the authorize flow is done so it can invalidate the session return osinserver.AuthorizeHandlerFunc(func(ar *osin.AuthorizeRequest, resp *osin.Response, w http.ResponseWriter) (bool, error) { - if err := c.ExtraOAuthConfig.SessionAuth.InvalidateAuthentication(w, ar.HttpRequest); err != nil { + user, ok := ar.UserData.(kuser.Info) + if !ok { + glog.Errorf("the provided user data is not a user.Info object: %#v", user) + user = &kuser.DefaultInfo{} // set non-nil so we always try to invalidate + } + + if err := c.ExtraOAuthConfig.SessionAuth.InvalidateAuthentication(w, user); err != nil { glog.V(5).Infof("error invaliding cookie session: %v", err) } // do not fail the OAuth flow if we cannot invalidate the cookie @@ -446,6 +453,11 @@ func (c *OAuthServerConfig) getAuthenticationHandler(mux oauthserver.Mux, errorH selectProvider := selectprovider.NewSelectProvider(selectProviderRenderer, c.ExtraOAuthConfig.Options.AlwaysShowProviderSelection) + // the bootstrap user IDP is always set as the first one when sessions are enabled + if c.ExtraOAuthConfig.Options.SessionConfig != nil { + selectProvider = selectprovider.NewBootstrapSelectProvider(selectProvider, c.ExtraOAuthConfig.KubeClient.CoreV1()) + } + authHandler := handlers.NewUnionAuthenticationHandler(challengers, redirectors, errorHandler, selectProvider) return authHandler, nil } @@ -595,6 +607,9 @@ func (c *OAuthServerConfig) getPasswordAuthenticator(identityProvider configapi. return keystonepassword.New(identityProvider.Name, connectionInfo.URL, transport, provider.DomainName, identityMapper, provider.UseKeystoneIdentity), nil + case *configapi.BootstrapIdentityProvider: + return bootstrap.New(c.ExtraOAuthConfig.KubeClient.CoreV1()), nil + default: return nil, fmt.Errorf("No password auth found that matches %v. The OAuth server cannot start!", identityProvider) } diff --git a/pkg/oauthserver/oauthserver/oauth_apiserver.go b/pkg/oauthserver/oauthserver/oauth_apiserver.go index 1ca51892b27a..57e8d3d4326b 100644 --- a/pkg/oauthserver/oauthserver/oauth_apiserver.go +++ b/pkg/oauthserver/oauthserver/oauth_apiserver.go @@ -1,6 +1,7 @@ package oauthserver import ( + "bytes" "crypto/sha256" "fmt" "net/http" @@ -21,8 +22,6 @@ import ( corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" - "bytes" - legacyconfigv1 "github.com/openshift/api/legacyconfig/v1" oauthv1 "github.com/openshift/api/oauth/v1" osinv1 "github.com/openshift/api/osin/v1" @@ -33,8 +32,10 @@ import ( "github.com/openshift/origin/pkg/cmd/server/apis/config/latest" "github.com/openshift/origin/pkg/configconversion" "github.com/openshift/origin/pkg/oauth/urls" + "github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap" "github.com/openshift/origin/pkg/oauthserver/server/crypto" "github.com/openshift/origin/pkg/oauthserver/server/session" + "github.com/openshift/origin/pkg/oauthserver/userregistry/identitymapper" ) var ( @@ -64,17 +65,6 @@ func NewOAuthServerConfigFromInternal(oauthConfig configapi.OAuthConfig, userCli genericConfig := genericapiserver.NewRecommendedConfig(codecs) genericConfig.LoopbackClientConfig = userClientConfig - var sessionAuth *session.Authenticator - if oauthConfig.SessionConfig != nil { - // TODO we really need to enforce HTTPS always - secure := isHTTPS(oauthConfig.MasterPublicURL) - auth, err := buildSessionAuth(secure, oauthConfig.SessionConfig) - if err != nil { - return nil, err - } - sessionAuth = auth - } - userClient, err := userclient.NewForConfig(userClientConfig) if err != nil { return nil, err @@ -87,20 +77,58 @@ func NewOAuthServerConfigFromInternal(oauthConfig configapi.OAuthConfig, userCli if err != nil { return nil, err } + routeClient, err := routeclient.NewForConfig(userClientConfig) + if err != nil { + return nil, err + } + kubeClient, err := kclientset.NewForConfig(userClientConfig) + if err != nil { + return nil, err + } + + var sessionAuth *session.Authenticator + if oauthConfig.SessionConfig != nil { + // TODO we really need to enforce HTTPS always + secure := isHTTPS(oauthConfig.MasterPublicURL) + auth, err := buildSessionAuth(secure, oauthConfig.SessionConfig, kubeClient.CoreV1()) + if err != nil { + return nil, err + } + sessionAuth = auth + + // session capability is the only thing required to enable the bootstrap IDP + // we dynamically enable or disable its UI based on the backing secret + // this must be the first IDP to make sure that it can handle basic auth challenges first + // this mostly avoids weird cases with the allow all IDP + oauthConfig.IdentityProviders = append( + []configapi.IdentityProvider{ + { + Name: bootstrap.BootstrapUser, // will never conflict with other IDPs due to the : + UseAsChallenger: true, + UseAsLogin: true, + MappingMethod: string(identitymapper.MappingMethodClaim), // irrelevant, but needs to be valid + Provider: &configapi.BootstrapIdentityProvider{}, + }, + }, + oauthConfig.IdentityProviders..., + ) + } ret := &OAuthServerConfig{ GenericConfig: genericConfig, ExtraOAuthConfig: ExtraOAuthConfig{ Options: oauthConfig, - SessionAuth: sessionAuth, + KubeClient: kubeClient, EventsClient: eventsClient.Events(""), - IdentityClient: userClient.Identities(), + RouteClient: routeClient, UserClient: userClient.Users(), + IdentityClient: userClient.Identities(), UserIdentityMappingClient: userClient.UserIdentityMappings(), OAuthAccessTokenClient: oauthClient.OAuthAccessTokens(), OAuthAuthorizeTokenClient: oauthClient.OAuthAuthorizeTokens(), OAuthClientClient: oauthClient.OAuthClients(), OAuthClientAuthorizationClient: oauthClient.OAuthClientAuthorizations(), + SessionAuth: sessionAuth, }, } genericConfig.BuildHandlerChainFunc = ret.buildHandlerChainForOAuth @@ -108,13 +136,13 @@ func NewOAuthServerConfigFromInternal(oauthConfig configapi.OAuthConfig, userCli return ret, nil } -func buildSessionAuth(secure bool, config *configapi.SessionConfig) (*session.Authenticator, error) { +func buildSessionAuth(secure bool, config *configapi.SessionConfig, secretsGetter corev1.SecretsGetter) (*session.Authenticator, error) { secrets, err := getSessionSecrets(config.SessionSecretsFile) if err != nil { return nil, err } sessionStore := session.NewStore(config.SessionName, secure, secrets...) - return session.NewAuthenticator(sessionStore, time.Duration(config.SessionMaxAgeSeconds)*time.Second), nil + return session.NewAuthenticator(sessionStore, time.Duration(config.SessionMaxAgeSeconds)*time.Second, secretsGetter), nil } func getSessionSecrets(filename string) ([][]byte, error) { diff --git a/pkg/oauthserver/server/selectprovider/bootstrapselectprovider.go b/pkg/oauthserver/server/selectprovider/bootstrapselectprovider.go new file mode 100644 index 000000000000..884720a9b5b4 --- /dev/null +++ b/pkg/oauthserver/server/selectprovider/bootstrapselectprovider.go @@ -0,0 +1,39 @@ +package selectprovider + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/openshift/origin/pkg/oauthserver/api" + "github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap" + "github.com/openshift/origin/pkg/oauthserver/oauth/handlers" +) + +func NewBootstrapSelectProvider(delegate handlers.AuthenticationSelectionHandler, secretsGetter v1.SecretsGetter) handlers.AuthenticationSelectionHandler { + return &bootstrapSelectProvider{ + delegate: delegate, + secrets: secretsGetter.Secrets(metav1.NamespaceSystem), + } +} + +type bootstrapSelectProvider struct { + delegate handlers.AuthenticationSelectionHandler + secrets v1.SecretInterface +} + +func (b *bootstrapSelectProvider) SelectAuthentication(providers []api.ProviderInfo, w http.ResponseWriter, req *http.Request) (*api.ProviderInfo, bool, error) { + // this should never happen but let us not panic the server in case we screwed up + if len(providers) == 0 || providers[0].Name != bootstrap.BootstrapUser { + return b.delegate.SelectAuthentication(providers, w, req) + } + + _, _, ok, err := bootstrap.HashAndUID(b.secrets) + // filter out the bootstrap IDP if the secret is not functional + if err != nil || !ok { + return b.delegate.SelectAuthentication(providers[1:], w, req) + } + + return b.delegate.SelectAuthentication(providers, w, req) +} diff --git a/pkg/oauthserver/server/selectprovider/selectprovider.go b/pkg/oauthserver/server/selectprovider/selectprovider.go index b604393e18bf..56bab5df7c10 100644 --- a/pkg/oauthserver/server/selectprovider/selectprovider.go +++ b/pkg/oauthserver/server/selectprovider/selectprovider.go @@ -7,21 +7,23 @@ import ( "html/template" "net/http" - "github.com/openshift/origin/pkg/oauthserver/api" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + "github.com/openshift/origin/pkg/oauthserver/api" + "github.com/openshift/origin/pkg/oauthserver/oauth/handlers" ) type SelectProviderRenderer interface { Render(redirectors []api.ProviderInfo, w http.ResponseWriter, req *http.Request) } -type SelectProvider struct { +type selectProvider struct { render SelectProviderRenderer forceInterstitial bool } -func NewSelectProvider(render SelectProviderRenderer, forceInterstitial bool) *SelectProvider { - return &SelectProvider{ +func NewSelectProvider(render SelectProviderRenderer, forceInterstitial bool) handlers.AuthenticationSelectionHandler { + return &selectProvider{ render: render, forceInterstitial: forceInterstitial, } @@ -48,7 +50,7 @@ func NewSelectProviderRenderer(customSelectProviderTemplateFile string) (*select return r, nil } -func (s *SelectProvider) SelectAuthentication(providers []api.ProviderInfo, w http.ResponseWriter, req *http.Request) (*api.ProviderInfo, bool, error) { +func (s *selectProvider) SelectAuthentication(providers []api.ProviderInfo, w http.ResponseWriter, req *http.Request) (*api.ProviderInfo, bool, error) { if len(providers) == 0 { return nil, false, nil } diff --git a/pkg/oauthserver/server/session/authenticator.go b/pkg/oauthserver/server/session/authenticator.go index cfb6aeb4dfbc..46cddda8c0a1 100644 --- a/pkg/oauthserver/server/session/authenticator.go +++ b/pkg/oauthserver/server/session/authenticator.go @@ -4,7 +4,11 @@ import ( "net/http" "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap" ) const ( @@ -16,14 +20,16 @@ const ( ) type Authenticator struct { - store Store - maxAge time.Duration + store Store + maxAge time.Duration + secrets v1.SecretInterface } -func NewAuthenticator(store Store, maxAge time.Duration) *Authenticator { +func NewAuthenticator(store Store, maxAge time.Duration, secrets v1.SecretsGetter) *Authenticator { return &Authenticator{ - store: store, - maxAge: maxAge, + store: store, + maxAge: maxAge, + secrets: secrets.Secrets(metav1.NamespaceSystem), } } @@ -49,6 +55,19 @@ func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, return nil, false, nil } + // make sure that the password has not changed since this cookie was issued + // note that this is not really for security - it is so that we do not annoy the user + // by letting them log in successfully only to have a token that does not work + if name == bootstrap.BootstrapUser { + _, currentUID, ok, err := bootstrap.HashAndUID(a.secrets) + if err != nil || !ok { + return nil, ok, err + } + if currentUID != uid { + return nil, false, nil + } + } + return &user.DefaultInfo{ Name: name, UID: uid, @@ -56,10 +75,31 @@ func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, } func (a *Authenticator) AuthenticationSucceeded(user user.Info, state string, w http.ResponseWriter, req *http.Request) (bool, error) { - return false, a.put(w, user.GetName(), user.GetUID(), time.Now().Add(a.maxAge).Unix()) + return false, a.put(w, user.GetName(), user.GetUID(), time.Now().Add(a.getMaxAge(user)).Unix()) } -func (a *Authenticator) InvalidateAuthentication(w http.ResponseWriter, req *http.Request) error { +// TODO figure out how to extract the BootstrapUser logic from this function +// as that is the only thing that prevents us from layering the BootstrapUser +// bits as a separate unit on top of the standard session logic +func (a *Authenticator) getMaxAge(user user.Info) time.Duration { + // since osin is the IDP for this user, we increase the length + // of the session to allow for transitions between components + if user.GetName() == bootstrap.BootstrapUser { + // this means the user could stay authenticated for one hour + OAuth access token lifetime + return time.Hour + } + + return a.maxAge +} + +func (a *Authenticator) InvalidateAuthentication(w http.ResponseWriter, user user.Info) error { + // the IDP is responsible for maintaining the user's session + // since osin is the IDP for this user, we do not invalidate its session + // this is safe to do because we tie the cookie to the password hash + if user.GetName() == bootstrap.BootstrapUser { + return nil + } + // zero out all fields return a.put(w, "", "", 0) } diff --git a/test/integration/oauth_server_headers_test.go b/test/integration/oauth_server_headers_test.go index d0d577a65b46..7cba7eb10d69 100644 --- a/test/integration/oauth_server_headers_test.go +++ b/test/integration/oauth_server_headers_test.go @@ -45,16 +45,27 @@ func TestOAuthServerHeaders(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - // Hit the login URL - loginURL := *baseURL - loginURL.Path = "/login" - checkNewReqHeaders(t, transport, loginURL.String()) + for _, path := range []string{ + // hit the login URL for when there is only one IDP + // this is disabled since the OAuth server only handles this endpoint when + // there is a single IDP but that is not true for the integration tests + // "/login", - // Hit the grant URL - grantURL := *baseURL - grantURL.Path = "/oauth/authorize/approve" - checkNewReqHeaders(t, transport, grantURL.String()) + // hit the login URL for the bootstrap IDP + "/login/kube:admin", + // hit the login URL for the allow all IDP + "/login/anypassword", + + // hit the grant URL + "/oauth/authorize/approve", + } { + t.Run(path, func(t *testing.T) { + urlCopy := *baseURL + urlCopy.Path = path + checkNewReqHeaders(t, transport, urlCopy.String()) + }) + } } func checkNewReqHeaders(t *testing.T, rt http.RoundTripper, checkUrl string) {