Skip to content

feat: add separate max token lifetime for administrators #18267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@ NETWORKING / HTTP OPTIONS:
The maximum lifetime duration users can specify when creating an API
token.

--max-admin-token-lifetime duration, $CODER_MAX_ADMIN_TOKEN_LIFETIME (default: 168h0m0s)
The maximum lifetime duration administrators can specify when creating
an API token.

--proxy-health-interval duration, $CODER_PROXY_HEALTH_INTERVAL (default: 1m0s)
The interval in which coderd should be checking the status of
workspace proxies.
Expand Down
4 changes: 4 additions & 0 deletions cli/testdata/server-config.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ networking:
# The maximum lifetime duration users can specify when creating an API token.
# (default: 876600h0m0s, type: duration)
maxTokenLifetime: 876600h0m0s
# The maximum lifetime duration administrators can specify when creating an API
# token.
# (default: 168h0m0s, type: duration)
maxAdminTokenLifetime: 168h0m0s
# The token expiry duration for browser sessions. Sessions may last longer if they
# are actively making requests, but this functionality can be disabled via
# --disable-session-expiry-refresh.
Expand Down
3 changes: 3 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 42 additions & 7 deletions coderd/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/codersdk"
Expand Down Expand Up @@ -75,7 +76,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
}

if createToken.Lifetime != 0 {
err := api.validateAPIKeyLifetime(createToken.Lifetime)
err := api.validateAPIKeyLifetime(ctx, user.ID, createToken.Lifetime)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to validate create API key request.",
Expand Down Expand Up @@ -338,35 +339,69 @@ func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) {
// @Success 200 {object} codersdk.TokenConfig
// @Router /users/{user}/keys/tokens/tokenconfig [get]
func (api *API) tokenConfig(rw http.ResponseWriter, r *http.Request) {
values, err := api.DeploymentValues.WithoutSecrets()
user := httpmw.UserParam(r)
maxLifetime, err := api.getMaxTokenLifetime(r.Context(), user.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to get token configuration.",
Detail: err.Error(),
})
return
}

httpapi.Write(
r.Context(), rw, http.StatusOK,
codersdk.TokenConfig{
MaxTokenLifetime: values.Sessions.MaximumTokenDuration.Value(),
MaxTokenLifetime: maxLifetime,
},
)
}

func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
func (api *API) validateAPIKeyLifetime(ctx context.Context, userID uuid.UUID, lifetime time.Duration) error {
if lifetime <= 0 {
return xerrors.New("lifetime must be positive number greater than 0")
}

if lifetime > api.DeploymentValues.Sessions.MaximumTokenDuration.Value() {
maxLifetime, err := api.getMaxTokenLifetime(ctx, userID)
if err != nil {
return xerrors.Errorf("failed to get max token lifetime: %w", err)
}

if lifetime > maxLifetime {
return xerrors.Errorf(
"lifetime must be less than %v",
api.DeploymentValues.Sessions.MaximumTokenDuration,
maxLifetime,
)
}

return nil
}

// getMaxTokenLifetime returns the maximum allowed token lifetime for a user.
// It distinguishes between regular users and owners.
func (api *API) getMaxTokenLifetime(ctx context.Context, userID uuid.UUID) (time.Duration, error) {
subject, _, err := httpmw.UserRBACSubject(ctx, api.Database, userID, rbac.ScopeAll)
if err != nil {
return 0, xerrors.Errorf("failed to get user rbac subject: %w", err)
}

roles, err := subject.Roles.Expand()
if err != nil {
return 0, xerrors.Errorf("failed to expand user roles: %w", err)
}

maxLifetime := api.DeploymentValues.Sessions.MaximumTokenDuration.Value()
for _, role := range roles {
if role.Identifier.Name == codersdk.RoleOwner {
// Owners have a different max lifetime.
maxLifetime = api.DeploymentValues.Sessions.MaximumAdminTokenDuration.Value()
break
}
}

return maxLifetime, nil
}

func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (*http.Cookie, *database.APIKey, error) {
key, sessionToken, err := apikey.Generate(params)
if err != nil {
Expand Down
82 changes: 82 additions & 0 deletions coderd/apikey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,88 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
require.ErrorContains(t, err, "lifetime must be less")
}

func TestTokenAdminSetMaxLifetime(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dc := coderdtest.DeploymentValues(t)
dc.Sessions.MaximumTokenDuration = serpent.Duration(time.Hour * 24 * 7)
dc.Sessions.MaximumAdminTokenDuration = serpent.Duration(time.Hour * 24 * 14)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: dc,
})
adminUser := coderdtest.CreateFirstUser(t, client)
nonAdminClient, _ := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)

// Admin should be able to create a token with a lifetime longer than the non-admin max.
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 10,
})
require.NoError(t, err)

// Admin should NOT be able to create a token with a lifetime longer than the admin max.
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 15,
})
require.Error(t, err)
require.Contains(t, err.Error(), "lifetime must be less")

// Non-admin should NOT be able to create a token with a lifetime longer than the non-admin max.
_, err = nonAdminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 8,
})
require.Error(t, err)
require.Contains(t, err.Error(), "lifetime must be less")

// Non-admin should be able to create a token with a lifetime shorter than the non-admin max.
_, err = nonAdminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 6,
})
require.NoError(t, err)
}

func TestTokenAdminSetMaxLifetimeShorter(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dc := coderdtest.DeploymentValues(t)
dc.Sessions.MaximumTokenDuration = serpent.Duration(time.Hour * 24 * 14)
dc.Sessions.MaximumAdminTokenDuration = serpent.Duration(time.Hour * 24 * 7)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: dc,
})
adminUser := coderdtest.CreateFirstUser(t, client)
nonAdminClient, _ := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)

// Admin should NOT be able to create a token with a lifetime longer than the admin max.
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 8,
})
require.Error(t, err)
require.Contains(t, err.Error(), "lifetime must be less")

// Admin should be able to create a token with a lifetime shorter than the admin max.
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 6,
})
require.NoError(t, err)

// Non-admin should be able to create a token with a lifetime longer than the admin max.
_, err = nonAdminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 10,
})
require.NoError(t, err)

// Non-admin should NOT be able to create a token with a lifetime longer than the non-admin max.
_, err = nonAdminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 15,
})
require.Error(t, err)
require.Contains(t, err.Error(), "lifetime must be less")
}

func TestTokenCustomDefaultLifetime(t *testing.T) {
t.Parallel()

Expand Down
13 changes: 13 additions & 0 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ type SessionLifetime struct {
DefaultTokenDuration serpent.Duration `json:"default_token_lifetime,omitempty" typescript:",notnull"`

MaximumTokenDuration serpent.Duration `json:"max_token_lifetime,omitempty" typescript:",notnull"`

MaximumAdminTokenDuration serpent.Duration `json:"max_admin_token_lifetime,omitempty" typescript:",notnull"`
}

type DERP struct {
Expand Down Expand Up @@ -2340,6 +2342,17 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
YAML: "maxTokenLifetime",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
},
{
Name: "Maximum Admin Token Lifetime",
Description: "The maximum lifetime duration administrators can specify when creating an API token.",
Flag: "max-admin-token-lifetime",
Env: "CODER_MAX_ADMIN_TOKEN_LIFETIME",
Default: (7 * 24 * time.Hour).String(),
Value: &c.Sessions.MaximumAdminTokenDuration,
Group: &deploymentGroupNetworkingHTTP,
YAML: "maxAdminTokenLifetime",
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.",
Expand Down
1 change: 1 addition & 0 deletions docs/reference/api/general.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 10 additions & 6 deletions docs/reference/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions docs/reference/cli/server.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions enterprise/cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ NETWORKING / HTTP OPTIONS:
The maximum lifetime duration users can specify when creating an API
token.

--max-admin-token-lifetime duration, $CODER_MAX_ADMIN_TOKEN_LIFETIME (default: 168h0m0s)
The maximum lifetime duration administrators can specify when creating
an API token.

--proxy-health-interval duration, $CODER_PROXY_HEALTH_INTERVAL (default: 1m0s)
The interval in which coderd should be checking the status of
workspace proxies.
Expand Down
1 change: 1 addition & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading