Skip to content

Commit

Permalink
use library-go config observer for access token inactivity timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
vareti committed Jul 31, 2020
1 parent 24ac500 commit 9592a34
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"k8s.io/client-go/tools/cache"

configinformers "github.com/openshift/client-go/config/informers/externalversions"
"github.com/openshift/library-go/pkg/controller/factory"
"github.com/openshift/library-go/pkg/operator/configobserver"
libgoapiserver "github.com/openshift/library-go/pkg/operator/configobserver/apiserver"
"github.com/openshift/library-go/pkg/operator/configobserver/cloudprovider"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
configobserveroauth "github.com/openshift/library-go/pkg/operator/configobserver/oauth"
"github.com/openshift/library-go/pkg/operator/configobserver/proxy"
encryption "github.com/openshift/library-go/pkg/operator/encryption/observer"
"github.com/openshift/library-go/pkg/operator/events"
Expand All @@ -23,7 +25,6 @@ import (
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/network"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/scheduler"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/operatorclient"
"github.com/openshift/library-go/pkg/controller/factory"
)

var FeatureBlacklist sets.String
Expand Down Expand Up @@ -62,6 +63,7 @@ func NewConfigObserver(
configInformer.Config().V1().Images().Informer(),
configInformer.Config().V1().Infrastructures().Informer(),
configInformer.Config().V1().Authentications().Informer(),
configInformer.Config().V1().OAuths().Informer(),
configInformer.Config().V1().APIServers().Informer(),
configInformer.Config().V1().Networks().Informer(),
configInformer.Config().V1().Proxies().Informer(),
Expand All @@ -82,6 +84,7 @@ func NewConfigObserver(
ImageConfigLister: configInformer.Config().V1().Images().Lister(),
InfrastructureLister_: configInformer.Config().V1().Infrastructures().Lister(),
NetworkLister: configInformer.Config().V1().Networks().Lister(),
OAuthLister_: configInformer.Config().V1().OAuths().Lister(),
ProxyLister_: configInformer.Config().V1().Proxies().Lister(),
SchedulerLister: configInformer.Config().V1().Schedulers().Lister(),

Expand All @@ -104,6 +107,7 @@ func NewConfigObserver(
configInformer.Config().V1().Images().Informer().HasSynced,
configInformer.Config().V1().Infrastructures().Informer().HasSynced,
configInformer.Config().V1().Networks().Informer().HasSynced,
configInformer.Config().V1().OAuths().Informer().HasSynced,
configInformer.Config().V1().Proxies().Informer().HasSynced,
configInformer.Config().V1().Schedulers().Informer().HasSynced,
),
Expand Down Expand Up @@ -139,6 +143,7 @@ func NewConfigObserver(
network.ObserveServicesSubnet,
network.ObserveExternalIPPolicy,
network.ObserveServicesNodePortRange,
configobserveroauth.ObserveAccessTokenInactivityTimeout,
proxy.NewProxyObserveFunc([]string{"targetconfigcontroller", "proxy"}),
images.ObserveInternalRegistryHostname,
images.ObserveExternalRegistryHostnames,
Expand Down
5 changes: 5 additions & 0 deletions pkg/operator/configobservation/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Listers struct {
InfrastructureLister_ configlistersv1.InfrastructureLister
ImageConfigLister configlistersv1.ImageLister
NetworkLister configlistersv1.NetworkLister
OAuthLister_ configlistersv1.OAuthLister
ProxyLister_ configlistersv1.ProxyLister
SchedulerLister configlistersv1.SchedulerLister

Expand Down Expand Up @@ -46,6 +47,10 @@ func (l Listers) ResourceSyncer() resourcesynccontroller.ResourceSyncer {
return l.ResourceSync
}

func (l Listers) OAuthLister() configlistersv1.OAuthLister {
return l.OAuthLister_
}

func (l Listers) SecretLister() corelistersv1.SecretLister {
return l.SecretLister_
}
Expand Down
283 changes: 283 additions & 0 deletions test/e2e/token_inactivity_timeout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
package e2e

import (
"bytes"
"context"
"crypto/tls"
"net"
"net/http"
"strings"
"testing"
"time"

oauthapi "github.com/openshift/api/oauth/v1"
userapi "github.com/openshift/api/user/v1"
configclient "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
oauthclient "github.com/openshift/client-go/oauth/clientset/versioned/typed/oauth/v1"
userclient "github.com/openshift/client-go/user/clientset/versioned/typed/user/v1"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/wait"

test "github.com/openshift/cluster-kube-apiserver-operator/test/library"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

const (
defaultAccessTokenMaxAgeSeconds = 86400
)

func TestTokenInactivityTimeout(t *testing.T) {
kubeConfig, err := test.NewClientConfigForTest()
require.NoError(t, err)

userClient := userclient.NewForConfigOrDie(kubeConfig)
oauthClientClient := oauthclient.NewForConfigOrDie(kubeConfig)
configClient := configclient.NewForConfigOrDie(kubeConfig)

transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}

testTokenValidity := func(req *http.Request, statusCode int, bearerToken, errMsg string) {
req.Header.Set("Authorization", "Bearer "+bearerToken)
resp, err := transport.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
if resp.StatusCode != statusCode {
t.Fatalf("%s. Received status %q", errMsg, resp.Status)
}
}

// This checks that
// 1. Token with timeout works immediately after they are created
// 2. Token with timeout works anytime before it times out
// 3. Token with timeout does not work after it times out
// 4. Token without timeout works at anytime
testTokenTimeoutScenarios := func(t *testing.T, tokenWithTimeout, tokenWithoutTimeout *oauthapi.OAuthAccessToken, timeout time.Duration) {
req, err := http.NewRequest(http.MethodGet, kubeConfig.Host+"/apis/user.openshift.io/v1/users/~", &bytes.Buffer{})
require.NoError(t, err)

testTokenValidity(req, http.StatusOK, tokenWithTimeout.Name, "accessing token before it timed out should work")
testTokenValidity(req, http.StatusOK, tokenWithoutTimeout.Name, "token with out timeout should work")

time.Sleep(120 * time.Second)

testTokenValidity(req, http.StatusOK, tokenWithTimeout.Name, "accessing token before it timed out should work")
testTokenValidity(req, http.StatusOK, tokenWithoutTimeout.Name, "token with out timeout should work")

time.Sleep(timeout + 10*time.Second)

testTokenValidity(req, http.StatusUnauthorized, tokenWithTimeout.Name, "accessing token after it timed out should not work")
testTokenValidity(req, http.StatusOK, tokenWithoutTimeout.Name, "token with out timeout should work")
}

configInactivityTimeout := int32(600)
oauthClientTimeout := int32(300)

// No OAuthClient timeout and no OAuth config timeout.
t.Run("without-inactivity-timeout", func(t *testing.T) {
checkTokenAccess(t, userClient, oauthClientClient, configInactivityTimeout, nil, testTokenTimeoutScenarios)
})

updateOAuthConfigInactivityTimeout(t, configClient, &metav1.Duration{Duration: time.Duration(configInactivityTimeout) * time.Second})
test.WaitForKubeAPIServerStartProgressing(t, configClient)
test.WaitForKubeAPIServerClusterOperatorAvailableNotProgressingNotDegraded(t, configClient)

// With only OAuth config timeout.
t.Run("with-inactivity-timeout", func(t *testing.T) {
checkTokenAccess(t, userClient, oauthClientClient, configInactivityTimeout, nil, testTokenTimeoutScenarios)
})

// With both OAuth config timeout and OAuthClient timeout.
t.Run("with-client-timeout", func(t *testing.T) {
checkTokenAccess(t, userClient, oauthClientClient, configInactivityTimeout, &oauthClientTimeout, testTokenTimeoutScenarios)
})

updateOAuthConfigInactivityTimeout(t, configClient, nil)
test.WaitForKubeAPIServerStartProgressing(t, configClient)
test.WaitForKubeAPIServerClusterOperatorAvailableNotProgressingNotDegraded(t, configClient)

// No OAuthClient timeout and no OAuth config timeout.
t.Run("unset-inactivity-timeout-client-timeout", func(t *testing.T) {
checkTokenAccess(t, userClient, oauthClientClient, configInactivityTimeout, nil, testTokenTimeoutScenarios)
})

}

func checkTokenAccess(t *testing.T,
userClient *userclient.UserV1Client,
oauthClientClient *oauthclient.OauthV1Client,
configInactivityTimeout int32, oauthClientTimeout *int32,
testAccess func(*testing.T, *oauthapi.OAuthAccessToken, *oauthapi.OAuthAccessToken, time.Duration)) {
// Create the user, identity, oauthclient and oauthaccesstoken objects needed for authentication using Bearer tokens.
subTestNameHierarchy := strings.Split(t.Name(), "/")
prefix := subTestNameHierarchy[len(subTestNameHierarchy)-1] + "-"

userName := prefix + "testuser"
idpName := "htpasswd"
oauthClientName := prefix + "oauthclient"
redirectURIs := []string{"https://localhost"}
identityName := idpName + ":" + userName

uid, cleanup := createUser(t, userClient, userName, identityName)
defer cleanup()

cleanup = createIdentity(t, userClient, userName, identityName, idpName, uid)
defer cleanup()

cleanup = createOAuthClient(t, oauthClientClient, oauthClientName, redirectURIs, oauthClientTimeout)
defer cleanup()

tokenWithTimeout := &oauthapi.OAuthAccessToken{
ObjectMeta: metav1.ObjectMeta{
Name: prefix + "token-with-timeout",
},
ClientName: oauthClientName,
ExpiresIn: defaultAccessTokenMaxAgeSeconds,
Scopes: []string{"user:full"},
RedirectURI: redirectURIs[0],
UserName: userName,
UserUID: string(uid),
AuthorizeToken: "mJOQ7Es5l9V7WYDl0bvl3E_hRjnJ21ZZxXH6YZj3yeS",
InactivityTimeoutSeconds: 60,
}

tokenWithoutTimeout := &oauthapi.OAuthAccessToken{
ObjectMeta: metav1.ObjectMeta{
Name: prefix + "token-without-timeout",
},
ClientName: oauthClientName,
ExpiresIn: defaultAccessTokenMaxAgeSeconds,
Scopes: []string{"user:full"},
RedirectURI: redirectURIs[0],
UserName: userName,
UserUID: string(uid),
AuthorizeToken: "mJOQ7Es5l9V7WYDl0bvl3E_hRjnJ21ZZxXH6YZj3yeT",
}

// create tokens with and without timeouts
for _, accessToken := range []*oauthapi.OAuthAccessToken{tokenWithTimeout, tokenWithoutTimeout} {
_, err := oauthClientClient.OAuthAccessTokens().Create(context.TODO(), accessToken, metav1.CreateOptions{})
require.NoError(t, err)
defer func(name string) {
if err := oauthClientClient.OAuthAccessTokens().Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
t.Logf("%v", err)
}
}(accessToken.Name)
}

expectedInactivityTimeout := configInactivityTimeout
if oauthClientTimeout != nil {
expectedInactivityTimeout = *oauthClientTimeout
}
testAccess(t, tokenWithTimeout, tokenWithoutTimeout, time.Duration(expectedInactivityTimeout)*time.Second)
}

func updateOAuthConfigInactivityTimeout(t *testing.T, client *configclient.ConfigV1Client, duration *metav1.Duration) {
oauthConfig, err := client.OAuths().Get(context.TODO(), "cluster", metav1.GetOptions{})
require.NoError(t, err)

oauthConfig.Spec.TokenConfig.AccessTokenInactivityTimeout = duration

err = wait.PollImmediate(300*time.Millisecond, 2*time.Second, func() (bool, error) {
_, err := client.OAuths().Update(context.TODO(), oauthConfig, metav1.UpdateOptions{})
if err != nil {
t.Logf("failed to update oauth cluster config: %v", err)
return false, nil
}
return true, nil
})
require.NoError(t, err)
}

func createUser(t *testing.T, userClient *userclient.UserV1Client, userName, identity string) (types.UID, func()) {
user := &userapi.User{
ObjectMeta: metav1.ObjectMeta{
Name: userName,
},
Identities: []string{identity},
}

err := wait.PollImmediate(300*time.Millisecond, 2*time.Second, func() (bool, error) {
var err error
user, err = userClient.Users().Create(context.TODO(), user, metav1.CreateOptions{})
if err != nil {
t.Logf("failed to create user: %v", err)
return false, nil
}
return true, nil
})
require.NoError(t, err)
return user.UID, func() {
if err := userClient.Users().Delete(context.TODO(), userName, metav1.DeleteOptions{}); err != nil {
t.Logf("%v", err)
}
}
}

func createIdentity(t *testing.T, userClient *userclient.UserV1Client, userName, identityName, idpName string, uid types.UID) func() {
identity := &userapi.Identity{
ObjectMeta: metav1.ObjectMeta{
Name: identityName,
},
ProviderName: idpName,
ProviderUserName: userName,
User: corev1.ObjectReference{
Name: userName,
UID: uid,
},
}

err := wait.PollImmediate(300*time.Second, 2*time.Second, func() (bool, error) {
_, err := userClient.Identities().Create(context.TODO(), identity, metav1.CreateOptions{})
if err != nil {
t.Logf("failed to create user identity: %v", err)
return false, nil
}
return true, nil
})
require.NoError(t, err)
return func() {
if err := userClient.Identities().Delete(context.TODO(), identity.Name, metav1.DeleteOptions{}); err != nil {
t.Logf("%v", err)
}
}
}

func createOAuthClient(t *testing.T, oauthClientClient *oauthclient.OauthV1Client, oauthClientName string, redirectURIs []string, timeout *int32) func() {
oauthClient := &oauthapi.OAuthClient{
ObjectMeta: metav1.ObjectMeta{
Name: oauthClientName,
},
Secret: "the-secret-for-oauth-client",
RedirectURIs: redirectURIs,
GrantMethod: "auto",
AccessTokenInactivityTimeoutSeconds: timeout,
}

err := wait.PollImmediate(300*time.Millisecond, 2*time.Second, func() (bool, error) {
_, err := oauthClientClient.OAuthClients().Create(context.TODO(), oauthClient, metav1.CreateOptions{})
if err != nil {
t.Logf("failed to create oauth client: %v", err)
return false, nil
}
return true, nil
})
require.NoError(t, err)
return func() {
if err := oauthClientClient.OAuthClients().Delete(context.TODO(), oauthClient.Name, metav1.DeleteOptions{}); err != nil {
t.Logf("%v", err)
}
}
}
26 changes: 26 additions & 0 deletions test/library/cluster_operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,29 @@ func WaitForKubeAPIServerClusterOperatorAvailableNotProgressingNotDegraded(t *te
t.Fatal(err)
}
}

// WaitForKubeAPIServer waits for ClusterOperator/kube-apiserver to report
// status as active, progressing, and not failing.
func WaitForKubeAPIServerStartProgressing(t *testing.T, client configclient.ConfigV1Interface) {
err := wait.Poll(WaitPollInterval, WaitPollTimeout, func() (bool, error) {
clusterOperator, err := client.ClusterOperators().Get(context.TODO(), "kube-apiserver", metav1.GetOptions{})
if errors.IsNotFound(err) {
fmt.Println("ClusterOperator/kube-apiserver does not yet exist.")
return false, nil
}
if err != nil {
fmt.Println("Unable to retrieve ClusterOperator/kube-apiserver:", err)
return false, err
}
conditions := clusterOperator.Status.Conditions
available := clusteroperatorhelpers.IsStatusConditionPresentAndEqual(conditions, configv1.OperatorAvailable, configv1.ConditionTrue)
progressing := clusteroperatorhelpers.IsStatusConditionPresentAndEqual(conditions, configv1.OperatorProgressing, configv1.ConditionTrue)
notDegraded := clusteroperatorhelpers.IsStatusConditionPresentAndEqual(conditions, configv1.OperatorDegraded, configv1.ConditionFalse)
done := available && progressing && notDegraded
fmt.Printf("ClusterOperator/kube-apiserver: Available: %v Progressing: %v Degraded: %v\n", available, progressing, !notDegraded)
return done, nil
})
if err != nil {
t.Fatal(err)
}
}

0 comments on commit 9592a34

Please sign in to comment.