summaryrefslogtreecommitdiffstats
path: root/services/remote/promote.go
blob: eb41ace46270e4a57fed4a9d658c43d1327d286c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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
}