mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add login type 'none' to prevent password login (#8009)
* feat: add login type 'none' to prevent login Users with this login type must use tokens to authenticate. Tokens must come from some other source, not a /login with password authentication
This commit is contained in:
11
coderd/apidoc/docs.go
generated
11
coderd/apidoc/docs.go
generated
@ -6859,10 +6859,13 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"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.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
@ -7621,13 +7624,15 @@ const docTemplate = `{
|
||||
"password",
|
||||
"github",
|
||||
"oidc",
|
||||
"token"
|
||||
"token",
|
||||
"none"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"LoginTypePassword",
|
||||
"LoginTypeGithub",
|
||||
"LoginTypeOIDC",
|
||||
"LoginTypeToken"
|
||||
"LoginTypeToken",
|
||||
"LoginTypeNone"
|
||||
]
|
||||
},
|
||||
"codersdk.LoginWithPasswordRequest": {
|
||||
|
11
coderd/apidoc/swagger.json
generated
11
coderd/apidoc/swagger.json
generated
@ -6107,8 +6107,12 @@
|
||||
},
|
||||
"codersdk.CreateUserRequest": {
|
||||
"type": "object",
|
||||
"required": ["email", "password", "username"],
|
||||
"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.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
@ -6818,12 +6822,13 @@
|
||||
},
|
||||
"codersdk.LoginType": {
|
||||
"type": "string",
|
||||
"enum": ["password", "github", "oidc", "token"],
|
||||
"enum": ["password", "github", "oidc", "token", "none"],
|
||||
"x-enum-varnames": [
|
||||
"LoginTypePassword",
|
||||
"LoginTypeGithub",
|
||||
"LoginTypeOIDC",
|
||||
"LoginTypeToken"
|
||||
"LoginTypeToken",
|
||||
"LoginTypeNone"
|
||||
]
|
||||
},
|
||||
"codersdk.LoginWithPasswordRequest": {
|
||||
|
@ -503,16 +503,23 @@ 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
|
||||
@ -520,19 +527,33 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
|
||||
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()
|
||||
})
|
||||
|
5
coderd/database/dump.sql
generated
5
coderd/database/dump.sql
generated
@ -45,9 +45,12 @@ CREATE TYPE login_type AS ENUM (
|
||||
'password',
|
||||
'github',
|
||||
'oidc',
|
||||
'token'
|
||||
'token',
|
||||
'none'
|
||||
);
|
||||
|
||||
COMMENT ON TYPE login_type IS 'Specifies the method of authentication. "none" is a special case in which no authentication method is allowed.';
|
||||
|
||||
CREATE TYPE parameter_destination_scheme AS ENUM (
|
||||
'none',
|
||||
'environment_variable',
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
|
||||
-- EXISTS".
|
3
coderd/database/migrations/000126_login_type_none.up.sql
Normal file
3
coderd/database/migrations/000126_login_type_none.up.sql
Normal file
@ -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.';
|
@ -397,6 +397,7 @@ func AllLogSourceValues() []LogSource {
|
||||
}
|
||||
}
|
||||
|
||||
// Specifies the method of authentication. "none" is a special case in which no authentication method is allowed.
|
||||
type LoginType string
|
||||
|
||||
const (
|
||||
@ -404,6 +405,7 @@ const (
|
||||
LoginTypeGithub LoginType = "github"
|
||||
LoginTypeOIDC LoginType = "oidc"
|
||||
LoginTypeToken LoginType = "token"
|
||||
LoginTypeNone LoginType = "none"
|
||||
)
|
||||
|
||||
func (e *LoginType) Scan(src interface{}) error {
|
||||
@ -446,7 +448,8 @@ func (e LoginType) Valid() bool {
|
||||
case LoginTypePassword,
|
||||
LoginTypeGithub,
|
||||
LoginTypeOIDC,
|
||||
LoginTypeToken:
|
||||
LoginTypeToken,
|
||||
LoginTypeNone:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -458,6 +461,7 @@ func AllLoginTypeValues() []LoginType {
|
||||
LoginTypeGithub,
|
||||
LoginTypeOIDC,
|
||||
LoginTypeToken,
|
||||
LoginTypeNone,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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{
|
||||
|
Reference in New Issue
Block a user