Started LDAP support
This commit is contained in:
164
app/ipasso/main.go
Normal file
164
app/ipasso/main.go
Normal 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
191
app/ipasso/userdata.go
Normal 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
|
||||
}
|
||||
82
app/main.go
82
app/main.go
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user