Ctrl K
ComunidadGuías…

cli/server_createadminuser.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//go:build !slim

package cli

import (
	"database/sql"
	"fmt"
	"sort"

	"github.com/google/uuid"
	"golang.org/x/xerrors"

	"cdr.dev/slog/v3"
	"cdr.dev/slog/v3/sloggers/sloghuman"
	"github.com/coder/coder/v2/cli/cliui"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/awsiamrds"
	"github.com/coder/coder/v2/coderd/database/dbtime"
	"github.com/coder/coder/v2/coderd/gitsshkey"
	"github.com/coder/coder/v2/coderd/httpapi"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/userpassword"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/serpent"
)

type organizationMembership struct {
	OrganizationID   uuid.UUID `json:"organization_id"`
	OrganizationName string    `json:"organization_name"`
	Roles            []string  `json:"roles"`
}

// createAdminUserResponse never includes the password. This command talks to
// the database directly, so there is no codersdk type to reuse.
type createAdminUserResponse struct {
	ID            uuid.UUID                `json:"id"`
	Username      string                   `json:"username"`
	Email         string                   `json:"email"`
	Roles         []string                 `json:"roles"`
	Organizations []organizationMembership `json:"organizations"`
}

func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
	var (
		newUserDBURL              string
		newUserPgAuth             string
		newUserSSHKeygenAlgorithm string
		newUserUsername           string
		newUserEmail              string
		newUserPassword           string
		formatter                 = cliui.NewOutputFormatter(
			cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
				typed, ok := data.(createAdminUserResponse)
				if !ok {
					return "", xerrors.Errorf("expected createAdminUserResponse, got %T", data)
				}
				return fmt.Sprintf(
					"\nUser created successfully.\nID:       %s\nUsername: %s\nEmail:    %s\nPassword: ********\n",
					typed.ID, typed.Username, typed.Email,
				), nil
			}),
			cliui.JSONFormat(),
		)
	)
	createAdminUserCommand := &serpent.Command{
		Use:   "create-admin-user",
		Short: "Create a new admin user with the given username, email and password and adds it to every organization.",
		Handler: func(inv *serpent.Invocation) error {
			ctx := inv.Context()

			sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(newUserSSHKeygenAlgorithm)
			if err != nil {
				return xerrors.Errorf("parse ssh keygen algorithm %q: %w", newUserSSHKeygenAlgorithm, err)
			}

			cfg := r.createConfig()
			logger := inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr))
			if r.verbose {
				logger = logger.Leveled(slog.LevelDebug)
			}

			ctx, cancel := inv.SignalNotifyContext(ctx, StopSignals...)
			defer cancel()

			if newUserDBURL == "" {
				cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)", cfg.PostgresPath())
				url, closePg, err := startBuiltinPostgres(ctx, cfg, logger, "")
				if err != nil {
					return err
				}
				defer func() {
					_ = closePg()
				}()
				newUserDBURL = url
			}

			sqlDriver := "postgres"
			if codersdk.PostgresAuth(newUserPgAuth) == codersdk.PostgresAuthAWSIAMRDS {
				sqlDriver, err = awsiamrds.Register(inv.Context(), sqlDriver)
				if err != nil {
					return xerrors.Errorf("register aws rds iam auth: %w", err)
				}
			}

			sqlDB, err := ConnectToPostgres(ctx, logger, sqlDriver, newUserDBURL, nil)
			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.CreateUserRequestWithOrgs{
					Username:        "username",
					Name:            "Admin User",
					Email:           "email@coder.com",
					Password:        "ValidPa$$word123!",
					OrganizationIDs: []uuid.UUID{uuid.New()},
				}
				if username != "" {
					req.Username = username
				}
				if email != "" {
					req.Email = email
				}
				if password != "" {
					req.Password = password
				}

				return httpapi.Validate.Struct(req)
			}

			if newUserUsername == "" {
				newUserUsername, err = cliui.Prompt(inv, cliui.PromptOptions{
					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 == "" {
				newUserEmail, err = cliui.Prompt(inv, cliui.PromptOptions{
					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 == "" {
				newUserPassword, err = cliui.Prompt(inv, cliui.PromptOptions{
					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.
				_, err = cliui.Prompt(inv, cliui.PromptOptions{
					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
			var memberships []organizationMembership
			err = db.InTx(func(tx database.Store) error {
				orgs, err := tx.GetOrganizations(ctx, database.GetOrganizationsParams{})
				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
				})

				_, _ = fmt.Fprintln(inv.Stderr, "Creating user...")
				newUser, err = tx.InsertUser(ctx, database.InsertUserParams{
					ID:               uuid.New(),
					Email:            newUserEmail,
					Username:         newUserUsername,
					Name:             "Admin User",
					HashedPassword:   []byte(hashedPassword),
					CreatedAt:        dbtime.Now(),
					UpdatedAt:        dbtime.Now(),
					RBACRoles:        []string{rbac.RoleOwner().String()},
					LoginType:        database.LoginTypePassword,
					Status:           "",
					IsServiceAccount: false,
				})
				if err != nil {
					return xerrors.Errorf("insert user: %w", err)
				}

				_, _ = fmt.Fprintln(inv.Stderr, "Generating user SSH key...")
				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:       dbtime.Now(),
					UpdatedAt:       dbtime.Now(),
					PrivateKey:      privateKey,
					PrivateKeyKeyID: sql.NullString{}, // Plaintext; this CLI bypasses dbcrypt. Encrypted on next rotate.
					PublicKey:       publicKey,
				})
				if err != nil {
					return xerrors.Errorf("insert user gitsshkey: %w", err)
				}

				for _, org := range orgs {
					_, _ = fmt.Fprintf(inv.Stderr, "Adding user to organization %q (%s) as admin...\n", org.Name, org.ID.String())
					roles := []string{rbac.RoleOrgAdmin()}
					_, err := tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
						OrganizationID: org.ID,
						UserID:         newUser.ID,
						CreatedAt:      dbtime.Now(),
						UpdatedAt:      dbtime.Now(),
						Roles:          roles,
					})
					if err != nil {
						return xerrors.Errorf("insert organization member: %w", err)
					}
					memberships = append(memberships, organizationMembership{
						OrganizationID:   org.ID,
						OrganizationName: org.Name,
						Roles:            roles,
					})
				}

				return nil
			}, nil)
			if err != nil {
				return err
			}

			out, err := formatter.Format(ctx, createAdminUserResponse{
				ID:            newUser.ID,
				Username:      newUser.Username,
				Email:         newUser.Email,
				Roles:         newUser.RBACRoles,
				Organizations: memberships,
			})
			if err != nil {
				return err
			}
			if formatter.FormatID() == "json" {
				_, err = fmt.Fprintln(inv.Stdout, out)
			} else {
				_, err = fmt.Fprint(inv.Stderr, out)
			}
			return err
		},
	}

	createAdminUserCommand.Options.Add(
		serpent.Option{
			Env:         "CODER_PG_CONNECTION_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:       serpent.StringOf(&newUserDBURL),
		},
		serpent.Option{
			Name:        "Postgres Connection Auth",
			Description: "Type of auth to use when connecting to postgres.",
			Flag:        "postgres-connection-auth",
			Env:         "CODER_PG_CONNECTION_AUTH",
			Default:     "password",
			Value:       serpent.EnumOf(&newUserPgAuth, codersdk.PostgresAuthDrivers...),
		},
		serpent.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:       serpent.StringOf(&newUserSSHKeygenAlgorithm),
		},
		serpent.Option{
			Env:         "CODER_USERNAME",
			Flag:        "username",
			Description: "The username of the new user. If not specified, you will be prompted via stdin.",
			Value:       serpent.StringOf(&newUserUsername),
		},
		serpent.Option{
			Env:         "CODER_EMAIL",
			Flag:        "email",
			Description: "The email of the new user. If not specified, you will be prompted via stdin.",
			Value:       serpent.StringOf(&newUserEmail),
		},
		serpent.Option{
			Env:         "CODER_PASSWORD",
			Flag:        "password",
			Description: "The password of the new user. If not specified, you will be prompted via stdin.",
			Value:       serpent.StringOf(&newUserPassword),
		},
	)
	formatter.AttachOptions(&createAdminUserCommand.Options)

	return createAdminUserCommand
}