2024-02-25 10:32:59 +00:00
// 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
//
2024-05-09 13:49:37 +00:00
// If the source from which an authentication happens is OAuth2, an existing
2024-02-25 10:32:59 +00:00
// 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
}
2024-06-11 18:47:45 +00:00
func getRemoteUserToPromote ( ctx context . Context , source * auth_model . Source , loginName , email string ) ( * user_model . User , Reason , error ) { //nolint:unparam
2024-02-25 10:32:59 +00:00
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
}