Skip to content

Commit

Permalink
Allow root DB password and wiki DB username and password to be changed (
Browse files Browse the repository at this point in the history
#99)

* Add command line support for db passwords

* Fix overloaded password variable

* Add wikidbuser param to add wiki

* Get passwords before environment file is saved

* Move password generation earlier

* Fix name of root DB env var

* Read passwords from file if file exists

* Quote root password env variable

* Reference escape bug, get or generate passwords

* Default wiki DB password to root DB password when username is "root"

* Fix replacement and increase password length

* Update README.md

* Escape double quotes in password
  • Loading branch information
cicalese authored Jan 18, 2024
1 parent c155a51 commit 1a27fc2
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 43 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ sudo canasta create [flags]
- `-s, --password`: Initial wiki admin password.
- `-f, --yamlfile`: Initial wiki yaml file for wiki farm setup.
- `-k, --keep-config`: Keep the config files on installation failure.
- `-r, --override`: Name of a file to copy to docker-compose.override.yml.
- `--rootdbpass`: Read root database user password from .root-db-password file or prompt for it if file does not exist (default password: "mediawiki").
- `--wikidbuser`: The username of the wiki database user (default: "root").
- `--wikidbpass`: Read wiki database user password from .wiki-db-password file or prompt for it if file does not exist (default password: "mediawiki").

**YAML Format for Wiki Farm:**
To create a wiki farm, you first need to create a YAML file with the following format:
Expand Down Expand Up @@ -79,6 +83,9 @@ sudo canasta add [flags]
- `-i, --id`: Canasta instance ID.
- `-o, --orchestrator`: Orchestrator to use for installation (default: "docker-compose").
- `-d, --database`: Path to the existing database dump.
- `-a, --admin`: Admin name of the new wiki.
- `--wikidbuser`: The username of the wiki database user (default: "root").


### remove
**Description:** Removes a wiki from a Canasta instance.
Expand Down
8 changes: 5 additions & 3 deletions cmd/add/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func NewCmdCreate() *cobra.Command {
var databasePath string
var url string
var admin string
var wikidbuser string

addCmd := &cobra.Command{
Use: "add",
Expand All @@ -36,7 +37,7 @@ func NewCmdCreate() *cobra.Command {
log.Fatal(err)
}
fmt.Printf("Adding wiki '%s' to Canasta instance '%s'...\n", wikiName, instance.Id)
err = AddWiki(wikiName, domainName, wikiPath, siteName, databasePath, admin, instance)
err = AddWiki(wikiName, domainName, wikiPath, siteName, databasePath, admin, wikidbuser, instance)
if err != nil {
log.Fatal(err)
}
Expand All @@ -58,11 +59,12 @@ func NewCmdCreate() *cobra.Command {
addCmd.Flags().StringVarP(&instance.Orchestrator, "orchestrator", "o", "docker-compose", "Orchestrator to use for installation")
addCmd.Flags().StringVarP(&databasePath, "database", "d", "", "Path to the existing database dump")
addCmd.Flags().StringVarP(&admin, "admin", "a", "", "Admin name of the new wiki")
addCmd.Flags().StringVar(&wikidbuser, "wikidbuser", "root", "The username of the wiki database user (default: \"root\")")
return addCmd
}

// addWiki accepts the Canasta instance ID, the name, domain and path of the new wiki, and the initial admin info, then creates a new wiki in the instance.
func AddWiki(name, domain, wikipath, siteName, databasePath, admin string, instance config.Installation) error {
func AddWiki(name, domain, wikipath, siteName, databasePath, admin, wikidbuser string, instance config.Installation) error {
var err error

//Checking Installation existence
Expand Down Expand Up @@ -121,7 +123,7 @@ func AddWiki(name, domain, wikipath, siteName, databasePath, admin string, insta
return err
}

err = mediawiki.InstallOne(instance.Path, name, domain, wikipath, admin,instance.Orchestrator)
err = mediawiki.InstallOne(instance.Path, name, domain, admin, wikidbuser, instance.Orchestrator)
if err != nil {
return err
}
Expand Down
12 changes: 10 additions & 2 deletions cmd/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ func NewCmdCreate() *cobra.Command {
keepConfig bool
canastaInfo canasta.CanastaVariables
override string
rootdbpass bool
wikidbpass bool
)
createCmd := &cobra.Command{
Use: "create",
Short: "Create a Canasta installation",
Long: "Creates a Canasta installation using an orchestrator of your choice.",
RunE: func(cmd *cobra.Command, args []string) error {
if name, canastaInfo, err = prompt.PromptUser(name, yamlPath, canastaInfo); err != nil {
if name, canastaInfo, err = prompt.PromptUser(name, yamlPath, rootdbpass, wikidbpass, canastaInfo); err != nil {
log.Fatal(err)
}
if canastaInfo, err = canasta.GeneratePasswords(path, canastaInfo); err != nil {
log.Fatal(err)
}
fmt.Println("Creating Canasta installation '" + canastaInfo.Id + "'...")
Expand Down Expand Up @@ -71,6 +76,9 @@ func NewCmdCreate() *cobra.Command {
createCmd.Flags().StringVarP(&yamlPath, "yamlfile", "f", "", "Initial wiki yaml file")
createCmd.Flags().BoolVarP(&keepConfig, "keep-config", "k", false, "Keep the config files on installation failure")
createCmd.Flags().StringVarP(&override, "override", "r", "", "Name of a file to copy to docker-compose.override.yml")
createCmd.Flags().BoolVar(&rootdbpass, "rootdbpass", false, "Read root database user password from .root-db-password file or prompt for it if file does not exist (default password: \"mediawiki\")")
createCmd.Flags().StringVar(&canastaInfo.WikiDBUsername, "wikidbuser", "root", "The username of the wiki database user (default: \"root\")")
createCmd.Flags().BoolVar(&wikidbpass, "wikidbpass", false, "Read wiki database user password from .wiki-db-password file or prompt for it if file does not exist (default password: \"mediawiki\")")
return createCmd
}

Expand All @@ -88,7 +96,7 @@ func createCanasta(canastaInfo canasta.CanastaVariables, pwd, path, name, domain
if err := canasta.CopyYaml(yamlPath, path); err != nil {
return err
}
if err := canasta.CopyEnv("", path, pwd); err != nil {
if err := canasta.CopyEnv("", path, pwd, canastaInfo.RootDBPassword); err != nil {
return err
}
if err := canasta.CopySettings(path); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/import/importExisting.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func importCanasta(pwd, canastaId, domainName, path, orchestrator, databasePath,
if err := canasta.CloneStackRepo(orchestrator, canastaId, &path); err != nil {
return err
}
if err := canasta.CopyEnv(envPath, path, pwd); err != nil {
if err := canasta.CopyEnv(envPath, path, pwd, ""); err != nil {
return err
}
if err := canasta.CopyDatabase(databasePath, path, pwd); err != nil {
Expand Down
81 changes: 77 additions & 4 deletions internal/canasta/canasta.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import (
"github.com/CanastaWiki/Canasta-CLI-Go/internal/git"
"github.com/CanastaWiki/Canasta-CLI-Go/internal/logging"
"github.com/CanastaWiki/Canasta-CLI-Go/internal/orchestrators"
"github.com/sethvargo/go-password/password"
)

type CanastaVariables struct {
Id string
AdminPassword string
AdminName string
Id string
AdminPassword string
AdminName string
RootDBPassword string
WikiDBUsername string
WikiDBPassword string
}

// CloneStackRepo() accepts the orchestrator from the CLI,
Expand All @@ -36,7 +40,7 @@ func CloneStackRepo(orchestrator, canastaId string, path *string) error {
// if envPath is passed as argument,
// copies the file located at envPath to the installation directory
// else copies .env.example to .env in the installation directory
func CopyEnv(envPath, path, pwd string) error {
func CopyEnv(envPath, path, pwd, rootDBpass string) error {
yamlPath := path + "/config/wikis.yaml"

if envPath == "" {
Expand All @@ -59,6 +63,12 @@ func CopyEnv(envPath, path, pwd string) error {
if err := SaveEnvVariable(path+"/.env", "MW_SITE_FQDN", domainNames[0]); err != nil {
return err
}
if rootDBpass != "" {
pass := "\"" + strings.ReplaceAll(rootDBpass, "\"", "\\\"") + "\""
if err := SaveEnvVariable(path+"/.env", "MYSQL_PASSWORD", pass); err != nil {
return err
}
}
return nil
}

Expand Down Expand Up @@ -266,6 +276,69 @@ func SanityChecks(databasePath, localSettingsPath string) error {
return nil
}

func GeneratePasswords(path string, canastaInfo CanastaVariables) (CanastaVariables, error) {
var err error

canastaInfo.AdminPassword, err = GetOrGenerateAndSavePassword(canastaInfo.AdminPassword, path, "admin", ".admin-password")
if err != nil {
return canastaInfo, err
}

canastaInfo.RootDBPassword, err = GetOrGenerateAndSavePassword(canastaInfo.RootDBPassword, path, "root database", ".root-db-password")
if err != nil {
return canastaInfo, err
}

if (canastaInfo.WikiDBUsername == "root") {
canastaInfo.WikiDBPassword = canastaInfo.RootDBPassword
} else {
canastaInfo.WikiDBPassword, err = GetOrGenerateAndSavePassword(canastaInfo.WikiDBPassword, path, "wiki database", ".wiki-db-password")
if err != nil {
return canastaInfo, err
}
}

return canastaInfo, nil
}

func GetOrGenerateAndSavePassword(pwd, path, prompt, filename string) (string, error) {
var err error
if pwd != "" {
return pwd, nil
}
if pwd, err = GetPasswordFromFile(path, filename); err == nil {
fmt.Printf("Retrieved %s password from %s/%s\n", prompt, path, filename)
return pwd, nil
}
pwd, err = password.Generate(30, 4, 6, false, true)
if err != nil {
return "", err
}
// dollar signs in the root DB password break the installer
// https://phabricator.wikimedia.org/T355013
pwd = strings.ReplaceAll(pwd, "$", "#")
fmt.Printf("Saving %s password to %s/%s\n", prompt, path, filename)
file, err := os.Create(path + "/" + filename)
if err != nil {
return "", err
}
defer file.Close()
_, err = file.WriteString(pwd)
return pwd, err
}

func GetPasswordFromFile(path, filename string) (string, error) {
file, err := os.Open(filepath.Join(path, filename))
if err != nil {
return "", err
}
defer file.Close()

scanner := bufio.NewScanner(file)
scanner.Scan() // get the first line
return scanner.Text(), nil
}

// Make changes to the .env file at the installation directory
func SaveEnvVariable(envPath, key, value string) error {
file, err := os.ReadFile(envPath)
Expand Down
49 changes: 18 additions & 31 deletions internal/mediawiki/mediawiki.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package mediawiki

import (
"bufio"
"errors"
"fmt"
"os"
Expand All @@ -14,7 +13,6 @@ import (
"github.com/CanastaWiki/Canasta-CLI-Go/internal/farmsettings"
"github.com/CanastaWiki/Canasta-CLI-Go/internal/logging"
"github.com/CanastaWiki/Canasta-CLI-Go/internal/orchestrators"
"github.com/sethvargo/go-password/password"
)

const dbServer = "db"
Expand All @@ -25,31 +23,13 @@ func Install(path, yamlPath, orchestrator string, canastaInfo canasta.CanastaVar
var err error
logging.Print("Configuring MediaWiki Installation\n")
logging.Print("Running install.php\n")
envVariables := canasta.GetEnvVariable(path + "/.env")
settingsName := "CommonSettings.php"

command := "/wait-for-it.sh -t 60 db:3306"
output, err := orchestrators.ExecWithError(path, orchestrator, "web", command)
if err != nil {
return canastaInfo, fmt.Errorf(output)
}
if canastaInfo.AdminPassword == "" {
canastaInfo.AdminPassword, err = password.Generate(12, 2, 4, false, true)
if err != nil {
return canastaInfo, err
}
// Save automatically generated password to .admin-password inside the configuration folder
fmt.Printf("Saving password to %s/.admin-password\n", path)
file, err := os.Create(path + "/.admin-password")
if err != nil {
return canastaInfo, err
}
defer file.Close()
_, err = file.WriteString(canastaInfo.AdminPassword)
if err != nil {
return canastaInfo, err
}
}

fmt.Printf("Saving adminname to %s/.admin\n", path)
file, err := os.Create(path + "/.admin")
Expand All @@ -70,8 +50,8 @@ func Install(path, yamlPath, orchestrator string, canastaInfo canasta.CanastaVar
wikiName := WikiNames[i]
domainName := domainNames[i]

command := fmt.Sprintf("php maintenance/install.php --skins='Vector' --dbserver=%s --dbname='%s' --confpath=%s --scriptpath=%s --server='https://%s' --dbuser='%s' --dbpass='%s' --pass='%s' '%s' '%s'",
dbServer, wikiName, confPath, scriptPath, domainName, "root", envVariables["MYSQL_PASSWORD"], canastaInfo.AdminPassword, wikiName, canastaInfo.AdminName)
command := fmt.Sprintf("php maintenance/install.php --skins='Vector' --dbserver=%s --dbname='%s' --confpath=%s --scriptpath=%s --server='https://%s' --installdbuser='%s' --installdbpass='%s' --dbuser='%s' --dbpass='%s' --pass='%s' '%s' '%s'",
dbServer, wikiName, confPath, scriptPath, domainName, "root", canastaInfo.RootDBPassword, canastaInfo.WikiDBUsername, canastaInfo.WikiDBPassword, canastaInfo.AdminPassword, wikiName, canastaInfo.AdminName)

output, err = orchestrators.ExecWithError(path, orchestrator, "web", command)
if err != nil {
Expand Down Expand Up @@ -103,7 +83,7 @@ func Install(path, yamlPath, orchestrator string, canastaInfo canasta.CanastaVar
return canastaInfo, err
}

func InstallOne(path, name, domain, wikipath, admin, orchestrator string) error {
func InstallOne(path, name, domain, admin, dbuser, orchestrator string) error {
var err error
logging.Print("Configuring MediaWiki Installation\n")
logging.Print("Running install.php\n")
Expand Down Expand Up @@ -136,18 +116,25 @@ func InstallOne(path, name, domain, wikipath, admin, orchestrator string) error
}
}

file, err := os.Open(filepath.Join(path, ".admin-password"))
installdbuser := "root"
installdbpass := envVariables["MYSQL_PASSWORD"]
var dbpass string
if dbuser != installdbuser {
dbpass, err = canasta.GetPasswordFromFile(path, ".wiki-db-password")
if err != nil {
return err
}
} else {
dbpass = installdbpass
}

AdminPassword, err := canasta.GetPasswordFromFile(path, ".admin-password")
if err != nil {
return err
}
defer file.Close()

scanner := bufio.NewScanner(file)
scanner.Scan() // get the first line
AdminPassword := scanner.Text()

command = fmt.Sprintf("php maintenance/install.php --skins='Vector' --dbserver=%s --dbname='%s' --confpath=%s --scriptpath=%s --server='https://%s' --dbuser='%s' --dbpass='%s' --pass='%s' '%s' '%s'",
dbServer, name, confPath, scriptPath, domain, "root", envVariables["MYSQL_PASSWORD"], AdminPassword, name, admin)
command = fmt.Sprintf("php maintenance/install.php --skins='Vector' --dbserver=%s --dbname='%s' --confpath=%s --scriptpath=%s --server='https://%s' --installdbuser='%s' --installdbpass='%s' --dbuser='%s' --dbpass='%s' --pass='%s' '%s' '%s'",
dbServer, name, confPath, scriptPath, domain, installdbuser, installdbpass, dbuser, dbpass, AdminPassword, name, admin)
output, err = orchestrators.ExecWithError(path, orchestrator, "web", command)
if err != nil {
return fmt.Errorf(output)
Expand Down
38 changes: 36 additions & 2 deletions internal/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"golang.org/x/term"
)

func PromptUser(name, yamlPath string, canastaInfo canasta.CanastaVariables) (string, canasta.CanastaVariables, error) {
func PromptUser(name, yamlPath string, rootdbpass bool, wikidbpass bool, canastaInfo canasta.CanastaVariables) (string, canasta.CanastaVariables, error) {
var err error
if yamlPath == "" {
if name, err = promptForInput(name, "WikiID"); err != nil {
Expand All @@ -27,6 +27,12 @@ func PromptUser(name, yamlPath string, canastaInfo canasta.CanastaVariables) (st
if canastaInfo.AdminName, canastaInfo.AdminPassword, err = promptForUserPassword(canastaInfo.AdminName, canastaInfo.AdminPassword); err != nil {
return name, canastaInfo, err
}
if canastaInfo.RootDBPassword, err = promptForDBPassword("root", rootdbpass); err != nil {
return name, canastaInfo, err
}
if canastaInfo.WikiDBPassword, err = promptForDBPassword("wiki", wikidbpass); err != nil {
return name, canastaInfo, err
}
return name, canastaInfo, nil
}

Expand Down Expand Up @@ -106,7 +112,7 @@ func promptForUserPassword(username, password string) (string, string, error) {
}

func getAndConfirmPassword(username string) (string, string, error) {
fmt.Print("Enter the admin password (Press Enter to autogenerate the password): \n")
fmt.Print("Enter the admin password (Press Enter to get saved password or, if one does not exist, autogenerate a password): \n")
password, err := getPasswordInput()
if err != nil {
return "", "", err
Expand All @@ -122,6 +128,34 @@ func getAndConfirmPassword(username string) (string, string, error) {
return username, password, nil
}

func promptForDBPassword(whichpass string, passflag bool) (string, error) {
var password = "mediawiki"
var err error
if passflag {
if password, err = getAndConfirmDBPassword(whichpass); err != nil {
return "", err
}
}
return password, nil
}

func getAndConfirmDBPassword(whichpass string) (string, error) {
fmt.Printf("Enter the %s database password (Press Enter to get saved password or, if one does not exist, autogenerate a password): \n", whichpass)
password, err := getPasswordInput()
if err != nil {
return "", err
}
if password == "" {
return "", nil
}
fmt.Printf("Re-enter the %s database password: \n", whichpass)
confirmedPassword, err := getPasswordInput()
if err != nil || password != confirmedPassword {
return "", fmt.Errorf("Passwords do not match, please try again.")
}
return password, nil
}

func validateWikiID(id string) error {
// Check if the ID contains a hyphen (-)
if strings.Contains(id, "-") {
Expand Down

0 comments on commit 1a27fc2

Please sign in to comment.