mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
a9f48ff561
* Create Bulk Users * WIP: Adding a test for bulk user import * adding a user bulk create test * Fixing description, removing password required, and adding more test cases * Fixing description, removing password required, and adding more test cases * Fixed all comments and added Random Password Generator * returning an error in generateRandomPassword * Using 2 loops to create user list and then create the actual users * Adding a bulk user delete * fixing a mistake in temp csv * fixed lints and removed yamlFlag
376 lines
10 KiB
Go
376 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/sethvargo/go-password/password"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const (
|
|
globalRoleFlagName = "global-role"
|
|
teamFlagName = "team"
|
|
passwordFlagName = "password"
|
|
emailFlagName = "email"
|
|
nameFlagName = "name"
|
|
ssoFlagName = "sso"
|
|
apiOnlyFlagName = "api-only"
|
|
csvFlagName = "csv"
|
|
)
|
|
|
|
func userCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "user",
|
|
Usage: "Manage Fleet users",
|
|
Subcommands: []*cli.Command{
|
|
createUserCommand(),
|
|
deleteUserCommand(),
|
|
createBulkUsersCommand(),
|
|
deleteBulkUsersCommand(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func createUserCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "create",
|
|
Usage: "Create a new user",
|
|
UsageText: `This command will create a new user in Fleet. By default, the user will authenticate with a password and will be a global observer.
|
|
|
|
If a password is required and not provided by flag, the command will prompt for password input through stdin.`,
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: emailFlagName,
|
|
Usage: "Email for new user (required)",
|
|
Required: true,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: nameFlagName,
|
|
Usage: "User's full name or nickname (required)",
|
|
Required: true,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: passwordFlagName,
|
|
Usage: "Password for new user",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: ssoFlagName,
|
|
Usage: "Enable user login via SSO",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: apiOnlyFlagName,
|
|
Usage: "Make \"API-only\" user",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: globalRoleFlagName,
|
|
Usage: "Global role to assign to user (default \"observer\")",
|
|
},
|
|
&cli.StringSliceFlag{
|
|
Name: "team",
|
|
Aliases: []string{"t"},
|
|
Usage: "Team assignments in team_id:role pairs (multiple may be specified)",
|
|
},
|
|
configFlag(),
|
|
contextFlag(),
|
|
yamlFlag(),
|
|
debugFlag(),
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
client, err := clientFromCLI(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
password := c.String(passwordFlagName)
|
|
email := c.String(emailFlagName)
|
|
name := c.String(nameFlagName)
|
|
sso := c.Bool(ssoFlagName)
|
|
apiOnly := c.Bool(apiOnlyFlagName)
|
|
globalRoleString := c.String(globalRoleFlagName)
|
|
teamStrings := c.StringSlice(teamFlagName)
|
|
|
|
var globalRole *string
|
|
var teams []fleet.UserTeam
|
|
if globalRoleString != "" && len(teamStrings) > 0 {
|
|
return errors.New("Users may not have global_role and teams.")
|
|
} else if globalRoleString == "" && len(teamStrings) == 0 {
|
|
globalRole = ptr.String(fleet.RoleObserver)
|
|
} else if globalRoleString != "" {
|
|
if !fleet.ValidGlobalRole(globalRoleString) {
|
|
return fmt.Errorf("'%s' is not a valid global role", globalRoleString)
|
|
}
|
|
globalRole = ptr.String(globalRoleString)
|
|
} else {
|
|
for _, t := range teamStrings {
|
|
parts := strings.Split(t, ":")
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf("Unable to parse '%s' as team_id:role", t)
|
|
}
|
|
teamID, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to parse team_id: %w", err)
|
|
}
|
|
if !fleet.ValidTeamRole(parts[1]) {
|
|
return fmt.Errorf("'%s' is not a valid team role", parts[1])
|
|
}
|
|
|
|
teams = append(teams, fleet.UserTeam{Team: fleet.Team{ID: uint(teamID)}, Role: parts[1]})
|
|
}
|
|
}
|
|
|
|
if sso && len(password) > 0 {
|
|
return errors.New("Password may not be provided for SSO users.")
|
|
}
|
|
if !sso && len(password) == 0 {
|
|
fmt.Print("Enter password for user: ")
|
|
passBytes, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
|
fmt.Println()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to read password: %w", err)
|
|
}
|
|
if len(passBytes) == 0 {
|
|
return errors.New("Password may not be empty.")
|
|
}
|
|
|
|
fmt.Print("Enter password for user (confirm): ")
|
|
confBytes, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
|
fmt.Println()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to read confirmation: %w", err)
|
|
}
|
|
|
|
if !bytes.Equal(passBytes, confBytes) {
|
|
return errors.New("Confirmation does not match")
|
|
}
|
|
|
|
password = string(passBytes)
|
|
}
|
|
|
|
// Only set the password reset flag if SSO is not enabled and user is not API-only. Otherwise
|
|
// the user will be stuck in a bad state and not be able to log in.
|
|
force_reset := !sso && !apiOnly
|
|
|
|
// password requirements are validated as part of `CreateUser`
|
|
err = client.CreateUser(fleet.UserPayload{
|
|
Password: &password,
|
|
Email: &email,
|
|
Name: &name,
|
|
SSOEnabled: &sso,
|
|
AdminForcedPasswordReset: &force_reset,
|
|
APIOnly: &apiOnly,
|
|
GlobalRole: globalRole,
|
|
Teams: &teams,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func createBulkUsersCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "create-users",
|
|
Usage: "Create bulk users",
|
|
UsageText: `This command will create a set of users in Fleet by importing a CSV file. Expected columns are: Name,Email,SSO,API Only,Global Role,Teams. Created Users by default get random password and Observer Role.`,
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: csvFlagName,
|
|
Usage: "csv file with all the users (required)",
|
|
Required: true,
|
|
},
|
|
configFlag(),
|
|
contextFlag(),
|
|
debugFlag(),
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
client, err := clientFromCLI(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
csvFilePath := c.String(csvFlagName)
|
|
|
|
csvFile, err := os.Open(csvFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer csvFile.Close()
|
|
csvLines, err := csv.NewReader(csvFile).ReadAll()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
users := []fleet.UserPayload{}
|
|
for _, record := range csvLines[1:] {
|
|
name := record[0]
|
|
email := record[1]
|
|
password, passErr := generateRandomPassword()
|
|
sso, ssoErr := strconv.ParseBool(record[2])
|
|
apiOnly, apiErr := strconv.ParseBool(record[3])
|
|
globalRoleString := record[4]
|
|
teamStrings := strings.Split(record[5], " ")
|
|
if ssoErr != nil {
|
|
return fmt.Errorf("SSO is not a vailed Boolean value: %w", err)
|
|
}
|
|
if apiErr != nil {
|
|
return fmt.Errorf("API Only is not a vailed Boolean value: %w", err)
|
|
}
|
|
if passErr != nil {
|
|
return fmt.Errorf("not able to generate a random password: %w", err)
|
|
}
|
|
|
|
var globalRole *string
|
|
var teams []fleet.UserTeam
|
|
|
|
if globalRoleString != "" && len(teamStrings) > 0 && teamStrings[0] != "" {
|
|
return errors.New("Users may not have global_role and teams.")
|
|
} else if globalRoleString == "" && (len(teamStrings) == 0 || teamStrings[0] == "") {
|
|
globalRole = ptr.String(fleet.RoleObserver)
|
|
} else if globalRoleString != "" {
|
|
if !fleet.ValidGlobalRole(globalRoleString) {
|
|
return fmt.Errorf("'%s' is not a valid team role", globalRoleString)
|
|
}
|
|
globalRole = ptr.String(globalRoleString)
|
|
} else {
|
|
for _, t := range teamStrings {
|
|
parts := strings.Split(t, ":")
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf("Unable to parse '%s' as team_id:role", t)
|
|
}
|
|
teamID, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to parse team_id: %w", err)
|
|
}
|
|
if !fleet.ValidTeamRole(parts[1]) {
|
|
return fmt.Errorf("'%s' is not a valid team role", parts[1])
|
|
}
|
|
|
|
teams = append(teams, fleet.UserTeam{Team: fleet.Team{ID: uint(teamID)}, Role: parts[1]})
|
|
}
|
|
}
|
|
|
|
if sso && len(password) > 0 {
|
|
password = ""
|
|
}
|
|
force_reset := !sso
|
|
users = append(users, fleet.UserPayload{
|
|
Password: &password,
|
|
Email: &email,
|
|
Name: &name,
|
|
SSOEnabled: &sso,
|
|
AdminForcedPasswordReset: &force_reset,
|
|
APIOnly: &apiOnly,
|
|
GlobalRole: globalRole,
|
|
Teams: &teams,
|
|
})
|
|
}
|
|
|
|
for _, user := range users {
|
|
err = client.CreateUser(user)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create user: %w", err)
|
|
}
|
|
if *user.SSOEnabled {
|
|
fmt.Printf("Email: %v SSO: %v\n", *user.Email, *user.SSOEnabled)
|
|
} else {
|
|
fmt.Printf("Email: %v Generated password: %v\n", *user.Email, *user.Password)
|
|
}
|
|
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func deleteUserCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "delete",
|
|
Usage: "Delete a user",
|
|
UsageText: `This command will delete a user specified by their email in Fleet.`,
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: emailFlagName,
|
|
Usage: "Email for user (required)",
|
|
Required: true,
|
|
},
|
|
configFlag(),
|
|
contextFlag(),
|
|
yamlFlag(),
|
|
debugFlag(),
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
client, err := clientFromCLI(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
email := c.String(emailFlagName)
|
|
return client.DeleteUser(email)
|
|
},
|
|
}
|
|
}
|
|
|
|
func deleteBulkUsersCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "delete-users",
|
|
Usage: "Delete a list of user",
|
|
UsageText: `This command will delete a list of users by importing a CSV file containing a list of emails. Expected columns are:Email`,
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: csvFlagName,
|
|
Usage: "csv file with all the users (required)",
|
|
Required: true,
|
|
},
|
|
configFlag(),
|
|
contextFlag(),
|
|
debugFlag(),
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
client, err := clientFromCLI(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
csvFilePath := c.String(csvFlagName)
|
|
|
|
csvFile, err := os.Open(csvFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer csvFile.Close()
|
|
csvLines, err := csv.NewReader(csvFile).ReadAll()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, user := range csvLines[1:] {
|
|
email := user[0]
|
|
if err := client.DeleteUser(email); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
|
|
},
|
|
}
|
|
}
|
|
func generateRandomPassword() (string, error) {
|
|
password, err := password.Generate(20, 2, 2, false, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return password, nil
|
|
}
|