Skip to content

Commit

Permalink
Save initial signup information for users to aid in spam prevention (#…
Browse files Browse the repository at this point in the history
…31852)

This will allow instance admins to view signup pattern patterns for
public instances. It is modelled after discourse, mastodon, and
MediaWiki's approaches.

Note: This has privacy implications, but as the above-stated open-source
projects take this approach, especially MediaWiki, which I have no doubt
looked into this thoroughly, it is likely okay for us, too. However, I
would be appreciative of any feedback on how this could be improved.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
  • Loading branch information
techknowlogick and GiteaBot authored Sep 9, 2024
1 parent a323a82 commit f183783
Show file tree
Hide file tree
Showing 18 changed files with 61 additions and 23 deletions.
2 changes: 1 addition & 1 deletion cmd/admin_user_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func runCreateUser(c *cli.Context) error {
IsRestricted: restricted,
}

if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
return fmt.Errorf("CreateUser: %w", err)
}

Expand Down
3 changes: 3 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,9 @@ INTERNAL_TOKEN =
;; stemming from cached/logged plain-text API tokens.
;; In future releases, this will become the default behavior
;DISABLE_QUERY_AUTH_TOKEN = false
;;
;; On user registration, record the IP address and user agent of the user to help identify potential abuse.
;; RECORD_USER_SIGNUP_METADATA = false

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
4 changes: 4 additions & 0 deletions models/user/setting_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ const (
UserActivityPubPrivPem = "activitypub.priv_pem"
// UserActivityPubPubPem is user's public key
UserActivityPubPubPem = "activitypub.pub_pem"
// SignupIP is the IP address that the user signed up with
SignupIP = "signup.ip"
// SignupUserAgent is the user agent that the user signed up with
SignupUserAgent = "signup.user_agent"
)
34 changes: 29 additions & 5 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ type User struct {
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
}

// Meta defines the meta information of a user, to be stored in the K/V table
type Meta struct {
// Store the initial registration of the user, to aid in spam prevention
// Ensure that one IP isn't creating many accounts (following mediawiki approach)
InitialIP string
InitialUserAgent string
}

func init() {
db.RegisterModel(new(User))
}
Expand Down Expand Up @@ -615,17 +623,17 @@ type CreateUserOverwriteOptions struct {
}

// CreateUser creates record of a new user.
func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
return createUser(ctx, u, false, overwriteDefault...)
func CreateUser(ctx context.Context, u *User, meta *Meta, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
return createUser(ctx, u, meta, false, overwriteDefault...)
}

// AdminCreateUser is used by admins to manually create users
func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
return createUser(ctx, u, true, overwriteDefault...)
func AdminCreateUser(ctx context.Context, u *User, meta *Meta, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
return createUser(ctx, u, meta, true, overwriteDefault...)
}

// createUser creates record of a new user.
func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
if err = IsUsableUsername(u.Name); err != nil {
return err
}
Expand Down Expand Up @@ -745,6 +753,22 @@ func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefa
return err
}

if setting.RecordUserSignupMetadata {
// insert initial IP and UserAgent
if err = SetUserSetting(ctx, u.ID, SignupIP, meta.InitialIP); err != nil {
return err
}

// trim user agent string to a reasonable length, if necessary
userAgent := strings.TrimSpace(meta.InitialUserAgent)
if len(userAgent) > 255 {
userAgent = userAgent[:255]
}
if err = SetUserSetting(ctx, u.ID, SignupUserAgent, userAgent); err != nil {
return err
}
}

// insert email address
if err := db.Insert(ctx, &EmailAddress{
UID: u.ID,
Expand Down
8 changes: 4 additions & 4 deletions models/user/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func TestCreateUserInvalidEmail(t *testing.T) {
MustChangePassword: false,
}

err := user_model.CreateUser(db.DefaultContext, user)
err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailCharIsNotSupported(err))
}
Expand All @@ -241,7 +241,7 @@ func TestCreateUserEmailAlreadyUsed(t *testing.T) {
user.Name = "testuser"
user.LowerName = strings.ToLower(user.Name)
user.ID = 0
err := user_model.CreateUser(db.DefaultContext, user)
err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
}
Expand All @@ -258,7 +258,7 @@ func TestCreateUserCustomTimestamps(t *testing.T) {
user.ID = 0
user.Email = "unique@example.com"
user.CreatedUnix = creationTimestamp
err := user_model.CreateUser(db.DefaultContext, user)
err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.NoError(t, err)

fetched, err := user_model.GetUserByID(context.Background(), user.ID)
Expand All @@ -283,7 +283,7 @@ func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
user.Email = "unique@example.com"
user.CreatedUnix = 0
user.UpdatedUnix = 0
err := user_model.CreateUser(db.DefaultContext, user)
err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.NoError(t, err)

timestampEnd := time.Now().Unix()
Expand Down
3 changes: 3 additions & 0 deletions modules/setting/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var (
DisableQueryAuthToken bool
CSRFCookieName = "_csrf"
CSRFCookieHTTPOnly = true
RecordUserSignupMetadata = false
)

// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
Expand Down Expand Up @@ -164,6 +165,8 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
// TODO: default value should be true in future releases
DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false)

RecordUserSignupMetadata = sec.Key("RECORD_USER_SIGNUP_METADATA").MustBool(false)

// warn if the setting is set to false explicitly
if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken {
log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.")
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func CreateUser(ctx *context.APIContext) {
u.UpdatedUnix = u.CreatedUnix
}

if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
if err := user_model.AdminCreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
if user_model.IsErrUserAlreadyExist(err) ||
user_model.IsErrEmailAlreadyUsed(err) ||
db.IsErrNameReserved(err) ||
Expand Down
2 changes: 1 addition & 1 deletion routers/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ func SubmitInstall(ctx *context.Context) {
IsActive: optional.Some(true),
}

if err = user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
if err = user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
if !user_model.IsErrUserAlreadyExist(err) {
setting.InstallLock = false
ctx.Data["Err_AdminName"] = true
Expand Down
2 changes: 1 addition & 1 deletion routers/web/admin/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func NewUserPost(ctx *context.Context) {
u.MustChangePassword = form.MustChangePassword
}

if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
if err := user_model.AdminCreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
Expand Down
6 changes: 5 additions & 1 deletion routers/web/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,11 @@ func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form any
// createUserInContext creates a user and handles errors within a given context.
// Optionally a template can be specified.
func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) {
if err := user_model.CreateUser(ctx, u, overwrites); err != nil {
meta := &user_model.Meta{
InitialIP: ctx.RemoteAddr(),
InitialUserAgent: ctx.Req.UserAgent(),
}
if err := user_model.CreateUser(ctx, u, meta, overwrites); err != nil {
if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
var user *user_model.User
Expand Down
2 changes: 1 addition & 1 deletion services/auth/reverseproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func (r *ReverseProxy) newUser(req *http.Request) *user_model.User {
IsActive: optional.Some(true),
}

if err := user_model.CreateUser(req.Context(), user, &overwriteDefault); err != nil {
if err := user_model.CreateUser(req.Context(), user, &user_model.Meta{}, &overwriteDefault); err != nil {
// FIXME: should I create a system notice?
log.Error("CreateUser: %v", err)
return nil
Expand Down
2 changes: 1 addition & 1 deletion services/auth/source/ldap/source_authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
IsActive: optional.Some(true),
}

err := user_model.CreateUser(ctx, user, overwriteDefault)
err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault)
if err != nil {
return user, err
}
Expand Down
2 changes: 1 addition & 1 deletion services/auth/source/ldap/source_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
IsActive: optional.Some(true),
}

err = user_model.CreateUser(ctx, usr, overwriteDefault)
err = user_model.CreateUser(ctx, usr, &user_model.Meta{}, overwriteDefault)
if err != nil {
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.authSource.Name, su.Username, err)
}
Expand Down
2 changes: 1 addition & 1 deletion services/auth/source/oauth2/source_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestSource(t *testing.T) {
Email: "external@example.com",
}

err := user_model.CreateUser(context.Background(), user, &user_model.CreateUserOverwriteOptions{})
err := user_model.CreateUser(context.Background(), user, &user_model.Meta{}, &user_model.CreateUserOverwriteOptions{})
assert.NoError(t, err)

e := &user_model.ExternalLoginUser{
Expand Down
2 changes: 1 addition & 1 deletion services/auth/source/pam/source_authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
IsActive: optional.Some(true),
}

if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
if err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault); err != nil {
return user, err
}

Expand Down
2 changes: 1 addition & 1 deletion services/auth/source/smtp/source_authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
IsActive: optional.Some(true),
}

if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
if err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault); err != nil {
return user, err
}

Expand Down
2 changes: 1 addition & 1 deletion services/auth/sspi.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) (
KeepEmailPrivate: optional.Some(true),
EmailNotificationsPreference: &emailNotificationPreference,
}
if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
if err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault); err != nil {
return nil, err
}

Expand Down
4 changes: 2 additions & 2 deletions services/user/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func TestCreateUser(t *testing.T) {
MustChangePassword: false,
}

assert.NoError(t, user_model.CreateUser(db.DefaultContext, user))
assert.NoError(t, user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{}))

assert.NoError(t, DeleteUser(db.DefaultContext, user, false))
}
Expand Down Expand Up @@ -177,7 +177,7 @@ func TestCreateUser_Issue5882(t *testing.T) {
for _, v := range tt {
setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation

assert.NoError(t, user_model.CreateUser(db.DefaultContext, v.user))
assert.NoError(t, user_model.CreateUser(db.DefaultContext, v.user, &user_model.Meta{}))

u, err := user_model.GetUserByEmail(db.DefaultContext, v.user.Email)
assert.NoError(t, err)
Expand Down

0 comments on commit f183783

Please sign in to comment.