organizations.go 18 KB

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