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:
Steven Masley
2023-06-14 12:48:43 -05:00
committed by GitHub
parent cbd49abfcd
commit 6c4c3d6ce5
18 changed files with 160 additions and 41 deletions

11
coderd/apidoc/docs.go generated
View File

@ -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": {

View File

@ -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": {

View File

@ -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()
})

View File

@ -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',

View File

@ -0,0 +1,2 @@
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
-- EXISTS".

View 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.';

View File

@ -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,
}
}

View File

@ -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) {

View File

@ -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{