0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-18 21:42:56 +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/* \ /tmp/local/etc/s6/.s6-svscan/* \
/go/src/code.gitea.io/gitea/gitea \ /go/src/code.gitea.io/gitea/gitea \
/go/src/code.gitea.io/gitea/environment-to-ini /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 FROM docker.io/library/alpine:3.22
LABEL maintainer="maintainers@gitea.io" 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 /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/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/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 \ /tmp/local/usr/local/bin/gitea \
/go/src/code.gitea.io/gitea/gitea \ /go/src/code.gitea.io/gitea/gitea \
/go/src/code.gitea.io/gitea/environment-to-ini /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 FROM docker.io/library/alpine:3.22
LABEL maintainer="maintainers@gitea.io" 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 /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/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 --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 # git:git
USER 1000:1000 USER 1000:1000

View File

@ -87,6 +87,14 @@ func oauthCLIFlags() []cli.Flag {
Value: nil, Value: nil,
Usage: "Scopes to request when to authenticate against this OAuth2 source", 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{ &cli.StringFlag{
Name: "required-claim-name", Name: "required-claim-name",
Value: "", Value: "",
@ -177,6 +185,8 @@ func parseOAuth2Config(c *cli.Command) *oauth2.Source {
RestrictedGroup: c.String("restricted-group"), RestrictedGroup: c.String("restricted-group"),
GroupTeamMap: c.String("group-team-map"), GroupTeamMap: c.String("group-team-map"),
GroupTeamMapRemoval: c.Bool("group-team-map-removal"), 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") { if c.IsSet("group-team-map-removal") {
oAuth2Config.GroupTeamMapRemoval = c.Bool("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 // update custom URL mapping
customURLMapping := &oauth2.CustomURLMapping{} customURLMapping := &oauth2.CustomURLMapping{}

View File

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

View File

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

View File

@ -355,13 +355,13 @@ func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.So
return sshKeysNeedUpdate 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 { func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
var sshKeysNeedUpdate bool var sshKeysNeedUpdate bool
log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name) 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 var giteaKeys []string
keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{ keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
OwnerID: usr.ID, OwnerID: usr.ID,

View File

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

View File

@ -334,7 +334,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
err = registerableSource.RegisterSource() err = registerableSource.RegisterSource()
if err != nil { 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 { if _, err := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil {
log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err) 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.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. // 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. // 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 { if doer == nil {
return []*RecentlyPushedNewBranch{}, 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. // 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 { func (repo *Repository) CanEnableEditor() bool {
return repo.CanContentChange()
}
func (repo *Repository) CanContentChange() bool {
return !repo.IsMirror && !repo.IsArchived return !repo.IsMirror && !repo.IsArchived
} }

View File

@ -1166,12 +1166,6 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
for _, c := range oldCommits { for _, c := range oldCommits {
user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? 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{ newCommits = append(newCommits, &UserCommit{
User: user, User: user,
Commit: c, Commit: c,
@ -1195,12 +1189,14 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
needCheckEmails := make(container.Set[string]) needCheckEmails := make(container.Set[string])
needCheckUserNames := make(container.Set[string]) needCheckUserNames := make(container.Set[string])
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
for _, email := range emails { for _, email := range emails {
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) { emailLower := strings.ToLower(email)
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress) if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
needCheckUserNames.Add(strings.ToLower(username)) needCheckUserNames.Add(noReplyUserNameLower)
needCheckEmails.Add(emailLower)
} else { } 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) 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 { type Commit struct {
Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache" 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 ID ObjectID
Author *Signature Author *Signature // never nil
Committer *Signature Committer *Signature // never nil
CommitMessage string CommitMessage string
Signature *CommitSignature Signature *CommitSignature

View File

@ -28,6 +28,13 @@ func FromPtr[T any](v *T) Option[T] {
return Some(*v) 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] { func FromNonDefault[T comparable](v T) Option[T] {
var zero T var zero T
if v == zero { if v == zero {

View File

@ -56,6 +56,12 @@ func TestOption(t *testing.T) {
opt3 := optional.FromNonDefault(1) opt3 := optional.FromNonDefault(1)
assert.True(t, opt3.Has()) assert.True(t, opt3.Has())
assert.Equal(t, int(1), opt3.Value()) 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) { func Test_ParseBool(t *testing.T) {

View File

@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log" "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 type OAuth2UsernameType string
const ( 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 = 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_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_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_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_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) 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), Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active), IsActive: optional.FromPtr(form.Active),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), 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), AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal), AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation), MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),

View File

@ -391,7 +391,7 @@ func Edit(ctx *context.APIContext) {
Description: optional.Some(form.Description), Description: optional.Some(form.Description),
Website: optional.Some(form.Website), Website: optional.Some(form.Website),
Location: optional.Some(form.Location), Location: optional.Some(form.Location),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
} }
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { 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, AdminGroup: form.Oauth2AdminGroup,
GroupTeamMap: form.Oauth2GroupTeamMap, GroupTeamMap: form.Oauth2GroupTeamMap,
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval, 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/templates"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
) )
@ -75,7 +74,7 @@ func TwoFactorPost(ctx *context.Context) {
} }
if ctx.Session.Get("linkAccount") != nil { if ctx.Session.Get("linkAccount") != nil {
err = externalaccount.LinkAccountFromStore(ctx, ctx.Session, u) err = linkAccountFromContext(ctx, u)
if err != nil { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
return return

View File

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

View File

@ -5,7 +5,6 @@ package auth
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
@ -21,8 +20,6 @@ import (
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"github.com/markbates/goth"
) )
var tplLinkAccount templates.TplName = "user/auth/link_account" 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["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" 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 // 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) // 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 // no account in session, so just redirect to the login page, then the user could restart the process
ctx.Redirect(setting.AppSubURL + "/user/login") ctx.Redirect(setting.AppSubURL + "/user/login")
return return
} }
if missingFields, ok := gothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok { if missingFields, ok := linkAccountData.GothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok {
ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, strings.Join(missingFields, ",")) 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 { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
return return
} }
email := gothUser.Email email := linkAccountData.GothUser.Email
ctx.Data["user_name"] = uname ctx.Data["user_name"] = uname
ctx.Data["email"] = email ctx.Data["email"] = email
@ -152,8 +149,8 @@ func LinkAccountPostSignIn(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser := ctx.Session.Get("linkAccountGothUser") linkAccountData := oauth2GetLinkAccountData(ctx)
if gothUser == nil { if linkAccountData == nil {
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session")) ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
return return
} }
@ -169,11 +166,14 @@ func LinkAccountPostSignIn(ctx *context.Context) {
return 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) { func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData *LinkAccountData, remember bool) {
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u) 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. // If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page. // 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 return
} }
err = externalaccount.LinkAccountToUser(ctx, u, gothUser) err = externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, u, linkAccountData.GothUser)
if err != nil { if err != nil {
ctx.ServerError("UserLinkAccount", err) ctx.ServerError("UserLinkAccount", err)
return return
@ -243,17 +243,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUserInterface := ctx.Session.Get("linkAccountGothUser") linkAccountData := oauth2GetLinkAccountData(ctx)
if gothUserInterface == nil { if linkAccountData == nil {
ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session")) ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
return 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() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tplLinkAccount) ctx.HTML(http.StatusOK, tplLinkAccount)
return 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{ u := &user_model.User{
Name: form.UserName, Name: form.UserName,
Email: form.Email, Email: form.Email,
Passwd: form.Password, Passwd: form.Password,
LoginType: auth.OAuth2, LoginType: auth.OAuth2,
LoginSource: authSource.ID, LoginSource: linkAccountData.AuthSource.ID,
LoginName: gothUser.UserID, LoginName: linkAccountData.GothUser.UserID,
} }
if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) { if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, linkAccountData) {
// error already handled // error already handled
return return
} }
source := authSource.Cfg.(*oauth2.Source) source := linkAccountData.AuthSource.Cfg.(*oauth2.Source)
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil { if err := syncGroupsToTeams(ctx, source, &linkAccountData.GothUser, u); err != nil {
ctx.ServerError("SyncGroupsToTeams", err) ctx.ServerError("SyncGroupsToTeams", err)
return return
} }
handleSignIn(ctx, u, false) 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/optional"
"code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
source_service "code.gitea.io/gitea/services/auth/source" source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/oauth2"
@ -35,9 +34,8 @@ import (
// SignInOAuth handles the OAuth2 login buttons // SignInOAuth handles the OAuth2 login buttons
func SignInOAuth(ctx *context.Context) { func SignInOAuth(ctx *context.Context) {
provider := ctx.PathParam("provider") authName := ctx.PathParam("provider")
authSource, err := auth.GetActiveOAuth2SourceByAuthName(ctx, authName)
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
if err != nil { if err != nil {
ctx.ServerError("SignIn", err) ctx.ServerError("SignIn", err)
return return
@ -74,8 +72,6 @@ func SignInOAuth(ctx *context.Context) {
// SignInOAuthCallback handles the callback from the given provider // SignInOAuthCallback handles the callback from the given provider
func SignInOAuthCallback(ctx *context.Context) { func SignInOAuthCallback(ctx *context.Context) {
provider := ctx.PathParam("provider")
if ctx.Req.FormValue("error") != "" { if ctx.Req.FormValue("error") != "" {
var errorKeyValues []string var errorKeyValues []string
for k, vv := range ctx.Req.Form { for k, vv := range ctx.Req.Form {
@ -88,7 +84,8 @@ func SignInOAuthCallback(ctx *context.Context) {
} }
// first look if the provider is still active // 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 { if err != nil {
ctx.ServerError("SignIn", err) ctx.ServerError("SignIn", err)
return return
@ -133,7 +130,7 @@ func SignInOAuthCallback(ctx *context.Context) {
if u == nil { if u == nil {
if ctx.Doer != nil { if ctx.Doer != nil {
// attach user to the current signed-in user // 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 { if err != nil {
ctx.ServerError("UserLinkAccount", err) ctx.ServerError("UserLinkAccount", err)
return return
@ -174,12 +171,11 @@ func SignInOAuthCallback(ctx *context.Context) {
gothUser.RawData = make(map[string]any) gothUser.RawData = make(map[string]any)
} }
gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields
showLinkingLogin(ctx, gothUser) showLinkingLogin(ctx, authSource, gothUser)
return return
} }
u = &user_model.User{ u = &user_model.User{
Name: uname, Name: uname,
FullName: gothUser.Name,
Email: gothUser.Email, Email: gothUser.Email,
LoginType: auth.OAuth2, LoginType: auth.OAuth2,
LoginSource: authSource.ID, LoginSource: authSource.ID,
@ -196,7 +192,11 @@ func SignInOAuthCallback(ctx *context.Context) {
u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted) 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 // error already handled
return return
} }
@ -207,7 +207,7 @@ func SignInOAuthCallback(ctx *context.Context) {
} }
} else { } else {
// no existing user is found, request attach or new account // no existing user is found, request attach or new account
showLinkingLogin(ctx, gothUser) showLinkingLogin(ctx, authSource, gothUser)
return return
} }
} }
@ -271,9 +271,22 @@ func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *g
return isAdmin, isRestricted 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{ if err := updateSession(ctx, nil, map[string]any{
"linkAccountGothUser": gothUser, "linkAccountData": LinkAccountData{*authSource, gothUser},
}); err != nil { }); err != nil {
ctx.ServerError("updateSession", err) ctx.ServerError("updateSession", err)
return return
@ -281,7 +294,7 @@ func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
ctx.Redirect(setting.AppSubURL + "/user/link_account") 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 { if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
resp, err := http.Get(url) resp, err := http.Get(url)
if err == nil { 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) { func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) {
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u) oauth2SignInSync(ctx, authSource, u, gothUser)
if ctx.Written() {
return
}
needs2FA := false needs2FA := false
if !source.TwoFactorShouldSkip() { if !authSource.TwoFactorShouldSkip() {
_, err := auth.GetTwoFactorByUID(ctx, u.ID) _, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
@ -312,7 +328,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
needs2FA = err == nil needs2FA = err == nil
} }
oauth2Source := source.Cfg.(*oauth2.Source) oauth2Source := authSource.Cfg.(*oauth2.Source)
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap) groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap)
if err != nil { if err != nil {
ctx.ServerError("UnmarshalGroupTeamMapping", err) 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) ctx.ServerError("EnsureLinkExternalToUser", err)
return 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, Email: form.Email,
Passwd: password, Passwd: password,
} }
if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false) { if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil) {
// error already handled // error already handled
return return
} }

View File

@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
@ -150,7 +149,7 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
// Now handle account linking if that's requested // Now handle account linking if that's requested
if ctx.Session.Get("linkAccount") != nil { 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) ctx.ServerError("LinkAccountFromStore", err)
return return
} }
@ -268,7 +267,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
// Now handle account linking if that's requested // Now handle account linking if that's requested
if ctx.Session.Get("linkAccount") != nil { 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) ctx.ServerError("LinkAccountFromStore", err)
return 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["Title"] = ctx.Tr("repo.pulls")
ctx.Data["PageIsPullList"] = true ctx.Data["PageIsPullList"] = true
prepareRecentlyPushedNewBranches(ctx)
if ctx.Written() {
return
}
} else { } else {
MustEnableIssues(ctx) MustEnableIssues(ctx)
if ctx.Written() { if ctx.Written() {

View File

@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" 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" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit" unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -196,56 +195,6 @@ func prepareUpstreamDivergingInfo(ctx *context.Context) {
ctx.Data["UpstreamDivergingInfo"] = upstreamDivergingInfo 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) { func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) {
if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status { if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status {
return return

View File

@ -5,6 +5,7 @@ package agit
import ( import (
"context" "context"
"encoding/base64"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -18,17 +19,30 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull" 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 // ProcReceive handle proc receive work
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) { 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)) results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush) forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
topicBranch := opts.GitPushOptions["topic"] 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) objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
userName := strings.ToLower(opts.UserName) 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. // ParseCommitWithSignature check if signature is good against keystore.
func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *asymkey_model.CommitVerification { func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *asymkey_model.CommitVerification {
var committer *user_model.User committer, err := user_model.GetUserByEmail(ctx, c.Committer.Email)
if c.Committer != nil { if err != nil && !user_model.IsErrUserNotExist(err) {
var err error log.Error("GetUserByEmail: %v", err)
// Find Committer account return &asymkey_model.CommitVerification{
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 Verified: false,
if err != nil { // Skipping not user for committer Reason: "gpg.error.no_committer_account", // this error is not right, but such error should seldom happen
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",
}
}
} }
} }
return ParseCommitWithSignatureCommitter(ctx, c, committer) 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 { 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 { if c.Signature == nil {
return &asymkey_model.CommitVerification{ return &asymkey_model.CommitVerification{
CommittingUser: committer, CommittingUser: committer,
Verified: false, // Default value Verified: false,
Reason: "gpg.error.not_signed_commit", // Default value Reason: "gpg.error.not_signed_commit",
} }
} }
// to support instance key, we need a fake committer user (not really needed, but legacy code accesses the committer without nil-check)
// If this a SSH signature handle it differently if committer == nil {
if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") { committer = &user_model.User{
return ParseCommitWithSSHSignature(ctx, c, committer) 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 // Parsing signature
sig, err := asymkey_model.ExtractSignature(c.Signature.Signature) sig, err := asymkey_model.ExtractSignature(c.Signature.Signature)
if err != nil { // Skipping failed to extract sign 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 { if err := gpgSettings.LoadPublicKeyContent(); err != nil {
log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) 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 { if commitVerification.Reason == asymkey_model.BadSignature {
defaultReason = asymkey_model.BadSignature defaultReason = asymkey_model.BadSignature
} else { } else {
@ -180,7 +176,7 @@ func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, commi
} else if defaultGPGSettings == nil { } else if defaultGPGSettings == nil {
log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String())
} else if defaultGPGSettings.Sign { } 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 { if commitVerification.Reason == asymkey_model.BadSignature {
defaultReason = asymkey_model.BadSignature defaultReason = asymkey_model.BadSignature
} else { } 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 // First try to find the key in the db
if commitVerification := HashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { if commitVerification := HashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
return commitVerification 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) return verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, sshPubKey, committerUser, signerUser, committerGitEmail)
} }
// ParseCommitWithSSHSignature check if signature is good against keystore. // parseCommitWithSSHSignature check if signature is good against keystore.
func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification { 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 // Now try to associate the signature with the committer, if present
if committerUser.ID != 0 { if committerUser.ID != 0 {
keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ 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", Name: "User Two",
Email: "user2@example.com", Email: "user2@example.com",
} }
ret := ParseCommitWithSSHSignature(t.Context(), commit, committingUser) ret := parseCommitWithSSHSignature(t.Context(), commit, committingUser)
require.NotNil(t, ret) require.NotNil(t, ret)
assert.True(t, ret.Verified) assert.True(t, ret.Verified)
assert.False(t, ret.Warning) assert.False(t, ret.Warning)

View File

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

View File

@ -14,6 +14,13 @@ import (
type BaseProvider struct { type BaseProvider struct {
name string name string
displayName 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 // Name provides the technical name for this provider

View File

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

View File

@ -27,6 +27,9 @@ type Source struct {
GroupTeamMap string GroupTeamMap string
GroupTeamMapRemoval bool GroupTeamMapRemoval bool
RestrictedGroup string RestrictedGroup string
SSHPublicKeyClaimName string
FullNameClaimName string
} }
// FromDB fills up an OAuth2Config from serialized format. // 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" "strconv"
"strings" "strings"
"code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -17,15 +16,11 @@ import (
"github.com/markbates/goth" "github.com/markbates/goth"
) )
func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser goth.User) (*user_model.ExternalLoginUser, error) { func toExternalLoginUser(authSourceID int64, user *user_model.User, gothUser goth.User) *user_model.ExternalLoginUser {
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
if err != nil {
return nil, err
}
return &user_model.ExternalLoginUser{ return &user_model.ExternalLoginUser{
ExternalID: gothUser.UserID, ExternalID: gothUser.UserID,
UserID: user.ID, UserID: user.ID,
LoginSourceID: authSource.ID, LoginSourceID: authSourceID,
RawData: gothUser.RawData, RawData: gothUser.RawData,
Provider: gothUser.Provider, Provider: gothUser.Provider,
Email: gothUser.Email, Email: gothUser.Email,
@ -40,15 +35,12 @@ func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser go
AccessTokenSecret: gothUser.AccessTokenSecret, AccessTokenSecret: gothUser.AccessTokenSecret,
RefreshToken: gothUser.RefreshToken, RefreshToken: gothUser.RefreshToken,
ExpiresAt: gothUser.ExpiresAt, ExpiresAt: gothUser.ExpiresAt,
}, nil }
} }
// LinkAccountToUser link the gothUser to the user // LinkAccountToUser link the gothUser to the user
func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error { func LinkAccountToUser(ctx context.Context, authSourceID int64, user *user_model.User, gothUser goth.User) error {
externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser) externalLoginUser := toExternalLoginUser(authSourceID, user, gothUser)
if err != nil {
return err
}
if err := user_model.LinkExternalToUser(ctx, user, externalLoginUser); err != nil { if err := user_model.LinkExternalToUser(ctx, user, externalLoginUser); err != nil {
return err return err
@ -72,12 +64,8 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth
} }
// EnsureLinkExternalToUser link the gothUser to the user // EnsureLinkExternalToUser link the gothUser to the user
func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error { func EnsureLinkExternalToUser(ctx context.Context, authSourceID int64, user *user_model.User, gothUser goth.User) error {
externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser) externalLoginUser := toExternalLoginUser(authSourceID, user, gothUser)
if err != nil {
return err
}
return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser) return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser)
} }

View File

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

View File

@ -35,13 +35,6 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository,
for _, c := range oldCommits { for _, c := range oldCommits {
committerUser := emailUsers.GetByEmail(c.Committer.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? 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{ signCommit := &asymkey_model.SignCommit{
UserCommit: c, UserCommit: c,
Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committerUser), 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}}"> <input id="oauth2_tenant" name="oauth2_tenant" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.Tenant}}{{end}}">
</div> </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}}_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}}_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}}_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}}_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}}_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"> <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"> <div class="field">
<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label> <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}}"> <input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}">
</div> </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"> <div class="field">
<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label> <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}}"> <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}}"> <input id="oauth2_tenant" name="oauth2_tenant" value="{{.oauth2_tenant}}">
</div> </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}}_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}}_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}}_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}}_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}}_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"> <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"> <div class="field">
<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label> <label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
<input id="oauth2_scopes" name="oauth2_scopes" value="{{.oauth2_scopes}}"> <input id="oauth2_scopes" name="oauth2_scopes" value="{{.oauth2_scopes}}">
</div> </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"> <div class="field">
<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label> <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}}"> <input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{.oauth2_required_claim_name}}">

View File

@ -1,12 +1,18 @@
{{range .RecentlyPushedNewBranches}} {{/* Template Attributes:
<div class="ui positive message tw-flex tw-items-center tw-gap-2"> * RecentBranchesPromptData
<div class="tw-flex-1 tw-break-anywhere"> */}}
{{$timeSince := DateUtils.TimeSince .CommitTime}} {{$data := .RecentBranchesPromptData}}
{{$branchLink := HTMLFormat `<a href="%s">%s</a>` .BranchLink .BranchDisplayName}} {{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}} {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
</div> </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"}} {{ctx.Locale.Tr "repo.pulls.compare_changes"}}
</a> </a>
</div> </div>
{{end}}
{{end}} {{end}}

View File

@ -147,7 +147,7 @@
<div class="flex-text-inline"> <div class="flex-text-inline">
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} {{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> <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}} {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}}
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a>
{{else}} {{else}}

View File

@ -16,7 +16,7 @@
<td class="author"> <td class="author">
<div class="tw-flex"> <div class="tw-flex">
{{$userName := .Author.Name}} {{$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}} {{if and .User.FullName DefaultShowFullName}}
{{$userName = .User.FullName}} {{$userName = .User.FullName}}
{{end}} {{end}}

View File

@ -15,7 +15,7 @@
</div> </div>
{{end}} {{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="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}">
<div class="repo-home-filelist"> <div class="repo-home-filelist">

View File

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

View File

@ -14,7 +14,7 @@
</div> </div>
{{end}} {{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="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}}> <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" "fmt"
"io" "io"
"net/http" "net/http"
"net/http/httptest"
"strings" "strings"
"testing" "testing"
asymkey_model "code.gitea.io/gitea/models/asymkey"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
@ -20,9 +22,13 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test" "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/services/oauth2_provider"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -931,3 +937,107 @@ func testOAuth2WellKnown(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.Enabled, false)() defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound) 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) { func TestRepoCommits(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2") session := loginUser(t, "user2")
// Request repository commits page t.Run("CommitList", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master") req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master")
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body) var commits, userHrefs []string
commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") doc := NewHTMLParser(t, resp.Body)
assert.True(t, exists) doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) {
assert.NotEmpty(t, commitURL) commits = append(commits, path.Base(s.AttrOr("href", "")))
} })
doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) {
func Test_ReposGitCommitListNotMaster(t *testing.T) { userHrefs = append(userHrefs, s.AttrOr("href", ""))
defer tests.PrepareTestEnv(t)() })
session := loginUser(t, "user2") assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits)
req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master") assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs)
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))
}) })
assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits)
var userHrefs []string t.Run("LastCommit", func(t *testing.T) {
doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) { req := NewRequest(t, "GET", "/user2/repo16")
userHref, _ := s.Attr("href") resp := session.MakeRequest(t, req, http.StatusOK)
userHrefs = append(userHrefs, userHref) 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) { func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {

View File

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

View File

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