mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CLI: Remove manager-users
conflict users cli (#95135)
* Remove conflict_user cli * Delete pkg/build/cmd/exportversion.go
This commit is contained in:
parent
986c024dd7
commit
fe6ec1258f
@ -167,63 +167,6 @@ var adminCommands = []*cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "user-manager",
|
|
||||||
Usage: "Runs different helpful user commands",
|
|
||||||
Subcommands: []*cli.Command{
|
|
||||||
// TODO: reset password for user
|
|
||||||
{
|
|
||||||
Name: "conflicts",
|
|
||||||
Usage: "runs a conflict resolution to find users with multiple entries",
|
|
||||||
CustomHelpTemplate: `
|
|
||||||
This command will find users with multiple entries in the database and try to resolve the conflicts.
|
|
||||||
explanation of each field:
|
|
||||||
|
|
||||||
explanation of each field:
|
|
||||||
* email - the user’s email
|
|
||||||
* login - the user’s login/username
|
|
||||||
* last_seen_at - the user’s last login
|
|
||||||
* auth_module - if the user was created/signed in using an authentication provider
|
|
||||||
* conflict_email - a boolean if we consider the email to be a conflict
|
|
||||||
* conflict_login - a boolean if we consider the login to be a conflict
|
|
||||||
|
|
||||||
# lists all the conflicting users
|
|
||||||
grafana-cli user-manager conflicts list
|
|
||||||
|
|
||||||
# creates a conflict patch file to edit
|
|
||||||
grafana-cli user-manager conflicts generate-file
|
|
||||||
|
|
||||||
# reads edited conflict patch file for validation
|
|
||||||
grafana-cli user-manager conflicts validate-file <filepath>
|
|
||||||
|
|
||||||
# validates and ingests edited patch file
|
|
||||||
grafana-cli user-manager conflicts ingest-file <filepath>
|
|
||||||
`,
|
|
||||||
Subcommands: []*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "list",
|
|
||||||
Usage: "returns a list of users with more than one entry in the database",
|
|
||||||
Action: runListConflictUsers(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "generate-file",
|
|
||||||
Usage: "creates a conflict users file. Safe to execute multiple times.",
|
|
||||||
Action: runGenerateConflictUsersFile(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "validate-file",
|
|
||||||
Usage: "validates the conflict users file. Safe to execute multiple times.",
|
|
||||||
Action: runValidateConflictUsersFile(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "ingest-file",
|
|
||||||
Usage: "ingests the conflict users file. > Note: This is irreversible it will change the state of the database.",
|
|
||||||
Action: runIngestConflictUsersFile(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var Commands = []*cli.Command{
|
var Commands = []*cli.Command{
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
grafana = {
|
|
||||||
source = "grafana/grafana"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure the Grafana Provider
|
|
||||||
provider "grafana" {
|
|
||||||
url = "http://localhost:3000/"
|
|
||||||
auth = "admin:admin"
|
|
||||||
}
|
|
||||||
|
|
||||||
// login conflict
|
|
||||||
// Creating the grafana-login
|
|
||||||
resource "grafana_user" "grafana-login" {
|
|
||||||
email = "grafana_login@grafana.com"
|
|
||||||
login = "GRAFANA_LOGIN"
|
|
||||||
password = "grafana_login@grafana.com"
|
|
||||||
is_admin = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creating the grafana-login
|
|
||||||
resource "grafana_user" "grafana-login-2" {
|
|
||||||
email = "grafana_login_2@grafana.com"
|
|
||||||
login = "grafana_login"
|
|
||||||
password = "grafana_login@grafana.com"
|
|
||||||
is_admin = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// email conflict
|
|
||||||
// Creating the grafana-email
|
|
||||||
resource "grafana_user" "grafana-email" {
|
|
||||||
email = "grafana_email@grafana.com"
|
|
||||||
login = "user_login_a"
|
|
||||||
password = "grafana_email@grafana.com"
|
|
||||||
is_admin = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creating the grafana-email
|
|
||||||
resource "grafana_user" "grafana-email-2" {
|
|
||||||
email = "GRAFANA_EMAIL@grafana.com"
|
|
||||||
login = "user_login_b"
|
|
||||||
password = "grafana_email@grafana.com"
|
|
||||||
is_admin = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// email and login conflict
|
|
||||||
// Creating the grafana-user
|
|
||||||
resource "grafana_user" "grafana-user" {
|
|
||||||
email = "grafana_user@grafana.com"
|
|
||||||
login = "grafana_user"
|
|
||||||
password = "grafana_user@grafana.com"
|
|
||||||
is_admin = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creating the grafana-user
|
|
||||||
resource "grafana_user" "grafana-user-2" {
|
|
||||||
email = "GRAFANA_USER@grafana.com"
|
|
||||||
login = "GRAFANA_USER"
|
|
||||||
password = "grafana_user@grafana.com"
|
|
||||||
is_admin = false
|
|
||||||
}
|
|
@ -1,802 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
|
||||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
|
||||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initConflictCfg(cmd *utils.ContextCommandLine) (*setting.Cfg, tracing.Tracer, featuremgmt.FeatureToggles, error) {
|
|
||||||
configOptions := strings.Split(cmd.String("configOverrides"), " ")
|
|
||||||
configOptions = append(configOptions, cmd.Args().Slice()...)
|
|
||||||
cfg, err := setting.NewCfgFromArgs(setting.CommandLineArgs{
|
|
||||||
Config: cmd.ConfigFile(),
|
|
||||||
HomePath: cmd.HomePath(),
|
|
||||||
Args: append(configOptions, "cfg:log.level=error"), // tailing arguments have precedence over the options string
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
features, err := featuremgmt.ProvideManagerService(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tracingCfg, err := tracing.ProvideTracingConfig(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, fmt.Errorf("%v: %w", "failed to initialize tracer config", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tracer, err := tracing.ProvideService(tracingCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, fmt.Errorf("%v: %w", "failed to initialize tracer service", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, tracer, features, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx *cli.Context) (*ConflictResolver, error) {
|
|
||||||
cfg, tracer, features, err := initConflictCfg(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to load configuration", err)
|
|
||||||
}
|
|
||||||
s, err := getSqlStore(cfg, tracer, features)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to get to sql", err)
|
|
||||||
}
|
|
||||||
conflicts, err := GetUsersWithConflictingEmailsOrLogins(ctx, s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to get users with conflicting logins", err)
|
|
||||||
}
|
|
||||||
quotaService := quotaimpl.ProvideService(s, cfg)
|
|
||||||
userService, err := userimpl.ProvideService(s, nil, cfg, nil, nil, tracer, quotaService, supportbundlestest.NewFakeBundleService())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to get user service", err)
|
|
||||||
}
|
|
||||||
routing := routing.ProvideRegister()
|
|
||||||
|
|
||||||
acService, err := acimpl.ProvideService(cfg, s, routing, nil, nil, nil, features, tracer, zanzana.NewNoopClient(), permreg.ProvidePermissionRegistry(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to get access control", err)
|
|
||||||
}
|
|
||||||
resolver := ConflictResolver{Users: conflicts, Store: s, userService: userService, ac: acService}
|
|
||||||
resolver.BuildConflictBlocks(conflicts, f)
|
|
||||||
return &resolver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSqlStore(cfg *setting.Cfg, tracer tracing.Tracer, features featuremgmt.FeatureToggles) (*sqlstore.SQLStore, error) {
|
|
||||||
bus := bus.ProvideBus(tracer)
|
|
||||||
return sqlstore.ProvideService(cfg, features, &migrations.OSSMigrations{}, bus, tracer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runListConflictUsers() func(context *cli.Context) error {
|
|
||||||
return func(context *cli.Context) error {
|
|
||||||
cmd := &utils.ContextCommandLine{Context: context}
|
|
||||||
whiteBold := color.New(color.FgWhite).Add(color.Bold)
|
|
||||||
r, err := initializeConflictResolver(cmd, whiteBold.Sprintf, context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%v: %w", "failed to initialize conflict resolver", err)
|
|
||||||
}
|
|
||||||
if len(r.Users) < 1 {
|
|
||||||
logger.Info(color.GreenString("No Conflicting users found.\n\n"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
logger.Info("\n\nShowing conflicts\n\n")
|
|
||||||
logger.Info(r.ToStringPresentation())
|
|
||||||
logger.Info("\n")
|
|
||||||
if len(r.DiscardedBlocks) != 0 {
|
|
||||||
r.logDiscardedUsers()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runGenerateConflictUsersFile() func(context *cli.Context) error {
|
|
||||||
return func(context *cli.Context) error {
|
|
||||||
cmd := &utils.ContextCommandLine{Context: context}
|
|
||||||
r, err := initializeConflictResolver(cmd, fmt.Sprintf, context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%v: %w", "failed to initialize conflict resolver", err)
|
|
||||||
}
|
|
||||||
if len(r.Users) < 1 {
|
|
||||||
logger.Info(color.GreenString("No Conflicting users found.\n\n"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tmpFile, err := generateConflictUsersFile(r)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generating file return error: %w", err)
|
|
||||||
}
|
|
||||||
logger.Infof("\n\ngenerated file\n")
|
|
||||||
logger.Infof("%s\n\n", tmpFile.Name())
|
|
||||||
logger.Infof("once the file is edited and resolved conflicts, you can either validate or ingest the file\n\n")
|
|
||||||
if len(r.DiscardedBlocks) != 0 {
|
|
||||||
r.logDiscardedUsers()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runValidateConflictUsersFile() func(context *cli.Context) error {
|
|
||||||
return func(context *cli.Context) error {
|
|
||||||
cmd := &utils.ContextCommandLine{Context: context}
|
|
||||||
r, err := initializeConflictResolver(cmd, fmt.Sprintf, context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%v: %w", "failed to initialize conflict resolver", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read in the file to validate
|
|
||||||
// read in the file to ingest
|
|
||||||
arg := cmd.Args().First()
|
|
||||||
if arg == "" {
|
|
||||||
return fmt.Errorf("please specify a absolute path to file to read from")
|
|
||||||
}
|
|
||||||
b, err := os.ReadFile(filepath.Clean(arg))
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(color.RedString("validation failed with an error"))
|
|
||||||
return fmt.Errorf("could not read file with error %s", err)
|
|
||||||
}
|
|
||||||
validErr := getValidConflictUsers(r, b)
|
|
||||||
if validErr != nil {
|
|
||||||
logger.Error(color.RedString("validation failed with an error"))
|
|
||||||
return fmt.Errorf("could not validate file with error:\n%s", validErr)
|
|
||||||
}
|
|
||||||
logger.Info(color.GreenString("File validation complete.\n"))
|
|
||||||
logger.Info("File can be used with the `ingest-file` command.\n\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runIngestConflictUsersFile() func(context *cli.Context) error {
|
|
||||||
return func(context *cli.Context) error {
|
|
||||||
cmd := &utils.ContextCommandLine{Context: context}
|
|
||||||
r, err := initializeConflictResolver(cmd, fmt.Sprintf, context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%v: %w", "failed to initialize conflict resolver", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read in the file to ingest
|
|
||||||
arg := cmd.Args().First()
|
|
||||||
if arg == "" {
|
|
||||||
return errors.New("please specify a absolute path to file to read from")
|
|
||||||
}
|
|
||||||
b, err := os.ReadFile(filepath.Clean(arg))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not read file with error %e", err)
|
|
||||||
}
|
|
||||||
validErr := getValidConflictUsers(r, b)
|
|
||||||
if validErr != nil {
|
|
||||||
return fmt.Errorf("could not validate file with error:\n%s", validErr)
|
|
||||||
}
|
|
||||||
// should we rebuild blocks here?
|
|
||||||
// kind of a weird thing maybe?
|
|
||||||
if len(r.ValidUsers) == 0 {
|
|
||||||
return fmt.Errorf("no users")
|
|
||||||
}
|
|
||||||
r.showChanges()
|
|
||||||
if !confirm("\n\nWe encourage users to create a db backup before running this command. \n Proceed with operation") {
|
|
||||||
return fmt.Errorf("user cancelled")
|
|
||||||
}
|
|
||||||
err = r.MergeConflictingUsers(context.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("not able to merge with %e", err)
|
|
||||||
}
|
|
||||||
logger.Info("\n\nconflicts resolved.\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDocumentationForFile() string {
|
|
||||||
return `# Conflicts File
|
|
||||||
# This file is generated by the grafana-cli command ` + color.CyanString("grafana-cli admin user-manager conflicts generate-file") + `.
|
|
||||||
#
|
|
||||||
# Commands:
|
|
||||||
# +, keep <user> = keep user
|
|
||||||
# -, delete <user> = delete user
|
|
||||||
#
|
|
||||||
# The fields conflict_email and conflict_login
|
|
||||||
# indicate that we see a conflict in email and/or login with another user.
|
|
||||||
# Both these fields can be true.
|
|
||||||
#
|
|
||||||
# There needs to be exactly one picked user per conflict block.
|
|
||||||
#
|
|
||||||
# The lines can be re-ordered.
|
|
||||||
#
|
|
||||||
# If you feel like you want to wait with a specific block,
|
|
||||||
# delete all lines regarding that conflict block.
|
|
||||||
# email - the user’s email
|
|
||||||
# login - the user’s login/username
|
|
||||||
# last_seen_at - the user’s last login
|
|
||||||
# auth_module - if the user was created/signed in using an authentication provider
|
|
||||||
# conflict_email - a boolean if we consider the email to be a conflict
|
|
||||||
# conflict_login - a boolean if we consider the login to be a conflict
|
|
||||||
#
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateConflictUsersFile(r *ConflictResolver) (*os.File, error) {
|
|
||||||
tmpFile, err := os.CreateTemp(os.TempDir(), "conflicting_user_*.diff")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := tmpFile.WriteString(getDocumentationForFile()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := tmpFile.WriteString(r.ToStringPresentation()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tmpFile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getValidConflictUsers(r *ConflictResolver, b []byte) error {
|
|
||||||
newConflicts := make(ConflictingUsers, 0)
|
|
||||||
// need to verify that id or email exists
|
|
||||||
previouslySeenIds := map[string]bool{}
|
|
||||||
previouslySeenEmails := map[string]bool{}
|
|
||||||
previouslySeenLogins := map[string]bool{}
|
|
||||||
for _, users := range r.Blocks {
|
|
||||||
for _, u := range users {
|
|
||||||
previouslySeenIds[strings.ToLower(u.ID)] = true
|
|
||||||
previouslySeenEmails[strings.ToLower(u.Email)] = true
|
|
||||||
previouslySeenLogins[strings.ToLower(u.Login)] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// tested in https://regex101.com/r/una3zC/1
|
|
||||||
diffPattern := `^[+-]`
|
|
||||||
// compiling since in a loop
|
|
||||||
matchingExpression, err := regexp.Compile(diffPattern)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to compile regex %s: %w", diffPattern, err)
|
|
||||||
}
|
|
||||||
counterKeepUsersForBlock := map[string]int{}
|
|
||||||
counterDeleteUsersForBlock := map[string]int{}
|
|
||||||
currentBlock := ""
|
|
||||||
for rowNumber, row := range strings.Split(string(b), "\n") {
|
|
||||||
// end of file
|
|
||||||
if row == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// if the row starts with a #, it is a comment
|
|
||||||
if row[0] == '#' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
entryRow := matchingExpression.MatchString(row)
|
|
||||||
// not an entry row -> is a conflict block row
|
|
||||||
if !entryRow {
|
|
||||||
// check for malformed row
|
|
||||||
// rows should be of the form
|
|
||||||
// conflict: <conflict>
|
|
||||||
// or
|
|
||||||
// + id: <id>
|
|
||||||
// - id: <id>
|
|
||||||
if (row[0] != '-') && (row[0] != '+') && (row[0] != 'c') {
|
|
||||||
return fmt.Errorf("invalid start character (expected '+,-') found %c for row number %d", row[0], rowNumber+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// is a conflict block row
|
|
||||||
// conflict: hej
|
|
||||||
currentBlock = row
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// need to track how many keep users we have for a block
|
|
||||||
if _, ok := counterKeepUsersForBlock[currentBlock]; !ok {
|
|
||||||
counterKeepUsersForBlock[currentBlock] = 0
|
|
||||||
}
|
|
||||||
if _, ok := counterDeleteUsersForBlock[currentBlock]; !ok {
|
|
||||||
counterDeleteUsersForBlock[currentBlock] = 0
|
|
||||||
}
|
|
||||||
if row[0] == '+' {
|
|
||||||
counterKeepUsersForBlock[currentBlock] += 1
|
|
||||||
}
|
|
||||||
if row[0] == '-' {
|
|
||||||
counterDeleteUsersForBlock[currentBlock] += 1
|
|
||||||
}
|
|
||||||
newUser := &ConflictingUser{}
|
|
||||||
err := newUser.Marshal(row)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not parse the content of the file with error %e", err)
|
|
||||||
}
|
|
||||||
if newUser.ConflictEmail != "" && !previouslySeenEmails[strings.ToLower(newUser.Email)] {
|
|
||||||
return fmt.Errorf("not valid email: %s, email not seen in previous conflicts", newUser.Email)
|
|
||||||
}
|
|
||||||
if newUser.ConflictLogin != "" && !previouslySeenLogins[strings.ToLower(newUser.Login)] {
|
|
||||||
return fmt.Errorf("not valid login: %s, login not seen in previous conflicts", newUser.Login)
|
|
||||||
}
|
|
||||||
// valid entry
|
|
||||||
newConflicts = append(newConflicts, *newUser)
|
|
||||||
}
|
|
||||||
for block, count := range counterKeepUsersForBlock {
|
|
||||||
// check if we only have one addition for each block
|
|
||||||
if count != 1 {
|
|
||||||
return fmt.Errorf("invalid number of users to keep, expected 1, got %d for block: %s", count, block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for block, count := range counterDeleteUsersForBlock {
|
|
||||||
// check if we have at least one deletion for each block
|
|
||||||
if count < 1 {
|
|
||||||
return fmt.Errorf("invalid number of users to delete, should be at least 1, got %d for block %s", count, block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.ValidUsers = newConflicts
|
|
||||||
r.BuildConflictBlocks(newConflicts, fmt.Sprintf)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ConflictResolver) MergeConflictingUsers(ctx context.Context) error {
|
|
||||||
for block, users := range r.Blocks {
|
|
||||||
if len(users) < 2 {
|
|
||||||
return fmt.Errorf("not enough users to perform merge, found %d for id %s, should be at least 2", len(users), block)
|
|
||||||
}
|
|
||||||
var intoUser user.User
|
|
||||||
var intoUserId int64
|
|
||||||
var fromUserIds []int64
|
|
||||||
|
|
||||||
// creating a session for each block of users
|
|
||||||
// we want to rollback incase something happens during update / delete
|
|
||||||
if err := r.Store.InTransaction(ctx, func(ctx context.Context) error {
|
|
||||||
for _, u := range users {
|
|
||||||
if u.Direction == "+" {
|
|
||||||
id, err := strconv.ParseInt(u.ID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not convert id in +")
|
|
||||||
}
|
|
||||||
intoUserId = id
|
|
||||||
} else if u.Direction == "-" {
|
|
||||||
id, err := strconv.ParseInt(u.ID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not convert id in -")
|
|
||||||
}
|
|
||||||
fromUserIds = append(fromUserIds, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := r.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: intoUserId}); err != nil {
|
|
||||||
return fmt.Errorf("could not find intoUser: %w", err)
|
|
||||||
}
|
|
||||||
for _, fromUserId := range fromUserIds {
|
|
||||||
_, err := r.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: fromUserId})
|
|
||||||
if err != nil && errors.Is(err, user.ErrUserNotFound) {
|
|
||||||
fmt.Printf("user with id %d does not exist, skipping\n", fromUserId)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not find fromUser: %w", err)
|
|
||||||
}
|
|
||||||
// delete the user
|
|
||||||
delErr := r.userService.Delete(ctx, &user.DeleteUserCommand{UserID: fromUserId})
|
|
||||||
if delErr != nil {
|
|
||||||
return fmt.Errorf("error during deletion of user: %w", delErr)
|
|
||||||
}
|
|
||||||
delACErr := r.ac.DeleteUserPermissions(ctx, 0, fromUserId)
|
|
||||||
if delACErr != nil {
|
|
||||||
return fmt.Errorf("error during deletion of user access control: %w", delACErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMainCommand := &user.UpdateUserCommand{
|
|
||||||
UserID: intoUser.ID,
|
|
||||||
Login: strings.ToLower(intoUser.Login),
|
|
||||||
Email: strings.ToLower(intoUser.Email),
|
|
||||||
}
|
|
||||||
updateErr := r.userService.Update(ctx, updateMainCommand)
|
|
||||||
if updateErr != nil {
|
|
||||||
return fmt.Errorf("could not update user: %w", updateErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
hej@test.com+hej@test.com
|
|
||||||
all of the permissions, roles and ownership will be transferred to the user.
|
|
||||||
+ id: 1, email: hej@test.com, login: hej@test.com
|
|
||||||
these user(s) will be deleted and their permissions transferred.
|
|
||||||
- id: 2, email: HEJ@TEST.COM, login: HEJ@TEST.COM
|
|
||||||
- id: 3, email: hej@TEST.com, login: hej@TEST.com
|
|
||||||
*/
|
|
||||||
func (r *ConflictResolver) showChanges() {
|
|
||||||
if len(r.ValidUsers) == 0 {
|
|
||||||
fmt.Println("no changes will take place as we have no valid users.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
for block, users := range r.Blocks {
|
|
||||||
if _, ok := r.DiscardedBlocks[block]; ok {
|
|
||||||
// skip block
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// looping as we want to can get these out of order (meaning the + and -)
|
|
||||||
var mainUser ConflictingUser
|
|
||||||
for _, u := range users {
|
|
||||||
if u.Direction == "+" {
|
|
||||||
mainUser = u
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString("Keep the following user.\n")
|
|
||||||
b.WriteString(block)
|
|
||||||
b.WriteByte('\n')
|
|
||||||
b.WriteString(color.GreenString(fmt.Sprintf("id: %s, email: %s, login: %s\n", mainUser.ID, mainUser.Email, mainUser.Login)))
|
|
||||||
for _, r := range fmt.Sprintf("%s%s", mainUser.Email, mainUser.Login) {
|
|
||||||
if unicode.IsUpper(r) {
|
|
||||||
b.WriteString("Will be change to:\n")
|
|
||||||
b.WriteString(color.GreenString(fmt.Sprintf("id: %s, email: %s, login: %s\n", mainUser.ID, strings.ToLower(mainUser.Email), strings.ToLower(mainUser.Login))))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
b.WriteString("The following user(s) will be deleted.\n")
|
|
||||||
for _, user := range users {
|
|
||||||
if user.ID == mainUser.ID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// mergeable users
|
|
||||||
b.WriteString(color.RedString(fmt.Sprintf("id: %s, email: %s, login: %s\n", user.ID, user.Email, user.Login)))
|
|
||||||
}
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
logger.Info("\n\nChanges that will take place\n\n")
|
|
||||||
logger.Info(b.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formatter make it possible for us to write to terminal and to a file
|
|
||||||
// with different formats depending on the usecase
|
|
||||||
type Formatter func(format string, a ...any) string
|
|
||||||
|
|
||||||
func shouldDiscardBlock(seenUsersInBlock map[string]string, block string, user ConflictingUser) bool {
|
|
||||||
// loop through users to see if we should skip this block
|
|
||||||
// we have some more tricky scenarios where we have more than two users that can have conflicts with each other
|
|
||||||
// we have made the approach to discard any users that we have seen
|
|
||||||
if _, ok := seenUsersInBlock[user.ID]; ok {
|
|
||||||
// we have seen the user in different block than the current block
|
|
||||||
if seenUsersInBlock[user.ID] != block {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seenUsersInBlock[user.ID] = block
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildConflictBlocks builds blocks of users where each block is a unique email/login
|
|
||||||
// NOTE: currently this function assumes that the users are in order of grouping already
|
|
||||||
func (r *ConflictResolver) BuildConflictBlocks(users ConflictingUsers, f Formatter) {
|
|
||||||
discardedBlocks := make(map[string]bool)
|
|
||||||
seenUsersToBlock := make(map[string]string)
|
|
||||||
blocks := make(map[string]ConflictingUsers)
|
|
||||||
for _, user := range users {
|
|
||||||
// conflict blocks is how we identify a conflict in the user base.
|
|
||||||
var conflictBlock string
|
|
||||||
// sqlite generates string : ""/true
|
|
||||||
// postgres generates string : false/true
|
|
||||||
if user.ConflictEmail == "false" {
|
|
||||||
user.ConflictEmail = ""
|
|
||||||
}
|
|
||||||
if user.ConflictLogin == "false" {
|
|
||||||
user.ConflictLogin = ""
|
|
||||||
}
|
|
||||||
if user.ConflictEmail != "" {
|
|
||||||
conflictBlock = f("conflict: %s", strings.ToLower(user.Email))
|
|
||||||
} else if user.ConflictLogin != "" {
|
|
||||||
conflictBlock = f("conflict: %s", strings.ToLower(user.Login))
|
|
||||||
} else if user.ConflictEmail != "" && user.ConflictLogin != "" {
|
|
||||||
// both conflicts
|
|
||||||
// should not be here unless changed in sql
|
|
||||||
conflictBlock = f("conflict: %s%s", strings.ToLower(user.Email), strings.ToLower(user.Login))
|
|
||||||
}
|
|
||||||
|
|
||||||
// discard logic
|
|
||||||
if shouldDiscardBlock(seenUsersToBlock, conflictBlock, user) {
|
|
||||||
discardedBlocks[conflictBlock] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// adding users to blocks
|
|
||||||
if _, ok := blocks[conflictBlock]; !ok {
|
|
||||||
blocks[conflictBlock] = []ConflictingUser{user}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// skip user thats already part of the block
|
|
||||||
// since we get duplicate entries
|
|
||||||
if contains(blocks[conflictBlock], user) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
blocks[conflictBlock] = append(blocks[conflictBlock], user)
|
|
||||||
}
|
|
||||||
r.Blocks = blocks
|
|
||||||
r.DiscardedBlocks = discardedBlocks
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(cu ConflictingUsers, target ConflictingUser) bool {
|
|
||||||
for _, u := range cu {
|
|
||||||
if u.ID == target.ID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ConflictResolver) logDiscardedUsers() {
|
|
||||||
keys := make([]string, 0, len(r.DiscardedBlocks))
|
|
||||||
for block := range r.DiscardedBlocks {
|
|
||||||
for _, u := range r.Blocks[block] {
|
|
||||||
keys = append(keys, u.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
warn := color.YellowString("Note: We discarded some conflicts that have multiple conflicting types involved.")
|
|
||||||
logger.Infof(`
|
|
||||||
%s
|
|
||||||
|
|
||||||
users discarded with more than one conflict:
|
|
||||||
ids: %s
|
|
||||||
|
|
||||||
Solve conflicts and run the command again to see other conflicts.
|
|
||||||
`, warn, strings.Join(keys, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
// handling tricky cases::
|
|
||||||
// if we have seen a user already
|
|
||||||
// note the conflict of that user
|
|
||||||
// discard that conflict for next time that the user runs the command
|
|
||||||
|
|
||||||
// only present one conflict per user
|
|
||||||
// go through each conflict email/login
|
|
||||||
// if any has ids that have already been seen
|
|
||||||
// discard that conflict
|
|
||||||
// make note to the user to run again after fixing these conflicts
|
|
||||||
func (r *ConflictResolver) ToStringPresentation() string {
|
|
||||||
/*
|
|
||||||
hej@test.com+hej@test.com
|
|
||||||
+ id: 1, email: hej@test.com, login: hej@test.com
|
|
||||||
- id: 2, email: HEJ@TEST.COM, login: HEJ@TEST.COM
|
|
||||||
- id: 3, email: hej@TEST.com, login: hej@TEST.com
|
|
||||||
*/
|
|
||||||
startOfBlock := make(map[string]bool)
|
|
||||||
var b strings.Builder
|
|
||||||
for block, users := range r.Blocks {
|
|
||||||
if _, ok := r.DiscardedBlocks[block]; ok {
|
|
||||||
// skip block
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, user := range users {
|
|
||||||
if !startOfBlock[block] {
|
|
||||||
b.WriteString(fmt.Sprintf("%s\n", block))
|
|
||||||
startOfBlock[block] = true
|
|
||||||
b.WriteString(fmt.Sprintf("+ id: %s, email: %s, login: %s, last_seen_at: %s, auth_module: %s, conflict_email: %s, conflict_login: %s\n",
|
|
||||||
user.ID,
|
|
||||||
user.Email,
|
|
||||||
user.Login,
|
|
||||||
user.LastSeenAt,
|
|
||||||
user.AuthModule,
|
|
||||||
user.ConflictEmail,
|
|
||||||
user.ConflictLogin,
|
|
||||||
))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// mergeable users
|
|
||||||
b.WriteString(fmt.Sprintf("- id: %s, email: %s, login: %s, last_seen_at: %s, auth_module: %s, conflict_email: %s, conflict_login: %s\n",
|
|
||||||
user.ID,
|
|
||||||
user.Email,
|
|
||||||
user.Login,
|
|
||||||
user.LastSeenAt,
|
|
||||||
user.AuthModule,
|
|
||||||
user.ConflictEmail,
|
|
||||||
user.ConflictLogin,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConflictResolver struct {
|
|
||||||
Store *sqlstore.SQLStore
|
|
||||||
userService user.Service
|
|
||||||
ac accesscontrol.Service
|
|
||||||
Config *setting.Cfg
|
|
||||||
Users ConflictingUsers
|
|
||||||
ValidUsers ConflictingUsers
|
|
||||||
Blocks map[string]ConflictingUsers
|
|
||||||
DiscardedBlocks map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConflictingUser struct {
|
|
||||||
// direction is the +/- which indicates if we should keep or delete the user
|
|
||||||
Direction string `xorm:"direction"`
|
|
||||||
ID string `xorm:"id"`
|
|
||||||
Email string `xorm:"email"`
|
|
||||||
Login string `xorm:"login"`
|
|
||||||
LastSeenAt string `xorm:"last_seen_at"`
|
|
||||||
AuthModule string `xorm:"auth_module"`
|
|
||||||
ConflictEmail string `xorm:"conflict_email"`
|
|
||||||
ConflictLogin string `xorm:"conflict_login"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConflictingUsers []ConflictingUser
|
|
||||||
|
|
||||||
func (c *ConflictingUser) Marshal(filerow string) error {
|
|
||||||
// example view of the file to ingest
|
|
||||||
// +/- id: 1, email: hej, auth_module: LDAP
|
|
||||||
trimmedSpaces := strings.ReplaceAll(filerow, " ", "")
|
|
||||||
if trimmedSpaces[0] == '+' {
|
|
||||||
c.Direction = "+"
|
|
||||||
} else if trimmedSpaces[0] == '-' {
|
|
||||||
c.Direction = "-"
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("unable to get which operation was chosen")
|
|
||||||
}
|
|
||||||
trimmed := strings.TrimLeft(trimmedSpaces, "+-")
|
|
||||||
values := strings.Split(trimmed, ",")
|
|
||||||
|
|
||||||
if len(values) < 3 {
|
|
||||||
return fmt.Errorf("expected at least 3 values in entry row")
|
|
||||||
}
|
|
||||||
// expected fields
|
|
||||||
id := strings.Split(values[0], ":")
|
|
||||||
email := strings.Split(values[1], ":")
|
|
||||||
login := strings.Split(values[2], ":")
|
|
||||||
c.ID = id[1]
|
|
||||||
c.Email = email[1]
|
|
||||||
c.Login = login[1]
|
|
||||||
|
|
||||||
// why trim values, 2022-08-20:19:17:12
|
|
||||||
lastSeenAt := strings.TrimPrefix(values[3], "last_seen_at:")
|
|
||||||
authModule := strings.Split(values[4], ":")
|
|
||||||
if len(authModule) < 2 {
|
|
||||||
c.AuthModule = ""
|
|
||||||
} else {
|
|
||||||
c.AuthModule = authModule[1]
|
|
||||||
}
|
|
||||||
c.LastSeenAt = lastSeenAt
|
|
||||||
|
|
||||||
// which conflict
|
|
||||||
conflictEmail := strings.Split(values[5], ":")
|
|
||||||
conflictLogin := strings.Split(values[6], ":")
|
|
||||||
if len(conflictEmail) < 2 {
|
|
||||||
c.ConflictEmail = ""
|
|
||||||
} else {
|
|
||||||
c.ConflictEmail = conflictEmail[1]
|
|
||||||
}
|
|
||||||
if len(conflictLogin) < 2 {
|
|
||||||
c.ConflictLogin = ""
|
|
||||||
} else {
|
|
||||||
c.ConflictLogin = conflictLogin[1]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUsersWithConflictingEmailsOrLogins(ctx *cli.Context, s *sqlstore.SQLStore) (ConflictingUsers, error) {
|
|
||||||
queryUsers := make([]ConflictingUser, 0)
|
|
||||||
outerErr := s.WithDbSession(ctx.Context, func(dbSession *db.Session) error {
|
|
||||||
var rawSQL string
|
|
||||||
if s.GetDialect().DriverName() == migrator.Postgres {
|
|
||||||
rawSQL = conflictUserEntriesSQLPostgres()
|
|
||||||
} else if s.GetDialect().DriverName() == migrator.SQLite {
|
|
||||||
rawSQL = conflictingUserEntriesSQL(s)
|
|
||||||
}
|
|
||||||
err := dbSession.SQL(rawSQL).Find(&queryUsers)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
if outerErr != nil {
|
|
||||||
return queryUsers, outerErr
|
|
||||||
}
|
|
||||||
return queryUsers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// conflictingUserEntriesSQL orders conflicting users by their user_identification
|
|
||||||
// sorts the users by their useridentification and ids
|
|
||||||
func conflictingUserEntriesSQL(s *sqlstore.SQLStore) string {
|
|
||||||
userDialect := db.DB.GetDialect(s).Quote("user")
|
|
||||||
|
|
||||||
sqlQuery := `
|
|
||||||
SELECT DISTINCT
|
|
||||||
u1.id,
|
|
||||||
u1.email,
|
|
||||||
u1.login,
|
|
||||||
u1.last_seen_at,
|
|
||||||
user_auth.auth_module,
|
|
||||||
( SELECT
|
|
||||||
'true'
|
|
||||||
FROM
|
|
||||||
` + userDialect + `
|
|
||||||
WHERE (LOWER(u1.email) = LOWER(u2.email)) AND(u1.email != u2.email)) AS conflict_email,
|
|
||||||
( SELECT
|
|
||||||
'true'
|
|
||||||
FROM
|
|
||||||
` + userDialect + `
|
|
||||||
WHERE (LOWER(u1.login) = LOWER(u2.login) AND(u1.login != u2.login))) AS conflict_login
|
|
||||||
FROM
|
|
||||||
` + userDialect + ` AS u1, ` + userDialect + ` AS u2
|
|
||||||
LEFT JOIN user_auth on user_auth.user_id = u1.id
|
|
||||||
WHERE (conflict_email IS NOT NULL
|
|
||||||
OR conflict_login IS NOT NULL)
|
|
||||||
AND (u1.` + notServiceAccount(s) + `)
|
|
||||||
ORDER BY conflict_email, conflict_login, u1.id`
|
|
||||||
return sqlQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
func conflictUserEntriesSQLPostgres() string {
|
|
||||||
sqlQuery := `
|
|
||||||
SELECT DISTINCT
|
|
||||||
u1.id,
|
|
||||||
u1.email,
|
|
||||||
u1.login,
|
|
||||||
u1.last_seen_at,
|
|
||||||
ua.auth_module,
|
|
||||||
((LOWER(u1.email) = LOWER(u2.email))
|
|
||||||
AND(u1.email != u2.email)) AS conflict_email,
|
|
||||||
((LOWER(u1.login) = LOWER(u2.login))
|
|
||||||
AND(u1.login != u2.login)) AS conflict_login
|
|
||||||
FROM
|
|
||||||
"user" AS u1,
|
|
||||||
"user" AS u2
|
|
||||||
LEFT JOIN user_auth AS ua ON ua.user_id = u2.id
|
|
||||||
WHERE ((LOWER(u1.email) = LOWER(u2.email))
|
|
||||||
AND(u1.email != u2.email)) IS TRUE
|
|
||||||
OR((LOWER(u1.login) = LOWER(u2.login))
|
|
||||||
AND(u1.login != u2.login)) IS TRUE
|
|
||||||
AND(u1.is_service_account = FALSE)
|
|
||||||
ORDER BY
|
|
||||||
conflict_email,
|
|
||||||
conflict_login,
|
|
||||||
u1.id;
|
|
||||||
;
|
|
||||||
`
|
|
||||||
return sqlQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
func notServiceAccount(ss *sqlstore.SQLStore) string {
|
|
||||||
return fmt.Sprintf("is_service_account = %s",
|
|
||||||
ss.GetDialect().BooleanStr(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// confirm function asks for user input
|
|
||||||
// returns bool
|
|
||||||
func confirm(confirmPrompt string) bool {
|
|
||||||
var input string
|
|
||||||
logger.Infof("%s? [y|n]: ", confirmPrompt)
|
|
||||||
|
|
||||||
_, err := fmt.Scanln(&input)
|
|
||||||
if err != nil {
|
|
||||||
logger.Infof("could not parse input from user for confirmation")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
input = strings.ToLower(input)
|
|
||||||
if input == "y" || input == "yes" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
@ -1,964 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
|
||||||
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// "Skipping conflicting users test for mysql as it does make unique constraint case insensitive by default
|
|
||||||
const ignoredDatabase = migrator.MySQL
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
testsuite.Run(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildConflictBlock(t *testing.T) {
|
|
||||||
type testBuildConflictBlock struct {
|
|
||||||
desc string
|
|
||||||
users []user.User
|
|
||||||
expectedBlock string
|
|
||||||
wantDiscardedBlock string
|
|
||||||
wantConflictUser *ConflictingUser
|
|
||||||
wantedNumberOfUsers int
|
|
||||||
}
|
|
||||||
testOrgID := 1
|
|
||||||
testCases := []testBuildConflictBlock{
|
|
||||||
{
|
|
||||||
desc: "should get one block with only 3 users",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "ldap-editor",
|
|
||||||
Login: "ldap-editor",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "LDAP-EDITOR",
|
|
||||||
Login: "LDAP-EDITOR",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "overlapping conflict",
|
|
||||||
Login: "LDAP-editor",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "OVERLAPPING conflict",
|
|
||||||
Login: "no conflict",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedBlock: "conflict: ldap-editor",
|
|
||||||
wantDiscardedBlock: "conflict: overlapping conflict",
|
|
||||||
wantedNumberOfUsers: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should get conflict_email true and conflict_login empty string",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "conflict@email",
|
|
||||||
Login: "login",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "conflict@EMAIL",
|
|
||||||
Login: "plainlogin",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedBlock: "conflict: conflict@email",
|
|
||||||
wantedNumberOfUsers: 2,
|
|
||||||
wantConflictUser: &ConflictingUser{ConflictEmail: "true", ConflictLogin: ""},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should get conflict_email empty string and conflict_login true",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "regular@email",
|
|
||||||
Login: "CONFLICTLOGIN",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "regular-no-conflict@email",
|
|
||||||
Login: "conflictlogin",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedBlock: "conflict: conflictlogin",
|
|
||||||
wantedNumberOfUsers: 2,
|
|
||||||
wantConflictUser: &ConflictingUser{ConflictEmail: "", ConflictLogin: "true"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
|
||||||
// Restore after destructive operation
|
|
||||||
sqlStore, cfg := db.InitTestDBWithCfg(t)
|
|
||||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
|
||||||
userStore := userimpl.ProvideStore(sqlStore, cfg)
|
|
||||||
for _, u := range tc.users {
|
|
||||||
u := user.User{
|
|
||||||
Email: u.Email,
|
|
||||||
Name: u.Name,
|
|
||||||
Login: u.Login,
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
Created: time.Now(),
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
// call user store instead of user service so as not to prevent conflicting users
|
|
||||||
_, err := userStore.Insert(context.Background(), &u)
|
|
||||||
require.NoError(t, err, u)
|
|
||||||
}
|
|
||||||
m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
|
||||||
require.NoError(t, err)
|
|
||||||
r := ConflictResolver{Store: sqlStore}
|
|
||||||
r.BuildConflictBlocks(m, fmt.Sprintf)
|
|
||||||
require.Equal(t, tc.wantedNumberOfUsers, len(r.Blocks[tc.expectedBlock]))
|
|
||||||
if tc.wantDiscardedBlock != "" {
|
|
||||||
require.Equal(t, true, r.DiscardedBlocks[tc.wantDiscardedBlock])
|
|
||||||
}
|
|
||||||
if tc.wantConflictUser != nil {
|
|
||||||
for _, u := range m {
|
|
||||||
require.Equal(t, tc.wantConflictUser.ConflictEmail, u.ConflictEmail)
|
|
||||||
require.Equal(t, tc.wantConflictUser.ConflictLogin, u.ConflictLogin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildConflictBlockFromFileRepresentation(t *testing.T) {
|
|
||||||
type testBuildConflictBlock struct {
|
|
||||||
desc string
|
|
||||||
users []user.User
|
|
||||||
fileString string
|
|
||||||
expectedBlocks []string
|
|
||||||
expectedIdsInBlocks map[string][]string
|
|
||||||
}
|
|
||||||
testOrgID := 1
|
|
||||||
testCases := []testBuildConflictBlock{
|
|
||||||
{
|
|
||||||
desc: "should be able to parse the fileString containing the conflicts",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "test",
|
|
||||||
Login: "test",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "TEST",
|
|
||||||
Login: "TEST",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "test2",
|
|
||||||
Login: "test2",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "TEST2",
|
|
||||||
Login: "TEST2",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "Test2",
|
|
||||||
Login: "Test2",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fileString: `conflict: test
|
|
||||||
- id: 2, email: test, login: test, last_seen_at: 2012-09-19T08:31:20Z, auth_module: , conflict_email: true, conflict_login: true
|
|
||||||
+ id: 3, email: TEST, login: TEST, last_seen_at: 2012-09-19T08:31:29Z, auth_module: , conflict_email: true, conflict_login: true
|
|
||||||
conflict: test2
|
|
||||||
- id: 4, email: test2, login: test2, last_seen_at: 2012-09-19T08:31:41Z, auth_module: , conflict_email: true, conflict_login: true
|
|
||||||
+ id: 5, email: TEST2, login: TEST2, last_seen_at: 2012-09-19T08:31:51Z, auth_module: , conflict_email: true, conflict_login: true
|
|
||||||
- id: 6, email: Test2, login: Test2, last_seen_at: 2012-09-19T08:32:03Z, auth_module: , conflict_email: true, conflict_login: true`,
|
|
||||||
expectedBlocks: []string{"conflict: test", "conflict: test2"},
|
|
||||||
expectedIdsInBlocks: map[string][]string{"conflict: test": {"2", "3"}, "conflict: test2": {"4", "5", "6"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should be able to parse the fileString containing the conflicts 123",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "saml-misi@example.org",
|
|
||||||
Login: "saml-misi",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "saml-misi@example",
|
|
||||||
Login: "saml-Misi",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fileString: `conflict: saml-misi
|
|
||||||
+ id: 5, email: saml-misi@example.org, login: saml-misi, last_seen_at: 2022-09-22T12:00:49Z, auth_module: auth.saml, conflict_email: , conflict_login: true
|
|
||||||
- id: 15, email: saml-misi@example, login: saml-Misi, last_seen_at: 2012-09-26T11:31:32Z, auth_module: , conflict_email: , conflict_login: true`,
|
|
||||||
expectedBlocks: []string{"conflict: saml-misi"},
|
|
||||||
expectedIdsInBlocks: map[string][]string{"conflict: saml-misi": {"5", "15"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
|
||||||
// Restore after destructive operation
|
|
||||||
sqlStore, cfg := db.InitTestDBWithCfg(t)
|
|
||||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
|
||||||
userStore := userimpl.ProvideStore(sqlStore, cfg)
|
|
||||||
for _, u := range tc.users {
|
|
||||||
u := user.User{
|
|
||||||
Email: u.Email,
|
|
||||||
Name: u.Name,
|
|
||||||
Login: u.Login,
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
Created: time.Now(),
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
// call user store instead of user service so as not to prevent conflicting users
|
|
||||||
_, err := userStore.Insert(context.Background(), &u)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conflicts, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
|
||||||
r := ConflictResolver{Users: conflicts, Store: sqlStore}
|
|
||||||
r.BuildConflictBlocks(conflicts, fmt.Sprintf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
validErr := getValidConflictUsers(&r, []byte(tc.fileString))
|
|
||||||
require.NoError(t, validErr)
|
|
||||||
|
|
||||||
// test starts here
|
|
||||||
keys := make([]string, 0)
|
|
||||||
for k := range r.Blocks {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
require.Equal(t, tc.expectedBlocks, keys)
|
|
||||||
|
|
||||||
// we want to validate the ids in the blocks
|
|
||||||
for _, block := range tc.expectedBlocks {
|
|
||||||
// checking for parsing of ids
|
|
||||||
conflictIds := []string{}
|
|
||||||
for _, u := range r.Blocks[block] {
|
|
||||||
conflictIds = append(conflictIds, u.ID)
|
|
||||||
}
|
|
||||||
require.Equal(t, tc.expectedIdsInBlocks[block], conflictIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestGetConflictingUsers(t *testing.T) {
|
|
||||||
type testListConflictingUsers struct {
|
|
||||||
desc string
|
|
||||||
users []user.User
|
|
||||||
want int
|
|
||||||
wantErr error
|
|
||||||
}
|
|
||||||
testOrgID := 1
|
|
||||||
testCases := []testListConflictingUsers{
|
|
||||||
{
|
|
||||||
desc: "should get login conflicting users",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "xo",
|
|
||||||
Login: "ldap-admin",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "ldap-admin",
|
|
||||||
Login: "LDAP-ADMIN",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should get email conflicting users",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "oauth-admin@example.org",
|
|
||||||
Login: "No confli",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "oauth-admin@EXAMPLE.ORG",
|
|
||||||
Login: "oauth-admin",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should be 5 conflicting users, each conflict gets 2 users",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "user1",
|
|
||||||
Login: "USER_DUPLICATE_TEST_LOGIN",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "user2",
|
|
||||||
Login: "user_duplicate_test_login",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "USER2",
|
|
||||||
Login: "no-conflict-login",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "no-conflict",
|
|
||||||
Login: "user_DUPLICATE_test_login",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should be 8 conflicting users, each conflict gets 2 users",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "user1",
|
|
||||||
Login: "USER_DUPLICATE_TEST_LOGIN",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "user2",
|
|
||||||
Login: "user_duplicate_test_login",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "USER2",
|
|
||||||
Login: "no-conflict-login",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "xo",
|
|
||||||
Login: "ldap-admin",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "ldap-admin",
|
|
||||||
Login: "LDAP-ADMIN",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "oauth-admin@example.org",
|
|
||||||
Login: "No confli",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "oauth-admin@EXAMPLE.ORG",
|
|
||||||
Login: "oauth-admin",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: 8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should not get service accounts",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "sa-x",
|
|
||||||
Login: "sa-x",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
IsServiceAccount: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "sa-X",
|
|
||||||
Login: "sa-X",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
IsServiceAccount: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should get nil when no users in database",
|
|
||||||
users: []user.User{},
|
|
||||||
want: 0,
|
|
||||||
wantErr: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
|
||||||
// Restore after destructive operation
|
|
||||||
sqlStore, cfg := db.InitTestDBWithCfg(t)
|
|
||||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
|
||||||
userStore := userimpl.ProvideStore(sqlStore, cfg)
|
|
||||||
for _, u := range tc.users {
|
|
||||||
u := user.User{
|
|
||||||
Email: u.Email,
|
|
||||||
Name: u.Name,
|
|
||||||
Login: u.Login,
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
IsServiceAccount: u.IsServiceAccount,
|
|
||||||
Created: time.Now(),
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
// call user store instead of user service so as not to prevent conflicting users
|
|
||||||
_, err := userStore.Insert(context.Background(), &u)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.want, len(m))
|
|
||||||
if tc.wantErr != nil {
|
|
||||||
require.EqualError(t, err, tc.wantErr.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateConflictingUsersFile(t *testing.T) {
|
|
||||||
type testGenerateConflictUsers struct {
|
|
||||||
desc string
|
|
||||||
users []user.User
|
|
||||||
expectedDiscardedBlock string
|
|
||||||
expectedBlocks []string
|
|
||||||
expectedEmailInBlocks map[string][]string
|
|
||||||
}
|
|
||||||
testOrgID := 1
|
|
||||||
testCases := []testGenerateConflictUsers{
|
|
||||||
{
|
|
||||||
desc: "should get conflicting users",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "user1",
|
|
||||||
Login: "USER_DUPLICATE_TEST_LOGIN",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "user2",
|
|
||||||
Login: "user_duplicate_test_login",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "USER2",
|
|
||||||
Login: "no-conflict-login",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "xo",
|
|
||||||
Login: "ldap-admin",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "ldap-admin",
|
|
||||||
Login: "LDAP-ADMIN",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "oauth-admin@example.org",
|
|
||||||
Login: "No conflict",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "oauth-admin@EXAMPLE.ORG",
|
|
||||||
Login: "oauth-admin",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedBlocks: []string{"conflict: ldap-admin", "conflict: user_duplicate_test_login", "conflict: oauth-admin@example.org", "conflict: user2"},
|
|
||||||
expectedEmailInBlocks: map[string][]string{
|
|
||||||
"conflict: ldap-admin": {"ldap-admin", "xo"},
|
|
||||||
"conflict: user_duplicate_test_login": {"user1", "user2"},
|
|
||||||
"conflict: oauth-admin@example.org": {"oauth-admin@EXAMPLE.ORG", "oauth-admin@example.org"},
|
|
||||||
"conflict: user2": {"USER2", "user2"},
|
|
||||||
},
|
|
||||||
expectedDiscardedBlock: "conflict: user2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should get only one block with 3 users",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "ldap-editor",
|
|
||||||
Login: "ldap-editor",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "LDAP-EDITOR",
|
|
||||||
Login: "LDAP-EDITOR",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "No confli",
|
|
||||||
Login: "LDAP-editor",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedBlocks: []string{"conflict: ldap-editor"},
|
|
||||||
expectedEmailInBlocks: map[string][]string{"conflict: ldap-editor": {"ldap-editor", "LDAP-EDITOR", "No confli"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
|
||||||
// Restore after destructive operation
|
|
||||||
sqlStore, cfg := db.InitTestDBWithCfg(t)
|
|
||||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
|
||||||
userStore := userimpl.ProvideStore(sqlStore, cfg)
|
|
||||||
for _, u := range tc.users {
|
|
||||||
cmd := user.User{
|
|
||||||
Email: u.Email,
|
|
||||||
Name: u.Name,
|
|
||||||
Login: u.Login,
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
Created: time.Now(),
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
// call user store instead of user service so as not to prevent conflicting users
|
|
||||||
_, err := userStore.Insert(context.Background(), &cmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
|
||||||
require.NoError(t, err)
|
|
||||||
r := ConflictResolver{Store: sqlStore}
|
|
||||||
r.BuildConflictBlocks(m, fmt.Sprintf)
|
|
||||||
if tc.expectedDiscardedBlock != "" {
|
|
||||||
require.Equal(t, true, r.DiscardedBlocks[tc.expectedDiscardedBlock])
|
|
||||||
}
|
|
||||||
|
|
||||||
// test starts here
|
|
||||||
keys := make([]string, 0)
|
|
||||||
for k := range r.Blocks {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
expectedBlocks := tc.expectedBlocks
|
|
||||||
sort.Strings(keys)
|
|
||||||
sort.Strings(expectedBlocks)
|
|
||||||
require.Equal(t, expectedBlocks, keys)
|
|
||||||
|
|
||||||
// we want to validate the ids in the blocks
|
|
||||||
for _, block := range tc.expectedBlocks {
|
|
||||||
// checking for parsing of ids
|
|
||||||
conflictEmails := []string{}
|
|
||||||
for _, u := range r.Blocks[block] {
|
|
||||||
conflictEmails = append(conflictEmails, u.Email)
|
|
||||||
}
|
|
||||||
expectedEmailsInBlock := tc.expectedEmailInBlocks[block]
|
|
||||||
sort.Strings(conflictEmails)
|
|
||||||
sort.Strings(expectedEmailsInBlock)
|
|
||||||
require.Equal(t, expectedEmailsInBlock, conflictEmails)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunValidateConflictUserFile(t *testing.T) {
|
|
||||||
t.Run("should validate file thats gets created", func(t *testing.T) {
|
|
||||||
// Restore after destructive operation
|
|
||||||
sqlStore := db.InitTestDB(t)
|
|
||||||
|
|
||||||
const testOrgID int64 = 1
|
|
||||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
|
||||||
// add additional user with conflicting login where DOMAIN is upper case
|
|
||||||
err := sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
// create a user
|
|
||||||
// add additional user with conflicting login where DOMAIN is upper case
|
|
||||||
dupUserLogincmd := user.CreateUserCommand{
|
|
||||||
Email: "userduplicatetest1@test.com",
|
|
||||||
Login: "user_duplicate_test_1_login",
|
|
||||||
OrgID: testOrgID,
|
|
||||||
}
|
|
||||||
rawSQL := fmt.Sprintf(
|
|
||||||
"INSERT INTO %s (email, login, org_id, version, is_admin, created, updated) VALUES (?,?,?,0,%s,\"2024-03-18T15:25:32\",\"2024-03-18T15:25:32\")",
|
|
||||||
sqlStore.Quote("user"),
|
|
||||||
sqlStore.GetDialect().BooleanStr(false),
|
|
||||||
)
|
|
||||||
result, err := sess.Exec(rawSQL, dupUserLogincmd.Email, dupUserLogincmd.Login, dupUserLogincmd.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if n == 0 {
|
|
||||||
return user.ErrUserNotFound
|
|
||||||
}
|
|
||||||
dupUserEmailcmd := user.CreateUserCommand{
|
|
||||||
Email: "USERDUPLICATETEST1@TEST.COM",
|
|
||||||
Login: "USER_DUPLICATE_TEST_1_LOGIN",
|
|
||||||
OrgID: testOrgID,
|
|
||||||
}
|
|
||||||
result, err = sess.Exec(rawSQL, dupUserEmailcmd.Email, dupUserEmailcmd.Login, dupUserEmailcmd.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n, err = result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if n == 0 {
|
|
||||||
return user.ErrUserNotFound
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// get users
|
|
||||||
conflictUsers, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
|
||||||
require.NoError(t, err)
|
|
||||||
r := ConflictResolver{Store: sqlStore}
|
|
||||||
r.BuildConflictBlocks(conflictUsers, fmt.Sprintf)
|
|
||||||
tmpFile, err := generateConflictUsersFile(&r)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b, err := os.ReadFile(tmpFile.Name())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
validErr := getValidConflictUsers(&r, b)
|
|
||||||
require.NoError(t, validErr)
|
|
||||||
require.Equal(t, 2, len(r.ValidUsers))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntegrationMergeUser(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration test")
|
|
||||||
}
|
|
||||||
t.Run("should be able to merge user", func(t *testing.T) {
|
|
||||||
// Restore after destructive operation
|
|
||||||
sqlStore := db.InitTestDB(t)
|
|
||||||
teamSvc, err := teamimpl.ProvideService(sqlStore, setting.NewCfg(), tracing.InitializeTracerForTest())
|
|
||||||
require.NoError(t, err)
|
|
||||||
team1, err := teamSvc.CreateTeam(context.Background(), "team1 name", "", 1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
const testOrgID int64 = 1
|
|
||||||
|
|
||||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
|
||||||
// add additional user with conflicting login where DOMAIN is upper case
|
|
||||||
|
|
||||||
err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
// the order of adding the conflict matters
|
|
||||||
cmd := user.User{
|
|
||||||
Email: "userduplicatetest1@test.com",
|
|
||||||
Name: "user name 1",
|
|
||||||
Login: "user_duplicate_test_1_login",
|
|
||||||
OrgID: testOrgID,
|
|
||||||
Created: time.Now(),
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// call user store instead of user service so as not to prevent conflicting users
|
|
||||||
rawSQL := fmt.Sprintf(
|
|
||||||
"INSERT INTO %s (email, login, org_id, version, is_admin, created, updated) VALUES (?,?,?,0,%s,?,?)",
|
|
||||||
sqlStore.Quote("user"),
|
|
||||||
sqlStore.GetDialect().BooleanStr(false),
|
|
||||||
)
|
|
||||||
result, err := sess.Exec(rawSQL, cmd.Email, cmd.Login, cmd.OrgID, cmd.Created, cmd.Updated)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if n == 0 {
|
|
||||||
return user.ErrUserNotFound
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
cmd := user.User{
|
|
||||||
Email: "USERDUPLICATETEST1@TEST.COM",
|
|
||||||
Name: "user name 1",
|
|
||||||
Login: "USER_DUPLICATE_TEST_1_LOGIN",
|
|
||||||
OrgID: testOrgID,
|
|
||||||
Created: time.Now(),
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
// call user store instead of user service so as not to prevent conflicting users
|
|
||||||
rawSQL := fmt.Sprintf(
|
|
||||||
"INSERT INTO %s (email, login, org_id, version, is_admin, created, updated) VALUES (?,?,?,0,%s,?,?)",
|
|
||||||
sqlStore.Quote("user"),
|
|
||||||
sqlStore.GetDialect().BooleanStr(false),
|
|
||||||
)
|
|
||||||
result, err := sess.Exec(rawSQL, cmd.Email, cmd.Login, cmd.OrgID, cmd.Created, cmd.Updated)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if n == 0 {
|
|
||||||
return user.ErrUserNotFound
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
// this is the user we want to update to another team
|
|
||||||
return teamimpl.AddOrUpdateTeamMemberHook(sess, 1, testOrgID, team1.ID, false, 0)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get users
|
|
||||||
conflictUsers, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
|
||||||
require.NoError(t, err)
|
|
||||||
r := ConflictResolver{
|
|
||||||
Store: sqlStore,
|
|
||||||
userService: usertest.NewUserServiceFake(),
|
|
||||||
ac: actest.FakeService{},
|
|
||||||
}
|
|
||||||
r.BuildConflictBlocks(conflictUsers, fmt.Sprintf)
|
|
||||||
tmpFile, err := generateConflictUsersFile(&r)
|
|
||||||
require.NoError(t, err)
|
|
||||||
// validation to get newConflicts
|
|
||||||
// edited file
|
|
||||||
b, err := os.ReadFile(tmpFile.Name())
|
|
||||||
require.NoError(t, err)
|
|
||||||
validErr := getValidConflictUsers(&r, b)
|
|
||||||
require.NoError(t, validErr)
|
|
||||||
require.Equal(t, 2, len(r.ValidUsers))
|
|
||||||
|
|
||||||
// test starts here
|
|
||||||
err = r.MergeConflictingUsers(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntegrationMergeUserFromNewFileInput(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration test")
|
|
||||||
}
|
|
||||||
t.Run("should be able to merge users after choosing a different user to keep", func(t *testing.T) {
|
|
||||||
type testBuildConflictBlock struct {
|
|
||||||
desc string
|
|
||||||
users []user.User
|
|
||||||
fileString string
|
|
||||||
expectedValidationErr error
|
|
||||||
expectedBlocks []string
|
|
||||||
expectedIdsInBlocks map[string][]string
|
|
||||||
}
|
|
||||||
testOrgID := 1
|
|
||||||
m := make(map[string][]string)
|
|
||||||
conflict1 := "conflict: test"
|
|
||||||
conflict2 := "conflict: test2"
|
|
||||||
m[conflict1] = []string{"2", "3"}
|
|
||||||
m[conflict2] = []string{"4", "5", "6"}
|
|
||||||
testCases := []testBuildConflictBlock{
|
|
||||||
{
|
|
||||||
desc: "should be able to parse the fileString containing the conflicts",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "TEST",
|
|
||||||
Login: "TEST",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "test",
|
|
||||||
Login: "test",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "test2",
|
|
||||||
Login: "test2",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "TEST2",
|
|
||||||
Login: "TEST2",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "Test2",
|
|
||||||
Login: "Test2",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fileString: `conflict: test
|
|
||||||
- id: 1, email: test, login: test, last_seen_at: 2012-09-19T08:31:20Z, auth_module:, conflict_email: true, conflict_login: true
|
|
||||||
+ id: 2, email: TEST, login: TEST, last_seen_at: 2012-09-19T08:31:29Z, auth_module:, conflict_email: true, conflict_login: true
|
|
||||||
conflict: test2
|
|
||||||
- id: 3, email: test2, login: test2, last_seen_at: 2012-09-19T08:31:41Z, auth_module: , conflict_email: true, conflict_login: true
|
|
||||||
+ id: 4, email: TEST2, login: TEST2, last_seen_at: 2012-09-19T08:31:51Z, auth_module: , conflict_email: true, conflict_login: true
|
|
||||||
- id: 5, email: Test2, login: Test2, last_seen_at: 2012-09-19T08:32:03Z, auth_module: , conflict_email: true, conflict_login: true`,
|
|
||||||
expectedBlocks: []string{"conflict: test", "conflict: test2"},
|
|
||||||
expectedIdsInBlocks: m,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should give error for having wrong number of users to keep",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "TEST",
|
|
||||||
Login: "TEST",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "test",
|
|
||||||
Login: "test",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fileString: `conflict: test
|
|
||||||
+ id: 1, email: test, login: test, last_seen_at: 2012-09-19T08:31:20Z, auth_module:, conflict_email: true, conflict_login: true
|
|
||||||
+ id: 2, email: TEST, login: TEST, last_seen_at: 2012-09-19T08:31:29Z, auth_module:, conflict_email: true, conflict_login: true
|
|
||||||
`,
|
|
||||||
expectedValidationErr: fmt.Errorf("invalid number of users to keep, expected 1, got 2 for block: conflict: test"),
|
|
||||||
expectedBlocks: []string{"conflict: test"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should give error for having wrong character for user",
|
|
||||||
users: []user.User{
|
|
||||||
{
|
|
||||||
Email: "TEST",
|
|
||||||
Login: "TEST",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Email: "test",
|
|
||||||
Login: "test",
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fileString: `conflict: test
|
|
||||||
+ id: 1, email: test, login: test, last_seen_at: 2012-09-19T08:31:20Z, auth_module:, conflict_email: true, conflict_login: true
|
|
||||||
% id: 2, email: TEST, login: TEST, last_seen_at: 2012-09-19T08:31:29Z, auth_module:, conflict_email: true, conflict_login: true
|
|
||||||
`,
|
|
||||||
expectedValidationErr: fmt.Errorf("invalid start character (expected '+,-') found %% for row number 3"),
|
|
||||||
expectedBlocks: []string{"conflict: test"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
// Restore after destructive operation
|
|
||||||
sqlStore := db.InitTestDB(t)
|
|
||||||
if sqlStore.GetDialect().DriverName() != ignoredDatabase {
|
|
||||||
err := sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
for _, u := range tc.users {
|
|
||||||
cmd := user.User{
|
|
||||||
Email: u.Email,
|
|
||||||
Name: u.Name,
|
|
||||||
Login: u.Login,
|
|
||||||
OrgID: int64(testOrgID),
|
|
||||||
Created: time.Now(),
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
// call user store instead of user service so as not to prevent conflicting users
|
|
||||||
rawSQL := fmt.Sprintf(
|
|
||||||
"INSERT INTO %s (email, login, org_id, version, is_admin, created, updated) VALUES (?,?,?,0,%s,?,?)",
|
|
||||||
sqlStore.Quote("user"),
|
|
||||||
sqlStore.GetDialect().BooleanStr(false),
|
|
||||||
)
|
|
||||||
result, err := sess.Exec(rawSQL, cmd.Email, cmd.Login, cmd.OrgID, cmd.Created, cmd.Updated)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if n == 0 {
|
|
||||||
return user.ErrUserNotFound
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add additional user with conflicting login where DOMAIN is upper case
|
|
||||||
conflictUsers, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore)
|
|
||||||
require.NoError(t, err)
|
|
||||||
userFake := usertest.NewUserServiceFake()
|
|
||||||
userFake.ExpectedUser = &user.User{Email: "test", Login: "test", OrgID: int64(testOrgID)}
|
|
||||||
r := ConflictResolver{
|
|
||||||
Store: sqlStore,
|
|
||||||
userService: userFake,
|
|
||||||
ac: actest.FakeService{},
|
|
||||||
}
|
|
||||||
r.BuildConflictBlocks(conflictUsers, fmt.Sprintf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
// validation to get newConflicts
|
|
||||||
// edited file
|
|
||||||
// b, err := os.ReadFile(tmpFile.Name())
|
|
||||||
// mocked file input
|
|
||||||
b := tc.fileString
|
|
||||||
require.NoError(t, err)
|
|
||||||
validErr := getValidConflictUsers(&r, []byte(b))
|
|
||||||
if tc.expectedValidationErr != nil {
|
|
||||||
require.Equal(t, tc.expectedValidationErr, validErr)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, validErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test starts here
|
|
||||||
err = r.MergeConflictingUsers(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarshalConflictUser(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
inputRow string
|
|
||||||
expectedUser ConflictingUser
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "should be able to marshal expected input row",
|
|
||||||
inputRow: "+ id: 4, email: userduplicatetest1@test.com, login: userduplicatetest1, last_seen_at: 2012-07-26T16:08:11Z, auth_module: auth.saml, conflict_email: true, conflict_login: ",
|
|
||||||
expectedUser: ConflictingUser{
|
|
||||||
Direction: "+",
|
|
||||||
ID: "4",
|
|
||||||
Email: "userduplicatetest1@test.com",
|
|
||||||
Login: "userduplicatetest1",
|
|
||||||
LastSeenAt: "2012-07-26T16:08:11Z",
|
|
||||||
AuthModule: "auth.saml",
|
|
||||||
ConflictEmail: "true",
|
|
||||||
ConflictLogin: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should be able to marshal expected input row",
|
|
||||||
inputRow: "+ id: 1, email: userduplicatetest1@test.com, login: user_duplicate_test_1_login, last_seen_at: 2012-07-26T16:08:11Z, auth_module: , conflict_email: , conflict_login: true",
|
|
||||||
expectedUser: ConflictingUser{
|
|
||||||
Direction: "+",
|
|
||||||
ID: "1",
|
|
||||||
Email: "userduplicatetest1@test.com",
|
|
||||||
Login: "user_duplicate_test_1_login",
|
|
||||||
LastSeenAt: "2012-07-26T16:08:11Z",
|
|
||||||
AuthModule: "",
|
|
||||||
ConflictEmail: "",
|
|
||||||
ConflictLogin: "true",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
user := ConflictingUser{}
|
|
||||||
err := user.Marshal(tc.inputRow)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.expectedUser.Direction, user.Direction)
|
|
||||||
require.Equal(t, tc.expectedUser.ID, user.ID)
|
|
||||||
require.Equal(t, tc.expectedUser.Email, user.Email)
|
|
||||||
require.Equal(t, tc.expectedUser.Login, user.Login)
|
|
||||||
require.Equal(t, tc.expectedUser.LastSeenAt, user.LastSeenAt)
|
|
||||||
require.Equal(t, tc.expectedUser.ConflictEmail, user.ConflictEmail)
|
|
||||||
require.Equal(t, tc.expectedUser.ConflictLogin, user.ConflictLogin)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user