Started LDAP support

This commit is contained in:
2023-10-24 22:15:58 +02:00
parent 4a9a913d66
commit 7bcd00c97b
11 changed files with 669 additions and 82 deletions

164
app/ipasso/main.go Normal file
View File

@@ -0,0 +1,164 @@
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"git.thequux.com/thequux/ipasso/sso-proxy/backend"
"git.thequux.com/thequux/ipasso/util/startup"
"go.uber.org/zap"
htemplate "html/template"
"log"
"net"
"net/http"
"net/http/fcgi"
"os"
"strconv"
"strings"
"time"
)
var (
domain = flag.String("domain", "thequux.com", "The base domain to enable SSO for")
listen = flag.String("listen", "0.0.0.0:80", "The address to listen on")
datastore backend.Backend
)
var (
ErrNoExpiration = errors.New("Request missing expiration date; misconfigured reverse proxy?")
)
func main() {
flag.Parse()
development, err := zap.NewDevelopment()
if err != nil {
_, _ = fmt.Fprint(os.Stderr, "Failed to create logger", err)
os.Exit(1)
}
zap.ReplaceGlobals(development)
l := zap.L().Named("root")
startup.Logger.Run()
startup.PostFlags.Run()
initUserdata()
mux := http.NewServeMux()
mux.Handle("/env", http.HandlerFunc(dumpEnv))
l.Info("Starting")
listener, err := net.Listen("tcp", *listen)
if err != nil {
log.Fatalln("Failed to listen: ", err)
}
l.Info("Listening", zap.Stringer("addr", listener.Addr()))
log.Fatal(fcgi.Serve(listener, mux))
}
var envTemplate = htemplate.Must(htemplate.New("envdoc").Parse(`
<!DOCTYPE html>
<html>
<head>
<style>
pre { margin: 0 1ex; }
</style>
</head>
<body>
<h1>Headers</h1>
<table>
{{- range $k,$v := .Headers }}
<tr>
<td><pre>[{{ $k }}]</pre></td>
<td><pre>{{ $v }}</pre></td>
</tr>
{{- end }}
</table>
<h1>Process Environment</h1>
<table>
{{- range $k,$v := .ProcessEnv }}
<tr>
<td><pre>{{ $k }}</pre></td>
<td><pre>{{ $v }}</pre></td>
</tr>
{{- end }}
</table>
</body>
</html>
`))
type envdocArgs struct {
Headers map[string]string
ProcessEnv map[string]string
}
func dumpEnv(w http.ResponseWriter, r *http.Request) {
var headers = map[string]string{}
for k, v := range r.Header {
headers[k] = strings.Join(v, "\n")
}
var args = envdocArgs{ProcessEnv: fcgi.ProcessEnv(r), Headers: headers}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
if err := envTemplate.Execute(w, args); err != nil {
panic(err)
}
}
func loginKrb(w http.ResponseWriter, r *http.Request) {
// Uses information from the process environment; assumes that Apache has done a krb5 login
// The fact that we got here implies that the login succeeded
env := fcgi.ProcessEnv(r)
user := env["REMOTE_USER"]
uid := strings.Split(user, "@")[0]
expirationTxt, hasExpiration := env["GSS_SESSION_EXPIRATION"]
var expiration time.Time
if hasExpiration {
expInt, err := strconv.ParseInt(expirationTxt, 10, 64)
if err != nil {
// Invalid expiration date; bail
log.Printf("Invalid expiration date: %#v", expirationTxt)
w.WriteHeader(http.StatusInternalServerError)
} else {
expiration = time.Unix(expInt, 0)
}
} else {
log.Print(ErrNoExpiration)
ReportError(w, ErrNoExpiration)
return
}
SessionID, err := datastore.NewSessionID()
if err != nil {
ReportError(w, err)
return
}
ldapDN := fmt.Sprintf("uid=%s,cn=users,cn=accounts,%s", uid, *ldapRootDN)
session := backend.Session{
SessionID: SessionID,
Expiration: expiration,
UserID: uid,
LdapDN: ldapDN,
}
sessionCache, err := buildSessionCache(&session)
if err != nil {
ReportError(w, err)
return
}
datastore.PutSession(session, sessionCache)
}
type RespErr struct {
Err string
Status string
}
func ReportError(w http.ResponseWriter, err error) {
w.Header().Set("Content-Type", "text/json")
w.WriteHeader(http.StatusInternalServerError)
js, err0 := json.Marshal(RespErr{Err: err.Error(), Status: "ERROR"})
if err0 != nil {
panic(err0)
}
_, _ = w.Write(js)
}

191
app/ipasso/userdata.go Normal file
View File

@@ -0,0 +1,191 @@
package main
import (
"errors"
"flag"
"git.thequux.com/thequux/ipasso/sso-proxy/backend"
"git.thequux.com/thequux/ipasso/util"
"git.thequux.com/thequux/ipasso/util/genpool"
"git.thequux.com/thequux/ipasso/util/startup"
"github.com/go-ldap/ldap/gssapi"
"github.com/go-ldap/ldap/v3"
"github.com/jcmturner/gokrb5/v8/config"
"go.uber.org/zap"
"math/rand"
"net/url"
"strings"
)
var (
ldapServerUrl = flag.String("ldap-url", "", "URL at which LDAP server can be reached")
ldapRootDN = flag.String("rootDN", "", "LDAP Root DN. Defaults to dc=host,dc=tld based on -domain")
keytab = flag.String("keytab", "ipasso.keytab", "Keytab file used to authenticate server")
krb5Principal = flag.String("krb5-principal", "", "Default kerberos principal; default HTTP/sso.<domain>")
krb5realm = flag.String("krb5-realm", "", "Kerberos realm. Default based on krb5 config")
krb5conf = flag.String("krb5-conf", util.GetEnvDefault("KRB5_CONFIG", "/etc/krb5.conf"), "Config file for kerberos")
gssapiClient *gssapi.Client
ldapServerPool []ldapServerHost
ldapPool genpool.Pool[ldap.Conn]
)
var (
ErrNoValidServer = errors.New("no valid server")
ldapRootLogger, ldapPoolLogger *zap.Logger
)
func init() {
startup.Logger.Add(func() {
ldapRootLogger = zap.L().Named("ldap")
ldapPoolLogger = ldapRootLogger.Named("pool")
})
startup.PostFlags.Add(func() {
serverUrl, err := url.Parse(*ldapServerUrl)
if err != nil {
ldapRootLogger.Fatal("Invalid LDAP server url", zap.String("url", *ldapServerUrl), zap.Error(err))
}
ldapServerPool = []ldapServerHost{
{
SPN: "ldap/" + serverUrl.Hostname(),
Url: *ldapServerUrl,
Weight: 1,
},
}
if *ldapRootDN == "" {
rootDnElements := make([]string, 0, 4)
for _, v := range strings.Split(*domain, ".") {
rootDnElements = append(rootDnElements, "dc="+v)
}
*ldapRootDN = strings.Join(rootDnElements, ",")
ldapRootLogger.Debug("Configured LDAP rootDN", zap.String("rootDN", *ldapRootDN))
}
krb5Config, err := config.Load(*krb5conf)
var realmSource string = ""
if err != nil {
ldapRootLogger.Warn("Failed to load config", zap.Error(err))
} else {
if *krb5realm == "" {
*krb5realm = krb5Config.LibDefaults.DefaultRealm
realmSource = *krb5conf
}
}
if *krb5realm == "" {
// default from domain
*krb5realm = strings.ToUpper(*domain)
realmSource = "domain"
}
if realmSource != "" {
ldapRootLogger.Debug("Configured KRB5 realm", zap.String("source", realmSource), zap.String("realm", *krb5realm))
}
if *krb5Principal == "" {
*krb5Principal = "HTTP/sso." + *domain
ldapRootLogger.Debug("Configured local kerberos principal", zap.String("principal", *krb5Principal))
}
gssapiClient, err = gssapi.NewClientWithKeytab(*krb5Principal, *krb5realm, *keytab, *krb5conf)
if err != nil {
ldapRootLogger.Fatal("Failed to initialize kerberos", zap.Error(err))
}
// Create the LDAP pool
ldapPool = genpool.NewPool[ldap.Conn](&ldapPoolManager{gssapiClient: gssapiClient}, 5)
// Test the pool...
conn, err := ldapPool.Get()
if err != nil {
ldapPoolLogger.Warn("Failed to connect to LDAP server at startup", zap.Error(err))
} else {
defer ldapPool.Put(conn)
whoami, err := conn.WhoAmI(nil)
if err != nil {
ldapPoolLogger.Warn("Failed to call whoami at startup", zap.Error(err))
}
ldapPoolLogger.Info("Successfully connected to LDAP", zap.String("authzId", whoami.AuthzID))
}
})
}
type (
ldapServerHost struct {
SPN string
Url string
Weight int
}
ldapPoolManager struct {
// TODO: Fill this with results from a SRV request...
gssapiClient *gssapi.Client
}
)
func selectServer() *ldapServerHost {
serverSet := ldapServerPool
var selectedServer *ldapServerHost
var weight = 0
for _, server := range serverSet {
if server.Weight <= 0 {
continue
}
weight += server.Weight
if rand.Intn(weight) < server.Weight {
server := server // copy the server object
selectedServer = &server
}
}
if weight > 0 && selectedServer == nil {
ldapPoolLogger.DPanic("Failed to select a server when one was on offer")
}
return selectedServer
}
func (l *ldapPoolManager) Destroy(conn *ldap.Conn) {
err := conn.Close()
if err != nil {
ldapPoolLogger.Warn("Failed to close LDAP connection",
zap.Error(err),
)
} else {
ldapPoolLogger.Debug("Closed ldap connection")
}
}
func (l *ldapPoolManager) Create() (*ldap.Conn, error) {
server := selectServer()
if server == nil {
return nil, ErrNoValidServer
}
conn, err := ldap.DialURL(server.Url)
if err != nil {
return nil, err
}
if err := conn.GSSAPIBind(l.gssapiClient, server.SPN, ""); err != nil {
return nil, err
}
return conn, nil
}
func (l *ldapPoolManager) Validate(conn *ldap.Conn) bool {
_, err := conn.WhoAmI([]ldap.Control{})
conn.Unbind()
return err == nil
}
func initUserdata() {
var err error
if err != nil {
panic(err)
}
}
func buildSessionCache(b *backend.Session) (backend.SessionCache, error) {
return backend.SessionCache{}, nil
}

View File

@@ -1,82 +0,0 @@
package main
import (
"flag"
htemplate "html/template"
"log"
"net"
"net/http"
"net/http/fcgi"
"os"
"strings"
)
var (
domain = flag.String("domain", "thequux.com", "The base domain to enable SSO for")
listen = flag.String("listen", "0.0.0.0:80", "The address to listen on")
)
func main() {
flag.Parse()
l := log.New(os.Stderr, "ipaSSO: ", log.Ldate|log.Ltime|log.Lshortfile)
mux := http.NewServeMux()
mux.Handle("/env", http.HandlerFunc(dumpEnv))
l.Printf("Starting")
listener, err := net.Listen("tcp", *listen)
if err != nil {
log.Fatalln("Failed to listen: ", err)
}
l.Println("Listening on", listener.Addr())
log.Fatal(fcgi.Serve(listener, mux))
}
var envTemplate = htemplate.Must(htemplate.New("envdoc").Parse(`
<!DOCTYPE html>
<html>
<head>
<style>
pre { margin: 0 1ex; }
</style>
</head>
<body>
<h1>Headers</h1>
<table>
{{- range $k,$v := .Headers }}
<tr>
<td><pre>[{{ $k }}]</pre></td>
<td><pre>{{ $v }}</pre></td>
</tr>
{{- end }}
</table>
<h1>Process Environment</h1>
<table>
{{- range $k,$v := .ProcessEnv }}
<tr>
<td><pre>{{ $k }}</pre></td>
<td><pre>{{ $v }}</pre></td>
</tr>
{{- end }}
</table>
</body>
</html>
`))
type envdocArgs struct {
Headers map[string]string
ProcessEnv map[string]string
}
func dumpEnv(w http.ResponseWriter, r *http.Request) {
var headers = map[string]string{}
for k, v := range r.Header {
headers[k] = strings.Join(v, "\n")
}
var args = envdocArgs{ProcessEnv: fcgi.ProcessEnv(r), Headers: headers}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
if err := envTemplate.Execute(w, args); err != nil {
panic(err)
}
}