orgs.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. // Copyright 2022 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package db
  5. import (
  6. "context"
  7. "fmt"
  8. "strings"
  9. "github.com/pkg/errors"
  10. "gorm.io/gorm"
  11. "gogs.io/gogs/internal/dbutil"
  12. "gogs.io/gogs/internal/errutil"
  13. )
  14. // OrgsStore is the persistent interface for organizations.
  15. type OrgsStore interface {
  16. // AddMember adds a new member to the given organization.
  17. AddMember(ctx context.Context, orgID, userID int64) error
  18. // RemoveMember removes a member from the given organization.
  19. RemoveMember(ctx context.Context, orgID, userID int64) error
  20. // HasMember returns whether the given user is a member of the organization
  21. // (first), and whether the organization membership is public (second).
  22. HasMember(ctx context.Context, orgID, userID int64) (bool, bool)
  23. // ListMembers returns all members of the given organization, and sorted by the
  24. // given order (e.g. "id ASC").
  25. ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error)
  26. // IsOwnedBy returns true if the given user is an owner of the organization.
  27. IsOwnedBy(ctx context.Context, orgID, userID int64) bool
  28. // SetMemberVisibility sets the visibility of the given user in the organization.
  29. SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error
  30. // GetByName returns the organization with given name.
  31. GetByName(ctx context.Context, name string) (*Organization, error)
  32. // SearchByName returns a list of organizations whose username or full name
  33. // matches the given keyword case-insensitively. Results are paginated by given
  34. // page and page size, and sorted by the given order (e.g. "id DESC"). A total
  35. // count of all results is also returned. If the order is not given, it's up to
  36. // the database to decide.
  37. SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error)
  38. // List returns a list of organizations filtered by options.
  39. List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error)
  40. // CountByUser returns the number of organizations the user is a member of.
  41. CountByUser(ctx context.Context, userID int64) (int64, error)
  42. // Count returns the total number of organizations.
  43. Count(ctx context.Context) int64
  44. // GetTeamByName returns the team with given name under the given organization.
  45. // It returns ErrTeamNotExist whe not found.
  46. GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error)
  47. // AccessibleRepositoriesByUser returns a range of repositories in the
  48. // organization that the user has access to and the total number of it. Results
  49. // are paginated by given page and page size, and sorted by the given order
  50. // (e.g. "updated_unix DESC").
  51. AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error)
  52. }
  53. var Orgs OrgsStore
  54. var _ OrgsStore = (*orgs)(nil)
  55. type orgs struct {
  56. *gorm.DB
  57. }
  58. // NewOrgsStore returns a persistent interface for orgs with given database
  59. // connection.
  60. func NewOrgsStore(db *gorm.DB) OrgsStore {
  61. return &orgs{DB: db}
  62. }
  63. func (*orgs) recountMembers(tx *gorm.DB, orgID int64) error {
  64. /*
  65. Equivalent SQL for PostgreSQL:
  66. UPDATE "user"
  67. SET num_members = (
  68. SELECT COUNT(*) FROM org_user WHERE org_id = @orgID
  69. )
  70. WHERE id = @orgID
  71. */
  72. err := tx.Model(&User{}).
  73. Where("id = ?", orgID).
  74. Update(
  75. "num_members",
  76. tx.Model(&OrgUser{}).Select("COUNT(*)").Where("org_id = ?", orgID),
  77. ).
  78. Error
  79. if err != nil {
  80. return errors.Wrap(err, `update "user.num_members"`)
  81. }
  82. return nil
  83. }
  84. func (db *orgs) AddMember(ctx context.Context, orgID, userID int64) error {
  85. return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  86. ou := &OrgUser{
  87. UserID: userID,
  88. OrgID: orgID,
  89. }
  90. result := tx.FirstOrCreate(ou, ou)
  91. if result.Error != nil {
  92. return errors.Wrap(result.Error, "upsert")
  93. } else if result.RowsAffected <= 0 {
  94. return nil // Relation already exists
  95. }
  96. return db.recountMembers(tx, orgID)
  97. })
  98. }
  99. type ErrLastOrgOwner struct {
  100. args map[string]any
  101. }
  102. func IsErrLastOrgOwner(err error) bool {
  103. return errors.As(err, &ErrLastOrgOwner{})
  104. }
  105. func (err ErrLastOrgOwner) Error() string {
  106. return fmt.Sprintf("user is the last owner of the organization: %v", err.args)
  107. }
  108. func (db *orgs) RemoveMember(ctx context.Context, orgID, userID int64) error {
  109. ou, err := db.getOrgUser(ctx, orgID, userID)
  110. if err != nil {
  111. if errors.Is(err, gorm.ErrRecordNotFound) {
  112. return nil // Not a member
  113. }
  114. return errors.Wrap(err, "check organization membership")
  115. }
  116. // Check if the member to remove is the last owner.
  117. if ou.IsOwner {
  118. t, err := db.GetTeamByName(ctx, orgID, TeamNameOwners)
  119. if err != nil {
  120. return errors.Wrap(err, "get owners team")
  121. } else if t.NumMembers == 1 {
  122. return ErrLastOrgOwner{args: map[string]any{"orgID": orgID, "userID": userID}}
  123. }
  124. }
  125. return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  126. repoIDsConds := db.accessibleRepositoriesByUser(tx, orgID, userID, accessibleRepositoriesByUserOptions{}).Select("repository.id")
  127. err := tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Watch{}).Error
  128. if err != nil {
  129. return errors.Wrap(err, "unwatch repositories")
  130. }
  131. err = tx.Table("repository").
  132. Where("id IN (?)", repoIDsConds).
  133. UpdateColumn("num_watches", gorm.Expr("num_watches - 1")).
  134. Error
  135. if err != nil {
  136. return errors.Wrap(err, `decrease "repository.num_watches"`)
  137. }
  138. err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Access{}).Error
  139. if err != nil {
  140. return errors.Wrap(err, "delete repository accesses")
  141. }
  142. err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Collaboration{}).Error
  143. if err != nil {
  144. return errors.Wrap(err, "delete repository collaborations")
  145. }
  146. /*
  147. Equivalent SQL for PostgreSQL:
  148. UPDATE "team"
  149. SET num_members = num_members - 1
  150. WHERE id IN (
  151. SELECT team_id FROM "team_user"
  152. WHERE team_user.org_id = @orgID AND uid = @userID)
  153. )
  154. */
  155. err = tx.Table("team").
  156. Where(`id IN (?)`, tx.
  157. Select("team_id").
  158. Table("team_user").
  159. Where("org_id = ? AND uid = ?", orgID, userID),
  160. ).
  161. UpdateColumn("num_members", gorm.Expr("num_members - 1")).
  162. Error
  163. if err != nil {
  164. return errors.Wrap(err, `decrease "team.num_members"`)
  165. }
  166. err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&TeamUser{}).Error
  167. if err != nil {
  168. return errors.Wrap(err, "delete team membership")
  169. }
  170. err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&OrgUser{}).Error
  171. if err != nil {
  172. return errors.Wrap(err, "delete organization membership")
  173. }
  174. return db.recountMembers(tx, orgID)
  175. })
  176. }
  177. type accessibleRepositoriesByUserOptions struct {
  178. orderBy string
  179. page int
  180. pageSize int
  181. }
  182. func (*orgs) accessibleRepositoriesByUser(tx *gorm.DB, orgID, userID int64, opts accessibleRepositoriesByUserOptions) *gorm.DB {
  183. /*
  184. Equivalent SQL for PostgreSQL:
  185. <SELECT * FROM "repository">
  186. JOIN team_repo ON repository.id = team_repo.repo_id
  187. WHERE
  188. owner_id = @orgID
  189. AND (
  190. team_repo.team_id IN (
  191. SELECT team_id FROM "team_user"
  192. WHERE team_user.org_id = @orgID AND uid = @userID)
  193. )
  194. OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
  195. )
  196. [ORDER BY updated_unix DESC]
  197. [LIMIT @limit OFFSET @offset]
  198. */
  199. conds := tx.
  200. Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
  201. Where("owner_id = ? AND (?)", orgID, tx.
  202. Where("team_repo.team_id IN (?)", tx.
  203. Select("team_id").
  204. Table("team_user").
  205. Where("team_user.org_id = ? AND uid = ?", orgID, userID),
  206. ).
  207. Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
  208. )
  209. if opts.orderBy != "" {
  210. conds.Order(opts.orderBy)
  211. }
  212. if opts.page > 0 && opts.pageSize > 0 {
  213. conds.Limit(opts.pageSize).Offset((opts.page - 1) * opts.pageSize)
  214. }
  215. return conds
  216. }
  217. type AccessibleRepositoriesByUserOptions struct {
  218. // Whether to skip counting the total number of repositories.
  219. SkipCount bool
  220. }
  221. func (db *orgs) AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error) {
  222. conds := db.accessibleRepositoriesByUser(
  223. db.DB,
  224. orgID,
  225. userID,
  226. accessibleRepositoriesByUserOptions{
  227. orderBy: "updated_unix DESC",
  228. page: page,
  229. pageSize: pageSize,
  230. },
  231. ).WithContext(ctx)
  232. repos := make([]*Repository, 0, pageSize)
  233. err := conds.Find(&repos).Error
  234. if err != nil {
  235. return nil, 0, errors.Wrap(err, "list repositories")
  236. }
  237. if opts.SkipCount {
  238. return repos, 0, nil
  239. }
  240. var count int64
  241. err = conds.Model(&Repository{}).Count(&count).Error
  242. if err != nil {
  243. return nil, 0, errors.Wrap(err, "count repositories")
  244. }
  245. return repos, count, nil
  246. }
  247. func (db *orgs) getOrgUser(ctx context.Context, orgID, userID int64) (*OrgUser, error) {
  248. var ou OrgUser
  249. return &ou, db.WithContext(ctx).Where("org_id = ? AND uid = ?", orgID, userID).First(&ou).Error
  250. }
  251. func (db *orgs) IsOwnedBy(ctx context.Context, orgID, userID int64) bool {
  252. ou, err := db.getOrgUser(ctx, orgID, userID)
  253. return err == nil && ou.IsOwner
  254. }
  255. func (db *orgs) SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error {
  256. return db.Table("org_user").Where("org_id = ? AND uid = ?", orgID, userID).UpdateColumn("is_public", public).Error
  257. }
  258. func (db *orgs) HasMember(ctx context.Context, orgID, userID int64) (bool, bool) {
  259. ou, err := db.getOrgUser(ctx, orgID, userID)
  260. return err == nil, ou != nil && ou.IsPublic
  261. }
  262. type ListOrgMembersOptions struct {
  263. // The maximum number of members to return.
  264. Limit int
  265. }
  266. func (db *orgs) ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error) {
  267. /*
  268. Equivalent SQL for PostgreSQL:
  269. SELECT * FROM "user"
  270. JOIN org_user ON org_user.uid = user.id
  271. WHERE
  272. org_user.org_id = @orgID
  273. ORDER BY user.id ASC
  274. [LIMIT @limit]
  275. */
  276. conds := db.WithContext(ctx).
  277. Joins(dbutil.Quote("JOIN org_user ON org_user.uid = %s.id", "user")).
  278. Where("org_user.org_id = ?", orgID).
  279. Order(dbutil.Quote("%s.id ASC", "user"))
  280. if opts.Limit > 0 {
  281. conds.Limit(opts.Limit)
  282. }
  283. var users []*User
  284. return users, conds.Find(&users).Error
  285. }
  286. type ListOrgsOptions struct {
  287. // Filter by the membership with the given user ID.
  288. MemberID int64
  289. // Whether to include private memberships.
  290. IncludePrivateMembers bool
  291. }
  292. func (db *orgs) List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error) {
  293. if opts.MemberID <= 0 {
  294. return nil, errors.New("MemberID must be greater than 0")
  295. }
  296. /*
  297. Equivalent SQL for PostgreSQL:
  298. SELECT * FROM "user"
  299. JOIN org_user ON org_user.org_id = user.id
  300. WHERE
  301. org_user.uid = @memberID
  302. [AND org_user.is_public = @includePrivateMembers]
  303. ORDER BY user.id ASC
  304. */
  305. conds := db.WithContext(ctx).
  306. Joins(dbutil.Quote("JOIN org_user ON org_user.org_id = %s.id", "user")).
  307. Where("org_user.uid = ?", opts.MemberID).
  308. Order(dbutil.Quote("%s.id ASC", "user"))
  309. if !opts.IncludePrivateMembers {
  310. conds.Where("org_user.is_public = ?", true)
  311. }
  312. var orgs []*Organization
  313. return orgs, conds.Find(&orgs).Error
  314. }
  315. var _ errutil.NotFound = (*ErrUserNotExist)(nil)
  316. type ErrOrganizationNotExist struct {
  317. args errutil.Args
  318. }
  319. // IsErrOrganizationNotExist returns true if the underlying error has the type
  320. // ErrOrganizationNotExist.
  321. func IsErrOrganizationNotExist(err error) bool {
  322. return errors.As(err, &ErrOrganizationNotExist{})
  323. }
  324. func (err ErrOrganizationNotExist) Error() string {
  325. return fmt.Sprintf("organization does not exist: %v", err.args)
  326. }
  327. func (ErrOrganizationNotExist) NotFound() bool {
  328. return true
  329. }
  330. func (db *orgs) GetByName(ctx context.Context, name string) (*Organization, error) {
  331. org, err := getUserByUsername(ctx, db.DB, UserTypeOrganization, name)
  332. if err != nil {
  333. if IsErrUserNotExist(err) {
  334. return nil, ErrOrganizationNotExist{args: map[string]any{"name": name}}
  335. }
  336. return nil, errors.Wrap(err, "get organization by name")
  337. }
  338. return org, nil
  339. }
  340. func (db *orgs) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) {
  341. return searchUserByName(ctx, db.DB, UserTypeOrganization, keyword, page, pageSize, orderBy)
  342. }
  343. func (db *orgs) CountByUser(ctx context.Context, userID int64) (int64, error) {
  344. var count int64
  345. return count, db.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error
  346. }
  347. func (db *orgs) Count(ctx context.Context) int64 {
  348. var count int64
  349. db.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeOrganization).Count(&count)
  350. return count
  351. }
  352. var _ errutil.NotFound = (*ErrTeamNotExist)(nil)
  353. type ErrTeamNotExist struct {
  354. args map[string]any
  355. }
  356. func IsErrTeamNotExist(err error) bool {
  357. return errors.As(err, &ErrTeamNotExist{})
  358. }
  359. func (err ErrTeamNotExist) Error() string {
  360. return fmt.Sprintf("team does not exist: %v", err.args)
  361. }
  362. func (ErrTeamNotExist) NotFound() bool {
  363. return true
  364. }
  365. func (db *orgs) GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error) {
  366. var team Team
  367. err := db.WithContext(ctx).Where("org_id = ? AND lower_name = ?", orgID, strings.ToLower(name)).First(&team).Error
  368. if err != nil {
  369. if errors.Is(err, gorm.ErrRecordNotFound) {
  370. return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
  371. }
  372. return nil, errors.Wrap(err, "get team by name")
  373. }
  374. return &team, nil
  375. }
  376. type Organization = User
  377. func (u *Organization) TableName() string {
  378. return "user"
  379. }
  380. // IsOwnedBy returns true if the given user is an owner of the organization.
  381. //
  382. // TODO(unknwon): This is also used in templates, which should be fixed by
  383. // having a dedicated type `template.Organization`.
  384. func (u *Organization) IsOwnedBy(userID int64) bool {
  385. return Orgs.IsOwnedBy(context.TODO(), u.ID, userID)
  386. }
  387. // OrgUser represents relations of organizations and their members.
  388. type OrgUser struct {
  389. ID int64 `gorm:"primaryKey"`
  390. UserID int64 `xorm:"uid INDEX UNIQUE(s)" gorm:"column:uid;uniqueIndex:org_user_user_org_unique;index;not null" json:"Uid"`
  391. OrgID int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"`
  392. IsPublic bool `gorm:"not null;default:FALSE"`
  393. IsOwner bool `gorm:"not null;default:FALSE"`
  394. NumTeams int `gorm:"not null;default:0"`
  395. }