diff --git a/cli/testdata/coder_users_create_--help.golden b/cli/testdata/coder_users_create_--help.golden
index bb94cac633bc0..275e89803d4c6 100644
--- a/cli/testdata/coder_users_create_--help.golden
+++ b/cli/testdata/coder_users_create_--help.golden
@@ -1,15 +1,15 @@
Usage: coder users create [flags]
[1mOptions[0m
- --disable-login bool
- Disabling login for a user prevents the user from authenticating via
- password or IdP login. Authentication requires an API key/token
- generated by an admin. Be careful when using this flag as it can lock
- the user out of their account.
-
-e, --email string
Specifies an email address for the new user.
+ --login-type string
+ 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.
+
-p, --password string
Specifies a password for the new user.
diff --git a/cli/usercreate.go b/cli/usercreate.go
index b38bbb2d6401f..80118d7fced0e 100644
--- a/cli/usercreate.go
+++ b/cli/usercreate.go
@@ -2,6 +2,7 @@ package cli
import (
"fmt"
+ "strings"
"github.com/go-playground/validator/v10"
"golang.org/x/xerrors"
@@ -18,6 +19,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
username string
password string
disableLogin bool
+ loginType string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@@ -54,7 +56,18 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
return err
}
}
- if password == "" && !disableLogin {
+ userLoginType := codersdk.LoginTypePassword
+ if disableLogin && loginType != "" {
+ return xerrors.New("You cannot specify both --disable-login and --login-type")
+ }
+ if disableLogin {
+ userLoginType = codersdk.LoginTypeNone
+ } else if loginType != "" {
+ userLoginType = codersdk.LoginType(loginType)
+ }
+
+ if password == "" && userLoginType == codersdk.LoginTypePassword {
+ // Generate a random password
password, err = cryptorand.StringCharset(cryptorand.Human, 20)
if err != nil {
return err
@@ -66,14 +79,22 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
Username: username,
Password: password,
OrganizationID: organization.ID,
- DisableLogin: disableLogin,
+ UserLoginType: userLoginType,
})
if err != nil {
return err
}
- authenticationMethod := `Your password is: ` + cliui.DefaultStyles.Field.Render(password)
- if disableLogin {
+
+ authenticationMethod := ""
+ switch codersdk.LoginType(strings.ToLower(string(userLoginType))) {
+ case codersdk.LoginTypePassword:
+ authenticationMethod = `Your password is: ` + cliui.DefaultStyles.Field.Render(password)
+ case codersdk.LoginTypeNone:
authenticationMethod = "Login has been disabled for this user. Contact your administrator to authenticate."
+ case codersdk.LoginTypeGithub:
+ authenticationMethod = `Login is authenticated through GitHub.`
+ case codersdk.LoginTypeOIDC:
+ authenticationMethod = `Login is authenticated through the configured OIDC provider.`
}
_, _ = fmt.Fprintln(inv.Stderr, `A new user has been created!
@@ -111,11 +132,22 @@ Create a workspace `+cliui.DefaultStyles.Code.Render("coder create")+`!`)
Value: clibase.StringOf(&password),
},
{
- Flag: "disable-login",
- Description: "Disabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " +
+ Flag: "disable-login",
+ Hidden: true,
+ Description: "Deprecated: Use '--login-type=none'. \nDisabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " +
"Be careful when using this flag as it can lock the user out of their account.",
Value: clibase.BoolOf(&disableLogin),
},
+ {
+ Flag: "login-type",
+ Description: fmt.Sprintf("Optionally specify the login type for the user. Valid values are: %s. "+
+ "Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin.",
+ strings.Join([]string{
+ string(codersdk.LoginTypePassword), string(codersdk.LoginTypeNone), string(codersdk.LoginTypeGithub), string(codersdk.LoginTypeOIDC),
+ }, ", ",
+ )),
+ Value: clibase.StringOf(&loginType),
+ },
}
return cmd
}
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index f930bf74f48fc..e3324b11d674f 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -7529,13 +7529,21 @@ const docTemplate = `{
],
"properties": {
"disable_login": {
- "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.",
+ "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.\nDeprecated: Set UserLoginType=LoginTypeDisabled instead.",
"type": "boolean"
},
"email": {
"type": "string",
"format": "email"
},
+ "login_type": {
+ "description": "UserLoginType defaults to LoginTypePassword.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/codersdk.LoginType"
+ }
+ ]
+ },
"organization_id": {
"type": "string",
"format": "uuid"
@@ -8442,6 +8450,7 @@ const docTemplate = `{
"codersdk.LoginType": {
"type": "string",
"enum": [
+ "",
"password",
"github",
"oidc",
@@ -8449,6 +8458,7 @@ const docTemplate = `{
"none"
],
"x-enum-varnames": [
+ "LoginTypeUnknown",
"LoginTypePassword",
"LoginTypeGithub",
"LoginTypeOIDC",
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 0d691b237b655..a9b7a550e9daa 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -6708,13 +6708,21 @@
"required": ["email", "username"],
"properties": {
"disable_login": {
- "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.",
+ "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.\nDeprecated: Set UserLoginType=LoginTypeDisabled instead.",
"type": "boolean"
},
"email": {
"type": "string",
"format": "email"
},
+ "login_type": {
+ "description": "UserLoginType defaults to LoginTypePassword.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/codersdk.LoginType"
+ }
+ ]
+ },
"organization_id": {
"type": "string",
"format": "uuid"
@@ -7569,8 +7577,9 @@
},
"codersdk.LoginType": {
"type": "string",
- "enum": ["password", "github", "oidc", "token", "none"],
+ "enum": ["", "password", "github", "oidc", "token", "none"],
"x-enum-varnames": [
+ "LoginTypeUnknown",
"LoginTypePassword",
"LoginTypeGithub",
"LoginTypeOIDC",
diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go
index 5e2e55d5c032f..04470509682e2 100644
--- a/coderd/coderdtest/coderdtest.go
+++ b/coderd/coderdtest/coderdtest.go
@@ -588,14 +588,7 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
require.NoError(t, err)
var sessionToken string
- if !req.DisableLogin {
- login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
- Email: req.Email,
- Password: req.Password,
- })
- require.NoError(t, err)
- sessionToken = login.SessionToken
- } else {
+ if req.DisableLogin || req.UserLoginType == codersdk.LoginTypeNone {
// Cannot log in with a disabled login user. So make it an api key from
// the client making this user.
token, err := client.CreateToken(context.Background(), user.ID.String(), codersdk.CreateTokenRequest{
@@ -605,6 +598,13 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
})
require.NoError(t, err)
sessionToken = token.Key
+ } else {
+ login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
+ Email: req.Email,
+ Password: req.Password,
+ })
+ require.NoError(t, err)
+ sessionToken = login.SessionToken
}
if user.Status == codersdk.UserStatusDormant {
diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go
index efa7673890863..8910bce286818 100644
--- a/coderd/userauth_test.go
+++ b/coderd/userauth_test.go
@@ -145,7 +145,7 @@ func TestUserLogin(t *testing.T) {
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
// Password auth should fail if the user is made without password login.
- t.Run("LoginTypeNone", func(t *testing.T) {
+ t.Run("DisableLoginDeprecatedField", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
@@ -160,6 +160,22 @@ func TestUserLogin(t *testing.T) {
})
require.Error(t, err)
})
+
+ t.Run("LoginTypeNone", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ user := coderdtest.CreateFirstUser(t, client)
+ anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
+ r.Password = ""
+ r.UserLoginType = codersdk.LoginTypeNone
+ })
+
+ _, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
+ Email: anotherUser.Email,
+ Password: "SomeSecurePassword!",
+ })
+ require.Error(t, err)
+ })
}
func TestUserAuthMethods(t *testing.T) {
diff --git a/coderd/users.go b/coderd/users.go
index 017e20d408586..32d6fe935d079 100644
--- a/coderd/users.go
+++ b/coderd/users.go
@@ -287,11 +287,27 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
+ if req.UserLoginType == "" && req.DisableLogin {
+ // Handle the deprecated field
+ req.UserLoginType = codersdk.LoginTypeNone
+ }
+ if req.UserLoginType == "" {
+ // Default to password auth
+ req.UserLoginType = codersdk.LoginTypePassword
+ }
+
+ if req.UserLoginType != codersdk.LoginTypePassword && req.Password != "" {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: fmt.Sprintf("Password cannot be set for non-password (%q) authentication.", req.UserLoginType),
+ })
+ return
+ }
+
// If password auth is disabled, don't allow new users to be
// created with a password!
- if api.DeploymentValues.DisablePasswordAuth {
+ if api.DeploymentValues.DisablePasswordAuth && req.UserLoginType == codersdk.LoginTypePassword {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
- Message: "You cannot manually provision new users with password authentication disabled!",
+ Message: "Password based authentication is disabled! Unable to provision new users with password authentication.",
})
return
}
@@ -353,17 +369,11 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
}
}
- if req.DisableLogin && req.Password != "" {
- httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: "Cannot set password when disabling login.",
- })
- return
- }
-
var loginType database.LoginType
- if req.DisableLogin {
+ switch req.UserLoginType {
+ case codersdk.LoginTypeNone:
loginType = database.LoginTypeNone
- } else {
+ case codersdk.LoginTypePassword:
err = userpassword.Validate(req.Password)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -376,6 +386,14 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
loginType = database.LoginTypePassword
+ case codersdk.LoginTypeOIDC:
+ loginType = database.LoginTypeOIDC
+ case codersdk.LoginTypeGithub:
+ loginType = database.LoginTypeGithub
+ default:
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: fmt.Sprintf("Unsupported login type %q for manually creating new users.", req.UserLoginType),
+ })
}
user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
diff --git a/coderd/users_test.go b/coderd/users_test.go
index eff3174ad83a2..fdf30dcbd1334 100644
--- a/coderd/users_test.go
+++ b/coderd/users_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"time"
+ "github.com/golang-jwt/jwt"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -565,6 +566,71 @@ func TestPostUsers(t *testing.T) {
}
}
})
+
+ t.Run("CreateNoneLoginType", 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.CreateUser(ctx, codersdk.CreateUserRequest{
+ OrganizationID: first.OrganizationID,
+ Email: "another@user.org",
+ Username: "someone-else",
+ Password: "",
+ UserLoginType: codersdk.LoginTypeNone,
+ })
+ require.NoError(t, err)
+
+ found, err := client.User(ctx, user.ID.String())
+ require.NoError(t, err)
+ require.Equal(t, found.LoginType, codersdk.LoginTypeNone)
+ })
+
+ t.Run("CreateOIDCLoginType", func(t *testing.T) {
+ t.Parallel()
+ email := "another@user.org"
+ conf := coderdtest.NewOIDCConfig(t, "")
+ config := conf.OIDCConfig(t, jwt.MapClaims{
+ "email": email,
+ })
+ config.AllowSignups = false
+ config.IgnoreUserInfo = true
+
+ client := coderdtest.New(t, &coderdtest.Options{
+ OIDCConfig: config,
+ })
+ first := coderdtest.CreateFirstUser(t, client)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ _, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
+ OrganizationID: first.OrganizationID,
+ Email: email,
+ Username: "someone-else",
+ Password: "",
+ UserLoginType: codersdk.LoginTypeOIDC,
+ })
+ require.NoError(t, err)
+
+ // Try to log in with OIDC.
+ userClient := codersdk.New(client.URL)
+ resp := oidcCallback(t, userClient, conf.EncodeClaims(t, jwt.MapClaims{
+ "email": email,
+ }))
+ require.Equal(t, resp.StatusCode, http.StatusTemporaryRedirect)
+ // Set the client to use this OIDC context
+ authCookie := authCookieValue(resp.Cookies())
+ userClient.SetSessionToken(authCookie)
+ _ = resp.Body.Close()
+
+ found, err := userClient.User(ctx, "me")
+ require.NoError(t, err)
+ require.Equal(t, found.LoginType, codersdk.LoginTypeOIDC)
+ })
}
func TestUpdateUserProfile(t *testing.T) {
diff --git a/codersdk/apikey.go b/codersdk/apikey.go
index 514b519f5ffda..32c97cf538417 100644
--- a/codersdk/apikey.go
+++ b/codersdk/apikey.go
@@ -28,6 +28,7 @@ type APIKey struct {
type LoginType string
const (
+ LoginTypeUnknown LoginType = ""
LoginTypePassword LoginType = "password"
LoginTypeGithub LoginType = "github"
LoginTypeOIDC LoginType = "oidc"
diff --git a/codersdk/users.go b/codersdk/users.go
index daeefee5f12bf..c11846ebdac2b 100644
--- a/codersdk/users.go
+++ b/codersdk/users.go
@@ -78,9 +78,12 @@ type CreateFirstUserResponse struct {
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email" format:"email"`
Username string `json:"username" validate:"required,username"`
- Password string `json:"password" validate:"required_if=DisableLogin false"`
+ Password string `json:"password"`
+ // UserLoginType defaults to LoginTypePassword.
+ UserLoginType LoginType `json:"login_type"`
// DisableLogin sets the user's login type to 'none'. This prevents the user
// from being able to use a password or any other authentication method to login.
+ // Deprecated: Set UserLoginType=LoginTypeDisabled instead.
DisableLogin bool `json:"disable_login"`
OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"`
}
diff --git a/docs/api/audit.md b/docs/api/audit.md
index d5aeb78665d31..5efe1f3410809 100644
--- a/docs/api/audit.md
+++ b/docs/api/audit.md
@@ -63,7 +63,7 @@ curl -X GET http://coder-server:8080/api/v2/audit?q=string \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
diff --git a/docs/api/authorization.md b/docs/api/authorization.md
index d57a5e7542c35..17fc2e81d2299 100644
--- a/docs/api/authorization.md
+++ b/docs/api/authorization.md
@@ -129,7 +129,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \
```json
{
"password": "string",
- "to_type": "password"
+ "to_type": ""
}
```
@@ -148,7 +148,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \
{
"expires_at": "2019-08-24T14:15:22Z",
"state_string": "string",
- "to_type": "password",
+ "to_type": "",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md
index fc887cd12b6e3..15ba8c12b4ea3 100644
--- a/docs/api/enterprise.md
+++ b/docs/api/enterprise.md
@@ -183,7 +183,7 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -245,7 +245,7 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -307,7 +307,7 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -444,7 +444,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -502,6 +502,7 @@ Status Code **200**
| Property | Value |
| ------------ | ----------- |
+| `login_type` | `` |
| `login_type` | `password` |
| `login_type` | `github` |
| `login_type` | `oidc` |
@@ -562,7 +563,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -625,7 +626,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -988,7 +989,7 @@ curl -X PATCH http://coder-server:8080/api/v2/scim/v2/Users/{id} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1040,7 +1041,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"role": "admin",
"roles": [
@@ -1086,6 +1087,7 @@ Status Code **200**
| Property | Value |
| ------------ | ----------- |
+| `login_type` | `` |
| `login_type` | `password` |
| `login_type` | `github` |
| `login_type` | `oidc` |
@@ -1197,7 +1199,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1222,7 +1224,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1278,6 +1280,7 @@ Status Code **200**
| Property | Value |
| ------------ | ----------- |
+| `login_type` | `` |
| `login_type` | `password` |
| `login_type` | `github` |
| `login_type` | `oidc` |
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index 7aed5d4f60022..8a539b41f02dd 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -784,7 +784,7 @@ _None_
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -809,7 +809,7 @@ _None_
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1076,7 +1076,7 @@ _None_
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1153,7 +1153,7 @@ _None_
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1378,7 +1378,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json
{
"password": "string",
- "to_type": "password"
+ "to_type": ""
}
```
@@ -1655,6 +1655,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"disable_login": true,
"email": "user@example.com",
+ "login_type": "",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string",
"username": "string"
@@ -1663,13 +1664,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ----------------- | ------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `disable_login` | boolean | false | | Disable login sets the user's login type to 'none'. This prevents the user from being able to use a password or any other authentication method to login. |
-| `email` | string | true | | |
-| `organization_id` | string | false | | |
-| `password` | string | false | | |
-| `username` | string | true | | |
+| Name | Type | Required | Restrictions | Description |
+| ----------------- | ---------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `disable_login` | boolean | false | | Disable login sets the user's login type to 'none'. This prevents the user from being able to use a password or any other authentication method to login. Deprecated: Set UserLoginType=LoginTypeDisabled instead. |
+| `email` | string | true | | |
+| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. |
+| `organization_id` | string | false | | |
+| `password` | string | false | | |
+| `username` | string | true | | |
## codersdk.CreateWorkspaceBuildRequest
@@ -2742,7 +2744,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -2960,7 +2962,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -3178,7 +3180,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
## codersdk.LoginType
```json
-"password"
+""
```
### Properties
@@ -3187,6 +3189,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Value |
| ---------- |
+| `` |
| `password` |
| `github` |
| `oidc` |
@@ -3295,7 +3298,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"expires_at": "2019-08-24T14:15:22Z",
"state_string": "string",
- "to_type": "password",
+ "to_type": "",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
@@ -4553,7 +4556,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"role": "admin",
"roles": [
@@ -5061,7 +5064,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -5186,7 +5189,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
```json
{
- "login_type": "password"
+ "login_type": ""
}
```
diff --git a/docs/api/users.md b/docs/api/users.md
index 3c583e15787db..fdeed691da48f 100644
--- a/docs/api/users.md
+++ b/docs/api/users.md
@@ -36,7 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/users \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -79,6 +79,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
{
"disable_login": true,
"email": "user@example.com",
+ "login_type": "",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string",
"username": "string"
@@ -102,7 +103,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -360,7 +361,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -411,7 +412,7 @@ curl -X DELETE http://coder-server:8080/api/v2/users/{user} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -821,7 +822,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/login-type \
```json
{
- "login_type": "password"
+ "login_type": ""
}
```
@@ -1005,7 +1006,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1056,7 +1057,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/roles \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1117,7 +1118,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1168,7 +1169,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/activate \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1219,7 +1220,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/suspend \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
diff --git a/docs/cli/users_create.md b/docs/cli/users_create.md
index 2eb78318ffa0a..b89ff2aeb6d45 100644
--- a/docs/cli/users_create.md
+++ b/docs/cli/users_create.md
@@ -10,21 +10,21 @@ coder users create [flags]
## Options
-### --disable-login
+### -e, --email
-| | |
-| ---- | ----------------- |
-| Type | bool
|
+| | |
+| ---- | ------------------- |
+| Type | string
|
-Disabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. Be careful when using this flag as it can lock the user out of their account.
+Specifies an email address for the new user.
-### -e, --email
+### --login-type
| | |
| ---- | ------------------- |
| Type | string
|
-Specifies an email address 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.
### -p, --password
diff --git a/scripts/develop.sh b/scripts/develop.sh
index 671c46a0bd5cc..cc1ab23f0554e 100755
--- a/scripts/develop.sh
+++ b/scripts/develop.sh
@@ -131,7 +131,7 @@ fatal() {
trap 'fatal "Script encountered an error"' ERR
cdroot
- start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --dangerous-allow-cors-requests=true --experiments "*,moons" "$@"
+ start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --dangerous-allow-cors-requests=true "$@"
echo '== Waiting for Coder to become ready'
# Start the timeout in the background so interrupting this script
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 3e5acc5671299..5e1b9e608dd87 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -247,6 +247,7 @@ export interface CreateUserRequest {
readonly email: string
readonly username: string
readonly password: string
+ readonly login_type: LoginType
readonly disable_login: boolean
readonly organization_id: string
}
@@ -1670,8 +1671,9 @@ export type LogSource = "provisioner" | "provisioner_daemon"
export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"]
// From codersdk/apikey.go
-export type LoginType = "github" | "none" | "oidc" | "password" | "token"
+export type LoginType = "" | "github" | "none" | "oidc" | "password" | "token"
export const LoginTypes: LoginType[] = [
+ "",
"github",
"none",
"oidc",
diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx
index c2f03155e7c62..6270f0ca88799 100644
--- a/site/src/components/CreateUserForm/CreateUserForm.tsx
+++ b/site/src/components/CreateUserForm/CreateUserForm.tsx
@@ -13,6 +13,7 @@ import { FullPageForm } from "../FullPageForm/FullPageForm"
import { Stack } from "../Stack/Stack"
import { ErrorAlert } from "components/Alert/ErrorAlert"
import { hasApiFieldErrors, isApiError } from "api/errors"
+import MenuItem from "@mui/material/MenuItem"
export const Language = {
emailLabel: "Email",
@@ -31,6 +32,7 @@ export interface CreateUserFormProps {
error?: unknown
isLoading: boolean
myOrgId: string
+ authMethods?: TypesGen.AuthMethods
}
const validationSchema = Yup.object({
@@ -38,13 +40,31 @@ const validationSchema = Yup.object({
.trim()
.email(Language.emailInvalid)
.required(Language.emailRequired),
- password: Yup.string().required(Language.passwordRequired),
+ password: Yup.string().when("login_type", {
+ is: "password",
+ then: (schema) => schema.required(Language.passwordRequired),
+ otherwise: (schema) => schema,
+ }),
username: nameValidator(Language.usernameLabel),
})
+const authMethodSelect = (
+ title: string,
+ value: string,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- future will use this
+ description: string,
+) => {
+ return (
+
+ )
+}
+
export const CreateUserForm: FC<
React.PropsWithChildren
-> = ({ onSubmit, onCancel, error, isLoading, myOrgId }) => {
+> = ({ onSubmit, onCancel, error, isLoading, myOrgId, authMethods }) => {
const form: FormikContextType =
useFormik({
initialValues: {
@@ -53,6 +73,7 @@ export const CreateUserForm: FC<
username: "",
organization_id: myOrgId,
disable_login: false,
+ login_type: "password",
},
validationSchema,
onSubmit,
@@ -62,6 +83,42 @@ export const CreateUserForm: FC<
error,
)
+ const methods = []
+ if (authMethods?.password.enabled) {
+ methods.push(
+ authMethodSelect(
+ "Password",
+ "password",
+ "User can provide their email and password to login.",
+ ),
+ )
+ }
+ if (authMethods?.oidc.enabled) {
+ methods.push(
+ authMethodSelect(
+ "OpenID Connect",
+ "oidc",
+ "Uses an OpenID connect provider to authenticate the user.",
+ ),
+ )
+ }
+ if (authMethods?.github.enabled) {
+ methods.push(
+ authMethodSelect(
+ "Github",
+ "github",
+ "Uses github oauth to authenticate the user.",
+ ),
+ )
+ }
+ methods.push(
+ authMethodSelect(
+ "None",
+ "none",
+ "User authentication is disabled. This user an only be used if an api token is created for them.",
+ ),
+ )
+
return (
{isApiError(error) && !hasApiFieldErrors(error) && (
@@ -85,13 +142,39 @@ export const CreateUserForm: FC<
label={Language.emailLabel}
/>
+ {
+ if (e.target.value !== "password") {
+ await form.setFieldValue("password", "")
+ }
+ await form.setFieldValue("login_type", e.target.value)
+ }}
+ >
+ {methods}
+
diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx
index d342ace9bdf75..ceeb30528d4f3 100644
--- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx
+++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx
@@ -21,7 +21,7 @@ const renderCreateUserPage = async () => {
const fillForm = async ({
username = "someuser",
email = "someone@coder.com",
- password = "password",
+ password = "SomeSecurePassword!",
}: {
username?: string
email?: string
@@ -29,10 +29,15 @@ const fillForm = async ({
}) => {
const usernameField = screen.getByLabelText(FormLanguage.usernameLabel)
const emailField = screen.getByLabelText(FormLanguage.emailLabel)
- const passwordField = screen.getByLabelText(FormLanguage.passwordLabel)
+ const passwordField = screen
+ .getByTestId("password-input")
+ .querySelector("input")
+
+ const loginTypeField = screen.getByTestId("login-type-input")
await userEvent.type(usernameField, username)
await userEvent.type(emailField, email)
- await userEvent.type(passwordField, password)
+ await userEvent.type(loginTypeField, "password")
+ await userEvent.type(passwordField as HTMLElement, password)
const submitButton = await screen.findByText(
FooterLanguage.defaultSubmitLabel,
)
diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx
index cd905c39d88ae..cd92b6bc8141e 100644
--- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx
+++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx
@@ -8,6 +8,8 @@ import * as TypesGen from "../../../api/typesGenerated"
import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm"
import { Margins } from "../../../components/Margins/Margins"
import { pageTitle } from "../../../utils/page"
+import { getAuthMethods } from "api/api"
+import { useQuery } from "@tanstack/react-query"
export const Language = {
unknownError: "Oops, an unknown error occurred.",
@@ -25,6 +27,13 @@ export const CreateUserPage: FC = () => {
})
const { error } = createUserState.context
+ // TODO: We should probably place this somewhere else to reduce the number of calls.
+ // This would be called each time this page is loaded.
+ const { data: authMethods } = useQuery({
+ queryKey: ["authMethods"],
+ queryFn: getAuthMethods,
+ })
+
return (
@@ -33,6 +42,7 @@ export const CreateUserPage: FC = () => {
createUserSend({ type: "CREATE", user })
}