mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 06:21:11 +00:00 
			
		
		
		
	It is not an error for a remote user to not be promoted: this is the case for all users created via OAuth. Displaying an error is confusing to the admin when seen in the logs. Refs: https://codeberg.org/forgejo/forgejo/issues/7681 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7687 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Earl Warren <contact@earl-warren.org> Co-committed-by: Earl Warren <contact@earl-warren.org>
		
			
				
	
	
		
			133 lines
		
	
	
	
		
			4.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			133 lines
		
	
	
	
		
			4.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright Earl Warren <contact@earl-warren.org>
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package remote
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 
 | |
| 	auth_model "forgejo.org/models/auth"
 | |
| 	"forgejo.org/models/db"
 | |
| 	user_model "forgejo.org/models/user"
 | |
| 	"forgejo.org/modules/log"
 | |
| 	"forgejo.org/services/auth/source/oauth2"
 | |
| 	remote_source "forgejo.org/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.DEBUG, 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
 | |
| }
 |