maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package ctl
 20
 21import (
 22	"errors"
 23	"fmt"
 24	"os"
 25	"strings"
 26
 27	"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)
 34
 35func init() {
 36	maddycli.AddSubcommand(
 37		&cli.Command{
 38			Name:  "creds",
 39			Usage: "Local credentials management",
 40			Description: `These commands manipulate credential databases used by 
 41maddy mail server.
 42
 43Corresponding credential database should be defined in maddy.conf as
 44a top-level config block. By default the block name should be local_authdb (
 45can be changed using --cfg-block argument for subcommands).
 46
 47Note that it is not enough to create user credentials in order to grant 
 48IMAP 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 err
 66						}
 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.
 75
 76If configuration block uses auth.pass_table, then hash algorithm can be configured
 77using 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 err
107						}
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 err
133						}
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 err
160						}
161						defer closeIfNeeded(be)
162						return usersPassword(be, ctx)
163					},
164				},
165			},
166		})
167}
168
169func usersList(be module.PlainUserDB, ctx *cli.Context) error {
170	list, err := be.ListUsers()
171	if err != nil {
172		return err
173	}
174
175	if len(list) == 0 && !ctx.Bool("quiet") {
176		fmt.Fprintln(os.Stderr, "No users.")
177	}
178
179	for _, user := range list {
180		fmt.Println(user)
181	}
182	return nil
183}
184
185func 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	}
190
191	var pass string
192	if ctx.IsSet("password") {
193		pass = ctx.String("password")
194	} else {
195		var err error
196		pass, err = clitools2.ReadPassword("Enter password for new user")
197		if err != nil {
198			return err
199		}
200	}
201
202	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}
212
213func 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	}
218
219	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	}
224
225	return be.DeleteUser(username)
226}
227
228func 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	}
233
234	var pass string
235	if ctx.IsSet("password") {
236		pass = ctx.String("password")
237	} else {
238		var err error
239		pass, err = clitools2.ReadPassword("Enter new password")
240		if err != nil {
241			return err
242		}
243	}
244
245	return be.SetUserPassword(username, pass)
246}