Skip to content
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
6 changes: 3 additions & 3 deletions cli/templateedit.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ func (r *RootCmd) templateEdit() *serpent.Command {

req := codersdk.UpdateTemplateMeta{
Name: name,
DisplayName: displayName,
Description: description,
Icon: icon,
DisplayName: &displayName,
Description: &description,
Icon: &icon,
DefaultTTLMillis: defaultTTL.Milliseconds(),
ActivityBumpMillis: activityBump.Milliseconds(),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
Expand Down
16 changes: 10 additions & 6 deletions coderd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,12 +771,16 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
classicTemplateFlow = *req.UseClassicParameterFlow
}

displayName := ptr.NilToDefault(req.DisplayName, template.DisplayName)
description := ptr.NilToDefault(req.Description, template.Description)
icon := ptr.NilToDefault(req.Icon, template.Icon)

var updated database.Template
err = api.Database.InTx(func(tx database.Store) error {
if req.Name == template.Name &&
req.Description == template.Description &&
req.DisplayName == template.DisplayName &&
req.Icon == template.Icon &&
description == template.Description &&
displayName == template.DisplayName &&
icon == template.Icon &&
req.AllowUserAutostart == template.AllowUserAutostart &&
req.AllowUserAutostop == template.AllowUserAutostop &&
req.AllowUserCancelWorkspaceJobs == template.AllowUserCancelWorkspaceJobs &&
Expand Down Expand Up @@ -827,9 +831,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
ID: template.ID,
UpdatedAt: dbtime.Now(),
Name: name,
DisplayName: req.DisplayName,
Description: req.Description,
Icon: req.Icon,
DisplayName: displayName,
Description: description,
Icon: icon,
AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
GroupACL: groupACL,
MaxPortSharingLevel: maxPortShareLevel,
Expand Down
166 changes: 133 additions & 33 deletions coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -901,9 +901,9 @@ func TestPatchTemplateMeta(t *testing.T) {

req := codersdk.UpdateTemplateMeta{
Name: "new-template-name",
DisplayName: "Displayed Name 456",
Description: "lorem ipsum dolor sit amet et cetera",
Icon: "/icon/new-icon.png",
DisplayName: ptr.Ref("Displayed Name 456"),
Description: ptr.Ref("lorem ipsum dolor sit amet et cetera"),
Icon: ptr.Ref("/icon/new-icon.png"),
DefaultTTLMillis: 12 * time.Hour.Milliseconds(),
ActivityBumpMillis: 3 * time.Hour.Milliseconds(),
AllowUserCancelWorkspaceJobs: false,
Expand All @@ -918,9 +918,9 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, req.Name, updated.Name)
assert.Equal(t, req.DisplayName, updated.DisplayName)
assert.Equal(t, req.Description, updated.Description)
assert.Equal(t, req.Icon, updated.Icon)
assert.Equal(t, *req.DisplayName, updated.DisplayName)
assert.Equal(t, *req.Description, updated.Description)
assert.Equal(t, *req.Icon, updated.Icon)
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis)
assert.False(t, req.AllowUserCancelWorkspaceJobs)
Expand All @@ -930,9 +930,9 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, req.Name, updated.Name)
assert.Equal(t, req.DisplayName, updated.DisplayName)
assert.Equal(t, req.Description, updated.Description)
assert.Equal(t, req.Icon, updated.Icon)
assert.Equal(t, *req.DisplayName, updated.DisplayName)
assert.Equal(t, *req.Description, updated.Description)
assert.Equal(t, *req.Icon, updated.Icon)
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis)
assert.False(t, req.AllowUserCancelWorkspaceJobs)
Expand Down Expand Up @@ -1167,9 +1167,9 @@ func TestPatchTemplateMeta(t *testing.T) {

got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: template.DisplayName,
Description: template.Description,
Icon: template.Icon,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: 0,
AutostopRequirement: &template.AutostopRequirement,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
Expand Down Expand Up @@ -1202,9 +1202,9 @@ func TestPatchTemplateMeta(t *testing.T) {

got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: template.DisplayName,
Description: template.Description,
Icon: template.Icon,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: template.DefaultTTLMillis,
AutostopRequirement: &template.AutostopRequirement,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
Expand Down Expand Up @@ -1263,9 +1263,9 @@ func TestPatchTemplateMeta(t *testing.T) {
allowAutostop.Store(false)
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: template.DisplayName,
Description: template.Description,
Icon: template.Icon,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: template.DefaultTTLMillis,
AutostopRequirement: &template.AutostopRequirement,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
Expand Down Expand Up @@ -1294,9 +1294,9 @@ func TestPatchTemplateMeta(t *testing.T) {

got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: template.DisplayName,
Description: template.Description,
Icon: template.Icon,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
// Increase the default TTL to avoid error "not modified".
DefaultTTLMillis: template.DefaultTTLMillis + 1,
AutostopRequirement: &template.AutostopRequirement,
Expand Down Expand Up @@ -1326,8 +1326,8 @@ func TestPatchTemplateMeta(t *testing.T) {

req := codersdk.UpdateTemplateMeta{
Name: template.Name,
Description: template.Description,
Icon: template.Icon,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: template.DefaultTTLMillis,
ActivityBumpMillis: template.ActivityBumpMillis,
AutostopRequirement: nil,
Expand Down Expand Up @@ -1387,7 +1387,7 @@ func TestPatchTemplateMeta(t *testing.T) {
ctr.Icon = "/icon/code.png"
})
req := codersdk.UpdateTemplateMeta{
Icon: "",
Icon: ptr.Ref(""),
}

ctx := testutil.Context(t, testutil.WaitLong)
Expand Down Expand Up @@ -1442,9 +1442,9 @@ func TestPatchTemplateMeta(t *testing.T) {
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
req := codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: template.DisplayName,
Description: template.Description,
Icon: template.Icon,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: time.Hour.Milliseconds(),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
Expand Down Expand Up @@ -1519,9 +1519,9 @@ func TestPatchTemplateMeta(t *testing.T) {
require.EqualValues(t, 2, template.AutostopRequirement.Weeks)
req := codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: template.DisplayName,
Description: template.Description,
Icon: template.Icon,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: time.Hour.Milliseconds(),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
Expand Down Expand Up @@ -1556,9 +1556,9 @@ func TestPatchTemplateMeta(t *testing.T) {
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
req := codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: template.DisplayName,
Description: template.Description,
Icon: template.Icon,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: time.Hour.Milliseconds(),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
Expand Down Expand Up @@ -1618,6 +1618,106 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.False(t, updated.UseClassicParameterFlow, "expected false")
})

t.Run("SupportEmptyOrDefaultFields", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)

displayName := "Test Display Name"
description := "test-description"
icon := "/icon/icon.png"
defaultTTLMillis := 10 * time.Hour.Milliseconds()

reference := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DisplayName = displayName
ctr.Description = description
ctr.Icon = icon
ctr.DefaultTTLMillis = ptr.Ref(defaultTTLMillis)
})
require.Equal(t, displayName, reference.DisplayName)
require.Equal(t, description, reference.Description)
require.Equal(t, icon, reference.Icon)

restoreReq := codersdk.UpdateTemplateMeta{
DisplayName: &displayName,
Description: &description,
Icon: &icon,
DefaultTTLMillis: defaultTTLMillis,
}

type expected struct {
displayName string
description string
icon string
defaultTTLMillis int64
}

type testCase struct {
name string
req codersdk.UpdateTemplateMeta
expected expected
}

tests := []testCase{
{
name: "Only update default_ttl_ms",
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: 99 * time.Hour.Milliseconds()},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 99 * time.Hour.Milliseconds()},
},
{
name: "Clear display name",
req: codersdk.UpdateTemplateMeta{DisplayName: ptr.Ref("")},
expected: expected{displayName: "", description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0},
},
{
name: "Clear description",
req: codersdk.UpdateTemplateMeta{Description: ptr.Ref("")},
expected: expected{displayName: reference.DisplayName, description: "", icon: reference.Icon, defaultTTLMillis: 0},
},
{
name: "Clear icon",
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("")},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: "", defaultTTLMillis: 0},
},
{
name: "Nil display name defaults to reference display name",
req: codersdk.UpdateTemplateMeta{DisplayName: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0},
},
{
name: "Nil description defaults to reference description",
req: codersdk.UpdateTemplateMeta{Description: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0},
},
{
name: "Nil icon defaults to reference icon",
req: codersdk.UpdateTemplateMeta{Icon: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0},
},
}

for _, tc := range tests {
//nolint:tparallel,paralleltest
t.Run(tc.name, func(t *testing.T) {
defer func() {
ctx := testutil.Context(t, testutil.WaitLong)
// Restore reference after each test case
_, err := client.UpdateTemplateMeta(ctx, reference.ID, restoreReq)
require.NoError(t, err)
}()
ctx := testutil.Context(t, testutil.WaitLong)
updated, err := client.UpdateTemplateMeta(ctx, reference.ID, tc.req)
require.NoError(t, err)
assert.Equal(t, tc.expected.displayName, updated.DisplayName)
assert.Equal(t, tc.expected.description, updated.Description)
assert.Equal(t, tc.expected.icon, updated.Icon)
assert.Equal(t, tc.expected.defaultTTLMillis, updated.DefaultTTLMillis)
})
}
})
}

func TestDeleteTemplate(t *testing.T) {
Expand Down
10 changes: 5 additions & 5 deletions codersdk/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,11 @@ type ACLAvailable struct {
}

type UpdateTemplateMeta struct {
Name string `json:"name,omitempty" validate:"omitempty,template_name"`
DisplayName string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
Description string `json:"description,omitempty"`
Icon string `json:"icon,omitempty"`
DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"`
Name string `json:"name,omitempty" validate:"omitempty,template_name"`
DisplayName *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"`
// ActivityBumpMillis allows optionally specifying the activity bump
// duration for all workspaces created from this template. Defaults to 1h
// but can be set to 0 to disable activity bumping.
Expand Down
2 changes: 1 addition & 1 deletion docs/admin/templates/extending-templates/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ parameters in one of two ways:

Or set the [environment variable](../../setup/index.md), `CODER_EXPERIMENTS=auto-fill-parameters`

## Dynamic Parameters (beta)
## Dynamic Parameters

Coder v2.24.0 introduces [Dynamic Parameters](./dynamic-parameters.md) to extend the existing parameter system with
conditional form controls, enriched input types, and user identity awareness.
Expand Down
62 changes: 62 additions & 0 deletions docs/admin/users/oidc-auth/google.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Google authentication (OIDC)

This guide shows how to configure Coder to authenticate users with Google using OpenID Connect (OIDC).

## Prerequisites

- A Google Cloud project with the OAuth consent screen configured
- Permission to create OAuth 2.0 Client IDs in Google Cloud

## Step 1: Create an OAuth client in Google Cloud

1. Open Google Cloud Console → APIs & Services → Credentials → Create Credentials → OAuth client ID.
2. Application type: Web application.
3. Authorized redirect URIs: add your Coder callback URL:
- `https://coder.example.com/api/v2/users/oidc/callback`
4. Save and note the Client ID and Client secret.

## Step 2: Configure Coder OIDC for Google

Set the following environment variables on your Coder deployment and restart Coder:

```env
CODER_OIDC_ISSUER_URL=https://accounts.google.com
CODER_OIDC_CLIENT_ID=<client id>
CODER_OIDC_CLIENT_SECRET=<client secret>
# Restrict to one or more email domains (comma-separated)
CODER_OIDC_EMAIL_DOMAIN="example.com"
# Standard OIDC scopes for Google
CODER_OIDC_SCOPES=openid,profile,email
# Optional: customize the login button
CODER_OIDC_SIGN_IN_TEXT="Sign in with Google"
CODER_OIDC_ICON_URL=/icon/google.svg
```

> [!NOTE]
> The redirect URI must exactly match what you configured in Google Cloud.

## Enable refresh tokens (recommended)

Google uses auth URL parameters to issue refresh tokens. Configure:

```env
# Keep standard scopes
CODER_OIDC_SCOPES=openid,profile,email
# Add Google-specific auth URL params
CODER_OIDC_AUTH_URL_PARAMS='{"access_type": "offline", "prompt": "consent"}'
```

After changing settings, users must log out and back in once to obtain refresh tokens.

Learn more in [Configure OIDC refresh tokens](./refresh-tokens.md).

## Troubleshooting

- "invalid redirect_uri": ensure the redirect URI in Google Cloud matches `https://<your-coder-host>/api/v2/users/oidc/callback`.
- Domain restriction: if users from unexpected domains can log in, verify `CODER_OIDC_EMAIL_DOMAIN`.
- Claims: to inspect claims returned by Google, see guidance in the [OIDC overview](./index.md#oidc-claims).

## See also

- [OIDC overview](./index.md)
- [Configure OIDC refresh tokens](./refresh-tokens.md)
Loading
Loading