package handler import ( "context" "crypto/tls" "encoding/json" "errors" "fmt" "io" "net" "net/http" "strings" "sync" "time" "github.com/rs/zerolog" "go.opentelemetry.io/otel/trace" "github.com/glauth/glauth/v2/internal/monitoring" "github.com/glauth/glauth/v2/pkg/config" "github.com/glauth/glauth/v2/pkg/stats" "github.com/glauth/ldap" msgraph "github.com/yaegashi/msgraph.go/v1.0" ) type ownCloudSession struct { log *zerolog.Logger client *http.Client user string password string endpoint string useGraphAPI bool } type ownCloudHandler struct { backend config.Backend log *zerolog.Logger client *http.Client sessions map[string]ownCloudSession lock *sync.Mutex monitor monitoring.MonitorInterface tracer trace.Tracer } // global lock for ownCloudHandler sessions & servers manipulation var ownCloudLock sync.Mutex func NewOwnCloudHandler(opts ...Option) Handler { options := newOptions(opts...) return ownCloudHandler{ backend: options.Backend, log: options.Logger, sessions: make(map[string]ownCloudSession), lock: &ownCloudLock, client: &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: options.Backend.Insecure, }, }, }, monitor: options.Monitor, tracer: options.Tracer, } } func (h ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (result ldap.LDAPResultCode, err error) { start := time.Now() defer func() { h.monitor.SetResponseTimeMetric( map[string]string{"operation": "bind", "status": fmt.Sprintf("%v", result)}, time.Since(start).Seconds(), ) }() bindDN = strings.ToLower(bindDN) baseDN := strings.ToLower("," + h.backend.BaseDN) h.log.Debug().Str("binddn", bindDN).Str("basedn", h.backend.BaseDN).Str("src", conn.RemoteAddr().String()).Msg("Bind request") stats.Frontend.Add("bind_reqs", 1) // parse the bindDN - ensure that the bindDN ends with the BaseDN if !strings.HasSuffix(bindDN, baseDN) { h.log.Warn().Str("binddn", bindDN).Str("basedn", h.backend.BaseDN).Msg("BindDN not part of our BaseDN") return ldap.LDAPResultInvalidCredentials, nil } parts := strings.Split(strings.TrimSuffix(bindDN, baseDN), ",") if len(parts) > 2 { h.log.Warn().Str("binddn", bindDN).Int("numparts", len(parts)).Msg("BindDN should have only one or two parts") return ldap.LDAPResultInvalidCredentials, nil } userName := strings.TrimPrefix(parts[0], "cn=") // try to login if !h.login(userName, bindSimplePw) { h.log.Warn().Str("username", userName).Str("basedn", h.backend.BaseDN).Msg("Login failed") return ldap.LDAPResultInvalidCredentials, nil } // TODO reuse HTTP connection id := connID(conn) s := ownCloudSession{ log: h.log, user: userName, password: bindSimplePw, endpoint: h.backend.Servers[0], useGraphAPI: h.backend.UseGraphAPI, client: h.client, } h.lock.Lock() h.sessions[id] = s h.lock.Unlock() stats.Frontend.Add("bind_successes", 1) h.log.Debug().Str("binddn", bindDN).Str("basedn", h.backend.BaseDN).Str("src", conn.RemoteAddr().String()).Msg("Bind success") return ldap.LDAPResultSuccess, nil } func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) { start := time.Now() defer func() { h.monitor.SetResponseTimeMetric( map[string]string{"operation": "search", "status": fmt.Sprintf("%v", result.ResultCode)}, time.Since(start).Seconds(), ) }() bindDN = strings.ToLower(bindDN) baseDN := strings.ToLower("," + h.backend.BaseDN) searchBaseDN := strings.ToLower(searchReq.BaseDN) h.log.Debug().Str("binddn", bindDN).Str("basedn", baseDN).Str("src", conn.RemoteAddr().String()).Str("filter", searchReq.Filter).Msg("Search request") stats.Frontend.Add("search_reqs", 1) // validate the user is authenticated and has appropriate access if len(bindDN) < 1 { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: Anonymous BindDN not allowed %s", bindDN) } if !strings.HasSuffix(bindDN, baseDN) { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: BindDN %s not in our BaseDN %s", bindDN, h.backend.BaseDN) } if !strings.HasSuffix(searchBaseDN, h.backend.BaseDN) { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.backend.BaseDN) } // return all users in the config file - the LDAP library will filter results for us entries := []*ldap.Entry{} filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter) if err != nil { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("search error: error parsing filter: %s", searchReq.Filter) } h.lock.Lock() id := connID(conn) session := h.sessions[id] h.lock.Unlock() switch filterEntity { default: return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("search error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter) case "posixgroup": groups, err := session.getGroups() if err != nil { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("search error: error getting groups") } for _, g := range groups { attrs := []*ldap.EntryAttribute{} for _, groupAttr := range h.backend.GroupFormatAsArray { attrs = append(attrs, &ldap.EntryAttribute{Name: groupAttr, Values: []string{*g.ID}}) } attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s from ownCloud", *g.ID)}}) // attrs = append(attrs, &ldap.EntryAttribute{"gidNumber", []string{fmt.Sprintf("%d", g.GIDNumber)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixGroup"}}) if g.Members != nil { members := make([]string, len(g.Members)) for i, v := range g.Members { members[i] = *v.ID } attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: members}) } dn := fmt.Sprintf("%s=%s,ou=groups,%s", h.backend.GroupFormatAsArray[0], *g.ID, h.backend.BaseDN) entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) } case "posixaccount", "": userName := "" if searchBaseDN != strings.ToLower(h.backend.BaseDN) { parts := strings.Split(strings.TrimSuffix(searchBaseDN, baseDN), ",") if len(parts) >= 1 { userName = strings.TrimPrefix(parts[0], "cn=") } } users, err := session.getUsers(userName) if err != nil { h.log.Debug().Str("username", userName).Err(err).Msg("Could not get user") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("search error: error getting users") } for _, u := range users { attrs := []*ldap.EntryAttribute{} for _, nameAttr := range h.backend.NameFormatAsArray { attrs = append(attrs, &ldap.EntryAttribute{Name: nameAttr, Values: []string{*u.ID}}) } if u.DisplayName != nil { attrs = append(attrs, &ldap.EntryAttribute{Name: "givenName", Values: []string{*u.DisplayName}}) } if u.Mail != nil { attrs = append(attrs, &ldap.EntryAttribute{Name: "mail", Values: []string{*u.Mail}}) } attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixAccount"}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s from ownCloud", *u.ID)}}) dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormatAsArray[0], *u.ID, h.backend.GroupFormatAsArray[0], "users", h.backend.BaseDN) entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) } } stats.Frontend.Add("search_successes", 1) h.log.Debug().Str("filter", searchReq.Filter).Msg("AP: Search OK") return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil } // Add is not yet supported for the owncloud backend func (h ownCloudHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) { start := time.Now() defer func() { h.monitor.SetResponseTimeMetric( map[string]string{"operation": "add", "status": fmt.Sprintf("%v", result)}, time.Since(start).Seconds(), ) }() return ldap.LDAPResultInsufficientAccessRights, nil } // Modify is not yet supported for the owncloud backend func (h ownCloudHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) { start := time.Now() defer func() { h.monitor.SetResponseTimeMetric( map[string]string{"operation": "modify", "status": fmt.Sprintf("%v", result)}, time.Since(start).Seconds(), ) }() return ldap.LDAPResultInsufficientAccessRights, nil } // Delete is not yet supported for the owncloud backend func (h ownCloudHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) { _, span := h.tracer.Start(context.Background(), "handler.configHandler.Delete") defer span.End() start := time.Now() defer func() { h.monitor.SetResponseTimeMetric( map[string]string{"operation": "delete", "status": fmt.Sprintf("%v", result)}, time.Since(start).Seconds(), ) }() return ldap.LDAPResultInsufficientAccessRights, nil } // FindUser with the given username. Called by the ldap backend to authenticate the bind. Optional func (h ownCloudHandler) FindUser(ctx context.Context, userName string, searchByUPN bool) (found bool, user config.User, err error) { _, span := h.tracer.Start(ctx, "handler.ownCloudHandler.FindUser") defer span.End() return false, config.User{}, nil } func (h ownCloudHandler) FindGroup(ctx context.Context, groupName string) (found bool, group config.Group, err error) { _, span := h.tracer.Start(ctx, "handler.ownCloudHandler.FindGroup") defer span.End() return false, config.Group{}, nil } func (h ownCloudHandler) Close(boundDN string, conn net.Conn) error { conn.Close() // close connection to the server when then client is closed h.lock.Lock() defer h.lock.Unlock() delete(h.sessions, connID(conn)) stats.Frontend.Add("closes", 1) stats.Backend.Add("closes", 1) return nil } func (h ownCloudHandler) login(name, pw string) bool { var req *http.Request if h.backend.UseGraphAPI { // TODO oc10 graphapi app should implement /me req, _ = http.NewRequest("GET", h.backend.Servers[0]+"/users/"+name, nil) } else { // use provisioning api meURL := fmt.Sprintf("%s/ocs/v2.php/cloud/user?format=json", h.backend.Servers[0]) req, _ = http.NewRequest("GET", meURL, nil) } req.SetBasicAuth(name, pw) resp, err := h.client.Do(req) if err != nil || resp.StatusCode != http.StatusOK { h.log.Error().Err(err).Int("status", resp.StatusCode).Msg("failed login") return false } defer resp.Body.Close() return true } type OCSGroupsResponse struct { Ocs struct { Meta struct { Message interface{} `json:"message"` Statuscode int `json:"statuscode"` Status string `json:"status"` } `json:"meta"` Data struct { Groups []string `json:"groups"` } `json:"data"` } `json:"ocs"` } func (s ownCloudSession) getGroups() ([]msgraph.Group, error) { if s.useGraphAPI { ctx := context.Background() req := s.NewClient().Groups().Request() req.Expand("members") return req.Get(ctx) } groupsUrl := fmt.Sprintf("%s/ocs/v2.php/cloud/groups?format=json", s.endpoint) req, _ := http.NewRequest("GET", groupsUrl, nil) req.SetBasicAuth(s.user, s.password) resp, err := s.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var f OCSGroupsResponse if err := json.Unmarshal(b, &f); err != nil { return nil, err } ret := make([]msgraph.Group, len(f.Ocs.Data.Groups)) for i := range f.Ocs.Data.Groups { ret[i] = msgraph.Group{DirectoryObject: msgraph.DirectoryObject{Entity: msgraph.Entity{ID: &f.Ocs.Data.Groups[i]}}} } return ret, nil } type OCSUsersResponse struct { Ocs struct { Data struct { Users []string `json:"users"` } `json:"data"` Meta struct { Statuscode int `json:"statuscode"` Message interface{} `json:"message"` Status string `json:"status"` } `json:"meta"` } `json:"ocs"` } // NewClient returns GraphService request builder with default base URL func (s ownCloudSession) NewClient() *msgraph.GraphServiceRequestBuilder { httpClient := &http.Client{ Transport: s, } g := msgraph.NewClient(httpClient) g.SetURL(s.endpoint) return g } func (s ownCloudSession) RoundTrip(req *http.Request) (*http.Response, error) { req.SetBasicAuth(s.user, s.password) return s.client.Transport.RoundTrip(req) } func (s ownCloudSession) getUsers(userName string) ([]msgraph.User, error) { if s.useGraphAPI { s.log.Debug().Msg("using graph api") ctx := context.Background() req := s.NewClient().Users() if len(userName) > 0 { s.log.Debug().Msg("fetching single user") u, err := req.ID(userName).Request().Get(ctx) if err != nil { return nil, err } return []msgraph.User{*u}, nil } s.log.Debug().Msg("fetching all users") return req.Request().Get(ctx) } s.log.Debug().Msg("using provisioning api") usersUrl := fmt.Sprintf("%s/ocs/v2.php/cloud/users?format=json", s.endpoint) req, _ := http.NewRequest("GET", usersUrl, nil) req.SetBasicAuth(s.user, s.password) resp, err := s.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var f OCSUsersResponse if err := json.Unmarshal(b, &f); err != nil { return nil, err } ret := make([]msgraph.User, len(f.Ocs.Data.Users)) for i := range f.Ocs.Data.Users { ret[i] = msgraph.User{ DirectoryObject: msgraph.DirectoryObject{ Entity: msgraph.Entity{ID: &f.Ocs.Data.Users[i]}, }, } } return ret, nil } func (s ownCloudSession) redirectPolicyFunc(req *http.Request, via []*http.Request) error { s.log.Debug().Str("username", s.user).Msg("Setting user and password") req.SetBasicAuth(s.user, s.password) return nil }