chatdesk-ui/auth_v2.169.0/internal/models/linking.go

204 lines
6.7 KiB
Go

package models
import (
"strings"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/storage"
)
// GetAccountLinkingDomain returns a string that describes the account linking
// domain. An account linking domain describes a set of Identity entities that
// _should_ generally fall under the same User entity. It's just a runtime
// string, and is not typically persisted in the database. This value can vary
// across time.
func GetAccountLinkingDomain(provider string) string {
if strings.HasPrefix(provider, "sso:") {
// when the provider ID is a SSO provider, then the linking
// domain is the provider itself i.e. there can only be one
// user + identity per identity provider
return provider
}
// otherwise, the linking domain is the default linking domain that
// links all accounts
return "default"
}
type AccountLinkingDecision = int
const (
AccountExists AccountLinkingDecision = iota
CreateAccount
LinkAccount
MultipleAccounts
)
type AccountLinkingResult struct {
Decision AccountLinkingDecision
User *User
Identities []*Identity
LinkingDomain string
CandidateEmail provider.Email
}
// DetermineAccountLinking uses the provided data and database state to compute a decision on whether:
// - A new User should be created (CreateAccount)
// - A new Identity should be created (LinkAccount) with a UserID pointing to an existing user account
// - Nothing should be done (AccountExists)
// - It's not possible to decide due to data inconsistency (MultipleAccounts) and the caller should decide
//
// Errors signal failure in processing only, like database access errors.
func DetermineAccountLinking(tx *storage.Connection, config *conf.GlobalConfiguration, emails []provider.Email, aud, providerName, sub string) (AccountLinkingResult, error) {
var verifiedEmails []string
var candidateEmail provider.Email
for _, email := range emails {
if email.Verified || config.Mailer.Autoconfirm {
verifiedEmails = append(verifiedEmails, strings.ToLower(email.Email))
}
if email.Primary {
candidateEmail = email
candidateEmail.Email = strings.ToLower(email.Email)
}
}
if identity, terr := FindIdentityByIdAndProvider(tx, sub, providerName); terr == nil {
// account exists
var user *User
if user, terr = FindUserByID(tx, identity.UserID); terr != nil {
return AccountLinkingResult{}, terr
}
// we overwrite the email with the existing user's email since the user
// could have an empty email
candidateEmail.Email = user.GetEmail()
return AccountLinkingResult{
Decision: AccountExists,
User: user,
Identities: []*Identity{identity},
LinkingDomain: GetAccountLinkingDomain(providerName),
CandidateEmail: candidateEmail,
}, nil
} else if !IsNotFoundError(terr) {
return AccountLinkingResult{}, terr
}
// the identity does not exist, so we need to check if we should create a new account
// or link to an existing one
// this is the linking domain for the new identity
candidateLinkingDomain := GetAccountLinkingDomain(providerName)
if len(verifiedEmails) == 0 {
// if there are no verified emails, we always decide to create a new account
user, terr := IsDuplicatedEmail(tx, candidateEmail.Email, aud, nil)
if terr != nil {
return AccountLinkingResult{}, terr
}
if user != nil {
candidateEmail.Email = ""
}
return AccountLinkingResult{
Decision: CreateAccount,
LinkingDomain: candidateLinkingDomain,
CandidateEmail: candidateEmail,
}, nil
}
var similarIdentities []*Identity
var similarUsers []*User
// look for similar identities and users based on email
if terr := tx.Q().Eager().Where("email = any (?)", verifiedEmails).All(&similarIdentities); terr != nil {
return AccountLinkingResult{}, terr
}
if !strings.HasPrefix(providerName, "sso:") {
// there can be multiple user accounts with the same email when is_sso_user is true
// so we just do not consider those similar user accounts
if terr := tx.Q().Eager().Where("email = any (?) and is_sso_user = false", verifiedEmails).All(&similarUsers); terr != nil {
return AccountLinkingResult{}, terr
}
}
// Need to check if the new identity should be assigned to an
// existing user or to create a new user, according to the automatic
// linking rules
var linkingIdentities []*Identity
// now let's see if there are any existing and similar identities in
// the same linking domain
for _, identity := range similarIdentities {
if GetAccountLinkingDomain(identity.Provider) == candidateLinkingDomain {
linkingIdentities = append(linkingIdentities, identity)
}
}
if len(linkingIdentities) == 0 {
if len(similarUsers) == 1 {
// no similarIdentities but a user with the same email exists
// so we link this new identity to the user
// TODO: Backfill the missing identity for the user
return AccountLinkingResult{
Decision: LinkAccount,
User: similarUsers[0],
Identities: linkingIdentities,
LinkingDomain: candidateLinkingDomain,
CandidateEmail: candidateEmail,
}, nil
} else if len(similarUsers) > 1 {
// this shouldn't happen since there is a partial unique index on (email and is_sso_user = false)
return AccountLinkingResult{
Decision: MultipleAccounts,
Identities: linkingIdentities,
LinkingDomain: candidateLinkingDomain,
CandidateEmail: candidateEmail,
}, nil
} else {
// there are no identities in the linking domain, we have to
// create a new identity and new user
return AccountLinkingResult{
Decision: CreateAccount,
LinkingDomain: candidateLinkingDomain,
CandidateEmail: candidateEmail,
}, nil
}
}
// there is at least one identity in the linking domain let's do a
// sanity check to see if all of the identities in the domain share the
// same user ID
linkingUserId := linkingIdentities[0].UserID
for _, identity := range linkingIdentities {
if identity.UserID != linkingUserId {
// ok this linking domain has more than one user account
// caller should decide what to do
return AccountLinkingResult{
Decision: MultipleAccounts,
Identities: linkingIdentities,
LinkingDomain: candidateLinkingDomain,
CandidateEmail: candidateEmail,
}, nil
}
}
// there's only one user ID in this linking domain, we can go on and
// create a new identity and link it to the existing account
var user *User
var terr error
if user, terr = FindUserByID(tx, linkingUserId); terr != nil {
return AccountLinkingResult{}, terr
}
return AccountLinkingResult{
Decision: LinkAccount,
User: user,
Identities: linkingIdentities,
LinkingDomain: candidateLinkingDomain,
CandidateEmail: candidateEmail,
}, nil
}