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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy