-
Notifications
You must be signed in to change notification settings - Fork 157
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
use library-go config observer for access token inactivity timeout
- Loading branch information
Showing
4 changed files
with
320 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters