diff --git a/app/ipasso/main.go b/app/ipasso/main.go index 18668ab..4371a04 100644 --- a/app/ipasso/main.go +++ b/app/ipasso/main.go @@ -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(` +

Request to:

+
{{ .ReqURL }}

Headers

{{- 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 { diff --git a/app/ipasso/userdata.go b/app/ipasso/userdata.go index 394e948..183741a 100644 --- a/app/ipasso/userdata.go +++ b/app/ipasso/userdata.go @@ -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, + }) } diff --git a/sso-proxy/backend/interface.go b/sso-proxy/backend/interface.go index 174630a..16bb2fe 100644 --- a/sso-proxy/backend/interface.go +++ b/sso-proxy/backend/interface.go @@ -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 +}