mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-28 16:46:17 +02:00
This PR replaces a set of struct-based `Get` lookups with explicit `db.Get` / `db.Exist` conditions in places where zero-value fields can lead to ambiguous matches or incorrect records being returned. The main goal is to make read paths deterministic and avoid accidentally matching the wrong row when only part of a struct is populated. ### What changed - replace many `db.GetEngine(ctx).Get(bean)` calls with explicit `builder.Eq` conditions across models such as actions, admin tasks, issues, pull requests, repositories, users, packages, redirects, watches, stars, and follows - use quoted column names where needed for reserved fields like `index`, `type`, and `name` - add dedicated user lookup helpers for: - primary email - OAuth login source / login name - update sign-in and OAuth-related flows to use explicit individual-user lookups instead of partially populated `User` structs - tighten package property and Terraform lock lookups to avoid ambiguous reads and updates - keep existing fallback behavior where needed, while removing reliance on zero-value struct matching ### User-facing impact These changes primarily affect authentication and account lookup paths: - email/username sign-in now re-fetches users through explicit keys - OAuth2 auto-linking now resolves users by name or primary email explicitly - OAuth2 login/sync now looks up users by login source, login type, and login name explicitly - non-individual accounts are no longer implicitly matched through partial user lookups in these flows This should reduce the risk of incorrect account matches and make query behavior more predictable across the codebase. --------- Co-authored-by: bircni <bircni@icloud.com>
250 lines
6.9 KiB
Go
250 lines
6.9 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.dev/models/db"
|
|
"gitea.dev/modules/cache"
|
|
"gitea.dev/modules/json"
|
|
setting_module "gitea.dev/modules/setting"
|
|
"gitea.dev/modules/util"
|
|
|
|
"xorm.io/builder"
|
|
"xorm.io/xorm/convert"
|
|
)
|
|
|
|
// Setting is a key value store of user settings
|
|
type Setting struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
UserID int64 `xorm:"index unique(key_userid)"` // to load all of someone's settings
|
|
SettingKey string `xorm:"varchar(255) index unique(key_userid)"` // ensure key is always lowercase
|
|
SettingValue string `xorm:"text"`
|
|
}
|
|
|
|
// TableName sets the table name for the settings struct
|
|
func (s *Setting) TableName() string {
|
|
return "user_setting"
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(Setting))
|
|
}
|
|
|
|
// ErrUserSettingIsNotExist represents an error that a setting is not exist with special key
|
|
type ErrUserSettingIsNotExist struct {
|
|
Key string
|
|
}
|
|
|
|
// Error implements error
|
|
func (err ErrUserSettingIsNotExist) Error() string {
|
|
return fmt.Sprintf("Setting[%s] is not exist", err.Key)
|
|
}
|
|
|
|
func (err ErrUserSettingIsNotExist) Unwrap() error {
|
|
return util.ErrNotExist
|
|
}
|
|
|
|
// genSettingCacheKey returns the cache key for some configuration
|
|
func genSettingCacheKey(userID int64, key string) string {
|
|
return fmt.Sprintf("user_%d.setting.%s", userID, key)
|
|
}
|
|
|
|
// GetSetting returns the setting value via the key
|
|
func GetSetting(ctx context.Context, uid int64, key string) (string, error) {
|
|
return cache.GetString(genSettingCacheKey(uid, key), func() (string, error) {
|
|
res, err := GetSettingNoCache(ctx, uid, key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return res.SettingValue, nil
|
|
})
|
|
}
|
|
|
|
// GetSettingNoCache returns specific setting without using the cache
|
|
func GetSettingNoCache(ctx context.Context, uid int64, key string) (*Setting, error) {
|
|
v, err := GetSettings(ctx, uid, []string{key})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(v) == 0 {
|
|
return nil, ErrUserSettingIsNotExist{key}
|
|
}
|
|
return v[key], nil
|
|
}
|
|
|
|
// GetSettings returns specific settings from user
|
|
func GetSettings(ctx context.Context, uid int64, keys []string) (map[string]*Setting, error) {
|
|
settings := make([]*Setting, 0, len(keys))
|
|
if err := db.GetEngine(ctx).
|
|
Where("user_id=?", uid).
|
|
And(builder.In("setting_key", keys)).
|
|
Find(&settings); err != nil {
|
|
return nil, err
|
|
}
|
|
settingsMap := make(map[string]*Setting)
|
|
for _, s := range settings {
|
|
settingsMap[s.SettingKey] = s
|
|
}
|
|
return settingsMap, nil
|
|
}
|
|
|
|
// GetUserAllSettings returns all settings from user
|
|
func GetUserAllSettings(ctx context.Context, uid int64) (map[string]*Setting, error) {
|
|
settings := make([]*Setting, 0, 5)
|
|
if err := db.GetEngine(ctx).
|
|
Where("user_id=?", uid).
|
|
Find(&settings); err != nil {
|
|
return nil, err
|
|
}
|
|
settingsMap := make(map[string]*Setting)
|
|
for _, s := range settings {
|
|
settingsMap[s.SettingKey] = s
|
|
}
|
|
return settingsMap, nil
|
|
}
|
|
|
|
func validateUserSettingKey(key string) error {
|
|
if len(key) == 0 {
|
|
return errors.New("setting key must be set")
|
|
}
|
|
if strings.ToLower(key) != key {
|
|
return errors.New("setting key should be lowercase")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetUserSetting gets a specific setting for a user
|
|
func GetUserSetting(ctx context.Context, userID int64, key string, def ...string) (string, error) {
|
|
if err := validateUserSettingKey(key); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
setting, has, err := db.Get[Setting](ctx, builder.Eq{"user_id": userID, "setting_key": key})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !has {
|
|
if len(def) == 1 {
|
|
return def[0], nil
|
|
}
|
|
return "", nil
|
|
}
|
|
return setting.SettingValue, nil
|
|
}
|
|
|
|
// DeleteUserSetting deletes a specific setting for a user
|
|
func DeleteUserSetting(ctx context.Context, userID int64, key string) error {
|
|
if err := validateUserSettingKey(key); err != nil {
|
|
return err
|
|
}
|
|
|
|
cache.Remove(genSettingCacheKey(userID, key))
|
|
_, err := db.GetEngine(ctx).Delete(&Setting{UserID: userID, SettingKey: key})
|
|
|
|
return err
|
|
}
|
|
|
|
// SetUserSetting updates a users' setting for a specific key
|
|
func SetUserSetting(ctx context.Context, userID int64, key, value string) error {
|
|
if err := validateUserSettingKey(key); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := upsertUserSettingValue(ctx, userID, key, value); err != nil {
|
|
return err
|
|
}
|
|
|
|
cc := cache.GetCache()
|
|
if cc != nil {
|
|
return cc.Put(genSettingCacheKey(userID, key), value, setting_module.CacheService.TTLSeconds())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func upsertUserSettingValue(ctx context.Context, userID int64, key, value string) error {
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
e := db.GetEngine(ctx)
|
|
|
|
// here we use a general method to do a safe upsert for different databases (and most transaction levels)
|
|
// 1. try to UPDATE the record and acquire the transaction write lock
|
|
// if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly
|
|
// if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed
|
|
// 2. do a SELECT to check if the row exists or not (we already have the transaction lock)
|
|
// 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe)
|
|
//
|
|
// to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1`
|
|
// to make sure the UPDATE always returns a non-zero value for existing (unchanged) records.
|
|
|
|
res, err := e.Exec("UPDATE user_setting SET setting_value=? WHERE setting_key=? AND user_id=?", value, key, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, _ := res.RowsAffected()
|
|
if rows > 0 {
|
|
// the existing row is updated, so we can return
|
|
return nil
|
|
}
|
|
|
|
// in case the value isn't changed, update would return 0 rows changed, so we need this check
|
|
has, err := e.Exist(&Setting{UserID: userID, SettingKey: key})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if has {
|
|
return nil
|
|
}
|
|
|
|
// if no existing row, insert a new row
|
|
_, err = e.Insert(&Setting{UserID: userID, SettingKey: key, SettingValue: value})
|
|
return err
|
|
})
|
|
}
|
|
|
|
func GetUserSettingJSON[T any](ctx context.Context, userID int64, key string, def T) (ret T, _ error) {
|
|
ret = def
|
|
str, err := GetUserSetting(ctx, userID, key)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
|
|
conv, ok := any(&ret).(convert.ConversionFrom)
|
|
if !ok {
|
|
conv, ok = any(ret).(convert.ConversionFrom)
|
|
}
|
|
if ok {
|
|
if err := conv.FromDB(util.UnsafeStringToBytes(str)); err != nil {
|
|
return ret, err
|
|
}
|
|
} else {
|
|
if str == "" {
|
|
return ret, nil
|
|
}
|
|
err = json.Unmarshal(util.UnsafeStringToBytes(str), &ret)
|
|
}
|
|
return ret, err
|
|
}
|
|
|
|
func SetUserSettingJSON[T any](ctx context.Context, userID int64, key string, val T) (err error) {
|
|
conv, ok := any(&val).(convert.ConversionTo)
|
|
if !ok {
|
|
conv, ok = any(val).(convert.ConversionTo)
|
|
}
|
|
var bs []byte
|
|
if ok {
|
|
bs, err = conv.ToDB()
|
|
} else {
|
|
bs, err = json.Marshal(val)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return SetUserSetting(ctx, userID, key, util.UnsafeBytesToString(bs))
|
|
}
|