Basic LDAP search works
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user