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 pass_table
 20
 21import (
 22	"context"
 23	"fmt"
 24	"strings"
 25
 26	"github.com/foxcpp/maddy/framework/config"
 27	modconfig "github.com/foxcpp/maddy/framework/config/module"
 28	"github.com/foxcpp/maddy/framework/module"
 29	"golang.org/x/crypto/bcrypt"
 30	"golang.org/x/text/secure/precis"
 31)
 32
 33type Auth struct {
 34	modName    string
 35	instName   string
 36	inlineArgs []string
 37
 38	table module.Table
 39}
 40
 41func New(modName, instName string, _, inlineArgs []string) (module.Module, error) {
 42	return &Auth{
 43		modName:    modName,
 44		instName:   instName,
 45		inlineArgs: inlineArgs,
 46	}, nil
 47}
 48
 49func (a *Auth) Init(cfg *config.Map) error {
 50	if len(a.inlineArgs) != 0 {
 51		return modconfig.ModuleFromNode("table", a.inlineArgs, cfg.Block, cfg.Globals, &a.table)
 52	}
 53
 54	cfg.Custom("table", false, true, nil, modconfig.TableDirective, &a.table)
 55	_, err := cfg.Process()
 56	return err
 57}
 58
 59func (a *Auth) Name() string {
 60	return a.modName
 61}
 62
 63func (a *Auth) InstanceName() string {
 64	return a.instName
 65}
 66
 67func (a *Auth) Lookup(ctx context.Context, username string) (string, bool, error) {
 68	key, err := precis.UsernameCaseMapped.CompareKey(username)
 69	if err != nil {
 70		return "", false, err
 71	}
 72
 73	return a.table.Lookup(ctx, key)
 74}
 75
 76func (a *Auth) AuthPlain(username, password string) error {
 77	key, err := precis.UsernameCaseMapped.CompareKey(username)
 78	if err != nil {
 79		return err
 80	}
 81
 82	hash, ok, err := a.table.Lookup(context.TODO(), key)
 83	if !ok {
 84		return module.ErrUnknownCredentials
 85	}
 86	if err != nil {
 87		return err
 88	}
 89
 90	parts := strings.SplitN(hash, ":", 2)
 91	if len(parts) != 2 {
 92		return fmt.Errorf("%s: auth plain %s: no hash tag", a.modName, key)
 93	}
 94	hashVerify := HashVerify[parts[0]]
 95	if hashVerify == nil {
 96		return fmt.Errorf("%s: auth plain %s: unknown hash: %s", a.modName, key, parts[0])
 97	}
 98	return hashVerify(password, parts[1])
 99}
100
101func (a *Auth) ListUsers() ([]string, error) {
102	tbl, ok := a.table.(module.MutableTable)
103	if !ok {
104		return nil, fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)
105	}
106
107	l, err := tbl.Keys()
108	if err != nil {
109		return nil, fmt.Errorf("%s: list users: %w", a.modName, err)
110	}
111	return l, nil
112}
113
114func (a *Auth) CreateUser(username, password string) error {
115	return a.CreateUserHash(username, password, HashBcrypt, HashOpts{
116		BcryptCost: bcrypt.DefaultCost,
117	})
118}
119
120func (a *Auth) CreateUserHash(username, password string, hashAlgo string, opts HashOpts) error {
121	tbl, ok := a.table.(module.MutableTable)
122	if !ok {
123		return fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)
124	}
125
126	if _, ok := HashCompute[hashAlgo]; !ok {
127		return fmt.Errorf("%s: unknown hash function: %v", a.modName, hashAlgo)
128	}
129
130	key, err := precis.UsernameCaseMapped.CompareKey(username)
131	if err != nil {
132		return fmt.Errorf("%s: create user %s (raw): %w", a.modName, username, err)
133	}
134
135	_, ok, err = tbl.Lookup(context.TODO(), key)
136	if err != nil {
137		return fmt.Errorf("%s: create user %s: %w", a.modName, key, err)
138	}
139	if ok {
140		return fmt.Errorf("%s: credentials for %s already exist", a.modName, key)
141	}
142
143	hash, err := HashCompute[hashAlgo](opts, password)
144	if err != nil {
145		return fmt.Errorf("%s: create user %s: hash generation: %w", a.modName, key, err)
146	}
147
148	if err := tbl.SetKey(key, hashAlgo+":"+hash); err != nil {
149		return fmt.Errorf("%s: create user %s: %w", a.modName, key, err)
150	}
151	return nil
152}
153
154func (a *Auth) SetUserPassword(username, password string) error {
155	tbl, ok := a.table.(module.MutableTable)
156	if !ok {
157		return fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)
158	}
159
160	key, err := precis.UsernameCaseMapped.CompareKey(username)
161	if err != nil {
162		return fmt.Errorf("%s: set password %s (raw): %w", a.modName, username, err)
163	}
164
165	// TODO: Allow to customize hash function.
166	hash, err := HashCompute[HashBcrypt](HashOpts{
167		BcryptCost: bcrypt.DefaultCost,
168	}, password)
169	if err != nil {
170		return fmt.Errorf("%s: set password %s: hash generation: %w", a.modName, key, err)
171	}
172
173	if err := tbl.SetKey(key, "bcrypt:"+hash); err != nil {
174		return fmt.Errorf("%s: set password %s: %w", a.modName, key, err)
175	}
176	return nil
177}
178
179func (a *Auth) DeleteUser(username string) error {
180	tbl, ok := a.table.(module.MutableTable)
181	if !ok {
182		return fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)
183	}
184
185	key, err := precis.UsernameCaseMapped.CompareKey(username)
186	if err != nil {
187		return fmt.Errorf("%s: del user %s (raw): %w", a.modName, username, err)
188	}
189
190	if err := tbl.RemoveKey(key); err != nil {
191		return fmt.Errorf("%s: del user %s: %w", a.modName, key, err)
192	}
193	return nil
194}
195
196func init() {
197	module.Register("auth.pass_table", New)
198}