diff --git a/cli/templatecreate.go b/cli/templatecreate.go
index a49997c3315ab..eb23b10fdb30f 100644
--- a/cli/templatecreate.go
+++ b/cli/templatecreate.go
@@ -28,6 +28,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
provisionerTags []string
variablesFile string
variables []string
+ disableEveryone bool
defaultTTL time.Duration
failureTTL time.Duration
inactivityTTL time.Duration
@@ -121,11 +122,12 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
}
createReq := codersdk.CreateTemplateRequest{
- Name: templateName,
- VersionID: job.ID,
- DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
- FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
- InactivityTTLMillis: ptr.Ref(inactivityTTL.Milliseconds()),
+ Name: templateName,
+ VersionID: job.ID,
+ DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
+ FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
+ InactivityTTLMillis: ptr.Ref(inactivityTTL.Milliseconds()),
+ DisableEveryoneGroupAccess: disableEveryone,
}
_, err = client.CreateTemplate(inv.Context(), organization.ID, createReq)
@@ -144,6 +146,12 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
+ {
+ Flag: "private",
+ Description: "Disable the default behavior of granting template access to the 'everyone' group. " +
+ "The template permissions must be updated to allow non-admin users to use this template.",
+ Value: clibase.BoolOf(&disableEveryone),
+ },
{
Flag: "variables-file",
Description: "Specify a file path with values for Terraform-managed variables.",
diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden
index 4695d5dbfe6bb..cf6e5c9e3a40b 100644
--- a/cli/testdata/coder_templates_create_--help.golden
+++ b/cli/testdata/coder_templates_create_--help.golden
@@ -17,6 +17,11 @@ Create a template from the current directory or as specified by flag
Specify an inactivity TTL for workspaces created from this template.
This licensed feature's default is 0h (off).
+ --private bool
+ Disable the default behavior of granting template access to the
+ 'everyone' group. The template permissions must be updated to allow
+ non-admin users to use this template.
+
--provisioner-tag string-array
Specify a set of tags to target provisioner daemons.
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index c29243d6c6e1a..38414682caa06 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -6664,6 +6664,10 @@ const docTemplate = `{
"description": "Description is a description of what the template contains. It must be\nless than 128 bytes.",
"type": "string"
},
+ "disable_everyone_group_access": {
+ "description": "DisableEveryoneGroupAccess allows optionally disabling the default\nbehavior of granting the 'everyone' group access to use the template.\nIf this is set to true, the template will not be available to all users,\nand must be explicitly granted to users or groups in the permissions settings\nof the template.",
+ "type": "boolean"
+ },
"display_name": {
"description": "DisplayName is the displayed name of the template.",
"type": "string"
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 38f72a070b17b..8b4e09a041993 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -5935,6 +5935,10 @@
"description": "Description is a description of what the template contains. It must be\nless than 128 bytes.",
"type": "string"
},
+ "disable_everyone_group_access": {
+ "description": "DisableEveryoneGroupAccess allows optionally disabling the default\nbehavior of granting the 'everyone' group access to use the template.\nIf this is set to true, the template will not be available to all users,\nand must be explicitly granted to users or groups in the permissions settings\nof the template.",
+ "type": "boolean"
+ },
"display_name": {
"description": "DisplayName is the displayed name of the template.",
"type": "string"
diff --git a/coderd/audit/audit.go b/coderd/audit/audit.go
index cf941611500c8..d3e83d19e85fb 100644
--- a/coderd/audit/audit.go
+++ b/coderd/audit/audit.go
@@ -42,6 +42,14 @@ type MockAuditor struct {
auditLogs []database.AuditLog
}
+// ResetLogs removes all audit logs from the mock auditor.
+// This is helpful for testing to get a clean slate.
+func (a *MockAuditor) ResetLogs() {
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+ a.auditLogs = make([]database.AuditLog, 0)
+}
+
func (a *MockAuditor) AuditLogs() []database.AuditLog {
a.mutex.Lock()
defer a.mutex.Unlock()
diff --git a/coderd/templates.go b/coderd/templates.go
index e54c0b3ddb8a7..9aefb174172ca 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -274,22 +274,26 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
allowUserAutostop = ptr.NilToDefault(createTemplate.AllowUserAutostop, true)
)
+ defaultsGroups := database.TemplateACL{}
+ if !createTemplate.DisableEveryoneGroupAccess {
+ // The organization ID is used as the group ID for the everyone group
+ // in this organization.
+ defaultsGroups[organization.ID.String()] = []rbac.Action{rbac.ActionRead}
+ }
err = api.Database.InTx(func(tx database.Store) error {
now := database.Now()
dbTemplate, err = tx.InsertTemplate(ctx, database.InsertTemplateParams{
- ID: uuid.New(),
- CreatedAt: now,
- UpdatedAt: now,
- OrganizationID: organization.ID,
- Name: createTemplate.Name,
- Provisioner: importJob.Provisioner,
- ActiveVersionID: templateVersion.ID,
- Description: createTemplate.Description,
- CreatedBy: apiKey.UserID,
- UserACL: database.TemplateACL{},
- GroupACL: database.TemplateACL{
- organization.ID.String(): []rbac.Action{rbac.ActionRead},
- },
+ ID: uuid.New(),
+ CreatedAt: now,
+ UpdatedAt: now,
+ OrganizationID: organization.ID,
+ Name: createTemplate.Name,
+ Provisioner: importJob.Provisioner,
+ ActiveVersionID: templateVersion.ID,
+ Description: createTemplate.Description,
+ CreatedBy: apiKey.UserID,
+ UserACL: database.TemplateACL{},
+ GroupACL: defaultsGroups,
DisplayName: createTemplate.DisplayName,
Icon: createTemplate.Icon,
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index 03a3556f56e1a..fbfdfd6786c3b 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -48,25 +48,28 @@ func TestPostTemplateByOrganization(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
- user := coderdtest.CreateFirstUser(t, client)
- version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ owner := coderdtest.CreateFirstUser(t, client)
+ // By default, everyone in the org can read the template.
+ user, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
+ auditor.ResetLogs()
+
+ version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
- expected := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ expected := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
- got, err := client.Template(ctx, expected.ID)
+ got, err := user.Template(ctx, expected.ID)
require.NoError(t, err)
assert.Equal(t, expected.Name, got.Name)
assert.Equal(t, expected.Description, got.Description)
- require.Len(t, auditor.AuditLogs(), 4)
- assert.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[0].Action)
- assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[1].Action)
- assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[2].Action)
- assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[3].Action)
+ require.Len(t, auditor.AuditLogs(), 3)
+ assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[0].Action)
+ assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[1].Action)
+ assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[2].Action)
})
t.Run("AlreadyExists", func(t *testing.T) {
@@ -126,6 +129,27 @@ func TestPostTemplateByOrganization(t *testing.T) {
require.Zero(t, got.DefaultTTLMillis)
})
+ t.Run("DisableEveryone", func(t *testing.T) {
+ t.Parallel()
+ auditor := audit.NewMock()
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
+ owner := coderdtest.CreateFirstUser(t, client)
+ user, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
+ version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
+
+ expected := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
+ request.DisableEveryoneGroupAccess = true
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ _, err := user.Template(ctx, expected.ID)
+ var apiErr *codersdk.Error
+ require.ErrorAs(t, err, &apiErr)
+ require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
+ })
+
t.Run("Unauthorized", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
diff --git a/codersdk/organizations.go b/codersdk/organizations.go
index 1eb2aab973cb3..19af059277ea0 100644
--- a/codersdk/organizations.go
+++ b/codersdk/organizations.go
@@ -108,6 +108,13 @@ type CreateTemplateRequest struct {
// InactivityTTLMillis allows optionally specifying the max lifetime before Coder
// deletes inactive workspaces created from this template.
InactivityTTLMillis *int64 `json:"inactivity_ttl_ms,omitempty"`
+
+ // DisableEveryoneGroupAccess allows optionally disabling the default
+ // behavior of granting the 'everyone' group access to use the template.
+ // If this is set to true, the template will not be available to all users,
+ // and must be explicitly granted to users or groups in the permissions settings
+ // of the template.
+ DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"`
}
// CreateWorkspaceRequest provides options for creating a new workspace.
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index e4fd03c5cd0b7..2a15b0706d42a 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -1343,6 +1343,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"allow_user_cancel_workspace_jobs": true,
"default_ttl_ms": 0,
"description": "string",
+ "disable_everyone_group_access": true,
"display_name": "string",
"failure_ttl_ms": 0,
"icon": "string",
@@ -1355,20 +1356,21 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `allow_user_autostart` | boolean | false | | Allow user autostart allows users to set a schedule for autostarting their workspace. By default this is true. This can only be disabled when using an enterprise license. |
-| `allow_user_autostop` | boolean | false | | Allow user autostop allows users to set a custom workspace TTL to use in place of the template's DefaultTTL field. By default this is true. If false, the DefaultTTL will always be used. This can only be disabled when using an enterprise license. |
-| `allow_user_cancel_workspace_jobs` | boolean | false | | Allow users to cancel in-progress workspace jobs. \*bool as the default value is "true". |
-| `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. |
-| `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. |
-| `display_name` | string | false | | Display name is the displayed name of the template. |
-| `failure_ttl_ms` | integer | false | | Failure ttl ms allows optionally specifying the max lifetime before Coder stops all resources for failed workspaces created from this template. |
-| `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. |
-| `inactivity_ttl_ms` | integer | false | | Inactivity ttl ms allows optionally specifying the max lifetime before Coder deletes inactive workspaces created from this template. |
-| `max_ttl_ms` | integer | false | | Max ttl ms allows optionally specifying the max lifetime for workspaces created from this template. |
-| `name` | string | true | | Name is the name of the template. |
-| `template_version_id` | string | true | | Template version ID is an in-progress or completed job to use as an initial version of the template. |
+| Name | Type | Required | Restrictions | Description |
+| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `allow_user_autostart` | boolean | false | | Allow user autostart allows users to set a schedule for autostarting their workspace. By default this is true. This can only be disabled when using an enterprise license. |
+| `allow_user_autostop` | boolean | false | | Allow user autostop allows users to set a custom workspace TTL to use in place of the template's DefaultTTL field. By default this is true. If false, the DefaultTTL will always be used. This can only be disabled when using an enterprise license. |
+| `allow_user_cancel_workspace_jobs` | boolean | false | | Allow users to cancel in-progress workspace jobs. \*bool as the default value is "true". |
+| `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. |
+| `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. |
+| `disable_everyone_group_access` | boolean | false | | Disable everyone group access allows optionally disabling the default behavior of granting the 'everyone' group access to use the template. If this is set to true, the template will not be available to all users, and must be explicitly granted to users or groups in the permissions settings of the template. |
+| `display_name` | string | false | | Display name is the displayed name of the template. |
+| `failure_ttl_ms` | integer | false | | Failure ttl ms allows optionally specifying the max lifetime before Coder stops all resources for failed workspaces created from this template. |
+| `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. |
+| `inactivity_ttl_ms` | integer | false | | Inactivity ttl ms allows optionally specifying the max lifetime before Coder deletes inactive workspaces created from this template. |
+| `max_ttl_ms` | integer | false | | Max ttl ms allows optionally specifying the max lifetime for workspaces created from this template. |
+| `name` | string | true | | Name is the name of the template. |
+| `template_version_id` | string | true | | Template version ID is an in-progress or completed job to use as an initial version of the template. |
| This is required on creation to enable a user-flow of validating a template works. There is no reason the data-model cannot support empty templates, but it doesn't make sense for users. |
## codersdk.CreateTemplateVersionDryRunRequest
diff --git a/docs/api/templates.md b/docs/api/templates.md
index 50275ab168f90..b9e3033a83e73 100644
--- a/docs/api/templates.md
+++ b/docs/api/templates.md
@@ -129,6 +129,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
"allow_user_cancel_workspace_jobs": true,
"default_ttl_ms": 0,
"description": "string",
+ "disable_everyone_group_access": true,
"display_name": "string",
"failure_ttl_ms": 0,
"icon": "string",
diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md
index bec1eb4165074..123bd0bed9205 100644
--- a/docs/cli/templates_create.md
+++ b/docs/cli/templates_create.md
@@ -48,6 +48,14 @@ Specify a failure TTL for workspaces created from this template. This licensed f
Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+### --private
+
+| | |
+| ---- | ----------------- |
+| Type | bool |
+
+Disable the default behavior of granting template access to the 'everyone' group. The template permissions must be updated to allow non-admin users to use this template.
+
### --provisioner-tag
| | |
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index dea9dd74080ff..0af5c0c06a65e 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -179,6 +179,7 @@ export interface CreateTemplateRequest {
readonly allow_user_autostop?: boolean
readonly failure_ttl_ms?: number
readonly inactivity_ttl_ms?: number
+ readonly disable_everyone_group_access: boolean
}
// From codersdk/templateversions.go
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx
index 3a2a87bc65a70..22811b15cf874 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx
@@ -17,6 +17,7 @@ export default {
component: CreateTemplateForm,
args: {
isSubmitting: false,
+ allowDisableEveryoneAccess: true,
},
} as ComponentMeta
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
index cbaa48adadcb0..4b833164472a3 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
@@ -102,6 +102,7 @@ const defaultInitialValues: CreateTemplateData = {
allow_user_cancel_workspace_jobs: false,
allow_user_autostart: false,
allow_user_autostop: false,
+ allow_everyone_group_access: true,
}
type GetInitialValuesParams = {
@@ -174,6 +175,7 @@ export interface CreateTemplateFormProps {
logs?: ProvisionerJobLog[]
allowAdvancedScheduling: boolean
copiedTemplate?: Template
+ allowDisableEveryoneAccess: boolean
}
export const CreateTemplateForm: FC = ({
@@ -188,6 +190,7 @@ export const CreateTemplateForm: FC = ({
jobError,
logs,
allowAdvancedScheduling,
+ allowDisableEveryoneAccess,
}) => {
const styles = useStyles()
const form = useFormik({
@@ -379,44 +382,90 @@ export const CreateTemplateForm: FC = ({
- {/* Operations */}
+ {/* Permissions */}
-
-
-
+
+
+
+
+
+
+
+
+
+ Allow everyone to use the template
+
+
+
+ If unchecked, only users with the 'template
+ admin' and 'owner' role can use this
+ template until the permissions are updated. Navigate to{" "}
+
+ Templates > Select a template > Settings >
+ Permissions
+ {" "}
+ to update permissions.
+
+
+
+
+ This setting requires an enterprise license for the
+
+ 'Template RBAC'
+ {" "}
+ feature to customize permissions.
+
+
+
+
+
+
{/* Variables */}
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
index 7632d0b385047..b22b6e72fefe7 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
@@ -39,6 +39,10 @@ const CreateTemplatePage: FC = () => {
const { entitlements } = useDashboard()
const allowAdvancedScheduling =
entitlements.features["advanced_template_scheduling"].enabled
+ // Requires the template RBAC feature, otherwise disabling everyone access
+ // means no one can access.
+ const allowDisableEveryoneAccess =
+ entitlements.features["template_rbac"].enabled
const onCancel = () => {
navigate(-1)
@@ -64,6 +68,7 @@ const CreateTemplatePage: FC = () => {
user_variable_values?: VariableValue[]
+ allow_everyone_group_access: boolean
}
interface CreateTemplateContext {
organizationId: string
@@ -457,11 +458,13 @@ export const createTemplateMachine =
default_ttl_hours,
max_ttl_hours,
parameter_values_by_name,
+ allow_everyone_group_access,
...safeTemplateData
} = templateData
return createTemplate(organizationId, {
...safeTemplateData,
+ disable_everyone_group_access: !allow_everyone_group_access,
default_ttl_ms: templateData.default_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
max_ttl_ms: templateData.max_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
template_version_id: version.id,