mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat: add flag for token lifetime (#5385)
This commit is contained in:
@ -424,6 +424,12 @@ func newConfig() *codersdk.DeploymentConfig {
|
|||||||
Flag: "update-check",
|
Flag: "update-check",
|
||||||
Default: flag.Lookup("test.v") == nil && !buildinfo.IsDev(),
|
Default: flag.Lookup("test.v") == nil && !buildinfo.IsDev(),
|
||||||
},
|
},
|
||||||
|
MaxTokenLifetime: &codersdk.DeploymentConfigField[time.Duration]{
|
||||||
|
Name: "Max Token Lifetime",
|
||||||
|
Usage: "The maximum lifetime duration for any user creating a token.",
|
||||||
|
Flag: "max-token-lifetime",
|
||||||
|
Default: 24 * 30 * time.Hour,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
cli/testdata/coder_server_--help.golden
vendored
4
cli/testdata/coder_server_--help.golden
vendored
@ -65,6 +65,10 @@ Flags:
|
|||||||
production.
|
production.
|
||||||
Consumes $CODER_EXPERIMENTAL
|
Consumes $CODER_EXPERIMENTAL
|
||||||
-h, --help help for server
|
-h, --help help for server
|
||||||
|
--max-token-lifetime duration The maximum lifetime duration for any
|
||||||
|
user creating a token.
|
||||||
|
Consumes $CODER_MAX_TOKEN_LIFETIME
|
||||||
|
(default 720h0m0s)
|
||||||
--oauth2-github-allow-everyone Allow all logins, setting this option
|
--oauth2-github-allow-everyone Allow all logins, setting this option
|
||||||
means allowed orgs and teams must be
|
means allowed orgs and teams must be
|
||||||
empty.
|
empty.
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/cli/cliflag"
|
||||||
"github.com/coder/coder/cli/cliui"
|
"github.com/coder/coder/cli/cliui"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
)
|
)
|
||||||
@ -46,6 +47,9 @@ func tokens() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createToken() *cobra.Command {
|
func createToken() *cobra.Command {
|
||||||
|
var (
|
||||||
|
tokenLifetime time.Duration
|
||||||
|
)
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "create",
|
Use: "create",
|
||||||
Short: "Create a tokens",
|
Short: "Create a tokens",
|
||||||
@ -55,7 +59,9 @@ func createToken() *cobra.Command {
|
|||||||
return xerrors.Errorf("create codersdk client: %w", err)
|
return xerrors.Errorf("create codersdk client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := client.CreateToken(cmd.Context(), codersdk.Me, codersdk.CreateTokenRequest{})
|
res, err := client.CreateToken(cmd.Context(), codersdk.Me, codersdk.CreateTokenRequest{
|
||||||
|
Lifetime: tokenLifetime,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create tokens: %w", err)
|
return xerrors.Errorf("create tokens: %w", err)
|
||||||
}
|
}
|
||||||
@ -74,6 +80,8 @@ func createToken() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cliflag.DurationVarP(cmd.Flags(), &tokenLifetime, "lifetime", "", "CODER_TOKEN_LIFETIME", 30*24*time.Hour, "Specify a duration for the lifetime of the token.")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +44,21 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
|
|||||||
scope = database.APIKeyScope(createToken.Scope)
|
scope = database.APIKeyScope(createToken.Scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tokens last 100 years
|
// default lifetime is 30 days
|
||||||
lifeTime := time.Hour * 876000
|
lifeTime := 30 * 24 * time.Hour
|
||||||
|
if createToken.Lifetime != 0 {
|
||||||
|
lifeTime = createToken.Lifetime
|
||||||
|
}
|
||||||
|
|
||||||
|
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, err := api.createAPIKey(ctx, createAPIKeyParams{
|
cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
LoginType: database.LoginTypeToken,
|
LoginType: database.LoginTypeToken,
|
||||||
@ -65,7 +78,6 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new session key, used for logging in via the CLI.
|
// Creates a new session key, used for logging in via the CLI.
|
||||||
// DEPRECATED: use postToken instead.
|
|
||||||
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
user := httpmw.UserParam(r)
|
user := httpmw.UserParam(r)
|
||||||
@ -214,6 +226,18 @@ type createAPIKeyParams struct {
|
|||||||
Scope database.APIKeyScope
|
Scope database.APIKeyScope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
|
||||||
|
if lifetime <= 0 {
|
||||||
|
return xerrors.New("lifetime must be positive number greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lifetime > api.DeploymentConfig.MaxTokenLifetime.Value {
|
||||||
|
return xerrors.Errorf("lifetime must be less than %s", api.DeploymentConfig.MaxTokenLifetime.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, error) {
|
func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, error) {
|
||||||
keyID, keySecret, err := generateAPIKeyIDSecret()
|
keyID, keySecret, err := generateAPIKeyIDSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -12,63 +12,101 @@ import (
|
|||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTokens(t *testing.T) {
|
func TestTokenCRUD(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("CRUD", func(t *testing.T) {
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
t.Parallel()
|
defer cancel()
|
||||||
|
client := coderdtest.New(t, nil)
|
||||||
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
|
keys, err := client.GetTokens(ctx, codersdk.Me)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, keys)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
|
||||||
defer cancel()
|
require.NoError(t, err)
|
||||||
client := coderdtest.New(t, nil)
|
require.Greater(t, len(res.Key), 2)
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
|
||||||
keys, err := client.GetTokens(ctx, codersdk.Me)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, keys)
|
|
||||||
|
|
||||||
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
|
keys, err = client.GetTokens(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Greater(t, len(res.Key), 2)
|
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.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
|
||||||
|
|
||||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
// no update
|
||||||
require.NoError(t, err)
|
|
||||||
require.EqualValues(t, len(keys), 1)
|
|
||||||
require.Contains(t, res.Key, keys[0].ID)
|
|
||||||
// expires_at must be greater than 50 years
|
|
||||||
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
|
|
||||||
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
|
|
||||||
|
|
||||||
// no update
|
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
keys, err = client.GetTokens(ctx, codersdk.Me)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, keys)
|
||||||
|
}
|
||||||
|
|
||||||
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
|
func TestTokenScoped(t *testing.T) {
|
||||||
require.NoError(t, err)
|
t.Parallel()
|
||||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
|
||||||
require.NoError(t, err)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
require.Empty(t, keys)
|
defer cancel()
|
||||||
|
client := coderdtest.New(t, nil)
|
||||||
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
|
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
|
||||||
|
Scope: codersdk.APIKeyScopeApplicationConnect,
|
||||||
})
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Greater(t, len(res.Key), 2)
|
||||||
|
|
||||||
t.Run("Scoped", func(t *testing.T) {
|
keys, err := client.GetTokens(ctx, codersdk.Me)
|
||||||
t.Parallel()
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, len(keys), 1)
|
||||||
|
require.Contains(t, res.Key, keys[0].ID)
|
||||||
|
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
func TestTokenDuration(t *testing.T) {
|
||||||
defer cancel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
|
||||||
|
|
||||||
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
Scope: codersdk.APIKeyScopeApplicationConnect,
|
defer cancel()
|
||||||
})
|
client := coderdtest.New(t, nil)
|
||||||
require.NoError(t, err)
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
require.Greater(t, len(res.Key), 2)
|
|
||||||
|
|
||||||
keys, err := client.GetTokens(ctx, codersdk.Me)
|
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
|
||||||
require.NoError(t, err)
|
Lifetime: time.Hour * 24 * 7,
|
||||||
require.EqualValues(t, len(keys), 1)
|
|
||||||
require.Contains(t, res.Key, keys[0].ID)
|
|
||||||
// expires_at must be greater than 50 years
|
|
||||||
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
|
|
||||||
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
|
|
||||||
})
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
keys, err := client.GetTokens(ctx, codersdk.Me)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*6*24))
|
||||||
|
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*8*24))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenMaxLifetime(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
dc := coderdtest.DeploymentConfig(t)
|
||||||
|
dc.MaxTokenLifetime.Value = time.Hour * 24 * 7
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
DeploymentConfig: dc,
|
||||||
|
})
|
||||||
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
|
// success
|
||||||
|
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
|
||||||
|
Lifetime: time.Hour * 24 * 6,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// fail
|
||||||
|
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
|
||||||
|
Lifetime: time.Hour * 24 * 8,
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "lifetime must be less")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIKey(t *testing.T) {
|
func TestAPIKey(t *testing.T) {
|
||||||
|
@ -313,7 +313,8 @@ func TestPostLogin(t *testing.T) {
|
|||||||
apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0])
|
apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0])
|
||||||
require.NoError(t, err, "fetch api key")
|
require.NoError(t, err, "fetch api key")
|
||||||
|
|
||||||
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*438300)), "tokens lasts more than 50 years")
|
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.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "token should have longer lifetime")
|
require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "token should have longer lifetime")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateTokenRequest struct {
|
type CreateTokenRequest struct {
|
||||||
Scope APIKeyScope `json:"scope"`
|
Lifetime time.Duration `json:"lifetime"`
|
||||||
|
Scope APIKeyScope `json:"scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateAPIKeyResponse contains an API key for a user.
|
// GenerateAPIKeyResponse contains an API key for a user.
|
||||||
|
@ -42,6 +42,7 @@ type DeploymentConfig struct {
|
|||||||
APIRateLimit *DeploymentConfigField[int] `json:"api_rate_limit" typescript:",notnull"`
|
APIRateLimit *DeploymentConfigField[int] `json:"api_rate_limit" typescript:",notnull"`
|
||||||
Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"`
|
Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"`
|
||||||
UpdateCheck *DeploymentConfigField[bool] `json:"update_check" typescript:",notnull"`
|
UpdateCheck *DeploymentConfigField[bool] `json:"update_check" typescript:",notnull"`
|
||||||
|
MaxTokenLifetime *DeploymentConfigField[time.Duration] `json:"max_token_lifetime" typescript:",notnull"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DERP struct {
|
type DERP struct {
|
||||||
|
@ -206,6 +206,8 @@ export interface CreateTestAuditLogRequest {
|
|||||||
|
|
||||||
// From codersdk/apikey.go
|
// From codersdk/apikey.go
|
||||||
export interface CreateTokenRequest {
|
export interface CreateTokenRequest {
|
||||||
|
// This is likely an enum in an external package ("time.Duration")
|
||||||
|
readonly lifetime: number
|
||||||
readonly scope: APIKeyScope
|
readonly scope: APIKeyScope
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,6 +305,7 @@ export interface DeploymentConfig {
|
|||||||
readonly api_rate_limit: DeploymentConfigField<number>
|
readonly api_rate_limit: DeploymentConfigField<number>
|
||||||
readonly experimental: DeploymentConfigField<boolean>
|
readonly experimental: DeploymentConfigField<boolean>
|
||||||
readonly update_check: DeploymentConfigField<boolean>
|
readonly update_check: DeploymentConfigField<boolean>
|
||||||
|
readonly max_token_lifetime: DeploymentConfigField<number>
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/deploymentconfig.go
|
// From codersdk/deploymentconfig.go
|
||||||
|
Reference in New Issue
Block a user