From edcc7f43a96fb622f3ecdc444ad3a09e57511dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 6 Dec 2019 14:33:19 +0100 Subject: [PATCH 1/2] Read more attributes from LDAP and implement /v1.0/users/{userID} --- pkg/service/v0/service.go | 68 ++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/pkg/service/v0/service.go b/pkg/service/v0/service.go index 97adfa3a9..ab8057a9d 100644 --- a/pkg/service/v0/service.go +++ b/pkg/service/v0/service.go @@ -2,6 +2,7 @@ package svc import ( "encoding/json" + "fmt" "net/http" "github.com/go-chi/chi" @@ -30,8 +31,15 @@ func NewService(opts ...Option) Service { mux: m, } - m.HandleFunc("/v1.0/me", svc.Me) - m.HandleFunc("/v1.0/users", svc.Users) + m.Route("/v1.0", func(r chi.Router) { + r.Get("/me", svc.Me) + r.Route("/users", func(r chi.Router) { + r.Get("/", svc.Users) + r.Route("/{userId}", func(r chi.Router) { + r.Get("/", svc.Users) + }) + }) + }) return svc } @@ -67,6 +75,12 @@ func (g Graph) Me(w http.ResponseWriter, r *http.Request) { // Users implements the Service interface. func (g Graph) Users(w http.ResponseWriter, r *http.Request) { + userID := chi.URLParam(r, "userId") + filter := "(objectclass=*)" + if userID != "" { + filter = fmt.Sprintf("(entryuuid=%s)", userID) + } + con, err := ldap.Dial("tcp", "localhost:10389") if err != nil { @@ -82,19 +96,20 @@ func (g Graph) Users(w http.ResponseWriter, r *http.Request) { } search := ldap.NewSearchRequest( - "ou=groups,dc=example,dc=org", + "ou=users,dc=example,dc=org", ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - "(objectclass=*)", - []string{ - "dn", - "uuid", + filter, + []string{"dn", "uid", - "givenName", + "givenname", "mail", + "displayname", + "entryuuid", + "sn", }, nil, ) @@ -107,14 +122,24 @@ func (g Graph) Users(w http.ResponseWriter, r *http.Request) { return } - users := make([]*msgraph.User, len(result.Entries)) + if userID != "" { + if len(result.Entries) == 0 { + w.WriteHeader(http.StatusNotFound) + return + } + user := createUserModelFromLDAP(result.Entries[0]) + render.Status(r, http.StatusOK) + render.JSON(w, r, user) + return + } + + var users []*msgraph.User for _, user := range result.Entries { users = append( users, - createUserModel( - user.DN, - "1234-5678-9000-000", + createUserModelFromLDAP( + user, ), ) } @@ -134,3 +159,22 @@ func createUserModel(displayName string, id string) *msgraph.User { }, } } + +func createUserModelFromLDAP(entry *ldap.Entry) *msgraph.User { + displayName := entry.GetAttributeValue("displayname") + givenName := entry.GetAttributeValue("givenname") + mail := entry.GetAttributeValue("mail") + surName := entry.GetAttributeValue("sn") + id := entry.GetAttributeValue("entryuuid") + return &msgraph.User{ + DisplayName: &displayName, + GivenName: &givenName, + Surname: &surName, + Mail: &mail, + DirectoryObject: msgraph.DirectoryObject{ + Entity: msgraph.Entity{ + ID: &id, + }, + }, + } +} From c31ddc2417afd7f7e8c457ceaddb5d5551ebf161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 9 Dec 2019 11:53:26 +0100 Subject: [PATCH 2/2] A bit more structure to the code base ... --- pkg/service/v0/graph.go | 203 +++++++++++++++++++++++++++++++++++ pkg/service/v0/instrument.go | 17 +-- pkg/service/v0/logging.go | 17 +-- pkg/service/v0/service.go | 156 ++------------------------- pkg/service/v0/tracing.go | 17 +-- 5 files changed, 245 insertions(+), 165 deletions(-) create mode 100644 pkg/service/v0/graph.go diff --git a/pkg/service/v0/graph.go b/pkg/service/v0/graph.go new file mode 100644 index 000000000..a7f871f8c --- /dev/null +++ b/pkg/service/v0/graph.go @@ -0,0 +1,203 @@ +package svc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/render" + "github.com/owncloud/ocis-graph/pkg/config" + "github.com/owncloud/ocis-pkg/log" + msgraph "github.com/yaegashi/msgraph.go/v1.0" + ldap "gopkg.in/ldap.v3" +) + +// Graph defines implements the business logic for Service. +type Graph struct { + config *config.Config + mux *chi.Mux + logger *log.Logger +} + +// ServeHTTP implements the Service interface. +func (g Graph) ServeHTTP(w http.ResponseWriter, r *http.Request) { + g.mux.ServeHTTP(w, r) +} + +// UserCtx middleware is used to load an User object from +// the URL parameters passed through as the request. In case +// the User could not be found, we stop here and return a 404. +func (g Graph) UserCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var user *ldap.Entry + var err error + + if userID := chi.URLParam(r, "userID"); userID != "" { + user, err = g.ldapGetUser(userID) + } else { + // TODO: we should not give this error out to users + // http.Error(w, err.Error(), http.StatusInternalServerError) + render.Status(r, http.StatusNotFound) + return + } + if err != nil { + g.logger.Info().Msgf("error reading user: %s", err.Error()) + render.Status(r, http.StatusNotFound) + return + } + + ctx := context.WithValue(r.Context(), userIDKey, user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// GetMe implements the Service interface. +func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { + me := createUserModel( + "Alice", + "1234-5678-9000-000", + ) + + resp, err := json.Marshal(me) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, resp) +} + +// GetUsers implements the Service interface. +func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { + con, err := g.initLdap() + if err != nil { + // TODO: we should not give this error out to users + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + result, err := g.ldapSearch(con, "(objectclass=*)") + + if err != nil { + // TODO: we should not give this error out to users + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var users []*msgraph.User + + for _, user := range result.Entries { + users = append( + users, + createUserModelFromLDAP( + user, + ), + ) + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, users) +} + +// GetUser implements the Service interface. +func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(userIDKey).(*ldap.Entry) + + render.Status(r, http.StatusOK) + render.JSON(w, r, createUserModelFromLDAP(user)) +} + +func (g Graph) ldapGetUser(userID string) (*ldap.Entry, error) { + conn, err := g.initLdap() + if err != nil { + return nil, err + } + filter := fmt.Sprintf("(entryuuid=%s)", userID) + result, err := g.ldapSearch(conn, filter) + if err != nil { + return nil, err + } + if len(result.Entries) == 0 { + return nil, errors.New("user not found") + } + return result.Entries[0], nil +} + +func (g Graph) initLdap() (*ldap.Conn, error) { + g.logger.Info().Msg("Dailing ldap.... ") + con, err := ldap.Dial("tcp", "localhost:10389") + + if err != nil { + return nil, err + } + + if err := con.Bind("cn=admin,dc=example,dc=org", "admin"); err != nil { + return nil, err + } + return con, nil +} + +func (g Graph) ldapSearch(con *ldap.Conn, filter string) (*ldap.SearchResult, error) { + search := ldap.NewSearchRequest( + "ou=users,dc=example,dc=org", + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, + 0, + false, + filter, + []string{"dn", + "uid", + "givenname", + "mail", + "displayname", + "entryuuid", + "sn", + }, + nil, + ) + + return con.Search(search) +} + +func createUserModel(displayName string, id string) *msgraph.User { + return &msgraph.User{ + DisplayName: &displayName, + GivenName: &displayName, + DirectoryObject: msgraph.DirectoryObject{ + Entity: msgraph.Entity{ + ID: &id, + }, + }, + } +} + +func createUserModelFromLDAP(entry *ldap.Entry) *msgraph.User { + displayName := entry.GetAttributeValue("displayname") + givenName := entry.GetAttributeValue("givenname") + mail := entry.GetAttributeValue("mail") + surName := entry.GetAttributeValue("sn") + id := entry.GetAttributeValue("entryuuid") + return &msgraph.User{ + DisplayName: &displayName, + GivenName: &givenName, + Surname: &surName, + Mail: &mail, + DirectoryObject: msgraph.DirectoryObject{ + Entity: msgraph.Entity{ + ID: &id, + }, + }, + } +} + +// The key type is unexported to prevent collisions with context keys defined in +// other packages. +type key int + +const userIDKey key = 0 diff --git a/pkg/service/v0/instrument.go b/pkg/service/v0/instrument.go index 2e2488c5f..b427a0c51 100644 --- a/pkg/service/v0/instrument.go +++ b/pkg/service/v0/instrument.go @@ -24,12 +24,17 @@ func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) { i.next.ServeHTTP(w, r) } -// Me implements the Service interface. -func (i instrument) Me(w http.ResponseWriter, r *http.Request) { - i.next.Me(w, r) +// GetMe implements the Service interface. +func (i instrument) GetMe(w http.ResponseWriter, r *http.Request) { + i.next.GetMe(w, r) } -// Users implements the Service interface. -func (i instrument) Users(w http.ResponseWriter, r *http.Request) { - i.next.Users(w, r) +// GetUsers implements the Service interface. +func (i instrument) GetUsers(w http.ResponseWriter, r *http.Request) { + i.next.GetUsers(w, r) +} + +// GetUsers implements the Service interface. +func (i instrument) GetUser(w http.ResponseWriter, r *http.Request) { + i.next.GetUser(w, r) } diff --git a/pkg/service/v0/logging.go b/pkg/service/v0/logging.go index 7b7944395..a44cb9552 100644 --- a/pkg/service/v0/logging.go +++ b/pkg/service/v0/logging.go @@ -24,12 +24,17 @@ func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { l.next.ServeHTTP(w, r) } -// Me implements the Service interface. -func (l logging) Me(w http.ResponseWriter, r *http.Request) { - l.next.Me(w, r) +// GetMe implements the Service interface. +func (l logging) GetMe(w http.ResponseWriter, r *http.Request) { + l.next.GetMe(w, r) } -// Users implements the Service interface. -func (l logging) Users(w http.ResponseWriter, r *http.Request) { - l.next.Users(w, r) +// GetUsers implements the Service interface. +func (l logging) GetUsers(w http.ResponseWriter, r *http.Request) { + l.next.GetUsers(w, r) +} + +// GetUser implements the Service interface. +func (l logging) GetUser(w http.ResponseWriter, r *http.Request) { + l.next.GetUser(w, r) } diff --git a/pkg/service/v0/service.go b/pkg/service/v0/service.go index ab8057a9d..a2137ff6e 100644 --- a/pkg/service/v0/service.go +++ b/pkg/service/v0/service.go @@ -1,22 +1,17 @@ package svc import ( - "encoding/json" - "fmt" "net/http" "github.com/go-chi/chi" - "github.com/go-chi/render" - "github.com/owncloud/ocis-graph/pkg/config" - msgraph "github.com/yaegashi/msgraph.go/v1.0" - ldap "gopkg.in/ldap.v3" ) // Service defines the extension handlers. type Service interface { ServeHTTP(http.ResponseWriter, *http.Request) - Me(http.ResponseWriter, *http.Request) - Users(http.ResponseWriter, *http.Request) + GetMe(http.ResponseWriter, *http.Request) + GetUsers(http.ResponseWriter, *http.Request) + GetUser(http.ResponseWriter, *http.Request) } // NewService returns a service implementation for Service. @@ -29,152 +24,19 @@ func NewService(opts ...Option) Service { svc := Graph{ config: options.Config, mux: m, + logger: &options.Logger, } m.Route("/v1.0", func(r chi.Router) { - r.Get("/me", svc.Me) + r.Get("/me", svc.GetMe) r.Route("/users", func(r chi.Router) { - r.Get("/", svc.Users) - r.Route("/{userId}", func(r chi.Router) { - r.Get("/", svc.Users) + r.Get("/", svc.GetUsers) + r.Route("/{userID}", func(r chi.Router) { + r.Use(svc.UserCtx) + r.Get("/", svc.GetUser) }) }) }) return svc } - -// Graph defines implements the business logic for Service. -type Graph struct { - config *config.Config - mux *chi.Mux -} - -// ServeHTTP implements the Service interface. -func (g Graph) ServeHTTP(w http.ResponseWriter, r *http.Request) { - g.mux.ServeHTTP(w, r) -} - -// Me implements the Service interface. -func (g Graph) Me(w http.ResponseWriter, r *http.Request) { - me := createUserModel( - "Alice", - "1234-5678-9000-000", - ) - - resp, err := json.Marshal(me) - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, resp) -} - -// Users implements the Service interface. -func (g Graph) Users(w http.ResponseWriter, r *http.Request) { - userID := chi.URLParam(r, "userId") - filter := "(objectclass=*)" - if userID != "" { - filter = fmt.Sprintf("(entryuuid=%s)", userID) - } - - con, err := ldap.Dial("tcp", "localhost:10389") - - if err != nil { - // TODO: we should not give this error out to users - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if err := con.Bind("cn=admin,dc=example,dc=org", "admin"); err != nil { - // TODO: we should not give this error out to users - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - search := ldap.NewSearchRequest( - "ou=users,dc=example,dc=org", - ldap.ScopeWholeSubtree, - ldap.NeverDerefAliases, - 0, - 0, - false, - filter, - []string{"dn", - "uid", - "givenname", - "mail", - "displayname", - "entryuuid", - "sn", - }, - nil, - ) - - result, err := con.Search(search) - - if err != nil { - // TODO: we should not give this error out to users - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if userID != "" { - if len(result.Entries) == 0 { - w.WriteHeader(http.StatusNotFound) - return - } - user := createUserModelFromLDAP(result.Entries[0]) - render.Status(r, http.StatusOK) - render.JSON(w, r, user) - return - } - - var users []*msgraph.User - - for _, user := range result.Entries { - users = append( - users, - createUserModelFromLDAP( - user, - ), - ) - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, users) -} - -func createUserModel(displayName string, id string) *msgraph.User { - return &msgraph.User{ - DisplayName: &displayName, - GivenName: &displayName, - DirectoryObject: msgraph.DirectoryObject{ - Entity: msgraph.Entity{ - ID: &id, - }, - }, - } -} - -func createUserModelFromLDAP(entry *ldap.Entry) *msgraph.User { - displayName := entry.GetAttributeValue("displayname") - givenName := entry.GetAttributeValue("givenname") - mail := entry.GetAttributeValue("mail") - surName := entry.GetAttributeValue("sn") - id := entry.GetAttributeValue("entryuuid") - return &msgraph.User{ - DisplayName: &displayName, - GivenName: &givenName, - Surname: &surName, - Mail: &mail, - DirectoryObject: msgraph.DirectoryObject{ - Entity: msgraph.Entity{ - ID: &id, - }, - }, - } -} diff --git a/pkg/service/v0/tracing.go b/pkg/service/v0/tracing.go index 423c492b7..64cbc9708 100644 --- a/pkg/service/v0/tracing.go +++ b/pkg/service/v0/tracing.go @@ -20,12 +20,17 @@ func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) { t.next.ServeHTTP(w, r) } -// Me implements the Service interface. -func (t tracing) Me(w http.ResponseWriter, r *http.Request) { - t.next.Me(w, r) +// GetMe implements the Service interface. +func (t tracing) GetMe(w http.ResponseWriter, r *http.Request) { + t.next.GetMe(w, r) } -// Users implements the Service interface. -func (t tracing) Users(w http.ResponseWriter, r *http.Request) { - t.next.Users(w, r) +// GetUsers implements the Service interface. +func (t tracing) GetUsers(w http.ResponseWriter, r *http.Request) { + t.next.GetUsers(w, r) +} + +// GetUser implements the Service interface. +func (t tracing) GetUser(w http.ResponseWriter, r *http.Request) { + t.next.GetUser(w, r) }