Implement reading Group Members on Graph API

This implement the graph/v1.0/groups/{groupid}/members endpoint.
Starting with the LDAP backend.
This commit is contained in:
Ralf Haferkamp
2022-01-19 16:54:43 +01:00
parent dd7ae9cb47
commit 6f14ef7843
8 changed files with 100 additions and 9 deletions

View File

@@ -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 {

View File

@@ -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{}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)