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:
commit
9df2d9ab30
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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{}
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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 (
|
||||||
|
@ -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)
|
||||||
|
@ -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),
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
88
routers/web/auth/oauth_signin_sync.go
Normal file
88
routers/web/auth/oauth_signin_sync.go
Normal 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)
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
73
routers/web/repo/common_recentbranches.go
Normal file
73
routers/web/repo/common_recentbranches.go
Normal 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}
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
16
services/agit/agit_test.go
Normal file
16
services/agit/agit_test.go
Normal 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"))
|
||||||
|
}
|
@ -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{
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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.
|
||||||
|
@ -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))
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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}}">
|
||||||
|
@ -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}}">
|
||||||
|
@ -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}}
|
||||||
|
@ -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}}
|
||||||
|
@ -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}}
|
||||||
|
@ -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">
|
||||||
|
@ -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}}
|
||||||
|
@ -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}}>
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user