0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-19 10:41:01 +02:00

Merge branch 'main' into patch7

This commit is contained in:
Lunny Xiao 2025-07-11 07:18:20 +08:00 committed by GitHub
commit 9df2d9ab30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 701 additions and 378 deletions

View File

@ -39,7 +39,6 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/tmp/local/etc/s6/.s6-svscan/* \
/go/src/code.gitea.io/gitea/gitea \
/go/src/code.gitea.io/gitea/environment-to-ini
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
FROM docker.io/library/alpine:3.22
LABEL maintainer="maintainers@gitea.io"
@ -83,4 +82,3 @@ CMD ["/usr/bin/s6-svscan", "/etc/s6"]
COPY --from=build-env /tmp/local /
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh

View File

@ -37,7 +37,6 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
/tmp/local/usr/local/bin/gitea \
/go/src/code.gitea.io/gitea/gitea \
/go/src/code.gitea.io/gitea/environment-to-ini
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
FROM docker.io/library/alpine:3.22
LABEL maintainer="maintainers@gitea.io"
@ -72,7 +71,6 @@ RUN chown git:git /var/lib/gitea /etc/gitea
COPY --from=build-env /tmp/local /
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh
# git:git
USER 1000:1000

View File

@ -87,6 +87,14 @@ func oauthCLIFlags() []cli.Flag {
Value: nil,
Usage: "Scopes to request when to authenticate against this OAuth2 source",
},
&cli.StringFlag{
Name: "ssh-public-key-claim-name",
Usage: "Claim name that provides SSH public keys",
},
&cli.StringFlag{
Name: "full-name-claim-name",
Usage: "Claim name that provides user's full name",
},
&cli.StringFlag{
Name: "required-claim-name",
Value: "",
@ -177,6 +185,8 @@ func parseOAuth2Config(c *cli.Command) *oauth2.Source {
RestrictedGroup: c.String("restricted-group"),
GroupTeamMap: c.String("group-team-map"),
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
SSHPublicKeyClaimName: c.String("ssh-public-key-claim-name"),
FullNameClaimName: c.String("full-name-claim-name"),
}
}
@ -268,6 +278,12 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error
if c.IsSet("group-team-map-removal") {
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
}
if c.IsSet("ssh-public-key-claim-name") {
oAuth2Config.SSHPublicKeyClaimName = c.String("ssh-public-key-claim-name")
}
if c.IsSet("full-name-claim-name") {
oAuth2Config.FullNameClaimName = c.String("full-name-claim-name")
}
// update custom URL mapping
customURLMapping := &oauth2.CustomURLMapping{}

View File

@ -88,6 +88,8 @@ func TestAddOauth(t *testing.T) {
"--restricted-group", "restricted",
"--group-team-map", `{"group1": [1,2]}`,
"--group-team-map-removal=true",
"--ssh-public-key-claim-name", "attr_ssh_pub_key",
"--full-name-claim-name", "attr_full_name",
},
source: &auth_model.Source{
Type: auth_model.OAuth2,
@ -104,15 +106,17 @@ func TestAddOauth(t *testing.T) {
EmailURL: "https://example.com/email",
Tenant: "some_tenant",
},
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: true,
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: true,
SSHPublicKeyClaimName: "attr_ssh_pub_key",
FullNameClaimName: "attr_full_name",
},
TwoFactorPolicy: "skip",
},
@ -223,15 +227,17 @@ func TestUpdateOauth(t *testing.T) {
EmailURL: "https://old.example.com/email",
Tenant: "old_tenant",
},
IconURL: "https://old.example.com/icon",
Scopes: []string{"old_scope1", "old_scope2"},
RequiredClaimName: "old_claim_name",
RequiredClaimValue: "old_claim_value",
GroupClaimName: "old_group_name",
AdminGroup: "old_admin",
RestrictedGroup: "old_restricted",
GroupTeamMap: `{"old_group1": [1,2]}`,
GroupTeamMapRemoval: true,
IconURL: "https://old.example.com/icon",
Scopes: []string{"old_scope1", "old_scope2"},
RequiredClaimName: "old_claim_name",
RequiredClaimValue: "old_claim_value",
GroupClaimName: "old_group_name",
AdminGroup: "old_admin",
RestrictedGroup: "old_restricted",
GroupTeamMap: `{"old_group1": [1,2]}`,
GroupTeamMapRemoval: true,
SSHPublicKeyClaimName: "old_ssh_pub_key",
FullNameClaimName: "old_full_name",
},
TwoFactorPolicy: "",
},
@ -257,6 +263,8 @@ func TestUpdateOauth(t *testing.T) {
"--restricted-group", "restricted",
"--group-team-map", `{"group1": [1,2]}`,
"--group-team-map-removal=false",
"--ssh-public-key-claim-name", "new_ssh_pub_key",
"--full-name-claim-name", "new_full_name",
},
authSource: &auth_model.Source{
ID: 1,
@ -274,15 +282,17 @@ func TestUpdateOauth(t *testing.T) {
EmailURL: "https://example.com/email",
Tenant: "new_tenant",
},
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: false,
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: false,
SSHPublicKeyClaimName: "new_ssh_pub_key",
FullNameClaimName: "new_full_name",
},
TwoFactorPolicy: "skip",
},

View File

@ -15,25 +15,6 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
// __________________ ________ ____ __.
// / _____/\______ \/ _____/ | |/ _|____ ___.__.
// / \ ___ | ___/ \ ___ | <_/ __ < | |
// \ \_\ \| | \ \_\ \ | | \ ___/\___ |
// \______ /|____| \______ / |____|__ \___ > ____|
// \/ \/ \/ \/\/
// _________ .__ __
// \_ ___ \ ____ _____ _____ |__|/ |_
// / \ \/ / _ \ / \ / \| \ __\
// \ \___( <_> ) Y Y \ Y Y \ || |
// \______ /\____/|__|_| /__|_| /__||__|
// \/ \/ \/
// ____ ____ .__ _____.__ __ .__
// \ \ / /___________|__|/ ____\__| ____ _____ _/ |_|__| ____ ____
// \ Y // __ \_ __ \ \ __\| |/ ___\\__ \\ __\ |/ _ \ / \
// \ /\ ___/| | \/ || | | \ \___ / __ \| | | ( <_> ) | \
// \___/ \___ >__| |__||__| |__|\___ >____ /__| |__|\____/|___| /
// \/ \/ \/ \/
// This file provides functions relating commit verification
// CommitVerification represents a commit validation of signature
@ -41,8 +22,8 @@ type CommitVerification struct {
Verified bool
Warning bool
Reason string
SigningUser *user_model.User
CommittingUser *user_model.User
SigningUser *user_model.User // if Verified, then SigningUser is non-nil
CommittingUser *user_model.User // if Verified, then CommittingUser is non-nil
SigningEmail string
SigningKey *GPGKey
SigningSSHKey *PublicKey

View File

@ -355,13 +355,13 @@ func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.So
return sshKeysNeedUpdate
}
// SynchronizePublicKeys updates a users public keys. Returns true if there are changes.
// SynchronizePublicKeys updates a user's public keys. Returns true if there are changes.
func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
var sshKeysNeedUpdate bool
log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
// Get Public Keys from DB with current LDAP source
// Get Public Keys from DB with the current auth source
var giteaKeys []string
keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
OwnerID: usr.ID,

View File

@ -612,8 +612,8 @@ func (err ErrOAuthApplicationNotFound) Unwrap() error {
return util.ErrNotExist
}
// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name
func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) {
// GetActiveOAuth2SourceByAuthName returns a OAuth2 AuthSource based on the given name
func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source, error) {
authSource := new(Source)
has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
if err != nil {

View File

@ -334,7 +334,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
err = registerableSource.RegisterSource()
if err != nil {
// restore original values since we cannot update the provider it self
// restore original values since we cannot update the provider itself
if _, err := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil {
log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
}

View File

@ -472,7 +472,7 @@ type RecentlyPushedNewBranch struct {
// if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours
// if opts.ListOptions is not set, we will only display top 2 latest branches.
// Protected branches will be skipped since they are unlikely to be used to create new PRs.
func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts *FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) {
func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) {
if doer == nil {
return []*RecentlyPushedNewBranch{}, nil
}

View File

@ -652,7 +652,13 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool {
}
// CanEnableEditor returns true if repository meets the requirements of web editor.
// FIXME: most CanEnableEditor calls should be replaced with CanContentChange
// And all other like CanCreateBranch / CanEnablePulls should also be updated
func (repo *Repository) CanEnableEditor() bool {
return repo.CanContentChange()
}
func (repo *Repository) CanContentChange() bool {
return !repo.IsMirror && !repo.IsArchived
}

View File

@ -1166,12 +1166,6 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
for _, c := range oldCommits {
user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
if user == nil {
user = &User{
Name: c.Author.Name,
Email: c.Author.Email,
}
}
newCommits = append(newCommits, &UserCommit{
User: user,
Commit: c,
@ -1195,12 +1189,14 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
needCheckEmails := make(container.Set[string])
needCheckUserNames := make(container.Set[string])
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
for _, email := range emails {
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
needCheckUserNames.Add(strings.ToLower(username))
emailLower := strings.ToLower(email)
if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
needCheckUserNames.Add(noReplyUserNameLower)
needCheckEmails.Add(emailLower)
} else {
needCheckEmails.Add(strings.ToLower(email))
needCheckEmails.Add(emailLower)
}
}

View File

@ -85,6 +85,11 @@ func TestUserEmails(t *testing.T) {
testGetUserByEmail(t, c.Email, c.UID)
})
}
t.Run("NoReplyConflict", func(t *testing.T) {
setting.Service.NoReplyAddress = "example.com"
testGetUserByEmail(t, "user1-2@example.COM", 1)
})
})
}

View File

@ -22,9 +22,9 @@ import (
type Commit struct {
Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache"
ID ObjectID // The ID of this commit object
Author *Signature
Committer *Signature
ID ObjectID
Author *Signature // never nil
Committer *Signature // never nil
CommitMessage string
Signature *CommitSignature

View File

@ -28,6 +28,13 @@ func FromPtr[T any](v *T) Option[T] {
return Some(*v)
}
func FromMapLookup[K comparable, V any](m map[K]V, k K) Option[V] {
if v, ok := m[k]; ok {
return Some(v)
}
return None[V]()
}
func FromNonDefault[T comparable](v T) Option[T] {
var zero T
if v == zero {

View File

@ -56,6 +56,12 @@ func TestOption(t *testing.T) {
opt3 := optional.FromNonDefault(1)
assert.True(t, opt3.Has())
assert.Equal(t, int(1), opt3.Value())
opt4 := optional.FromMapLookup(map[string]int{"a": 1}, "a")
assert.True(t, opt4.Has())
assert.Equal(t, 1, opt4.Value())
opt4 = optional.FromMapLookup(map[string]int{"a": 1}, "b")
assert.False(t, opt4.Has())
}
func Test_ParseBool(t *testing.T) {

View File

@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log"
)
// OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data
// OAuth2UsernameType is enum describing the way gitea generates its 'username' from oauth2 data
type OAuth2UsernameType string
const (

View File

@ -3251,6 +3251,8 @@ auths.oauth2_required_claim_name_helper = Set this name to restrict login from t
auths.oauth2_required_claim_value = Required Claim Value
auths.oauth2_required_claim_value_helper = Set this value to restrict login from this source to users with a claim with this name and value
auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional)
auths.oauth2_full_name_claim_name = Full Name Claim Name. (Optional, if set, the user's full name will always be synchronized with this claim)
auths.oauth2_ssh_public_key_claim_name = SSH Public Key Claim Name
auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above)
auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above)
auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above)

View File

@ -240,7 +240,7 @@ func EditUser(ctx *context.APIContext) {
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),

View File

@ -391,7 +391,7 @@ func Edit(ctx *context.APIContext) {
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {

View File

@ -199,6 +199,9 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
AdminGroup: form.Oauth2AdminGroup,
GroupTeamMap: form.Oauth2GroupTeamMap,
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
SSHPublicKeyClaimName: form.Oauth2SSHPublicKeyClaimName,
FullNameClaimName: form.Oauth2FullNameClaimName,
}
}

View File

@ -14,7 +14,6 @@ import (
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
)
@ -75,7 +74,7 @@ func TwoFactorPost(ctx *context.Context) {
}
if ctx.Session.Get("linkAccount") != nil {
err = externalaccount.LinkAccountFromStore(ctx, ctx.Session, u)
err = linkAccountFromContext(ctx, u)
if err != nil {
ctx.ServerError("UserSignIn", err)
return

View File

@ -329,6 +329,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
"twofaUid",
"twofaRemember",
"linkAccount",
"linkAccountData",
}, map[string]any{
session.KeyUID: u.ID,
session.KeyUname: u.Name,
@ -519,7 +520,7 @@ func SignUpPost(ctx *context.Context) {
Passwd: form.Password,
}
if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false) {
if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil) {
// error already handled
return
}
@ -530,22 +531,22 @@ func SignUpPost(ctx *context.Context) {
// createAndHandleCreatedUser calls createUserInContext and
// then handleUserCreated.
func createAndHandleCreatedUser(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool {
if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink) {
func createAndHandleCreatedUser(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, possibleLinkAccountData *LinkAccountData) bool {
if !createUserInContext(ctx, tpl, form, u, overwrites, possibleLinkAccountData) {
return false
}
return handleUserCreated(ctx, u, gothUser)
return handleUserCreated(ctx, u, possibleLinkAccountData)
}
// createUserInContext creates a user and handles errors within a given context.
// Optionally a template can be specified.
func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) {
// Optionally, a template can be specified.
func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, possibleLinkAccountData *LinkAccountData) (ok bool) {
meta := &user_model.Meta{
InitialIP: ctx.RemoteAddr(),
InitialUserAgent: ctx.Req.UserAgent(),
}
if err := user_model.CreateUser(ctx, u, meta, overwrites); err != nil {
if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
if possibleLinkAccountData != nil && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
switch setting.OAuth2Client.AccountLinking {
case setting.OAuth2AccountLinkingAuto:
var user *user_model.User
@ -561,15 +562,15 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
}
// TODO: probably we should respect 'remember' user's choice...
linkAccount(ctx, user, *gothUser, true)
oauth2LinkAccount(ctx, user, possibleLinkAccountData, true)
return false // user is already created here, all redirects are handled
case setting.OAuth2AccountLinkingLogin:
showLinkingLogin(ctx, *gothUser)
showLinkingLogin(ctx, &possibleLinkAccountData.AuthSource, possibleLinkAccountData.GothUser)
return false // user will be created only after linking login
}
}
// handle error without template
// handle error without a template
if len(tpl) == 0 {
ctx.ServerError("CreateUser", err)
return false
@ -610,7 +611,7 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
// handleUserCreated does additional steps after a new user is created.
// It auto-sets admin for the only user, updates the optional external user and
// sends a confirmation email if required.
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
func handleUserCreated(ctx *context.Context, u *user_model.User, possibleLinkAccountData *LinkAccountData) (ok bool) {
// Auto-set admin for the only user.
hasUsers, err := user_model.HasUsers(ctx)
if err != nil {
@ -631,8 +632,8 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
}
// update external user information
if gothUser != nil {
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil {
if possibleLinkAccountData != nil {
if err := externalaccount.EnsureLinkExternalToUser(ctx, possibleLinkAccountData.AuthSource.ID, u, possibleLinkAccountData.GothUser); err != nil {
log.Error("EnsureLinkExternalToUser failed: %v", err)
}
}

View File

@ -5,7 +5,6 @@ package auth
import (
"errors"
"fmt"
"net/http"
"strings"
@ -21,8 +20,6 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
"github.com/markbates/goth"
)
var tplLinkAccount templates.TplName = "user/auth/link_account"
@ -52,28 +49,28 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
linkAccountData := oauth2GetLinkAccountData(ctx)
// If you'd like to quickly debug the "link account" page layout, just uncomment the blow line
// Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign)
// gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check
// linkAccountData = &LinkAccountData{authSource, gothUser} // intentionally use invalid data to avoid pass the registration check
if !ok {
if linkAccountData == nil {
// no account in session, so just redirect to the login page, then the user could restart the process
ctx.Redirect(setting.AppSubURL + "/user/login")
return
}
if missingFields, ok := gothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok {
ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, strings.Join(missingFields, ","))
if missingFields, ok := linkAccountData.GothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok {
ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", linkAccountData.GothUser.Provider, strings.Join(missingFields, ","))
}
uname, err := extractUserNameFromOAuth2(&gothUser)
uname, err := extractUserNameFromOAuth2(&linkAccountData.GothUser)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
email := gothUser.Email
email := linkAccountData.GothUser.Email
ctx.Data["user_name"] = uname
ctx.Data["email"] = email
@ -152,8 +149,8 @@ func LinkAccountPostSignIn(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser := ctx.Session.Get("linkAccountGothUser")
if gothUser == nil {
linkAccountData := oauth2GetLinkAccountData(ctx)
if linkAccountData == nil {
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
return
}
@ -169,11 +166,14 @@ func LinkAccountPostSignIn(ctx *context.Context) {
return
}
linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
oauth2LinkAccount(ctx, u, linkAccountData, signInForm.Remember)
}
func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool) {
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData *LinkAccountData, remember bool) {
oauth2SignInSync(ctx, &linkAccountData.AuthSource, u, linkAccountData.GothUser)
if ctx.Written() {
return
}
// If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page.
@ -185,7 +185,7 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r
return
}
err = externalaccount.LinkAccountToUser(ctx, u, gothUser)
err = externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, u, linkAccountData.GothUser)
if err != nil {
ctx.ServerError("UserLinkAccount", err)
return
@ -243,17 +243,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUserInterface := ctx.Session.Get("linkAccountGothUser")
if gothUserInterface == nil {
linkAccountData := oauth2GetLinkAccountData(ctx)
if linkAccountData == nil {
ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
return
}
gothUser, ok := gothUserInterface.(goth.User)
if !ok {
ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
return
}
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplLinkAccount)
return
@ -296,31 +290,33 @@ func LinkAccountPostRegister(ctx *context.Context) {
}
}
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
if err != nil {
ctx.ServerError("CreateUser", err)
return
}
u := &user_model.User{
Name: form.UserName,
Email: form.Email,
Passwd: form.Password,
LoginType: auth.OAuth2,
LoginSource: authSource.ID,
LoginName: gothUser.UserID,
LoginSource: linkAccountData.AuthSource.ID,
LoginName: linkAccountData.GothUser.UserID,
}
if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) {
if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, linkAccountData) {
// error already handled
return
}
source := authSource.Cfg.(*oauth2.Source)
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
source := linkAccountData.AuthSource.Cfg.(*oauth2.Source)
if err := syncGroupsToTeams(ctx, source, &linkAccountData.GothUser, u); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
handleSignIn(ctx, u, false)
}
func linkAccountFromContext(ctx *context.Context, user *user_model.User) error {
linkAccountData := oauth2GetLinkAccountData(ctx)
if linkAccountData == nil {
return errors.New("not in LinkAccount session")
}
return externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, user, linkAccountData.GothUser)
}

View File

@ -20,7 +20,6 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/auth/source/oauth2"
@ -35,9 +34,8 @@ import (
// SignInOAuth handles the OAuth2 login buttons
func SignInOAuth(ctx *context.Context) {
provider := ctx.PathParam("provider")
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
authName := ctx.PathParam("provider")
authSource, err := auth.GetActiveOAuth2SourceByAuthName(ctx, authName)
if err != nil {
ctx.ServerError("SignIn", err)
return
@ -74,8 +72,6 @@ func SignInOAuth(ctx *context.Context) {
// SignInOAuthCallback handles the callback from the given provider
func SignInOAuthCallback(ctx *context.Context) {
provider := ctx.PathParam("provider")
if ctx.Req.FormValue("error") != "" {
var errorKeyValues []string
for k, vv := range ctx.Req.Form {
@ -88,7 +84,8 @@ func SignInOAuthCallback(ctx *context.Context) {
}
// first look if the provider is still active
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
authName := ctx.PathParam("provider")
authSource, err := auth.GetActiveOAuth2SourceByAuthName(ctx, authName)
if err != nil {
ctx.ServerError("SignIn", err)
return
@ -133,7 +130,7 @@ func SignInOAuthCallback(ctx *context.Context) {
if u == nil {
if ctx.Doer != nil {
// attach user to the current signed-in user
err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
err = externalaccount.LinkAccountToUser(ctx, authSource.ID, ctx.Doer, gothUser)
if err != nil {
ctx.ServerError("UserLinkAccount", err)
return
@ -174,12 +171,11 @@ func SignInOAuthCallback(ctx *context.Context) {
gothUser.RawData = make(map[string]any)
}
gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields
showLinkingLogin(ctx, gothUser)
showLinkingLogin(ctx, authSource, gothUser)
return
}
u = &user_model.User{
Name: uname,
FullName: gothUser.Name,
Email: gothUser.Email,
LoginType: auth.OAuth2,
LoginSource: authSource.ID,
@ -196,7 +192,11 @@ func SignInOAuthCallback(ctx *context.Context) {
u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted)
if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
linkAccountData := &LinkAccountData{*authSource, gothUser}
if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingDisabled {
linkAccountData = nil
}
if !createAndHandleCreatedUser(ctx, "", nil, u, overwriteDefault, linkAccountData) {
// error already handled
return
}
@ -207,7 +207,7 @@ func SignInOAuthCallback(ctx *context.Context) {
}
} else {
// no existing user is found, request attach or new account
showLinkingLogin(ctx, gothUser)
showLinkingLogin(ctx, authSource, gothUser)
return
}
}
@ -271,9 +271,22 @@ func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *g
return isAdmin, isRestricted
}
func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
type LinkAccountData struct {
AuthSource auth.Source
GothUser goth.User
}
func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
v, ok := ctx.Session.Get("linkAccountData").(LinkAccountData)
if !ok {
return nil
}
return &v
}
func showLinkingLogin(ctx *context.Context, authSource *auth.Source, gothUser goth.User) {
if err := updateSession(ctx, nil, map[string]any{
"linkAccountGothUser": gothUser,
"linkAccountData": LinkAccountData{*authSource, gothUser},
}); err != nil {
ctx.ServerError("updateSession", err)
return
@ -281,7 +294,7 @@ func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
ctx.Redirect(setting.AppSubURL + "/user/link_account")
}
func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
resp, err := http.Get(url)
if err == nil {
@ -299,11 +312,14 @@ func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
}
}
func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) {
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) {
oauth2SignInSync(ctx, authSource, u, gothUser)
if ctx.Written() {
return
}
needs2FA := false
if !source.TwoFactorShouldSkip() {
if !authSource.TwoFactorShouldSkip() {
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("UserSignIn", err)
@ -312,7 +328,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
needs2FA = err == nil
}
oauth2Source := source.Cfg.(*oauth2.Source)
oauth2Source := authSource.Cfg.(*oauth2.Source)
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap)
if err != nil {
ctx.ServerError("UnmarshalGroupTeamMapping", err)
@ -338,7 +354,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
}
}
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil {
if err := externalaccount.EnsureLinkExternalToUser(ctx, authSource.ID, u, gothUser); err != nil {
ctx.ServerError("EnsureLinkExternalToUser", err)
return
}

View File

@ -0,0 +1,88 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"fmt"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
"github.com/markbates/goth"
)
func oauth2SignInSync(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) {
oauth2UpdateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
oauth2Source, _ := authSource.Cfg.(*oauth2.Source)
if !authSource.IsOAuth2() || oauth2Source == nil {
ctx.ServerError("oauth2SignInSync", fmt.Errorf("source %s is not an OAuth2 source", gothUser.Provider))
return
}
// sync full name
fullNameKey := util.IfZero(oauth2Source.FullNameClaimName, "name")
fullName, _ := gothUser.RawData[fullNameKey].(string)
fullName = util.IfZero(fullName, gothUser.Name)
// need to update if the user has no full name set
shouldUpdateFullName := u.FullName == ""
// force to update if the attribute is set
shouldUpdateFullName = shouldUpdateFullName || oauth2Source.FullNameClaimName != ""
// only update if the full name is different
shouldUpdateFullName = shouldUpdateFullName && u.FullName != fullName
if shouldUpdateFullName {
u.FullName = fullName
if err := user_model.UpdateUserCols(ctx, u, "full_name"); err != nil {
log.Error("Unable to sync OAuth2 user full name %s: %v", gothUser.Provider, err)
}
}
err := oauth2UpdateSSHPubIfNeed(ctx, authSource, &gothUser, u)
if err != nil {
log.Error("Unable to sync OAuth2 SSH public key %s: %v", gothUser.Provider, err)
}
}
func oauth2SyncGetSSHKeys(source *oauth2.Source, gothUser *goth.User) ([]string, error) {
value, exists := gothUser.RawData[source.SSHPublicKeyClaimName]
if !exists {
return []string{}, nil
}
rawSlice, ok := value.([]any)
if !ok {
return nil, fmt.Errorf("invalid SSH public key value type: %T", value)
}
sshKeys := make([]string, 0, len(rawSlice))
for _, v := range rawSlice {
str, ok := v.(string)
if !ok {
return nil, fmt.Errorf("invalid SSH public key value item type: %T", v)
}
sshKeys = append(sshKeys, str)
}
return sshKeys, nil
}
func oauth2UpdateSSHPubIfNeed(ctx *context.Context, authSource *auth.Source, gothUser *goth.User, user *user_model.User) error {
oauth2Source, _ := authSource.Cfg.(*oauth2.Source)
if oauth2Source == nil || oauth2Source.SSHPublicKeyClaimName == "" {
return nil
}
sshKeys, err := oauth2SyncGetSSHKeys(oauth2Source, gothUser)
if err != nil {
return err
}
if !asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys) {
return nil
}
return asymkey_service.RewriteAllPublicKeys(ctx)
}

View File

@ -361,7 +361,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
Email: form.Email,
Passwd: password,
}
if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false) {
if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil) {
// error already handled
return
}

View File

@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
@ -150,7 +149,7 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
// Now handle account linking if that's requested
if ctx.Session.Get("linkAccount") != nil {
if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil {
if err := linkAccountFromContext(ctx, user); err != nil {
ctx.ServerError("LinkAccountFromStore", err)
return
}
@ -268,7 +267,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
// Now handle account linking if that's requested
if ctx.Session.Get("linkAccount") != nil {
if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil {
if err := linkAccountFromContext(ctx, user); err != nil {
ctx.ServerError("LinkAccountFromStore", err)
return
}

View File

@ -0,0 +1,73 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
git_model "code.gitea.io/gitea/models/git"
access_model "code.gitea.io/gitea/models/perm/access"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
type RecentBranchesPromptDataStruct struct {
RecentlyPushedNewBranches []*git_model.RecentlyPushedNewBranch
}
func prepareRecentlyPushedNewBranches(ctx *context.Context) {
if ctx.Doer == nil {
return
}
if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil {
log.Error("GetBaseRepo: %v", err)
return
}
opts := git_model.FindRecentlyPushedNewBranchesOptions{
Repo: ctx.Repo.Repository,
BaseRepo: ctx.Repo.Repository,
}
if ctx.Repo.Repository.IsFork {
opts.BaseRepo = ctx.Repo.Repository.BaseRepo
}
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
if err != nil {
log.Error("GetUserRepoPermission: %v", err)
return
}
if !opts.Repo.CanContentChange() || !opts.BaseRepo.CanContentChange() {
return
}
if !opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || !baseRepoPerm.CanRead(unit_model.TypePullRequests) {
return
}
var finalBranches []*git_model.RecentlyPushedNewBranch
branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
if err != nil {
log.Error("FindRecentlyPushedNewBranches failed: %v", err)
return
}
for _, branch := range branches {
divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx,
branch.BranchRepo, branch.BranchName, // "base" repo for diverging info
opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info
)
if err != nil {
log.Error("GetBranchDivergingInfo failed: %v", err)
continue
}
branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits
baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind
if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 {
finalBranches = append(finalBranches, branch)
}
}
if len(finalBranches) > 0 {
ctx.Data["RecentBranchesPromptData"] = RecentBranchesPromptDataStruct{finalBranches}
}
}

View File

@ -767,6 +767,10 @@ func Issues(ctx *context.Context) {
}
ctx.Data["Title"] = ctx.Tr("repo.pulls")
ctx.Data["PageIsPullList"] = true
prepareRecentlyPushedNewBranches(ctx)
if ctx.Written() {
return
}
} else {
MustEnableIssues(ctx)
if ctx.Written() {

View File

@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@ -196,56 +195,6 @@ func prepareUpstreamDivergingInfo(ctx *context.Context) {
ctx.Data["UpstreamDivergingInfo"] = upstreamDivergingInfo
}
func prepareRecentlyPushedNewBranches(ctx *context.Context) {
if ctx.Doer != nil {
if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
opts := &git_model.FindRecentlyPushedNewBranchesOptions{
Repo: ctx.Repo.Repository,
BaseRepo: ctx.Repo.Repository,
}
if ctx.Repo.Repository.IsFork {
opts.BaseRepo = ctx.Repo.Repository.BaseRepo
}
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
baseRepoPerm.CanRead(unit_model.TypePullRequests) {
var finalBranches []*git_model.RecentlyPushedNewBranch
branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
if err != nil {
log.Error("FindRecentlyPushedNewBranches failed: %v", err)
}
for _, branch := range branches {
divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx,
branch.BranchRepo, branch.BranchName, // "base" repo for diverging info
opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info
)
if err != nil {
log.Error("GetBranchDivergingInfo failed: %v", err)
continue
}
branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits
baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind
if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 {
finalBranches = append(finalBranches, branch)
}
}
ctx.Data["RecentlyPushedNewBranches"] = finalBranches
}
}
}
func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) {
if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status {
return

View File

@ -5,6 +5,7 @@ package agit
import (
"context"
"encoding/base64"
"fmt"
"os"
"strings"
@ -18,17 +19,30 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
)
func parseAgitPushOptionValue(s string) string {
if base64Value, ok := strings.CutPrefix(s, "{base64}"); ok {
decoded, err := base64.StdEncoding.DecodeString(base64Value)
return util.Iif(err == nil, string(decoded), s)
}
return s
}
// ProcReceive handle proc receive work
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
topicBranch := opts.GitPushOptions["topic"]
title := strings.TrimSpace(opts.GitPushOptions["title"])
description := strings.TrimSpace(opts.GitPushOptions["description"])
// some options are base64-encoded with "{base64}" prefix if they contain new lines
// other agit push options like "issue", "reviewer" and "cc" are not supported
title := parseAgitPushOptionValue(opts.GitPushOptions["title"])
description := parseAgitPushOptionValue(opts.GitPushOptions["description"])
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
userName := strings.ToLower(opts.UserName)

View File

@ -0,0 +1,16 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package agit
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseAgitPushOptionValue(t *testing.T) {
assert.Equal(t, "a", parseAgitPushOptionValue("a"))
assert.Equal(t, "a", parseAgitPushOptionValue("{base64}YQ=="))
assert.Equal(t, "{base64}invalid value", parseAgitPushOptionValue("{base64}invalid value"))
}

View File

@ -24,47 +24,43 @@ import (
// ParseCommitWithSignature check if signature is good against keystore.
func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *asymkey_model.CommitVerification {
var committer *user_model.User
if c.Committer != nil {
var err error
// Find Committer account
committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not
if err != nil { // Skipping not user for committer
committer = &user_model.User{
Name: c.Committer.Name,
Email: c.Committer.Email,
}
// We can expect this to often be an ErrUserNotExist. in the case
// it is not, however, it is important to log it.
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return &asymkey_model.CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.no_committer_account",
}
}
committer, err := user_model.GetUserByEmail(ctx, c.Committer.Email)
if err != nil && !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return &asymkey_model.CommitVerification{
Verified: false,
Reason: "gpg.error.no_committer_account", // this error is not right, but such error should seldom happen
}
}
return ParseCommitWithSignatureCommitter(ctx, c, committer)
}
// ParseCommitWithSignatureCommitter parses a commit's GPG or SSH signature.
// If the commit is singed by an instance key, then committer can be nil.
// If the signature exists, even if committer is nil, the returned CommittingUser will be a non-nil fake user.
func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification {
// If no signature just report the committer
// If no signature, just report the committer
if c.Signature == nil {
return &asymkey_model.CommitVerification{
CommittingUser: committer,
Verified: false, // Default value
Reason: "gpg.error.not_signed_commit", // Default value
Verified: false,
Reason: "gpg.error.not_signed_commit",
}
}
// If this a SSH signature handle it differently
if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") {
return ParseCommitWithSSHSignature(ctx, c, committer)
// to support instance key, we need a fake committer user (not really needed, but legacy code accesses the committer without nil-check)
if committer == nil {
committer = &user_model.User{
Name: c.Committer.Name,
Email: c.Committer.Email,
}
}
if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") {
return parseCommitWithSSHSignature(ctx, c, committer)
}
return parseCommitWithGPGSignature(ctx, c, committer)
}
func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification {
// Parsing signature
sig, err := asymkey_model.ExtractSignature(c.Signature.Signature)
if err != nil { // Skipping failed to extract sign
@ -165,7 +161,7 @@ func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, commi
}
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
} else if commitVerification := VerifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
} else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
if commitVerification.Reason == asymkey_model.BadSignature {
defaultReason = asymkey_model.BadSignature
} else {
@ -180,7 +176,7 @@ func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, commi
} else if defaultGPGSettings == nil {
log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String())
} else if defaultGPGSettings.Sign {
if commitVerification := VerifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
if commitVerification.Reason == asymkey_model.BadSignature {
defaultReason = asymkey_model.BadSignature
} else {
@ -295,7 +291,7 @@ func HashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload s
}
}
func VerifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *asymkey_model.CommitVerification {
func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *asymkey_model.CommitVerification {
// First try to find the key in the db
if commitVerification := HashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
return commitVerification
@ -375,8 +371,8 @@ func verifySSHCommitVerificationByInstanceKey(c *git.Commit, committerUser, sign
return verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, sshPubKey, committerUser, signerUser, committerGitEmail)
}
// ParseCommitWithSSHSignature check if signature is good against keystore.
func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification {
// parseCommitWithSSHSignature check if signature is good against keystore.
func parseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification {
// Now try to associate the signature with the committer, if present
if committerUser.ID != 0 {
keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{

View File

@ -41,7 +41,7 @@ Initial commit with signed file
Name: "User Two",
Email: "user2@example.com",
}
ret := ParseCommitWithSSHSignature(t.Context(), commit, committingUser)
ret := parseCommitWithSSHSignature(t.Context(), commit, committingUser)
require.NotNil(t, ret)
assert.True(t, ret.Verified)
assert.False(t, ret.Warning)

View File

@ -27,6 +27,7 @@ type Provider interface {
DisplayName() string
IconHTML(size int) template.HTML
CustomURLSettings() *CustomURLSettings
SupportSSHPublicKey() bool
}
// GothProviderCreator provides a function to create a goth.Provider

View File

@ -14,6 +14,13 @@ import (
type BaseProvider struct {
name string
displayName string
// TODO: maybe some providers also support SSH public keys, then they can set this to true
supportSSHPublicKey bool
}
func (b *BaseProvider) SupportSSHPublicKey() bool {
return b.supportSSHPublicKey
}
// Name provides the technical name for this provider

View File

@ -17,6 +17,10 @@ import (
// OpenIDProvider is a GothProvider for OpenID
type OpenIDProvider struct{}
func (o *OpenIDProvider) SupportSSHPublicKey() bool {
return true
}
// Name provides the technical name for this provider
func (o *OpenIDProvider) Name() string {
return "openidConnect"

View File

@ -27,6 +27,9 @@ type Source struct {
GroupTeamMap string
GroupTeamMapRemoval bool
RestrictedGroup string
SSHPublicKeyClaimName string
FullNameClaimName string
}
// FromDB fills up an OAuth2Config from serialized format.

View File

@ -1,30 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package externalaccount
import (
"context"
"errors"
user_model "code.gitea.io/gitea/models/user"
"github.com/markbates/goth"
)
// Store represents a thing that stores things
type Store interface {
Get(any) any
Set(any, any) error
Release() error
}
// LinkAccountFromStore links the provided user with a stored external user
func LinkAccountFromStore(ctx context.Context, store Store, user *user_model.User) error {
gothUser := store.Get("linkAccountGothUser")
if gothUser == nil {
return errors.New("not in LinkAccount session")
}
return LinkAccountToUser(ctx, user, gothUser.(goth.User))
}

View File

@ -8,7 +8,6 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@ -17,15 +16,11 @@ import (
"github.com/markbates/goth"
)
func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser goth.User) (*user_model.ExternalLoginUser, error) {
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
if err != nil {
return nil, err
}
func toExternalLoginUser(authSourceID int64, user *user_model.User, gothUser goth.User) *user_model.ExternalLoginUser {
return &user_model.ExternalLoginUser{
ExternalID: gothUser.UserID,
UserID: user.ID,
LoginSourceID: authSource.ID,
LoginSourceID: authSourceID,
RawData: gothUser.RawData,
Provider: gothUser.Provider,
Email: gothUser.Email,
@ -40,15 +35,12 @@ func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser go
AccessTokenSecret: gothUser.AccessTokenSecret,
RefreshToken: gothUser.RefreshToken,
ExpiresAt: gothUser.ExpiresAt,
}, nil
}
}
// LinkAccountToUser link the gothUser to the user
func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
if err != nil {
return err
}
func LinkAccountToUser(ctx context.Context, authSourceID int64, user *user_model.User, gothUser goth.User) error {
externalLoginUser := toExternalLoginUser(authSourceID, user, gothUser)
if err := user_model.LinkExternalToUser(ctx, user, externalLoginUser); err != nil {
return err
@ -72,12 +64,8 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth
}
// EnsureLinkExternalToUser link the gothUser to the user
func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
if err != nil {
return err
}
func EnsureLinkExternalToUser(ctx context.Context, authSourceID int64, user *user_model.User, gothUser goth.User) error {
externalLoginUser := toExternalLoginUser(authSourceID, user, gothUser)
return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser)
}

View File

@ -18,45 +18,54 @@ type AuthenticationForm struct {
Type int `binding:"Range(2,7)"`
Name string `binding:"Required;MaxSize(30)"`
TwoFactorPolicy string
IsActive bool
IsSyncEnabled bool
Host string
Port int
BindDN string
BindPassword string
UserBase string
UserDN string
AttributeUsername string
AttributeName string
AttributeSurname string
AttributeMail string
AttributeSSHPublicKey string
AttributeAvatar string
AttributesInBind bool
UsePagedSearch bool
SearchPageSize int
Filter string
AdminFilter string
GroupsEnabled bool
GroupDN string
GroupFilter string
GroupMemberUID string
UserUID string
RestrictedFilter string
AllowDeactivateAll bool
IsActive bool
IsSyncEnabled bool
SMTPAuth string
SMTPHost string
SMTPPort int
AllowedDomains string
SecurityProtocol int `binding:"Range(0,2)"`
TLS bool
SkipVerify bool
HeloHostname string
DisableHelo bool
ForceSMTPS bool
PAMServiceName string
PAMEmailDomain string
// LDAP
Host string
Port int
BindDN string
BindPassword string
UserBase string
UserDN string
AttributeUsername string
AttributeName string
AttributeSurname string
AttributeMail string
AttributeSSHPublicKey string
AttributeAvatar string
AttributesInBind bool
UsePagedSearch bool
SearchPageSize int
Filter string
AdminFilter string
GroupsEnabled bool
GroupDN string
GroupFilter string
GroupMemberUID string
UserUID string
RestrictedFilter string
AllowDeactivateAll bool
GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
// SMTP
SMTPAuth string
SMTPHost string
SMTPPort int
AllowedDomains string
SecurityProtocol int `binding:"Range(0,2)"`
TLS bool
SkipVerify bool
HeloHostname string
DisableHelo bool
ForceSMTPS bool
// PAM
PAMServiceName string
PAMEmailDomain string
// Oauth2 & OIDC
Oauth2Provider string
Oauth2Key string
Oauth2Secret string
@ -76,13 +85,15 @@ type AuthenticationForm struct {
Oauth2RestrictedGroup string
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
Oauth2GroupTeamMapRemoval bool
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string
GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
Oauth2SSHPublicKeyClaimName string
Oauth2FullNameClaimName string
// SSPI
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string
}
// Validate validates fields

View File

@ -35,13 +35,6 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository,
for _, c := range oldCommits {
committerUser := emailUsers.GetByEmail(c.Committer.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
if committerUser == nil {
committerUser = &user_model.User{
Name: c.Committer.Name,
Email: c.Committer.Email,
}
}
signCommit := &asymkey_model.SignCommit{
UserCommit: c,
Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committerUser),

View File

@ -301,19 +301,30 @@
<input id="oauth2_tenant" name="oauth2_tenant" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.Tenant}}{{end}}">
</div>
{{range .OAuth2Providers}}{{if .CustomURLSettings}}
{{range .OAuth2Providers}}
<input id="{{.Name}}_SupportSSHPublicKey" value="{{.SupportSSHPublicKey}}" type="hidden">
{{if .CustomURLSettings}}
<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
{{end}}{{end}}
{{end}}
{{end}}
<div class="field">
<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "admin.auths.oauth2_full_name_claim_name"}}</label>
<input name="oauth2_full_name_claim_name" value="{{$cfg.FullNameClaimName}}" placeholder="name">
</div>
<div class="field oauth2_ssh_public_key_claim_name">
<label>{{ctx.Locale.Tr "admin.auths.oauth2_ssh_public_key_claim_name"}}</label>
<input name="oauth2_ssh_public_key_claim_name" value="{{$cfg.SSHPublicKeyClaimName}}" placeholder="sshpubkey">
</div>
<div class="field">
<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{$cfg.RequiredClaimName}}">

View File

@ -63,19 +63,31 @@
<input id="oauth2_tenant" name="oauth2_tenant" value="{{.oauth2_tenant}}">
</div>
{{range .OAuth2Providers}}{{if .CustomURLSettings}}
{{range .OAuth2Providers}}
<input id="{{.Name}}_SupportSSHPublicKey" value="{{.SupportSSHPublicKey}}" type="hidden">
{{if .CustomURLSettings}}
<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
{{end}}{{end}}
{{end}}
{{end}}
<div class="field">
<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
<input id="oauth2_scopes" name="oauth2_scopes" value="{{.oauth2_scopes}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "admin.auths.oauth2_full_name_claim_name"}}</label>
<input name="oauth2_full_name_claim_name" value="{{.oauth2_full_name_claim_name}}" placeholder="name">
</div>
<div class="field oauth2_ssh_public_key_claim_name">
<label>{{ctx.Locale.Tr "admin.auths.oauth2_ssh_public_key_claim_name"}}</label>
<input name="oauth2_ssh_public_key_claim_name" value="{{.oauth2_ssh_public_key_claim_name}}" placeholder="sshpubkey">
</div>
<div class="field">
<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{.oauth2_required_claim_name}}">

View File

@ -1,12 +1,18 @@
{{range .RecentlyPushedNewBranches}}
<div class="ui positive message tw-flex tw-items-center tw-gap-2">
<div class="tw-flex-1 tw-break-anywhere">
{{$timeSince := DateUtils.TimeSince .CommitTime}}
{{$branchLink := HTMLFormat `<a href="%s">%s</a>` .BranchLink .BranchDisplayName}}
{{/* Template Attributes:
* RecentBranchesPromptData
*/}}
{{$data := .RecentBranchesPromptData}}
{{if $data}}
{{range $recentBranch := $data.RecentlyPushedNewBranches}}
<div class="ui positive message flex-text-block">
<div class="tw-flex-1">
{{$timeSince := DateUtils.TimeSince $recentBranch.CommitTime}}
{{$branchLink := HTMLFormat `<a href="%s">%s</a>` $recentBranch.BranchLink .BranchDisplayName}}
{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
</div>
<a role="button" class="ui compact green button tw-m-0" href="{{QueryBuild .BranchCompareURL "expand" 1}}">
<a role="button" class="ui compact green button" href="{{QueryBuild $recentBranch.BranchCompareURL "expand" 1}}">
{{ctx.Locale.Tr "repo.pulls.compare_changes"}}
</a>
</div>
{{end}}
{{end}}

View File

@ -147,7 +147,7 @@
<div class="flex-text-inline">
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
<span class="text grey">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
{{if ne .Verification.CommittingUser.ID 0}}
{{if and .Verification.CommittingUser .Verification.CommittingUser.ID}}
{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}}
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a>
{{else}}

View File

@ -16,7 +16,7 @@
<td class="author">
<div class="tw-flex">
{{$userName := .Author.Name}}
{{if and .User (gt .User.ID 0)}} /* User with id == 0 is a fake user from git author */
{{if .User}}
{{if and .User.FullName DefaultShowFullName}}
{{$userName = .User.FullName}}
{{end}}

View File

@ -15,7 +15,7 @@
</div>
{{end}}
{{template "repo/code/recently_pushed_new_branches" .}}
{{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}">
<div class="repo-home-filelist">

View File

@ -4,6 +4,8 @@
<div class="ui container">
{{template "base/alert" .}}
{{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
{{if .PinnedIssues}}
<div id="issue-pins" {{if .IsRepoAdmin}}data-is-repo-admin{{end}}>
{{range .PinnedIssues}}

View File

@ -14,7 +14,7 @@
</div>
{{end}}
{{template "repo/code/recently_pushed_new_branches" .}}
{{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
<div class="repo-view-container">
<div class="tw-flex tw-flex-col repo-view-file-tree-container not-mobile {{if not .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}" {{if .IsSigned}}data-user-is-signed-in{{end}}>

View File

@ -9,9 +9,11 @@ import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
asymkey_model "code.gitea.io/gitea/models/asymkey"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
@ -20,9 +22,13 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/oauth2_provider"
"code.gitea.io/gitea/tests"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -931,3 +937,107 @@ func testOAuth2WellKnown(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
}
func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
cfg.Provider = util.IfZero(cfg.Provider, "gitea")
err := auth_model.CreateSource(db.DefaultContext, &auth_model.Source{
Type: auth_model.OAuth2,
Name: authName,
IsActive: true,
Cfg: &cfg,
})
require.NoError(t, err)
}
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
defer tests.PrepareTestEnv(t)()
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/.well-known/openid-configuration":
_, _ = w.Write([]byte(`{
"issuer": "` + mockServer.URL + `",
"authorization_endpoint": "` + mockServer.URL + `/authorize",
"token_endpoint": "` + mockServer.URL + `/token",
"userinfo_endpoint": "` + mockServer.URL + `/userinfo"
}`))
default:
http.NotFound(w, r)
}
}))
defer mockServer.Close()
ctx := t.Context()
oauth2Source := oauth2.Source{
Provider: "openidConnect",
ClientID: "test-client-id",
SSHPublicKeyClaimName: "sshpubkey",
FullNameClaimName: "name",
OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
}
addOAuth2Source(t, "test-oidc-source", oauth2Source)
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(ctx, "test-oidc-source")
require.NoError(t, err)
sshKey1 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf"
sshKey2 := "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIE7kM1R02+4ertDKGKEDcKG0s+2vyDDcIvceJ0Gqv5f1AAAABHNzaDo="
sshKey3 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9"
cases := []struct {
testName string
mockFullName string
mockRawData map[string]any
expectedSSHPubKeys []string
}{
{
testName: "Login1",
mockFullName: "FullName1",
mockRawData: map[string]any{"sshpubkey": []any{sshKey1 + " any-comment"}},
expectedSSHPubKeys: []string{sshKey1},
},
{
testName: "Login2",
mockFullName: "FullName2",
mockRawData: map[string]any{"sshpubkey": []any{sshKey2 + " any-comment", sshKey3}},
expectedSSHPubKeys: []string{sshKey2, sshKey3},
},
{
testName: "Login3",
mockFullName: "FullName3",
mockRawData: map[string]any{},
expectedSSHPubKeys: []string{},
},
}
session := emptyTestSession(t)
for _, c := range cases {
t.Run(c.testName, func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.Username, "")()
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{
Provider: authSource.Cfg.(*oauth2.Source).Provider,
UserID: "oidc-userid",
Email: "oidc-email@example.com",
RawData: c.mockRawData,
Name: c.mockFullName,
}, nil
})()
req := NewRequest(t, "GET", "/user/oauth2/test-oidc-source/callback?code=XYZ&state=XYZ")
session.MakeRequest(t, req, http.StatusSeeOther)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "oidc-userid"})
keys, _, err := db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: db.ListOptionsAll,
OwnerID: user.ID,
LoginSourceID: authSource.ID,
})
require.NoError(t, err)
var sshPubKeys []string
for _, key := range keys {
sshPubKeys = append(sshPubKeys, key.Content)
}
assert.ElementsMatch(t, c.expectedSSHPubKeys, sshPubKeys)
assert.Equal(t, c.mockFullName, user.FullName)
})
}
}

View File

@ -24,39 +24,59 @@ import (
func TestRepoCommits(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Request repository commits page
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
resp := session.MakeRequest(t, req, http.StatusOK)
t.Run("CommitList", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master")
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href")
assert.True(t, exists)
assert.NotEmpty(t, commitURL)
}
func Test_ReposGitCommitListNotMaster(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master")
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
var commits []string
doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) {
commitURL, _ := s.Attr("href")
commits = append(commits, path.Base(commitURL))
var commits, userHrefs []string
doc := NewHTMLParser(t, resp.Body)
doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) {
commits = append(commits, path.Base(s.AttrOr("href", "")))
})
doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) {
userHrefs = append(userHrefs, s.AttrOr("href", ""))
})
assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits)
assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs)
})
assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits)
var userHrefs []string
doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) {
userHref, _ := s.Attr("href")
userHrefs = append(userHrefs, userHref)
t.Run("LastCommit", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo16")
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "")
authorHref := doc.doc.Find(".latest-commit .author-wrapper").AttrOr("href", "")
assert.Equal(t, "/user2/repo16/commit/69554a64c1e6030f051e5c3f94bfbd773cd6a324", commitHref)
assert.Equal(t, "/user2", authorHref)
})
t.Run("CommitListNonExistingCommiter", func(t *testing.T) {
// check the commit list for a repository with no gitea user
// * commit 985f0301dba5e7b34be866819cd15ad3d8f508ee (branch2)
// * Author: 6543 <6543@obermui.de>
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/branch2")
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
commitHref := doc.doc.Find("#commits-table tr:first-child .commit-id-short").AttrOr("href", "")
assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref)
authorElem := doc.doc.Find("#commits-table tr:first-child .author-wrapper")
assert.Equal(t, "6543", authorElem.Text())
assert.Equal(t, "span", authorElem.Nodes[0].Data)
})
t.Run("LastCommitNonExistingCommiter", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/src/branch/branch2")
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "")
assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref)
authorElem := doc.doc.Find(".latest-commit .author-wrapper")
assert.Equal(t, "6543", authorElem.Text())
assert.Equal(t, "span", authorElem.Nodes[0].Data)
})
assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs)
}
func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {

View File

@ -9,6 +9,7 @@ import (
"strings"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@ -17,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/tests"
@ -103,8 +105,9 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
defer tests.PrepareTestEnv(t)()
mockLinkAccount := func(ctx *context.Context) {
authSource := auth_model.Source{ID: 1}
gothUser := goth.User{Email: "invalid-email", Name: "."}
_ = ctx.Session.Set("linkAccountGothUser", gothUser)
_ = ctx.Session.Set("linkAccountData", auth.LinkAccountData{AuthSource: authSource, GothUser: gothUser})
}
t.Run("EnablePasswordSignInForm=false", func(t *testing.T) {

View File

@ -102,6 +102,9 @@ function initAdminAuthentication() {
break;
}
}
const supportSshPublicKey = document.querySelector<HTMLInputElement>(`#${provider}_SupportSSHPublicKey`)?.value === 'true';
toggleElem('.field.oauth2_ssh_public_key_claim_name', supportSshPublicKey);
onOAuth2UseCustomURLChange(applyDefaultValues);
}