feat: allow creating manual oidc/github based users (#9000)

* feat: allow creating manual oidc/github based users
* Add unit test for oidc and no login type create
This commit is contained in:
Steven Masley 2023-08-10 20:04:35 -05:00 committed by GitHub
parent 6fd5344d0a
commit 40f3fc3a1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 356 additions and 94 deletions

View File

@ -1,15 +1,15 @@
Usage: coder users create [flags] Usage: coder users create [flags]
Options Options
--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 -e, --email string
Specifies an email address for the new user. 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 -p, --password string
Specifies a password for the new user. Specifies a password for the new user.

View File

@ -2,6 +2,7 @@ package cli
import ( import (
"fmt" "fmt"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"golang.org/x/xerrors" "golang.org/x/xerrors"
@ -18,6 +19,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
username string username string
password string password string
disableLogin bool disableLogin bool
loginType string
) )
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &clibase.Cmd{ cmd := &clibase.Cmd{
@ -54,7 +56,18 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
return err 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) password, err = cryptorand.StringCharset(cryptorand.Human, 20)
if err != nil { if err != nil {
return err return err
@ -66,14 +79,22 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
Username: username, Username: username,
Password: password, Password: password,
OrganizationID: organization.ID, OrganizationID: organization.ID,
DisableLogin: disableLogin, UserLoginType: userLoginType,
}) })
if err != nil { if err != nil {
return err 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." 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! _, _ = 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), Value: clibase.StringOf(&password),
}, },
{ {
Flag: "disable-login", 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. " + 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.", "Be careful when using this flag as it can lock the user out of their account.",
Value: clibase.BoolOf(&disableLogin), 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 return cmd
} }

12
coderd/apidoc/docs.go generated
View File

@ -7536,13 +7536,21 @@ const docTemplate = `{
], ],
"properties": { "properties": {
"disable_login": { "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" "type": "boolean"
}, },
"email": { "email": {
"type": "string", "type": "string",
"format": "email" "format": "email"
}, },
"login_type": {
"description": "UserLoginType defaults to LoginTypePassword.",
"allOf": [
{
"$ref": "#/definitions/codersdk.LoginType"
}
]
},
"organization_id": { "organization_id": {
"type": "string", "type": "string",
"format": "uuid" "format": "uuid"
@ -8449,6 +8457,7 @@ const docTemplate = `{
"codersdk.LoginType": { "codersdk.LoginType": {
"type": "string", "type": "string",
"enum": [ "enum": [
"",
"password", "password",
"github", "github",
"oidc", "oidc",
@ -8456,6 +8465,7 @@ const docTemplate = `{
"none" "none"
], ],
"x-enum-varnames": [ "x-enum-varnames": [
"LoginTypeUnknown",
"LoginTypePassword", "LoginTypePassword",
"LoginTypeGithub", "LoginTypeGithub",
"LoginTypeOIDC", "LoginTypeOIDC",

View File

@ -6715,13 +6715,21 @@
"required": ["email", "username"], "required": ["email", "username"],
"properties": { "properties": {
"disable_login": { "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" "type": "boolean"
}, },
"email": { "email": {
"type": "string", "type": "string",
"format": "email" "format": "email"
}, },
"login_type": {
"description": "UserLoginType defaults to LoginTypePassword.",
"allOf": [
{
"$ref": "#/definitions/codersdk.LoginType"
}
]
},
"organization_id": { "organization_id": {
"type": "string", "type": "string",
"format": "uuid" "format": "uuid"
@ -7576,8 +7584,9 @@
}, },
"codersdk.LoginType": { "codersdk.LoginType": {
"type": "string", "type": "string",
"enum": ["password", "github", "oidc", "token", "none"], "enum": ["", "password", "github", "oidc", "token", "none"],
"x-enum-varnames": [ "x-enum-varnames": [
"LoginTypeUnknown",
"LoginTypePassword", "LoginTypePassword",
"LoginTypeGithub", "LoginTypeGithub",
"LoginTypeOIDC", "LoginTypeOIDC",

View File

@ -588,14 +588,7 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
require.NoError(t, err) require.NoError(t, err)
var sessionToken string var sessionToken string
if !req.DisableLogin { if req.DisableLogin || req.UserLoginType == codersdk.LoginTypeNone {
login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: req.Email,
Password: req.Password,
})
require.NoError(t, err)
sessionToken = login.SessionToken
} else {
// Cannot log in with a disabled login user. So make it an api key from // Cannot log in with a disabled login user. So make it an api key from
// the client making this user. // the client making this user.
token, err := client.CreateToken(context.Background(), user.ID.String(), codersdk.CreateTokenRequest{ 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) require.NoError(t, err)
sessionToken = token.Key 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 { if user.Status == codersdk.UserStatusDormant {

View File

@ -145,7 +145,7 @@ func TestUserLogin(t *testing.T) {
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
}) })
// Password auth should fail if the user is made without password login. // 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() t.Parallel()
client := coderdtest.New(t, nil) client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
@ -160,6 +160,22 @@ func TestUserLogin(t *testing.T) {
}) })
require.Error(t, err) 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) { func TestUserAuthMethods(t *testing.T) {

View File

@ -287,11 +287,27 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return 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 // If password auth is disabled, don't allow new users to be
// created with a password! // created with a password!
if api.DeploymentValues.DisablePasswordAuth { if api.DeploymentValues.DisablePasswordAuth && req.UserLoginType == codersdk.LoginTypePassword {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ 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 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 var loginType database.LoginType
if req.DisableLogin { switch req.UserLoginType {
case codersdk.LoginTypeNone:
loginType = database.LoginTypeNone loginType = database.LoginTypeNone
} else { case codersdk.LoginTypePassword:
err = userpassword.Validate(req.Password) err = userpassword.Validate(req.Password)
if err != nil { if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@ -376,6 +386,14 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return return
} }
loginType = database.LoginTypePassword 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{ user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{

View File

@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/golang-jwt/jwt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -566,6 +567,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) { func TestUpdateUserProfile(t *testing.T) {

View File

@ -28,6 +28,7 @@ type APIKey struct {
type LoginType string type LoginType string
const ( const (
LoginTypeUnknown LoginType = ""
LoginTypePassword LoginType = "password" LoginTypePassword LoginType = "password"
LoginTypeGithub LoginType = "github" LoginTypeGithub LoginType = "github"
LoginTypeOIDC LoginType = "oidc" LoginTypeOIDC LoginType = "oidc"

View File

@ -78,9 +78,12 @@ type CreateFirstUserResponse struct {
type CreateUserRequest struct { type CreateUserRequest struct {
Email string `json:"email" validate:"required,email" format:"email"` Email string `json:"email" validate:"required,email" format:"email"`
Username string `json:"username" validate:"required,username"` 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 // 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. // from being able to use a password or any other authentication method to login.
// Deprecated: Set UserLoginType=LoginTypeDisabled instead.
DisableLogin bool `json:"disable_login"` DisableLogin bool `json:"disable_login"`
OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"` OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"`
} }

2
docs/api/audit.md generated
View File

@ -63,7 +63,7 @@ curl -X GET http://coder-server:8080/api/v2/audit?q=string \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {

View File

@ -129,7 +129,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \
```json ```json
{ {
"password": "string", "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", "expires_at": "2019-08-24T14:15:22Z",
"state_string": "string", "state_string": "string",
"to_type": "password", "to_type": "",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```

23
docs/api/enterprise.md generated
View File

@ -183,7 +183,7 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -245,7 +245,7 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -307,7 +307,7 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -444,7 +444,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -502,6 +502,7 @@ Status Code **200**
| Property | Value | | Property | Value |
| ------------ | ----------- | | ------------ | ----------- |
| `login_type` | `` |
| `login_type` | `password` | | `login_type` | `password` |
| `login_type` | `github` | | `login_type` | `github` |
| `login_type` | `oidc` | | `login_type` | `oidc` |
@ -562,7 +563,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -625,7 +626,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -988,7 +989,7 @@ curl -X PATCH http://coder-server:8080/api/v2/scim/v2/Users/{id} \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1040,7 +1041,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"role": "admin", "role": "admin",
"roles": [ "roles": [
@ -1086,6 +1087,7 @@ Status Code **200**
| Property | Value | | Property | Value |
| ------------ | ----------- | | ------------ | ----------- |
| `login_type` | `` |
| `login_type` | `password` | | `login_type` | `password` |
| `login_type` | `github` | | `login_type` | `github` |
| `login_type` | `oidc` | | `login_type` | `oidc` |
@ -1197,7 +1199,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1222,7 +1224,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1278,6 +1280,7 @@ Status Code **200**
| Property | Value | | Property | Value |
| ------------ | ----------- | | ------------ | ----------- |
| `login_type` | `` |
| `login_type` | `password` | | `login_type` | `password` |
| `login_type` | `github` | | `login_type` | `github` |
| `login_type` | `oidc` | | `login_type` | `oidc` |

41
docs/api/schemas.md generated
View File

@ -792,7 +792,7 @@ _None_
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -817,7 +817,7 @@ _None_
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1086,7 +1086,7 @@ _None_
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1163,7 +1163,7 @@ _None_
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1388,7 +1388,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json ```json
{ {
"password": "string", "password": "string",
"to_type": "password" "to_type": ""
} }
``` ```
@ -1665,6 +1665,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{ {
"disable_login": true, "disable_login": true,
"email": "user@example.com", "email": "user@example.com",
"login_type": "",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string", "password": "string",
"username": "string" "username": "string"
@ -1673,13 +1674,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties ### Properties
| Name | Type | Required | Restrictions | Description | | 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. | | `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 | | | | `email` | string | true | | |
| `organization_id` | string | false | | | | `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. |
| `password` | string | false | | | | `organization_id` | string | false | | |
| `username` | string | true | | | | `password` | string | false | | |
| `username` | string | true | | |
## codersdk.CreateWorkspaceBuildRequest ## codersdk.CreateWorkspaceBuildRequest
@ -2752,7 +2754,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -2970,7 +2972,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -3188,7 +3190,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
## codersdk.LoginType ## codersdk.LoginType
```json ```json
"password" ""
``` ```
### Properties ### Properties
@ -3197,6 +3199,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Value | | Value |
| ---------- | | ---------- |
| `` |
| `password` | | `password` |
| `github` | | `github` |
| `oidc` | | `oidc` |
@ -3305,7 +3308,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{ {
"expires_at": "2019-08-24T14:15:22Z", "expires_at": "2019-08-24T14:15:22Z",
"state_string": "string", "state_string": "string",
"to_type": "password", "to_type": "",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
@ -4563,7 +4566,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"role": "admin", "role": "admin",
"roles": [ "roles": [
@ -5071,7 +5074,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -5196,7 +5199,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
```json ```json
{ {
"login_type": "password" "login_type": ""
} }
``` ```

21
docs/api/users.md generated
View File

@ -36,7 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/users \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -79,6 +79,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
{ {
"disable_login": true, "disable_login": true,
"email": "user@example.com", "email": "user@example.com",
"login_type": "",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string", "password": "string",
"username": "string" "username": "string"
@ -102,7 +103,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -360,7 +361,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -411,7 +412,7 @@ curl -X DELETE http://coder-server:8080/api/v2/users/{user} \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -821,7 +822,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/login-type \
```json ```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", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1056,7 +1057,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/roles \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1117,7 +1118,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1168,7 +1169,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/activate \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
@ -1219,7 +1220,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/suspend \
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "password", "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {

View File

@ -10,14 +10,6 @@ coder users create [flags]
## Options ## Options
### --disable-login
| | |
| ---- | ----------------- |
| Type | <code>bool</code> |
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 ### -e, --email
| | | | | |
@ -26,6 +18,14 @@ Disabling login for a user prevents the user from authenticating via password or
Specifies an email address for the new user. Specifies an email address for the new user.
### --login-type
| | |
| ---- | ------------------- |
| Type | <code>string</code> |
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 ### -p, --password
| | | | | |

View File

@ -131,7 +131,7 @@ fatal() {
trap 'fatal "Script encountered an error"' ERR trap 'fatal "Script encountered an error"' ERR
cdroot 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' echo '== Waiting for Coder to become ready'
# Start the timeout in the background so interrupting this script # Start the timeout in the background so interrupting this script

View File

@ -247,6 +247,7 @@ export interface CreateUserRequest {
readonly email: string readonly email: string
readonly username: string readonly username: string
readonly password: string readonly password: string
readonly login_type: LoginType
readonly disable_login: boolean readonly disable_login: boolean
readonly organization_id: string readonly organization_id: string
} }
@ -1674,8 +1675,9 @@ export type LogSource = "provisioner" | "provisioner_daemon"
export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"] export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"]
// From codersdk/apikey.go // From codersdk/apikey.go
export type LoginType = "github" | "none" | "oidc" | "password" | "token" export type LoginType = "" | "github" | "none" | "oidc" | "password" | "token"
export const LoginTypes: LoginType[] = [ export const LoginTypes: LoginType[] = [
"",
"github", "github",
"none", "none",
"oidc", "oidc",

View File

@ -13,6 +13,7 @@ import { FullPageForm } from "../FullPageForm/FullPageForm"
import { Stack } from "../Stack/Stack" import { Stack } from "../Stack/Stack"
import { ErrorAlert } from "components/Alert/ErrorAlert" import { ErrorAlert } from "components/Alert/ErrorAlert"
import { hasApiFieldErrors, isApiError } from "api/errors" import { hasApiFieldErrors, isApiError } from "api/errors"
import MenuItem from "@mui/material/MenuItem"
export const Language = { export const Language = {
emailLabel: "Email", emailLabel: "Email",
@ -31,6 +32,7 @@ export interface CreateUserFormProps {
error?: unknown error?: unknown
isLoading: boolean isLoading: boolean
myOrgId: string myOrgId: string
authMethods?: TypesGen.AuthMethods
} }
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@ -38,13 +40,31 @@ const validationSchema = Yup.object({
.trim() .trim()
.email(Language.emailInvalid) .email(Language.emailInvalid)
.required(Language.emailRequired), .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), 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 (
<MenuItem key="value" id={"item-" + value} value={value}>
{title}
{/* TODO: Add description */}
</MenuItem>
)
}
export const CreateUserForm: FC< export const CreateUserForm: FC<
React.PropsWithChildren<CreateUserFormProps> React.PropsWithChildren<CreateUserFormProps>
> = ({ onSubmit, onCancel, error, isLoading, myOrgId }) => { > = ({ onSubmit, onCancel, error, isLoading, myOrgId, authMethods }) => {
const form: FormikContextType<TypesGen.CreateUserRequest> = const form: FormikContextType<TypesGen.CreateUserRequest> =
useFormik<TypesGen.CreateUserRequest>({ useFormik<TypesGen.CreateUserRequest>({
initialValues: { initialValues: {
@ -53,6 +73,7 @@ export const CreateUserForm: FC<
username: "", username: "",
organization_id: myOrgId, organization_id: myOrgId,
disable_login: false, disable_login: false,
login_type: "password",
}, },
validationSchema, validationSchema,
onSubmit, onSubmit,
@ -62,6 +83,42 @@ export const CreateUserForm: FC<
error, 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 ( return (
<FullPageForm title="Create user"> <FullPageForm title="Create user">
{isApiError(error) && !hasApiFieldErrors(error) && ( {isApiError(error) && !hasApiFieldErrors(error) && (
@ -85,13 +142,39 @@ export const CreateUserForm: FC<
label={Language.emailLabel} label={Language.emailLabel}
/> />
<TextField <TextField
{...getFieldHelpers("password")} {...getFieldHelpers(
"password",
form.values.login_type === "password"
? ""
: "No password required for this login type",
)}
autoComplete="current-password" autoComplete="current-password"
fullWidth fullWidth
id="password" id="password"
data-testid="password-input"
disabled={form.values.login_type !== "password"}
label={Language.passwordLabel} label={Language.passwordLabel}
type="password" type="password"
/> />
<TextField
{...getFieldHelpers(
"login_type",
"Authentication method for this user",
)}
select
id="login_type"
data-testid="login-type-input"
value={form.values.login_type}
label="Login Type"
onChange={async (e) => {
if (e.target.value !== "password") {
await form.setFieldValue("password", "")
}
await form.setFieldValue("login_type", e.target.value)
}}
>
{methods}
</TextField>
</Stack> </Stack>
<FormFooter onCancel={onCancel} isLoading={isLoading} /> <FormFooter onCancel={onCancel} isLoading={isLoading} />
</form> </form>

View File

@ -21,7 +21,7 @@ const renderCreateUserPage = async () => {
const fillForm = async ({ const fillForm = async ({
username = "someuser", username = "someuser",
email = "someone@coder.com", email = "someone@coder.com",
password = "password", password = "SomeSecurePassword!",
}: { }: {
username?: string username?: string
email?: string email?: string
@ -29,10 +29,15 @@ const fillForm = async ({
}) => { }) => {
const usernameField = screen.getByLabelText(FormLanguage.usernameLabel) const usernameField = screen.getByLabelText(FormLanguage.usernameLabel)
const emailField = screen.getByLabelText(FormLanguage.emailLabel) 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(usernameField, username)
await userEvent.type(emailField, email) 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( const submitButton = await screen.findByText(
FooterLanguage.defaultSubmitLabel, FooterLanguage.defaultSubmitLabel,
) )

View File

@ -8,6 +8,8 @@ import * as TypesGen from "../../../api/typesGenerated"
import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm"
import { Margins } from "../../../components/Margins/Margins" import { Margins } from "../../../components/Margins/Margins"
import { pageTitle } from "../../../utils/page" import { pageTitle } from "../../../utils/page"
import { getAuthMethods } from "api/api"
import { useQuery } from "@tanstack/react-query"
export const Language = { export const Language = {
unknownError: "Oops, an unknown error occurred.", unknownError: "Oops, an unknown error occurred.",
@ -25,6 +27,13 @@ export const CreateUserPage: FC = () => {
}) })
const { error } = createUserState.context 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 ( return (
<Margins> <Margins>
<Helmet> <Helmet>
@ -33,6 +42,7 @@ export const CreateUserPage: FC = () => {
<CreateUserForm <CreateUserForm
error={error} error={error}
authMethods={authMethods}
onSubmit={(user: TypesGen.CreateUserRequest) => onSubmit={(user: TypesGen.CreateUserRequest) =>
createUserSend({ type: "CREATE", user }) createUserSend({ type: "CREATE", user })
} }