chatdesk-ui/auth_v2.169.0/internal/api/admin.go

643 lines
18 KiB
Go

package api
import (
"context"
"net/http"
"time"
"github.com/fatih/structs"
"github.com/go-chi/chi/v5"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/sethvargo/go-password/password"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/observability"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/utilities"
"golang.org/x/crypto/bcrypt"
)
type AdminUserParams struct {
Id string `json:"id"`
Aud string `json:"aud"`
Role string `json:"role"`
Email string `json:"email"`
Phone string `json:"phone"`
Password *string `json:"password"`
PasswordHash string `json:"password_hash"`
EmailConfirm bool `json:"email_confirm"`
PhoneConfirm bool `json:"phone_confirm"`
UserMetaData map[string]interface{} `json:"user_metadata"`
AppMetaData map[string]interface{} `json:"app_metadata"`
BanDuration string `json:"ban_duration"`
}
type adminUserDeleteParams struct {
ShouldSoftDelete bool `json:"should_soft_delete"`
}
type adminUserUpdateFactorParams struct {
FriendlyName string `json:"friendly_name"`
Phone string `json:"phone"`
}
type AdminListUsersResponse struct {
Users []*models.User `json:"users"`
Aud string `json:"aud"`
}
func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
db := a.db.WithContext(ctx)
userID, err := uuid.FromString(chi.URLParam(r, "user_id"))
if err != nil {
return nil, notFoundError(ErrorCodeValidationFailed, "user_id must be an UUID")
}
observability.LogEntrySetField(r, "user_id", userID)
u, err := models.FindUserByID(db, userID)
if err != nil {
if models.IsNotFoundError(err) {
return nil, notFoundError(ErrorCodeUserNotFound, "User not found")
}
return nil, internalServerError("Database error loading user").WithInternalError(err)
}
return withUser(ctx, u), nil
}
// Use only after requireAuthentication, so that there is a valid user
func (a *API) loadFactor(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
db := a.db.WithContext(ctx)
user := getUser(ctx)
factorID, err := uuid.FromString(chi.URLParam(r, "factor_id"))
if err != nil {
return nil, notFoundError(ErrorCodeValidationFailed, "factor_id must be an UUID")
}
observability.LogEntrySetField(r, "factor_id", factorID)
factor, err := user.FindOwnedFactorByID(db, factorID)
if err != nil {
if models.IsNotFoundError(err) {
return nil, notFoundError(ErrorCodeMFAFactorNotFound, "Factor not found")
}
return nil, internalServerError("Database error loading factor").WithInternalError(err)
}
return withFactor(ctx, factor), nil
}
func (a *API) getAdminParams(r *http.Request) (*AdminUserParams, error) {
params := &AdminUserParams{}
if err := retrieveRequestParams(r, params); err != nil {
return nil, err
}
return params, nil
}
// adminUsers responds with a list of all users in a given audience
func (a *API) adminUsers(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
aud := a.requestAud(ctx, r)
pageParams, err := paginate(r)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Bad Pagination Parameters: %v", err).WithInternalError(err)
}
sortParams, err := sort(r, map[string]bool{models.CreatedAt: true}, []models.SortField{{Name: models.CreatedAt, Dir: models.Descending}})
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Bad Sort Parameters: %v", err)
}
filter := r.URL.Query().Get("filter")
users, err := models.FindUsersInAudience(db, aud, pageParams, sortParams, filter)
if err != nil {
return internalServerError("Database error finding users").WithInternalError(err)
}
addPaginationHeaders(w, r, pageParams)
return sendJSON(w, http.StatusOK, AdminListUsersResponse{
Users: users,
Aud: aud,
})
}
// adminUserGet returns information about a single user
func (a *API) adminUserGet(w http.ResponseWriter, r *http.Request) error {
user := getUser(r.Context())
return sendJSON(w, http.StatusOK, user)
}
// adminUserUpdate updates a single user object
func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
config := a.config
user := getUser(ctx)
adminUser := getAdminUser(ctx)
params, err := a.getAdminParams(r)
if err != nil {
return err
}
if params.Email != "" {
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
}
if params.Phone != "" {
params.Phone, err = validatePhone(params.Phone)
if err != nil {
return err
}
}
var banDuration *time.Duration
if params.BanDuration != "" {
duration := time.Duration(0)
if params.BanDuration != "none" {
duration, err = time.ParseDuration(params.BanDuration)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "invalid format for ban duration: %v", err)
}
}
banDuration = &duration
}
if params.Password != nil {
password := *params.Password
if err := a.checkPasswordStrength(ctx, password); err != nil {
return err
}
if err := user.SetPassword(ctx, password, config.Security.DBEncryption.Encrypt, config.Security.DBEncryption.EncryptionKeyID, config.Security.DBEncryption.EncryptionKey); err != nil {
return err
}
}
err = db.Transaction(func(tx *storage.Connection) error {
if params.Role != "" {
if terr := user.SetRole(tx, params.Role); terr != nil {
return terr
}
}
if params.EmailConfirm {
if terr := user.Confirm(tx); terr != nil {
return terr
}
}
if params.PhoneConfirm {
if terr := user.ConfirmPhone(tx); terr != nil {
return terr
}
}
if params.Password != nil {
if terr := user.UpdatePassword(tx, nil); terr != nil {
return terr
}
}
var identities []models.Identity
if params.Email != "" {
if identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), "email"); terr != nil && !models.IsNotFoundError(terr) {
return terr
} else if identity == nil {
// if the user doesn't have an existing email
// then updating the user's email should create a new email identity
i, terr := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
Subject: user.ID.String(),
Email: params.Email,
EmailVerified: params.EmailConfirm,
}))
if terr != nil {
return terr
}
identities = append(identities, *i)
} else {
// update the existing email identity
if terr := identity.UpdateIdentityData(tx, map[string]interface{}{
"email": params.Email,
"email_verified": params.EmailConfirm,
}); terr != nil {
return terr
}
}
if user.IsAnonymous && params.EmailConfirm {
user.IsAnonymous = false
if terr := tx.UpdateOnly(user, "is_anonymous"); terr != nil {
return terr
}
}
if terr := user.SetEmail(tx, params.Email); terr != nil {
return terr
}
}
if params.Phone != "" {
if identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), "phone"); terr != nil && !models.IsNotFoundError(terr) {
return terr
} else if identity == nil {
// if the user doesn't have an existing phone
// then updating the user's phone should create a new phone identity
identity, terr := a.createNewIdentity(tx, user, "phone", structs.Map(provider.Claims{
Subject: user.ID.String(),
Phone: params.Phone,
PhoneVerified: params.PhoneConfirm,
}))
if terr != nil {
return terr
}
identities = append(identities, *identity)
} else {
// update the existing phone identity
if terr := identity.UpdateIdentityData(tx, map[string]interface{}{
"phone": params.Phone,
"phone_verified": params.PhoneConfirm,
}); terr != nil {
return terr
}
}
if user.IsAnonymous && params.PhoneConfirm {
user.IsAnonymous = false
if terr := tx.UpdateOnly(user, "is_anonymous"); terr != nil {
return terr
}
}
if terr := user.SetPhone(tx, params.Phone); terr != nil {
return terr
}
}
user.Identities = append(user.Identities, identities...)
if params.AppMetaData != nil {
if terr := user.UpdateAppMetaData(tx, params.AppMetaData); terr != nil {
return terr
}
}
if params.UserMetaData != nil {
if terr := user.UpdateUserMetaData(tx, params.UserMetaData); terr != nil {
return terr
}
}
if banDuration != nil {
if terr := user.Ban(tx, *banDuration); terr != nil {
return terr
}
}
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserModifiedAction, "", map[string]interface{}{
"user_id": user.ID,
"user_email": user.Email,
"user_phone": user.Phone,
}); terr != nil {
return terr
}
return nil
})
if err != nil {
return internalServerError("Error updating user").WithInternalError(err)
}
return sendJSON(w, http.StatusOK, user)
}
// adminUserCreate creates a new user based on the provided data
func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
config := a.config
adminUser := getAdminUser(ctx)
params, err := a.getAdminParams(r)
if err != nil {
return err
}
aud := a.requestAud(ctx, r)
if params.Aud != "" {
aud = params.Aud
}
if params.Email == "" && params.Phone == "" {
return badRequestError(ErrorCodeValidationFailed, "Cannot create a user without either an email or phone")
}
var providers []string
if params.Email != "" {
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
if user, err := models.IsDuplicatedEmail(db, params.Email, aud, nil); err != nil {
return internalServerError("Database error checking email").WithInternalError(err)
} else if user != nil {
return unprocessableEntityError(ErrorCodeEmailExists, DuplicateEmailMsg)
}
providers = append(providers, "email")
}
if params.Phone != "" {
params.Phone, err = validatePhone(params.Phone)
if err != nil {
return err
}
if exists, err := models.IsDuplicatedPhone(db, params.Phone, aud); err != nil {
return internalServerError("Database error checking phone").WithInternalError(err)
} else if exists {
return unprocessableEntityError(ErrorCodePhoneExists, "Phone number already registered by another user")
}
providers = append(providers, "phone")
}
if params.Password != nil && params.PasswordHash != "" {
return badRequestError(ErrorCodeValidationFailed, "Only a password or a password hash should be provided")
}
if (params.Password == nil || *params.Password == "") && params.PasswordHash == "" {
password, err := password.Generate(64, 10, 0, false, true)
if err != nil {
return internalServerError("Error generating password").WithInternalError(err)
}
params.Password = &password
}
var user *models.User
if params.PasswordHash != "" {
user, err = models.NewUserWithPasswordHash(params.Phone, params.Email, params.PasswordHash, aud, params.UserMetaData)
} else {
user, err = models.NewUser(params.Phone, params.Email, *params.Password, aud, params.UserMetaData)
}
if err != nil {
if errors.Is(err, bcrypt.ErrPasswordTooLong) {
return badRequestError(ErrorCodeValidationFailed, err.Error())
}
return internalServerError("Error creating user").WithInternalError(err)
}
if params.Id != "" {
customId, err := uuid.FromString(params.Id)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "ID must conform to the uuid v4 format")
}
if customId == uuid.Nil {
return badRequestError(ErrorCodeValidationFailed, "ID cannot be a nil uuid")
}
user.ID = customId
}
user.AppMetaData = map[string]interface{}{
// TODO: Deprecate "provider" field
// default to the first provider in the providers slice
"provider": providers[0],
"providers": providers,
}
var banDuration *time.Duration
if params.BanDuration != "" {
duration := time.Duration(0)
if params.BanDuration != "none" {
duration, err = time.ParseDuration(params.BanDuration)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "invalid format for ban duration: %v", err)
}
}
banDuration = &duration
}
err = db.Transaction(func(tx *storage.Connection) error {
if terr := tx.Create(user); terr != nil {
return terr
}
var identities []models.Identity
if user.GetEmail() != "" {
identity, terr := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
Subject: user.ID.String(),
Email: user.GetEmail(),
}))
if terr != nil {
return terr
}
identities = append(identities, *identity)
}
if user.GetPhone() != "" {
identity, terr := a.createNewIdentity(tx, user, "phone", structs.Map(provider.Claims{
Subject: user.ID.String(),
Phone: user.GetPhone(),
}))
if terr != nil {
return terr
}
identities = append(identities, *identity)
}
user.Identities = identities
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserSignedUpAction, "", map[string]interface{}{
"user_id": user.ID,
"user_email": user.Email,
"user_phone": user.Phone,
}); terr != nil {
return terr
}
role := config.JWT.DefaultGroupName
if params.Role != "" {
role = params.Role
}
if terr := user.SetRole(tx, role); terr != nil {
return terr
}
if params.AppMetaData != nil {
if terr := user.UpdateAppMetaData(tx, params.AppMetaData); terr != nil {
return terr
}
}
if params.EmailConfirm {
if terr := user.Confirm(tx); terr != nil {
return terr
}
}
if params.PhoneConfirm {
if terr := user.ConfirmPhone(tx); terr != nil {
return terr
}
}
if banDuration != nil {
if terr := user.Ban(tx, *banDuration); terr != nil {
return terr
}
}
return nil
})
if err != nil {
return internalServerError("Database error creating new user").WithInternalError(err)
}
return sendJSON(w, http.StatusOK, user)
}
// adminUserDelete deletes a user
func (a *API) adminUserDelete(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
adminUser := getAdminUser(ctx)
// ShouldSoftDelete defaults to false
params := &adminUserDeleteParams{}
if body, _ := utilities.GetBodyBytes(r); len(body) != 0 {
// we only want to parse the body if it's not empty
// retrieveRequestParams will handle any errors with stream
if err := retrieveRequestParams(r, params); err != nil {
return err
}
}
err := a.db.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserDeletedAction, "", map[string]interface{}{
"user_id": user.ID,
"user_email": user.Email,
"user_phone": user.Phone,
}); terr != nil {
return internalServerError("Error recording audit log entry").WithInternalError(terr)
}
if params.ShouldSoftDelete {
if user.DeletedAt != nil {
// user has been soft deleted already
return nil
}
if terr := user.SoftDeleteUser(tx); terr != nil {
return internalServerError("Error soft deleting user").WithInternalError(terr)
}
if terr := user.SoftDeleteUserIdentities(tx); terr != nil {
return internalServerError("Error soft deleting user identities").WithInternalError(terr)
}
// hard delete all associated factors
if terr := models.DeleteFactorsByUserId(tx, user.ID); terr != nil {
return internalServerError("Error deleting user's factors").WithInternalError(terr)
}
// hard delete all associated sessions
if terr := models.Logout(tx, user.ID); terr != nil {
return internalServerError("Error deleting user's sessions").WithInternalError(terr)
}
} else {
if terr := tx.Destroy(user); terr != nil {
return internalServerError("Database error deleting user").WithInternalError(terr)
}
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, map[string]interface{}{})
}
func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
factor := getFactor(ctx)
err := a.db.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(r, tx, user, models.DeleteFactorAction, r.RemoteAddr, map[string]interface{}{
"user_id": user.ID,
"factor_id": factor.ID,
}); terr != nil {
return terr
}
if terr := tx.Destroy(factor); terr != nil {
return internalServerError("Database error deleting factor").WithInternalError(terr)
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, factor)
}
func (a *API) adminUserGetFactors(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
return sendJSON(w, http.StatusOK, user.Factors)
}
// adminUserUpdate updates a single factor object
func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
factor := getFactor(ctx)
user := getUser(ctx)
adminUser := getAdminUser(ctx)
params := &adminUserUpdateFactorParams{}
if err := retrieveRequestParams(r, params); err != nil {
return err
}
err := a.db.Transaction(func(tx *storage.Connection) error {
if params.FriendlyName != "" {
if terr := factor.UpdateFriendlyName(tx, params.FriendlyName); terr != nil {
return terr
}
}
if params.Phone != "" && factor.IsPhoneFactor() {
phone, err := validatePhone(params.Phone)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Invalid phone number format (E.164 required)")
}
if terr := factor.UpdatePhone(tx, phone); terr != nil {
return terr
}
}
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UpdateFactorAction, "", map[string]interface{}{
"user_id": user.ID,
"factor_id": factor.ID,
"factor_type": factor.FactorType,
}); terr != nil {
return terr
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, factor)
}