feat: make service accounts a Premium feature (#24020)

This commit is contained in:
Kayla はな
2026-04-07 14:25:32 -04:00
committed by GitHub
parent 655d647d40
commit c5f1a2fccf
15 changed files with 238 additions and 134 deletions
+1
View File
@@ -134,6 +134,7 @@ func TestUserCreate(t *testing.T) {
{ {
name: "ServiceAccount", name: "ServiceAccount",
args: []string{"--service-account", "-u", "dean"}, args: []string{"--service-account", "-u", "dean"},
err: "Premium feature",
}, },
{ {
name: "ServiceAccountLoginType", name: "ServiceAccountLoginType",
+16 -5
View File
@@ -123,6 +123,10 @@ func UsersPagination(
require.Contains(t, gotUsers[0].Name, "after") require.Contains(t, gotUsers[0].Name, "after")
} }
type UsersFilterOptions struct {
CreateServiceAccounts bool
}
// UsersFilter creates a set of users to run various filters against for // 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. // testing. It can be used to test filtering both users and group members.
func UsersFilter( func UsersFilter(
@@ -130,11 +134,16 @@ func UsersFilter(
t *testing.T, t *testing.T,
client *codersdk.Client, client *codersdk.Client,
db database.Store, db database.Store,
options *UsersFilterOptions,
setup func(users []codersdk.User), setup func(users []codersdk.User),
fetch func(ctx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser, fetch func(ctx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser,
) { ) {
t.Helper() t.Helper()
if options == nil {
options = &UsersFilterOptions{}
}
firstUser, err := client.User(setupCtx, codersdk.Me) firstUser, err := client.User(setupCtx, codersdk.Me)
require.NoError(t, err, "fetch me") require.NoError(t, err, "fetch me")
@@ -211,11 +220,13 @@ func UsersFilter(
} }
// Add some service accounts. // Add some service accounts.
for range 3 { if options.CreateServiceAccounts {
_, user := CreateAnotherUserMutators(t, client, orgID, nil, func(r *codersdk.CreateUserRequestWithOrgs) { for range 3 {
r.ServiceAccount = true _, user := CreateAnotherUserMutators(t, client, orgID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
}) r.ServiceAccount = true
users = append(users, user) })
users = append(users, user)
}
} }
hashedPassword, err := userpassword.Hash("SomeStrongPassword!") hashedPassword, err := userpassword.Hash("SomeStrongPassword!")
+1 -1
View File
@@ -148,7 +148,7 @@ func TestGetOrgMembersFilter(t *testing.T) {
setupCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) setupCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() 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) res, err := client.OrganizationMembersPaginated(testCtx, first.OrganizationID, req)
require.NoError(t, err) require.NoError(t, err)
reduced := make([]codersdk.ReducedUser, len(res.Members)) reduced := make([]codersdk.ReducedUser, len(res.Members))
+8
View File
@@ -475,6 +475,14 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
} }
req.UserLoginType = codersdk.LoginTypeNone 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 == "" { } else if req.UserLoginType == "" {
// Default to password auth // Default to password auth
req.UserLoginType = codersdk.LoginTypePassword req.UserLoginType = codersdk.LoginTypePassword
+5 -113
View File
@@ -979,7 +979,7 @@ func TestPostUsers(t *testing.T) {
require.Equal(t, found.LoginType, codersdk.LoginTypeOIDC) 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() t.Parallel()
client := coderdtest.New(t, nil) client := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, client) first := coderdtest.CreateFirstUser(t, client)
@@ -987,98 +987,16 @@ func TestPostUsers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() defer cancel()
user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{first.OrganizationID}, OrganizationIDs: []uuid.UUID{first.OrganizationID},
Username: "service-acct-ok", Username: "service-acct-ok",
UserLoginType: codersdk.LoginTypeNone, UserLoginType: codersdk.LoginTypeNone,
ServiceAccount: true, 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 var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr) require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Email cannot be set for service accounts") require.Contains(t, apiErr.Message, "Premium feature")
})
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)
}) })
t.Run("NonServiceAccount/WithoutEmail", func(t *testing.T) { t.Run("NonServiceAccount/WithoutEmail", func(t *testing.T) {
@@ -1098,32 +1016,6 @@ func TestPostUsers(t *testing.T) {
require.ErrorAs(t, err, &apiErr) require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) 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) { func TestNotifyCreatedUser(t *testing.T) {
@@ -1832,7 +1724,7 @@ func TestGetUsersFilter(t *testing.T) {
setupCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) setupCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() 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) res, err := client.Users(testCtx, req)
require.NoError(t, err) require.NoError(t, err)
reduced := make([]codersdk.ReducedUser, len(res.Users)) reduced := make([]codersdk.ReducedUser, len(res.Users))
+4 -1
View File
@@ -196,6 +196,7 @@ const (
FeatureWorkspaceExternalAgent FeatureName = "workspace_external_agent" FeatureWorkspaceExternalAgent FeatureName = "workspace_external_agent"
FeatureAIBridge FeatureName = "aibridge" FeatureAIBridge FeatureName = "aibridge"
FeatureBoundary FeatureName = "boundary" FeatureBoundary FeatureName = "boundary"
FeatureServiceAccounts FeatureName = "service_accounts"
FeatureAIGovernanceUserLimit FeatureName = "ai_governance_user_limit" FeatureAIGovernanceUserLimit FeatureName = "ai_governance_user_limit"
) )
@@ -227,6 +228,7 @@ var (
FeatureWorkspaceExternalAgent, FeatureWorkspaceExternalAgent,
FeatureAIBridge, FeatureAIBridge,
FeatureBoundary, FeatureBoundary,
FeatureServiceAccounts,
FeatureAIGovernanceUserLimit, FeatureAIGovernanceUserLimit,
} }
@@ -275,6 +277,7 @@ func (n FeatureName) AlwaysEnable() bool {
FeatureWorkspacePrebuilds: true, FeatureWorkspacePrebuilds: true,
FeatureWorkspaceExternalAgent: true, FeatureWorkspaceExternalAgent: true,
FeatureBoundary: true, FeatureBoundary: true,
FeatureServiceAccounts: true,
}[n] }[n]
} }
@@ -282,7 +285,7 @@ func (n FeatureName) AlwaysEnable() bool {
func (n FeatureName) Enterprise() bool { func (n FeatureName) Enterprise() bool {
switch n { switch n {
// Add all features that should be excluded in the Enterprise feature set. // Add all features that should be excluded in the Enterprise feature set.
case FeatureMultipleOrganizations, FeatureCustomRoles: case FeatureMultipleOrganizations, FeatureCustomRoles, FeatureServiceAccounts:
return false return false
default: default:
return true return true
+15 -8
View File
@@ -1,31 +1,38 @@
# Headless Authentication # Headless Authentication
Headless user accounts that cannot use the web UI to log in to Coder. This is > [!NOTE]
useful for creating accounts for automated systems, such as CI/CD pipelines or > Creating service accounts requires a [Premium license](https://coder.com/pricing).
for users who only consume Coder via another client/API.
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
<div class="tabs"> <div class="tabs">
## CLI ## CLI
Use the `--service-account` flag to create a dedicated service account:
```sh ```sh
coder users create \ coder users create \
--email="coder-bot@coder.com" \
--username="coder-bot" \ --username="coder-bot" \
--login-type="none" \ --service-account
``` ```
## UI ## 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) ![Create a user via the UI](../../images/admin/users/headless-user.png)
</div> </div>
## Authenticate as a service account
To make API or CLI requests on behalf of the headless user, learn how to 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). [generate API tokens on behalf of a user](./sessions-tokens.md#generate-a-long-lived-api-token-on-behalf-of-another-user).
+2 -1
View File
@@ -495,7 +495,8 @@
{ {
"title": "Headless Authentication", "title": "Headless Authentication",
"description": "Create and manage headless service accounts for automated systems and API integrations", "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", "title": "Groups \u0026 Roles",
+4 -2
View File
@@ -1161,7 +1161,8 @@ func TestGetGroupMembersFilter(t *testing.T) {
}, },
LicenseOptions: &coderdenttest.LicenseOptions{ LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{ 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) require.NoError(t, err)
return res.Users 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) { func TestGetGroupMembersPagination(t *testing.T) {
+164
View File
@@ -614,4 +614,168 @@ func TestEnterprisePostUser(t *testing.T) {
require.Len(t, memberedOrgs, 2) require.Len(t, memberedOrgs, 2)
require.ElementsMatch(t, []uuid.UUID{second.ID, third.ID}, []uuid.UUID{memberedOrgs[0].ID, memberedOrgs[1].ID}) 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)
})
} }
+9 -2
View File
@@ -231,7 +231,13 @@ func TestWorkspaceSharingDisabled(t *testing.T) {
t.Run("ACLEndpointsForbiddenServiceAccountsMode", func(t *testing.T) { t.Run("ACLEndpointsForbiddenServiceAccountsMode", func(t *testing.T) {
t.Parallel() 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) regularClient, regularUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
regularWS := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ regularWS := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
@@ -444,7 +450,8 @@ func TestWorkspaceSharingDisabled(t *testing.T) {
}, },
LicenseOptions: &coderdenttest.LicenseOptions{ LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{ Features: license.Features{
codersdk.FeatureTemplateRBAC: 1, codersdk.FeatureTemplateRBAC: 1,
codersdk.FeatureServiceAccounts: 1,
}, },
}, },
}) })
+2
View File
@@ -3511,6 +3511,7 @@ export type FeatureName =
| "multiple_external_auth" | "multiple_external_auth"
| "multiple_organizations" | "multiple_organizations"
| "scim" | "scim"
| "service_accounts"
| "task_batch_actions" | "task_batch_actions"
| "template_rbac" | "template_rbac"
| "user_limit" | "user_limit"
@@ -3539,6 +3540,7 @@ export const FeatureNames: FeatureName[] = [
"multiple_external_auth", "multiple_external_auth",
"multiple_organizations", "multiple_organizations",
"scim", "scim",
"service_accounts",
"task_batch_actions", "task_batch_actions",
"template_rbac", "template_rbac",
"user_limit", "user_limit",
@@ -17,6 +17,7 @@ const meta: Meta<typeof CreateUserForm> = {
onCancel: action("cancel"), onCancel: action("cancel"),
onSubmit: action("submit"), onSubmit: action("submit"),
isLoading: false, isLoading: false,
serviceAccountsEnabled: true,
}, },
}; };
@@ -87,6 +87,7 @@ interface CreateUserFormProps {
onCancel: () => void; onCancel: () => void;
authMethods?: TypesGen.AuthMethods; authMethods?: TypesGen.AuthMethods;
showOrganizations: boolean; showOrganizations: boolean;
serviceAccountsEnabled: boolean;
} }
export const CreateUserForm: FC<CreateUserFormProps> = ({ export const CreateUserForm: FC<CreateUserFormProps> = ({
@@ -96,12 +97,13 @@ export const CreateUserForm: FC<CreateUserFormProps> = ({
onCancel, onCancel,
showOrganizations, showOrganizations,
authMethods, authMethods,
serviceAccountsEnabled,
}) => { }) => {
const availableLoginTypes = [ const availableLoginTypes = [
authMethods?.password.enabled && "password", authMethods?.password.enabled && "password",
authMethods?.oidc.enabled && "oidc", authMethods?.oidc.enabled && "oidc",
authMethods?.github.enabled && "github", authMethods?.github.enabled && "github",
"none", serviceAccountsEnabled && "none",
].filter(Boolean) as Array<keyof typeof loginTypeOptions>; ].filter(Boolean) as Array<keyof typeof loginTypeOptions>;
const defaultLoginType = availableLoginTypes[0]; const defaultLoginType = availableLoginTypes[0];
@@ -6,6 +6,7 @@ import { getErrorDetail, getErrorMessage } from "#/api/errors";
import { authMethods, createUser } from "#/api/queries/users"; import { authMethods, createUser } from "#/api/queries/users";
import { Margins } from "#/components/Margins/Margins"; import { Margins } from "#/components/Margins/Margins";
import { useDashboard } from "#/modules/dashboard/useDashboard"; import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { pageTitle } from "#/utils/page"; import { pageTitle } from "#/utils/page";
import { CreateUserForm } from "./CreateUserForm"; import { CreateUserForm } from "./CreateUserForm";
@@ -15,6 +16,7 @@ const CreateUserPage: FC = () => {
const createUserMutation = useMutation(createUser(queryClient)); const createUserMutation = useMutation(createUser(queryClient));
const authMethodsQuery = useQuery(authMethods()); const authMethodsQuery = useQuery(authMethods());
const { showOrganizations } = useDashboard(); const { showOrganizations } = useDashboard();
const { service_accounts: serviceAccountsEnabled } = useFeatureVisibility();
return ( return (
<Margins> <Margins>
@@ -58,6 +60,7 @@ const CreateUserPage: FC = () => {
}} }}
authMethods={authMethodsQuery.data} authMethods={authMethodsQuery.data}
showOrganizations={showOrganizations} showOrganizations={showOrganizations}
serviceAccountsEnabled={serviceAccountsEnabled}
/> />
</Margins> </Margins>
); );