Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions models/perm/access/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mo
}
}

// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) {
minMode := perm.AccessModeRead
if err := repo.LoadOwner(ctx); err != nil {
Expand All @@ -104,30 +103,76 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
minMode = perm.AccessModeWrite
}

newAccesses := make([]Access, 0, len(accessMap))
// Query existing accesses for cross-comparison
var existingAccesses []Access
if err := db.GetEngine(ctx).Where(builder.Eq{"repo_id": repo.ID}).Find(&existingAccesses); err != nil {
return fmt.Errorf("find existing accesses: %w", err)
}
existingMap := make(map[int64]perm.AccessMode, len(existingAccesses))
for _, a := range existingAccesses {
existingMap[a.UserID] = a.Mode
}

var toDelete []int64
var toInsert []Access
var toUpdate []struct {
UserID int64
Mode perm.AccessMode
}
Comment on lines +118 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can reuse the Access struct, and it could be clearer for the code below


// Determine changes
for userID, ua := range accessMap {
if ua.Mode < minMode && !ua.User.IsRestricted {
continue
// Should not have access
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment seems not right.

If I read correctly, it doesn't mean "Should not have access", but "default access" is enough?


If it really means "should not have access", why ua.User.IsRestricted is kept

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a test:

  1. Add U1 as public repo's collaborator with "read" access
  2. Trigger "refreshAccesses"
  3. U1's "access" record is skipped
  4. U1 can still access the public repo because the repo is public

if _, exists := existingMap[userID]; exists {
toDelete = append(toDelete, userID)
}
} else {
desiredMode := ua.Mode
if existingMode, exists := existingMap[userID]; exists {
if existingMode != desiredMode {
toUpdate = append(toUpdate, struct {
UserID int64
Mode perm.AccessMode
}{UserID: userID, Mode: desiredMode})
}
} else {
toInsert = append(toInsert, Access{
UserID: userID,
RepoID: repo.ID,
Mode: desiredMode,
})
}
}
delete(existingMap, userID)
}

newAccesses = append(newAccesses, Access{
UserID: userID,
RepoID: repo.ID,
Mode: ua.Mode,
})
// Remaining in existingMap should be deleted
for userID := range existingMap {
toDelete = append(toDelete, userID)
}

// Delete old accesses and insert new ones for repository.
if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil {
return fmt.Errorf("delete old accesses: %w", err)
// Execute deletions
if len(toDelete) > 0 {
if _, err = db.GetEngine(ctx).In("user_id", toDelete).And("repo_id = ?", repo.ID).Delete(&Access{}); err != nil {
return fmt.Errorf("delete accesses: %w", err)
}
}
if len(newAccesses) == 0 {
return nil

// Execute updates
for _, u := range toUpdate {
if _, err = db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", u.UserID, repo.ID).Cols("mode").Update(&Access{Mode: u.Mode}); err != nil {
return fmt.Errorf("update access for user %d: %w", u.UserID, err)
}
}

if err = db.Insert(ctx, newAccesses); err != nil {
return fmt.Errorf("insert new accesses: %w", err)
// Execute insertions
if len(toInsert) > 0 {
if err = db.Insert(ctx, toInsert); err != nil {
return fmt.Errorf("insert new accesses: %w", err)
}
}

return nil
}

Expand Down
35 changes: 35 additions & 0 deletions models/perm/access/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,38 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
assert.NoError(t, err)
assert.False(t, has)
}

func TestRepository_RecalculateAccesses_UpdateMode(t *testing.T) {
// Test the update path in refreshAccesses optimization
// Scenario: User's access mode changes from Read to Write
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.NoError(t, repo.LoadOwner(t.Context()))

// Verify initial access mode
initialAccess := &access_model.Access{UserID: 4, RepoID: 4}
has, err := db.GetEngine(t.Context()).Get(initialAccess)
assert.NoError(t, err)
assert.True(t, has)
initialMode := initialAccess.Mode

// Change collaboration mode to trigger update path
newMode := perm_model.AccessModeAdmin
assert.NotEqual(t, initialMode, newMode, "New mode should differ from initial mode")

_, err = db.GetEngine(t.Context()).
Where("user_id = ? AND repo_id = ?", 4, 4).
Cols("mode").
Update(&repo_model.Collaboration{Mode: newMode})
assert.NoError(t, err)

// Recalculate accesses - should UPDATE existing access, not delete+insert
assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo))

// Verify access was updated, not deleted and re-inserted
updatedAccess := &access_model.Access{UserID: 4, RepoID: 4}
has, err = db.GetEngine(t.Context()).Get(updatedAccess)
assert.NoError(t, err)
assert.True(t, has, "Access should still exist")
assert.Equal(t, newMode, updatedAccess.Mode, "Access mode should be updated to new collaboration mode")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to have another use case to remove this user's permission and have a test again.

}