organizations.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  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. "os"
  9. "strings"
  10. "github.com/pkg/errors"
  11. "gorm.io/gorm"
  12. "gogs.io/gogs/internal/dbutil"
  13. "gogs.io/gogs/internal/errutil"
  14. "gogs.io/gogs/internal/repoutil"
  15. "gogs.io/gogs/internal/userutil"
  16. )
  17. // OrganizationsStore is the persistent interface for organizations.
  18. type OrganizationsStore interface {
  19. // Create creates a new organization with the initial owner and persists to
  20. // database. It returns ErrNameNotAllowed if the given name or pattern of the
  21. // name is not allowed as an organization name, or ErrOrganizationAlreadyExist
  22. // when a user or an organization with same name already exists.
  23. Create(ctx context.Context, name string, ownerID int64, opts CreateOrganizationOptions) (*Organization, error)
  24. // GetByName returns the organization with given name. It returns
  25. // ErrOrganizationNotExist when not found.
  26. GetByName(ctx context.Context, name string) (*Organization, error)
  27. // SearchByName returns a list of organizations whose username or full name
  28. // matches the given keyword case-insensitively. Results are paginated by given
  29. // page and page size, and sorted by the given order (e.g. "id DESC"). A total
  30. // count of all results is also returned. If the order is not given, it's up to
  31. // the database to decide.
  32. SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error)
  33. // List returns a list of organizations filtered by options.
  34. List(ctx context.Context, opts ListOrganizationsOptions) ([]*Organization, error)
  35. // CountByUser returns the number of organizations the user is a member of.
  36. CountByUser(ctx context.Context, userID int64) (int64, error)
  37. // Count returns the total number of organizations.
  38. Count(ctx context.Context) int64
  39. // DeleteByID deletes the given organization and all their resources. It returns
  40. // ErrOrganizationOwnRepos when the user still has repository ownership.
  41. DeleteByID(ctx context.Context, orgID int64) error
  42. // AddMember adds a new member to the given organization.
  43. AddMember(ctx context.Context, orgID, userID int64) error
  44. // RemoveMember removes a member from the given organization.
  45. RemoveMember(ctx context.Context, orgID, userID int64) error
  46. // HasMember returns whether the given user is a member of the organization
  47. // (first), and whether the organization membership is public (second).
  48. HasMember(ctx context.Context, orgID, userID int64) (bool, bool)
  49. // ListMembers returns all members of the given organization, and sorted by the
  50. // given order (e.g. "id ASC").
  51. ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error)
  52. // IsOwnedBy returns true if the given user is an owner of the organization.
  53. IsOwnedBy(ctx context.Context, orgID, userID int64) bool
  54. // SetMemberVisibility sets the visibility of the given user in the organization.
  55. SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error
  56. // GetTeamByName returns the team with given name under the given organization.
  57. // It returns ErrTeamNotExist whe not found.
  58. GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error)
  59. // AccessibleRepositoriesByUser returns a range of repositories in the
  60. // organization that the user has access to and the total number of it. Results
  61. // are paginated by given page and page size, and OrderByUpdatedDesc is used.
  62. AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error)
  63. // MirrorRepositoriesByUser returns a list of mirror repositories of the
  64. // organization which the user has access to.
  65. MirrorRepositoriesByUser(ctx context.Context, orgID, userID int64) ([]*Repository, error)
  66. }
  67. var Organizations OrganizationsStore
  68. var _ OrganizationsStore = (*organizations)(nil)
  69. type organizations struct {
  70. *gorm.DB
  71. }
  72. // NewOrganizationsStore returns a persistent interface for orgs with given
  73. // database connection.
  74. func NewOrganizationsStore(db *gorm.DB) OrganizationsStore {
  75. return &organizations{DB: db}
  76. }
  77. func (*organizations) recountMembers(tx *gorm.DB, orgID int64) error {
  78. /*
  79. Equivalent SQL for PostgreSQL:
  80. UPDATE "user"
  81. SET num_members = (
  82. SELECT COUNT(*) FROM org_user WHERE org_id = @orgID
  83. )
  84. WHERE id = @orgID
  85. */
  86. err := tx.Model(&User{}).
  87. Where("id = ?", orgID).
  88. Update(
  89. "num_members",
  90. tx.Model(&OrgUser{}).Select("COUNT(*)").Where("org_id = ?", orgID),
  91. ).
  92. Error
  93. if err != nil {
  94. return errors.Wrap(err, `update "user.num_members"`)
  95. }
  96. return nil
  97. }
  98. func (db *organizations) AddMember(ctx context.Context, orgID, userID int64) error {
  99. return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  100. ou := &OrgUser{
  101. UserID: userID,
  102. OrgID: orgID,
  103. }
  104. result := tx.FirstOrCreate(ou, ou)
  105. if result.Error != nil {
  106. return errors.Wrap(result.Error, "upsert")
  107. } else if result.RowsAffected <= 0 {
  108. return nil // Relation already exists
  109. }
  110. return db.recountMembers(tx, orgID)
  111. })
  112. }
  113. type ErrLastOrgOwner struct {
  114. args map[string]any
  115. }
  116. func IsErrLastOrgOwner(err error) bool {
  117. return errors.As(err, &ErrLastOrgOwner{})
  118. }
  119. func (err ErrLastOrgOwner) Error() string {
  120. return fmt.Sprintf("user is the last owner of the organization: %v", err.args)
  121. }
  122. func (db *organizations) RemoveMember(ctx context.Context, orgID, userID int64) error {
  123. ou, err := db.getOrgUser(ctx, orgID, userID)
  124. if err != nil {
  125. if errors.Is(err, gorm.ErrRecordNotFound) {
  126. return nil // Not a member
  127. }
  128. return errors.Wrap(err, "check organization membership")
  129. }
  130. // Check if the member to remove is the last owner.
  131. if ou.IsOwner {
  132. t, err := db.GetTeamByName(ctx, orgID, TeamNameOwners)
  133. if err != nil {
  134. return errors.Wrap(err, "get owners team")
  135. } else if t.NumMembers == 1 {
  136. return ErrLastOrgOwner{args: errutil.Args{"orgID": orgID, "userID": userID}}
  137. }
  138. }
  139. return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  140. repoIDsConds := db.accessibleRepositoriesByUser(tx, orgID, userID, accessibleRepositoriesByUserOptions{}).Select("repository.id")
  141. err := tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Watch{}).Error
  142. if err != nil {
  143. return errors.Wrap(err, "unwatch repositories")
  144. }
  145. err = tx.
  146. Table("repository").
  147. Where("id IN (?)", repoIDsConds).
  148. UpdateColumn("num_watches", gorm.Expr("num_watches - 1")).
  149. Error
  150. if err != nil {
  151. return errors.Wrap(err, `decrease "repository.num_watches"`)
  152. }
  153. err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Access{}).Error
  154. if err != nil {
  155. return errors.Wrap(err, "delete repository accesses")
  156. }
  157. err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Collaboration{}).Error
  158. if err != nil {
  159. return errors.Wrap(err, "delete repository collaborations")
  160. }
  161. /*
  162. Equivalent SQL for PostgreSQL:
  163. UPDATE "team"
  164. SET num_members = num_members - 1
  165. WHERE id IN (
  166. SELECT team_id FROM "team_user"
  167. WHERE team_user.org_id = @orgID AND uid = @userID)
  168. )
  169. */
  170. err = tx.
  171. Table("team").
  172. Where("id IN (?)", tx.
  173. Select("team_id").
  174. Table("team_user").
  175. Where("org_id = ? AND uid = ?", orgID, userID),
  176. ).
  177. UpdateColumn("num_members", gorm.Expr("num_members - 1")).
  178. Error
  179. if err != nil {
  180. return errors.Wrap(err, `decrease "team.num_members"`)
  181. }
  182. err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&TeamUser{}).Error
  183. if err != nil {
  184. return errors.Wrap(err, "delete team membership")
  185. }
  186. err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&OrgUser{}).Error
  187. if err != nil {
  188. return errors.Wrap(err, "delete organization membership")
  189. }
  190. return db.recountMembers(tx, orgID)
  191. })
  192. }
  193. type OrderBy int
  194. const (
  195. OrderByIDAsc OrderBy = iota + 1
  196. OrderByUpdatedDesc
  197. )
  198. type accessibleRepositoriesByUserOptions struct {
  199. orderBy OrderBy
  200. page int
  201. pageSize int
  202. }
  203. func (*organizations) accessibleRepositoriesByUser(tx *gorm.DB, orgID, userID int64, opts accessibleRepositoriesByUserOptions) *gorm.DB {
  204. /*
  205. Equivalent SQL for PostgreSQL:
  206. SELECT * FROM "repository"
  207. WHERE
  208. owner_id = @orgID
  209. AND (
  210. id IN (
  211. SELECT repo_id
  212. FROM "team_repo"
  213. JOIN "team_user" ON team_user.org_id = team_repo.org_id
  214. WHERE team_repo.org_id = @orgID AND team_user.uid = @userID
  215. )
  216. OR (is_private = FALSE AND is_unlisted = FALSE)
  217. )
  218. [ORDER BY updated_unix DESC]
  219. [LIMIT @limit OFFSET @offset]
  220. */
  221. conds := tx.
  222. Table("repository").
  223. Where("owner_id = ? AND (id IN (?) OR (is_private = ? AND is_unlisted = ?))",
  224. orgID,
  225. tx.Select("repo_id").
  226. Table("team_repo").
  227. Joins("JOIN team_user ON team_user.org_id = team_repo.org_id").
  228. Where("team_repo.org_id = ? AND team_user.uid = ?", orgID, userID),
  229. false,
  230. false,
  231. )
  232. if opts.orderBy == OrderByUpdatedDesc {
  233. conds.Order("updated_unix DESC")
  234. }
  235. if opts.page > 0 && opts.pageSize > 0 {
  236. conds.Limit(opts.pageSize).Offset((opts.page - 1) * opts.pageSize)
  237. }
  238. return conds
  239. }
  240. type AccessibleRepositoriesByUserOptions struct {
  241. // Whether to skip counting the total number of repositories.
  242. SkipCount bool
  243. }
  244. func (db *organizations) AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error) {
  245. conds := db.accessibleRepositoriesByUser(
  246. db.DB,
  247. orgID,
  248. userID,
  249. accessibleRepositoriesByUserOptions{
  250. orderBy: OrderByUpdatedDesc,
  251. page: page,
  252. pageSize: pageSize,
  253. },
  254. ).WithContext(ctx)
  255. repos := make([]*Repository, 0, pageSize)
  256. err := conds.Find(&repos).Error
  257. if err != nil {
  258. return nil, 0, errors.Wrap(err, "list repositories")
  259. }
  260. if opts.SkipCount {
  261. return repos, 0, nil
  262. }
  263. var count int64
  264. err = conds.Count(&count).Error
  265. if err != nil {
  266. return nil, 0, errors.Wrap(err, "count repositories")
  267. }
  268. return repos, count, nil
  269. }
  270. func (db *organizations) MirrorRepositoriesByUser(ctx context.Context, orgID, userID int64) ([]*Repository, error) {
  271. /*
  272. Equivalent SQL for PostgreSQL:
  273. SELECT * FROM "repository"
  274. JOIN team_repo ON repository.id = team_repo.repo_id
  275. WHERE
  276. owner_id = @orgID
  277. AND repository.is_mirror = TRUE
  278. AND (
  279. team_repo.team_id IN (
  280. SELECT team_id FROM "team_user"
  281. WHERE team_user.org_id = @orgID AND uid = @userID)
  282. )
  283. OR repository.is_private = FALSE
  284. )
  285. ORDER BY updated_unix DESC
  286. */
  287. var repos []*Repository
  288. return repos, db.WithContext(ctx).
  289. Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
  290. Where("owner_id = ? AND repository.is_mirror = ? AND (?)", orgID, true, db.
  291. Where("team_repo.team_id IN (?)", db.
  292. Select("team_id").
  293. Table("team_user").
  294. Where("team_user.org_id = ? AND uid = ?", orgID, userID),
  295. ).
  296. Or("repository.is_private = ?", false),
  297. ).
  298. Order("updated_unix DESC").
  299. Find(&repos).
  300. Error
  301. }
  302. func (db *organizations) getOrgUser(ctx context.Context, orgID, userID int64) (*OrgUser, error) {
  303. var ou OrgUser
  304. return &ou, db.WithContext(ctx).Where("org_id = ? AND uid = ?", orgID, userID).First(&ou).Error
  305. }
  306. func (db *organizations) IsOwnedBy(ctx context.Context, orgID, userID int64) bool {
  307. ou, err := db.getOrgUser(ctx, orgID, userID)
  308. return err == nil && ou.IsOwner
  309. }
  310. func (db *organizations) SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error {
  311. return db.Table("org_user").Where("org_id = ? AND uid = ?", orgID, userID).UpdateColumn("is_public", public).Error
  312. }
  313. func (db *organizations) HasMember(ctx context.Context, orgID, userID int64) (bool, bool) {
  314. ou, err := db.getOrgUser(ctx, orgID, userID)
  315. return err == nil, ou != nil && ou.IsPublic
  316. }
  317. type ListOrgMembersOptions struct {
  318. // The maximum number of members to return.
  319. Limit int
  320. }
  321. func (db *organizations) ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error) {
  322. /*
  323. Equivalent SQL for PostgreSQL:
  324. SELECT * FROM "user"
  325. JOIN org_user ON org_user.uid = user.id
  326. WHERE
  327. org_user.org_id = @orgID
  328. ORDER BY user.id ASC
  329. [LIMIT @limit]
  330. */
  331. conds := db.WithContext(ctx).
  332. Joins(dbutil.Quote("JOIN org_user ON org_user.uid = %s.id", "user")).
  333. Where("org_user.org_id = ?", orgID).
  334. Order(dbutil.Quote("%s.id ASC", "user"))
  335. if opts.Limit > 0 {
  336. conds.Limit(opts.Limit)
  337. }
  338. var users []*User
  339. return users, conds.Find(&users).Error
  340. }
  341. type ListOrganizationsOptions struct {
  342. // Filter by the membership with the given user ID. It cannot be set when the
  343. // OwnerID is also set.
  344. MemberID int64
  345. // Filter by the ownership with the given user ID. It cannot be set when the
  346. // MemberID is also set.
  347. OwnerID int64
  348. // Whether to include private memberships.
  349. IncludePrivateMembers bool
  350. // Order by the given field and direction. Default is OrderByIDAsc.
  351. OrderBy OrderBy
  352. // 1-based page number.
  353. Page int
  354. // Number of results per page.
  355. PageSize int
  356. }
  357. func (db *organizations) List(ctx context.Context, opts ListOrganizationsOptions) ([]*Organization, error) {
  358. if opts.MemberID > 0 && opts.OwnerID > 0 {
  359. return nil, errors.New("cannot filter by both MemberID and OwnerID")
  360. }
  361. /*
  362. Equivalent SQL for PostgreSQL:
  363. SELECT * FROM "user"
  364. [JOIN org_user ON org_user.org_id = user.id]
  365. WHERE
  366. type = @type
  367. [AND org_user.uid = (@memberID | @ownerID)
  368. AND org_user.is_public = @includePrivateMembers
  369. AND org_user.is_owner = @ownerID > 0]
  370. ORDER BY (user.id ASC | user.updated_unix DESC)
  371. [LIMIT @limit OFFSET @offset]
  372. */
  373. conds := db.WithContext(ctx).Where("type = ?", UserTypeOrganization)
  374. if opts.MemberID > 0 || opts.OwnerID > 0 {
  375. conds.Joins(dbutil.Quote("JOIN org_user ON org_user.org_id = %s.id", "user"))
  376. }
  377. if opts.MemberID > 0 {
  378. conds.Where("org_user.uid = ?", opts.MemberID)
  379. } else if opts.OwnerID > 0 {
  380. conds.Where("org_user.uid = ? AND org_user.is_owner = ?", opts.OwnerID, true)
  381. }
  382. if (opts.MemberID > 0 || opts.OwnerID > 0) && !opts.IncludePrivateMembers {
  383. conds.Where("org_user.is_public = ?", true)
  384. }
  385. if opts.OrderBy == OrderByUpdatedDesc {
  386. conds.Order(dbutil.Quote("%s.updated_unix DESC", "user"))
  387. } else {
  388. conds.Order(dbutil.Quote("%s.id ASC", "user"))
  389. }
  390. if opts.Page > 0 && opts.PageSize > 0 {
  391. conds.Limit(opts.PageSize).Offset((opts.Page - 1) * opts.PageSize)
  392. }
  393. var orgs []*Organization
  394. return orgs, conds.Find(&orgs).Error
  395. }
  396. type CreateOrganizationOptions struct {
  397. FullName string
  398. Email string
  399. Location string
  400. Website string
  401. Description string
  402. }
  403. type ErrOrganizationAlreadyExist struct {
  404. args errutil.Args
  405. }
  406. // IsErrOrganizationAlreadyExist returns true if the underlying error has the
  407. // type ErrOrganizationAlreadyExist.
  408. func IsErrOrganizationAlreadyExist(err error) bool {
  409. return errors.As(err, &ErrOrganizationAlreadyExist{})
  410. }
  411. func (err ErrOrganizationAlreadyExist) Error() string {
  412. return fmt.Sprintf("organization already exists: %v", err.args)
  413. }
  414. func (db *organizations) Create(ctx context.Context, name string, ownerID int64, opts CreateOrganizationOptions) (*Organization, error) {
  415. err := isUsernameAllowed(name)
  416. if err != nil {
  417. return nil, err
  418. }
  419. if NewUsersStore(db.DB).IsUsernameUsed(ctx, name, 0) {
  420. return nil, ErrOrganizationAlreadyExist{
  421. args: errutil.Args{
  422. "name": name,
  423. },
  424. }
  425. }
  426. org := &Organization{
  427. LowerName: strings.ToLower(name),
  428. Name: name,
  429. FullName: opts.FullName,
  430. Email: opts.Email,
  431. Type: UserTypeOrganization,
  432. Location: opts.Location,
  433. Website: opts.Website,
  434. MaxRepoCreation: -1,
  435. IsActive: true,
  436. UseCustomAvatar: true,
  437. Description: opts.Description,
  438. NumTeams: 1, // The default "owners" team
  439. NumMembers: 1, // The initial owner
  440. }
  441. org.Rands, err = userutil.RandomSalt()
  442. if err != nil {
  443. return nil, err
  444. }
  445. org.Salt, err = userutil.RandomSalt()
  446. if err != nil {
  447. return nil, err
  448. }
  449. return org, db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  450. err := tx.Create(org).Error
  451. if err != nil {
  452. return errors.Wrap(err, "create organization")
  453. }
  454. err = tx.Create(&OrgUser{
  455. UserID: ownerID,
  456. OrgID: org.ID,
  457. IsOwner: true,
  458. NumTeams: 1,
  459. }).Error
  460. if err != nil {
  461. return errors.Wrap(err, "create org-user relation")
  462. }
  463. team := &Team{
  464. OrgID: org.ID,
  465. LowerName: strings.ToLower(TeamNameOwners),
  466. Name: TeamNameOwners,
  467. Authorize: AccessModeOwner,
  468. NumMembers: 1,
  469. }
  470. err = tx.Create(team).Error
  471. if err != nil {
  472. return errors.Wrap(err, "create owner team")
  473. }
  474. err = tx.Create(&TeamUser{
  475. UID: ownerID,
  476. OrgID: org.ID,
  477. TeamID: team.ID,
  478. }).Error
  479. if err != nil {
  480. return errors.Wrap(err, "create team-user relation")
  481. }
  482. err = userutil.GenerateRandomAvatar(org.ID, org.Name, org.Email)
  483. if err != nil {
  484. return errors.Wrap(err, "generate organization avatar")
  485. }
  486. err = os.MkdirAll(repoutil.UserPath(org.Name), os.ModePerm)
  487. if err != nil {
  488. return errors.Wrap(err, "create organization directory")
  489. }
  490. return nil
  491. })
  492. }
  493. var _ errutil.NotFound = (*ErrUserNotExist)(nil)
  494. type ErrOrganizationNotExist struct {
  495. args errutil.Args
  496. }
  497. // IsErrOrganizationNotExist returns true if the underlying error has the type
  498. // ErrOrganizationNotExist.
  499. func IsErrOrganizationNotExist(err error) bool {
  500. return errors.As(err, &ErrOrganizationNotExist{})
  501. }
  502. func (err ErrOrganizationNotExist) Error() string {
  503. return fmt.Sprintf("organization does not exist: %v", err.args)
  504. }
  505. func (ErrOrganizationNotExist) NotFound() bool {
  506. return true
  507. }
  508. func (db *organizations) GetByName(ctx context.Context, name string) (*Organization, error) {
  509. org, err := getUserByUsername(ctx, db.DB, UserTypeOrganization, name)
  510. if err != nil {
  511. if IsErrUserNotExist(err) {
  512. return nil, ErrOrganizationNotExist{args: map[string]any{"name": name}}
  513. }
  514. return nil, errors.Wrap(err, "get organization by name")
  515. }
  516. return org, nil
  517. }
  518. func (db *organizations) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) {
  519. return searchUserByName(ctx, db.DB, UserTypeOrganization, keyword, page, pageSize, orderBy)
  520. }
  521. func (db *organizations) CountByUser(ctx context.Context, userID int64) (int64, error) {
  522. var count int64
  523. return count, db.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error
  524. }
  525. func (db *organizations) Count(ctx context.Context) int64 {
  526. var count int64
  527. db.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeOrganization).Count(&count)
  528. return count
  529. }
  530. type ErrOrganizationOwnRepos struct {
  531. args errutil.Args
  532. }
  533. // IsErrOrganizationOwnRepos returns true if the underlying error has the type
  534. // ErrOrganizationOwnRepos.
  535. func IsErrOrganizationOwnRepos(err error) bool {
  536. return errors.As(errors.Cause(err), &ErrOrganizationOwnRepos{})
  537. }
  538. func (err ErrOrganizationOwnRepos) Error() string {
  539. return fmt.Sprintf("organization still has repository ownership: %v", err.args)
  540. }
  541. func (db *organizations) DeleteByID(ctx context.Context, orgID int64) error {
  542. return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  543. for _, t := range []any{&Team{}, &OrgUser{}, &TeamUser{}} {
  544. err := tx.Where("org_id = ?", orgID).Delete(t).Error
  545. if err != nil {
  546. return errors.Wrapf(err, "clean up table %T", t)
  547. }
  548. }
  549. err := NewUsersStore(tx).DeleteByID(ctx, orgID, false)
  550. if err != nil {
  551. if IsErrUserOwnRepos(err) {
  552. return ErrOrganizationOwnRepos{args: map[string]any{"orgID": orgID}}
  553. }
  554. return errors.Wrap(err, "delete organization")
  555. }
  556. return nil
  557. })
  558. }
  559. var _ errutil.NotFound = (*ErrTeamNotExist)(nil)
  560. type ErrTeamNotExist struct {
  561. args map[string]any
  562. }
  563. func IsErrTeamNotExist(err error) bool {
  564. return errors.As(err, &ErrTeamNotExist{})
  565. }
  566. func (err ErrTeamNotExist) Error() string {
  567. return fmt.Sprintf("team does not exist: %v", err.args)
  568. }
  569. func (ErrTeamNotExist) NotFound() bool {
  570. return true
  571. }
  572. func (db *organizations) GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error) {
  573. var team Team
  574. err := db.WithContext(ctx).Where("org_id = ? AND lower_name = ?", orgID, strings.ToLower(name)).First(&team).Error
  575. if err != nil {
  576. if errors.Is(err, gorm.ErrRecordNotFound) {
  577. return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
  578. }
  579. return nil, errors.Wrap(err, "get team by name")
  580. }
  581. return &team, nil
  582. }
  583. type Organization = User
  584. func (u *Organization) TableName() string {
  585. return "user"
  586. }
  587. // IsOwnedBy returns true if the given user is an owner of the organization.
  588. //
  589. // TODO(unknwon): This is also used in templates, which should be fixed by
  590. // having a dedicated type `template.Organization`.
  591. func (u *Organization) IsOwnedBy(userID int64) bool {
  592. return Organizations.IsOwnedBy(context.TODO(), u.ID, userID)
  593. }
  594. // OrgUser represents relations of organizations and their members.
  595. type OrgUser struct {
  596. ID int64 `gorm:"primaryKey"`
  597. UserID int64 `xorm:"uid INDEX UNIQUE(s)" gorm:"column:uid;uniqueIndex:org_user_user_org_unique;index;not null" json:"Uid"`
  598. OrgID int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"`
  599. IsPublic bool `gorm:"not null;default:FALSE"`
  600. IsOwner bool `gorm:"not null;default:FALSE"`
  601. NumTeams int `gorm:"not null;default:0"`
  602. }