From 1f533c45213a2675023b18e449491e0f3a7bd70d Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 10 May 2024 13:28:51 +0200 Subject: [PATCH 01/17] BLENDER: Blender ID goth provider Provider authored by Matti Ranta and Arnd Marijnissen. --- go.mod | 1 + go.sum | 3 + public/assets/img/blenderid.png | 0 .../auth/source/oauth2/blenderid/blenderid.go | 181 ++++++++++++++++++ .../source/oauth2/blenderid/blenderid_test.go | 70 +++++++ .../oauth2/blenderid/gitealize_usernames.go | 65 +++++++ .../blenderid/gitealize_usernames_test.go | 43 +++++ .../auth/source/oauth2/blenderid/session.go | 66 +++++++ .../source/oauth2/blenderid/session_test.go | 51 +++++ .../auth/source/oauth2/providers_custom.go | 11 ++ 10 files changed, 491 insertions(+) create mode 100644 public/assets/img/blenderid.png create mode 100644 services/auth/source/oauth2/blenderid/blenderid.go create mode 100644 services/auth/source/oauth2/blenderid/blenderid_test.go create mode 100644 services/auth/source/oauth2/blenderid/gitealize_usernames.go create mode 100644 services/auth/source/oauth2/blenderid/gitealize_usernames_test.go create mode 100644 services/auth/source/oauth2/blenderid/session.go create mode 100644 services/auth/source/oauth2/blenderid/session_test.go diff --git a/go.mod b/go.mod index f6d079dbbb336..683ce90aa8068 100644 --- a/go.mod +++ b/go.mod @@ -257,6 +257,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mozillazg/go-unidecode v0.2.0 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 9b200cc2d9477..2bc60270c1374 100644 --- a/go.sum +++ b/go.sum @@ -590,6 +590,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mozillazg/go-unidecode v0.2.0 h1:vFGEzAH9KSwyWmXCOblazEWDh7fOkpmy/Z4ArmamSUc= +github.com/mozillazg/go-unidecode v0.2.0/go.mod h1:zB48+/Z5toiRolOZy9ksLryJ976VIwmDmpQ2quyt1aA= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= diff --git a/public/assets/img/blenderid.png b/public/assets/img/blenderid.png new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/services/auth/source/oauth2/blenderid/blenderid.go b/services/auth/source/oauth2/blenderid/blenderid.go new file mode 100644 index 0000000000000..671e5e4f541a5 --- /dev/null +++ b/services/auth/source/oauth2/blenderid/blenderid.go @@ -0,0 +1,181 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +// Package blenderid implements the OAuth2 protocol for authenticating users through Blender ID +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +package blenderid + +// Allow "encoding/json" import. +import ( + "bytes" + "encoding/json" //nolint:depguard + "errors" + "fmt" + "io" + "net/http" + "strconv" + + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +// These vars define the default Authentication, Token, and Profile URLS for Blender ID +// +// Examples: +// +// blenderid.AuthURL = "https://id.blender.org/oauth/authorize +// blenderid.TokenURL = "https://id.blender.org/oauth/token +// blenderid.ProfileURL = "https://id.blender.org/api/me +var ( + AuthURL = "https://id.blender.org/oauth/authorize" + TokenURL = "https://id.blender.org/oauth/token" + ProfileURL = "https://id.blender.org/api/me" +) + +// Provider is the implementation of `goth.Provider` for accessing Blender ID +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string + profileURL string +} + +// New creates a new Blender ID provider and sets up important connection details. +// You should always call `blenderid.New` to get a new provider. Never try to +// create one manually. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, scopes...) +} + +// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to +func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "blenderid", + profileURL: profileURL, + } + p.config = newConfig(p, authURL, tokenURL, scopes) + return p +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the blenderid package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Blender ID for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + return &Session{ + AuthURL: p.config.AuthCodeURL(state), + }, nil +} + +// FetchUser will go to Blender ID and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + sess := session.(*Session) + user := goth.User{ + AccessToken: sess.AccessToken, + Provider: p.Name(), + RefreshToken: sess.RefreshToken, + ExpiresAt: sess.ExpiresAt, + } + + if user.AccessToken == "" { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + req, err := http.NewRequest("GET", p.profileURL, nil) + if err != nil { + return user, err + } + + req.Header.Add("Authorization", "Bearer "+sess.AccessToken) + response, err := p.Client().Do(req) + if err != nil { + return user, err + } + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("Blender ID responded with a %d trying to fetch user information", response.StatusCode) + } + + bits, err := io.ReadAll(response.Body) + if err != nil { + return user, err + } + + err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) + if err != nil { + return user, err + } + + err = userFromReader(bytes.NewReader(bits), &user) + if err != nil { + return user, err + } + + return user, err +} + +func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: authURL, + TokenURL: tokenURL, + }, + Scopes: []string{}, + } + + if len(scopes) > 0 { + c.Scopes = append(c.Scopes, scopes...) + } + return c +} + +func userFromReader(r io.Reader, user *goth.User) error { + u := struct { + Name string `json:"full_name"` + Email string `json:"email"` + NickName string `json:"nickname"` + ID int `json:"id"` + }{} + err := json.NewDecoder(r).Decode(&u) + if err != nil { + return err + } + user.Email = u.Email + user.Name = u.Name + user.NickName = gitealizeUsername(u.NickName) + user.UserID = strconv.Itoa(u.ID) + user.AvatarURL = fmt.Sprintf("https://id.blender.org/api/user/%s/avatar", user.UserID) + return nil +} + +// RefreshTokenAvailable refresh token is not provided by Blender ID +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +// RefreshToken refresh token is not provided by Blender ID +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return nil, errors.New("Refresh token is not provided by Blender ID") +} diff --git a/services/auth/source/oauth2/blenderid/blenderid_test.go b/services/auth/source/oauth2/blenderid/blenderid_test.go new file mode 100644 index 0000000000000..283ba0898a83f --- /dev/null +++ b/services/auth/source/oauth2/blenderid/blenderid_test.go @@ -0,0 +1,70 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package blenderid_test + +import ( + "os" + "testing" + + "code.gitea.io/gitea/services/auth/source/oauth2/blenderid" + + "github.com/markbates/goth" + "github.com/stretchr/testify/assert" +) + +func Test_New(t *testing.T) { + t.Parallel() + a := assert.New(t) + p := provider() + + a.Equal(p.ClientKey, os.Getenv("BLENDERID_KEY")) + a.Equal(p.Secret, os.Getenv("BLENDERID_SECRET")) + a.Equal("/foo", p.CallbackURL) +} + +func Test_NewCustomisedURL(t *testing.T) { + t.Parallel() + a := assert.New(t) + p := urlCustomisedURLProvider() + session, err := p.BeginAuth("test_state") + s := session.(*blenderid.Session) + a.NoError(err) + a.Contains(s.AuthURL, "http://authURL") +} + +func Test_Implements_Provider(t *testing.T) { + t.Parallel() + a := assert.New(t) + a.Implements((*goth.Provider)(nil), provider()) +} + +func Test_BeginAuth(t *testing.T) { + t.Parallel() + a := assert.New(t) + p := provider() + session, err := p.BeginAuth("test_state") + s := session.(*blenderid.Session) + a.NoError(err) + a.Contains(s.AuthURL, "id.blender.org/oauth/authorize") +} + +func Test_SessionFromJSON(t *testing.T) { + t.Parallel() + a := assert.New(t) + + p := provider() + session, err := p.UnmarshalSession(`{"AuthURL":"https://id.blender.org/oauth/authorize","AccessToken":"1234567890"}`) + a.NoError(err) + + s := session.(*blenderid.Session) + a.Equal("https://id.blender.org/oauth/authorize", s.AuthURL) + a.Equal("1234567890", s.AccessToken) +} + +func provider() *blenderid.Provider { + return blenderid.New(os.Getenv("BLENDERID_KEY"), os.Getenv("BLENDERID_SECRET"), "/foo") +} + +func urlCustomisedURLProvider() *blenderid.Provider { + return blenderid.NewCustomisedURL(os.Getenv("BLENDERID_KEY"), os.Getenv("BLENDERID_SECRET"), "/foo", "http://authURL", "http://tokenURL", "http://profileURL") +} diff --git a/services/auth/source/oauth2/blenderid/gitealize_usernames.go b/services/auth/source/oauth2/blenderid/gitealize_usernames.go new file mode 100644 index 0000000000000..880516c8e28f1 --- /dev/null +++ b/services/auth/source/oauth2/blenderid/gitealize_usernames.go @@ -0,0 +1,65 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package blenderid + +import ( + "regexp" + "strings" + + "code.gitea.io/gitea/models/user" + + "github.com/mozillazg/go-unidecode" +) + +var ( + reInvalidCharsPattern = regexp.MustCompile(`[^\da-zA-Z.\w-]+`) + + // Consecutive non-alphanumeric at start: + reConsPrefix = regexp.MustCompile(`^[._-]+`) + reConsSuffix = regexp.MustCompile(`[._-]+$`) + reConsInfix = regexp.MustCompile(`[._-]{2,}`) +) + +// gitealizeUsername turns a valid Blender ID nickname into a valid Gitea username. +func gitealizeUsername(bidNickname string) string { + // Remove accents and other non-ASCIIness. + asciiUsername := unidecode.Unidecode(bidNickname) + asciiUsername = strings.TrimSpace(asciiUsername) + asciiUsername = strings.ReplaceAll(asciiUsername, " ", "_") + + err := user.IsUsableUsername(asciiUsername) + if err == nil && len(asciiUsername) <= 40 { + return asciiUsername + } + + newUsername := asciiUsername + newUsername = reInvalidCharsPattern.ReplaceAllString(newUsername, "_") + newUsername = reConsPrefix.ReplaceAllString(newUsername, "") + newUsername = reConsSuffix.ReplaceAllString(newUsername, "") + newUsername = reConsInfix.ReplaceAllStringFunc( + newUsername, + func(match string) string { + firstRune := []rune(match)[0] + return string(firstRune) + }) + + if newUsername == "" { + // Everything was stripped and nothing was left. Better to keep as-is and + // just let Gitea bork on it. + return asciiUsername + } + + // This includes a test for reserved names, which are easily circumvented by + // appending another character. + if user.IsUsableUsername(newUsername) != nil { + if len(newUsername) > 39 { + return newUsername[:39] + "2" + } + return newUsername + "2" + } + + if len(newUsername) > 40 { + return newUsername[:40] + } + return newUsername +} diff --git a/services/auth/source/oauth2/blenderid/gitealize_usernames_test.go b/services/auth/source/oauth2/blenderid/gitealize_usernames_test.go new file mode 100644 index 0000000000000..7d633198e8886 --- /dev/null +++ b/services/auth/source/oauth2/blenderid/gitealize_usernames_test.go @@ -0,0 +1,43 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package blenderid + +import "testing" + +func Test_gitealizeUsername(t *testing.T) { + tests := []struct { + name string + bidNickname string + want string + }{ + {"empty", "", ""}, + {"underscore", "_", "_"}, + {"reserved-name", "ghost", "ghost2"}, // Reserved name in Gitea. + {"short", "x", "x"}, + {"simple", "simple", "simple"}, + {"start-bad", "____startbad", "startbad"}, + {"end-bad", "endbad___", "endbad"}, + {"mid-bad-1", "mid__bad", "mid_bad"}, + {"mid-bad-2", "user_.-name", "user_name"}, + {"plus-mid-single", "RT2+356", "RT2_356"}, + {"plus-mid-many", "RT2+++356", "RT2_356"}, + {"plus-end", "RT2356+", "RT2356"}, + { + "too-long", // # Max username length is 40: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + {"accented-latin", "Ümlaut-Đenja", "Umlaut-Denja"}, + {"thai", "แบบไทย", "aebbaithy"}, + {"mandarin", "普通话", "Pu_Tong_Hua"}, + {"cyrillic", "ћирилица", "tshirilitsa"}, + {"all-bad", "------", "------"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := gitealizeUsername(tt.bidNickname); got != tt.want { + t.Errorf("gitealizeUsername() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/services/auth/source/oauth2/blenderid/session.go b/services/auth/source/oauth2/blenderid/session.go new file mode 100644 index 0000000000000..52a2d2174584c --- /dev/null +++ b/services/auth/source/oauth2/blenderid/session.go @@ -0,0 +1,66 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package blenderid + +// Allow "encoding/json" import. +import ( + "encoding/json" //nolint:depguard + "errors" + "strings" + "time" + + "github.com/markbates/goth" +) + +// Session stores data during the auth process with Blender ID +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time +} + +var _ goth.Session = &Session{} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Blender ID provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Blender ID and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + s := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(s) + return s, err +} diff --git a/services/auth/source/oauth2/blenderid/session_test.go b/services/auth/source/oauth2/blenderid/session_test.go new file mode 100644 index 0000000000000..7f5b6198739fd --- /dev/null +++ b/services/auth/source/oauth2/blenderid/session_test.go @@ -0,0 +1,51 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package blenderid_test + +import ( + "testing" + + "code.gitea.io/gitea/services/auth/source/oauth2/blenderid" + + "github.com/markbates/goth" + "github.com/stretchr/testify/assert" +) + +func Test_Implements_Session(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &blenderid.Session{} + + a.Implements((*goth.Session)(nil), s) +} + +func Test_GetAuthURL(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &blenderid.Session{} + + _, err := s.GetAuthURL() + a.Error(err) + + s.AuthURL = "/foo" + + url, _ := s.GetAuthURL() + a.Equal("/foo", url) +} + +func Test_ToJSON(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &blenderid.Session{} + + data := s.Marshal() + a.JSONEq(`{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z"}`, data) +} + +func Test_String(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &blenderid.Session{} + + a.Equal(s.String(), s.Marshal()) +} diff --git a/services/auth/source/oauth2/providers_custom.go b/services/auth/source/oauth2/providers_custom.go index 65cf538ad7386..f6f49ada048aa 100644 --- a/services/auth/source/oauth2/providers_custom.go +++ b/services/auth/source/oauth2/providers_custom.go @@ -5,6 +5,7 @@ package oauth2 import ( "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/auth/source/oauth2/blenderid" "github.com/markbates/goth" "github.com/markbates/goth/providers/azureadv2" @@ -120,4 +121,14 @@ func init() { }), nil }, )) + + RegisterGothProvider(NewCustomProvider( + "blenderid", "Blender ID", &CustomURLSettings{ + TokenURL: requiredAttribute(blenderid.TokenURL), + AuthURL: requiredAttribute(blenderid.AuthURL), + ProfileURL: requiredAttribute(blenderid.ProfileURL), + }, + func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) { + return blenderid.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...), nil + })) } From 60c9d5b3537a5b1b122ec61bad719415a5e21782 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 10 May 2024 13:31:35 +0200 Subject: [PATCH 02/17] BLENDER: Show warning for target branch switching --- templates/repo/issue/view_title.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index b8f28dfd9bd3a..27be7ed3349e4 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -118,6 +118,7 @@ +
Remember to rebase the branch in your fork, see documentation.
{{end}} {{else}} From 17352785587c3d4c96f59374c6780ee0fc1f4f90 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 10 May 2024 13:32:53 +0200 Subject: [PATCH 03/17] BLENDER: Workaround LFS files not being available in pull requests Patch taken from issue number 17715, associating the LFS pointer when the file is downloaded. --- models/git/lfs.go | 2 +- services/lfs/server.go | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/models/git/lfs.go b/models/git/lfs.go index bb6361050aaef..83a2e7883d1cf 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -236,7 +236,7 @@ func CountLFSMetaObjects(ctx context.Context, repoID int64) (int64, error) { // LFSObjectAccessible checks if a provided Oid is accessible to the user func LFSObjectAccessible(ctx context.Context, user *user_model.User, oid string) (bool, error) { - if user.IsAdmin { + if user != nil && user.IsAdmin { count, err := db.GetEngine(ctx).Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}) return count > 0, err } diff --git a/services/lfs/server.go b/services/lfs/server.go index 0a99287ed9cd5..40456cbbbb3d6 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -254,6 +254,26 @@ func BatchHandler(ctx *context.Context) { responseObject = buildObjectResponse(rc, p, false, !exists, err) } else { var err *lfs_module.ObjectError + + if exists && meta == nil { + accessible, accessibleErr := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid) + if accessibleErr != nil { + log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err) + writeStatus(ctx, http.StatusInternalServerError) + return + } + if accessible { + _, newMetaObjErr := git_model.NewLFSMetaObject(ctx, repository.ID, p) + if newMetaObjErr != nil { + log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err) + writeStatus(ctx, http.StatusInternalServerError) + return + } + } else { + exists = false + } + } + if !exists || meta == nil { err = &lfs_module.ObjectError{ Code: http.StatusNotFound, From 48b49194b8d3cf9408c24443915eca9411e92797 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 10 May 2024 13:33:32 +0200 Subject: [PATCH 04/17] BLENDER: Don't allow assigning large teams as reviewers To avoid accidentally spamming hundreds of people. --- models/organization/team.go | 16 ++++++++++++++++ routers/web/repo/issue.go | 4 ++-- services/pull/reviewer.go | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/models/organization/team.go b/models/organization/team.go index 7f3a9b3829592..f0717faf2eb36 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -247,3 +247,19 @@ func IncrTeamRepoNum(ctx context.Context, teamID int64) error { _, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team)) return err } + +// Avoid notifying large teams accidentally +func FilterLargeTeams(teams []*Team, err error) ([]*Team, error) { + if err != nil { + return nil, err + } + + var smallTeams []*Team + for _, team := range teams { + if team.NumMembers <= 10 { + smallTeams = append(smallTeams, team) + } + } + + return smallTeams, nil +} diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a4747964c6f02..97086a363b120 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -673,13 +673,13 @@ func handleMentionableAssigneesAndTeams(ctx *context.Context, assignees []*user_ } if isAdmin { - teams, err = org.LoadTeams(ctx) + teams, err = organization.FilterLargeTeams(org.LoadTeams(ctx)) if err != nil { ctx.ServerError("LoadTeams", err) return } } else { - teams, err = org.GetUserTeams(ctx, ctx.Doer.ID) + teams, err = organization.FilterLargeTeams(org.GetUserTeams(ctx, ctx.Doer.ID)) if err != nil { ctx.ServerError("GetUserTeams", err) return diff --git a/services/pull/reviewer.go b/services/pull/reviewer.go index bf0d8cb298c8b..c3b0858aa30b1 100644 --- a/services/pull/reviewer.go +++ b/services/pull/reviewer.go @@ -85,5 +85,5 @@ func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*orga return nil, nil } - return organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) + return organization.FilterLargeTeams(organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)) } From 7a12c4dc34245396d1ce65e0361f171a37288f9c Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 10 May 2024 13:58:05 +0200 Subject: [PATCH 05/17] BLENDER: Support both exclusive and non-exclusive scope for labels --- models/issues/label.go | 15 ++++++++++----- modules/templates/util_render.go | 2 +- templates/repo/issue/filter_actions.tmpl | 10 +++++----- templates/repo/issue/filter_item_label.tmpl | 10 +++++----- templates/repo/issue/sidebar/label_list.tmpl | 14 ++++++++------ 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/models/issues/label.go b/models/issues/label.go index cfbe100926990..cb7726aa717d2 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -184,11 +184,8 @@ func (l *Label) BelongsToRepo() bool { return l.RepoID > 0 } -// ExclusiveScope returns scope substring of label name, or empty string if none exists -func (l *Label) ExclusiveScope() string { - if !l.Exclusive { - return "" - } +// Return scope substring of label name, or empty string if none exists +func (l *Label) Scope() string { lastIndex := strings.LastIndex(l.Name, "/") if lastIndex == -1 || lastIndex == 0 || lastIndex == len(l.Name)-1 { return "" @@ -196,6 +193,14 @@ func (l *Label) ExclusiveScope() string { return l.Name[:lastIndex] } +// ExclusiveScope returns scope substring of label name, or empty string if none exists +func (l *Label) ExclusiveScope() string { + if !l.Exclusive { + return "" + } + return l.Scope() +} + // NewLabel creates a new label func NewLabel(ctx context.Context, l *Label) error { color, err := label.NormalizeColor(l.Color) diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 8d9ba1000c882..53a16944af01b 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -127,7 +127,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { locale := ut.ctx.Value(translation.ContextKey).(translation.Locale) var extraCSSClasses string textColor := util.ContrastColor(label.Color) - labelScope := label.ExclusiveScope() + labelScope := label.Scope() descriptionText := emoji.ReplaceAliases(label.Description) if label.IsArchived() { diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index 8e2410393d871..0d196d1f8751f 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -22,15 +22,15 @@
{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}
- {{$previousExclusiveScope := "_no_scope"}} + {{$previousScope := "_no_scope"}} {{range .Labels}} - {{$exclusiveScope := .ExclusiveScope}} - {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}} + {{$scope := .Scope}} + {{if and (ne $previousScope "_no_scope") (ne $previousScope $scope)}}
{{end}} - {{$previousExclusiveScope = $exclusiveScope}} + {{$previousScope = $scope}}
- {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}{{end}} {{ctx.RenderUtils.RenderLabel .}} + {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{svg (Iif .ExclusiveScope "octicon-dot-fill" "octicon-check")}}{{end}} {{ctx.RenderUtils.RenderLabel .}} {{template "repo/issue/labels/label_archived" .}}
{{end}} diff --git a/templates/repo/issue/filter_item_label.tmpl b/templates/repo/issue/filter_item_label.tmpl index 0883d9380416b..11a3ddcc6f610 100644 --- a/templates/repo/issue/filter_item_label.tmpl +++ b/templates/repo/issue/filter_item_label.tmpl @@ -26,19 +26,19 @@ {{/* The logic here is not the same as the label selector in the issue sidebar. The one in the issue sidebar renders "repo labels | divider | org labels". Maybe the logic should be updated to be consistent.*/}} - {{$previousExclusiveScope := "_no_scope"}} + {{$previousScope := "_no_scope"}} {{range .Labels}} - {{$exclusiveScope := .ExclusiveScope}} - {{if and (ne $previousExclusiveScope $exclusiveScope)}} + {{$scope := .Scope}} + {{if and (ne $previousScope $scope)}}
{{end}} - {{$previousExclusiveScope = $exclusiveScope}} + {{$previousScope = $scope}} {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} - {{Iif $exclusiveScope (svg "octicon-dot-fill") (svg "octicon-check")}} + {{Iif .ExclusiveScope (svg "octicon-dot-fill") (svg "octicon-check")}} {{end}} {{ctx.RenderUtils.RenderLabel .}}

{{template "repo/issue/labels/label_archived" .}}

diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl index ed514f6725c8c..2b3255371a38c 100644 --- a/templates/repo/issue/sidebar/label_list.tmpl +++ b/templates/repo/issue/sidebar/label_list.tmpl @@ -19,23 +19,25 @@
From 17e996da18ccbf73543e889dcb183cc0040087b3 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 10 May 2024 14:00:36 +0200 Subject: [PATCH 06/17] BLENDER: Projects: button to show/hide issue details and closed issue Both are off by default. This is implemented fully on the frontend, so all issues and their details are still always loaded. --- templates/projects/view.tmpl | 43 ++++++++++++++++++++++++++++++- templates/repo/issue/card.tmpl | 25 ++++++++++-------- web_src/css/features/projects.css | 8 ++++++ web_src/css/repo/issue-card.css | 2 ++ 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 7e89db0005d54..413f3065857b9 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -17,6 +17,16 @@ "TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee") }} + {{if $canWriteProject}}