From fce7b71cff0bf509a33b74633fa30c71dbe0105d Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sat, 13 Jan 2018 13:49:23 +0000
Subject: [PATCH] Propagate backend errors upstream

This allows Dovecot to fail temporarily if the key lookup backend is
unavailable or has issues talking to LDAP.
---
 backend/ldap.go     | 23 ++++++++++-------------
 dovecot/dict.go     | 12 ++++++++++--
 dovecot/keyproxy.go | 34 ++++++++++++++++++++--------------
 server/keystore.go  |  7 +++++--
 4 files changed, 45 insertions(+), 31 deletions(-)

diff --git a/backend/ldap.go b/backend/ldap.go
index f4f5e4d6..a27b3a8a 100644
--- a/backend/ldap.go
+++ b/backend/ldap.go
@@ -3,8 +3,8 @@ package backend
 import (
 	"context"
 	"errors"
+	"fmt"
 	"io/ioutil"
-	"log"
 	"strings"
 
 	ldaputil "git.autistici.org/ai3/go-common/ldap"
@@ -133,11 +133,10 @@ func NewLDAPBackend(config *LDAPConfig) (*ldapBackend, error) {
 	}, nil
 }
 
-func (b *ldapBackend) GetPrivateKeys(ctx context.Context, username string) [][]byte {
+func (b *ldapBackend) GetPrivateKeys(ctx context.Context, username string) ([][]byte, error) {
 	result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username, b.config.Query.PrivateKeyAttr))
 	if err != nil {
-		log.Printf("LDAP error: %v", err)
-		return nil
+		return nil, err
 	}
 
 	var out [][]byte
@@ -146,26 +145,24 @@ func (b *ldapBackend) GetPrivateKeys(ctx context.Context, username string) [][]b
 			out = append(out, []byte(val))
 		}
 	}
-	return out
+	return out, nil
 }
 
-func (b *ldapBackend) GetPublicKey(ctx context.Context, username string) []byte {
+func (b *ldapBackend) GetPublicKey(ctx context.Context, username string) ([]byte, error) {
 	result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username, b.config.Query.PublicKeyAttr))
 	if err != nil {
-		log.Printf("LDAP error: %v", err)
-		return nil
+		return nil, err
 	}
 	if len(result.Entries) == 0 {
-		return nil
+		return nil, nil
 	}
 	if len(result.Entries) > 1 {
-		log.Printf("public key query for %s returned too many results (%d)", username, len(result.Entries))
-		return nil
+		return nil, fmt.Errorf("public key query for %s returned too many results (%d)", username, len(result.Entries))
 	}
 
 	s := result.Entries[0].GetAttributeValue(b.config.Query.PublicKeyAttr)
 	if s == "" {
-		return nil
+		return nil, nil
 	}
-	return []byte(s)
+	return []byte(s), nil
 }
diff --git a/dovecot/dict.go b/dovecot/dict.go
index 8278e3c4..fe605380 100644
--- a/dovecot/dict.go
+++ b/dovecot/dict.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"log"
 
 	"git.autistici.org/ai3/go-common/unix"
 )
@@ -17,7 +18,10 @@ var (
 // DictDatabase is an interface to a key/value store by way of the Lookup
 // method.
 type DictDatabase interface {
-	Lookup(context.Context, string) (interface{}, bool)
+	// Lookup a key. The result boolean is the presence flag, to
+	// avoid having to check the object for nullness. Errors
+	// result in failure being propagated upstream to Dovecot.
+	Lookup(context.Context, string) (interface{}, bool, error)
 }
 
 // DictProxyServer exposes a Database using the Dovecot dict proxy
@@ -56,7 +60,11 @@ func (p *DictProxyServer) handleHello(ctx context.Context, lw unix.LineResponseW
 }
 
 func (p *DictProxyServer) handleLookup(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error {
-	obj, ok := p.db.Lookup(ctx, string(arg))
+	obj, ok, err := p.db.Lookup(ctx, string(arg))
+	if err != nil {
+		log.Printf("error: %v", err)
+		return lw.WriteLine(failResponse)
+	}
 	if !ok {
 		return lw.WriteLine(noMatchResponse)
 	}
diff --git a/dovecot/keyproxy.go b/dovecot/keyproxy.go
index 628cad9c..f1248dcc 100644
--- a/dovecot/keyproxy.go
+++ b/dovecot/keyproxy.go
@@ -24,8 +24,8 @@ type Config struct {
 // Database represents the interface to the underlying backend for
 // encrypted user keys.
 type Database interface {
-	GetPublicKey(context.Context, string) []byte
-	GetPrivateKeys(context.Context, string) [][]byte
+	GetPublicKey(context.Context, string) ([]byte, error)
+	GetPrivateKeys(context.Context, string) ([][]byte, error)
 }
 
 func (c *Config) check() error {
@@ -82,7 +82,7 @@ func NewKeyLookupProxy(config *Config) (*KeyLookupProxy, error) {
 //
 // We can be sent a userdb lookup, or a passdb lookup, and we can tell
 // them apart with the key prefix (passdb/ or userdb/).
-func (s *KeyLookupProxy) Lookup(ctx context.Context, key string) (interface{}, bool) {
+func (s *KeyLookupProxy) Lookup(ctx context.Context, key string) (interface{}, bool, error) {
 	switch {
 	case strings.HasPrefix(key, "passdb/"):
 		kparts := strings.SplitN(key[7:], passwordSep, 2)
@@ -95,39 +95,45 @@ func (s *KeyLookupProxy) Lookup(ctx context.Context, key string) (interface{}, b
 	}
 }
 
-func (s *KeyLookupProxy) lookupUserdb(ctx context.Context, username string) (interface{}, bool) {
-	pub := s.db.GetPublicKey(ctx, username)
+func (s *KeyLookupProxy) lookupUserdb(ctx context.Context, username string) (interface{}, bool, error) {
+	pub, err := s.db.GetPublicKey(ctx, username)
+	if err != nil {
+		return nil, false, err
+	}
 	if pub == nil {
-		log.Printf("failed userdb lookup for %s", username)
-		return nil, false
+		log.Printf("userdb lookup for %s (no keys)", username)
+		return nil, false, nil
 	}
 	log.Printf("userdb lookup for %s", username)
-	return &userdbResponse{PublicKey: b64encode(pub)}, true
+	return &userdbResponse{PublicKey: b64encode(pub)}, true, nil
 }
 
-func (s *KeyLookupProxy) lookupPassdb(ctx context.Context, username, password string) (interface{}, bool) {
+func (s *KeyLookupProxy) lookupPassdb(ctx context.Context, username, password string) (interface{}, bool, error) {
 	// If the password is a SSO token, try to fetch the
 	// unencrypted key from the keystore daemon.
 	priv, err := s.keystore.Get(ctx, s.config.Shard, username, password)
 	if err == nil {
 		log.Printf("passdb lookup for %s (from keystore)", username)
-		return &passdbResponse{PrivateKey: b64encode(priv)}, true
+		return &passdbResponse{PrivateKey: b64encode(priv)}, true, nil
 	}
 
 	// Otherwise, fetch encrypted keys from the db and attempt to
 	// decrypt them.
-	encKeys := s.db.GetPrivateKeys(ctx, username)
+	encKeys, err := s.db.GetPrivateKeys(ctx, username)
+	if err != nil {
+		return nil, false, err
+	}
 	if len(encKeys) == 0 {
 		log.Printf("failed passdb lookup for %s (no keys)", username)
-		return nil, false
+		return nil, false, nil
 	}
 	priv, err = userenckey.Decrypt(encKeys, []byte(password))
 	if err != nil {
 		log.Printf("failed passdb lookup for %s (could not decrypt key)", username)
-		return nil, false
+		return nil, false, err
 	}
 	log.Printf("passdb lookup for %s (decrypted)", username)
-	return &passdbResponse{PrivateKey: b64encode(priv)}, true
+	return &passdbResponse{PrivateKey: b64encode(priv)}, true, nil
 }
 
 func b64encode(b []byte) string {
diff --git a/server/keystore.go b/server/keystore.go
index 9cc820b3..af4f9fe7 100644
--- a/server/keystore.go
+++ b/server/keystore.go
@@ -23,7 +23,7 @@ var (
 // Database represents the interface to the underlying backend for
 // encrypted user keys.
 type Database interface {
-	GetPrivateKeys(context.Context, string) [][]byte
+	GetPrivateKeys(context.Context, string) ([][]byte, error)
 }
 
 type userKey struct {
@@ -133,7 +133,10 @@ func (s *KeyStore) Open(ctx context.Context, username, password string, ttlSecon
 		return ErrInvalidTTL
 	}
 
-	encKeys := s.db.GetPrivateKeys(ctx, username)
+	encKeys, err := s.db.GetPrivateKeys(ctx, username)
+	if err != nil {
+		return err
+	}
 	if len(encKeys) == 0 {
 		return ErrNoKeys
 	}
-- 
GitLab