mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat!: add --default-token-lifetime (#14631)
This commit is contained in:
+5
@@ -25,6 +25,11 @@ OPTIONS:
|
||||
systemd. This directory is NOT safe to be configured as a shared
|
||||
directory across coderd/provisionerd replicas.
|
||||
|
||||
--default-token-lifetime duration, $CODER_DEFAULT_TOKEN_LIFETIME (default: 168h0m0s)
|
||||
The default lifetime duration for API tokens. This value is used when
|
||||
creating a token without specifying a duration, such as when
|
||||
authenticating the CLI or an IDE plugin.
|
||||
|
||||
--disable-owner-workspace-access bool, $CODER_DISABLE_OWNER_WORKSPACE_ACCESS
|
||||
Remove the permission for the 'owner' role to have workspace execution
|
||||
on all workspaces. This prevents the 'owner' from ssh, apps, and
|
||||
|
||||
+5
@@ -423,6 +423,11 @@ experiments: []
|
||||
# performed once per day.
|
||||
# (default: false, type: bool)
|
||||
updateCheck: false
|
||||
# The default lifetime duration for API tokens. This value is used when creating a
|
||||
# token without specifying a duration, such as when authenticating the CLI or an
|
||||
# IDE plugin.
|
||||
# (default: 168h0m0s, type: duration)
|
||||
defaultTokenLifetime: 168h0m0s
|
||||
# Expose the swagger endpoint via /swagger.
|
||||
# (default: <unset>, type: bool)
|
||||
enableSwagger: false
|
||||
|
||||
Generated
+4
-1
@@ -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": {
|
||||
|
||||
Generated
+4
-1
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
+13
-1
@@ -454,9 +454,11 @@ type SessionLifetime struct {
|
||||
// creation is the lifetime of the api key.
|
||||
DisableExpiryRefresh serpent.Bool `json:"disable_expiry_refresh,omitempty" typescript:",notnull"`
|
||||
|
||||
// DefaultDuration is for api keys, not tokens.
|
||||
// DefaultDuration is only for browser, workspace app and oauth sessions.
|
||||
DefaultDuration serpent.Duration `json:"default_duration" typescript:",notnull"`
|
||||
|
||||
DefaultTokenDuration serpent.Duration `json:"default_token_lifetime,omitempty" typescript:",notnull"`
|
||||
|
||||
MaximumTokenDuration serpent.Duration `json:"max_token_lifetime,omitempty" typescript:",notnull"`
|
||||
}
|
||||
|
||||
@@ -1998,6 +2000,16 @@ when required by your organization's security policy.`,
|
||||
YAML: "maxTokenLifetime",
|
||||
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
|
||||
},
|
||||
{
|
||||
Name: "Default Token Lifetime",
|
||||
Description: "The default lifetime duration for API tokens. This value is used when creating a token without specifying a duration, such as when authenticating the CLI or an IDE plugin.",
|
||||
Flag: "default-token-lifetime",
|
||||
Env: "CODER_DEFAULT_TOKEN_LIFETIME",
|
||||
Default: (7 * 24 * time.Hour).String(),
|
||||
Value: &c.Sessions.DefaultTokenDuration,
|
||||
YAML: "defaultTokenLifetime",
|
||||
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
|
||||
},
|
||||
{
|
||||
Name: "Enable swagger endpoint",
|
||||
Description: "Expose the swagger endpoint via /swagger.",
|
||||
|
||||
Generated
+1
@@ -396,6 +396,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
||||
"secure_auth_cookie": true,
|
||||
"session_lifetime": {
|
||||
"default_duration": 0,
|
||||
"default_token_lifetime": 0,
|
||||
"disable_expiry_refresh": true,
|
||||
"max_token_lifetime": 0
|
||||
},
|
||||
|
||||
Generated
+5
-1
@@ -1902,6 +1902,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"secure_auth_cookie": true,
|
||||
"session_lifetime": {
|
||||
"default_duration": 0,
|
||||
"default_token_lifetime": 0,
|
||||
"disable_expiry_refresh": true,
|
||||
"max_token_lifetime": 0
|
||||
},
|
||||
@@ -2328,6 +2329,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"secure_auth_cookie": true,
|
||||
"session_lifetime": {
|
||||
"default_duration": 0,
|
||||
"default_token_lifetime": 0,
|
||||
"disable_expiry_refresh": true,
|
||||
"max_token_lifetime": 0
|
||||
},
|
||||
@@ -4772,6 +4774,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
```json
|
||||
{
|
||||
"default_duration": 0,
|
||||
"default_token_lifetime": 0,
|
||||
"disable_expiry_refresh": true,
|
||||
"max_token_lifetime": 0
|
||||
}
|
||||
@@ -4781,7 +4784,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------------ | ------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `default_duration` | integer | false | | Default duration is for api keys, not tokens. |
|
||||
| `default_duration` | integer | false | | Default duration is only for browser, workspace app and oauth sessions. |
|
||||
| `default_token_lifetime` | integer | false | | |
|
||||
| `disable_expiry_refresh` | boolean | false | | Disable expiry refresh will disable automatically refreshing api keys when they are used from the api. This means the api key lifetime at creation is the lifetime of the api key. |
|
||||
| `max_token_lifetime` | integer | false | | |
|
||||
|
||||
|
||||
Generated
+11
@@ -910,6 +910,17 @@ Periodically check for new releases of Coder and inform the owner. The check is
|
||||
|
||||
The maximum lifetime duration users can specify when creating an API token.
|
||||
|
||||
### --default-token-lifetime
|
||||
|
||||
| | |
|
||||
| ----------- | ------------------------------------------ |
|
||||
| Type | <code>duration</code> |
|
||||
| Environment | <code>$CODER_DEFAULT_TOKEN_LIFETIME</code> |
|
||||
| YAML | <code>defaultTokenLifetime</code> |
|
||||
| Default | <code>168h0m0s</code> |
|
||||
|
||||
The default lifetime duration for API tokens. This value is used when creating a token without specifying a duration, such as when authenticating the CLI or an IDE plugin.
|
||||
|
||||
### --swagger-enable
|
||||
|
||||
| | |
|
||||
|
||||
@@ -26,6 +26,11 @@ OPTIONS:
|
||||
systemd. This directory is NOT safe to be configured as a shared
|
||||
directory across coderd/provisionerd replicas.
|
||||
|
||||
--default-token-lifetime duration, $CODER_DEFAULT_TOKEN_LIFETIME (default: 168h0m0s)
|
||||
The default lifetime duration for API tokens. This value is used when
|
||||
creating a token without specifying a duration, such as when
|
||||
authenticating the CLI or an IDE plugin.
|
||||
|
||||
--disable-owner-workspace-access bool, $CODER_DISABLE_OWNER_WORKSPACE_ACCESS
|
||||
Remove the permission for the 'owner' role to have workspace execution
|
||||
on all workspaces. This prevents the 'owner' from ssh, apps, and
|
||||
|
||||
Generated
+1
@@ -1208,6 +1208,7 @@ export interface SessionCountDeploymentStats {
|
||||
export interface SessionLifetime {
|
||||
readonly disable_expiry_refresh?: boolean;
|
||||
readonly default_duration: number;
|
||||
readonly default_token_lifetime?: number;
|
||||
readonly max_token_lifetime?: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user