134 lines
4.7 KiB
Go
134 lines
4.7 KiB
Go
// 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) {
|
|
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
|
|
}
|