diff --git a/cli/testdata/coder_users_create_--help.golden b/cli/testdata/coder_users_create_--help.golden index 04f976ab68..cbf2a51ec9 100644 --- a/cli/testdata/coder_users_create_--help.golden +++ b/cli/testdata/coder_users_create_--help.golden @@ -24,6 +24,10 @@ OPTIONS: -p, --password string Specifies a password for the new user. + --service-account bool + Create a user account intended to be used by a service or as an + intermediary rather than by a human. + -u, --username string Specifies a username for the new user. diff --git a/cli/usercreate.go b/cli/usercreate.go index 5db55cef40..e2ac81a703 100644 --- a/cli/usercreate.go +++ b/cli/usercreate.go @@ -17,13 +17,14 @@ import ( func (r *RootCmd) userCreate() *serpent.Command { var ( - email string - username string - name string - password string - disableLogin bool - loginType string - orgContext = NewOrganizationContext() + email string + username string + name string + password string + disableLogin bool + loginType string + serviceAccount bool + orgContext = NewOrganizationContext() ) cmd := &serpent.Command{ Use: "create", @@ -32,6 +33,23 @@ func (r *RootCmd) userCreate() *serpent.Command { serpent.RequireNArgs(0), ), Handler: func(inv *serpent.Invocation) error { + if serviceAccount { + switch { + case loginType != "": + return xerrors.New("You cannot use --login-type with --service-account") + case password != "": + return xerrors.New("You cannot use --password with --service-account") + case email != "": + return xerrors.New("You cannot use --email with --service-account") + case disableLogin: + return xerrors.New("You cannot use --disable-login with --service-account") + } + } + + if disableLogin && loginType != "" { + return xerrors.New("You cannot specify both --disable-login and --login-type") + } + client, err := r.InitClient(inv) if err != nil { return err @@ -59,7 +77,7 @@ func (r *RootCmd) userCreate() *serpent.Command { return err } } - if email == "" { + if email == "" && !serviceAccount { email, err = cliui.Prompt(inv, cliui.PromptOptions{ Text: "Email:", Validate: func(s string) error { @@ -87,10 +105,7 @@ func (r *RootCmd) userCreate() *serpent.Command { } } userLoginType := codersdk.LoginTypePassword - if disableLogin && loginType != "" { - return xerrors.New("You cannot specify both --disable-login and --login-type") - } - if disableLogin { + if disableLogin || serviceAccount { userLoginType = codersdk.LoginTypeNone } else if loginType != "" { userLoginType = codersdk.LoginType(loginType) @@ -111,6 +126,7 @@ func (r *RootCmd) userCreate() *serpent.Command { Password: password, OrganizationIDs: []uuid.UUID{organization.ID}, UserLoginType: userLoginType, + ServiceAccount: serviceAccount, }) if err != nil { return err @@ -127,6 +143,10 @@ func (r *RootCmd) userCreate() *serpent.Command { case codersdk.LoginTypeOIDC: authenticationMethod = `Login is authenticated through the configured OIDC provider.` } + if serviceAccount { + email = "n/a" + authenticationMethod = "Service accounts must authenticate with a token and cannot log in." + } _, _ = fmt.Fprintln(inv.Stderr, `A new user has been created! Share the instructions below to get them started. @@ -194,6 +214,11 @@ Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`! )), Value: serpent.StringOf(&loginType), }, + { + Flag: "service-account", + Description: "Create a user account intended to be used by a service or as an intermediary rather than by a human.", + Value: serpent.BoolOf(&serviceAccount), + }, } orgContext.AttachOptions(cmd) diff --git a/cli/usercreate_test.go b/cli/usercreate_test.go index 81e1d0dceb..5f29f28970 100644 --- a/cli/usercreate_test.go +++ b/cli/usercreate_test.go @@ -8,6 +8,7 @@ import ( "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/testutil" ) @@ -124,4 +125,56 @@ func TestUserCreate(t *testing.T) { assert.Equal(t, args[5], created.Username) assert.Empty(t, created.Name) }) + + tests := []struct { + name string + args []string + err string + }{ + { + name: "ServiceAccount", + args: []string{"--service-account", "-u", "dean"}, + }, + { + name: "ServiceAccountLoginType", + args: []string{"--service-account", "-u", "dean", "--login-type", "none"}, + err: "You cannot use --login-type with --service-account", + }, + { + name: "ServiceAccountDisableLogin", + args: []string{"--service-account", "-u", "dean", "--disable-login"}, + err: "You cannot use --disable-login with --service-account", + }, + { + name: "ServiceAccountEmail", + args: []string{"--service-account", "-u", "dean", "--email", "dean@coder.com"}, + err: "You cannot use --email with --service-account", + }, + { + name: "ServiceAccountPassword", + args: []string{"--service-account", "-u", "dean", "--password", "1n5ecureP4ssw0rd!"}, + err: "You cannot use --password with --service-account", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + inv, root := clitest.New(t, append([]string{"users", "create"}, tt.args...)...) + clitest.SetupConfig(t, client, root) + err := inv.Run() + if tt.err == "" { + require.NoError(t, err) + ctx := testutil.Context(t, testutil.WaitShort) + created, err := client.User(ctx, "dean") + require.NoError(t, err) + assert.Equal(t, codersdk.LoginTypeNone, created.LoginType) + } else { + require.Error(t, err) + require.ErrorContains(t, err, tt.err) + } + }) + } } diff --git a/docs/reference/cli/users_create.md b/docs/reference/cli/users_create.md index 646eb55ffb..420eec8a28 100644 --- a/docs/reference/cli/users_create.md +++ b/docs/reference/cli/users_create.md @@ -51,6 +51,14 @@ Specifies a password for the new user. Optionally specify the login type for the user. Valid values are: password, none, github, oidc. Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin. +### --service-account + +| | | +|------|-------------------| +| Type | bool | + +Create a user account intended to be used by a service or as an intermediary rather than by a human. + ### -O, --org | | |