mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-11 04:14:41 +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>
193 lines
5.6 KiB
Go
193 lines
5.6 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"context"
|
|
|
|
"gitea.dev/models/db"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/timeutil"
|
|
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
// WatchMode specifies what kind of watch the user has on a repository
|
|
type WatchMode int8
|
|
|
|
const (
|
|
// WatchModeNone don't watch
|
|
WatchModeNone WatchMode = iota // 0
|
|
// WatchModeNormal watch repository (from other sources)
|
|
WatchModeNormal // 1
|
|
// WatchModeDont explicit don't auto-watch
|
|
WatchModeDont // 2
|
|
// WatchModeAuto watch repository (from AutoWatchOnChanges)
|
|
WatchModeAuto // 3
|
|
)
|
|
|
|
// Watch is connection request for receiving repository notification.
|
|
type Watch struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
UserID int64 `xorm:"UNIQUE(watch)"`
|
|
RepoID int64 `xorm:"UNIQUE(watch)"`
|
|
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(Watch))
|
|
}
|
|
|
|
// GetWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found
|
|
func GetWatch(ctx context.Context, userID, repoID int64) (*Watch, error) {
|
|
watch, has, err := db.Get[Watch](ctx, builder.Eq{"user_id": userID, "repo_id": repoID})
|
|
if err != nil {
|
|
return watch, err
|
|
}
|
|
if watch == nil {
|
|
watch = &Watch{UserID: userID, RepoID: repoID}
|
|
}
|
|
if !has {
|
|
watch.Mode = WatchModeNone
|
|
}
|
|
return watch, nil
|
|
}
|
|
|
|
// IsWatchMode Decodes watchability of WatchMode
|
|
func IsWatchMode(mode WatchMode) bool {
|
|
return mode != WatchModeNone && mode != WatchModeDont
|
|
}
|
|
|
|
// IsWatching checks if user has watched given repository.
|
|
func IsWatching(ctx context.Context, userID, repoID int64) bool {
|
|
watch, err := GetWatch(ctx, userID, repoID)
|
|
return err == nil && IsWatchMode(watch.Mode)
|
|
}
|
|
|
|
func watchRepoMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
|
|
if watch.Mode == mode {
|
|
return nil
|
|
}
|
|
if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchMode(watch.Mode)) {
|
|
// Don't auto watch if already watching or deliberately not watching
|
|
return nil
|
|
}
|
|
|
|
hadrec := watch.Mode != WatchModeNone
|
|
needsrec := mode != WatchModeNone
|
|
repodiff := 0
|
|
|
|
if IsWatchMode(mode) && !IsWatchMode(watch.Mode) {
|
|
repodiff = 1
|
|
} else if !IsWatchMode(mode) && IsWatchMode(watch.Mode) {
|
|
repodiff = -1
|
|
}
|
|
|
|
watch.Mode = mode
|
|
|
|
if !hadrec && needsrec {
|
|
watch.Mode = mode
|
|
if err = db.Insert(ctx, watch); err != nil {
|
|
return err
|
|
}
|
|
} else if needsrec {
|
|
watch.Mode = mode
|
|
if _, err := db.GetEngine(ctx).ID(watch.ID).AllCols().Update(watch); err != nil {
|
|
return err
|
|
}
|
|
} else if _, err = db.DeleteByID[Watch](ctx, watch.ID); err != nil {
|
|
return err
|
|
}
|
|
if repodiff != 0 {
|
|
_, err = db.GetEngine(ctx).Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repodiff, watch.RepoID)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// WatchRepo watch or unwatch repository.
|
|
func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
|
|
watch, err := GetWatch(ctx, doer.ID, repo.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !doWatch && watch.Mode == WatchModeAuto {
|
|
return watchRepoMode(ctx, watch, WatchModeDont)
|
|
} else if !doWatch {
|
|
return watchRepoMode(ctx, watch, WatchModeNone)
|
|
}
|
|
|
|
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
|
|
return user_model.ErrBlockedUser
|
|
}
|
|
|
|
return watchRepoMode(ctx, watch, WatchModeNormal)
|
|
}
|
|
|
|
// GetWatchers returns all watchers of given repository.
|
|
func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
|
|
watches := make([]*Watch, 0, 10)
|
|
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
|
|
And("`watch`.mode<>?", WatchModeDont).
|
|
And("`user`.is_active=?", true).
|
|
And("`user`.prohibit_login=?", false).
|
|
Join("INNER", "`user`", "`user`.id = `watch`.user_id").
|
|
Find(&watches)
|
|
}
|
|
|
|
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
|
|
// but avoids joining with `user` for performance reasons
|
|
// User permissions must be verified elsewhere if required
|
|
func GetRepoWatchersIDs(ctx context.Context, repoID int64) ([]int64, error) {
|
|
ids := make([]int64, 0, 64)
|
|
return ids, db.GetEngine(ctx).Table("watch").
|
|
Where("watch.repo_id=?", repoID).
|
|
And("watch.mode<>?", WatchModeDont).
|
|
Select("user_id").
|
|
Find(&ids)
|
|
}
|
|
|
|
// GetRepoWatchers returns range of users watching given repository.
|
|
func GetRepoWatchers(ctx context.Context, repoID int64, opts db.ListOptions) ([]*user_model.User, error) {
|
|
sess := db.GetEngine(ctx).Where("watch.repo_id=?", repoID).
|
|
Join("LEFT", "watch", "`user`.id=`watch`.user_id").
|
|
And("`watch`.mode<>?", WatchModeDont)
|
|
if opts.Page > 0 {
|
|
db.SetSessionPagination(sess, &opts)
|
|
users := make([]*user_model.User, 0, opts.PageSize)
|
|
|
|
return users, sess.Find(&users)
|
|
}
|
|
|
|
users := make([]*user_model.User, 0, 8)
|
|
return users, sess.Find(&users)
|
|
}
|
|
|
|
// WatchIfAuto subscribes to repo if AutoWatchOnChanges is set
|
|
func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error {
|
|
if !isWrite || !setting.Service.AutoWatchOnChanges {
|
|
return nil
|
|
}
|
|
watch, err := GetWatch(ctx, userID, repoID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if watch.Mode != WatchModeNone {
|
|
return nil
|
|
}
|
|
return watchRepoMode(ctx, watch, WatchModeAuto)
|
|
}
|
|
|
|
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
|
|
// Used when a repository is set to private.
|
|
func ClearRepoWatches(ctx context.Context, repoID int64) error {
|
|
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_watches = 0 WHERE id = ?", repoID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return db.DeleteBeans(ctx, Watch{RepoID: repoID})
|
|
}
|