Skip to content

Commit 40f3fc3

Browse files
authored
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
1 parent 6fd5344 commit 40f3fc3

21 files changed

+355
-93
lines changed

cli/testdata/coder_users_create_--help.golden

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
Usage: coder users create [flags]
22

33
Options
4-
--disable-login bool
5-
Disabling login for a user prevents the user from authenticating via
6-
password or IdP login. Authentication requires an API key/token
7-
generated by an admin. Be careful when using this flag as it can lock
8-
the user out of their account.
9-
104
-e, --email string
115
Specifies an email address for the new user.
126

7+
--login-type string
8+
Optionally specify the login type for the user. Valid values are:
9+
password, none, github, oidc. Using 'none' prevents the user from
10+
authenticating and requires an API key/token to be generated by an
11+
admin.
12+
1313
-p, --password string
1414
Specifies a password for the new user.
1515

cli/usercreate.go

+38-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 && loginType != "" {
61+
return xerrors.New("You cannot specify both --disable-login and --login-type")
62+
}
63+
if disableLogin {
64+
userLoginType = codersdk.LoginTypeNone
65+
} else if loginType != "" {
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,22 @@ 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+
"Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin.",
145+
strings.Join([]string{
146+
string(codersdk.LoginTypePassword), string(codersdk.LoginTypeNone), string(codersdk.LoginTypeGithub), string(codersdk.LoginTypeOIDC),
147+
}, ", ",
148+
)),
149+
Value: clibase.StringOf(&loginType),
150+
},
119151
}
120152
return cmd
121153
}

coderd/apidoc/docs.go

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

coderd/apidoc/swagger.json

+11-2
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{

coderd/users_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99
"time"
1010

11+
"github.com/golang-jwt/jwt"
1112
"github.com/google/uuid"
1213
"github.com/stretchr/testify/assert"
1314
"github.com/stretchr/testify/require"
@@ -566,6 +567,71 @@ func TestPostUsers(t *testing.T) {
566567
}
567568
}
568569
})
570+
571+
t.Run("CreateNoneLoginType", func(t *testing.T) {
572+
t.Parallel()
573+
client := coderdtest.New(t, nil)
574+
first := coderdtest.CreateFirstUser(t, client)
575+
576+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
577+
defer cancel()
578+
579+
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
580+
OrganizationID: first.OrganizationID,
581+
Email: "another@user.org",
582+
Username: "someone-else",
583+
Password: "",
584+
UserLoginType: codersdk.LoginTypeNone,
585+
})
586+
require.NoError(t, err)
587+
588+
found, err := client.User(ctx, user.ID.String())
589+
require.NoError(t, err)
590+
require.Equal(t, found.LoginType, codersdk.LoginTypeNone)
591+
})
592+
593+
t.Run("CreateOIDCLoginType", func(t *testing.T) {
594+
t.Parallel()
595+
email := "another@user.org"
596+
conf := coderdtest.NewOIDCConfig(t, "")
597+
config := conf.OIDCConfig(t, jwt.MapClaims{
598+
"email": email,
599+
})
600+
config.AllowSignups = false
601+
config.IgnoreUserInfo = true
602+
603+
client := coderdtest.New(t, &coderdtest.Options{
604+
OIDCConfig: config,
605+
})
606+
first := coderdtest.CreateFirstUser(t, client)
607+
608+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
609+
defer cancel()
610+
611+
_, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
612+
OrganizationID: first.OrganizationID,
613+
Email: email,
614+
Username: "someone-else",
615+
Password: "",
616+
UserLoginType: codersdk.LoginTypeOIDC,
617+
})
618+
require.NoError(t, err)
619+
620+
// Try to log in with OIDC.
621+
userClient := codersdk.New(client.URL)
622+
resp := oidcCallback(t, userClient, conf.EncodeClaims(t, jwt.MapClaims{
623+
"email": email,
624+
}))
625+
require.Equal(t, resp.StatusCode, http.StatusTemporaryRedirect)
626+
// Set the client to use this OIDC context
627+
authCookie := authCookieValue(resp.Cookies())
628+
userClient.SetSessionToken(authCookie)
629+
_ = resp.Body.Close()
630+
631+
found, err := userClient.User(ctx, "me")
632+
require.NoError(t, err)
633+
require.Equal(t, found.LoginType, codersdk.LoginTypeOIDC)
634+
})
569635
}
570636

571637
func TestUpdateUserProfile(t *testing.T) {

codersdk/apikey.go

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type APIKey struct {
2828
type LoginType string
2929

3030
const (
31+
LoginTypeUnknown LoginType = ""
3132
LoginTypePassword LoginType = "password"
3233
LoginTypeGithub LoginType = "github"
3334
LoginTypeOIDC LoginType = "oidc"

0 commit comments

Comments
 (0)