Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/testdata/coder_users_create_--help.golden
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Usage: coder users create [flags]

Options
--disable-login bool
Disabling login for a user prevents the user from authenticating
themselves via a login. Authentication would require an api
keys/token. 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.

Expand Down
23 changes: 18 additions & 5 deletions cli/usercreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (

func (r *RootCmd) userCreate() *clibase.Cmd {
var (
email string
username string
password string
email string
username string
password string
disableLogin bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Expand Down Expand Up @@ -53,7 +54,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
return err
}
}
if password == "" {
if password == "" && !disableLogin {
password, err = cryptorand.StringCharset(cryptorand.Human, 20)
if err != nil {
return err
Expand All @@ -65,10 +66,16 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
Username: username,
Password: password,
OrganizationID: organization.ID,
DisableLogin: disableLogin,
})
if err != nil {
return err
}
authenticationMethod := `Your password is: ` + cliui.DefaultStyles.Field.Render(password)
if disableLogin {
authenticationMethod = "Login has been disabled for this user. Contact your administrator to authenticate."
}

_, _ = fmt.Fprintln(inv.Stderr, `A new user has been created!
Share the instructions below to get them started.
`+cliui.DefaultStyles.Placeholder.Render("—————————————————————————————————————————————————")+`
Expand All @@ -78,7 +85,7 @@ https://github.com/coder/coder/releases
Run `+cliui.DefaultStyles.Code.Render("coder login "+client.URL.String())+` to authenticate.

Your email is: `+cliui.DefaultStyles.Field.Render(email)+`
Your password is: `+cliui.DefaultStyles.Field.Render(password)+`
`+authenticationMethod+`

Create a workspace `+cliui.DefaultStyles.Code.Render("coder create")+`!`)
return nil
Expand All @@ -103,6 +110,12 @@ Create a workspace `+cliui.DefaultStyles.Code.Render("coder create")+`!`)
Description: "Specifies a password for the new user.",
Value: clibase.StringOf(&password),
},
{
Flag: "disable-login",
Description: "Disabling login for a user prevents the user from authenticating themselves via a login. Authentication would require an api keys/token. " +
"Be careful when using this flag as it can lock the user out of their account.",
Value: clibase.BoolOf(&disableLogin),
},
}
return cmd
}
11 changes: 8 additions & 3 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 30 additions & 9 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,36 +497,57 @@ func CreateFirstUser(t testing.TB, client *codersdk.Client) codersdk.CreateFirst

// CreateAnotherUser creates and authenticates a new user.
func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uuid.UUID, roles ...string) (*codersdk.Client, codersdk.User) {
return createAnotherUserRetry(t, client, organizationID, 5, roles...)
return createAnotherUserRetry(t, client, organizationID, 5, roles)
}

func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationID uuid.UUID, retries int, roles ...string) (*codersdk.Client, codersdk.User) {
func CreateAnotherUserMutators(t *testing.T, client *codersdk.Client, organizationID uuid.UUID, roles []string, mutators ...func(r *codersdk.CreateUserRequest)) (*codersdk.Client, codersdk.User) {
return createAnotherUserRetry(t, client, organizationID, 5, roles, mutators...)
}

func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationID uuid.UUID, retries int, roles []string, mutators ...func(r *codersdk.CreateUserRequest)) (*codersdk.Client, codersdk.User) {
req := codersdk.CreateUserRequest{
Email: namesgenerator.GetRandomName(10) + "@coder.com",
Username: randomUsername(t),
Password: "SomeSecurePassword!",
OrganizationID: organizationID,
}
for _, m := range mutators {
m(&req)
}

user, err := client.CreateUser(context.Background(), req)
var apiError *codersdk.Error
// If the user already exists by username or email conflict, try again up to "retries" times.
if err != nil && retries >= 0 && xerrors.As(err, &apiError) {
if apiError.StatusCode() == http.StatusConflict {
retries--
return createAnotherUserRetry(t, client, organizationID, retries, roles...)
return createAnotherUserRetry(t, client, organizationID, retries, roles)
}
}
require.NoError(t, err)

login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: req.Email,
Password: req.Password,
})
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 {
// 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{
Lifetime: time.Hour * 24,
Scope: codersdk.APIKeyScopeAll,
TokenName: "no-password-user-token",
})
require.NoError(t, err)
sessionToken = token.Key
}

other := codersdk.New(client.URL)
other.SetSessionToken(login.SessionToken)
other.SetSessionToken(sessionToken)
t.Cleanup(func() {
other.HTTPClient.CloseIdleConnections()
})
Expand Down
5 changes: 4 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions coderd/database/migrations/000126_login_type_none.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
-- EXISTS".
3 changes: 3 additions & 0 deletions coderd/database/migrations/000126_login_type_none.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TYPE login_type ADD VALUE IF NOT EXISTS 'none';

COMMENT ON TYPE login_type IS 'Specifies the method of authentication. "none" is a special case in which no authentication method is allowed.';
6 changes: 5 additions & 1 deletion coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions coderd/userauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ func TestUserLogin(t *testing.T) {
require.ErrorAs(t, err, &apiErr)
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.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.DisableLogin = true
})

_, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: anotherUser.Email,
Password: "SomeSecurePassword!",
})
require.Error(t, err)
})
}

func TestUserAuthMethods(t *testing.T) {
Expand Down
29 changes: 21 additions & 8 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,21 +351,34 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
}
}

err = userpassword.Validate(req.Password)
if err != nil {
if req.DisableLogin && req.Password != "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Password not strong enough!",
Validations: []codersdk.ValidationError{{
Field: "password",
Detail: err.Error(),
}},
Message: "Cannot set password when disabling login.",
})
return
}

var loginType database.LoginType
if req.DisableLogin {
loginType = database.LoginTypeNone
} else {
err = userpassword.Validate(req.Password)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Password not strong enough!",
Validations: []codersdk.ValidationError{{
Field: "password",
Detail: err.Error(),
}},
})
return
}
loginType = database.LoginTypePassword
}

user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
CreateUserRequest: req,
LoginType: database.LoginTypePassword,
LoginType: loginType,
})
if dbauthz.IsNotAuthorizedError(err) {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Expand Down
5 changes: 5 additions & 0 deletions codersdk/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const (
LoginTypeGithub LoginType = "github"
LoginTypeOIDC LoginType = "oidc"
LoginTypeToken LoginType = "token"
// LoginTypeNone is used if no login method is available for this user.
// If this is set, the user has no method of logging in.
// API keys can still be created by an owner and used by the user.
// These keys would use the `LoginTypeToken` type.
LoginTypeNone LoginType = "none"
)

type APIKeyScope string
Expand Down
9 changes: 6 additions & 3 deletions codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,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"`
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"`
// 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.
DisableLogin bool `json:"disable_login"`
OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"`
}

Expand Down
15 changes: 9 additions & 6 deletions docs/api/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in

```json
{
"disable_login": true,
"email": "[email protected]",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string",
Expand All @@ -1526,12 +1527,13 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in

### Properties

| Name | Type | Required | Restrictions | Description |
| ----------------- | ------ | -------- | ------------ | ----------- |
| `email` | string | true | | |
| `organization_id` | string | false | | |
| `password` | string | true | | |
| `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. |
| `email` | string | true | | |
| `organization_id` | string | false | | |
| `password` | string | false | | |
| `username` | string | true | | |

## codersdk.CreateWorkspaceBuildRequest

Expand Down Expand Up @@ -2825,6 +2827,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `github` |
| `oidc` |
| `token` |
| `none` |

## codersdk.LoginWithPasswordRequest

Expand Down
1 change: 1 addition & 0 deletions docs/api/users.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ curl -X POST http://coder-server:8080/api/v2/users \

```json
{
"disable_login": true,
"email": "[email protected]",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string",
Expand Down
Loading