mirror of
https://github.com/foxcpp/maddy.git
synced 2025-04-05 05:57:39 +03:00
Implement table-based authentication provider
This commit is contained in:
parent
a91d8c2334
commit
cdd01c8e37
10 changed files with 450 additions and 1 deletions
77
cmd/maddyctl/hash.go
Normal file
77
cmd/maddyctl/hash.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/foxcpp/maddy/cmd/maddyctl/clitools"
|
||||||
|
"github.com/foxcpp/maddy/internal/auth/pass_table"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hashCommand(ctx *cli.Context) error {
|
||||||
|
hashFunc := ctx.String("hash")
|
||||||
|
if hashFunc == "" {
|
||||||
|
hashFunc = pass_table.DefaultHash
|
||||||
|
}
|
||||||
|
|
||||||
|
hashCompute := pass_table.HashCompute[hashFunc]
|
||||||
|
if hashCompute == nil {
|
||||||
|
var funcs []string
|
||||||
|
for k := range pass_table.HashCompute {
|
||||||
|
funcs = append(funcs, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Error: Unknown hash function, available: %s", strings.Join(funcs, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := pass_table.HashOpts{
|
||||||
|
BcryptCost: bcrypt.DefaultCost,
|
||||||
|
Argon2Memory: 1024,
|
||||||
|
Argon2Time: 2,
|
||||||
|
Argon2Threads: 1,
|
||||||
|
}
|
||||||
|
if ctx.IsSet("bcrypt-cost") {
|
||||||
|
if ctx.Int("bcrypt-cost") > bcrypt.MaxCost {
|
||||||
|
return errors.New("Error: too big bcrypt cost")
|
||||||
|
}
|
||||||
|
if ctx.Int("bcrypt-cost") < bcrypt.MinCost {
|
||||||
|
return errors.New("Error: too small bcrypt cost")
|
||||||
|
}
|
||||||
|
opts.BcryptCost = ctx.Int("bcrypt-cost")
|
||||||
|
}
|
||||||
|
if ctx.IsSet("argon2-memory") {
|
||||||
|
opts.Argon2Memory = uint32(ctx.Int("argon2-memory"))
|
||||||
|
}
|
||||||
|
if ctx.IsSet("argon2-time") {
|
||||||
|
opts.Argon2Memory = uint32(ctx.Int("argon2-time"))
|
||||||
|
}
|
||||||
|
if ctx.IsSet("argon2-threads") {
|
||||||
|
opts.Argon2Threads = uint8(ctx.Int("argon2-threads"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var pass string
|
||||||
|
if ctx.IsSet("password") {
|
||||||
|
pass = ctx.String("password")
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
pass, err = clitools.ReadPassword("Password")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pass == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "WARNING: This is the hash of empty string")
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := hashCompute(opts, pass)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(hashFunc + ":" + hash)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -558,6 +558,42 @@ func main() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "hash",
|
||||||
|
Usage: "Generate password hashes for use with pass_table",
|
||||||
|
Action: hashCommand,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "password,p",
|
||||||
|
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!",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "hash",
|
||||||
|
Usage: "Use specified hash algorithm",
|
||||||
|
Value: "bcrypt",
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "bcrypt-cost",
|
||||||
|
Usage: "Specify bcrypt cost value",
|
||||||
|
Value: bcrypt.DefaultCost,
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "argon2-time",
|
||||||
|
Usage: "Time factor for Argon2id",
|
||||||
|
Value: 3,
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "argon2-memory",
|
||||||
|
Usage: "Memory in KiB to use for Argon2id",
|
||||||
|
Value: 1024,
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "argon2-threads",
|
||||||
|
Usage: "Threads to use for Argon2id",
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Run(os.Args); err != nil {
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
|
|
@ -145,9 +145,40 @@ chown root:maddy /usr/lib/maddy/maddy-shadow-helper
|
||||||
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-shadow-helper
|
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-shadow-helper
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Table-based password hash lookup (pass_table)
|
||||||
|
|
||||||
|
This module implements username:password authentication by looking up the
|
||||||
|
password hash using a table module (maddy-tables(5)). It can be used
|
||||||
|
to load user credentials from text file (file_table module) or SQL query
|
||||||
|
(sql_table module).
|
||||||
|
|
||||||
|
|
||||||
|
Definition:
|
||||||
|
```
|
||||||
|
pass_table <table> [table arguments] {
|
||||||
|
[additional table config]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example, read username:password pair from the text file:
|
||||||
|
```
|
||||||
|
smtp tcp://0.0.0.0:587 {
|
||||||
|
auth pass_table file_table /etc/maddy/smtp_passwd
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password hashes
|
||||||
|
|
||||||
|
pass_table expects the used table to contain certain structured values with
|
||||||
|
hash algorithm name, salt and other necessary parameters.
|
||||||
|
|
||||||
|
You should use 'maddyctl hash' command to generate suitable values.
|
||||||
|
See 'maddyctl hash --help' for details.
|
||||||
|
|
||||||
# Separate username and password lookup (plain_separate)
|
# Separate username and password lookup (plain_separate)
|
||||||
|
|
||||||
This modules implements authentication using username:password pairs but can
|
This module implements authentication using username:password pairs but can
|
||||||
use zero or more "table modules" (maddy-tables(5)) and one or more
|
use zero or more "table modules" (maddy-tables(5)) and one or more
|
||||||
authentication providers to verify credentials.
|
authentication providers to verify credentials.
|
||||||
|
|
||||||
|
|
1
go.sum
1
go.sum
|
@ -136,6 +136,7 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vK
|
||||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
|
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
|
||||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
|
162
internal/auth/pass_table/hash.go
Normal file
162
internal/auth/pass_table/hash.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
package pass_table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HashSHA256 = "sha256"
|
||||||
|
HashBcrypt = "bcrypt"
|
||||||
|
HashArgon2 = "argon2"
|
||||||
|
|
||||||
|
DefaultHash = HashBcrypt
|
||||||
|
|
||||||
|
Argon2Salt = 16
|
||||||
|
Argon2Size = 64
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// HashOpts is the structure that holds additional parameters for used hash
|
||||||
|
// functions. They are used for new passwords.
|
||||||
|
//
|
||||||
|
// These parameters should be stored together with the hashed password
|
||||||
|
// so it can be verified independently of the used HashOpts.
|
||||||
|
HashOpts struct {
|
||||||
|
// Bcrypt cost value to use. Should be at least 10.
|
||||||
|
BcryptCost int
|
||||||
|
|
||||||
|
Argon2Time uint32
|
||||||
|
Argon2Memory uint32
|
||||||
|
Argon2Threads uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
FuncHashCompute func(opts HashOpts, pass string) (string, error)
|
||||||
|
FuncHashVerify func(pass, hashSalt string) error
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
HashCompute = map[string]FuncHashCompute{
|
||||||
|
HashBcrypt: computeBcrypt,
|
||||||
|
HashArgon2: computeArgon2,
|
||||||
|
}
|
||||||
|
HashVerify = map[string]FuncHashVerify{
|
||||||
|
HashBcrypt: verifyBcrypt,
|
||||||
|
HashArgon2: verifyArgon2,
|
||||||
|
}
|
||||||
|
|
||||||
|
Hashes = []string{HashSHA256, HashBcrypt, HashArgon2}
|
||||||
|
)
|
||||||
|
|
||||||
|
func computeArgon2(opts HashOpts, pass string) (string, error) {
|
||||||
|
salt := make([]byte, Argon2Salt)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||||
|
return "", fmt.Errorf("pass_table: failed to generate salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := argon2.IDKey([]byte(pass), salt, opts.Argon2Time, opts.Argon2Memory, opts.Argon2Threads, Argon2Size)
|
||||||
|
var out strings.Builder
|
||||||
|
out.WriteString(strconv.FormatUint(uint64(opts.Argon2Time), 10))
|
||||||
|
out.WriteRune(':')
|
||||||
|
out.WriteString(strconv.FormatUint(uint64(opts.Argon2Memory), 10))
|
||||||
|
out.WriteRune(':')
|
||||||
|
out.WriteString(strconv.FormatUint(uint64(opts.Argon2Threads), 10))
|
||||||
|
out.WriteRune(':')
|
||||||
|
out.WriteString(base64.StdEncoding.EncodeToString(salt))
|
||||||
|
out.WriteRune(':')
|
||||||
|
out.WriteString(base64.StdEncoding.EncodeToString(hash))
|
||||||
|
return out.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyArgon2(pass, hashSalt string) error {
|
||||||
|
parts := strings.SplitN(hashSalt, ":", 5)
|
||||||
|
|
||||||
|
time, err := strconv.ParseUint(parts[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass_table: malformed hash string: %w", err)
|
||||||
|
}
|
||||||
|
memory, err := strconv.ParseUint(parts[1], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass_table: malformed hash string: %w", err)
|
||||||
|
}
|
||||||
|
threads, err := strconv.ParseUint(parts[2], 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass_table: malformed hash string: %w", err)
|
||||||
|
}
|
||||||
|
salt, err := base64.StdEncoding.DecodeString(parts[3])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass_table: malformed hash string: %w", err)
|
||||||
|
}
|
||||||
|
hash, err := base64.StdEncoding.DecodeString(parts[4])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass_table: malformed hash string: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
passHash := argon2.IDKey([]byte(pass), salt, uint32(time), uint32(memory), uint8(threads), Argon2Size)
|
||||||
|
if subtle.ConstantTimeCompare(passHash, hash) != 1 {
|
||||||
|
return fmt.Errorf("pass_table: hash mismatch")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeSHA256(_ HashOpts, pass string) (string, error) {
|
||||||
|
salt := make([]byte, 32)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||||
|
return "", fmt.Errorf("pass_table: failed to generate salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashInput := salt
|
||||||
|
hashInput = append(hashInput, []byte(pass)...)
|
||||||
|
sum := sha256.Sum256(hashInput)
|
||||||
|
return base64.StdEncoding.EncodeToString(salt) + ":" + base64.StdEncoding.EncodeToString(sum[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifySHA256(pass, hashSalt string) error {
|
||||||
|
parts := strings.Split(hashSalt, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Errorf("pass_table: malformed hash string, no salt")
|
||||||
|
}
|
||||||
|
salt, err := base64.StdEncoding.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass_table: malformed hash string, cannot decode pass: %w", err)
|
||||||
|
}
|
||||||
|
hash, err := base64.StdEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass_table: malformed hash string, cannot decode pass: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashInput := salt
|
||||||
|
hashInput = append(hashInput, []byte(pass)...)
|
||||||
|
sum := sha256.Sum256(hashInput)
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(sum[:], hash) != 1 {
|
||||||
|
return fmt.Errorf("pass_table: hash mismatch")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeBcrypt(opts HashOpts, pass string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(pass), opts.BcryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyBcrypt(pass, hashSalt string) error {
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(hashSalt), []byte(pass))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSHA256() {
|
||||||
|
HashCompute[HashSHA256] = computeSHA256
|
||||||
|
HashVerify[HashSHA256] = verifySHA256
|
||||||
|
}
|
72
internal/auth/pass_table/table.go
Normal file
72
internal/auth/pass_table/table.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package pass_table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/foxcpp/maddy/internal/config"
|
||||||
|
modconfig "github.com/foxcpp/maddy/internal/config/module"
|
||||||
|
"github.com/foxcpp/maddy/internal/module"
|
||||||
|
"golang.org/x/text/secure/precis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
modName string
|
||||||
|
instName string
|
||||||
|
inlineArgs []string
|
||||||
|
|
||||||
|
table module.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(modName, instName string, _, inlineArgs []string) (module.Module, error) {
|
||||||
|
if len(inlineArgs) < 1 {
|
||||||
|
return nil, fmt.Errorf("%s: specify the table to use", modName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Auth{
|
||||||
|
modName: modName,
|
||||||
|
instName: instName,
|
||||||
|
inlineArgs: inlineArgs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) Init(cfg *config.Map) error {
|
||||||
|
return modconfig.ModuleFromNode(a.inlineArgs, cfg.Block, cfg.Globals, &a.table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) Name() string {
|
||||||
|
return a.modName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) InstanceName() string {
|
||||||
|
return a.instName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) AuthPlain(username, password string) error {
|
||||||
|
key, err := precis.UsernameCaseMapped.CompareKey(username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, ok, err := a.table.Lookup(key)
|
||||||
|
if !ok {
|
||||||
|
return module.ErrUnknownCredentials
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(hash, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Errorf("%s: no hash tag", a.modName)
|
||||||
|
}
|
||||||
|
hashVerify := HashVerify[parts[0]]
|
||||||
|
if hashVerify == nil {
|
||||||
|
return fmt.Errorf("%s: unknown hash: %s", a.modName, parts[0])
|
||||||
|
}
|
||||||
|
return hashVerify(password, parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
module.Register("pass_table", New)
|
||||||
|
}
|
52
internal/auth/pass_table/table_test.go
Normal file
52
internal/auth/pass_table/table_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package pass_table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/foxcpp/maddy/internal/config"
|
||||||
|
"github.com/foxcpp/maddy/internal/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuth_AuthPlain(t *testing.T) {
|
||||||
|
addSHA256()
|
||||||
|
|
||||||
|
mod, err := New("pass_table", "", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = mod.Init(config.NewMap(nil, &config.Node{
|
||||||
|
Children: []config.Node{
|
||||||
|
{
|
||||||
|
// We replace it later with our mock.
|
||||||
|
Name: "table",
|
||||||
|
Args: []string{"dummy"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
a := mod.(*Auth)
|
||||||
|
a.table = testutils.Table{
|
||||||
|
M: map[string]string{
|
||||||
|
"foxcpp": "sha256:U0FMVA==:8PDRAgaUqaLSk34WpYniXjaBgGM93Lc6iF4pw2slthw=",
|
||||||
|
"not-foxcpp": "bcrypt:$2y$10$4tEJtJ6dApmhETg8tJ4WHOeMtmYXQwmHDKIyfg09Bw1F/smhLjlaa",
|
||||||
|
"not-foxcpp-2": "argon2:1:8:1:U0FBQUFBTFQ=:KHUshl3DcpHR3AoVd28ZeBGmZ1Fj1gwJgNn98Ia8DAvGHqI0BvFOMJPxtaAfO8F+qomm2O3h0P0yV50QGwXI/Q==",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
check := func(user, pass string, ok bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
err := a.AuthPlain(user, pass)
|
||||||
|
if (err == nil) != ok {
|
||||||
|
t.Errorf("ok=%v, err: %v", ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check("foxcpp", "password", true)
|
||||||
|
check("foxcpp", "different-password", false)
|
||||||
|
check("not-foxcpp", "password", true)
|
||||||
|
check("not-foxcpp", "different-password", false)
|
||||||
|
check("not-foxcpp-2", "password", true)
|
||||||
|
}
|
|
@ -19,6 +19,10 @@ func (d *Dummy) AuthPlain(username, _ string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Dummy) Lookup(_ string) (string, bool, error) {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Dummy) Name() string {
|
func (d *Dummy) Name() string {
|
||||||
return "dummy"
|
return "dummy"
|
||||||
}
|
}
|
||||||
|
|
11
internal/testutils/table.go
Normal file
11
internal/testutils/table.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package testutils
|
||||||
|
|
||||||
|
type Table struct {
|
||||||
|
M map[string]string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Table) Lookup(a string) (string, bool, error) {
|
||||||
|
b, ok := m.M[a]
|
||||||
|
return b, ok, m.Err
|
||||||
|
}
|
3
maddy.go
3
maddy.go
|
@ -21,6 +21,8 @@ import (
|
||||||
// Import packages for side-effect of module registration.
|
// Import packages for side-effect of module registration.
|
||||||
_ "github.com/foxcpp/maddy/internal/auth/external"
|
_ "github.com/foxcpp/maddy/internal/auth/external"
|
||||||
_ "github.com/foxcpp/maddy/internal/auth/pam"
|
_ "github.com/foxcpp/maddy/internal/auth/pam"
|
||||||
|
_ "github.com/foxcpp/maddy/internal/auth/pass_table"
|
||||||
|
_ "github.com/foxcpp/maddy/internal/auth/plain_separate"
|
||||||
_ "github.com/foxcpp/maddy/internal/auth/shadow"
|
_ "github.com/foxcpp/maddy/internal/auth/shadow"
|
||||||
_ "github.com/foxcpp/maddy/internal/check/command"
|
_ "github.com/foxcpp/maddy/internal/check/command"
|
||||||
_ "github.com/foxcpp/maddy/internal/check/dkim"
|
_ "github.com/foxcpp/maddy/internal/check/dkim"
|
||||||
|
@ -33,6 +35,7 @@ import (
|
||||||
_ "github.com/foxcpp/maddy/internal/modify"
|
_ "github.com/foxcpp/maddy/internal/modify"
|
||||||
_ "github.com/foxcpp/maddy/internal/modify/dkim"
|
_ "github.com/foxcpp/maddy/internal/modify/dkim"
|
||||||
_ "github.com/foxcpp/maddy/internal/storage/sql"
|
_ "github.com/foxcpp/maddy/internal/storage/sql"
|
||||||
|
_ "github.com/foxcpp/maddy/internal/table"
|
||||||
_ "github.com/foxcpp/maddy/internal/target/queue"
|
_ "github.com/foxcpp/maddy/internal/target/queue"
|
||||||
_ "github.com/foxcpp/maddy/internal/target/remote"
|
_ "github.com/foxcpp/maddy/internal/target/remote"
|
||||||
_ "github.com/foxcpp/maddy/internal/target/smtp_downstream"
|
_ "github.com/foxcpp/maddy/internal/target/smtp_downstream"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue