summaryrefslogtreecommitdiffstats
path: root/services/remote/promote.go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /services/remote/promote.go
parentInitial commit. (diff)
downloadforgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.tar.xz
forgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.zip
Adding upstream version 9.0.0.upstream/9.0.0upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'services/remote/promote.go')
-rw-r--r--services/remote/promote.go133
1 files changed, 133 insertions, 0 deletions
diff --git a/services/remote/promote.go b/services/remote/promote.go
new file mode 100644
index 0000000..eb41ace
--- /dev/null
+++ b/services/remote/promote.go
@@ -0,0 +1,133 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// SPDX-License-Identifier: MIT
+
+package remote
+
+import (
+ "context"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/auth/source/oauth2"
+ remote_source "code.gitea.io/gitea/services/auth/source/remote"
+)
+
+type Reason int
+
+const (
+ ReasonNoMatch Reason = iota
+ ReasonNotAuth2
+ ReasonBadAuth2
+ ReasonLoginNameNotExists
+ ReasonNotRemote
+ ReasonEmailIsSet
+ ReasonNoSource
+ ReasonSourceWrongType
+ ReasonCanPromote
+ ReasonPromoted
+ ReasonUpdateFail
+ ReasonErrorLoginName
+ ReasonErrorGetSource
+)
+
+func NewReason(level log.Level, reason Reason, message string, args ...any) Reason {
+ log.Log(1, level, message, args...)
+ return reason
+}
+
+func getUsersByLoginName(ctx context.Context, name string) ([]*user_model.User, error) {
+ if len(name) == 0 {
+ return nil, user_model.ErrUserNotExist{Name: name}
+ }
+
+ users := make([]*user_model.User, 0, 5)
+
+ return users, db.GetEngine(ctx).
+ Table("user").
+ Where("login_name = ? AND login_type = ? AND type = ?", name, auth_model.Remote, user_model.UserTypeRemoteUser).
+ Find(&users)
+}
+
+// The remote user has:
+//
+// Type UserTypeRemoteUser
+// LogingType Remote
+// LoginName set to the unique identifier of the originating authentication source
+// LoginSource set to the Remote source that can be matched against an OAuth2 source
+//
+// If the source from which an authentication happens is OAuth2, an existing
+// remote user will be promoted to an OAuth2 user provided:
+//
+// user.LoginName is the same as goth.UserID (argument loginName)
+// user.LoginSource has a MatchingSource equals to the name of the OAuth2 provider
+//
+// Once promoted, the user will be logged in without further interaction from the
+// user and will own all repositories, issues, etc. associated with it.
+func MaybePromoteRemoteUser(ctx context.Context, source *auth_model.Source, loginName, email string) (promoted bool, reason Reason, err error) {
+ user, reason, err := getRemoteUserToPromote(ctx, source, loginName, email)
+ if err != nil || user == nil {
+ return false, reason, err
+ }
+ promote := &user_model.User{
+ ID: user.ID,
+ Type: user_model.UserTypeIndividual,
+ Email: email,
+ LoginSource: source.ID,
+ LoginType: source.Type,
+ }
+ reason = NewReason(log.DEBUG, ReasonPromoted, "promote user %v: LoginName %v => %v, LoginSource %v => %v, LoginType %v => %v, Email %v => %v", user.ID, user.LoginName, promote.LoginName, user.LoginSource, promote.LoginSource, user.LoginType, promote.LoginType, user.Email, promote.Email)
+ if err := user_model.UpdateUserCols(ctx, promote, "type", "email", "login_source", "login_type"); err != nil {
+ return false, ReasonUpdateFail, err
+ }
+ return true, reason, nil
+}
+
+func getRemoteUserToPromote(ctx context.Context, source *auth_model.Source, loginName, email string) (*user_model.User, Reason, error) { //nolint:unparam
+ if !source.IsOAuth2() {
+ return nil, NewReason(log.DEBUG, ReasonNotAuth2, "source %v is not OAuth2", source), nil
+ }
+ oauth2Source, ok := source.Cfg.(*oauth2.Source)
+ if !ok {
+ return nil, NewReason(log.ERROR, ReasonBadAuth2, "source claims to be OAuth2 but is not"), nil
+ }
+
+ users, err := getUsersByLoginName(ctx, loginName)
+ if err != nil {
+ return nil, NewReason(log.ERROR, ReasonErrorLoginName, "getUserByLoginName('%s') %v", loginName, err), err
+ }
+ if len(users) == 0 {
+ return nil, NewReason(log.ERROR, ReasonLoginNameNotExists, "no user with LoginType UserTypeRemoteUser and LoginName '%s'", loginName), nil
+ }
+
+ reason := ReasonNoSource
+ for _, u := range users {
+ userSource, err := auth_model.GetSourceByID(ctx, u.LoginSource)
+ if err != nil {
+ if auth_model.IsErrSourceNotExist(err) {
+ reason = NewReason(log.DEBUG, ReasonNoSource, "source id = %v for user %v not found %v", u.LoginSource, u.ID, err)
+ continue
+ }
+ return nil, NewReason(log.ERROR, ReasonErrorGetSource, "GetSourceByID('%s') %v", u.LoginSource, err), err
+ }
+ if u.Email != "" {
+ reason = NewReason(log.DEBUG, ReasonEmailIsSet, "the user email is already set to '%s'", u.Email)
+ continue
+ }
+ remoteSource, ok := userSource.Cfg.(*remote_source.Source)
+ if !ok {
+ reason = NewReason(log.DEBUG, ReasonSourceWrongType, "expected a remote source but got %T %v", userSource, userSource)
+ continue
+ }
+
+ if oauth2Source.Provider != remoteSource.MatchingSource {
+ reason = NewReason(log.DEBUG, ReasonNoMatch, "skip OAuth2 source %s because it is different from %s which is the expected match for the remote source %s", oauth2Source.Provider, remoteSource.MatchingSource, remoteSource.URL)
+ continue
+ }
+
+ return u, ReasonCanPromote, nil
+ }
+
+ return nil, reason, nil
+}