Basic LDAP search works

This commit is contained in:
2023-10-25 00:50:24 +02:00
parent 7bcd00c97b
commit b934270d14
3 changed files with 175 additions and 27 deletions

View File

@@ -23,11 +23,12 @@ 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
datastore backend.Backend = &backend.NullBackend{}
)
var (
ErrNoExpiration = errors.New("Request missing expiration date; misconfigured reverse proxy?")
ErrNoExpiration = errors.New("Request missing expiration date; misconfigured reverse proxy?")
ErrInvalidResult = errors.New("invalid result from LDAP search")
)
func main() {
@@ -43,10 +44,9 @@ func main() {
startup.PostFlags.Run()
initUserdata()
mux := http.NewServeMux()
mux.Handle("/env", http.HandlerFunc(dumpEnv))
mux.Handle("/env/", http.HandlerFunc(dumpEnv))
mux.Handle("/login/krb5", http.HandlerFunc(loginKrb))
l.Info("Starting")
listener, err := net.Listen("tcp", *listen)
if err != nil {
@@ -65,6 +65,8 @@ var envTemplate = htemplate.Must(htemplate.New("envdoc").Parse(`
</style>
</head>
<body>
<h1>Request to:</h1>
<pre>{{ .ReqURL }}</pre>
<h1>Headers</h1>
<table>
{{- range $k,$v := .Headers }}
@@ -90,16 +92,36 @@ var envTemplate = htemplate.Must(htemplate.New("envdoc").Parse(`
type envdocArgs struct {
Headers map[string]string
ProcessEnv map[string]string
ReqURL string
}
// Try to continue negotiation via a www-authenticate header, if this is an error page.
func continueNegotate(w http.ResponseWriter, r *http.Request) bool {
procEnv := fcgi.ProcessEnv(r)
l := zap.L().Named("req")
l.Debug("Received request", zap.Any("env", procEnv))
rStatus, ok := procEnv["REDIRECT_STATUS"]
if ok && rStatus == "401" {
w.Header().Set("WWW-Authenticate", "Negotiate")
w.WriteHeader(401)
return true
}
return false
}
func dumpEnv(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
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)
var args = envdocArgs{
ProcessEnv: fcgi.ProcessEnv(r),
Headers: headers,
ReqURL: r.URL.String(),
}
if err := envTemplate.Execute(w, args); err != nil {
panic(err)
}
@@ -109,7 +131,7 @@ 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"]
user := env["GSS_NAME"]
uid := strings.Split(user, "@")[0]
expirationTxt, hasExpiration := env["GSS_SESSION_EXPIRATION"]
var expiration time.Time
@@ -133,19 +155,25 @@ func loginKrb(w http.ResponseWriter, r *http.Request) {
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)
entry, err := getUserByPrincipal(user)
if err != nil {
ReportError(w, err)
return
}
datastore.PutSession(session, sessionCache)
session := backend.Session{
SessionID: SessionID,
Expiration: expiration,
UserID: uid,
LdapDN: entry.DN,
}
sessionCache, err := buildSessionCache(&session, entry)
if err != nil {
ReportError(w, err)
return
}
_ = datastore.PutSession(session, sessionCache)
}
type RespErr struct {

View File

@@ -28,6 +28,10 @@ var (
ldapServerPool []ldapServerHost
ldapPool genpool.Pool[ldap.Conn]
ldapUserBase string
ldapServiceDn string
ldapHostDn string
)
var (
@@ -174,18 +178,88 @@ func (l *ldapPoolManager) Create() (*ldap.Conn, error) {
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, entry *ldap.Entry) (cache backend.SessionCache, err error) {
if entry == nil {
entry, err = getUserByDn(b.LdapDN)
if err != nil {
return
}
}
//ldapRootLogger.Info("Building session cache", zap.Any("entry", entry))
cache.Valid = true
for _, attr := range entry.Attributes {
if attr.Name == "displayName" && len(attr.Values) > 0 {
cache.DisplayName = attr.Values[0]
} else if attr.Name == "givenName" && len(attr.Values) > 0 {
cache.GivenName = strings.Join(attr.Values, " ")
} else if attr.Name == "sn" {
cache.SurName = strings.Join(attr.Values, " ")
} else if attr.Name == "mail" && len(attr.Values) > 0 {
cache.Email = attr.Values[0]
} else if attr.Name == "memberOf" {
grpSuffix := "cn=groups,cn=accounts," + *ldapRootDN
for _, grp := range attr.Values {
gnames := strings.SplitN(grp, ",", 2)
if gnames[1] != grpSuffix {
continue
}
cn, found := strings.CutPrefix(gnames[0], "cn=")
if found {
cache.Groups = append(cache.Groups, cn)
} else {
ldapRootLogger.Warn("Unexpected group name", zap.String("gdn", grp), zap.String("dn", entry.DN))
}
}
}
}
return
}
func buildSessionCache(b *backend.Session) (backend.SessionCache, error) {
return backend.SessionCache{}, nil
var interestingUserAttr = []string{"displayName", "givenName", "sn", "uid", "mail", "memberOf", "serverHostName"}
func ldapSearchSingle(req *ldap.SearchRequest) (*ldap.Entry, error) {
server, err := ldapPool.Get()
if err != nil {
return nil, err
}
defer ldapPool.Put(server)
searchRes, err := server.Search(req)
if err != nil {
ldapRootLogger.Warn("Failed LDAP search", zap.Any("req", req), zap.Error(err))
return nil, err
}
nEntries := len(searchRes.Entries)
if nEntries == 0 {
ldapRootLogger.Info("No entries found for search", zap.String("filter", req.Filter))
return nil, ErrInvalidResult
} else if nEntries > 1 {
ldapRootLogger.Info("Multiple entries found for search", zap.String("filter", req.Filter))
return nil, ErrInvalidResult
}
return searchRes.Entries[0], nil
}
func getUserByPrincipal(principal string) (*ldap.Entry, error) {
return ldapSearchSingle(&ldap.SearchRequest{
Filter: "(&(krbprincipalname=" + principal + ")(objectClass=inetorgperson))",
BaseDN: *ldapRootDN,
Scope: ldap.ScopeWholeSubtree,
Attributes: interestingUserAttr,
})
}
func getUserByDn(dn string) (*ldap.Entry, error) {
return ldapSearchSingle(&ldap.SearchRequest{
BaseDN: dn,
Scope: ldap.ScopeBaseObject,
Attributes: interestingUserAttr,
})
}

View File

@@ -1,12 +1,25 @@
package backend
import "time"
import (
"errors"
"go.uber.org/zap"
"time"
)
type UserType string
var (
UtPerson UserType = "person"
UtHost UserType = "host"
UtService UserType = "service"
)
// Session holds info about the logged-in user that won't change
type Session struct {
SessionID string
Expiration time.Time
UserID string
UserType UserType
LdapDN string
}
@@ -15,6 +28,8 @@ type SessionCache struct {
Valid bool
Groups []string
DisplayName string
GivenName string
SurName string
Email string
}
@@ -26,3 +41,34 @@ type Backend interface {
DoMaintenance()
Ping() error
}
type NullBackend struct{}
func (n *NullBackend) PutSession(session Session, cache SessionCache) error {
//TODO implement me
zap.L().Info("Put session", zap.Any("session", session), zap.Any("cache", cache))
return nil
}
func (n *NullBackend) GetSession(id string) (Session, *SessionCache, error) {
//TODO implement me
return Session{}, nil, errors.New("no such session")
}
func (n *NullBackend) EndSession(id string) {
//TODO implement me
}
func (n *NullBackend) NewSessionID() (string, error) {
//TODO implement me
return "foo", nil
}
func (n *NullBackend) DoMaintenance() {
//TODO implement me
}
func (n *NullBackend) Ping() error {
//TODO implement me
return nil
}