From 6f14ef7843758addefad21b067221900c3359ec5 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 19 Jan 2022 16:54:43 +0100 Subject: [PATCH] Implement reading Group Members on Graph API This implement the graph/v1.0/groups/{groupid}/members endpoint. Starting with the LDAP backend. --- graph/pkg/identity/backend.go | 1 + graph/pkg/identity/cs3.go | 5 +++ graph/pkg/identity/ldap.go | 58 +++++++++++++++++++++++++----- graph/pkg/service/v0/groups.go | 28 +++++++++++++++ graph/pkg/service/v0/instrument.go | 5 +++ graph/pkg/service/v0/logging.go | 5 +++ graph/pkg/service/v0/service.go | 2 ++ graph/pkg/service/v0/tracing.go | 5 +++ 8 files changed, 100 insertions(+), 9 deletions(-) diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index 6d7f652e2..5d9e57fa2 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -25,6 +25,7 @@ type Backend interface { CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) GetGroup(ctx context.Context, nameOrID string) (*libregraph.Group, error) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error) + GetGroupMembers(ctx context.Context, id string) ([]*libregraph.User, error) } func CreateUserModelFromCS3(u *cs3.User) *libregraph.User { diff --git a/graph/pkg/identity/cs3.go b/graph/pkg/identity/cs3.go index 1b40eb92c..bdfdb163c 100644 --- a/graph/pkg/identity/cs3.go +++ b/graph/pkg/identity/cs3.go @@ -174,6 +174,11 @@ func (i *CS3) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, return createGroupModelFromCS3(res.Group), nil } +// GetGroupMembers implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) GetGroupMembers(ctx context.Context, groupID string) ([]*libregraph.User, error) { + return nil, errorcode.New(errorcode.NotSupported, "not implemented") +} + func createGroupModelFromCS3(g *cs3group.Group) *libregraph.Group { if g.Id == nil { g.Id = &cs3group.GroupId{} diff --git a/graph/pkg/identity/ldap.go b/graph/pkg/identity/ldap.go index 1d5221fce..5d805d2a9 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -364,16 +364,30 @@ func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregra return users, nil } -func (i *LDAP) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, error) { +func (i *LDAP) GetGroup(ctx context.Context, nameOrID string) (*libregraph.Group, error) { i.logger.Debug().Str("backend", "ldap").Msg("GetGroup") - groupID = ldap.EscapeFilter(groupID) + e, err := i.getLDAPGroupByNameOrID(nameOrID, false) + if err != nil { + return nil, err + } + return i.createGroupModelFromLDAP(e), nil +} + +func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ldap.Entry, error) { + nameOrID = ldap.EscapeFilter(nameOrID) + attrs := []string{ + i.groupAttributeMap.name, + i.groupAttributeMap.id, + } + + if requestMembers { + attrs = append(attrs, i.groupAttributeMap.member) + } + searchRequest := ldap.NewSearchRequest( i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 1, 0, false, - fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.groupFilter, i.groupAttributeMap.name, groupID, i.groupAttributeMap.id, groupID), - []string{ - i.groupAttributeMap.name, - i.groupAttributeMap.id, - }, + fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.groupFilter, i.groupAttributeMap.name, nameOrID, i.groupAttributeMap.id, nameOrID), + attrs, nil, ) i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.groupBaseDN) @@ -383,7 +397,7 @@ func (i *LDAP) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, var errmsg string if lerr, ok := err.(*ldap.Error); ok { if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded { - errmsg = fmt.Sprintf("too many results searching for group '%s'", groupID) + errmsg = fmt.Sprintf("too many results searching for group '%s'", nameOrID) i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg) } } @@ -393,7 +407,7 @@ func (i *LDAP) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, return nil, errNotFound } - return i.createGroupModelFromLDAP(res.Entries[0]), nil + return res.Entries[0], nil } func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error) { @@ -436,6 +450,32 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregr return groups, nil } +// GetGroupMembers implements the Backend Interface for the LDAP Backend +func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregraph.User, error) { + e, err := i.getLDAPGroupByNameOrID(groupID, true) + if err != nil { + return nil, err + } + + result := []*libregraph.User{} + + for _, memberDN := range e.GetEqualFoldAttributeValues(i.groupAttributeMap.member) { + if memberDN == "" { + continue + } + i.logger.Debug().Str("memberDN", memberDN).Msg("lookup") + ue, err := i.getUserByDN(memberDN) + if err != nil { + // Ignore errors when reading a specific member fails, just log them and continue + i.logger.Warn().Err(err).Str("member", memberDN).Msg("error reading group member") + continue + } + result = append(result, i.createUserModelFromLDAP(ue)) + } + + return result, nil +} + // CreateGroup implements the Backend Interface for the LDAP Backend // It is currently restricted to managing groups based on the "groupOfNames" ObjectClass. // As "groupOfNames" requires a "member" Attribute to be present. Empty Groups (groups diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index fdbfab3f2..cefb6622a 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -87,3 +87,31 @@ func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) render.JSON(w, r, group) } + +func (g Graph) GetGroupMembers(w http.ResponseWriter, r *http.Request) { + groupID := chi.URLParam(r, "groupID") + groupID, err := url.PathUnescape(groupID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed") + return + } + + if groupID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") + return + } + + members, err := g.identityBackend.GetGroupMembers(r.Context(), groupID) + if err != nil { + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, members) +} diff --git a/graph/pkg/service/v0/instrument.go b/graph/pkg/service/v0/instrument.go index 627a9784d..bb7fae1ba 100644 --- a/graph/pkg/service/v0/instrument.go +++ b/graph/pkg/service/v0/instrument.go @@ -69,6 +69,11 @@ func (i instrument) PostGroup(w http.ResponseWriter, r *http.Request) { i.next.PostGroup(w, r) } +// GetGroupMembers implements the Service interface. +func (i instrument) GetGroupMembers(w http.ResponseWriter, r *http.Request) { + i.next.GetGroupMembers(w, r) +} + // GetDrives implements the Service interface. func (i instrument) GetDrives(w http.ResponseWriter, r *http.Request) { i.next.GetDrives(w, r) diff --git a/graph/pkg/service/v0/logging.go b/graph/pkg/service/v0/logging.go index eec57f995..ce39fcff2 100644 --- a/graph/pkg/service/v0/logging.go +++ b/graph/pkg/service/v0/logging.go @@ -69,6 +69,11 @@ func (l logging) PostGroup(w http.ResponseWriter, r *http.Request) { l.next.PostGroup(w, r) } +// GetGroupMembers implements the Service interface. +func (l logging) GetGroupMembers(w http.ResponseWriter, r *http.Request) { + l.next.GetGroupMembers(w, r) +} + // GetDrives implements the Service interface. func (l logging) GetDrives(w http.ResponseWriter, r *http.Request) { l.next.GetDrives(w, r) diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index f29982a01..b417d74ff 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -34,6 +34,7 @@ type Service interface { GetGroups(http.ResponseWriter, *http.Request) GetGroup(http.ResponseWriter, *http.Request) PostGroup(http.ResponseWriter, *http.Request) + GetGroupMembers(http.ResponseWriter, *http.Request) GetDrives(w http.ResponseWriter, r *http.Request) } @@ -116,6 +117,7 @@ func NewService(opts ...Option) Service { r.Post("/", svc.PostGroup) r.Route("/{groupID}", func(r chi.Router) { r.Get("/", svc.GetGroup) + r.Get("/members", svc.GetGroupMembers) }) }) r.Group(func(r chi.Router) { diff --git a/graph/pkg/service/v0/tracing.go b/graph/pkg/service/v0/tracing.go index 1f58944c2..6784d32f1 100644 --- a/graph/pkg/service/v0/tracing.go +++ b/graph/pkg/service/v0/tracing.go @@ -65,6 +65,11 @@ func (t tracing) PostGroup(w http.ResponseWriter, r *http.Request) { t.next.PostGroup(w, r) } +// GetGroupMembers implements the Service interface. +func (t tracing) GetGroupMembers(w http.ResponseWriter, r *http.Request) { + t.next.GetGroupMembers(w, r) +} + // GetDrives implements the Service interface. func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) { t.next.GetDrives(w, r)