2023-02-06 14:58:21 +00:00
//go:build !slim
package cli
import (
"fmt"
"os/signal"
"sort"
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
2023-03-23 22:42:20 +00:00
"github.com/coder/coder/cli/clibase"
2023-02-06 14:58:21 +00:00
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/userpassword"
"github.com/coder/coder/codersdk"
)
2023-03-23 22:42:20 +00:00
func ( r * RootCmd ) newCreateAdminUserCommand ( ) * clibase . Cmd {
2023-02-06 14:58:21 +00:00
var (
newUserDBURL string
newUserSSHKeygenAlgorithm string
newUserUsername string
newUserEmail string
newUserPassword string
)
2023-03-23 22:42:20 +00:00
createAdminUserCommand := & clibase . Cmd {
2023-02-06 14:58:21 +00:00
Use : "create-admin-user" ,
Short : "Create a new admin user with the given username, email and password and adds it to every organization." ,
2023-03-23 22:42:20 +00:00
Handler : func ( inv * clibase . Invocation ) error {
ctx := inv . Context ( )
2023-02-06 14:58:21 +00:00
sshKeygenAlgorithm , err := gitsshkey . ParseAlgorithm ( newUserSSHKeygenAlgorithm )
if err != nil {
return xerrors . Errorf ( "parse ssh keygen algorithm %q: %w" , newUserSSHKeygenAlgorithm , err )
}
2023-03-23 22:42:20 +00:00
cfg := r . createConfig ( )
logger := slog . Make ( sloghuman . Sink ( inv . Stderr ) )
if r . verbose {
2023-02-06 14:58:21 +00:00
logger = logger . Leveled ( slog . LevelDebug )
}
ctx , cancel := signal . NotifyContext ( ctx , InterruptSignals ... )
defer cancel ( )
if newUserDBURL == "" {
2023-03-23 22:42:20 +00:00
cliui . Infof ( inv . Stdout , "Using built-in PostgreSQL (%s)\n" , cfg . PostgresPath ( ) )
2023-02-06 14:58:21 +00:00
url , closePg , err := startBuiltinPostgres ( ctx , cfg , logger )
if err != nil {
return err
}
defer func ( ) {
_ = closePg ( )
} ( )
newUserDBURL = url
}
sqlDB , err := connectToPostgres ( ctx , logger , "postgres" , newUserDBURL )
if err != nil {
return xerrors . Errorf ( "connect to postgres: %w" , err )
}
defer func ( ) {
_ = sqlDB . Close ( )
} ( )
db := database . New ( sqlDB )
validateInputs := func ( username , email , password string ) error {
// Use the validator tags so we match the API's validation.
req := codersdk . CreateUserRequest {
Username : "username" ,
Email : "email@coder.com" ,
Password : "ValidPa$$word123!" ,
OrganizationID : uuid . New ( ) ,
}
if username != "" {
req . Username = username
}
if email != "" {
req . Email = email
}
if password != "" {
req . Password = password
}
return httpapi . Validate . Struct ( req )
}
if newUserUsername == "" {
2023-03-23 22:42:20 +00:00
newUserUsername , err = cliui . Prompt ( inv , cliui . PromptOptions {
2023-02-06 14:58:21 +00:00
Text : "Username" ,
Validate : func ( val string ) error {
if val == "" {
return xerrors . New ( "username cannot be empty" )
}
return validateInputs ( val , "" , "" )
} ,
} )
if err != nil {
return err
}
}
if newUserEmail == "" {
2023-03-23 22:42:20 +00:00
newUserEmail , err = cliui . Prompt ( inv , cliui . PromptOptions {
2023-02-06 14:58:21 +00:00
Text : "Email" ,
Validate : func ( val string ) error {
if val == "" {
return xerrors . New ( "email cannot be empty" )
}
return validateInputs ( "" , val , "" )
} ,
} )
if err != nil {
return err
}
}
if newUserPassword == "" {
2023-03-23 22:42:20 +00:00
newUserPassword , err = cliui . Prompt ( inv , cliui . PromptOptions {
2023-02-06 14:58:21 +00:00
Text : "Password" ,
Secret : true ,
Validate : func ( val string ) error {
if val == "" {
return xerrors . New ( "password cannot be empty" )
}
return validateInputs ( "" , "" , val )
} ,
} )
if err != nil {
return err
}
// Prompt again.
2023-03-23 22:42:20 +00:00
_ , err = cliui . Prompt ( inv , cliui . PromptOptions {
2023-02-06 14:58:21 +00:00
Text : "Confirm password" ,
Secret : true ,
Validate : func ( val string ) error {
if val != newUserPassword {
return xerrors . New ( "passwords do not match" )
}
return nil
} ,
} )
if err != nil {
return err
}
}
err = validateInputs ( newUserUsername , newUserEmail , newUserPassword )
if err != nil {
return xerrors . Errorf ( "validate inputs: %w" , err )
}
hashedPassword , err := userpassword . Hash ( newUserPassword )
if err != nil {
return xerrors . Errorf ( "hash password: %w" , err )
}
// Create the user.
var newUser database . User
err = db . InTx ( func ( tx database . Store ) error {
orgs , err := tx . GetOrganizations ( ctx )
if err != nil {
return xerrors . Errorf ( "get organizations: %w" , err )
}
// Sort organizations by name so that test output is consistent.
sort . Slice ( orgs , func ( i , j int ) bool {
return orgs [ i ] . Name < orgs [ j ] . Name
} )
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stderr , "Creating user..." )
2023-02-06 14:58:21 +00:00
newUser , err = tx . InsertUser ( ctx , database . InsertUserParams {
ID : uuid . New ( ) ,
Email : newUserEmail ,
Username : newUserUsername ,
HashedPassword : [ ] byte ( hashedPassword ) ,
CreatedAt : database . Now ( ) ,
UpdatedAt : database . Now ( ) ,
RBACRoles : [ ] string { rbac . RoleOwner ( ) } ,
LoginType : database . LoginTypePassword ,
} )
if err != nil {
return xerrors . Errorf ( "insert user: %w" , err )
}
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stderr , "Generating user SSH key..." )
2023-02-06 14:58:21 +00:00
privateKey , publicKey , err := gitsshkey . Generate ( sshKeygenAlgorithm )
if err != nil {
return xerrors . Errorf ( "generate user gitsshkey: %w" , err )
}
_ , err = tx . InsertGitSSHKey ( ctx , database . InsertGitSSHKeyParams {
UserID : newUser . ID ,
CreatedAt : database . Now ( ) ,
UpdatedAt : database . Now ( ) ,
PrivateKey : privateKey ,
PublicKey : publicKey ,
} )
if err != nil {
return xerrors . Errorf ( "insert user gitsshkey: %w" , err )
}
for _ , org := range orgs {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintf ( inv . Stderr , "Adding user to organization %q (%s) as admin...\n" , org . Name , org . ID . String ( ) )
2023-02-06 14:58:21 +00:00
_ , err := tx . InsertOrganizationMember ( ctx , database . InsertOrganizationMemberParams {
OrganizationID : org . ID ,
UserID : newUser . ID ,
CreatedAt : database . Now ( ) ,
UpdatedAt : database . Now ( ) ,
Roles : [ ] string { rbac . RoleOrgAdmin ( org . ID ) } ,
} )
if err != nil {
return xerrors . Errorf ( "insert organization member: %w" , err )
}
}
return nil
} , nil )
if err != nil {
return err
}
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stderr , "" )
_ , _ = fmt . Fprintln ( inv . Stderr , "User created successfully." )
_ , _ = fmt . Fprintln ( inv . Stderr , "ID: " + newUser . ID . String ( ) )
_ , _ = fmt . Fprintln ( inv . Stderr , "Username: " + newUser . Username )
_ , _ = fmt . Fprintln ( inv . Stderr , "Email: " + newUser . Email )
_ , _ = fmt . Fprintln ( inv . Stderr , "Password: ********" )
2023-02-06 14:58:21 +00:00
return nil
} ,
}
2023-03-23 22:42:20 +00:00
createAdminUserCommand . Options . Add (
clibase . Option {
Env : "CODER_POSTGRES_URL" ,
Flag : "postgres-url" ,
Description : "URL of a PostgreSQL database. If empty, the built-in PostgreSQL deployment will be used (Coder must not be already running in this case)." ,
Value : clibase . StringOf ( & newUserDBURL ) ,
} ,
clibase . Option {
Env : "CODER_SSH_KEYGEN_ALGORITHM" ,
Flag : "ssh-keygen-algorithm" ,
Description : "The algorithm to use for generating ssh keys. Accepted values are \"ed25519\", \"ecdsa\", or \"rsa4096\"." ,
Default : "ed25519" ,
Value : clibase . StringOf ( & newUserSSHKeygenAlgorithm ) ,
} ,
clibase . Option {
Env : "CODER_USERNAME" ,
Flag : "username" ,
Description : "The username of the new user. If not specified, you will be prompted via stdin." ,
Value : clibase . StringOf ( & newUserUsername ) ,
} ,
clibase . Option {
Env : "CODER_EMAIL" ,
Flag : "email" ,
Description : "The email of the new user. If not specified, you will be prompted via stdin." ,
Value : clibase . StringOf ( & newUserEmail ) ,
} ,
clibase . Option {
Env : "CODER_PASSWORD" ,
Flag : "password" ,
Description : "The password of the new user. If not specified, you will be prompted via stdin." ,
Value : clibase . StringOf ( & newUserPassword ) ,
} ,
)
2023-02-06 14:58:21 +00:00
return createAdminUserCommand
}