feat!: add --default-token-lifetime (#14631)

This commit is contained in:
Ethan
2024-09-18 21:23:42 +10:00
committed by GitHub
parent 6de59371ea
commit fccf6f1e0e
14 changed files with 165 additions and 38 deletions
+4 -1
View File
@@ -12416,7 +12416,10 @@ const docTemplate = `{
"type": "object",
"properties": {
"default_duration": {
"description": "DefaultDuration is for api keys, not tokens.",
"description": "DefaultDuration is only for browser, workspace app and oauth sessions.",
"type": "integer"
},
"default_token_lifetime": {
"type": "integer"
},
"disable_expiry_refresh": {
+4 -1
View File
@@ -11240,7 +11240,10 @@
"type": "object",
"properties": {
"default_duration": {
"description": "DefaultDuration is for api keys, not tokens.",
"description": "DefaultDuration is only for browser, workspace app and oauth sessions.",
"type": "integer"
},
"default_token_lifetime": {
"type": "integer"
},
"disable_expiry_refresh": {
+20 -27
View File
@@ -23,7 +23,7 @@ import (
"github.com/coder/coder/v2/codersdk"
)
// Creates a new token API key that effectively doesn't expire.
// Creates a new token API key with the given scope and lifetime.
//
// @Summary Create token API key
// @ID create-token-api-key
@@ -60,36 +60,34 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
scope = database.APIKeyScope(createToken.Scope)
}
// default lifetime is 30 days
lifeTime := 30 * 24 * time.Hour
if createToken.Lifetime != 0 {
lifeTime = createToken.Lifetime
}
tokenName := namesgenerator.GetRandomName(1)
if len(createToken.TokenName) != 0 {
tokenName = createToken.TokenName
}
err := api.validateAPIKeyLifetime(lifeTime)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to validate create API key request.",
Detail: err.Error(),
})
return
}
cookie, key, err := api.createAPIKey(ctx, apikey.CreateParams{
params := apikey.CreateParams{
UserID: user.ID,
LoginType: database.LoginTypeToken,
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
ExpiresAt: dbtime.Now().Add(lifeTime),
DefaultLifetime: api.DeploymentValues.Sessions.DefaultTokenDuration.Value(),
Scope: scope,
LifetimeSeconds: int64(lifeTime.Seconds()),
TokenName: tokenName,
})
}
if createToken.Lifetime != 0 {
err := api.validateAPIKeyLifetime(createToken.Lifetime)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to validate create API key request.",
Detail: err.Error(),
})
return
}
params.ExpiresAt = dbtime.Now().Add(createToken.Lifetime)
params.LifetimeSeconds = int64(createToken.Lifetime.Seconds())
}
cookie, key, err := api.createAPIKey(ctx, params)
if err != nil {
if database.IsUniqueViolation(err, database.UniqueIndexAPIKeyName) {
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
@@ -125,16 +123,11 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := httpmw.UserParam(r)
lifeTime := time.Hour * 24 * 7
cookie, _, err := api.createAPIKey(ctx, apikey.CreateParams{
UserID: user.ID,
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
DefaultLifetime: api.DeploymentValues.Sessions.DefaultTokenDuration.Value(),
LoginType: database.LoginTypePassword,
RemoteAddr: r.RemoteAddr,
// All api generated keys will last 1 week. Browser login tokens have
// a shorter life.
ExpiresAt: dbtime.Now().Add(lifeTime),
LifetimeSeconds: int64(lifeTime.Seconds()),
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
+88 -4
View File
@@ -45,8 +45,8 @@ func TestTokenCRUD(t *testing.T) {
require.EqualValues(t, len(keys), 1)
require.Contains(t, res.Key, keys[0].ID)
// expires_at should default to 30 days
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*29*24))
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*31*24))
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*6))
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*8))
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
// no update
@@ -115,8 +115,8 @@ func TestDefaultTokenDuration(t *testing.T) {
require.NoError(t, err)
keys, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
require.NoError(t, err)
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*29*24))
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*31*24))
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*6))
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*8))
}
func TestTokenUserSetMaxLifetime(t *testing.T) {
@@ -144,6 +144,27 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
require.ErrorContains(t, err, "lifetime must be less")
}
func TestTokenCustomDefaultLifetime(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dc := coderdtest.DeploymentValues(t)
dc.Sessions.DefaultTokenDuration = serpent.Duration(time.Hour * 12)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: dc,
})
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
require.NoError(t, err)
tokens, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
require.NoError(t, err)
require.Len(t, tokens, 1)
require.EqualValues(t, dc.Sessions.DefaultTokenDuration.Value().Seconds(), tokens[0].LifetimeSeconds)
}
func TestSessionExpiry(t *testing.T) {
t.Parallel()
@@ -224,3 +245,66 @@ func TestAPIKey_Deleted(t *testing.T) {
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
}
func TestAPIKey_Refresh(t *testing.T) {
t.Parallel()
db, pubsub := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubsub,
})
owner := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
token, err := client.CreateAPIKey(ctx, owner.UserID.String())
require.NoError(t, err)
split := strings.Split(token.Key, "-")
apiKey1, err := db.GetAPIKeyByID(ctx, split[0])
require.NoError(t, err)
require.Equal(t, int64(604800), apiKey1.LifetimeSeconds, "default should be 7 days")
err = db.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
ID: apiKey1.ID,
LastUsed: apiKey1.LastUsed,
// Cross the no-refresh threshold
ExpiresAt: apiKey1.ExpiresAt.Add(time.Hour * -2),
IPAddress: apiKey1.IPAddress,
})
require.NoError(t, err, "update login key")
// Refresh the token
client.SetSessionToken(token.Key)
_, err = client.User(ctx, codersdk.Me)
require.NoError(t, err)
apiKey2, err := client.APIKeyByID(ctx, owner.UserID.String(), split[0])
require.NoError(t, err)
require.True(t, apiKey2.ExpiresAt.After(apiKey1.ExpiresAt), "token should have a later expiry")
}
func TestAPIKey_SetDefault(t *testing.T) {
t.Parallel()
db, pubsub := dbtestutil.NewDB(t)
dc := coderdtest.DeploymentValues(t)
dc.Sessions.DefaultTokenDuration = serpent.Duration(time.Hour * 12)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubsub,
DeploymentValues: dc,
})
owner := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
token, err := client.CreateAPIKey(ctx, owner.UserID.String())
require.NoError(t, err)
split := strings.Split(token.Key, "-")
apiKey1, err := db.GetAPIKeyByID(ctx, split[0])
require.NoError(t, err)
require.EqualValues(t, dc.Sessions.DefaultTokenDuration.Value().Seconds(), apiKey1.LifetimeSeconds)
}
@@ -1958,7 +1958,7 @@ func (s *server) regenerateSessionToken(ctx context.Context, user database.User,
UserID: user.ID,
LoginType: user.LoginType,
TokenName: workspaceSessionTokenName(workspace),
DefaultLifetime: s.DeploymentValues.Sessions.DefaultDuration.Value(),
DefaultLifetime: s.DeploymentValues.Sessions.DefaultTokenDuration.Value(),
LifetimeSeconds: int64(s.DeploymentValues.Sessions.MaximumTokenDuration.Value().Seconds()),
})
if err != nil {
+2 -2
View File
@@ -299,8 +299,8 @@ func TestPostLogin(t *testing.T) {
apiKey, err := client.APIKeyByID(ctx, owner.UserID.String(), split[0])
require.NoError(t, err, "fetch api key")
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*29)), "default tokens lasts more than 29 days")
require.True(t, apiKey.ExpiresAt.Before(time.Now().Add(time.Hour*24*31)), "default tokens lasts less than 31 days")
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*6)), "default tokens lasts more than 6 days")
require.True(t, apiKey.ExpiresAt.Before(time.Now().Add(time.Hour*24*8)), "default tokens lasts less than 8 days")
require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "token should have longer lifetime")
})
}