satellite/console: PendingBotVerification status for users with high captcha score
Added new captcha score cutoff threshold config value (default is 0.8). Added new user status PendingBotVerification which is applied right after account activation if signup captcha score is above threshold. Restricted project creation/joining if user's status is PendingBotVerification. Issue: https://github.com/storj/storj-private/issues/503 Change-Id: I9fa9932ffad48ea4f5ce8235178bd4af45a1bc48
This commit is contained in:
parent
fb31761bad
commit
594b0933f1
@ -37,8 +37,10 @@ type Config struct {
|
||||
|
||||
// CaptchaConfig contains configurations for login/registration captcha system.
|
||||
type CaptchaConfig struct {
|
||||
Login MultiCaptchaConfig `json:"login"`
|
||||
Registration MultiCaptchaConfig `json:"registration"`
|
||||
FlagBotsEnabled bool `help:"indicates if flagging bot accounts is enabled" default:"false"`
|
||||
ScoreCutoffThreshold float64 `help:"bad captcha score threshold which is used to prevent bot user activity" default:"0.8"`
|
||||
Login MultiCaptchaConfig `json:"login"`
|
||||
Registration MultiCaptchaConfig `json:"registration"`
|
||||
}
|
||||
|
||||
// MultiCaptchaConfig contains configurations for Recaptcha and Hcaptcha systems.
|
||||
|
@ -277,6 +277,11 @@ func (p *Projects) CreateProject(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
project, err := p.service.CreateProject(ctx, payload)
|
||||
if err != nil {
|
||||
if console.ErrBotUser.Has(err) {
|
||||
p.serveJSONError(ctx, w, http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
if console.ErrUnauthorized.Has(err) {
|
||||
p.serveJSONError(ctx, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
@ -663,6 +668,8 @@ func (p *Projects) RespondToInvitation(w http.ResponseWriter, r *http.Request) {
|
||||
status = http.StatusNotFound
|
||||
case console.ErrValidation.Has(err):
|
||||
status = http.StatusBadRequest
|
||||
case console.ErrBotUser.Has(err):
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
p.serveJSONError(ctx, w, status, err)
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ const (
|
||||
projInviteDoesntExistErrMsg = "An invitation for '%s' does not exist"
|
||||
newInviteLimitErrMsg = "Only one new invitation can be sent at a time"
|
||||
paidTierInviteErrMsg = "Only paid tier users can invite project members"
|
||||
contactSupportErrMsg = "Please contact support"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -162,6 +163,9 @@ var (
|
||||
|
||||
// ErrNotPaidTier occurs when a user must be paid tier in order to complete an operation.
|
||||
ErrNotPaidTier = errs.Class("user is not paid tier")
|
||||
|
||||
// ErrBotUser occurs when a user must be verified by admin first in order to complete operation.
|
||||
ErrBotUser = errs.Class("user has to be verified by admin first")
|
||||
)
|
||||
|
||||
// Service is handling accounts related logic.
|
||||
@ -1047,6 +1051,10 @@ func (s *Service) SetAccountActive(ctx context.Context, user *User) (err error)
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
status := Active
|
||||
if s.config.Captcha.FlagBotsEnabled && user.SignupCaptcha != nil && *user.SignupCaptcha >= s.config.Captcha.ScoreCutoffThreshold {
|
||||
status = PendingBotVerification
|
||||
}
|
||||
|
||||
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
||||
Status: &status,
|
||||
})
|
||||
@ -1203,16 +1211,28 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (response *TokenI
|
||||
}
|
||||
}
|
||||
|
||||
user, unverified, err := s.store.Users().GetByEmailWithUnverified(ctx, request.Email)
|
||||
user, nonActiveUsers, err := s.store.Users().GetByEmailWithUnverified(ctx, request.Email)
|
||||
if user == nil {
|
||||
if len(unverified) > 0 {
|
||||
mon.Counter("login_email_unverified").Inc(1) //mon:locked
|
||||
s.auditLog(ctx, "login: failed email unverified", nil, request.Email)
|
||||
} else {
|
||||
mon.Counter("login_email_invalid").Inc(1) //mon:locked
|
||||
s.auditLog(ctx, "login: failed invalid email", nil, request.Email)
|
||||
isBotAccount := false
|
||||
for _, usr := range nonActiveUsers {
|
||||
if usr.Status == PendingBotVerification {
|
||||
isBotAccount = true
|
||||
botAccount := usr
|
||||
user = &botAccount
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isBotAccount {
|
||||
if len(nonActiveUsers) > 0 {
|
||||
mon.Counter("login_email_unverified").Inc(1) //mon:locked
|
||||
s.auditLog(ctx, "login: failed email unverified", nil, request.Email)
|
||||
} else {
|
||||
mon.Counter("login_email_invalid").Inc(1) //mon:locked
|
||||
s.auditLog(ctx, "login: failed invalid email", nil, request.Email)
|
||||
}
|
||||
return nil, ErrLoginCredentials.New(credentialsErrMsg)
|
||||
}
|
||||
return nil, ErrLoginCredentials.New(credentialsErrMsg)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@ -1763,6 +1783,10 @@ func (s *Service) CreateProject(ctx context.Context, projectInfo UpsertProjectIn
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
if user.Status == PendingBotVerification {
|
||||
return nil, ErrBotUser.New(contactSupportErrMsg)
|
||||
}
|
||||
|
||||
currentProjectCount, err := s.checkProjectLimit(ctx, user.ID)
|
||||
if err != nil {
|
||||
s.analytics.TrackProjectLimitError(user.ID, user.Email)
|
||||
@ -3268,7 +3292,7 @@ func (s *Service) authorize(ctx context.Context, userID uuid.UUID, expiration ti
|
||||
return nil, Error.New("authorization failed. no user with id: %s", userID.String())
|
||||
}
|
||||
|
||||
if user.Status != Active {
|
||||
if user.Status != Active && user.Status != PendingBotVerification {
|
||||
return nil, Error.New("authorization failed. no active user with id: %s", userID.String())
|
||||
}
|
||||
return WithUser(ctx, user), nil
|
||||
@ -3812,6 +3836,10 @@ func (s *Service) RespondToProjectInvitation(ctx context.Context, projectID uuid
|
||||
return ErrValidation.New(projInviteResponseInvalidErrMsg)
|
||||
}
|
||||
|
||||
if user.Status == PendingBotVerification {
|
||||
return ErrBotUser.New(contactSupportErrMsg)
|
||||
}
|
||||
|
||||
proj, err := s.GetProjectNoAuth(ctx, projectID)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
|
@ -158,6 +158,31 @@ func TestService(t *testing.T) {
|
||||
require.Nil(t, createdProject)
|
||||
})
|
||||
|
||||
t.Run("CreateProject when bot account", func(t *testing.T) {
|
||||
user, err := sat.AddUser(ctx, console.CreateUser{
|
||||
FullName: "Bot User",
|
||||
Email: "mfauser@mail.test",
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
botStatus := console.PendingBotVerification
|
||||
err = sat.API.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
|
||||
Status: &botStatus,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
userCtx, err := sat.UserContext(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Creating a project by bot account must fail.
|
||||
createdProject, err := service.CreateProject(userCtx, console.UpsertProjectInfo{
|
||||
Name: "test name",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, console.ErrBotUser.Has(err))
|
||||
require.Nil(t, createdProject)
|
||||
})
|
||||
|
||||
t.Run("CreateProject with placement", func(t *testing.T) {
|
||||
uid := planet.Uplinks[2].Projects[0].Owner.ID
|
||||
err := sat.API.DB.Console().Users().Update(ctx, uid, console.UpdateUserRequest{
|
||||
@ -2514,5 +2539,24 @@ func TestProjectInvitations(t *testing.T) {
|
||||
err = service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept)
|
||||
require.True(t, console.ErrProjectInviteInvalid.Has(err))
|
||||
})
|
||||
|
||||
t.Run("respond by bot account", func(t *testing.T) {
|
||||
user := addUser(t, ctx)
|
||||
botStatus := console.PendingBotVerification
|
||||
err := sat.API.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
|
||||
Status: &botStatus,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
proj := addProject(t, ctx)
|
||||
_ = addInvite(t, ctx, proj, user.Email)
|
||||
|
||||
userCtx, err := sat.UserContext(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.RespondToProjectInvitation(userCtx, proj.ID, console.ProjectInvitationDecline)
|
||||
require.Error(t, err)
|
||||
require.True(t, console.ErrBotUser.Has(err))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ type UsersPage struct {
|
||||
}
|
||||
|
||||
// IsValid checks UserInfo validity and returns error describing whats wrong.
|
||||
// The returned error has the class ErrValiation.
|
||||
// The returned error has the class ErrValidation.
|
||||
func (user *UserInfo) IsValid() error {
|
||||
// validate fullName
|
||||
if err := ValidateFullName(user.FullName); err != nil {
|
||||
@ -168,16 +168,18 @@ type TokenInfo struct {
|
||||
type UserStatus int
|
||||
|
||||
const (
|
||||
// Inactive is a user status that he receives after registration.
|
||||
// Inactive is a status that user receives after registration.
|
||||
Inactive UserStatus = 0
|
||||
// Active is a user status that he receives after account activation.
|
||||
// Active is a status that user receives after account activation.
|
||||
Active UserStatus = 1
|
||||
// Deleted is a user status that he receives after deleting account.
|
||||
// Deleted is a status that user receives after deleting account.
|
||||
Deleted UserStatus = 2
|
||||
// PendingDeletion is a user status that he receives before deleting account.
|
||||
// PendingDeletion is a status that user receives before deleting account.
|
||||
PendingDeletion UserStatus = 3
|
||||
// LegalHold is a user status that he receives for legal reasons.
|
||||
// LegalHold is a status that user receives for legal reasons.
|
||||
LegalHold UserStatus = 4
|
||||
// PendingBotVerification is a status that user receives after account activation but with high captcha score.
|
||||
PendingBotVerification UserStatus = 5
|
||||
)
|
||||
|
||||
// String returns a string representation of the user status.
|
||||
@ -193,6 +195,8 @@ func (s UserStatus) String() string {
|
||||
return "Pending Deletion"
|
||||
case LegalHold:
|
||||
return "Legal Hold"
|
||||
case PendingBotVerification:
|
||||
return "Pending Bot Verification"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
6
scripts/testdata/satellite-config.yaml.lock
vendored
6
scripts/testdata/satellite-config.yaml.lock
vendored
@ -223,6 +223,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
||||
# The maximum body size allowed to be received by the API
|
||||
# console.body-size-limit: 100.00 KB
|
||||
|
||||
# indicates if flagging bot accounts is enabled
|
||||
# console.captcha.flag-bots-enabled: false
|
||||
|
||||
# whether or not captcha is enabled
|
||||
# console.captcha.login.hcaptcha.enabled: false
|
||||
|
||||
@ -259,6 +262,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
||||
# captcha site key
|
||||
# console.captcha.registration.recaptcha.site-key: ""
|
||||
|
||||
# bad captcha score threshold which is used to prevent bot user activity
|
||||
# console.captcha.score-cutoff-threshold: 0.8
|
||||
|
||||
# url link to contacts page
|
||||
# console.contact-info-url: https://forum.storj.io
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user