feat: add flag for token lifetime (#5385)

This commit is contained in:
Garrett Delfosse
2022-12-12 15:39:31 -05:00
committed by GitHub
parent 760419a965
commit 40a5c0476f
9 changed files with 136 additions and 50 deletions

View File

@ -424,6 +424,12 @@ func newConfig() *codersdk.DeploymentConfig {
Flag: "update-check",
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,
},
}
}

View File

@ -65,6 +65,10 @@ Flags:
production.
Consumes $CODER_EXPERIMENTAL
-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
means allowed orgs and teams must be
empty.

View File

@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
@ -46,6 +47,9 @@ func tokens() *cobra.Command {
}
func createToken() *cobra.Command {
var (
tokenLifetime time.Duration
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a tokens",
@ -55,7 +59,9 @@ func createToken() *cobra.Command {
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 {
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
}

View File

@ -44,8 +44,21 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
scope = database.APIKeyScope(createToken.Scope)
}
// tokens last 100 years
lifeTime := time.Hour * 876000
// default lifetime is 30 days
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{
UserID: user.ID,
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.
// DEPRECATED: use postToken instead.
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := httpmw.UserParam(r)
@ -214,6 +226,18 @@ type createAPIKeyParams struct {
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) {
keyID, keySecret, err := generateAPIKeyIDSecret()
if err != nil {

View File

@ -12,10 +12,7 @@ import (
"github.com/coder/coder/testutil"
)
func TestTokens(t *testing.T) {
t.Parallel()
t.Run("CRUD", func(t *testing.T) {
func TestTokenCRUD(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@ -34,8 +31,9 @@ func TestTokens(t *testing.T) {
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))
// 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)
// no update
@ -45,9 +43,9 @@ func TestTokens(t *testing.T) {
keys, err = client.GetTokens(ctx, codersdk.Me)
require.NoError(t, err)
require.Empty(t, keys)
})
}
t.Run("Scoped", func(t *testing.T) {
func TestTokenScoped(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@ -65,10 +63,50 @@ func TestTokens(t *testing.T) {
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, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
}
func TestTokenDuration(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 7,
})
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) {

View File

@ -313,7 +313,8 @@ func TestPostLogin(t *testing.T) {
apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0])
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")
})
}

View File

@ -40,6 +40,7 @@ const (
)
type CreateTokenRequest struct {
Lifetime time.Duration `json:"lifetime"`
Scope APIKeyScope `json:"scope"`
}

View File

@ -42,6 +42,7 @@ type DeploymentConfig struct {
APIRateLimit *DeploymentConfigField[int] `json:"api_rate_limit" typescript:",notnull"`
Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"`
UpdateCheck *DeploymentConfigField[bool] `json:"update_check" typescript:",notnull"`
MaxTokenLifetime *DeploymentConfigField[time.Duration] `json:"max_token_lifetime" typescript:",notnull"`
}
type DERP struct {

View File

@ -206,6 +206,8 @@ export interface CreateTestAuditLogRequest {
// From codersdk/apikey.go
export interface CreateTokenRequest {
// This is likely an enum in an external package ("time.Duration")
readonly lifetime: number
readonly scope: APIKeyScope
}
@ -303,6 +305,7 @@ export interface DeploymentConfig {
readonly api_rate_limit: DeploymentConfigField<number>
readonly experimental: DeploymentConfigField<boolean>
readonly update_check: DeploymentConfigField<boolean>
readonly max_token_lifetime: DeploymentConfigField<number>
}
// From codersdk/deploymentconfig.go