You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

search.go 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package user
  4. import (
  5. "fmt"
  6. "strings"
  7. "code.gitea.io/gitea/models/db"
  8. "code.gitea.io/gitea/modules/structs"
  9. "code.gitea.io/gitea/modules/util"
  10. "xorm.io/builder"
  11. "xorm.io/xorm"
  12. )
  13. // SearchUserOptions contains the options for searching
  14. type SearchUserOptions struct {
  15. db.ListOptions
  16. Keyword string
  17. Type UserType
  18. UID int64
  19. LoginName string // this option should be used only for admin user
  20. SourceID int64 // this option should be used only for admin user
  21. OrderBy db.SearchOrderBy
  22. Visible []structs.VisibleType
  23. Actor *User // The user doing the search
  24. SearchByEmail bool // Search by email as well as username/full name
  25. IsActive util.OptionalBool
  26. IsAdmin util.OptionalBool
  27. IsRestricted util.OptionalBool
  28. IsTwoFactorEnabled util.OptionalBool
  29. IsProhibitLogin util.OptionalBool
  30. ExtraParamStrings map[string]string
  31. }
  32. func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session {
  33. var cond builder.Cond = builder.Eq{"type": opts.Type}
  34. if len(opts.Keyword) > 0 {
  35. lowerKeyword := strings.ToLower(opts.Keyword)
  36. keywordCond := builder.Or(
  37. builder.Like{"lower_name", lowerKeyword},
  38. builder.Like{"LOWER(full_name)", lowerKeyword},
  39. )
  40. if opts.SearchByEmail {
  41. keywordCond = keywordCond.Or(builder.Like{"LOWER(email)", lowerKeyword})
  42. }
  43. cond = cond.And(keywordCond)
  44. }
  45. // If visibility filtered
  46. if len(opts.Visible) > 0 {
  47. cond = cond.And(builder.In("visibility", opts.Visible))
  48. }
  49. cond = cond.And(BuildCanSeeUserCondition(opts.Actor))
  50. if opts.UID > 0 {
  51. cond = cond.And(builder.Eq{"id": opts.UID})
  52. }
  53. if opts.SourceID > 0 {
  54. cond = cond.And(builder.Eq{"login_source": opts.SourceID})
  55. }
  56. if opts.LoginName != "" {
  57. cond = cond.And(builder.Eq{"login_name": opts.LoginName})
  58. }
  59. if !opts.IsActive.IsNone() {
  60. cond = cond.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
  61. }
  62. if !opts.IsAdmin.IsNone() {
  63. cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
  64. }
  65. if !opts.IsRestricted.IsNone() {
  66. cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.IsTrue()})
  67. }
  68. if !opts.IsProhibitLogin.IsNone() {
  69. cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.IsTrue()})
  70. }
  71. e := db.GetEngine(db.DefaultContext)
  72. if opts.IsTwoFactorEnabled.IsNone() {
  73. return e.Where(cond)
  74. }
  75. // 2fa filter uses LEFT JOIN to check whether a user has a 2fa record
  76. // While using LEFT JOIN, sometimes the performance might not be good, but it won't be a problem now, such SQL is seldom executed.
  77. // There are some possible methods to refactor this SQL in future when we really need to optimize the performance (but not now):
  78. // (1) add a column in user table (2) add a setting value in user_setting table (3) use search engines (bleve/elasticsearch)
  79. if opts.IsTwoFactorEnabled.IsTrue() {
  80. cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL"))
  81. } else {
  82. cond = cond.And(builder.Expr("two_factor.uid IS NULL"))
  83. }
  84. return e.Join("LEFT OUTER", "two_factor", "two_factor.uid = `user`.id").
  85. Where(cond)
  86. }
  87. // SearchUsers takes options i.e. keyword and part of user name to search,
  88. // it returns results in given range and number of total results.
  89. func SearchUsers(opts *SearchUserOptions) (users []*User, _ int64, _ error) {
  90. sessCount := opts.toSearchQueryBase()
  91. defer sessCount.Close()
  92. count, err := sessCount.Count(new(User))
  93. if err != nil {
  94. return nil, 0, fmt.Errorf("Count: %w", err)
  95. }
  96. if len(opts.OrderBy) == 0 {
  97. opts.OrderBy = db.SearchOrderByAlphabetically
  98. }
  99. sessQuery := opts.toSearchQueryBase().OrderBy(opts.OrderBy.String())
  100. defer sessQuery.Close()
  101. if opts.Page != 0 {
  102. sessQuery = db.SetSessionPagination(sessQuery, opts)
  103. }
  104. // the sql may contain JOIN, so we must only select User related columns
  105. sessQuery = sessQuery.Select("`user`.*")
  106. users = make([]*User, 0, opts.PageSize)
  107. return users, count, sessQuery.Find(&users)
  108. }
  109. // BuildCanSeeUserCondition creates a condition which can be used to restrict results to users/orgs the actor can see
  110. func BuildCanSeeUserCondition(actor *User) builder.Cond {
  111. if actor != nil {
  112. // If Admin - they see all users!
  113. if !actor.IsAdmin {
  114. // Users can see an organization they are a member of
  115. cond := builder.In("`user`.id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": actor.ID}))
  116. if !actor.IsRestricted {
  117. // Not-Restricted users can see public and limited users/organizations
  118. cond = cond.Or(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
  119. }
  120. // Don't forget about self
  121. return cond.Or(builder.Eq{"`user`.id": actor.ID})
  122. }
  123. return nil
  124. }
  125. // Force visibility for privacy
  126. // Not logged in - only public users
  127. return builder.In("`user`.visibility", structs.VisibleTypePublic)
  128. }