Skip to content

Commit 9f15ef9

Browse files
committed
feat(api): add max admin token lifetime configuration and validation
Change-Id: I4540ce3eeb46ab58909ac37e60c3ece93668212a Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent d47a53d commit 9f15ef9

File tree

12 files changed

+166
-13
lines changed

12 files changed

+166
-13
lines changed

cli/testdata/coder_server_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ NETWORKING / HTTP OPTIONS:
332332
The maximum lifetime duration users can specify when creating an API
333333
token.
334334

335+
--max-admin-token-lifetime duration, $CODER_MAX_ADMIN_TOKEN_LIFETIME (default: 168h0m0s)
336+
The maximum lifetime duration administrators can specify when creating
337+
an API token.
338+
335339
--proxy-health-interval duration, $CODER_PROXY_HEALTH_INTERVAL (default: 1m0s)
336340
The interval in which coderd should be checking the status of
337341
workspace proxies.

cli/testdata/server-config.yaml.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ networking:
2525
# The maximum lifetime duration users can specify when creating an API token.
2626
# (default: 876600h0m0s, type: duration)
2727
maxTokenLifetime: 876600h0m0s
28+
# The maximum lifetime duration administrators can specify when creating an API
29+
# token.
30+
# (default: 168h0m0s, type: duration)
31+
maxAdminTokenLifetime: 168h0m0s
2832
# The token expiry duration for browser sessions. Sessions may last longer if they
2933
# are actively making requests, but this functionality can be disabled via
3034
# --disable-session-expiry-refresh.

coderd/apidoc/docs.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apikey.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/coder/coder/v2/coderd/database/dbtime"
1919
"github.com/coder/coder/v2/coderd/httpapi"
2020
"github.com/coder/coder/v2/coderd/httpmw"
21+
"github.com/coder/coder/v2/coderd/rbac"
2122
"github.com/coder/coder/v2/coderd/rbac/policy"
2223
"github.com/coder/coder/v2/coderd/telemetry"
2324
"github.com/coder/coder/v2/codersdk"
@@ -75,7 +76,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
7576
}
7677

7778
if createToken.Lifetime != 0 {
78-
err := api.validateAPIKeyLifetime(createToken.Lifetime)
79+
err := api.validateAPIKeyLifetime(ctx, user.ID, createToken.Lifetime)
7980
if err != nil {
8081
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
8182
Message: "Failed to validate create API key request.",
@@ -338,35 +339,69 @@ func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) {
338339
// @Success 200 {object} codersdk.TokenConfig
339340
// @Router /users/{user}/keys/tokens/tokenconfig [get]
340341
func (api *API) tokenConfig(rw http.ResponseWriter, r *http.Request) {
341-
values, err := api.DeploymentValues.WithoutSecrets()
342+
user := httpmw.UserParam(r)
343+
maxLifetime, err := api.getMaxTokenLifetime(r.Context(), user.ID)
342344
if err != nil {
343-
httpapi.InternalServerError(rw, err)
345+
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
346+
Message: "Failed to get token configuration.",
347+
Detail: err.Error(),
348+
})
344349
return
345350
}
346351

347352
httpapi.Write(
348353
r.Context(), rw, http.StatusOK,
349354
codersdk.TokenConfig{
350-
MaxTokenLifetime: values.Sessions.MaximumTokenDuration.Value(),
355+
MaxTokenLifetime: maxLifetime,
351356
},
352357
)
353358
}
354359

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

360-
if lifetime > api.DeploymentValues.Sessions.MaximumTokenDuration.Value() {
365+
maxLifetime, err := api.getMaxTokenLifetime(ctx, userID)
366+
if err != nil {
367+
return xerrors.Errorf("failed to get max token lifetime: %w", err)
368+
}
369+
370+
if lifetime > maxLifetime {
361371
return xerrors.Errorf(
362372
"lifetime must be less than %v",
363-
api.DeploymentValues.Sessions.MaximumTokenDuration,
373+
maxLifetime,
364374
)
365375
}
366376

367377
return nil
368378
}
369379

380+
// getMaxTokenLifetime returns the maximum allowed token lifetime for a user.
381+
// It distinguishes between regular users and owners.
382+
func (api *API) getMaxTokenLifetime(ctx context.Context, userID uuid.UUID) (time.Duration, error) {
383+
subject, _, err := httpmw.UserRBACSubject(ctx, api.Database, userID, rbac.ScopeAll)
384+
if err != nil {
385+
return 0, xerrors.Errorf("failed to get user rbac subject: %w", err)
386+
}
387+
388+
roles, err := subject.Roles.Expand()
389+
if err != nil {
390+
return 0, xerrors.Errorf("failed to expand user roles: %w", err)
391+
}
392+
393+
maxLifetime := api.DeploymentValues.Sessions.MaximumTokenDuration.Value()
394+
for _, role := range roles {
395+
if role.Identifier.Name == codersdk.RoleOwner {
396+
// Owners have a different max lifetime.
397+
maxLifetime = api.DeploymentValues.Sessions.MaximumAdminTokenDuration.Value()
398+
break
399+
}
400+
}
401+
402+
return maxLifetime, nil
403+
}
404+
370405
func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (*http.Cookie, *database.APIKey, error) {
371406
key, sessionToken, err := apikey.Generate(params)
372407
if err != nil {

coderd/apikey_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,76 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
144144
require.ErrorContains(t, err, "lifetime must be less")
145145
}
146146

147+
func TestTokenAdminSetMaxLifetime(t *testing.T) {
148+
t.Parallel()
149+
150+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
151+
defer cancel()
152+
dc := coderdtest.DeploymentValues(t)
153+
dc.Sessions.MaximumTokenDuration = serpent.Duration(time.Hour * 24 * 7)
154+
dc.Sessions.MaximumAdminTokenDuration = serpent.Duration(time.Hour * 24 * 14)
155+
client := coderdtest.New(t, &coderdtest.Options{
156+
DeploymentValues: dc,
157+
})
158+
adminUser := coderdtest.CreateFirstUser(t, client)
159+
nonAdminClient, _ := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)
160+
161+
// Admin should be able to create a token with a lifetime longer than the non-admin max.
162+
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
163+
Lifetime: time.Hour * 24 * 10,
164+
})
165+
require.NoError(t, err)
166+
167+
// Admin should NOT be able to create a token with a lifetime longer than the admin max.
168+
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
169+
Lifetime: time.Hour * 24 * 15,
170+
})
171+
require.Error(t, err)
172+
require.Contains(t, err.Error(), "lifetime must be less")
173+
174+
// Non-admin should NOT be able to create a token with a lifetime longer than the non-admin max.
175+
_, err = nonAdminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
176+
Lifetime: time.Hour * 24 * 8,
177+
})
178+
require.Error(t, err)
179+
require.Contains(t, err.Error(), "lifetime must be less")
180+
}
181+
182+
func TestTokenAdminSetMaxLifetimeShorter(t *testing.T) {
183+
t.Parallel()
184+
185+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
186+
defer cancel()
187+
dc := coderdtest.DeploymentValues(t)
188+
dc.Sessions.MaximumTokenDuration = serpent.Duration(time.Hour * 24 * 14)
189+
dc.Sessions.MaximumAdminTokenDuration = serpent.Duration(time.Hour * 24 * 7)
190+
client := coderdtest.New(t, &coderdtest.Options{
191+
DeploymentValues: dc,
192+
})
193+
adminUser := coderdtest.CreateFirstUser(t, client)
194+
nonAdminClient, _ := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)
195+
196+
// Admin should NOT be able to create a token with a lifetime longer than the admin max.
197+
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
198+
Lifetime: time.Hour * 24 * 8,
199+
})
200+
require.Error(t, err)
201+
require.Contains(t, err.Error(), "lifetime must be less")
202+
203+
// Non-admin should be able to create a token with a lifetime longer than the admin max.
204+
_, err = nonAdminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
205+
Lifetime: time.Hour * 24 * 10,
206+
})
207+
require.NoError(t, err)
208+
209+
// Non-admin should NOT be able to create a token with a lifetime longer than the non-admin max.
210+
_, err = nonAdminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
211+
Lifetime: time.Hour * 24 * 15,
212+
})
213+
require.Error(t, err)
214+
require.Contains(t, err.Error(), "lifetime must be less")
215+
}
216+
147217
func TestTokenCustomDefaultLifetime(t *testing.T) {
148218
t.Parallel()
149219

codersdk/deployment.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,8 @@ type SessionLifetime struct {
468468
DefaultTokenDuration serpent.Duration `json:"default_token_lifetime,omitempty" typescript:",notnull"`
469469

470470
MaximumTokenDuration serpent.Duration `json:"max_token_lifetime,omitempty" typescript:",notnull"`
471+
472+
MaximumAdminTokenDuration serpent.Duration `json:"max_admin_token_lifetime,omitempty" typescript:",notnull"`
471473
}
472474

473475
type DERP struct {
@@ -2340,6 +2342,17 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
23402342
YAML: "maxTokenLifetime",
23412343
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
23422344
},
2345+
{
2346+
Name: "Maximum Admin Token Lifetime",
2347+
Description: "The maximum lifetime duration administrators can specify when creating an API token.",
2348+
Flag: "max-admin-token-lifetime",
2349+
Env: "CODER_MAX_ADMIN_TOKEN_LIFETIME",
2350+
Default: (7 * 24 * time.Hour).String(),
2351+
Value: &c.Sessions.MaximumAdminTokenDuration,
2352+
Group: &deploymentGroupNetworkingHTTP,
2353+
YAML: "maxAdminTokenLifetime",
2354+
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
2355+
},
23432356
{
23442357
Name: "Default Token Lifetime",
23452358
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.",

docs/reference/api/general.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/api/schemas.md

Lines changed: 10 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/cli/server.md

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/cli/testdata/coder_server_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,10 @@ NETWORKING / HTTP OPTIONS:
333333
The maximum lifetime duration users can specify when creating an API
334334
token.
335335

336+
--max-admin-token-lifetime duration, $CODER_MAX_ADMIN_TOKEN_LIFETIME (default: 168h0m0s)
337+
The maximum lifetime duration administrators can specify when creating
338+
an API token.
339+
336340
--proxy-health-interval duration, $CODER_PROXY_HEALTH_INTERVAL (default: 1m0s)
337341
The interval in which coderd should be checking the status of
338342
workspace proxies.

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)