Skip to content

Commit bdec01f

Browse files
committed
feat: allow creating manual oidc/github based users
1 parent f7a35e0 commit bdec01f

File tree

11 files changed

+124
-37
lines changed

11 files changed

+124
-37
lines changed

cli/usercreate.go

+36-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"fmt"
5+
"strings"
56

67
"github.com/go-playground/validator/v10"
78
"golang.org/x/xerrors"
@@ -18,6 +19,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
1819
username string
1920
password string
2021
disableLogin bool
22+
loginType string
2123
)
2224
client := new(codersdk.Client)
2325
cmd := &clibase.Cmd{
@@ -54,7 +56,18 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
5456
return err
5557
}
5658
}
57-
if password == "" && !disableLogin {
59+
userLoginType := codersdk.LoginTypePassword
60+
if disableLogin {
61+
userLoginType = codersdk.LoginTypeNone
62+
} else if loginType != "" {
63+
if disableLogin {
64+
return xerrors.New("You cannot specify both --disable-login and --login-type")
65+
}
66+
userLoginType = codersdk.LoginType(loginType)
67+
}
68+
69+
if password == "" && userLoginType == codersdk.LoginTypePassword {
70+
// Generate a random password
5871
password, err = cryptorand.StringCharset(cryptorand.Human, 20)
5972
if err != nil {
6073
return err
@@ -66,14 +79,22 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
6679
Username: username,
6780
Password: password,
6881
OrganizationID: organization.ID,
69-
DisableLogin: disableLogin,
82+
UserLoginType: userLoginType,
7083
})
7184
if err != nil {
7285
return err
7386
}
74-
authenticationMethod := `Your password is: ` + cliui.DefaultStyles.Field.Render(password)
75-
if disableLogin {
87+
88+
authenticationMethod := ""
89+
switch codersdk.LoginType(strings.ToLower(string(userLoginType))) {
90+
case codersdk.LoginTypePassword:
91+
authenticationMethod = `Your password is: ` + cliui.DefaultStyles.Field.Render(password)
92+
case codersdk.LoginTypeNone:
7693
authenticationMethod = "Login has been disabled for this user. Contact your administrator to authenticate."
94+
case codersdk.LoginTypeGithub:
95+
authenticationMethod = `Login is authenticated through GitHub.`
96+
case codersdk.LoginTypeOIDC:
97+
authenticationMethod = `Login is authenticated through the configured OIDC provider.`
7798
}
7899

79100
_, _ = fmt.Fprintln(inv.Stderr, `A new user has been created!
@@ -111,11 +132,20 @@ Create a workspace `+cliui.DefaultStyles.Code.Render("coder create")+`!`)
111132
Value: clibase.StringOf(&password),
112133
},
113134
{
114-
Flag: "disable-login",
115-
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. " +
135+
Flag: "disable-login",
136+
Hidden: true,
137+
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. " +
116138
"Be careful when using this flag as it can lock the user out of their account.",
117139
Value: clibase.BoolOf(&disableLogin),
118140
},
141+
{
142+
Flag: "login-type",
143+
Description: fmt.Sprintf("Optionally specify the login type for the user. Valid values are: %s.",
144+
strings.Join([]string{
145+
string(codersdk.LoginTypePassword), string(codersdk.LoginTypeNone), string(codersdk.LoginTypeGithub), string(codersdk.LoginTypeOIDC)}, ", ",
146+
)),
147+
Value: clibase.StringOf(&loginType),
148+
},
119149
}
120150
return cmd
121151
}

coderd/apidoc/docs.go

+9-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+9-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderdtest/coderdtest.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -588,14 +588,7 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
588588
require.NoError(t, err)
589589

590590
var sessionToken string
591-
if !req.DisableLogin {
592-
login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
593-
Email: req.Email,
594-
Password: req.Password,
595-
})
596-
require.NoError(t, err)
597-
sessionToken = login.SessionToken
598-
} else {
591+
if req.DisableLogin || req.UserLoginType == codersdk.LoginTypeNone {
599592
// Cannot log in with a disabled login user. So make it an api key from
600593
// the client making this user.
601594
token, err := client.CreateToken(context.Background(), user.ID.String(), codersdk.CreateTokenRequest{
@@ -605,6 +598,13 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
605598
})
606599
require.NoError(t, err)
607600
sessionToken = token.Key
601+
} else {
602+
login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
603+
Email: req.Email,
604+
Password: req.Password,
605+
})
606+
require.NoError(t, err)
607+
sessionToken = login.SessionToken
608608
}
609609

610610
if user.Status == codersdk.UserStatusDormant {

coderd/userauth_test.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func TestUserLogin(t *testing.T) {
145145
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
146146
})
147147
// Password auth should fail if the user is made without password login.
148-
t.Run("LoginTypeNone", func(t *testing.T) {
148+
t.Run("DisableLoginDeprecatedField", func(t *testing.T) {
149149
t.Parallel()
150150
client := coderdtest.New(t, nil)
151151
user := coderdtest.CreateFirstUser(t, client)
@@ -160,6 +160,22 @@ func TestUserLogin(t *testing.T) {
160160
})
161161
require.Error(t, err)
162162
})
163+
164+
t.Run("LoginTypeNone", func(t *testing.T) {
165+
t.Parallel()
166+
client := coderdtest.New(t, nil)
167+
user := coderdtest.CreateFirstUser(t, client)
168+
anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
169+
r.Password = ""
170+
r.UserLoginType = codersdk.LoginTypeNone
171+
})
172+
173+
_, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
174+
Email: anotherUser.Email,
175+
Password: "SomeSecurePassword!",
176+
})
177+
require.Error(t, err)
178+
})
163179
}
164180

165181
func TestUserAuthMethods(t *testing.T) {

coderd/users.go

+29-11
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,27 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
287287
return
288288
}
289289

290+
if req.UserLoginType == "" && req.DisableLogin {
291+
// Handle the deprecated field
292+
req.UserLoginType = codersdk.LoginTypeNone
293+
}
294+
if req.UserLoginType == "" {
295+
// Default to password auth
296+
req.UserLoginType = codersdk.LoginTypePassword
297+
}
298+
299+
if req.UserLoginType != codersdk.LoginTypePassword && req.Password != "" {
300+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
301+
Message: fmt.Sprintf("Password cannot be set for non-password (%q) authentication.", req.UserLoginType),
302+
})
303+
return
304+
}
305+
290306
// If password auth is disabled, don't allow new users to be
291307
// created with a password!
292-
if api.DeploymentValues.DisablePasswordAuth {
308+
if api.DeploymentValues.DisablePasswordAuth && req.UserLoginType == codersdk.LoginTypePassword {
293309
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
294-
Message: "You cannot manually provision new users with password authentication disabled!",
310+
Message: "Password based authentication is disabled! Unable to provision new users with password authentication.",
295311
})
296312
return
297313
}
@@ -353,17 +369,11 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
353369
}
354370
}
355371

356-
if req.DisableLogin && req.Password != "" {
357-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
358-
Message: "Cannot set password when disabling login.",
359-
})
360-
return
361-
}
362-
363372
var loginType database.LoginType
364-
if req.DisableLogin {
373+
switch req.UserLoginType {
374+
case codersdk.LoginTypeNone:
365375
loginType = database.LoginTypeNone
366-
} else {
376+
case codersdk.LoginTypePassword:
367377
err = userpassword.Validate(req.Password)
368378
if err != nil {
369379
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -376,6 +386,14 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
376386
return
377387
}
378388
loginType = database.LoginTypePassword
389+
case codersdk.LoginTypeOIDC:
390+
loginType = database.LoginTypeOIDC
391+
case codersdk.LoginTypeGithub:
392+
loginType = database.LoginTypeGithub
393+
default:
394+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
395+
Message: fmt.Sprintf("Unsupported login type %q for manually creating new users.", req.UserLoginType),
396+
})
379397
}
380398

381399
user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{

codersdk/users.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,12 @@ type CreateFirstUserResponse struct {
7878
type CreateUserRequest struct {
7979
Email string `json:"email" validate:"required,email" format:"email"`
8080
Username string `json:"username" validate:"required,username"`
81-
Password string `json:"password" validate:"required_if=DisableLogin false"`
81+
Password string `json:"password"`
82+
// UserLoginType defaults to LoginTypePassword.
83+
UserLoginType LoginType `json:"login_type"`
8284
// DisableLogin sets the user's login type to 'none'. This prevents the user
8385
// from being able to use a password or any other authentication method to login.
86+
// Deprecated: Set UserLoginType=LoginTypeDisabled instead.
8487
DisableLogin bool `json:"disable_login"`
8588
OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"`
8689
}

docs/api/schemas.md

+9-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/api/users.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/develop.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ fatal() {
131131
trap 'fatal "Script encountered an error"' ERR
132132

133133
cdroot
134-
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" "$@"
134+
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 "$@"
135135

136136
echo '== Waiting for Coder to become ready'
137137
# Start the timeout in the background so interrupting this script

site/src/api/typesGenerated.ts

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)