From 76f89047183fc70a077ac37b4168d90539f12e09 Mon Sep 17 00:00:00 2001 From: Peter Smit Date: Fri, 23 Jan 2015 09:54:16 +0200 Subject: [PATCH 01/26] Introducing Collaboration Struct --- models/access.go | 20 ++++++ models/issue.go | 6 +- models/migrations/migrations.go | 98 ++++++++++++++++++++++++++- models/models.go | 7 +- models/repo.go | 113 ++++++++++++++++++-------------- routers/api/v1/repo.go | 21 +++--- routers/repo/issue.go | 26 ++++---- routers/repo/setting.go | 57 ++++------------ routers/user/home.go | 14 ++-- 9 files changed, 234 insertions(+), 128 deletions(-) diff --git a/models/access.go b/models/access.go index 81aa43dc78..65f1d00323 100644 --- a/models/access.go +++ b/models/access.go @@ -78,3 +78,23 @@ func HasAccess(uname, repoName string, mode AccessType) (bool, error) { } return true, nil } + +// GetAccessibleRepositories finds all repositories where a user has access to, +// besides his own. +func (u *User) GetAccessibleRepositories() (map[*Repository]AccessType, error) { + accesses := make([]*Access, 0, 10) + if err := x.Find(&accesses, &Access{UserName: u.LowerName}); err != nil { + return nil, err + } + + repos := make(map[*Repository]AccessType, len(accesses)) + for _, access := range accesses { + repo, err := GetRepositoryByRef(access.RepoName) + if err != nil { + return nil, err + } + repos[repo] = access.Mode + } + + return repos, nil +} diff --git a/models/issue.go b/models/issue.go index c756e49759..8d0525d7c5 100644 --- a/models/issue.go +++ b/models/issue.go @@ -282,10 +282,10 @@ type IssueUser struct { } // NewIssueUserPairs adds new issue-user pairs for new issue of repository. -func NewIssueUserPairs(rid, iid, oid, pid, aid int64, repoName string) (err error) { - iu := &IssueUser{IssueId: iid, RepoId: rid} +func NewIssueUserPairs(repo *Repository, iid, oid, pid, aid int64) (err error) { + iu := &IssueUser{IssueId: iid, RepoId: repo.Id} - us, err := GetCollaborators(repoName) + us, err := repo.GetCollaborators() if err != nil { return err } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 3586e5d0b6..43ec4e50b0 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -2,6 +2,9 @@ package migrations import ( "errors" + "strconv" + "strings" + "time" "github.com/go-xorm/xorm" ) @@ -16,7 +19,9 @@ type Version struct { // This is a sequence of migrations. Add new migrations to the bottom of the list. // If you want to "retire" a migration, replace it with "expiredMigration" -var migrations = []migration{} +var migrations = []migration{ + accessToCollaboration, +} // Migrate database to current version func Migrate(x *xorm.Engine) error { @@ -29,6 +34,21 @@ func Migrate(x *xorm.Engine) error { if err != nil { return err } else if !has { + needsMigration, err := x.IsTableExist("user") + if err != nil { + return err + } + if needsMigration { + isEmpty, err := x.IsTableEmpty("user") + if err != nil { + return err + } + needsMigration = !isEmpty + } + if !needsMigration { + currentVersion.Version = int64(len(migrations)) + } + if _, err = x.InsertOne(currentVersion); err != nil { return err } @@ -51,3 +71,79 @@ func Migrate(x *xorm.Engine) error { func expiredMigration(x *xorm.Engine) error { return errors.New("You are migrating from a too old gogs version") } + +func mustParseInt64(in []byte) int64 { + i, err := strconv.ParseInt(string(in), 10, 64) + if err != nil { + i = 0 + } + return i +} + +func accessToCollaboration(x *xorm.Engine) error { + type Collaboration struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Created time.Time `xorm:"CREATED"` + } + + x.Sync(new(Collaboration)) + + sql := `SELECT u.id AS uid, a.repo_name AS repo, a.mode AS mode FROM access a JOIN user u ON a.user_name=u.lower_name` + results, err := x.Query(sql) + if err != nil { + return err + } + + for _, result := range results { + userID := mustParseInt64(result["uid"]) + repoRefName := string(result["repo"]) + mode := mustParseInt64(result["mode"]) + + //Collaborators must have write access + if mode < 2 { + continue + } + + parts := strings.SplitN(repoRefName, "/", 2) + ownerName := parts[0] + repoName := parts[1] + + sql = `SELECT u.id as uid, ou.uid as memberid FROM user u LEFT JOIN org_user ou ON ou.org_id=u.id WHERE u.lower_name=?` + results, err := x.Query(sql, ownerName) + if err != nil { + return err + } + if len(results) < 1 { + continue + } + ownerID := mustParseInt64(results[0]["uid"]) + + for _, member := range results { + memberID := mustParseInt64(member["memberid"]) + // We can skip all cases that a user is member of the owning organization + if memberID == userID { + continue + } + } + + sql = `SELECT id FROM repository WHERE owner_id=? AND lower_name=?` + results, err = x.Query(sql, ownerID, repoName) + if err != nil { + return err + } + if len(results) < 1 { + continue + } + + repoID := results[0]["id"] + + sql = `INSERT INTO collaboration (user_id, repo_id) VALUES (?,?)` + _, err = x.Exec(sql, userID, repoID) + if err != nil { + return err + } + } + return nil +} diff --git a/models/models.go b/models/models.go index 55e7bf5822..cf4e291cff 100644 --- a/models/models.go +++ b/models/models.go @@ -12,6 +12,7 @@ import ( "strings" _ "github.com/go-sql-driver/mysql" + "github.com/go-xorm/core" "github.com/go-xorm/xorm" _ "github.com/lib/pq" @@ -46,7 +47,7 @@ func init() { new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone), new(Mirror), new(Release), new(LoginSource), new(Webhook), new(UpdateTask), new(HookTask), new(Team), new(OrgUser), new(TeamUser), - new(Notice), new(EmailAddress)) + new(Notice), new(EmailAddress), new(Collaboration)) } func LoadModelsConfig() { @@ -100,6 +101,7 @@ func NewTestEngine(x *xorm.Engine) (err error) { return fmt.Errorf("models.init(fail to connect to database): %v", err) } + x.SetMapper(core.GonicMapper{}) return x.Sync(tables...) } @@ -109,6 +111,8 @@ func SetEngine() (err error) { return fmt.Errorf("models.init(fail to connect to database): %v", err) } + x.SetMapper(core.GonicMapper{}) + // WARNING: for serv command, MUST remove the output to os.stdout, // so use log file to instead print to stdout. logPath := path.Join(setting.LogRootPath, "xorm.log") @@ -140,6 +144,7 @@ func NewEngine() (err error) { if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil { return fmt.Errorf("sync database struct error: %v\n", err) } + return nil } diff --git a/models/repo.go b/models/repo.go index 65689b6a1e..663e227ae4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1065,71 +1065,74 @@ func GetRepositoryCount(user *User) (int64, error) { return x.Count(&Repository{OwnerId: user.Id}) } -// GetCollaboratorNames returns a list of user name of repository's collaborators. -func GetCollaboratorNames(repoName string) ([]string, error) { - accesses := make([]*Access, 0, 10) - if err := x.Find(&accesses, &Access{RepoName: strings.ToLower(repoName)}); err != nil { +// GetCollaborators returns the collaborators for a repository +func (r *Repository) GetCollaborators() ([]*User, error) { + collaborations := make([]*Collaboration, 0) + if err := x.Find(&collaborations, &Collaboration{RepoID: r.Id}); err != nil { return nil, err } - names := make([]string, len(accesses)) - for i := range accesses { - names[i] = accesses[i].UserName - } - return names, nil -} - -// CollaborativeRepository represents a repository with collaborative information. -type CollaborativeRepository struct { - *Repository - CanPush bool -} - -// GetCollaborativeRepos returns a list of repositories that user is collaborator. -func GetCollaborativeRepos(uname string) ([]*CollaborativeRepository, error) { - uname = strings.ToLower(uname) - accesses := make([]*Access, 0, 10) - if err := x.Find(&accesses, &Access{UserName: uname}); err != nil { - return nil, err - } - - repos := make([]*CollaborativeRepository, 0, 10) - for _, access := range accesses { - infos := strings.Split(access.RepoName, "/") - if infos[0] == uname { - continue - } - - u, err := GetUserByName(infos[0]) + users := make([]*User, len(collaborations)) + for i, c := range collaborations { + user, err := GetUserById(c.UserID) if err != nil { return nil, err } - - repo, err := GetRepositoryByName(u.Id, infos[1]) - if err != nil { - return nil, err - } - repo.Owner = u - repos = append(repos, &CollaborativeRepository{repo, access.Mode == WRITABLE}) + users[i] = user } - return repos, nil + return users, nil } -// GetCollaborators returns a list of users of repository's collaborators. -func GetCollaborators(repoName string) (us []*User, err error) { - accesses := make([]*Access, 0, 10) - if err = x.Find(&accesses, &Access{RepoName: strings.ToLower(repoName)}); err != nil { - return nil, err +// Add collaborator and accompanying access +func (r *Repository) AddCollaborator(u *User) error { + collaboration := &Collaboration{RepoID: r.Id, UserID: u.Id} + + has, err := x.Get(collaboration) + if err != nil { + return err + } + if has { + return nil } - us = make([]*User, len(accesses)) - for i := range accesses { - us[i], err = GetUserByName(accesses[i].UserName) + if _, err = x.InsertOne(collaboration); err != nil { + return err + } + + if err = r.GetOwner(); err != nil { + return err + } + + return AddAccess(&Access{UserName: u.LowerName, RepoName: path.Join(r.Owner.LowerName, r.LowerName), Mode: WRITABLE}) +} + +// Delete collaborator and accompanying access +func (r *Repository) DeleteCollaborator(u *User) error { + collaboration := &Collaboration{RepoID: r.Id, UserID: u.Id} + + if has, err := x.Delete(collaboration); err != nil || has == 0 { + return err + } + + if err := r.GetOwner(); err != nil { + return err + } + + needDelete := true + if r.Owner.IsOrganization() { + auth, err := GetHighestAuthorize(r.Owner.Id, u.Id, r.Id, 0) if err != nil { - return nil, err + return err + } + if auth > 0 { + needDelete = false } } - return us, nil + if needDelete { + return DeleteAccess(&Access{UserName: u.LowerName, RepoName: path.Join(r.Owner.LowerName, r.LowerName), Mode: WRITABLE}) + } + + return nil } type SearchOption struct { @@ -1547,3 +1550,11 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (*Repositor return repo, nil } + +// A Collaboration is a relation between an individual and a repository +type Collaboration struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Created time.Time `xorm:"CREATED"` +} diff --git a/routers/api/v1/repo.go b/routers/api/v1/repo.go index fbf9c73ea7..469e4808fe 100644 --- a/routers/api/v1/repo.go +++ b/routers/api/v1/repo.go @@ -237,28 +237,31 @@ func ListMyRepos(ctx *middleware.Context) { } numOwnRepos := len(ownRepos) - collaRepos, err := models.GetCollaborativeRepos(ctx.User.Name) + accessibleRepos, err := ctx.User.GetAccessibleRepositories() if err != nil { - ctx.JSON(500, &base.ApiJsonErr{"GetCollaborativeRepos: " + err.Error(), base.DOC_URL}) + ctx.JSON(500, &base.ApiJsonErr{"GetAccessibleRepositories: " + err.Error(), base.DOC_URL}) return } - repos := make([]*api.Repository, numOwnRepos+len(collaRepos)) + repos := make([]*api.Repository, numOwnRepos+len(accessibleRepos)) for i := range ownRepos { repos[i] = ToApiRepository(ctx.User, ownRepos[i], api.Permission{true, true, true}) } - for i := range collaRepos { - if err = collaRepos[i].GetOwner(); err != nil { + i := numOwnRepos + + for repo, access := range accessibleRepos { + if err = repo.GetOwner(); err != nil { ctx.JSON(500, &base.ApiJsonErr{"GetOwner: " + err.Error(), base.DOC_URL}) return } - j := i + numOwnRepos - repos[j] = ToApiRepository(collaRepos[i].Owner, collaRepos[i].Repository, api.Permission{false, collaRepos[i].CanPush, true}) + + repos[i] = ToApiRepository(repo.Owner, repo, api.Permission{false, access >= models.WRITABLE, true}) // FIXME: cache result to reduce DB query? - if collaRepos[i].Owner.IsOrganization() && collaRepos[i].Owner.IsOwnedBy(ctx.User.Id) { - repos[j].Permissions.Admin = true + if repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(ctx.User.Id) { + repos[i].Permissions.Admin = true } + i++ } ctx.JSON(200, &repos) diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 999fd0a892..921348dbd1 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -174,7 +174,7 @@ func CreateIssue(ctx *middleware.Context) { return } - us, err := models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) + us, err := ctx.Repo.Repository.GetCollaborators() if err != nil { ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err) return @@ -218,7 +218,7 @@ func CreateIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) { return } - _, err = models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) + _, err = ctx.Repo.Repository.GetCollaborators() if err != nil { send(500, nil, err) return @@ -246,8 +246,8 @@ func CreateIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) { if err := models.NewIssue(issue); err != nil { send(500, nil, err) return - } else if err := models.NewIssueUserPairs(issue.RepoId, issue.Id, ctx.Repo.Owner.Id, - ctx.User.Id, form.AssigneeId, ctx.Repo.Repository.Name); err != nil { + } else if err := models.NewIssueUserPairs(ctx.Repo.Repository, issue.Id, ctx.Repo.Owner.Id, + ctx.User.Id, form.AssigneeId); err != nil { send(500, nil, err) return } @@ -384,7 +384,7 @@ func ViewIssue(ctx *middleware.Context) { } // Get all collaborators. - ctx.Data["Collaborators"], err = models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) + ctx.Data["Collaborators"], err = ctx.Repo.Repository.GetCollaborators() if err != nil { ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err) return @@ -1122,18 +1122,18 @@ func IssueGetAttachment(ctx *middleware.Context) { // testing route handler for new issue ui page // todo : move to Issue() function -func Issues2(ctx *middleware.Context){ - ctx.HTML(200,"repo/issue2/list") +func Issues2(ctx *middleware.Context) { + ctx.HTML(200, "repo/issue2/list") } -func PullRequest2(ctx *middleware.Context){ - ctx.HTML(200,"repo/pr2/list") +func PullRequest2(ctx *middleware.Context) { + ctx.HTML(200, "repo/pr2/list") } -func Labels2(ctx *middleware.Context){ - ctx.HTML(200,"repo/issue2/labels") +func Labels2(ctx *middleware.Context) { + ctx.HTML(200, "repo/issue2/labels") } -func Milestones2(ctx *middleware.Context){ - ctx.HTML(200,"repo/milestone2/list") +func Milestones2(ctx *middleware.Context) { + ctx.HTML(200, "repo/milestone2/list") } diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 33bf1eab28..a6f50d3068 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -10,7 +10,6 @@ import ( "fmt" "strings" "time" - "path" "github.com/Unknwon/com" @@ -170,22 +169,12 @@ func SettingsCollaboration(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsCollaboration"] = true - repoLink := path.Join(ctx.Repo.Owner.LowerName, ctx.Repo.Repository.LowerName) - if ctx.Req.Method == "POST" { name := strings.ToLower(ctx.Query("collaborator")) if len(name) == 0 || ctx.Repo.Owner.LowerName == name { ctx.Redirect(setting.AppSubUrl + ctx.Req.URL.Path) return } - has, err := models.HasAccess(name, repoLink, models.WRITABLE) - if err != nil { - ctx.Handle(500, "HasAccess", err) - return - } else if has { - ctx.Redirect(setting.AppSubUrl + ctx.Req.URL.Path) - return - } u, err := models.GetUserByName(name) if err != nil { @@ -205,9 +194,8 @@ func SettingsCollaboration(ctx *middleware.Context) { return } - if err = models.AddAccess(&models.Access{UserName: name, RepoName: repoLink, - Mode: models.WRITABLE}); err != nil { - ctx.Handle(500, "AddAccess", err) + if err = ctx.Repo.Repository.AddCollaborator(u); err != nil { + ctx.Handle(500, "AddCollaborator", err) return } @@ -226,50 +214,27 @@ func SettingsCollaboration(ctx *middleware.Context) { // Delete collaborator. remove := strings.ToLower(ctx.Query("remove")) if len(remove) > 0 && remove != ctx.Repo.Owner.LowerName { - needDelete := true - if ctx.User.IsOrganization() { - // Check if user belongs to a team that has access to this repository. - auth, err := models.GetHighestAuthorize(ctx.Repo.Owner.Id, ctx.User.Id, ctx.Repo.Repository.Id, 0) - if err != nil { - ctx.Handle(500, "GetHighestAuthorize", err) - return - } - if auth > 0 { - needDelete = false - } + u, err := models.GetUserByName(remove) + if err != nil { + ctx.Handle(500, "GetUserByName", err) + return } - - if needDelete { - if err := models.DeleteAccess(&models.Access{UserName: remove, RepoName: repoLink}); err != nil { - ctx.Handle(500, "DeleteAccess", err) - return - } + if err := ctx.Repo.Repository.DeleteCollaborator(u); err != nil { + ctx.Handle(500, "DeleteCollaborator", err) + return } ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") return } - names, err := models.GetCollaboratorNames(repoLink) + users, err := ctx.Repo.Repository.GetCollaborators() if err != nil { ctx.Handle(500, "GetCollaborators", err) return } - collaborators := make([]*models.User, 0, len(names)) - for _, name := range names { - u, err := models.GetUserByName(name) - if err != nil { - ctx.Handle(500, "GetUserByName", err) - return - } - // Does not show organization members. - if ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOrgMember(u.Id) { - continue - } - collaborators = append(collaborators, u) - } - ctx.Data["Collaborators"] = collaborators + ctx.Data["Collaborators"] = users ctx.HTML(200, COLLABORATION) } diff --git a/routers/user/home.go b/routers/user/home.go index 1aabe08774..5b02154c10 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -49,13 +49,19 @@ func Dashboard(ctx *middleware.Context) { } else { // Normal user. ctxUser = ctx.User - collaborates, err := models.GetCollaborativeRepos(ctxUser.Name) + collaborates, err := ctx.User.GetAccessibleRepositories() if err != nil { - ctx.Handle(500, "GetCollaborativeRepos", err) + ctx.Handle(500, "GetAccessibleRepositories", err) return } - ctx.Data["CollaborateCount"] = len(collaborates) - ctx.Data["CollaborativeRepos"] = collaborates + + repositories := make([]*models.Repository, 0, len(collaborates)) + for repo := range collaborates { + repositories = append(repositories, repo) + } + + ctx.Data["CollaborateCount"] = len(repositories) + ctx.Data["CollaborativeRepos"] = repositories } ctx.Data["ContextUser"] = ctxUser From bef38d9d3f01bf04404aa97d5528ff046da38950 Mon Sep 17 00:00:00 2001 From: Peter Smit Date: Wed, 4 Feb 2015 15:47:40 +0200 Subject: [PATCH 02/26] Fix collaboration migration code --- models/migrations/migrations.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 43ec4e50b0..f0ed10b7aa 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -90,7 +90,7 @@ func accessToCollaboration(x *xorm.Engine) error { x.Sync(new(Collaboration)) - sql := `SELECT u.id AS uid, a.repo_name AS repo, a.mode AS mode FROM access a JOIN user u ON a.user_name=u.lower_name` + sql := `SELECT u.id AS uid, a.repo_name AS repo, a.mode AS mode, a.created as created FROM access a JOIN user u ON a.user_name=u.lower_name` results, err := x.Query(sql) if err != nil { return err @@ -100,12 +100,14 @@ func accessToCollaboration(x *xorm.Engine) error { userID := mustParseInt64(result["uid"]) repoRefName := string(result["repo"]) mode := mustParseInt64(result["mode"]) + created := result["created"] //Collaborators must have write access if mode < 2 { continue } + // find owner of repository parts := strings.SplitN(repoRefName, "/", 2) ownerName := parts[0] repoName := parts[1] @@ -118,15 +120,24 @@ func accessToCollaboration(x *xorm.Engine) error { if len(results) < 1 { continue } - ownerID := mustParseInt64(results[0]["uid"]) + ownerID := mustParseInt64(results[0]["uid"]) + if ownerID == userID { + continue + } + + // test if user is member of owning organization + isMember := false for _, member := range results { memberID := mustParseInt64(member["memberid"]) // We can skip all cases that a user is member of the owning organization if memberID == userID { - continue + isMember = true } } + if isMember { + continue + } sql = `SELECT id FROM repository WHERE owner_id=? AND lower_name=?` results, err = x.Query(sql, ownerID, repoName) @@ -139,8 +150,8 @@ func accessToCollaboration(x *xorm.Engine) error { repoID := results[0]["id"] - sql = `INSERT INTO collaboration (user_id, repo_id) VALUES (?,?)` - _, err = x.Exec(sql, userID, repoID) + sql = `INSERT INTO collaboration (user_id, repo_id, created) VALUES (?,?,?)` + _, err = x.Exec(sql, userID, repoID, created) if err != nil { return err } From fd1df86c44bfbd13b4df0a66840113b0d18695bc Mon Sep 17 00:00:00 2001 From: Peter Smit Date: Wed, 4 Feb 2015 16:08:55 +0200 Subject: [PATCH 03/26] Fix dashboard issue after collaboration migration --- models/access.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/models/access.go b/models/access.go index 65f1d00323..64bb921409 100644 --- a/models/access.go +++ b/models/access.go @@ -93,6 +93,10 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessType, error) { if err != nil { return nil, err } + err = repo.GetOwner() + if err != nil { + return nil, err + } repos[repo] = access.Mode } From 4e79adf6b5bf7ec7bc3b2b47469baafd1cb0b774 Mon Sep 17 00:00:00 2001 From: Peter Smit Date: Thu, 5 Feb 2015 15:29:08 +0200 Subject: [PATCH 04/26] Refactoring of the Access Table This commit does a lot of the work of refactoring the access table in a table with id's instead of strings. The result does compile, but has not been tested. It may eat your kittens. --- cmd/serve.go | 41 +++--- models/access.go | 178 ++++++++++++++--------- models/migrations/migrations.go | 6 + models/org.go | 242 ++++---------------------------- models/repo.go | 203 ++------------------------- models/user.go | 56 +------- modules/middleware/org.go | 2 +- modules/middleware/repo.go | 155 +++++--------------- routers/api/v1/repo.go | 2 +- routers/org/teams.go | 16 +-- routers/repo/http.go | 10 +- routers/user/home.go | 7 +- 12 files changed, 236 insertions(+), 682 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 2390962342..d9d1a06b4e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -8,7 +8,6 @@ import ( "fmt" "os" "os/exec" - "path" "path/filepath" "strings" "time" @@ -59,19 +58,19 @@ func parseCmd(cmd string) (string, string) { } var ( - COMMANDS_READONLY = map[string]models.AccessType{ - "git-upload-pack": models.WRITABLE, - "git upload-pack": models.WRITABLE, - "git-upload-archive": models.WRITABLE, + COMMANDS_READONLY = map[string]models.AccessMode{ + "git-upload-pack": models.WriteAccess, + "git upload-pack": models.WriteAccess, + "git-upload-archive": models.WriteAccess, } - COMMANDS_WRITE = map[string]models.AccessType{ - "git-receive-pack": models.READABLE, - "git receive-pack": models.READABLE, + COMMANDS_WRITE = map[string]models.AccessMode{ + "git-receive-pack": models.ReadAccess, + "git receive-pack": models.ReadAccess, } ) -func In(b string, sl map[string]models.AccessType) bool { +func In(b string, sl map[string]models.AccessMode) bool { _, e := sl[b] return e } @@ -130,9 +129,19 @@ func runServ(k *cli.Context) { } // Access check. + repo, err := models.GetRepositoryByName(repoUser.Id, repoName) + if err != nil { + if err == models.ErrRepoNotExist { + println("Gogs: given repository does not exist") + log.GitLogger.Fatal(2, "Repository does not exist: %s/%s", repoUser.Name, repoName) + } + println("Gogs: internal error:", err.Error()) + log.GitLogger.Fatal(2, "Fail to get repository: %v", err) + } + switch { case isWrite: - has, err := models.HasAccess(user.Name, path.Join(repoUserName, repoName), models.WRITABLE) + has, err := models.HasAccess(user, repo, models.WriteAccess) if err != nil { println("Gogs: internal error:", err.Error()) log.GitLogger.Fatal(2, "Fail to check write access:", err) @@ -141,21 +150,11 @@ func runServ(k *cli.Context) { log.GitLogger.Fatal(2, "User %s has no right to write repository %s", user.Name, repoPath) } case isRead: - repo, err := models.GetRepositoryByName(repoUser.Id, repoName) - if err != nil { - if err == models.ErrRepoNotExist { - println("Gogs: given repository does not exist") - log.GitLogger.Fatal(2, "Repository does not exist: %s/%s", repoUser.Name, repoName) - } - println("Gogs: internal error:", err.Error()) - log.GitLogger.Fatal(2, "Fail to get repository: %v", err) - } - if !repo.IsPrivate { break } - has, err := models.HasAccess(user.Name, path.Join(repoUserName, repoName), models.READABLE) + has, err := models.HasAccess(user, repo, models.ReadAccess) if err != nil { println("Gogs: internal error:", err.Error()) log.GitLogger.Fatal(2, "Fail to check read access:", err) diff --git a/models/access.go b/models/access.go index 64bb921409..ee678a0701 100644 --- a/models/access.go +++ b/models/access.go @@ -4,92 +4,80 @@ package models -import ( - "strings" - "time" +//import ( +// "github.com/go-xorm/xorm" +//) - "github.com/go-xorm/xorm" -) - -type AccessType int +type AccessMode int const ( - READABLE AccessType = iota + 1 - WRITABLE + NoAccess AccessMode = iota + ReadAccess + WriteAccess + AdminAccess + OwnerAccess ) -// Access represents the accessibility of user to repository. +func maxAccessMode(modes ...AccessMode) AccessMode { + max := NoAccess + for _, mode := range modes { + if mode > max { + max = mode + } + } + return max +} + +// Access represents the highest access level of a user to the repository. The only access type +// that is not in this table is the real owner of a repository. In case of an organization +// repository, the members of the owners team are in this table. type Access struct { - Id int64 - UserName string `xorm:"UNIQUE(s)"` - RepoName string `xorm:"UNIQUE(s)"` // / - Mode AccessType `xorm:"UNIQUE(s)"` - Created time.Time `xorm:"CREATED"` + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"UNIQUE(s)"` + RepoID int64 `xorm:"UNIQUE(s)"` + Mode AccessMode } -// AddAccess adds new access record. -func AddAccess(access *Access) error { - access.UserName = strings.ToLower(access.UserName) - access.RepoName = strings.ToLower(access.RepoName) - _, err := x.Insert(access) - return err +// HasAccess returns true if someone has the request access level. User can be nil! +func HasAccess(u *User, r *Repository, testMode AccessMode) (bool, error) { + mode, err := AccessLevel(u, r) + return testMode <= mode, err } -// UpdateAccess updates access information. -func UpdateAccess(access *Access) error { - access.UserName = strings.ToLower(access.UserName) - access.RepoName = strings.ToLower(access.RepoName) - _, err := x.Id(access.Id).Update(access) - return err -} - -// DeleteAccess deletes access record. -func DeleteAccess(access *Access) error { - _, err := x.Delete(access) - return err -} - -// UpdateAccess updates access information with session for rolling back. -func UpdateAccessWithSession(sess *xorm.Session, access *Access) error { - if _, err := sess.Id(access.Id).Update(access); err != nil { - sess.Rollback() - return err +// Return the Access a user has to a repository. Will return NoneAccess if the +// user does not have access. User can be nil! +func AccessLevel(u *User, r *Repository) (AccessMode, error) { + mode := NoAccess + if !r.IsPrivate { + mode = ReadAccess } - return nil -} -// HasAccess returns true if someone can read or write to given repository. -// The repoName should be in format /. -func HasAccess(uname, repoName string, mode AccessType) (bool, error) { - if len(repoName) == 0 { - return false, nil + if u != nil { + if u.Id == r.OwnerId { + return OwnerAccess, nil + } + + a := &Access{UserID: u.Id, RepoID: r.Id} + if has, err := x.Get(a); !has || err != nil { + return mode, err + } + return a.Mode, nil } - access := &Access{ - UserName: strings.ToLower(uname), - RepoName: strings.ToLower(repoName), - } - has, err := x.Get(access) - if err != nil { - return false, err - } else if !has { - return false, nil - } else if mode > access.Mode { - return false, nil - } - return true, nil + + return mode, nil } // GetAccessibleRepositories finds all repositories where a user has access to, // besides his own. -func (u *User) GetAccessibleRepositories() (map[*Repository]AccessType, error) { +func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) { accesses := make([]*Access, 0, 10) - if err := x.Find(&accesses, &Access{UserName: u.LowerName}); err != nil { + if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil { return nil, err } - repos := make(map[*Repository]AccessType, len(accesses)) + repos := make(map[*Repository]AccessMode, len(accesses)) for _, access := range accesses { - repo, err := GetRepositoryByRef(access.RepoName) + repo, err := GetRepositoryById(access.RepoID) if err != nil { return nil, err } @@ -102,3 +90,65 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessType, error) { return repos, nil } + +// Recalculate all accesses for repository +func (r *Repository) RecalcAccessSess() error { + accessMap := make(map[int64]AccessMode, 20) + + // Give all collaborators write access + collaborators, err := r.GetCollaborators() + if err != nil { + return err + } + for _, c := range collaborators { + accessMap[c.Id] = WriteAccess + } + + if err := r.GetOwner(); err != nil { + return err + } + if r.Owner.IsOrganization() { + if err = r.Owner.GetTeams(); err != nil { + return err + } + + for _, team := range r.Owner.Teams { + if !(team.IsOwnerTeam() || team.HasRepository(r)) { + continue + } + + if err = team.GetMembers(); err != nil { + return err + } + for _, u := range team.Members { + accessMap[u.Id] = maxAccessMode(accessMap[u.Id], team.Authorize) + } + } + } + + minMode := ReadAccess + if !r.IsPrivate { + minMode = WriteAccess + } + + newAccesses := make([]Access, 0, len(accessMap)) + for userID, mode := range accessMap { + if userID == r.OwnerId || mode <= minMode { + continue + } + newAccesses = append(newAccesses, Access{UserID: userID, RepoID: r.Id, Mode: mode}) + } + + // Delete old accesses for repository + if _, err = x.Delete(&Access{RepoID: r.Id}); err != nil { + return err + } + + // And insert the new ones + if _, err = x.Insert(newAccesses); err != nil { + return err + } + + return nil + +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index f0ed10b7aa..37986b68bd 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -21,6 +21,7 @@ type Version struct { // If you want to "retire" a migration, replace it with "expiredMigration" var migrations = []migration{ accessToCollaboration, + accessRefactor, } // Migrate database to current version @@ -158,3 +159,8 @@ func accessToCollaboration(x *xorm.Engine) error { } return nil } + +func accessRefactor(x *xorm.Engine) error { + //TODO + return nil +} diff --git a/models/org.go b/models/org.go index 5431a111c3..5e3bb0e070 100644 --- a/models/org.go +++ b/models/org.go @@ -6,9 +6,7 @@ package models import ( "errors" - "fmt" "os" - "path" "strings" "github.com/Unknwon/com" @@ -137,7 +135,7 @@ func CreateOrganization(org, owner *User) (*User, error) { OrgId: org.Id, LowerName: strings.ToLower(OWNER_TEAM), Name: OWNER_TEAM, - Authorize: ORG_ADMIN, + Authorize: OwnerAccess, NumMembers: 1, } if _, err = sess.Insert(t); err != nil { @@ -372,10 +370,10 @@ func RemoveOrgUser(orgId, uid int64) error { return err } access := &Access{ - UserName: u.LowerName, + UserID: u.Id, } for _, repo := range org.Repos { - access.RepoName = path.Join(org.LowerName, repo.LowerName) + access.RepoID = repo.Id if _, err = sess.Delete(access); err != nil { sess.Rollback() return err @@ -406,21 +404,6 @@ func RemoveOrgUser(orgId, uid int64) error { // |____| \___ >____ /__|_| / // \/ \/ \/ -type AuthorizeType int - -const ( - ORG_READABLE AuthorizeType = iota + 1 - ORG_WRITABLE - ORG_ADMIN -) - -func AuthorizeToAccessType(auth AuthorizeType) AccessType { - if auth == ORG_READABLE { - return READABLE - } - return WRITABLE -} - const OWNER_TEAM = "Owners" // Team represents a organization team. @@ -430,7 +413,7 @@ type Team struct { LowerName string Name string Description string - Authorize AuthorizeType + Authorize AccessMode RepoIds string `xorm:"TEXT"` Repos []*Repository `xorm:"-"` Members []*User `xorm:"-"` @@ -485,25 +468,6 @@ func (t *Team) RemoveMember(uid int64) error { return RemoveTeamMember(t.OrgId, t.Id, uid) } -// addAccessWithAuthorize inserts or updates access with given mode. -func addAccessWithAuthorize(sess *xorm.Session, access *Access, mode AccessType) error { - has, err := x.Get(access) - if err != nil { - return fmt.Errorf("fail to get access: %v", err) - } - access.Mode = mode - if has { - if _, err = sess.Id(access.Id).Update(access); err != nil { - return fmt.Errorf("fail to update access: %v", err) - } - } else { - if _, err = sess.Insert(access); err != nil { - return fmt.Errorf("fail to insert access: %v", err) - } - } - return nil -} - // AddRepository adds new repository to team of organization. func (t *Team) AddRepository(repo *Repository) (err error) { idStr := "$" + com.ToStr(repo.Id) + "|" @@ -532,26 +496,12 @@ func (t *Team) AddRepository(repo *Repository) (err error) { return err } - // Give access to team members. - mode := AuthorizeToAccessType(t.Authorize) + if err = repo.RecalcAccessSess(); err != nil { + sess.Rollback() + return err + } for _, u := range t.Members { - auth, err := GetHighestAuthorize(t.OrgId, u.Id, repo.Id, t.Id) - if err != nil { - sess.Rollback() - return err - } - - access := &Access{ - UserName: u.LowerName, - RepoName: path.Join(repo.Owner.LowerName, repo.LowerName), - } - if auth < t.Authorize { - if err = addAccessWithAuthorize(sess, access, mode); err != nil { - sess.Rollback() - return err - } - } if err = WatchRepo(u.Id, repo.Id, true); err != nil { sess.Rollback() return err @@ -560,6 +510,11 @@ func (t *Team) AddRepository(repo *Repository) (err error) { return sess.Commit() } +func (t *Team) HasRepository(r *Repository) bool { + idStr := "$" + com.ToStr(r.Id) + "|" + return strings.Contains(t.RepoIds, idStr) +} + // RemoveRepository removes repository from team of organization. func (t *Team) RemoveRepository(repoId int64) error { idStr := "$" + com.ToStr(repoId) + "|" @@ -591,32 +546,16 @@ func (t *Team) RemoveRepository(repoId int64) error { return err } - // Remove access to team members. + if err = repo.RecalcAccessSess(); err != nil { + sess.Rollback() + return err + } + for _, u := range t.Members { - auth, err := GetHighestAuthorize(t.OrgId, u.Id, repo.Id, t.Id) - if err != nil { + if err = WatchRepo(u.Id, repo.Id, false); err != nil { sess.Rollback() return err } - - access := &Access{ - UserName: u.LowerName, - RepoName: path.Join(repo.Owner.LowerName, repo.LowerName), - } - if auth == 0 { - if _, err = sess.Delete(access); err != nil { - sess.Rollback() - return fmt.Errorf("fail to delete access: %v", err) - } else if err = WatchRepo(u.Id, repo.Id, false); err != nil { - sess.Rollback() - return err - } - } else if auth < t.Authorize { - if err = addAccessWithAuthorize(sess, access, AuthorizeToAccessType(auth)); err != nil { - sess.Rollback() - return err - } - } } return sess.Commit() @@ -690,30 +629,6 @@ func GetTeamById(teamId int64) (*Team, error) { return t, nil } -// GetHighestAuthorize returns highest repository authorize level for given user and team. -func GetHighestAuthorize(orgId, uid, repoId, teamId int64) (AuthorizeType, error) { - ts, err := GetUserTeams(orgId, uid) - if err != nil { - return 0, err - } - - var auth AuthorizeType = 0 - for _, t := range ts { - // Not current team and has given repository. - if t.Id != teamId && strings.Contains(t.RepoIds, "$"+com.ToStr(repoId)+"|") { - // Fast return. - if t.Authorize == ORG_WRITABLE { - return ORG_WRITABLE, nil - } - if t.Authorize > auth { - auth = t.Authorize - } - } - } - - return auth, nil -} - // UpdateTeam updates information of team. func UpdateTeam(t *Team, authChanged bool) (err error) { if !IsLegalName(t.Name) { @@ -731,45 +646,14 @@ func UpdateTeam(t *Team, authChanged bool) (err error) { } // Update access for team members if needed. - if authChanged && !t.IsOwnerTeam() { + if authChanged { if err = t.GetRepositories(); err != nil { return err - } else if err = t.GetMembers(); err != nil { - return err } - // Get organization. - org, err := GetUserById(t.OrgId) - if err != nil { - return err - } - - // Update access. - mode := AuthorizeToAccessType(t.Authorize) - for _, repo := range t.Repos { - for _, u := range t.Members { - // ORG_WRITABLE is the highest authorize level for now. - // Skip checking others if current team has this level. - if t.Authorize < ORG_WRITABLE { - auth, err := GetHighestAuthorize(t.OrgId, u.Id, repo.Id, t.Id) - if err != nil { - sess.Rollback() - return err - } - if auth >= t.Authorize { - continue // Other team has higher or same authorize level. - } - } - - access := &Access{ - UserName: u.LowerName, - RepoName: path.Join(org.LowerName, repo.LowerName), - } - if err = addAccessWithAuthorize(sess, access, mode); err != nil { - sess.Rollback() - return err - } + if err = repo.RecalcAccessSess(); err != nil { + return err } } } @@ -805,29 +689,8 @@ func DeleteTeam(t *Team) error { // Delete all accesses. for _, repo := range t.Repos { - for _, u := range t.Members { - auth, err := GetHighestAuthorize(t.OrgId, u.Id, repo.Id, t.Id) - if err != nil { - sess.Rollback() - return err - } - - access := &Access{ - UserName: u.LowerName, - RepoName: path.Join(org.LowerName, repo.LowerName), - } - if auth == 0 { - if _, err = sess.Delete(access); err != nil { - sess.Rollback() - return fmt.Errorf("fail to delete access: %v", err) - } - } else if auth < t.Authorize { - // Downgrade authorize level. - if err = addAccessWithAuthorize(sess, access, AuthorizeToAccessType(auth)); err != nil { - sess.Rollback() - return err - } - } + if err = repo.RecalcAccessSess(); err != nil { + return err } } @@ -921,18 +784,6 @@ func AddTeamMember(orgId, teamId, uid int64) error { return err } - // Get organization. - org, err := GetUserById(orgId) - if err != nil { - return err - } - - // Get user. - u, err := GetUserById(uid) - if err != nil { - return err - } - sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -954,24 +805,11 @@ func AddTeamMember(orgId, teamId, uid int64) error { } // Give access to team repositories. - mode := AuthorizeToAccessType(t.Authorize) for _, repo := range t.Repos { - auth, err := GetHighestAuthorize(t.OrgId, u.Id, repo.Id, teamId) - if err != nil { + if err = repo.RecalcAccessSess(); err != nil { sess.Rollback() return err } - - access := &Access{ - UserName: u.LowerName, - RepoName: path.Join(org.LowerName, repo.LowerName), - } - if auth < t.Authorize { - if err = addAccessWithAuthorize(sess, access, mode); err != nil { - sess.Rollback() - return err - } - } } // We make sure it exists before. @@ -1021,12 +859,6 @@ func removeTeamMemberWithSess(orgId, teamId, uid int64, sess *xorm.Session) erro return err } - // Get user. - u, err := GetUserById(uid) - if err != nil { - return err - } - tu := &TeamUser{ Uid: uid, OrgId: orgId, @@ -1043,32 +875,10 @@ func removeTeamMemberWithSess(orgId, teamId, uid int64, sess *xorm.Session) erro // Delete access to team repositories. for _, repo := range t.Repos { - auth, err := GetHighestAuthorize(t.OrgId, u.Id, repo.Id, teamId) - if err != nil { + if err = repo.RecalcAccessSess(); err != nil { sess.Rollback() return err } - - access := &Access{ - UserName: u.LowerName, - RepoName: path.Join(org.LowerName, repo.LowerName), - } - // Delete access if this is the last team user belongs to. - if auth == 0 { - if _, err = sess.Delete(access); err != nil { - sess.Rollback() - return fmt.Errorf("fail to delete access: %v", err) - } else if err = WatchRepo(u.Id, repo.Id, false); err != nil { - sess.Rollback() - return err - } - } else if auth < t.Authorize { - // Downgrade authorize level. - if err = addAccessWithAuthorize(sess, access, AuthorizeToAccessType(auth)); err != nil { - sess.Rollback() - return err - } - } } // This must exist. diff --git a/models/repo.go b/models/repo.go index a06f1d3e4e..5943dd31ad 100644 --- a/models/repo.go +++ b/models/repo.go @@ -206,14 +206,6 @@ func (repo *Repository) IsOwnedBy(u *User) bool { return repo.OwnerId == u.Id } -func (repo *Repository) HasAccess(uname string) bool { - if err := repo.GetOwner(); err != nil { - return false - } - has, _ := HasAccess(uname, path.Join(repo.Owner.Name, repo.Name), READABLE) - return has -} - // DescriptionHtml does special handles to description and return HTML string. func (repo *Repository) DescriptionHtml() template.HTML { sanitize := func(s string) string { @@ -553,36 +545,11 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror var t *Team // Owner team. - mode := WRITABLE - if mirror { - mode = READABLE - } - access := &Access{ - UserName: u.LowerName, - RepoName: path.Join(u.LowerName, repo.LowerName), - Mode: mode, - } + // TODO fix code for mirrors? + // Give access to all members in owner team. if u.IsOrganization() { - t, err = u.GetOwnerTeam() - if err != nil { - sess.Rollback() - return nil, err - } - if err = t.GetMembers(); err != nil { - sess.Rollback() - return nil, err - } - for _, u := range t.Members { - access.Id = 0 - access.UserName = u.LowerName - if _, err = sess.Insert(access); err != nil { - sess.Rollback() - return nil, err - } - } - } else { - if _, err = sess.Insert(access); err != nil { + if err = repo.RecalcAccessSess(); err != nil { sess.Rollback() return nil, err } @@ -712,37 +679,10 @@ func TransferOwnership(u *User, newOwner string, repo *Repository) error { } owner := repo.Owner - oldRepoLink := path.Join(owner.LowerName, repo.LowerName) - // Delete all access first if current owner is an organization. - if owner.IsOrganization() { - if _, err = sess.Where("repo_name=?", oldRepoLink).Delete(new(Access)); err != nil { - sess.Rollback() - return fmt.Errorf("fail to delete current accesses: %v", err) - } - } else { - // Delete current owner access. - if _, err = sess.Where("repo_name=?", oldRepoLink).And("user_name=?", owner.LowerName). - Delete(new(Access)); err != nil { - sess.Rollback() - return fmt.Errorf("fail to delete access(owner): %v", err) - } - // In case new owner has access. - if _, err = sess.Where("repo_name=?", oldRepoLink).And("user_name=?", newUser.LowerName). - Delete(new(Access)); err != nil { - sess.Rollback() - return fmt.Errorf("fail to delete access(new user): %v", err) - } - } - - // Change accesses to new repository path. - if _, err = sess.Where("repo_name=?", oldRepoLink). - Update(&Access{RepoName: path.Join(newUser.LowerName, repo.LowerName)}); err != nil { - sess.Rollback() - return fmt.Errorf("fail to update access(change reponame): %v", err) - } // Update repository. repo.OwnerId = newUser.Id + repo.Owner = newUser if _, err := sess.Id(repo.Id).Update(repo); err != nil { sess.Rollback() return err @@ -759,53 +699,8 @@ func TransferOwnership(u *User, newOwner string, repo *Repository) error { return err } - mode := WRITABLE - if repo.IsMirror { - mode = READABLE - } - // New owner is organization. - if newUser.IsOrganization() { - access := &Access{ - RepoName: path.Join(newUser.LowerName, repo.LowerName), - Mode: mode, - } - - // Give access to all members in owner team. - t, err := newUser.GetOwnerTeam() - if err != nil { - sess.Rollback() - return err - } - if err = t.GetMembers(); err != nil { - sess.Rollback() - return err - } - for _, u := range t.Members { - access.Id = 0 - access.UserName = u.LowerName - if _, err = sess.Insert(access); err != nil { - sess.Rollback() - return err - } - } - - // Update owner team info and count. - t.RepoIds += "$" + com.ToStr(repo.Id) + "|" - t.NumRepos++ - if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { - sess.Rollback() - return err - } - } else { - access := &Access{ - RepoName: path.Join(newUser.LowerName, repo.LowerName), - UserName: newUser.LowerName, - Mode: mode, - } - if _, err = sess.Insert(access); err != nil { - sess.Rollback() - return fmt.Errorf("fail to insert access: %v", err) - } + if err = repo.RecalcAccessSess(); err != nil { + return err } // Change repository directory name. @@ -838,32 +733,8 @@ func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error) return ErrRepoNameIllegal } - // Update accesses. - accesses := make([]Access, 0, 10) - if err = x.Find(&accesses, &Access{RepoName: userName + "/" + oldRepoName}); err != nil { - return err - } - - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - for i := range accesses { - accesses[i].RepoName = userName + "/" + newRepoName - if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { - return err - } - } - // Change repository directory name. - if err = os.Rename(RepoPath(userName, oldRepoName), RepoPath(userName, newRepoName)); err != nil { - sess.Rollback() - return err - } - - return sess.Commit() + return os.Rename(RepoPath(userName, oldRepoName), RepoPath(userName, newRepoName)) } func UpdateRepository(repo *Repository) error { @@ -912,7 +783,7 @@ func DeleteRepository(uid, repoId int64, userName string) error { } // Delete all access. - if _, err := sess.Delete(&Access{RepoName: strings.ToLower(path.Join(userName, repo.Name))}); err != nil { + if _, err := sess.Delete(&Access{RepoID: repo.Id}); err != nil { sess.Rollback() return err } @@ -1105,7 +976,7 @@ func (r *Repository) AddCollaborator(u *User) error { return err } - return AddAccess(&Access{UserName: u.LowerName, RepoName: path.Join(r.Owner.LowerName, r.LowerName), Mode: WRITABLE}) + return r.RecalcAccessSess() } // Delete collaborator and accompanying access @@ -1116,25 +987,7 @@ func (r *Repository) DeleteCollaborator(u *User) error { return err } - if err := r.GetOwner(); err != nil { - return err - } - - needDelete := true - if r.Owner.IsOrganization() { - auth, err := GetHighestAuthorize(r.Owner.Id, u.Id, r.Id, 0) - if err != nil { - return err - } - if auth > 0 { - needDelete = false - } - } - if needDelete { - return DeleteAccess(&Access{UserName: u.LowerName, RepoName: path.Join(r.Owner.LowerName, r.LowerName), Mode: WRITABLE}) - } - - return nil + return r.RecalcAccessSess() } type SearchOption struct { @@ -1443,41 +1296,11 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (*Repositor return nil, err } + if err = repo.RecalcAccessSess(); err != nil { + return nil, err + } var t *Team // Owner team. - mode := WRITABLE - - access := &Access{ - UserName: u.LowerName, - RepoName: path.Join(u.LowerName, repo.LowerName), - Mode: mode, - } - // Give access to all members in owner team. - if u.IsOrganization() { - t, err = u.GetOwnerTeam() - if err != nil { - sess.Rollback() - return nil, err - } - if err = t.GetMembers(); err != nil { - sess.Rollback() - return nil, err - } - for _, u := range t.Members { - access.Id = 0 - access.UserName = u.LowerName - if _, err = sess.Insert(access); err != nil { - sess.Rollback() - return nil, err - } - } - } else { - if _, err = sess.Insert(access); err != nil { - sess.Rollback() - return nil, err - } - } - if _, err = sess.Exec( "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?", u.Id); err != nil { sess.Rollback() diff --git a/models/user.go b/models/user.go index 2da0881c81..b33e529aa6 100644 --- a/models/user.go +++ b/models/user.go @@ -396,59 +396,7 @@ func ChangeUserName(u *User, newUserName string) (err error) { return ErrUserNameIllegal } - newUserName = strings.ToLower(newUserName) - - // Update accesses of user. - accesses := make([]Access, 0, 10) - if err = x.Find(&accesses, &Access{UserName: u.LowerName}); err != nil { - return err - } - - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - for i := range accesses { - accesses[i].UserName = newUserName - if strings.HasPrefix(accesses[i].RepoName, u.LowerName+"/") { - accesses[i].RepoName = strings.Replace(accesses[i].RepoName, u.LowerName, newUserName, 1) - } - if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { - return err - } - } - - repos, err := GetRepositories(u.Id, true) - if err != nil { - return err - } - for i := range repos { - accesses = make([]Access, 0, 10) - // Update accesses of user repository. - if err = x.Find(&accesses, &Access{RepoName: u.LowerName + "/" + repos[i].LowerName}); err != nil { - return err - } - - for j := range accesses { - // if the access is not the user's access (already updated above) - if accesses[j].UserName != u.LowerName { - accesses[j].RepoName = newUserName + "/" + repos[i].LowerName - if err = UpdateAccessWithSession(sess, &accesses[j]); err != nil { - return err - } - } - } - } - - // Change user directory name. - if err = os.Rename(UserPath(u.LowerName), UserPath(newUserName)); err != nil { - sess.Rollback() - return err - } - - return sess.Commit() + return os.Rename(UserPath(u.LowerName), UserPath(newUserName)) } // UpdateUser updates user's information. @@ -521,7 +469,7 @@ func DeleteUser(u *User) error { return err } // Delete all accesses. - if _, err = x.Delete(&Access{UserName: u.LowerName}); err != nil { + if _, err = x.Delete(&Access{UserID: u.Id}); err != nil { return err } // Delete all alternative email addresses diff --git a/modules/middleware/org.go b/modules/middleware/org.go index e687258617..cbce54860d 100644 --- a/modules/middleware/org.go +++ b/modules/middleware/org.go @@ -87,7 +87,7 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { return } ctx.Data["Team"] = ctx.Org.Team - ctx.Org.IsAdminTeam = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.Authorize == models.ORG_ADMIN + ctx.Org.IsAdminTeam = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.Authorize >= models.AdminAccess } ctx.Data["IsAdminTeam"] = ctx.Org.IsAdminTeam if requireAdminTeam && !ctx.Org.IsAdminTeam { diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index d143d8a86b..66e6f3a570 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -5,7 +5,6 @@ package middleware import ( - "errors" "fmt" "net/url" "strings" @@ -29,17 +28,10 @@ func ApiRepoAssignment() macaron.Handler { err error ) - // Collaborators who have write access can be seen as owners. - if ctx.IsSigned { - ctx.Repo.IsOwner, err = models.HasAccess(ctx.User.Name, userName+"/"+repoName, models.WRITABLE) - if err != nil { - ctx.JSON(500, &base.ApiJsonErr{"HasAccess: " + err.Error(), base.DOC_URL}) - return - } - ctx.Repo.IsTrueOwner = ctx.User.LowerName == strings.ToLower(userName) - } - - if !ctx.Repo.IsTrueOwner { + // Check if the user is the same as the repository owner + if ctx.IsSigned && u.LowerName == strings.ToLower(userName) { + u = ctx.User + } else { u, err = models.GetUserByName(userName) if err != nil { if err == models.ErrUserNotExist { @@ -49,64 +41,38 @@ func ApiRepoAssignment() macaron.Handler { } return } - } else { - u = ctx.User } ctx.Repo.Owner = u - // Organization owner team members are true owners as well. - if ctx.IsSigned && ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOwnedBy(ctx.User.Id) { - ctx.Repo.IsTrueOwner = true - } - // Get repository. repo, err := models.GetRepositoryByName(u.Id, repoName) if err != nil { if err == models.ErrRepoNotExist { ctx.Error(404) - return + } else { + ctx.JSON(500, &base.ApiJsonErr{"GetRepositoryByName: " + err.Error(), base.DOC_URL}) } - ctx.JSON(500, &base.ApiJsonErr{"GetRepositoryByName: " + err.Error(), base.DOC_URL}) return } else if err = repo.GetOwner(); err != nil { ctx.JSON(500, &base.ApiJsonErr{"GetOwner: " + err.Error(), base.DOC_URL}) return } - // Check if the mirror repository owner(mirror repository doesn't have access). - if ctx.IsSigned && !ctx.Repo.IsOwner { - if repo.OwnerId == ctx.User.Id { - ctx.Repo.IsOwner = true - } - // Check if current user has admin permission to repository. - if u.IsOrganization() { - auth, err := models.GetHighestAuthorize(u.Id, ctx.User.Id, repo.Id, 0) - if err != nil { - ctx.JSON(500, &base.ApiJsonErr{"GetHighestAuthorize: " + err.Error(), base.DOC_URL}) - return - } - if auth == models.ORG_ADMIN { - ctx.Repo.IsOwner = true - ctx.Repo.IsAdmin = true - } + if ctx.IsSigned { + mode, err := models.AccessLevel(ctx.User, repo) + if err != nil { + ctx.JSON(500, &base.ApiJsonErr{"AccessLevel: " + err.Error(), base.DOC_URL}) + return } + ctx.Repo.IsOwner = mode >= models.WriteAccess + ctx.Repo.IsAdmin = mode >= models.ReadAccess + ctx.Repo.IsTrueOwner = mode >= models.OwnerAccess } // Check access. if repo.IsPrivate && !ctx.Repo.IsOwner { - if ctx.User == nil { - ctx.Error(404) - return - } - - hasAccess, err := models.HasAccess(ctx.User.Name, ctx.Repo.Owner.Name+"/"+repo.Name, models.READABLE) - if err != nil { - ctx.JSON(500, &base.ApiJsonErr{"HasAccess: " + err.Error(), base.DOC_URL}) - return - } else if !hasAccess { - ctx.Error(404) - return - } + ctx.Error(404) + return } ctx.Repo.HasAccess = true @@ -242,101 +208,54 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { refName = ctx.Params(":path") } - // Collaborators who have write access can be seen as owners. - if ctx.IsSigned { - ctx.Repo.IsOwner, err = models.HasAccess(ctx.User.Name, userName+"/"+repoName, models.WRITABLE) - if err != nil { - ctx.Handle(500, "HasAccess", err) - return - } - ctx.Repo.IsTrueOwner = ctx.User.LowerName == strings.ToLower(userName) - } - - if !ctx.Repo.IsTrueOwner { + // Check if the user is the same as the repository owner + if ctx.IsSigned && u.LowerName == strings.ToLower(userName) { + u = ctx.User + } else { u, err = models.GetUserByName(userName) if err != nil { if err == models.ErrUserNotExist { - ctx.Handle(404, "GetUserByName", err) - } else if redirect { - log.Error(4, "GetUserByName", err) - ctx.Redirect(setting.AppSubUrl + "/") + ctx.Error(404) } else { - ctx.Handle(500, "GetUserByName", err) + ctx.JSON(500, &base.ApiJsonErr{"GetUserByName: " + err.Error(), base.DOC_URL}) } return } - } else { - u = ctx.User - } - - if u == nil { - if redirect { - ctx.Redirect(setting.AppSubUrl + "/") - return - } - ctx.Handle(404, "RepoAssignment", errors.New("invliad user account for single repository")) - return } ctx.Repo.Owner = u - // Organization owner team members are true owners as well. - if ctx.IsSigned && ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOwnedBy(ctx.User.Id) { - ctx.Repo.IsTrueOwner = true - } - // Get repository. repo, err := models.GetRepositoryByName(u.Id, repoName) if err != nil { if err == models.ErrRepoNotExist { - ctx.Handle(404, "GetRepositoryByName", err) - return - } else if redirect { - ctx.Redirect(setting.AppSubUrl + "/") - return + ctx.Error(404) + } else { + ctx.JSON(500, &base.ApiJsonErr{"GetRepositoryByName: " + err.Error(), base.DOC_URL}) } - ctx.Handle(500, "GetRepositoryByName", err) return } else if err = repo.GetOwner(); err != nil { - ctx.Handle(500, "GetOwner", err) + ctx.JSON(500, &base.ApiJsonErr{"GetOwner: " + err.Error(), base.DOC_URL}) return } - // Check if the mirror repository owner(mirror repository doesn't have access). - if ctx.IsSigned && !ctx.Repo.IsOwner { - if repo.OwnerId == ctx.User.Id { - ctx.Repo.IsOwner = true - } - // Check if current user has admin permission to repository. - if u.IsOrganization() { - auth, err := models.GetHighestAuthorize(u.Id, ctx.User.Id, repo.Id, 0) - if err != nil { - ctx.Handle(500, "GetHighestAuthorize", err) - return - } - if auth == models.ORG_ADMIN { - ctx.Repo.IsOwner = true - ctx.Repo.IsAdmin = true - } + if ctx.IsSigned { + mode, err := models.AccessLevel(ctx.User, repo) + if err != nil { + ctx.JSON(500, &base.ApiJsonErr{"AccessLevel: " + err.Error(), base.DOC_URL}) + return } + ctx.Repo.IsOwner = mode >= models.WriteAccess + ctx.Repo.IsAdmin = mode >= models.ReadAccess + ctx.Repo.IsTrueOwner = mode >= models.OwnerAccess } // Check access. if repo.IsPrivate && !ctx.Repo.IsOwner { - if ctx.User == nil { - ctx.Handle(404, "HasAccess", nil) - return - } - - hasAccess, err := models.HasAccess(ctx.User.Name, ctx.Repo.Owner.Name+"/"+repo.Name, models.READABLE) - if err != nil { - ctx.Handle(500, "HasAccess", err) - return - } else if !hasAccess { - ctx.Handle(404, "HasAccess", nil) - return - } + ctx.Error(404) + return } ctx.Repo.HasAccess = true + ctx.Data["HasAccess"] = true if repo.IsMirror { diff --git a/routers/api/v1/repo.go b/routers/api/v1/repo.go index 469e4808fe..78c9f9a6a3 100644 --- a/routers/api/v1/repo.go +++ b/routers/api/v1/repo.go @@ -255,7 +255,7 @@ func ListMyRepos(ctx *middleware.Context) { return } - repos[i] = ToApiRepository(repo.Owner, repo, api.Permission{false, access >= models.WRITABLE, true}) + repos[i] = ToApiRepository(repo.Owner, repo, api.Permission{false, access >= models.WriteAccess, true}) // FIXME: cache result to reduce DB query? if repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(ctx.User.Id) { diff --git a/routers/org/teams.go b/routers/org/teams.go index 77a7b6e13c..4fef02c987 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -168,14 +168,14 @@ func NewTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { } // Validate permission level. - var auth models.AuthorizeType + var auth models.AccessMode switch form.Permission { case "read": - auth = models.ORG_READABLE + auth = models.ReadAccess case "write": - auth = models.ORG_WRITABLE + auth = models.WriteAccess case "admin": - auth = models.ORG_ADMIN + auth = models.AdminAccess default: ctx.Error(401) return @@ -249,14 +249,14 @@ func EditTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { isAuthChanged := false if !t.IsOwnerTeam() { // Validate permission level. - var auth models.AuthorizeType + var auth models.AccessMode switch form.Permission { case "read": - auth = models.ORG_READABLE + auth = models.ReadAccess case "write": - auth = models.ORG_WRITABLE + auth = models.WriteAccess case "admin": - auth = models.ORG_ADMIN + auth = models.AdminAccess default: ctx.Error(401) return diff --git a/routers/repo/http.go b/routers/repo/http.go index a5e01efc8f..716c71272a 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -115,18 +115,18 @@ func Http(ctx *middleware.Context) { } if !isPublicPull { - var tp = models.WRITABLE + var tp = models.WriteAccess if isPull { - tp = models.READABLE + tp = models.ReadAccess } - has, err := models.HasAccess(authUsername, username+"/"+reponame, tp) + has, err := models.HasAccess(authUser, repo, tp) if err != nil { ctx.Handle(401, "no basic auth and digit auth", nil) return } else if !has { - if tp == models.READABLE { - has, err = models.HasAccess(authUsername, username+"/"+reponame, models.WRITABLE) + if tp == models.ReadAccess { + has, err = models.HasAccess(authUser, repo, models.WriteAccess) if err != nil || !has { ctx.Handle(401, "no basic auth and digit auth", nil) return diff --git a/routers/user/home.go b/routers/user/home.go index 5b02154c10..82325cb747 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -103,8 +103,7 @@ func Dashboard(ctx *middleware.Context) { feeds := make([]*models.Action, 0, len(actions)) for _, act := range actions { if act.IsPrivate { - if has, _ := models.HasAccess(ctx.User.Name, act.RepoUserName+"/"+act.RepoName, - models.READABLE); !has { + if has, _ := models.HasAccess(ctx.User, &models.Repository{Id: act.RepoId, IsPrivate: true}, models.ReadAccess); !has { continue } } @@ -211,8 +210,8 @@ func Profile(ctx *middleware.Context) { if !ctx.IsSigned { continue } - if has, _ := models.HasAccess(ctx.User.Name, act.RepoUserName+"/"+act.RepoName, - models.READABLE); !has { + if has, _ := models.HasAccess(ctx.User, &models.Repository{Id: act.RepoId, IsPrivate: true}, + models.ReadAccess); !has { continue } } From 0d158e569b0c19614b5e946849e8b7a8e4a75015 Mon Sep 17 00:00:00 2001 From: Peter Smit Date: Mon, 9 Feb 2015 13:36:33 +0200 Subject: [PATCH 05/26] Change constants to UPPERCASE_WITH_UNDERSCORE style --- cmd/serve.go | 14 +++++++------- models/access.go | 24 ++++++++++++------------ models/org.go | 2 +- modules/middleware/org.go | 2 +- modules/middleware/repo.go | 12 ++++++------ routers/api/v1/repo.go | 2 +- routers/org/teams.go | 12 ++++++------ routers/repo/http.go | 12 ++++++------ routers/user/home.go | 4 ++-- 9 files changed, 42 insertions(+), 42 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index d9d1a06b4e..90e1045c8e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -59,14 +59,14 @@ func parseCmd(cmd string) (string, string) { var ( COMMANDS_READONLY = map[string]models.AccessMode{ - "git-upload-pack": models.WriteAccess, - "git upload-pack": models.WriteAccess, - "git-upload-archive": models.WriteAccess, + "git-upload-pack": models.ACCESS_MODE_WRITE, + "git upload-pack": models.ACCESS_MODE_WRITE, + "git-upload-archive": models.ACCESS_MODE_WRITE, } COMMANDS_WRITE = map[string]models.AccessMode{ - "git-receive-pack": models.ReadAccess, - "git receive-pack": models.ReadAccess, + "git-receive-pack": models.ACCESS_MODE_READ, + "git receive-pack": models.ACCESS_MODE_READ, } ) @@ -141,7 +141,7 @@ func runServ(k *cli.Context) { switch { case isWrite: - has, err := models.HasAccess(user, repo, models.WriteAccess) + has, err := models.HasAccess(user, repo, models.ACCESS_MODE_WRITE) if err != nil { println("Gogs: internal error:", err.Error()) log.GitLogger.Fatal(2, "Fail to check write access:", err) @@ -154,7 +154,7 @@ func runServ(k *cli.Context) { break } - has, err := models.HasAccess(user, repo, models.ReadAccess) + has, err := models.HasAccess(user, repo, models.ACCESS_MODE_READ) if err != nil { println("Gogs: internal error:", err.Error()) log.GitLogger.Fatal(2, "Fail to check read access:", err) diff --git a/models/access.go b/models/access.go index ee678a0701..916711f786 100644 --- a/models/access.go +++ b/models/access.go @@ -11,15 +11,15 @@ package models type AccessMode int const ( - NoAccess AccessMode = iota - ReadAccess - WriteAccess - AdminAccess - OwnerAccess + ACCESS_MODE_NONE AccessMode = iota + ACCESS_MODE_READ + ACCESS_MODE_WRITE + ACCESS_MODE_ADMIN + ACCESS_MODE_OWNER ) func maxAccessMode(modes ...AccessMode) AccessMode { - max := NoAccess + max := ACCESS_MODE_NONE for _, mode := range modes { if mode > max { max = mode @@ -47,14 +47,14 @@ func HasAccess(u *User, r *Repository, testMode AccessMode) (bool, error) { // Return the Access a user has to a repository. Will return NoneAccess if the // user does not have access. User can be nil! func AccessLevel(u *User, r *Repository) (AccessMode, error) { - mode := NoAccess + mode := ACCESS_MODE_NONE if !r.IsPrivate { - mode = ReadAccess + mode = ACCESS_MODE_READ } if u != nil { if u.Id == r.OwnerId { - return OwnerAccess, nil + return ACCESS_MODE_OWNER, nil } a := &Access{UserID: u.Id, RepoID: r.Id} @@ -101,7 +101,7 @@ func (r *Repository) RecalcAccessSess() error { return err } for _, c := range collaborators { - accessMap[c.Id] = WriteAccess + accessMap[c.Id] = ACCESS_MODE_WRITE } if err := r.GetOwner(); err != nil { @@ -126,9 +126,9 @@ func (r *Repository) RecalcAccessSess() error { } } - minMode := ReadAccess + minMode := ACCESS_MODE_READ if !r.IsPrivate { - minMode = WriteAccess + minMode = ACCESS_MODE_WRITE } newAccesses := make([]Access, 0, len(accessMap)) diff --git a/models/org.go b/models/org.go index 5e3bb0e070..f6d472a6d9 100644 --- a/models/org.go +++ b/models/org.go @@ -135,7 +135,7 @@ func CreateOrganization(org, owner *User) (*User, error) { OrgId: org.Id, LowerName: strings.ToLower(OWNER_TEAM), Name: OWNER_TEAM, - Authorize: OwnerAccess, + Authorize: ACCESS_MODE_OWNER, NumMembers: 1, } if _, err = sess.Insert(t); err != nil { diff --git a/modules/middleware/org.go b/modules/middleware/org.go index cbce54860d..0e544fe4a2 100644 --- a/modules/middleware/org.go +++ b/modules/middleware/org.go @@ -87,7 +87,7 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { return } ctx.Data["Team"] = ctx.Org.Team - ctx.Org.IsAdminTeam = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.Authorize >= models.AdminAccess + ctx.Org.IsAdminTeam = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.Authorize >= models.ACCESS_MODE_ADMIN } ctx.Data["IsAdminTeam"] = ctx.Org.IsAdminTeam if requireAdminTeam && !ctx.Org.IsAdminTeam { diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index 66e6f3a570..bc1d1f9692 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -64,9 +64,9 @@ func ApiRepoAssignment() macaron.Handler { ctx.JSON(500, &base.ApiJsonErr{"AccessLevel: " + err.Error(), base.DOC_URL}) return } - ctx.Repo.IsOwner = mode >= models.WriteAccess - ctx.Repo.IsAdmin = mode >= models.ReadAccess - ctx.Repo.IsTrueOwner = mode >= models.OwnerAccess + ctx.Repo.IsOwner = mode >= models.ACCESS_MODE_WRITE + ctx.Repo.IsAdmin = mode >= models.ACCESS_MODE_READ + ctx.Repo.IsTrueOwner = mode >= models.ACCESS_MODE_OWNER } // Check access. @@ -244,9 +244,9 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { ctx.JSON(500, &base.ApiJsonErr{"AccessLevel: " + err.Error(), base.DOC_URL}) return } - ctx.Repo.IsOwner = mode >= models.WriteAccess - ctx.Repo.IsAdmin = mode >= models.ReadAccess - ctx.Repo.IsTrueOwner = mode >= models.OwnerAccess + ctx.Repo.IsOwner = mode >= models.ACCESS_MODE_WRITE + ctx.Repo.IsAdmin = mode >= models.ACCESS_MODE_READ + ctx.Repo.IsTrueOwner = mode >= models.ACCESS_MODE_OWNER } // Check access. diff --git a/routers/api/v1/repo.go b/routers/api/v1/repo.go index 78c9f9a6a3..f5128e4746 100644 --- a/routers/api/v1/repo.go +++ b/routers/api/v1/repo.go @@ -255,7 +255,7 @@ func ListMyRepos(ctx *middleware.Context) { return } - repos[i] = ToApiRepository(repo.Owner, repo, api.Permission{false, access >= models.WriteAccess, true}) + repos[i] = ToApiRepository(repo.Owner, repo, api.Permission{false, access >= models.ACCESS_MODE_WRITE, true}) // FIXME: cache result to reduce DB query? if repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(ctx.User.Id) { diff --git a/routers/org/teams.go b/routers/org/teams.go index 4fef02c987..a315abe03e 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -171,11 +171,11 @@ func NewTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { var auth models.AccessMode switch form.Permission { case "read": - auth = models.ReadAccess + auth = models.ACCESS_MODE_READ case "write": - auth = models.WriteAccess + auth = models.ACCESS_MODE_WRITE case "admin": - auth = models.AdminAccess + auth = models.ACCESS_MODE_ADMIN default: ctx.Error(401) return @@ -252,11 +252,11 @@ func EditTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { var auth models.AccessMode switch form.Permission { case "read": - auth = models.ReadAccess + auth = models.ACCESS_MODE_READ case "write": - auth = models.WriteAccess + auth = models.ACCESS_MODE_WRITE case "admin": - auth = models.AdminAccess + auth = models.ACCESS_MODE_ADMIN default: ctx.Error(401) return diff --git a/routers/repo/http.go b/routers/repo/http.go index 716c71272a..4173c7a91f 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -115,9 +115,9 @@ func Http(ctx *middleware.Context) { } if !isPublicPull { - var tp = models.WriteAccess + var tp = models.ACCESS_MODE_WRITE if isPull { - tp = models.ReadAccess + tp = models.ACCESS_MODE_READ } has, err := models.HasAccess(authUser, repo, tp) @@ -125,8 +125,8 @@ func Http(ctx *middleware.Context) { ctx.Handle(401, "no basic auth and digit auth", nil) return } else if !has { - if tp == models.ReadAccess { - has, err = models.HasAccess(authUser, repo, models.WriteAccess) + if tp == models.ACCESS_MODE_READ { + has, err = models.HasAccess(authUser, repo, models.ACCESS_MODE_WRITE) if err != nil || !has { ctx.Handle(401, "no basic auth and digit auth", nil) return @@ -268,7 +268,7 @@ func serviceRpc(rpc string, hr handler) { access := hasAccess(r, hr.Config, dir, rpc, true) if access == false { - renderNoAccess(w) + renderACCESS_MODE_NONE(w) return } @@ -495,7 +495,7 @@ func renderNotFound(w http.ResponseWriter) { w.Write([]byte("Not Found")) } -func renderNoAccess(w http.ResponseWriter) { +func renderACCESS_MODE_NONE(w http.ResponseWriter) { w.WriteHeader(http.StatusForbidden) w.Write([]byte("Forbidden")) } diff --git a/routers/user/home.go b/routers/user/home.go index 82325cb747..ce82ae772c 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -103,7 +103,7 @@ func Dashboard(ctx *middleware.Context) { feeds := make([]*models.Action, 0, len(actions)) for _, act := range actions { if act.IsPrivate { - if has, _ := models.HasAccess(ctx.User, &models.Repository{Id: act.RepoId, IsPrivate: true}, models.ReadAccess); !has { + if has, _ := models.HasAccess(ctx.User, &models.Repository{Id: act.RepoId, IsPrivate: true}, models.ACCESS_MODE_READ); !has { continue } } @@ -211,7 +211,7 @@ func Profile(ctx *middleware.Context) { continue } if has, _ := models.HasAccess(ctx.User, &models.Repository{Id: act.RepoId, IsPrivate: true}, - models.ReadAccess); !has { + models.ACCESS_MODE_READ); !has { continue } } From 7e7160eefd6524013003d1c9668db4d358df5251 Mon Sep 17 00:00:00 2001 From: Unknwon Date: Wed, 11 Feb 2015 21:58:37 -0500 Subject: [PATCH 06/26] first pass work on migration #925 --- cmd/serve.go | 2 +- models/access.go | 42 +++---- models/issue.go | 21 ++-- models/migrations/migrations.go | 95 +++++++++------- models/models.go | 19 ++-- models/repo.go | 195 +++++++++++++++++++------------- models/user.go | 9 +- modules/setting/setting.go | 5 + templates/ng/base/footer.tmpl | 2 +- 9 files changed, 226 insertions(+), 164 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 25f7dd9191..e957d45de7 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -43,7 +43,7 @@ func setup(logPath string) { models.LoadModelsConfig() - if models.UseSQLite3 { + if setting.UseSQLite3 { workDir, _ := setting.WorkDir() os.Chdir(workDir) } diff --git a/models/access.go b/models/access.go index 64bb921409..6df1da29bc 100644 --- a/models/access.go +++ b/models/access.go @@ -7,8 +7,6 @@ package models import ( "strings" "time" - - "github.com/go-xorm/xorm" ) type AccessType int @@ -27,35 +25,40 @@ type Access struct { Created time.Time `xorm:"CREATED"` } -// AddAccess adds new access record. -func AddAccess(access *Access) error { +func addAccess(e Engine, access *Access) error { access.UserName = strings.ToLower(access.UserName) access.RepoName = strings.ToLower(access.RepoName) - _, err := x.Insert(access) + _, err := e.Insert(access) return err } +// AddAccess adds new access record. +func AddAccess(access *Access) error { + return addAccess(x, access) +} + +func updateAccess(e Engine, access *Access) error { + if _, err := e.Id(access.Id).Update(access); err != nil { + return err + } + return nil +} + // UpdateAccess updates access information. func UpdateAccess(access *Access) error { access.UserName = strings.ToLower(access.UserName) access.RepoName = strings.ToLower(access.RepoName) - _, err := x.Id(access.Id).Update(access) + return updateAccess(x, access) +} + +func deleteAccess(e Engine, access *Access) error { + _, err := e.Delete(access) return err } // DeleteAccess deletes access record. func DeleteAccess(access *Access) error { - _, err := x.Delete(access) - return err -} - -// UpdateAccess updates access information with session for rolling back. -func UpdateAccessWithSession(sess *xorm.Session, access *Access) error { - if _, err := sess.Id(access.Id).Update(access); err != nil { - sess.Rollback() - return err - } - return nil + return deleteAccess(x, access) } // HasAccess returns true if someone can read or write to given repository. @@ -93,9 +96,10 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessType, error) { if err != nil { return nil, err } - err = repo.GetOwner() - if err != nil { + if err = repo.GetOwner(); err != nil { return nil, err + } else if repo.OwnerId == u.Id { + continue } repos[repo] = access.Mode } diff --git a/models/issue.go b/models/issue.go index 9e1d52260f..3cd71d8aa4 100644 --- a/models/issue.go +++ b/models/issue.go @@ -282,30 +282,33 @@ type IssueUser struct { } // NewIssueUserPairs adds new issue-user pairs for new issue of repository. -func NewIssueUserPairs(repo *Repository, iid, oid, pid, aid int64) (err error) { - iu := &IssueUser{IssueId: iid, RepoId: repo.Id} - - us, err := repo.GetCollaborators() +func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID int64) (err error) { + users, err := repo.GetCollaborators() if err != nil { return err } + iu := &IssueUser{ + IssueId: issueID, + RepoId: repo.Id, + } + isNeedAddPoster := true - for _, u := range us { + for _, u := range users { iu.Uid = u.Id - iu.IsPoster = iu.Uid == pid + iu.IsPoster = iu.Uid == posterID if isNeedAddPoster && iu.IsPoster { isNeedAddPoster = false } - iu.IsAssigned = iu.Uid == aid + iu.IsAssigned = iu.Uid == assigneeID if _, err = x.Insert(iu); err != nil { return err } } if isNeedAddPoster { - iu.Uid = pid + iu.Uid = posterID iu.IsPoster = true - iu.IsAssigned = iu.Uid == aid + iu.IsAssigned = iu.Uid == assigneeID if _, err = x.Insert(iu); err != nil { return err } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index f0ed10b7aa..27ab36c844 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -1,14 +1,23 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package migrations import ( "errors" - "strconv" + "fmt" "strings" "time" + "github.com/Unknwon/com" "github.com/go-xorm/xorm" + + "github.com/gogits/gogs/modules/setting" ) +const _DB_VER = 1 + type migration func(*xorm.Engine) error // The version table. Should have only one row with id==1 @@ -26,39 +35,38 @@ var migrations = []migration{ // Migrate database to current version func Migrate(x *xorm.Engine) error { if err := x.Sync(new(Version)); err != nil { - return err + return fmt.Errorf("sync: %v", err) } currentVersion := &Version{Id: 1} has, err := x.Get(currentVersion) if err != nil { - return err + return fmt.Errorf("get: %v", err) } else if !has { needsMigration, err := x.IsTableExist("user") if err != nil { return err } - if needsMigration { - isEmpty, err := x.IsTableEmpty("user") - if err != nil { - return err - } - needsMigration = !isEmpty - } + // if needsMigration { + // isEmpty, err := x.IsTableEmpty("user") + // if err != nil { + // return err + // } + // needsMigration = !isEmpty + // } if !needsMigration { currentVersion.Version = int64(len(migrations)) } if _, err = x.InsertOne(currentVersion); err != nil { - return err + return fmt.Errorf("insert: %v", err) } } v := currentVersion.Version - for i, migration := range migrations[v:] { if err = migration(x); err != nil { - return err + return fmt.Errorf("run migration: %v", err) } currentVersion.Version = v + int64(i) + 1 if _, err = x.Id(1).Update(currentVersion); err != nil { @@ -72,48 +80,48 @@ func expiredMigration(x *xorm.Engine) error { return errors.New("You are migrating from a too old gogs version") } -func mustParseInt64(in []byte) int64 { - i, err := strconv.ParseInt(string(in), 10, 64) - if err != nil { - i = 0 - } - return i -} - func accessToCollaboration(x *xorm.Engine) error { type Collaboration struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Created time.Time `xorm:"CREATED"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Created time.Time } x.Sync(new(Collaboration)) - sql := `SELECT u.id AS uid, a.repo_name AS repo, a.mode AS mode, a.created as created FROM access a JOIN user u ON a.user_name=u.lower_name` - results, err := x.Query(sql) + results, err := x.Query("SELECT u.id AS `uid`, a.repo_name AS `repo`, a.mode AS `mode`, a.created as `created` FROM `access` a JOIN `user` u ON a.user_name=u.lower_name") if err != nil { return err } + offset := strings.Split(time.Now().String(), " ")[2] for _, result := range results { - userID := mustParseInt64(result["uid"]) - repoRefName := string(result["repo"]) - mode := mustParseInt64(result["mode"]) - created := result["created"] - - //Collaborators must have write access + mode := com.StrTo(result["mode"]).MustInt64() + // Collaborators must have write access. if mode < 2 { continue } + userID := com.StrTo(result["uid"]).MustInt64() + repoRefName := string(result["repo"]) + + var created time.Time + switch { + case setting.UseSQLite3: + created, _ = time.Parse(time.RFC3339, string(result["created"])) + case setting.UseMySQL: + created, _ = time.Parse("2006-01-02 15:04:05-0700", string(result["created"])+offset) + case setting.UsePostgreSQL: + created, _ = time.Parse("2006-01-02T15:04:05Z-0700", string(result["created"])+offset) + } + // find owner of repository parts := strings.SplitN(repoRefName, "/", 2) ownerName := parts[0] repoName := parts[1] - sql = `SELECT u.id as uid, ou.uid as memberid FROM user u LEFT JOIN org_user ou ON ou.org_id=u.id WHERE u.lower_name=?` - results, err := x.Query(sql, ownerName) + results, err := x.Query("SELECT u.id as `uid`, ou.uid as `memberid` FROM `user` u LEFT JOIN org_user ou ON ou.org_id=u.id WHERE u.lower_name=?", ownerName) if err != nil { return err } @@ -121,7 +129,7 @@ func accessToCollaboration(x *xorm.Engine) error { continue } - ownerID := mustParseInt64(results[0]["uid"]) + ownerID := com.StrTo(results[0]["uid"]).MustInt64() if ownerID == userID { continue } @@ -129,7 +137,7 @@ func accessToCollaboration(x *xorm.Engine) error { // test if user is member of owning organization isMember := false for _, member := range results { - memberID := mustParseInt64(member["memberid"]) + memberID := com.StrTo(member["memberid"]).MustInt64() // We can skip all cases that a user is member of the owning organization if memberID == userID { isMember = true @@ -139,8 +147,7 @@ func accessToCollaboration(x *xorm.Engine) error { continue } - sql = `SELECT id FROM repository WHERE owner_id=? AND lower_name=?` - results, err = x.Query(sql, ownerID, repoName) + results, err = x.Query("SELECT id FROM `repository` WHERE owner_id=? AND lower_name=?", ownerID, repoName) if err != nil { return err } @@ -148,11 +155,11 @@ func accessToCollaboration(x *xorm.Engine) error { continue } - repoID := results[0]["id"] - - sql = `INSERT INTO collaboration (user_id, repo_id, created) VALUES (?,?,?)` - _, err = x.Exec(sql, userID, repoID, created) - if err != nil { + if _, err = x.InsertOne(&Collaboration{ + UserID: userID, + RepoID: com.StrTo(results[0]["id"]).MustInt64(), + Created: created, + }); err != nil { return err } } diff --git a/models/models.go b/models/models.go index dbe3947495..141e3ac497 100644 --- a/models/models.go +++ b/models/models.go @@ -40,24 +40,29 @@ var ( } EnableSQLite3 bool - UseSQLite3 bool ) func init() { tables = append(tables, - new(User), new(PublicKey), new(Follow), new(Oauth2), new(AccessToken), - new(Repository), new(Watch), new(Star), new(Action), new(Access), + new(User), new(PublicKey), new(Oauth2), new(AccessToken), + new(Repository), new(Collaboration), new(Access), + new(Watch), new(Star), new(Follow), new(Action), new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone), new(Mirror), new(Release), new(LoginSource), new(Webhook), new(UpdateTask), new(HookTask), new(Team), new(OrgUser), new(TeamUser), - new(Notice), new(EmailAddress), new(Collaboration)) + new(Notice), new(EmailAddress)) } func LoadModelsConfig() { sec := setting.Cfg.Section("database") DbCfg.Type = sec.Key("DB_TYPE").String() - if DbCfg.Type == "sqlite3" { - UseSQLite3 = true + switch DbCfg.Type { + case "sqlite3": + setting.UseSQLite3 = true + case "mysql": + setting.UseMySQL = true + case "postgres": + setting.UsePostgreSQL = true } DbCfg.Host = sec.Key("HOST").String() DbCfg.Name = sec.Key("NAME").String() @@ -141,7 +146,7 @@ func NewEngine() (err error) { } if err = migrations.Migrate(x); err != nil { - return err + return fmt.Errorf("migrate: %v", err) } if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil { diff --git a/models/repo.go b/models/repo.go index d3faa09bba..35ee871fd4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -847,7 +847,8 @@ func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error) for i := range accesses { accesses[i].RepoName = userName + "/" + newRepoName - if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { + if err = updateAccess(sess, &accesses[i]); err != nil { + sess.Rollback() return err } } @@ -1062,76 +1063,6 @@ func GetRepositoryCount(user *User) (int64, error) { return x.Count(&Repository{OwnerId: user.Id}) } -// GetCollaborators returns the collaborators for a repository -func (r *Repository) GetCollaborators() ([]*User, error) { - collaborations := make([]*Collaboration, 0) - if err := x.Find(&collaborations, &Collaboration{RepoID: r.Id}); err != nil { - return nil, err - } - - users := make([]*User, len(collaborations)) - for i, c := range collaborations { - user, err := GetUserById(c.UserID) - if err != nil { - return nil, err - } - users[i] = user - } - return users, nil -} - -// Add collaborator and accompanying access -func (r *Repository) AddCollaborator(u *User) error { - collaboration := &Collaboration{RepoID: r.Id, UserID: u.Id} - - has, err := x.Get(collaboration) - if err != nil { - return err - } - if has { - return nil - } - - if _, err = x.InsertOne(collaboration); err != nil { - return err - } - - if err = r.GetOwner(); err != nil { - return err - } - - return AddAccess(&Access{UserName: u.LowerName, RepoName: path.Join(r.Owner.LowerName, r.LowerName), Mode: WRITABLE}) -} - -// Delete collaborator and accompanying access -func (r *Repository) DeleteCollaborator(u *User) error { - collaboration := &Collaboration{RepoID: r.Id, UserID: u.Id} - - if has, err := x.Delete(collaboration); err != nil || has == 0 { - return err - } - - if err := r.GetOwner(); err != nil { - return err - } - - needDelete := true - if r.Owner.IsOrganization() { - auth, err := GetHighestAuthorize(r.Owner.Id, u.Id, r.Id, 0) - if err != nil { - return err - } - if auth > 0 { - needDelete = false - } - } - if needDelete { - return DeleteAccess(&Access{UserName: u.LowerName, RepoName: path.Join(r.Owner.LowerName, r.LowerName), Mode: WRITABLE}) - } - - return nil -} - type SearchOption struct { Keyword string Uid int64 @@ -1279,6 +1210,120 @@ func GitGcRepos() error { }) } +// _________ .__ .__ ___. __ .__ +// \_ ___ \ ____ | | | | _____ \_ |__ ________________ _/ |_|__| ____ ____ +// / \ \/ / _ \| | | | \__ \ | __ \ / _ \_ __ \__ \\ __\ |/ _ \ / \ +// \ \___( <_> ) |_| |__/ __ \| \_\ ( <_> ) | \// __ \| | | ( <_> ) | \ +// \______ /\____/|____/____(____ /___ /\____/|__| (____ /__| |__|\____/|___| / +// \/ \/ \/ \/ \/ + +// A Collaboration is a relation between an individual and a repository +type Collaboration struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Created time.Time `xorm:"CREATED"` +} + +// Add collaborator and accompanying access +func (r *Repository) AddCollaborator(u *User) error { + collaboration := &Collaboration{RepoID: r.Id, UserID: u.Id} + has, err := x.Get(collaboration) + if err != nil { + return err + } else if has { + return nil + } + + if err = r.GetOwner(); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.InsertOne(collaboration); err != nil { + sess.Rollback() + return err + } else if err = addAccess(sess, &Access{ + UserName: u.LowerName, + RepoName: path.Join(r.Owner.LowerName, r.LowerName), + Mode: WRITABLE}); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + +// GetCollaborators returns the collaborators for a repository +func (r *Repository) GetCollaborators() ([]*User, error) { + collaborations := make([]*Collaboration, 0, 5) + if err := x.Where("repo_id=?", r.Id).Find(&collaborations); err != nil { + return nil, err + } + + users := make([]*User, len(collaborations)) + for i, c := range collaborations { + user, err := GetUserById(c.UserID) + if err != nil { + return nil, err + } + users[i] = user + } + return users, nil +} + +// Delete collaborator and accompanying access +func (r *Repository) DeleteCollaborator(u *User) (err error) { + collaboration := &Collaboration{RepoID: r.Id, UserID: u.Id} + has, err := x.Get(collaboration) + if err != nil { + return err + } else if !has { + return nil + } + + if err = r.GetOwner(); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + needDelete := true + if r.Owner.IsOrganization() { + auth, err := getHighestAuthorize(sess, r.Owner.Id, u.Id, r.Id, 0) + if err != nil { + sess.Rollback() + return err + } + if auth > 0 { + needDelete = false + } + } + if needDelete { + if err = deleteAccess(sess, &Access{ + UserName: u.LowerName, + RepoName: path.Join(r.Owner.LowerName, r.LowerName), + Mode: WRITABLE}); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Delete(collaboration); err != nil { + sess.Rollback() + return err + } + } + + return sess.Commit() +} + // __ __ __ .__ // / \ / \_____ _/ |_ ____ | |__ // \ \/\/ /\__ \\ __\/ ___\| | \ @@ -1559,11 +1604,3 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (*Repositor return repo, nil } - -// A Collaboration is a relation between an individual and a repository -type Collaboration struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Created time.Time `xorm:"CREATED"` -} diff --git a/models/user.go b/models/user.go index 2da0881c81..5606cea379 100644 --- a/models/user.go +++ b/models/user.go @@ -415,7 +415,8 @@ func ChangeUserName(u *User, newUserName string) (err error) { if strings.HasPrefix(accesses[i].RepoName, u.LowerName+"/") { accesses[i].RepoName = strings.Replace(accesses[i].RepoName, u.LowerName, newUserName, 1) } - if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { + if err = updateAccess(sess, &accesses[i]); err != nil { + sess.Rollback() return err } } @@ -435,7 +436,8 @@ func ChangeUserName(u *User, newUserName string) (err error) { // if the access is not the user's access (already updated above) if accesses[j].UserName != u.LowerName { accesses[j].RepoName = newUserName + "/" + repos[i].LowerName - if err = UpdateAccessWithSession(sess, &accesses[j]); err != nil { + if err = updateAccess(sess, &accesses[j]); err != nil { + sess.Rollback() return err } } @@ -564,8 +566,7 @@ func UserPath(userName string) string { func GetUserByKeyId(keyId int64) (*User, error) { user := new(User) - rawSql := "SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?" - has, err := x.Sql(rawSql, keyId).Get(user) + has, err := x.Sql("SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?", keyId).Get(user) if err != nil { return nil, err } else if !has { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 6664c41907..795bcc5ba2 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -67,6 +67,11 @@ var ( CookieRememberName string ReverseProxyAuthUser string + // Database settings. + UseSQLite3 bool + UseMySQL bool + UsePostgreSQL bool + // Webhook settings. Webhook struct { TaskInterval int diff --git a/templates/ng/base/footer.tmpl b/templates/ng/base/footer.tmpl index e152a4ded1..89836fca23 100644 --- a/templates/ng/base/footer.tmpl +++ b/templates/ng/base/footer.tmpl @@ -1,7 +1,7 @@