Flesh out examples/ldapauth, specifically:

Support 'virtual' users who have no homeDirectory, uidNumber or gidNumber.
Permit read-only access by a user named "anonymous", with any password.
Assume a conventional DIT with users under ou=people,dc=example,dc=com.
Read the LDAP bindPassword from a file (not baked into the code).
Log progress and problems to syslog.
This commit is contained in:
root 2021-09-30 17:13:41 -07:00 committed by Nicola Murino
parent e6f969cb04
commit 87751e562e

View file

@ -4,17 +4,22 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"log/syslog"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
const ( const (
bindUsername = "cn=Directory Manager" rootDN = "dc=example,dc=com"
bindPassword = "YOUR_ADMIN_PASSWORD_HERE" bindUsername = "cn=sftpgo," + rootDN
bindURL = "ldap://192.168.1.103:389" bindURL = "ldap:///" // That is, the server on the default port of localhost.
passwordFile = "/etc/sftpgo/admin-password.txt" // make this file readable only by the server
publicDir = "/var/www/webdav/public"
) )
type userFilters struct { type userFilters struct {
@ -32,6 +37,7 @@ type minimalSFTPGoUser struct {
} }
func exitError() { func exitError() {
log.Printf("exitError\n")
u := minimalSFTPGoUser{ u := minimalSFTPGoUser{
Username: "", Username: "",
} }
@ -40,7 +46,7 @@ func exitError() {
os.Exit(1) os.Exit(1)
} }
func printSuccessResponse(username, homeDir string, uid, gid int) { func printSuccessResponse(username, homeDir string, uid, gid int, permissions []string) {
u := minimalSFTPGoUser{ u := minimalSFTPGoUser{
Username: username, Username: username,
HomeDir: homeDir, HomeDir: homeDir,
@ -49,46 +55,66 @@ func printSuccessResponse(username, homeDir string, uid, gid int) {
Status: 1, Status: 1,
} }
u.Permissions = make(map[string][]string) u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{"*"} u.Permissions["/"] = permissions
// uncomment the next line to require publickey+password authentication // uncomment the next line to require publickey+password authentication
//u.Filters.DeniedLoginMethods = []string{"publickey", "password", "keyboard-interactive", "publickey+keyboard-interactive"} //u.Filters.DeniedLoginMethods = []string{"publickey", "password", "keyboard-interactive", "publickey+keyboard-interactive"}
resp, _ := json.Marshal(u) resp, _ := json.Marshal(u)
log.Printf("%v\n", string(resp))
fmt.Printf("%v\n", string(resp)) fmt.Printf("%v\n", string(resp))
os.Exit(0) os.Exit(0)
} }
func main() { func main() {
logWriter, err := syslog.New(syslog.LOG_NOTICE, "sftpgo")
if err == nil {
log.SetOutput(logWriter)
}
// get credentials from env vars // get credentials from env vars
username := os.Getenv("SFTPGO_AUTHD_USERNAME") username := os.Getenv("SFTPGO_AUTHD_USERNAME")
password := os.Getenv("SFTPGO_AUTHD_PASSWORD") password := os.Getenv("SFTPGO_AUTHD_PASSWORD")
publickey := os.Getenv("SFTPGO_AUTHD_PUBLIC_KEY") publickey := os.Getenv("SFTPGO_AUTHD_PUBLIC_KEY")
if strings.ToLower(username) == "anonymous" {
printSuccessResponse("anonymous", publicDir, 0, 0, []string{"list", "download"})
return
}
l, err := ldap.DialURL(bindURL) l, err := ldap.DialURL(bindURL)
if err != nil { if err != nil {
log.Printf("DialURL: %s\n", err.Error())
exitError() exitError()
} }
defer l.Close() defer l.Close()
// bind to the ldap server with an account that can read users // bind to the ldap server with an account that can read users
err = l.Bind(bindUsername, bindPassword) bindPassword, err := os.ReadFile(passwordFile)
if err != nil { if err != nil {
log.Printf("ReadFile(%s): %s\n", passwordFile, err.Error())
exitError()
}
err = l.Bind(bindUsername, string(bindPassword))
if err != nil {
log.Printf("Bind(%s): %s\n", bindUsername, err.Error())
exitError() exitError()
} }
// search the user trying to login and fetch some attributes, this search string is tested against 389ds using the default configuration // search the user trying to login and fetch some attributes, this search string is tested against 389ds using the default configuration
log.Printf("username=%s\n", username)
searchFilter := fmt.Sprintf("(uid=%s)", username)
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com", "ou=people," + rootDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=nsPerson)(uid=%s))", username), searchFilter,
[]string{"dn", "homeDirectory", "uidNumber", "gidNumber", "nsSshPublicKey"}, []string{"dn", "uid", "homeDirectory", "uidNumber", "gidNumber", "nsSshPublicKey"},
nil, nil,
) )
sr, err := l.Search(searchRequest) sr, err := l.Search(searchRequest)
if err != nil { if err != nil {
log.Printf("Search(%s): %s\n", searchFilter, err.Error())
exitError() exitError()
} }
// we expect exactly one user // we expect exactly one user
if len(sr.Entries) != 1 { if len(sr.Entries) != 1 {
log.Printf("Search(%s): %d entries\n", searchFilter, len(sr.Entries))
exitError() exitError()
} }
@ -96,6 +122,7 @@ func main() {
// check public key // check public key
userKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publickey)) userKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publickey))
if err != nil { if err != nil {
log.Printf("ParseAuthorizedKey(%s): %s\n", publickey, err.Error())
exitError() exitError()
} }
authOk := false authOk := false
@ -111,25 +138,38 @@ func main() {
} }
} }
if !authOk { if !authOk {
log.Printf("publickey %s !authOk\n", publickey)
exitError() exitError()
} }
} else { } else {
// bind to the LDAP server with the user dn and the given password to check the password // bind to the LDAP server with the user dn and the given password to check the password
userdn := sr.Entries[0].DN userdn := sr.Entries[0].DN
// log.Printf("password=%s\n", password)
err = l.Bind(userdn, password) err = l.Bind(userdn, password)
if err != nil { if err != nil {
log.Printf("Bind(%s): %s\n", userdn, err.Error())
exitError() exitError()
} }
} }
uid, err := strconv.Atoi(sr.Entries[0].GetAttributeValue("uidNumber")) // People in the LDAP directory aren't necessarily Linux users;
// so they might not have a uidNumber or gidNumber.
uidNumber := sr.Entries[0].GetAttributeValue("uidNumber")
uid, err := strconv.Atoi(uidNumber)
if err != nil { if err != nil {
exitError() //log.Printf("uid Atoi(%s) = %s\n", uidNumber, err.Error())
uid = 0
} }
gid, err := strconv.Atoi(sr.Entries[0].GetAttributeValue("gidNumber")) gidNumber := sr.Entries[0].GetAttributeValue("gidNumber")
gid, err := strconv.Atoi(gidNumber)
if err != nil { if err != nil {
exitError() //log.Printf("gid Atoi(%s) = %s\n", gidNumber, err.Error())
gid = 0
}
homeDir := sr.Entries[0].GetAttributeValue("homeDirectory")
if (len(homeDir) <= 0) {
homeDir = publicDir // homeDir is a required attribute.
} }
// return the authenticated user // return the authenticated user
printSuccessResponse(username, sr.Entries[0].GetAttributeValue("homeDirectory"), uid, gid) printSuccessResponse(sr.Entries[0].GetAttributeValue("uid"), homeDir, uid, gid, []string{"*"})
} }