From 37940d58c7216b26f3e6cc530baaf8f61e4a664f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Dec 2019 11:15:10 +0100 Subject: [PATCH 1/2] Adding /v1.0/groups --- go.sum | 1 + pkg/service/v0/graph.go | 94 +++++++++++++++++++++++++++++++++++---- pkg/service/v0/service.go | 7 +++ 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/go.sum b/go.sum index 48e28c970..73503e847 100644 --- a/go.sum +++ b/go.sum @@ -657,6 +657,7 @@ github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6Ut github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2 h1:37LbK2gAU+1oaWKC5NTz+fNOsR2LgdRj/SAFVMucgss= github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2/go.mod h1:tso14hwzqX4VbnWTNsxiL0DvMb2OwbGISFA7jDibdWc= +github.com/yaegashi/msgraph.go v0.0.0-20191206184644-860e82e7ce3b h1:cDzhOBSEXM4yhv5oBG13+PxlVhIzwtcS5affFh7VNJk= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/pkg/service/v0/graph.go b/pkg/service/v0/graph.go index ea7df70df..30747dbd6 100644 --- a/pkg/service/v0/graph.go +++ b/pkg/service/v0/graph.go @@ -42,7 +42,7 @@ func (g Graph) UserCtx(next http.Handler) http.Handler { errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest) return } - user, err = g.ldapGetUser(userID) + user, err = g.ldapGetSingleEntry(userID, g.config.Ldap.BaseDNUsers) if err != nil { g.logger.Info().Err(err).Msgf("Failed to read user %s", userID) errorcode.ItemNotFound.Render(w, r, http.StatusNotFound) @@ -54,6 +54,28 @@ func (g Graph) UserCtx(next http.Handler) http.Handler { }) } +// GroupCtx 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) GroupCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + groupID := chi.URLParam(r, "groupID") + if groupID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest) + return + } + group, err := g.ldapGetSingleEntry(groupID, g.config.Ldap.BaseDNGroups) + if err != nil { + g.logger.Info().Err(err).Msgf("Failed to read group %s", groupID) + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound) + return + } + + ctx := context.WithValue(r.Context(), groupIDKey, group) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + // GetMe implements the Service interface. func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { me := createUserModel( @@ -82,7 +104,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { return } - result, err := g.ldapSearch(con, "(objectclass=*)") + result, err := g.ldapSearch(con, "(objectclass=*)", g.config.Ldap.BaseDNUsers) if err != nil { g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectclass=*)'") @@ -113,18 +135,58 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, createUserModelFromLDAP(user)) } -func (g Graph) ldapGetUser(userID string) (*ldap.Entry, error) { +// GetGroups implements the Service interface. +func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { + con, err := g.initLdap() + if err != nil { + g.logger.Error().Err(err).Msg("Failed to initialize ldap") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + return + } + + result, err := g.ldapSearch(con, "(objectclass=*)", g.config.Ldap.BaseDNGroups) + + if err != nil { + g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectclass=*)'") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + return + } + + var groups []*msgraph.Group + + for _, group := range result.Entries { + groups = append( + groups, + createGroupModelFromLDAP( + group, + ), + ) + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, groups) +} + +// GetGroup implements the Service interface. +func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { + group := r.Context().Value(groupIDKey).(*ldap.Entry) + + render.Status(r, http.StatusOK) + render.JSON(w, r, createGroupModelFromLDAP(group)) +} + +func (g Graph) ldapGetSingleEntry(resourceID string, baseDn 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) + filter := fmt.Sprintf("(entryuuid=%s)", resourceID) + result, err := g.ldapSearch(conn, filter, baseDn) if err != nil { return nil, err } if len(result.Entries) == 0 { - return nil, errors.New("user not found") + return nil, errors.New("resource not found") } return result.Entries[0], nil } @@ -143,9 +205,9 @@ func (g Graph) initLdap() (*ldap.Conn, error) { return con, nil } -func (g Graph) ldapSearch(con *ldap.Conn, filter string) (*ldap.SearchResult, error) { +func (g Graph) ldapSearch(con *ldap.Conn, filter string, baseDN string) (*ldap.SearchResult, error) { search := ldap.NewSearchRequest( - g.config.Ldap.BaseDNUsers, + baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, @@ -159,6 +221,7 @@ func (g Graph) ldapSearch(con *ldap.Conn, filter string) (*ldap.SearchResult, er "displayname", "entryuuid", "sn", + "cn", }, nil, ) @@ -197,8 +260,23 @@ func createUserModelFromLDAP(entry *ldap.Entry) *msgraph.User { } } +func createGroupModelFromLDAP(entry *ldap.Entry) *msgraph.Group { + id := entry.GetAttributeValue("entryuuid") + displayName := entry.GetAttributeValue("cn") + + return &msgraph.Group{ + DisplayName: &displayName, + 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 +const groupIDKey key = 1 diff --git a/pkg/service/v0/service.go b/pkg/service/v0/service.go index fac8f516f..0ecf2f58f 100644 --- a/pkg/service/v0/service.go +++ b/pkg/service/v0/service.go @@ -36,6 +36,13 @@ func NewService(opts ...Option) Service { r.Get("/", svc.GetUser) }) }) + r.Route("/groups", func(r chi.Router) { + r.Get("/", svc.GetGroups) + r.Route("/{groupID}", func(r chi.Router) { + r.Use(svc.GroupCtx) + r.Get("/", svc.GetGroup) + }) + }) }) }) From 1d0e201379c4d764fdb9eab5cd7683a7c4b31e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Dec 2019 12:35:07 +0100 Subject: [PATCH 2/2] Render proper list responses for users and groups --- pkg/service/v0/graph.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/service/v0/graph.go b/pkg/service/v0/graph.go index 30747dbd6..cb1b9f7fd 100644 --- a/pkg/service/v0/graph.go +++ b/pkg/service/v0/graph.go @@ -124,7 +124,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { } render.Status(r, http.StatusOK) - render.JSON(w, r, users) + render.JSON(w, r, &listResponse{Value: users}) } // GetUser implements the Service interface. @@ -164,7 +164,7 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { } render.Status(r, http.StatusOK) - render.JSON(w, r, groups) + render.JSON(w, r, &listResponse{Value: groups}) } // GetGroup implements the Service interface. @@ -280,3 +280,7 @@ type key int const userIDKey key = 0 const groupIDKey key = 1 + +type listResponse struct { + Value interface{} `json:"value,omitempty"` +}