From c5f1a2fccf9e4ac4432311f702b6b74dcb771f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kayla=20=E3=81=AF=E3=81=AA?= Date: Tue, 7 Apr 2026 14:25:32 -0400 Subject: [PATCH] feat: make service accounts a Premium feature (#24020) --- cli/usercreate_test.go | 1 + coderd/coderdtest/users.go | 21 ++- coderd/members_test.go | 2 +- coderd/users.go | 8 + coderd/users_test.go | 118 +------------ codersdk/deployment.go | 5 +- docs/admin/users/headless-auth.md | 23 ++- docs/manifest.json | 3 +- enterprise/coderd/groups_test.go | 6 +- enterprise/coderd/users_test.go | 164 ++++++++++++++++++ enterprise/coderd/workspacesharing_test.go | 11 +- site/src/api/typesGenerated.ts | 2 + .../CreateUserPage/CreateUserForm.stories.tsx | 1 + .../pages/CreateUserPage/CreateUserForm.tsx | 4 +- .../pages/CreateUserPage/CreateUserPage.tsx | 3 + 15 files changed, 238 insertions(+), 134 deletions(-) diff --git a/cli/usercreate_test.go b/cli/usercreate_test.go index 5f29f28970..2c8d69fe14 100644 --- a/cli/usercreate_test.go +++ b/cli/usercreate_test.go @@ -134,6 +134,7 @@ func TestUserCreate(t *testing.T) { { name: "ServiceAccount", args: []string{"--service-account", "-u", "dean"}, + err: "Premium feature", }, { name: "ServiceAccountLoginType", diff --git a/coderd/coderdtest/users.go b/coderd/coderdtest/users.go index f5a798b06e..b77dc5ab5d 100644 --- a/coderd/coderdtest/users.go +++ b/coderd/coderdtest/users.go @@ -123,6 +123,10 @@ func UsersPagination( require.Contains(t, gotUsers[0].Name, "after") } +type UsersFilterOptions struct { + CreateServiceAccounts bool +} + // UsersFilter creates a set of users to run various filters against for // testing. It can be used to test filtering both users and group members. func UsersFilter( @@ -130,11 +134,16 @@ func UsersFilter( t *testing.T, client *codersdk.Client, db database.Store, + options *UsersFilterOptions, setup func(users []codersdk.User), fetch func(ctx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser, ) { t.Helper() + if options == nil { + options = &UsersFilterOptions{} + } + firstUser, err := client.User(setupCtx, codersdk.Me) require.NoError(t, err, "fetch me") @@ -211,11 +220,13 @@ func UsersFilter( } // Add some service accounts. - for range 3 { - _, user := CreateAnotherUserMutators(t, client, orgID, nil, func(r *codersdk.CreateUserRequestWithOrgs) { - r.ServiceAccount = true - }) - users = append(users, user) + if options.CreateServiceAccounts { + for range 3 { + _, user := CreateAnotherUserMutators(t, client, orgID, nil, func(r *codersdk.CreateUserRequestWithOrgs) { + r.ServiceAccount = true + }) + users = append(users, user) + } } hashedPassword, err := userpassword.Hash("SomeStrongPassword!") diff --git a/coderd/members_test.go b/coderd/members_test.go index 0eee9d1db0..c2bf219c1e 100644 --- a/coderd/members_test.go +++ b/coderd/members_test.go @@ -148,7 +148,7 @@ func TestGetOrgMembersFilter(t *testing.T) { setupCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - coderdtest.UsersFilter(setupCtx, t, client, api.Database, nil, func(testCtx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser { + coderdtest.UsersFilter(setupCtx, t, client, api.Database, nil, nil, func(testCtx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser { res, err := client.OrganizationMembersPaginated(testCtx, first.OrganizationID, req) require.NoError(t, err) reduced := make([]codersdk.ReducedUser, len(res.Members)) diff --git a/coderd/users.go b/coderd/users.go index 07603e0b0e..dc883c8953 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -475,6 +475,14 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { } req.UserLoginType = codersdk.LoginTypeNone + + // Service accounts are a Premium feature. + if !api.Entitlements.Enabled(codersdk.FeatureServiceAccounts) { + httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + Message: fmt.Sprintf("%s is a Premium feature. Contact sales!", codersdk.FeatureServiceAccounts.Humanize()), + }) + return + } } else if req.UserLoginType == "" { // Default to password auth req.UserLoginType = codersdk.LoginTypePassword diff --git a/coderd/users_test.go b/coderd/users_test.go index c47b1a5293..9f6d297541 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -979,7 +979,7 @@ func TestPostUsers(t *testing.T) { require.Equal(t, found.LoginType, codersdk.LoginTypeOIDC) }) - t.Run("ServiceAccount/OK", func(t *testing.T) { + t.Run("ServiceAccount/Unlicensed", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) @@ -987,98 +987,16 @@ func TestPostUsers(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{first.OrganizationID}, Username: "service-acct-ok", UserLoginType: codersdk.LoginTypeNone, ServiceAccount: true, }) - require.NoError(t, err) - require.Equal(t, codersdk.LoginTypeNone, user.LoginType) - require.Empty(t, user.Email) - require.Equal(t, "service-acct-ok", user.Username) - require.Equal(t, codersdk.UserStatusDormant, user.Status) - }) - - t.Run("ServiceAccount/WithEmail", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - OrganizationIDs: []uuid.UUID{first.OrganizationID}, - Username: "service-acct-email", - Email: "should-not-have@email.com", - ServiceAccount: true, - }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - require.Contains(t, apiErr.Message, "Email cannot be set for service accounts") - }) - - t.Run("ServiceAccount/WithPassword", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - OrganizationIDs: []uuid.UUID{first.OrganizationID}, - Username: "service-acct-password", - Password: "ShouldNotHavePassword123!", - ServiceAccount: true, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - require.Contains(t, apiErr.Message, "Password cannot be set for service accounts") - }) - - t.Run("ServiceAccount/WithInvalidLoginType", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - OrganizationIDs: []uuid.UUID{first.OrganizationID}, - Username: "service-acct-login-type", - UserLoginType: codersdk.LoginTypePassword, - ServiceAccount: true, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - require.Contains(t, apiErr.Message, "Service accounts must use login type 'none'") - }) - - t.Run("ServiceAccount/DefaultLoginType", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - OrganizationIDs: []uuid.UUID{first.OrganizationID}, - Username: "service-acct-default-login", - ServiceAccount: true, - }) - require.NoError(t, err) - - found, err := client.User(ctx, user.ID.String()) - require.NoError(t, err) - require.Equal(t, codersdk.LoginTypeNone, found.LoginType) - require.Empty(t, found.Email) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) + require.Contains(t, apiErr.Message, "Premium feature") }) t.Run("NonServiceAccount/WithoutEmail", func(t *testing.T) { @@ -1098,32 +1016,6 @@ func TestPostUsers(t *testing.T) { require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) - - t.Run("ServiceAccount/MultipleWithoutEmail", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - user1, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - OrganizationIDs: []uuid.UUID{first.OrganizationID}, - Username: "service-acct-multi-1", - ServiceAccount: true, - }) - require.NoError(t, err) - require.Empty(t, user1.Email) - - user2, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - OrganizationIDs: []uuid.UUID{first.OrganizationID}, - Username: "service-acct-multi-2", - ServiceAccount: true, - }) - require.NoError(t, err) - require.Empty(t, user2.Email) - require.NotEqual(t, user1.ID, user2.ID) - }) } func TestNotifyCreatedUser(t *testing.T) { @@ -1832,7 +1724,7 @@ func TestGetUsersFilter(t *testing.T) { setupCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - coderdtest.UsersFilter(setupCtx, t, client, api.Database, nil, func(testCtx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser { + coderdtest.UsersFilter(setupCtx, t, client, api.Database, nil, nil, func(testCtx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser { res, err := client.Users(testCtx, req) require.NoError(t, err) reduced := make([]codersdk.ReducedUser, len(res.Users)) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index d277fba53f..1c975c19f3 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -196,6 +196,7 @@ const ( FeatureWorkspaceExternalAgent FeatureName = "workspace_external_agent" FeatureAIBridge FeatureName = "aibridge" FeatureBoundary FeatureName = "boundary" + FeatureServiceAccounts FeatureName = "service_accounts" FeatureAIGovernanceUserLimit FeatureName = "ai_governance_user_limit" ) @@ -227,6 +228,7 @@ var ( FeatureWorkspaceExternalAgent, FeatureAIBridge, FeatureBoundary, + FeatureServiceAccounts, FeatureAIGovernanceUserLimit, } @@ -275,6 +277,7 @@ func (n FeatureName) AlwaysEnable() bool { FeatureWorkspacePrebuilds: true, FeatureWorkspaceExternalAgent: true, FeatureBoundary: true, + FeatureServiceAccounts: true, }[n] } @@ -282,7 +285,7 @@ func (n FeatureName) AlwaysEnable() bool { func (n FeatureName) Enterprise() bool { switch n { // Add all features that should be excluded in the Enterprise feature set. - case FeatureMultipleOrganizations, FeatureCustomRoles: + case FeatureMultipleOrganizations, FeatureCustomRoles, FeatureServiceAccounts: return false default: return true diff --git a/docs/admin/users/headless-auth.md b/docs/admin/users/headless-auth.md index 6aa780288a..e61124b7e5 100644 --- a/docs/admin/users/headless-auth.md +++ b/docs/admin/users/headless-auth.md @@ -1,31 +1,38 @@ # Headless Authentication -Headless user accounts that cannot use the web UI to log in to Coder. This is -useful for creating accounts for automated systems, such as CI/CD pipelines or -for users who only consume Coder via another client/API. +> [!NOTE] +> Creating service accounts requires a [Premium license](https://coder.com/pricing). -You must have the User Admin role or above to create headless users. +Service accounts are headless user accounts that cannot use the web UI to log in +to Coder. This is useful for creating accounts for automated systems, such as +CI/CD pipelines or for users who only consume Coder via another client/API. Service accounts do not have passwords or associated email addresses. -## Create a headless user +You must have the User Admin role or above to create service accounts. + +## Create a service account
## CLI +Use the `--service-account` flag to create a dedicated service account: + ```sh coder users create \ - --email="coder-bot@coder.com" \ --username="coder-bot" \ - --login-type="none" \ + --service-account ``` ## UI -Navigate to the `Users` > `Create user` in the topbar +Navigate to **Deployment** > **Users** > **Create user**, then select +**Service account** as the login type. ![Create a user via the UI](../../images/admin/users/headless-user.png)
+## Authenticate as a service account + To make API or CLI requests on behalf of the headless user, learn how to [generate API tokens on behalf of a user](./sessions-tokens.md#generate-a-long-lived-api-token-on-behalf-of-another-user). diff --git a/docs/manifest.json b/docs/manifest.json index 30b238efbc..57d5ae6e8c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -495,7 +495,8 @@ { "title": "Headless Authentication", "description": "Create and manage headless service accounts for automated systems and API integrations", - "path": "./admin/users/headless-auth.md" + "path": "./admin/users/headless-auth.md", + "state": ["premium"] }, { "title": "Groups \u0026 Roles", diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index ac20c372f5..59335e91c5 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -1161,7 +1161,8 @@ func TestGetGroupMembersFilter(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureServiceAccounts: 1, }, }, }) @@ -1191,7 +1192,8 @@ func TestGetGroupMembersFilter(t *testing.T) { require.NoError(t, err) return res.Users } - coderdtest.UsersFilter(setupCtx, t, client, db, setup, fetch) + options := &coderdtest.UsersFilterOptions{CreateServiceAccounts: true} + coderdtest.UsersFilter(setupCtx, t, client, db, options, setup, fetch) } func TestGetGroupMembersPagination(t *testing.T) { diff --git a/enterprise/coderd/users_test.go b/enterprise/coderd/users_test.go index d6c8324a07..564065d259 100644 --- a/enterprise/coderd/users_test.go +++ b/enterprise/coderd/users_test.go @@ -614,4 +614,168 @@ func TestEnterprisePostUser(t *testing.T) { require.Len(t, memberedOrgs, 2) require.ElementsMatch(t, []uuid.UUID{second.ID, third.ID}, []uuid.UUID{memberedOrgs[0].ID, memberedOrgs[1].ID}) }) + + t.Run("ServiceAccount/OK", func(t *testing.T) { + t.Parallel() + client, first := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureServiceAccounts: 1, + }, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + //nolint:gocritic + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Username: "service-acct-ok", + UserLoginType: codersdk.LoginTypeNone, + ServiceAccount: true, + }) + require.NoError(t, err) + require.Equal(t, codersdk.LoginTypeNone, user.LoginType) + require.Empty(t, user.Email) + require.Equal(t, "service-acct-ok", user.Username) + require.Equal(t, codersdk.UserStatusDormant, user.Status) + }) + + t.Run("ServiceAccount/WithEmail", func(t *testing.T) { + t.Parallel() + client, first := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureServiceAccounts: 1, + }, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + //nolint:gocritic + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Username: "service-acct-email", + Email: "should-not-have@email.com", + ServiceAccount: true, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + require.Contains(t, apiErr.Message, "Email cannot be set for service accounts") + }) + + t.Run("ServiceAccount/WithPassword", func(t *testing.T) { + t.Parallel() + client, first := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureServiceAccounts: 1, + }, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + //nolint:gocritic + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Username: "service-acct-password", + Password: "ShouldNotHavePassword123!", + ServiceAccount: true, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + require.Contains(t, apiErr.Message, "Password cannot be set for service accounts") + }) + + t.Run("ServiceAccount/WithInvalidLoginType", func(t *testing.T) { + t.Parallel() + client, first := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureServiceAccounts: 1, + }, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + //nolint:gocritic + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Username: "service-acct-login-type", + UserLoginType: codersdk.LoginTypePassword, + ServiceAccount: true, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + require.Contains(t, apiErr.Message, "Service accounts must use login type 'none'") + }) + + t.Run("ServiceAccount/DefaultLoginType", func(t *testing.T) { + t.Parallel() + client, first := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureServiceAccounts: 1, + }, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + //nolint:gocritic + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Username: "service-acct-default-login", + ServiceAccount: true, + }) + require.NoError(t, err) + + found, err := client.User(ctx, user.ID.String()) + require.NoError(t, err) + require.Equal(t, codersdk.LoginTypeNone, found.LoginType) + require.Empty(t, found.Email) + }) + + t.Run("ServiceAccount/MultipleWithoutEmail", func(t *testing.T) { + t.Parallel() + client, first := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureServiceAccounts: 1, + }, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + //nolint:gocritic + user1, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Username: "service-acct-multi-1", + ServiceAccount: true, + }) + require.NoError(t, err) + require.Empty(t, user1.Email) + + user2, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Username: "service-acct-multi-2", + ServiceAccount: true, + }) + require.NoError(t, err) + require.Empty(t, user2.Email) + require.NotEqual(t, user1.ID, user2.ID) + }) } diff --git a/enterprise/coderd/workspacesharing_test.go b/enterprise/coderd/workspacesharing_test.go index ac37b58914..76f6fe1881 100644 --- a/enterprise/coderd/workspacesharing_test.go +++ b/enterprise/coderd/workspacesharing_test.go @@ -231,7 +231,13 @@ func TestWorkspaceSharingDisabled(t *testing.T) { t.Run("ACLEndpointsForbiddenServiceAccountsMode", func(t *testing.T) { t.Parallel() - client, db, owner := coderdenttest.NewWithDatabase(t, nil) + client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureServiceAccounts: 1, + }, + }, + }) regularClient, regularUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) regularWS := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ @@ -444,7 +450,8 @@ func TestWorkspaceSharingDisabled(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureServiceAccounts: 1, }, }, }) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3fe98238fe..36d75c217e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3511,6 +3511,7 @@ export type FeatureName = | "multiple_external_auth" | "multiple_organizations" | "scim" + | "service_accounts" | "task_batch_actions" | "template_rbac" | "user_limit" @@ -3539,6 +3540,7 @@ export const FeatureNames: FeatureName[] = [ "multiple_external_auth", "multiple_organizations", "scim", + "service_accounts", "task_batch_actions", "template_rbac", "user_limit", diff --git a/site/src/pages/CreateUserPage/CreateUserForm.stories.tsx b/site/src/pages/CreateUserPage/CreateUserForm.stories.tsx index 3c0e42745b..5631923bb4 100644 --- a/site/src/pages/CreateUserPage/CreateUserForm.stories.tsx +++ b/site/src/pages/CreateUserPage/CreateUserForm.stories.tsx @@ -17,6 +17,7 @@ const meta: Meta = { onCancel: action("cancel"), onSubmit: action("submit"), isLoading: false, + serviceAccountsEnabled: true, }, }; diff --git a/site/src/pages/CreateUserPage/CreateUserForm.tsx b/site/src/pages/CreateUserPage/CreateUserForm.tsx index 46f641701d..e1ded99de3 100644 --- a/site/src/pages/CreateUserPage/CreateUserForm.tsx +++ b/site/src/pages/CreateUserPage/CreateUserForm.tsx @@ -87,6 +87,7 @@ interface CreateUserFormProps { onCancel: () => void; authMethods?: TypesGen.AuthMethods; showOrganizations: boolean; + serviceAccountsEnabled: boolean; } export const CreateUserForm: FC = ({ @@ -96,12 +97,13 @@ export const CreateUserForm: FC = ({ onCancel, showOrganizations, authMethods, + serviceAccountsEnabled, }) => { const availableLoginTypes = [ authMethods?.password.enabled && "password", authMethods?.oidc.enabled && "oidc", authMethods?.github.enabled && "github", - "none", + serviceAccountsEnabled && "none", ].filter(Boolean) as Array; const defaultLoginType = availableLoginTypes[0]; diff --git a/site/src/pages/CreateUserPage/CreateUserPage.tsx b/site/src/pages/CreateUserPage/CreateUserPage.tsx index ba92e1a033..0af2dca856 100644 --- a/site/src/pages/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/CreateUserPage/CreateUserPage.tsx @@ -6,6 +6,7 @@ import { getErrorDetail, getErrorMessage } from "#/api/errors"; import { authMethods, createUser } from "#/api/queries/users"; import { Margins } from "#/components/Margins/Margins"; import { useDashboard } from "#/modules/dashboard/useDashboard"; +import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility"; import { pageTitle } from "#/utils/page"; import { CreateUserForm } from "./CreateUserForm"; @@ -15,6 +16,7 @@ const CreateUserPage: FC = () => { const createUserMutation = useMutation(createUser(queryClient)); const authMethodsQuery = useQuery(authMethods()); const { showOrganizations } = useDashboard(); + const { service_accounts: serviceAccountsEnabled } = useFeatureVisibility(); return ( @@ -58,6 +60,7 @@ const CreateUserPage: FC = () => { }} authMethods={authMethodsQuery.data} showOrganizations={showOrganizations} + serviceAccountsEnabled={serviceAccountsEnabled} /> );