2020-11-05 01:06:55 +00:00
package main
import (
"bytes"
2022-06-22 16:34:58 +00:00
"encoding/csv"
2021-11-22 14:13:26 +00:00
"errors"
2020-11-05 01:06:55 +00:00
"fmt"
"os"
2021-06-07 20:23:15 +00:00
"strconv"
"strings"
2020-11-05 01:06:55 +00:00
2021-06-26 04:46:51 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
2022-06-22 16:34:58 +00:00
"github.com/sethvargo/go-password/password"
2021-03-13 00:42:38 +00:00
"github.com/urfave/cli/v2"
2020-11-05 01:06:55 +00:00
"golang.org/x/crypto/ssh/terminal"
)
const (
2021-06-07 20:23:15 +00:00
globalRoleFlagName = "global-role"
teamFlagName = "team"
passwordFlagName = "password"
emailFlagName = "email"
2021-06-24 20:42:29 +00:00
nameFlagName = "name"
2021-06-07 20:23:15 +00:00
ssoFlagName = "sso"
2021-06-17 01:11:28 +00:00
apiOnlyFlagName = "api-only"
2022-06-22 16:34:58 +00:00
csvFlagName = "csv"
2020-11-05 01:06:55 +00:00
)
2021-03-13 00:42:38 +00:00
func userCommand ( ) * cli . Command {
return & cli . Command {
2020-11-05 01:06:55 +00:00
Name : "user" ,
Usage : "Manage Fleet users" ,
2021-03-13 00:42:38 +00:00
Subcommands : [ ] * cli . Command {
2020-11-05 01:06:55 +00:00
createUserCommand ( ) ,
2021-07-21 17:03:10 +00:00
deleteUserCommand ( ) ,
2022-06-22 16:34:58 +00:00
createBulkUsersCommand ( ) ,
deleteBulkUsersCommand ( ) ,
2020-11-05 01:06:55 +00:00
} ,
}
}
2021-03-13 00:42:38 +00:00
func createUserCommand ( ) * cli . Command {
return & cli . Command {
2020-11-05 01:06:55 +00:00
Name : "create" ,
Usage : "Create a new user" ,
2021-06-07 20:23:15 +00:00
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 .
2020-11-05 01:06:55 +00:00
If a password is required and not provided by flag , the command will prompt for password input through stdin . ` ,
Flags : [ ] cli . Flag {
2021-03-13 00:42:38 +00:00
& cli . StringFlag {
2021-06-24 20:42:29 +00:00
Name : emailFlagName ,
Usage : "Email for new user (required)" ,
2020-11-05 01:06:55 +00:00
Required : true ,
} ,
2021-03-13 00:42:38 +00:00
& cli . StringFlag {
2021-06-24 20:42:29 +00:00
Name : nameFlagName ,
Usage : "User's full name or nickname (required)" ,
2020-11-05 01:06:55 +00:00
Required : true ,
} ,
2021-03-13 00:42:38 +00:00
& cli . StringFlag {
2020-11-05 01:06:55 +00:00
Name : passwordFlagName ,
Usage : "Password for new user" ,
} ,
2021-03-13 00:42:38 +00:00
& cli . BoolFlag {
2020-11-05 01:06:55 +00:00
Name : ssoFlagName ,
2021-06-17 01:11:28 +00:00
Usage : "Enable user login via SSO" ,
} ,
& cli . BoolFlag {
Name : apiOnlyFlagName ,
Usage : "Make \"API-only\" user" ,
2020-11-05 01:06:55 +00:00
} ,
2021-06-07 20:23:15 +00:00
& cli . StringFlag {
Name : globalRoleFlagName ,
2021-06-17 01:11:28 +00:00
Usage : "Global role to assign to user (default \"observer\")" ,
2021-06-07 20:23:15 +00:00
} ,
& cli . StringSliceFlag {
Name : "team" ,
Aliases : [ ] string { "t" } ,
Usage : "Team assignments in team_id:role pairs (multiple may be specified)" ,
} ,
2020-11-05 01:06:55 +00:00
configFlag ( ) ,
contextFlag ( ) ,
yamlFlag ( ) ,
2021-02-03 02:55:16 +00:00
debugFlag ( ) ,
2020-11-05 01:06:55 +00:00
} ,
Action : func ( c * cli . Context ) error {
2021-06-06 22:07:29 +00:00
client , err := clientFromCLI ( c )
2020-11-05 01:06:55 +00:00
if err != nil {
return err
}
password := c . String ( passwordFlagName )
email := c . String ( emailFlagName )
2021-06-24 20:42:29 +00:00
name := c . String ( nameFlagName )
2020-11-05 01:06:55 +00:00
sso := c . Bool ( ssoFlagName )
2021-06-17 01:11:28 +00:00
apiOnly := c . Bool ( apiOnlyFlagName )
2021-06-07 20:23:15 +00:00
globalRoleString := c . String ( globalRoleFlagName )
teamStrings := c . StringSlice ( teamFlagName )
var globalRole * string
var teams [ ] fleet . UserTeam
2024-10-18 17:38:26 +00:00
if globalRoleString != "" && len ( teamStrings ) > 0 { //nolint:gocritic // ignore ifElseChain
2021-06-07 20:23:15 +00:00
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 ) {
2022-06-22 16:34:58 +00:00
return fmt . Errorf ( "'%s' is not a valid global role" , globalRoleString )
2021-06-07 20:23:15 +00:00
}
globalRole = ptr . String ( globalRoleString )
} else {
for _ , t := range teamStrings {
parts := strings . Split ( t , ":" )
if len ( parts ) != 2 {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Unable to parse '%s' as team_id:role" , t )
2021-06-07 20:23:15 +00:00
}
teamID , err := strconv . Atoi ( parts [ 0 ] )
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Unable to parse team_id: %w" , err )
2021-06-07 20:23:15 +00:00
}
if ! fleet . ValidTeamRole ( parts [ 1 ] ) {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "'%s' is not a valid team role" , parts [ 1 ] )
2021-06-07 20:23:15 +00:00
}
2024-10-18 17:38:26 +00:00
teams = append ( teams , fleet . UserTeam { Team : fleet . Team { ID : uint ( teamID ) } , Role : parts [ 1 ] } ) //nolint:gosec // dismiss G115
2021-06-07 20:23:15 +00:00
}
}
2020-11-05 01:06:55 +00:00
if sso && len ( password ) > 0 {
2021-11-24 20:56:54 +00:00
return errors . New ( "Password may not be provided for SSO users." )
2020-11-05 01:06:55 +00:00
}
if ! sso && len ( password ) == 0 {
fmt . Print ( "Enter password for user: " )
passBytes , err := terminal . ReadPassword ( int ( os . Stdin . Fd ( ) ) )
fmt . Println ( )
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Failed to read password: %w" , err )
2020-11-05 01:06:55 +00:00
}
if len ( passBytes ) == 0 {
2021-11-24 20:56:54 +00:00
return errors . New ( "Password may not be empty." )
2020-11-05 01:06:55 +00:00
}
fmt . Print ( "Enter password for user (confirm): " )
confBytes , err := terminal . ReadPassword ( int ( os . Stdin . Fd ( ) ) )
fmt . Println ( )
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Failed to read confirmation: %w" , err )
2020-11-05 01:06:55 +00:00
}
if ! bytes . Equal ( passBytes , confBytes ) {
2021-11-24 20:56:54 +00:00
return errors . New ( "Confirmation does not match" )
2020-11-05 01:06:55 +00:00
}
password = string ( passBytes )
}
2022-04-12 13:57:57 +00:00
// Only set the password reset flag if SSO is not enabled and user is not API-only. Otherwise
2020-11-05 01:06:55 +00:00
// the user will be stuck in a bad state and not be able to log in.
2022-04-12 13:57:57 +00:00
force_reset := ! sso && ! apiOnly
2022-05-18 17:03:00 +00:00
// password requirements are validated as part of `CreateUser`
2024-06-13 22:10:27 +00:00
sessionKey , err := client . CreateUser ( fleet . UserPayload {
2020-11-05 01:06:55 +00:00
Password : & password ,
Email : & email ,
2021-06-24 20:42:29 +00:00
Name : & name ,
2020-11-05 01:06:55 +00:00
SSOEnabled : & sso ,
AdminForcedPasswordReset : & force_reset ,
2021-06-17 01:11:28 +00:00
APIOnly : & apiOnly ,
2021-06-07 20:23:15 +00:00
GlobalRole : globalRole ,
Teams : & teams ,
2020-11-05 01:06:55 +00:00
} )
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Failed to create user: %w" , err )
2020-11-05 01:06:55 +00:00
}
2024-06-13 22:10:27 +00:00
if apiOnly && sessionKey != nil && * sessionKey != "" {
fmt . Fprintf ( c . App . Writer , "Success! The API token for your new user is: %s\n" , * sessionKey )
}
2020-11-05 01:06:55 +00:00
return nil
} ,
}
}
2021-07-21 17:03:10 +00:00
2022-06-22 16:34:58 +00:00
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
2024-10-18 17:38:26 +00:00
if globalRoleString != "" && len ( teamStrings ) > 0 && teamStrings [ 0 ] != "" { //nolint:gocritic // ignore ifElseChain
2022-06-22 16:34:58 +00:00
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 ] )
}
2024-10-18 17:38:26 +00:00
teams = append ( teams ,
fleet . UserTeam { Team : fleet . Team { ID : uint ( teamID ) } , Role : parts [ 1 ] } ) //nolint:gosec // dismiss G115
2022-06-22 16:34:58 +00:00
}
}
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 {
2024-06-13 22:10:27 +00:00
_ , err = client . CreateUser ( user )
2022-06-22 16:34:58 +00:00
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
} ,
}
}
2021-07-21 17:03:10 +00:00
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 )
} ,
}
}
2022-06-22 16:34:58 +00:00
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
} ,
}
}
2024-06-13 22:10:27 +00:00
2022-06-22 16:34:58 +00:00
func generateRandomPassword ( ) ( string , error ) {
password , err := password . Generate ( 20 , 2 , 2 , false , true )
if err != nil {
return "" , err
}
return password , nil
}