1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package ctl2021import (22 "errors"23 "fmt"24 "os"25 "strings"2627 "github.com/foxcpp/maddy/framework/module"28 "github.com/foxcpp/maddy/internal/auth/pass_table"29 maddycli "github.com/foxcpp/maddy/internal/cli"30 clitools2 "github.com/foxcpp/maddy/internal/cli/clitools"31 "github.com/urfave/cli/v2"32 "golang.org/x/crypto/bcrypt"33)3435func init() {36 maddycli.AddSubcommand(37 &cli.Command{38 Name: "creds",39 Usage: "Local credentials management",40 Description: `These commands manipulate credential databases used by41maddy mail server.4243Corresponding credential database should be defined in maddy.conf as44a top-level config block. By default the block name should be local_authdb (45can be changed using --cfg-block argument for subcommands).4647Note that it is not enough to create user credentials in order to grant48IMAP access - IMAP account should be also created using 'imap-acct create' subcommand.49`,50 Subcommands: []*cli.Command{51 {52 Name: "list",53 Usage: "List created credentials",54 Flags: []cli.Flag{55 &cli.StringFlag{56 Name: "cfg-block",57 Usage: "Module configuration block to use",58 EnvVars: []string{"MADDY_CFGBLOCK"},59 Value: "local_authdb",60 },61 },62 Action: func(ctx *cli.Context) error {63 be, err := openUserDB(ctx)64 if err != nil {65 return err66 }67 defer closeIfNeeded(be)68 return usersList(be, ctx)69 },70 },71 {72 Name: "create",73 Usage: "Create user account",74 Description: `Reads password from stdin.7576If configuration block uses auth.pass_table, then hash algorithm can be configured77using command flags. Otherwise, these options cannot be used.78`,79 ArgsUsage: "USERNAME",80 Flags: []cli.Flag{81 &cli.StringFlag{82 Name: "cfg-block",83 Usage: "Module configuration block to use",84 EnvVars: []string{"MADDY_CFGBLOCK"},85 Value: "local_authdb",86 },87 &cli.StringFlag{88 Name: "password",89 Aliases: []string{"p"},90 Usage: "Use `PASSWORD instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!",91 },92 &cli.StringFlag{93 Name: "hash",94 Usage: "Use specified hash algorithm. Valid values: " + strings.Join(pass_table.Hashes, ", "),95 Value: "bcrypt",96 },97 &cli.IntFlag{98 Name: "bcrypt-cost",99 Usage: "Specify bcrypt cost value",100 Value: bcrypt.DefaultCost,101 },102 },103 Action: func(ctx *cli.Context) error {104 be, err := openUserDB(ctx)105 if err != nil {106 return err107 }108 defer closeIfNeeded(be)109 return usersCreate(be, ctx)110 },111 },112 {113 Name: "remove",114 Usage: "Delete user account",115 ArgsUsage: "USERNAME",116 Flags: []cli.Flag{117 &cli.StringFlag{118 Name: "cfg-block",119 Usage: "Module configuration block to use",120 EnvVars: []string{"MADDY_CFGBLOCK"},121 Value: "local_authdb",122 },123 &cli.BoolFlag{124 Name: "yes",125 Aliases: []string{"y"},126 Usage: "Don't ask for confirmation",127 },128 },129 Action: func(ctx *cli.Context) error {130 be, err := openUserDB(ctx)131 if err != nil {132 return err133 }134 defer closeIfNeeded(be)135 return usersRemove(be, ctx)136 },137 },138 {139 Name: "password",140 Usage: "Change account password",141 Description: "Reads password from stdin",142 ArgsUsage: "USERNAME",143 Flags: []cli.Flag{144 &cli.StringFlag{145 Name: "cfg-block",146 Usage: "Module configuration block to use",147 EnvVars: []string{"MADDY_CFGBLOCK"},148 Value: "local_authdb",149 },150 &cli.StringFlag{151 Name: "password",152 Aliases: []string{"p"},153 Usage: "Use `PASSWORD` instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!",154 },155 },156 Action: func(ctx *cli.Context) error {157 be, err := openUserDB(ctx)158 if err != nil {159 return err160 }161 defer closeIfNeeded(be)162 return usersPassword(be, ctx)163 },164 },165 },166 })167}168169func usersList(be module.PlainUserDB, ctx *cli.Context) error {170 list, err := be.ListUsers()171 if err != nil {172 return err173 }174175 if len(list) == 0 && !ctx.Bool("quiet") {176 fmt.Fprintln(os.Stderr, "No users.")177 }178179 for _, user := range list {180 fmt.Println(user)181 }182 return nil183}184185func usersCreate(be module.PlainUserDB, ctx *cli.Context) error {186 username := ctx.Args().First()187 if username == "" {188 return cli.Exit("Error: USERNAME is required", 2)189 }190191 var pass string192 if ctx.IsSet("password") {193 pass = ctx.String("password")194 } else {195 var err error196 pass, err = clitools2.ReadPassword("Enter password for new user")197 if err != nil {198 return err199 }200 }201202 if beHash, ok := be.(*pass_table.Auth); ok {203 return beHash.CreateUserHash(username, pass, ctx.String("hash"), pass_table.HashOpts{204 BcryptCost: ctx.Int("bcrypt-cost"),205 })206 } else if ctx.IsSet("hash") || ctx.IsSet("bcrypt-cost") {207 return cli.Exit("Error: --hash cannot be used with non-pass_table credentials DB", 2)208 } else {209 return be.CreateUser(username, pass)210 }211}212213func usersRemove(be module.PlainUserDB, ctx *cli.Context) error {214 username := ctx.Args().First()215 if username == "" {216 return errors.New("Error: USERNAME is required")217 }218219 if !ctx.Bool("yes") {220 if !clitools2.Confirmation("Are you sure you want to delete this user account?", false) {221 return errors.New("Cancelled")222 }223 }224225 return be.DeleteUser(username)226}227228func usersPassword(be module.PlainUserDB, ctx *cli.Context) error {229 username := ctx.Args().First()230 if username == "" {231 return errors.New("Error: USERNAME is required")232 }233234 var pass string235 if ctx.IsSet("password") {236 pass = ctx.String("password")237 } else {238 var err error239 pass, err = clitools2.ReadPassword("Enter new password")240 if err != nil {241 return err242 }243 }244245 return be.SetUserPassword(username, pass)246}