diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden
index 1cefe8767f..26e63ceb84 100644
--- a/cli/testdata/coder_server_--help.golden
+++ b/cli/testdata/coder_server_--help.golden
@@ -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.
diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden
index 7403819a2d..cc064e8fa2 100644
--- a/cli/testdata/server-config.yaml.golden
+++ b/cli/testdata/server-config.yaml.golden
@@ -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.
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 07a0407c00..d11a0635d6 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -15705,6 +15705,9 @@ const docTemplate = `{
"description": "DisableExpiryRefresh will disable automatically refreshing api\nkeys when they are used from the api. This means the api key lifetime at\ncreation is the lifetime of the api key.",
"type": "boolean"
},
+ "max_admin_token_lifetime": {
+ "type": "integer"
+ },
"max_token_lifetime": {
"type": "integer"
}
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 076f170d27..aabe0b9b12 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -14283,6 +14283,9 @@
"description": "DisableExpiryRefresh will disable automatically refreshing api\nkeys when they are used from the api. This means the api key lifetime at\ncreation is the lifetime of the api key.",
"type": "boolean"
},
+ "max_admin_token_lifetime": {
+ "type": "integer"
+ },
"max_token_lifetime": {
"type": "integer"
}
diff --git a/coderd/apikey.go b/coderd/apikey.go
index ddcf776771..895be440ef 100644
--- a/coderd/apikey.go
+++ b/coderd/apikey.go
@@ -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"
@@ -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.",
@@ -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 {
diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go
index 43e3325339..dbf5a3520a 100644
--- a/coderd/apikey_test.go
+++ b/coderd/apikey_test.go
@@ -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()
diff --git a/codersdk/deployment.go b/codersdk/deployment.go
index 696e6bda52..ac72ed2fc1 100644
--- a/codersdk/deployment.go
+++ b/codersdk/deployment.go
@@ -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 {
@@ -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.",
diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md
index 1245414556..e0fb97a151 100644
--- a/docs/reference/api/general.md
+++ b/docs/reference/api/general.md
@@ -454,6 +454,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"default_duration": 0,
"default_token_lifetime": 0,
"disable_expiry_refresh": true,
+ "max_admin_token_lifetime": 0,
"max_token_lifetime": 0
},
"ssh_keygen_algorithm": "string",
diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md
index 6b0f8254a7..4191ab8970 100644
--- a/docs/reference/api/schemas.md
+++ b/docs/reference/api/schemas.md
@@ -2625,6 +2625,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"default_duration": 0,
"default_token_lifetime": 0,
"disable_expiry_refresh": true,
+ "max_admin_token_lifetime": 0,
"max_token_lifetime": 0
},
"ssh_keygen_algorithm": "string",
@@ -3124,6 +3125,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"default_duration": 0,
"default_token_lifetime": 0,
"disable_expiry_refresh": true,
+ "max_admin_token_lifetime": 0,
"max_token_lifetime": 0
},
"ssh_keygen_algorithm": "string",
@@ -6767,18 +6769,20 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
"default_duration": 0,
"default_token_lifetime": 0,
"disable_expiry_refresh": true,
+ "max_admin_token_lifetime": 0,
"max_token_lifetime": 0
}
```
### Properties
-| Name | Type | Required | Restrictions | Description |
-|--------------------------|---------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `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 | | |
+| Name | Type | Required | Restrictions | Description |
+|----------------------------|---------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `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_admin_token_lifetime` | integer | false | | |
+| `max_token_lifetime` | integer | false | | |
## codersdk.SlimRole
diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md
index 1b4052e335..8b47ac00db 100644
--- a/docs/reference/cli/server.md
+++ b/docs/reference/cli/server.md
@@ -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.
+### --max-admin-token-lifetime
+
+| | |
+|-------------|----------------------------------------------------|
+| Type | duration
|
+| Environment | $CODER_MAX_ADMIN_TOKEN_LIFETIME
|
+| YAML | networking.http.maxAdminTokenLifetime
|
+| Default | 168h0m0s
|
+
+The maximum lifetime duration administrators can specify when creating an API token.
+
### --default-token-lifetime
| | |
diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden
index d11304742d..edacc0c43f 100644
--- a/enterprise/cli/testdata/coder_server_--help.golden
+++ b/enterprise/cli/testdata/coder_server_--help.golden
@@ -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.
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 9fa6e45fa3..c662b27386 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -2519,6 +2519,7 @@ export interface SessionLifetime {
readonly default_duration: number;
readonly default_token_lifetime?: number;
readonly max_token_lifetime?: number;
+ readonly max_admin_token_lifetime?: number;
}
// From codersdk/client.go