From dd7ae9cb472ca0181660bf0c594a40731998c64a Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 19 Jan 2022 15:46:22 +0100 Subject: [PATCH 1/8] Implement CreateGroup support for Graph LDAP Backend This add basic support to create Groups in LDAP via the GraphAPI. Currently this is hardcoded to use the standard LDAP "groupOfNames" objectClass. --- graph/pkg/identity/backend.go | 2 + graph/pkg/identity/cs3.go | 5 ++ graph/pkg/identity/ldap.go | 94 +++++++++++++++++++++++++----- graph/pkg/service/v0/groups.go | 33 +++++++++++ graph/pkg/service/v0/instrument.go | 15 +++++ graph/pkg/service/v0/logging.go | 15 +++++ graph/pkg/service/v0/service.go | 5 ++ graph/pkg/service/v0/tracing.go | 15 +++++ 8 files changed, 170 insertions(+), 14 deletions(-) diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index 62d9ccd13..6d7f652e2 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -21,6 +21,8 @@ type Backend interface { GetUser(ctx context.Context, nameOrID string) (*libregraph.User, error) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) + // Create Group creates the supplied group in the identity backend. + 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) } diff --git a/graph/pkg/identity/cs3.go b/graph/pkg/identity/cs3.go index 36e6108bd..1b40eb92c 100644 --- a/graph/pkg/identity/cs3.go +++ b/graph/pkg/identity/cs3.go @@ -142,6 +142,11 @@ func (i *CS3) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregra return groups, nil } +// CreateGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) { + return nil, errorcode.New(errorcode.NotSupported, "not implemented") +} + func (i *CS3) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, error) { client, err := pool.GetGatewayServiceClient(i.Config.Address) if err != nil { diff --git a/graph/pkg/identity/ldap.go b/graph/pkg/identity/ldap.go index 04c8d7e97..1d5221fce 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -46,8 +46,10 @@ type userAttributeMap struct { } type groupAttributeMap struct { - name string - id string + name string + id string + member string + memberSyntax string } func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LDAP, error) { @@ -66,8 +68,10 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD return nil, errors.New("invalid group attribute mappings") } gam := groupAttributeMap{ - name: config.GroupNameAttribute, - id: config.GroupIDAttribute, + name: config.GroupNameAttribute, + id: config.GroupIDAttribute, + member: "member", + memberSyntax: "dn", } var userScope, groupScope int @@ -180,7 +184,7 @@ func (i *LDAP) DeleteUser(ctx context.Context, nameOrID string) error { return nil } -// UpdateUser implements the Backend Interface. It's currently not suported for the CS3 backedn +// UpdateUser implements the Backend Interface for the LDAP Backend func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error) { if !i.writeEnabled { return nil, errReadOnly @@ -235,15 +239,28 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph. } func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) { + attrs := []string{ + i.userAttributeMap.displayName, + i.userAttributeMap.id, + i.userAttributeMap.mail, + i.userAttributeMap.userName, + } + return i.getEntryByDN(dn, attrs) +} + +func (i *LDAP) getGroupByDN(dn string) (*ldap.Entry, error) { + attrs := []string{ + i.groupAttributeMap.id, + i.groupAttributeMap.name, + } + return i.getEntryByDN(dn, attrs) +} + +func (i *LDAP) getEntryByDN(dn string, attrs []string) (*ldap.Entry, error) { searchRequest := ldap.NewSearchRequest( dn, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, "(objectclass=*)", - []string{ - i.userAttributeMap.displayName, - i.userAttributeMap.id, - i.userAttributeMap.mail, - i.userAttributeMap.userName, - }, + attrs, nil, ) @@ -419,6 +436,56 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregr return groups, 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 +// without a member) a represented by adding an empty DN as the single member. +func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) { + if !i.writeEnabled { + return nil, errorcode.New(errorcode.NotAllowed, "server is configured read-only") + } + ar := ldap.AddRequest{ + DN: fmt.Sprintf("cn=%s,%s", *group.DisplayName, i.groupBaseDN), + Attributes: []ldap.Attribute{ + { + Type: i.groupAttributeMap.name, + Vals: []string{*group.DisplayName}, + }, + // This is a crutch to allow groups without members for LDAP Server's which + // that apply strict Schema checking. The RFCs define "member/uniqueMember" + // as required attribute for groupOfNames/groupOfUniqueNames. So we + // add an empty string (which is a valid DN) as the initial member. + // It will be replace once real members are added. + // We might wanna use the newer, but not so broadly used "groupOfMembers" + // objectclass (RFC2307bis-02) where "member" is optional. + { + Type: i.groupAttributeMap.member, + Vals: []string{""}, + }, + }, + } + + // TODO make group objectclass configurable to support e.g. posixGroup, groupOfUniqueNames, groupOfMembers?} + objectClasses := []string{"groupOfNames", "top"} + + if !i.useServerUUID { + ar.Attribute("owncloudUUID", []string{uuid.Must(uuid.NewV4()).String()}) + objectClasses = append(objectClasses, "owncloud") + } + ar.Attribute("objectClass", objectClasses) + + if err := i.conn.Add(&ar); err != nil { + return nil, err + } + + // Read back group from LDAP to get the generated UUID + e, err := i.getGroupByDN(ar.DN) + if err != nil { + return nil, err + } + return i.createGroupModelFromLDAP(e), nil +} + func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { if e == nil { return nil @@ -433,9 +500,8 @@ func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group { return &libregraph.Group{ - DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)), - OnPremisesSamAccountName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)), - Id: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)), + DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)), + Id: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)), } } func pointerOrNil(val string) *string { diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index 059812636..fdbfab3f2 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -1,10 +1,12 @@ package svc import ( + "encoding/json" "errors" "net/http" "net/url" + libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" "github.com/go-chi/chi/v5" @@ -28,6 +30,37 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, &listResponse{Value: groups}) } +// PostGroup implements the Service interface. +func (g Graph) PostGroup(w http.ResponseWriter, r *http.Request) { + grp := libregraph.NewGroup() + err := json.NewDecoder(r.Body).Decode(grp) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + + if isNilOrEmpty(grp.DisplayName) { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Missing Required Attribute") + return + } + + // Disallow user-supplied IDs. It's supposed to be readonly. We're either + // generating them in the backend ourselves or rely on the Backend's + // storage (e.g. LDAP) to provide a unique ID. + if !isNilOrEmpty(grp.Id) { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "group id is a read-only attribute") + return + } + + if grp, err = g.identityBackend.CreateGroup(r.Context(), *grp); err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, grp) +} + // GetGroup implements the Service interface. func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { groupID := chi.URLParam(r, "groupID") diff --git a/graph/pkg/service/v0/instrument.go b/graph/pkg/service/v0/instrument.go index 05cb9306c..627a9784d 100644 --- a/graph/pkg/service/v0/instrument.go +++ b/graph/pkg/service/v0/instrument.go @@ -54,6 +54,21 @@ func (i instrument) PatchUser(w http.ResponseWriter, r *http.Request) { i.next.PatchUser(w, r) } +// GetGroups implements the Service interface. +func (i instrument) GetGroups(w http.ResponseWriter, r *http.Request) { + i.next.GetGroups(w, r) +} + +// GetGroup implements the Service interface. +func (i instrument) GetGroup(w http.ResponseWriter, r *http.Request) { + i.next.GetGroup(w, r) +} + +// PostGroup implements the Service interface. +func (i instrument) PostGroup(w http.ResponseWriter, r *http.Request) { + i.next.PostGroup(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 6a29c3ef8..eec57f995 100644 --- a/graph/pkg/service/v0/logging.go +++ b/graph/pkg/service/v0/logging.go @@ -54,6 +54,21 @@ func (l logging) PatchUser(w http.ResponseWriter, r *http.Request) { l.next.PatchUser(w, r) } +// GetGroups implements the Service interface. +func (l logging) GetGroups(w http.ResponseWriter, r *http.Request) { + l.next.GetGroups(w, r) +} + +// GetGroup implements the Service interface. +func (l logging) GetGroup(w http.ResponseWriter, r *http.Request) { + l.next.GetGroup(w, r) +} + +// PostGroup implements the Service interface. +func (l logging) PostGroup(w http.ResponseWriter, r *http.Request) { + l.next.PostGroup(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 870ea8786..f29982a01 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -31,6 +31,10 @@ type Service interface { DeleteUser(http.ResponseWriter, *http.Request) PatchUser(http.ResponseWriter, *http.Request) + GetGroups(http.ResponseWriter, *http.Request) + GetGroup(http.ResponseWriter, *http.Request) + PostGroup(http.ResponseWriter, *http.Request) + GetDrives(w http.ResponseWriter, r *http.Request) } @@ -109,6 +113,7 @@ func NewService(opts ...Option) Service { }) r.Route("/groups", func(r chi.Router) { r.Get("/", svc.GetGroups) + r.Post("/", svc.PostGroup) r.Route("/{groupID}", func(r chi.Router) { r.Get("/", svc.GetGroup) }) diff --git a/graph/pkg/service/v0/tracing.go b/graph/pkg/service/v0/tracing.go index 59e9197c7..1f58944c2 100644 --- a/graph/pkg/service/v0/tracing.go +++ b/graph/pkg/service/v0/tracing.go @@ -50,6 +50,21 @@ func (t tracing) PatchUser(w http.ResponseWriter, r *http.Request) { t.next.PatchUser(w, r) } +// GetGroups implements the Service interface. +func (t tracing) GetGroups(w http.ResponseWriter, r *http.Request) { + t.next.GetGroups(w, r) +} + +// GetGroup implements the Service interface. +func (t tracing) GetGroup(w http.ResponseWriter, r *http.Request) { + t.next.GetGroup(w, r) +} + +// PostGroup implements the Service interface. +func (t tracing) PostGroup(w http.ResponseWriter, r *http.Request) { + t.next.PostGroup(w, r) +} + // GetDrives implements the Service interface. func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) { t.next.GetDrives(w, r) From 6f14ef7843758addefad21b067221900c3359ec5 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 19 Jan 2022 16:54:43 +0100 Subject: [PATCH 2/8] 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) From 2ffd2d51f48943103c9159c3305074aff04e76e5 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 20 Jan 2022 17:22:08 +0100 Subject: [PATCH 3/8] Rework getLDAP*ByNameOrID helpers We need to be able to search by ID only as well. So split the helpers into getLDAP*ByID and getLDAP*ByNameOrID. --- graph/pkg/identity/ldap.go | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/graph/pkg/identity/ldap.go b/graph/pkg/identity/ldap.go index 5d805d2a9..edd35dd51 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -278,11 +278,22 @@ func (i *LDAP) getEntryByDN(dn string, attrs []string) (*ldap.Entry, error) { return res.Entries[0], nil } +func (i *LDAP) getLDAPUserByID(id string) (*ldap.Entry, error) { + id = ldap.EscapeFilter(id) + filter := fmt.Sprintf("(%s=%s)", i.userAttributeMap.id, id) + return i.getLDAPUserByFilter(filter) +} + func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) { nameOrID = ldap.EscapeFilter(nameOrID) + filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID) + return i.getLDAPUserByFilter(filter) +} + +func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) { searchRequest := ldap.NewSearchRequest( i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 1, 0, false, - fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.userFilter, i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID), + fmt.Sprintf("(&%s%s)", i.userFilter, filter), []string{ i.userAttributeMap.displayName, i.userAttributeMap.id, @@ -298,9 +309,9 @@ func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) { var errmsg string if lerr, ok := err.(*ldap.Error); ok { if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded { - errmsg = fmt.Sprintf("too many results searching for user '%s'", nameOrID) + errmsg = fmt.Sprintf("too many results searching for user '%s'", filter) i.logger.Debug().Str("backend", "ldap").Err(lerr). - Str("user", nameOrID).Msg("too many results searching for user") + Str("userfilter", filter).Msg("too many results searching for user") } } return nil, errorcode.New(errorcode.ItemNotFound, errmsg) @@ -373,8 +384,22 @@ func (i *LDAP) GetGroup(ctx context.Context, nameOrID string) (*libregraph.Group return i.createGroupModelFromLDAP(e), nil } +func (i *LDAP) getLDAPGroupByID(id string, requestMembers bool) (*ldap.Entry, error) { + id = ldap.EscapeFilter(id) + filter := fmt.Sprintf("(%s=%s)", i.groupAttributeMap.id, id) + return i.getLDAPGroupByFilter(filter, requestMembers) +} + func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ldap.Entry, error) { nameOrID = ldap.EscapeFilter(nameOrID) + filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.groupAttributeMap.name, nameOrID, i.groupAttributeMap.id, nameOrID) + return i.getLDAPGroupByFilter(filter, requestMembers) +} + +// Search for LDAP Groups matching the specified filter, if requestMembers is true the groupMemberShip +// attribute will be part of the result attributes. The LDAP filter is combined with the configured groupFilter +// resulting in a filter like "(&(LDAP.groupFilter)())" +func (i *LDAP) getLDAPGroupByFilter(filter string, requestMembers bool) (*ldap.Entry, error) { attrs := []string{ i.groupAttributeMap.name, i.groupAttributeMap.id, @@ -386,7 +411,7 @@ func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ld searchRequest := ldap.NewSearchRequest( i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 1, 0, false, - fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.groupFilter, i.groupAttributeMap.name, nameOrID, i.groupAttributeMap.id, nameOrID), + fmt.Sprintf("(&%s%s)", i.groupFilter, filter), attrs, nil, ) @@ -397,7 +422,7 @@ func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ld 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'", nameOrID) + errmsg = fmt.Sprintf("too many results searching for group '%s'", filter) i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg) } } From c0d486f3a5f2c25092f3fa427d61dfa6c214e3b6 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 20 Jan 2022 17:29:08 +0100 Subject: [PATCH 4/8] Implement adding a member to a Group This implements POST request to the graph/v1.0/groups/{groupid}/members/$ref endpoint. Allowing to add members to a group. --- go.mod | 3 +- go.sum | 25 +++++++++++- graph/pkg/identity/backend.go | 2 + graph/pkg/identity/cs3.go | 5 +++ graph/pkg/identity/ldap.go | 49 +++++++++++++++++++++++ graph/pkg/service/v0/groups.go | 63 +++++++++++++++++++++++++++++- graph/pkg/service/v0/instrument.go | 5 +++ graph/pkg/service/v0/logging.go | 5 +++ graph/pkg/service/v0/service.go | 6 ++- graph/pkg/service/v0/tracing.go | 5 +++ 10 files changed, 163 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 6797890fb..0d089f184 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 github.com/iancoleman/strcase v0.2.0 github.com/justinas/alice v1.2.0 + github.com/libregraph/idm v0.3.1-0.20220120080913-d0003ade935e github.com/libregraph/lico v0.53.1 github.com/mennanov/fieldmask-utils v0.5.0 github.com/mitchellh/mapstructure v1.4.3 @@ -190,7 +191,7 @@ require ( github.com/nats-io/nkeys v0.3.0 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/nxadm/tail v1.4.8 // indirect - github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect + github.com/orcaman/concurrent-map v1.0.0 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/xattr v0.4.4 // indirect diff --git a/go.sum b/go.sum index 3dc4f48de..63a9a8a65 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,7 @@ github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXl github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/Songmu/prompter v0.5.0/go.mod h1:S4Eg25l60kPlnfB2ttFVpvBKYw7RKJexzB3gzpAansY= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= @@ -132,7 +133,9 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387/go.mod h1:GuR5j/NW7AU7tDAQUDGCtpiPxWIOy/c3kiRDnlwiCHc= github.com/aliyun/alibaba-cloud-sdk-go v1.61.976/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= +github.com/amoghe/go-crypt v0.0.0-20191109212615-b2ff80594b7f/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -352,6 +355,7 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg= github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -412,6 +416,8 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-acme/lego/v4 v4.4.0/go.mod h1:l3+tFUFZb590dWcqhWZegynUthtaHJbG2fevUpoOOE0= +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.3 h1:u7utq56RUFiynqUzgVMFDymapcOtQ/MZkh3H4QYkxag= github.com/go-asn1-ber/asn1-ber v1.5.3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -442,8 +448,10 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU= github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= +github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3/go.mod h1:ZXFhGda43Z2TVbfGZefXyMJzsDHhCh0go3bZUcwTx7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -888,6 +896,8 @@ github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvf github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/libregraph/idm v0.3.1-0.20220120080913-d0003ade935e h1:iO7Gf/cMCn2sn28CoWsHUQ/O7VfH0DAJrhHyTE/xQk4= +github.com/libregraph/idm v0.3.1-0.20220120080913-d0003ade935e/go.mod h1:gdDXfHsvrrmoNwqcXroOGIi2+Lrx5iAipbpVwR7goeY= github.com/libregraph/lico v0.53.1 h1:c5JYFg0mrIgk6Ktj77rNJ2K8X01hs28JJ9Vw1N1iMAk= github.com/libregraph/lico v0.53.1/go.mod h1:EXf6Y9s9TygW6unSXosBpVXWUURoN2sPX0053o4zVfQ= github.com/linode/linodego v0.25.3/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE= @@ -1086,8 +1096,9 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= -github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ= github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= +github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= github.com/owncloud/libre-graph-api-go v0.9.0 h1:hMgjztJRCe1kRYqZ+w0paMFFzeUUpBX2o1TPNMNTmdU= github.com/owncloud/libre-graph-api-go v0.9.0/go.mod h1:579sFrPP7aP24LZXGPopLfvE+hAka/2DYHk0+Ij+w+U= @@ -1134,6 +1145,7 @@ github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -1253,6 +1265,7 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spacewander/go-suffix-tree v0.0.0-20191010040751-0865e368c784/go.mod h1:ff/5myEGgtsAwf26goQCO905GrEm5ugEZSd6OWTsrhM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= @@ -1299,6 +1312,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= github.com/thejerf/suture/v4 v4.0.1 h1:CLnC1wxLAiHA5zTbbvhSWMupVuGe5ZJ7YddWE3lvb4M= @@ -1307,6 +1321,7 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip/v6 v6.2.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= +github.com/trustelem/zxcvbn v1.0.1/go.mod h1:zonUyKeh7sw6psPf/e3DtRqkRyZvAbOfjNz/aO7YQ5s= github.com/tus/tusd v1.1.0/go.mod h1:3DWPOdeCnjBwKtv98y5dSws3itPqfce5TVa0s59LRiA= github.com/tus/tusd v1.8.0 h1:QODQ5uMhL2tFX3Ouk7rUHHqPqeDBvi2+gYIoyUO0n8Q= github.com/tus/tusd v1.8.0/go.mod h1:stZzKpol4qz7lX2HXy/1H526dn5mRnkIICTW2lrh9NM= @@ -1475,6 +1490,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e h1:1SzTfNOXwIS2oWiMF+6qu0OUDKb0dauo6MoDUQyu+yU= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1591,6 +1608,7 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1711,6 +1729,7 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1744,8 +1763,10 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210916214954-140adaaadfaf h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI= +golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index 5d9e57fa2..f9cda702f 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -26,6 +26,8 @@ type Backend interface { 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) + // AddMemberToGroup adds a new member (reference by ID) to supplied group in the identity backend. + AddMemberToGroup(ctx context.Context, groupID string, memberID string) error } func CreateUserModelFromCS3(u *cs3.User) *libregraph.User { diff --git a/graph/pkg/identity/cs3.go b/graph/pkg/identity/cs3.go index bdfdb163c..682ec5ae0 100644 --- a/graph/pkg/identity/cs3.go +++ b/graph/pkg/identity/cs3.go @@ -179,6 +179,11 @@ func (i *CS3) GetGroupMembers(ctx context.Context, groupID string) ([]*libregrap return nil, errorcode.New(errorcode.NotSupported, "not implemented") } +// AddMemberToGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) AddMemberToGroup(ctx context.Context, groupID string, memberID string) error { + return 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 edd35dd51..e902a49c0 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -8,6 +8,7 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/gofrs/uuid" + ldapdn "github.com/libregraph/idm/pkg/ldapdn" libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/graph/pkg/config" @@ -551,6 +552,53 @@ func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libreg return i.createGroupModelFromLDAP(e), nil } +// AddMemberToGroup implements the Backend Interface for the LDAP backend. +// Currently it is limited to adding Users as Group members. Adding other groups +// as members is not yet implemented +func (i *LDAP) AddMemberToGroup(ctx context.Context, groupID string, memberID string) error { + ge, err := i.getLDAPGroupByID(groupID, true) + if err != nil { + return err + } + me, err := i.getLDAPUserByID(memberID) + if err != nil { + return err + } + i.logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("Add Member") + + mr := ldap.ModifyRequest{DN: ge.DN} + // Handle empty groups (using the empty member attribute) + current := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member) + if len(current) == 1 && current[0] == "" { + mr.Delete(i.groupAttributeMap.member, []string{""}) + } + nUserDN, err := ldapdn.ParseNormalize(me.DN) + for _, member := range current { + if member == "" { + continue + } + if nMember, err := ldapdn.ParseNormalize(member); err != nil { + // We couldn't parse the member value as a DN. Let's keep it + // as it is but log a warning + i.logger.Warn().Str("memberDN", member).Err(err).Msg("Couldn't parse DN") + continue + } else { + if nMember == nUserDN { + i.logger.Info().Str("memberDN", member).Msg("User already present. Nothing to do") + return nil + } + } + + } + + mr.Add(i.groupAttributeMap.member, []string{me.DN}) + + if err := i.conn.Modify(&mr); err != nil { + return err + } + return nil +} + func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { if e == nil { return nil @@ -569,6 +617,7 @@ func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group { Id: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)), } } + func pointerOrNil(val string) *string { if val == "" { return nil diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index cefb6622a..4cbdfd1e9 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -5,13 +5,13 @@ import ( "errors" "net/http" "net/url" + "strings" libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" "github.com/go-chi/chi/v5" "github.com/go-chi/render" - //msgraph "github.com/owncloud/open-graph-api-go" // FIXME add groups to open graph, needs OnPremisesSamAccountName and OnPremisesDomainName ) // GetGroups implements the Service interface. @@ -115,3 +115,64 @@ func (g Graph) GetGroupMembers(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) render.JSON(w, r, members) } + +// PostGroupMember implements the Service interface. +func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) { + g.logger.Info().Msg("Calling PostGroupMember") + + 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 + } + memberRef := libregraph.NewMemberReference() + err = json.NewDecoder(r.Body).Decode(memberRef) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + memberRefURL, ok := memberRef.GetOdataIdOk() + if !ok { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "@odata.id refernce is missing") + return + } + memberURL, err := url.ParseRequestURI(*memberRefURL) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url") + return + } + segments := strings.Split(memberURL.Path, "/") + if len(segments) < 2 { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url path") + return + } + id := segments[len(segments)-1] + memberType := segments[len(segments)-2] + // The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact" + // we restrict this to users for now. Might add Groups as members later + if memberType != "users" { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Only user are allowed as group members") + return + } + + g.logger.Debug().Str("memberType", memberType).Str("id", id).Msg("Add Member") + err = g.identityBackend.AddMemberToGroup(r.Context(), groupID, id) + + 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.StatusNoContent) + render.NoContent(w, r) +} diff --git a/graph/pkg/service/v0/instrument.go b/graph/pkg/service/v0/instrument.go index bb7fae1ba..d4742d69a 100644 --- a/graph/pkg/service/v0/instrument.go +++ b/graph/pkg/service/v0/instrument.go @@ -74,6 +74,11 @@ func (i instrument) GetGroupMembers(w http.ResponseWriter, r *http.Request) { i.next.GetGroupMembers(w, r) } +// PostGroupMember implements the Service interface. +func (i instrument) PostGroupMember(w http.ResponseWriter, r *http.Request) { + i.next.PostGroupMember(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 ce39fcff2..436b7d11a 100644 --- a/graph/pkg/service/v0/logging.go +++ b/graph/pkg/service/v0/logging.go @@ -74,6 +74,11 @@ func (l logging) GetGroupMembers(w http.ResponseWriter, r *http.Request) { l.next.GetGroupMembers(w, r) } +// PostGroupMember implements the Service interface. +func (l logging) PostGroupMember(w http.ResponseWriter, r *http.Request) { + l.next.PostGroupMember(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 b417d74ff..e2d4374d5 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -35,6 +35,7 @@ type Service interface { GetGroup(http.ResponseWriter, *http.Request) PostGroup(http.ResponseWriter, *http.Request) GetGroupMembers(http.ResponseWriter, *http.Request) + PostGroupMember(http.ResponseWriter, *http.Request) GetDrives(w http.ResponseWriter, r *http.Request) } @@ -117,7 +118,10 @@ 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.Route("/members", func(r chi.Router) { + r.Get("/", svc.GetGroupMembers) + r.Post("/$ref", svc.PostGroupMember) + }) }) }) r.Group(func(r chi.Router) { diff --git a/graph/pkg/service/v0/tracing.go b/graph/pkg/service/v0/tracing.go index 6784d32f1..4507459f7 100644 --- a/graph/pkg/service/v0/tracing.go +++ b/graph/pkg/service/v0/tracing.go @@ -70,6 +70,11 @@ func (t tracing) GetGroupMembers(w http.ResponseWriter, r *http.Request) { t.next.GetGroupMembers(w, r) } +// PostGroupMember implements the Service interface. +func (t tracing) PostGroupMember(w http.ResponseWriter, r *http.Request) { + t.next.PostGroupMember(w, r) +} + // GetDrives implements the Service interface. func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) { t.next.GetDrives(w, r) From eb3ace862973fc6605572fb916cbd30f887df549 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 25 Jan 2022 13:06:41 +0100 Subject: [PATCH 5/8] Implement deleting a Group This implements DELETE request on the graph/v1.0/groups/{groupid} endpoint. Allowing to remove an entire group. --- graph/pkg/identity/backend.go | 4 +++- graph/pkg/identity/cs3.go | 5 +++++ graph/pkg/identity/ldap.go | 16 ++++++++++++++++ graph/pkg/service/v0/groups.go | 29 +++++++++++++++++++++++++++++ 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, 70 insertions(+), 1 deletion(-) diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index f9cda702f..24e84f326 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -21,8 +21,10 @@ type Backend interface { GetUser(ctx context.Context, nameOrID string) (*libregraph.User, error) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) - // Create Group creates the supplied group in the identity backend. + // CreateGroup creates the supplied group in the identity backend. CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) + // DeleteGroup deletes a given group, identified by id + DeleteGroup(ctx context.Context, id string) 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) diff --git a/graph/pkg/identity/cs3.go b/graph/pkg/identity/cs3.go index 682ec5ae0..fea10be53 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 } +// DeleteGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) DeleteGroup(ctx context.Context, id string) error { + return errorcode.New(errorcode.NotSupported, "not implemented") +} + // 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") diff --git a/graph/pkg/identity/ldap.go b/graph/pkg/identity/ldap.go index e902a49c0..1f032eeba 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -552,6 +552,22 @@ func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libreg return i.createGroupModelFromLDAP(e), nil } +// DeleteGroup implements the Backend Interface. +func (i *LDAP) DeleteGroup(ctx context.Context, id string) error { + if !i.writeEnabled { + return errorcode.New(errorcode.NotAllowed, "server is configured read-only") + } + e, err := i.getLDAPGroupByID(id, false) + if err != nil { + return err + } + dr := ldap.DelRequest{DN: e.DN} + if err = i.conn.Del(&dr); err != nil { + return err + } + return nil +} + // AddMemberToGroup implements the Backend Interface for the LDAP backend. // Currently it is limited to adding Users as Group members. Adding other groups // as members is not yet implemented diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index 4cbdfd1e9..379a1b7f0 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -88,6 +88,35 @@ func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, group) } +// DeleteGroup implements the Service interface. +func (g Graph) DeleteGroup(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 + } + + err = g.identityBackend.DeleteGroup(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.StatusNoContent) + render.NoContent(w, r) +} + func (g Graph) GetGroupMembers(w http.ResponseWriter, r *http.Request) { groupID := chi.URLParam(r, "groupID") groupID, err := url.PathUnescape(groupID) diff --git a/graph/pkg/service/v0/instrument.go b/graph/pkg/service/v0/instrument.go index d4742d69a..c3b36388e 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) } +// DeleteGroup implements the Service interface. +func (i instrument) DeleteGroup(w http.ResponseWriter, r *http.Request) { + i.next.DeleteGroup(w, r) +} + // GetGroupMembers implements the Service interface. func (i instrument) GetGroupMembers(w http.ResponseWriter, r *http.Request) { i.next.GetGroupMembers(w, r) diff --git a/graph/pkg/service/v0/logging.go b/graph/pkg/service/v0/logging.go index 436b7d11a..43116b9f6 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) } +// DeleteGroup implements the Service interface. +func (l logging) DeleteGroup(w http.ResponseWriter, r *http.Request) { + l.next.DeleteGroup(w, r) +} + // GetGroupMembers implements the Service interface. func (l logging) GetGroupMembers(w http.ResponseWriter, r *http.Request) { l.next.GetGroupMembers(w, r) diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index e2d4374d5..44b82f90f 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) + DeleteGroup(http.ResponseWriter, *http.Request) GetGroupMembers(http.ResponseWriter, *http.Request) PostGroupMember(http.ResponseWriter, *http.Request) @@ -118,6 +119,7 @@ func NewService(opts ...Option) Service { r.Post("/", svc.PostGroup) r.Route("/{groupID}", func(r chi.Router) { r.Get("/", svc.GetGroup) + r.Delete("/", svc.DeleteGroup) r.Route("/members", func(r chi.Router) { r.Get("/", svc.GetGroupMembers) r.Post("/$ref", svc.PostGroupMember) diff --git a/graph/pkg/service/v0/tracing.go b/graph/pkg/service/v0/tracing.go index 4507459f7..4ab90e503 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) } +// DeleteGroup implements the Service interface. +func (t tracing) DeleteGroup(w http.ResponseWriter, r *http.Request) { + t.next.DeleteGroup(w, r) +} + // GetGroupMembers implements the Service interface. func (t tracing) GetGroupMembers(w http.ResponseWriter, r *http.Request) { t.next.GetGroupMembers(w, r) From 166bda9ac6896785670ad80b7ed162a1d58189d1 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 25 Jan 2022 15:57:03 +0100 Subject: [PATCH 6/8] Implement removing a group member This implements DELETE request on the graph/v1.0/groups/{groupid}/members/{memberID}/$ref endpoint. Allowing to remove a single member from a group. --- graph/pkg/identity/backend.go | 2 ++ graph/pkg/identity/cs3.go | 5 +++ graph/pkg/identity/ldap.go | 50 ++++++++++++++++++++++++++++++ graph/pkg/service/v0/groups.go | 43 +++++++++++++++++++++++++ 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, 117 insertions(+) diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index 24e84f326..6e8004686 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -30,6 +30,8 @@ type Backend interface { GetGroupMembers(ctx context.Context, id string) ([]*libregraph.User, error) // AddMemberToGroup adds a new member (reference by ID) to supplied group in the identity backend. AddMemberToGroup(ctx context.Context, groupID string, memberID string) error + // RemoveMemberFromGroup removes a single member (by ID) from a group + RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error } func CreateUserModelFromCS3(u *cs3.User) *libregraph.User { diff --git a/graph/pkg/identity/cs3.go b/graph/pkg/identity/cs3.go index fea10be53..f05a5cb1e 100644 --- a/graph/pkg/identity/cs3.go +++ b/graph/pkg/identity/cs3.go @@ -189,6 +189,11 @@ func (i *CS3) AddMemberToGroup(ctx context.Context, groupID string, memberID str return errorcode.New(errorcode.NotSupported, "not implemented") } +// RemoveMemberFromGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error { + return 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 1f032eeba..fb0359448 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -615,6 +615,56 @@ func (i *LDAP) AddMemberToGroup(ctx context.Context, groupID string, memberID st return nil } +// RemoveMemberFromGroup implements the Backend Interface. +func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error { + ge, err := i.getLDAPGroupByID(groupID, true) + if err != nil { + i.logger.Warn().Str("backend", "ldap").Str("groupID", groupID).Msg("Error looking up group") + return err + } + me, err := i.getLDAPUserByID(memberID) + if err != nil { + i.logger.Warn().Str("backend", "ldap").Str("memberID", memberID).Msg("Error looking up group member") + return err + } + i.logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("remove member") + + nOldMemberDN, err := ldapdn.ParseNormalize(me.DN) + members := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member) + found := false + for _, member := range members { + if member == "" { + continue + } + if nMember, err := ldapdn.ParseNormalize(member); err != nil { + // We couldn't parse the member value as a DN. Let's keep it + // as it is but log a warning + i.logger.Warn().Str("memberDN", member).Err(err).Msg("Couldn't parse DN") + continue + } else { + if nMember == nOldMemberDN { + found = true + } + } + } + if !found { + i.logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN). + Msg("The target is not a member of the group") + return nil + } + + mr := ldap.ModifyRequest{DN: ge.DN} + if len(members) == 1 { + mr.Add(i.groupAttributeMap.member, []string{""}) + } + mr.Delete(i.groupAttributeMap.member, []string{me.DN}) + + if err := i.conn.Modify(&mr); err != nil { + return err + } + return nil +} + func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { if e == nil { return nil diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index 379a1b7f0..ea1ae43bd 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -205,3 +205,46 @@ func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusNoContent) render.NoContent(w, r) } + +// DeleteGroupMember implements the Service interface. +func (g Graph) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { + g.logger.Info().Msg("Calling DeleteGroupMember") + + 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 + } + + memberID := chi.URLParam(r, "memberID") + memberID, err = url.PathUnescape(memberID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed") + return + } + + if memberID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") + return + } + g.logger.Debug().Str("groupID", groupID).Str("memberID", memberID).Msg("DeleteGroupMember") + err = g.identityBackend.RemoveMemberFromGroup(r.Context(), groupID, memberID) + + 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.StatusNoContent) + render.NoContent(w, r) +} diff --git a/graph/pkg/service/v0/instrument.go b/graph/pkg/service/v0/instrument.go index c3b36388e..b3088ccdd 100644 --- a/graph/pkg/service/v0/instrument.go +++ b/graph/pkg/service/v0/instrument.go @@ -84,6 +84,11 @@ func (i instrument) PostGroupMember(w http.ResponseWriter, r *http.Request) { i.next.PostGroupMember(w, r) } +// DeleteGroupMember implements the Service interface. +func (i instrument) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { + i.next.DeleteGroupMember(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 43116b9f6..ac5b56a62 100644 --- a/graph/pkg/service/v0/logging.go +++ b/graph/pkg/service/v0/logging.go @@ -84,6 +84,11 @@ func (l logging) PostGroupMember(w http.ResponseWriter, r *http.Request) { l.next.PostGroupMember(w, r) } +// DeleteGroupMember implements the Service interface. +func (l logging) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { + l.next.DeleteGroupMember(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 44b82f90f..1cbbba55c 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -37,6 +37,7 @@ type Service interface { DeleteGroup(http.ResponseWriter, *http.Request) GetGroupMembers(http.ResponseWriter, *http.Request) PostGroupMember(http.ResponseWriter, *http.Request) + DeleteGroupMember(http.ResponseWriter, *http.Request) GetDrives(w http.ResponseWriter, r *http.Request) } @@ -123,6 +124,7 @@ func NewService(opts ...Option) Service { r.Route("/members", func(r chi.Router) { r.Get("/", svc.GetGroupMembers) r.Post("/$ref", svc.PostGroupMember) + r.Delete("/{memberID}/$ref", svc.DeleteGroupMember) }) }) }) diff --git a/graph/pkg/service/v0/tracing.go b/graph/pkg/service/v0/tracing.go index 4ab90e503..10e9e6c31 100644 --- a/graph/pkg/service/v0/tracing.go +++ b/graph/pkg/service/v0/tracing.go @@ -80,6 +80,11 @@ func (t tracing) PostGroupMember(w http.ResponseWriter, r *http.Request) { t.next.PostGroupMember(w, r) } +// DeleteGroupMember implements the Service interface. +func (t tracing) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { + t.next.DeleteGroupMember(w, r) +} + // GetDrives implements the Service interface. func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) { t.next.GetDrives(w, r) From d9aa33525ab2b641dc98cffb5f3d4fe0c8a9ccaf Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 26 Jan 2022 15:51:49 +0100 Subject: [PATCH 7/8] Add support for adding multiple members to a group at once Adding multiple members at once is done via PATCH on `groups/{group-oid}` with a body containing a list of refernces to the members. --- graph/pkg/identity/backend.go | 7 +-- graph/pkg/identity/cs3.go | 4 +- graph/pkg/identity/ldap.go | 69 ++++++++++++++--------- graph/pkg/service/v0/groups.go | 88 +++++++++++++++++++++++++++--- 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, 143 insertions(+), 42 deletions(-) diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index 6e8004686..49e71709b 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -11,13 +11,10 @@ import ( type Backend interface { // CreateUser creates a given user in the identity backend. CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error) - // DeleteUser deletes a given user, identified by username or id, from the backend DeleteUser(ctx context.Context, nameOrID string) error - // UpdateUser applies changes to given user, identified by username or id UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error) - GetUser(ctx context.Context, nameOrID string) (*libregraph.User, error) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) @@ -28,8 +25,8 @@ type Backend interface { 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) - // AddMemberToGroup adds a new member (reference by ID) to supplied group in the identity backend. - AddMemberToGroup(ctx context.Context, groupID string, memberID string) error + // AddMembersToGroup adds new members (reference by a slice of IDs) to supplied group in the identity backend. + AddMembersToGroup(ctx context.Context, groupID string, memberID []string) error // RemoveMemberFromGroup removes a single member (by ID) from a group RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error } diff --git a/graph/pkg/identity/cs3.go b/graph/pkg/identity/cs3.go index f05a5cb1e..e4b7c2054 100644 --- a/graph/pkg/identity/cs3.go +++ b/graph/pkg/identity/cs3.go @@ -184,8 +184,8 @@ func (i *CS3) GetGroupMembers(ctx context.Context, groupID string) ([]*libregrap return nil, errorcode.New(errorcode.NotSupported, "not implemented") } -// AddMemberToGroup implements the Backend Interface. It's currently not supported for the CS3 backend -func (i *CS3) AddMemberToGroup(ctx context.Context, groupID string, memberID string) error { +// AddMembersToGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) AddMembersToGroup(ctx context.Context, groupID string, memberID []string) error { return errorcode.New(errorcode.NotSupported, "not implemented") } diff --git a/graph/pkg/identity/ldap.go b/graph/pkg/identity/ldap.go index fb0359448..71f402eae 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -568,19 +568,14 @@ func (i *LDAP) DeleteGroup(ctx context.Context, id string) error { return nil } -// AddMemberToGroup implements the Backend Interface for the LDAP backend. +// AddMembersToGroup implements the Backend Interface for the LDAP backend. // Currently it is limited to adding Users as Group members. Adding other groups // as members is not yet implemented -func (i *LDAP) AddMemberToGroup(ctx context.Context, groupID string, memberID string) error { +func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs []string) error { ge, err := i.getLDAPGroupByID(groupID, true) if err != nil { return err } - me, err := i.getLDAPUserByID(memberID) - if err != nil { - return err - } - i.logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("Add Member") mr := ldap.ModifyRequest{DN: ge.DN} // Handle empty groups (using the empty member attribute) @@ -588,29 +583,47 @@ func (i *LDAP) AddMemberToGroup(ctx context.Context, groupID string, memberID st if len(current) == 1 && current[0] == "" { mr.Delete(i.groupAttributeMap.member, []string{""}) } - nUserDN, err := ldapdn.ParseNormalize(me.DN) - for _, member := range current { - if member == "" { - continue - } - if nMember, err := ldapdn.ParseNormalize(member); err != nil { - // We couldn't parse the member value as a DN. Let's keep it - // as it is but log a warning - i.logger.Warn().Str("memberDN", member).Err(err).Msg("Couldn't parse DN") - continue - } else { - if nMember == nUserDN { - i.logger.Info().Str("memberDN", member).Msg("User already present. Nothing to do") - return nil - } - } + // Create a Set of current members for faster lookups + currentSet := make(map[string]struct{}, len(current)) + for _, currentMember := range current { + // We can ignore any empty member value here + if currentMember == "" { + continue + } + nCurrentMember, err := ldapdn.ParseNormalize(currentMember) + if err != nil { + // We couldn't parse the member value as a DN. Let's skip it, but log a warning + i.logger.Warn().Str("memberDN", currentMember).Err(err).Msg("Couldn't parse DN") + continue + } + currentSet[nCurrentMember] = struct{}{} } - mr.Add(i.groupAttributeMap.member, []string{me.DN}) + var newMemberDNs []string + for _, memberID := range memberIDs { + me, err := i.getLDAPUserByID(memberID) + if err != nil { + return err + } + nDN, err := ldapdn.ParseNormalize(me.DN) + if err != nil { + i.logger.Error().Str("new member", me.DN).Err(err).Msg("Couldn't parse DN") + return err + } + if _, present := currentSet[nDN]; !present { + newMemberDNs = append(newMemberDNs, me.DN) + } else { + i.logger.Debug().Str("memberDN", me.DN).Msg("Member already present in group. Skipping") + } + } - if err := i.conn.Modify(&mr); err != nil { - return err + if len(newMemberDNs) > 0 { + mr.Add(i.groupAttributeMap.member, newMemberDNs) + + if err := i.conn.Modify(&mr); err != nil { + return err + } } return nil } @@ -630,6 +643,10 @@ func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, member i.logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("remove member") nOldMemberDN, err := ldapdn.ParseNormalize(me.DN) + if err != nil { + i.logger.Error().Str("old member", me.DN).Err(err).Msg("Couldn't parse DN") + return err + } members := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member) found := false for _, member := range members { diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index ea1ae43bd..f261cfc10 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -3,6 +3,7 @@ package svc import ( "encoding/json" "errors" + "fmt" "net/http" "net/url" "strings" @@ -14,6 +15,8 @@ import ( "github.com/go-chi/render" ) +const memberRefsLimit = 20 + // GetGroups implements the Service interface. func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { groups, err := g.identityBackend.GetGroups(r.Context(), r.URL.Query()) @@ -61,6 +64,66 @@ func (g Graph) PostGroup(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, grp) } +// PatchGroup implements the Service interface. +func (g Graph) PatchGroup(w http.ResponseWriter, r *http.Request) { + g.logger.Debug().Msg("Calling PatchGroup") + 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 + } + changes := libregraph.NewGroup() + err = json.NewDecoder(r.Body).Decode(changes) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + + if memberRefs, ok := changes.GetMembersodataBindOk(); ok { + // The spec defines a limit of 20 members maxium per Request + if len(memberRefs) > memberRefsLimit { + errorcode.NotAllowed.Render(w, r, http.StatusInternalServerError, + fmt.Sprintf("Request is limited to %d members", memberRefsLimit)) + return + } + memberIDs := make([]string, 0, len(memberRefs)) + for _, memberRef := range memberRefs { + memberType, id, err := g.parseMemberRef(memberRef) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing member@odata.bind values") + return + } + g.logger.Debug().Str("memberType", memberType).Str("memberid", id).Msg("Add Member") + // The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact" + // we restrict this to users for now. Might add Groups as members later + if memberType != "users" { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Only user are allowed as group members") + return + } + memberIDs = append(memberIDs, id) + } + err = g.identityBackend.AddMembersToGroup(r.Context(), groupID, memberIDs) + } + + 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.StatusNoContent) + render.NoContent(w, r) +} + // GetGroup implements the Service interface. func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { groupID := chi.URLParam(r, "groupID") @@ -171,18 +234,11 @@ func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) { errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "@odata.id refernce is missing") return } - memberURL, err := url.ParseRequestURI(*memberRefURL) + memberType, id, err := g.parseMemberRef(*memberRefURL) if err != nil { errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url") return } - segments := strings.Split(memberURL.Path, "/") - if len(segments) < 2 { - errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url path") - return - } - id := segments[len(segments)-1] - memberType := segments[len(segments)-2] // The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact" // we restrict this to users for now. Might add Groups as members later if memberType != "users" { @@ -191,7 +247,7 @@ func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) { } g.logger.Debug().Str("memberType", memberType).Str("id", id).Msg("Add Member") - err = g.identityBackend.AddMemberToGroup(r.Context(), groupID, id) + err = g.identityBackend.AddMembersToGroup(r.Context(), groupID, []string{id}) if err != nil { var errcode errorcode.Error @@ -248,3 +304,17 @@ func (g Graph) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusNoContent) render.NoContent(w, r) } + +func (g Graph) parseMemberRef(ref string) (string, string, error) { + memberURL, err := url.ParseRequestURI(ref) + if err != nil { + return "", "", err + } + segments := strings.Split(memberURL.Path, "/") + if len(segments) < 2 { + return "", "", errors.New("invalid member reference") + } + id := segments[len(segments)-1] + memberType := segments[len(segments)-2] + return memberType, id, nil +} diff --git a/graph/pkg/service/v0/instrument.go b/graph/pkg/service/v0/instrument.go index b3088ccdd..96b0183e5 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) } +// PatchGroup implements the Service interface. +func (i instrument) PatchGroup(w http.ResponseWriter, r *http.Request) { + i.next.PatchGroup(w, r) +} + // DeleteGroup implements the Service interface. func (i instrument) DeleteGroup(w http.ResponseWriter, r *http.Request) { i.next.DeleteGroup(w, r) diff --git a/graph/pkg/service/v0/logging.go b/graph/pkg/service/v0/logging.go index ac5b56a62..3ee85ff25 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) } +// PatchGroup implements the Service interface. +func (l logging) PatchGroup(w http.ResponseWriter, r *http.Request) { + l.next.PatchGroup(w, r) +} + // DeleteGroup implements the Service interface. func (l logging) DeleteGroup(w http.ResponseWriter, r *http.Request) { l.next.DeleteGroup(w, r) diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index 1cbbba55c..79859e070 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) + PatchGroup(http.ResponseWriter, *http.Request) DeleteGroup(http.ResponseWriter, *http.Request) GetGroupMembers(http.ResponseWriter, *http.Request) PostGroupMember(http.ResponseWriter, *http.Request) @@ -121,6 +122,7 @@ func NewService(opts ...Option) Service { r.Route("/{groupID}", func(r chi.Router) { r.Get("/", svc.GetGroup) r.Delete("/", svc.DeleteGroup) + r.Patch("/", svc.PatchGroup) r.Route("/members", func(r chi.Router) { r.Get("/", svc.GetGroupMembers) r.Post("/$ref", svc.PostGroupMember) diff --git a/graph/pkg/service/v0/tracing.go b/graph/pkg/service/v0/tracing.go index 10e9e6c31..0d2581ba9 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) } +// PatchGroup implements the Service interface. +func (t tracing) PatchGroup(w http.ResponseWriter, r *http.Request) { + t.next.PatchGroup(w, r) +} + // DeleteGroup implements the Service interface. func (t tracing) DeleteGroup(w http.ResponseWriter, r *http.Request) { t.next.DeleteGroup(w, r) From 38f5b86fdaad69e8f0526999190639127f465969 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 27 Jan 2022 16:21:46 +0100 Subject: [PATCH 8/8] Add changelog for graph user and group changes --- changelog/unreleased/graph-api-user-groups.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/unreleased/graph-api-user-groups.md diff --git a/changelog/unreleased/graph-api-user-groups.md b/changelog/unreleased/graph-api-user-groups.md new file mode 100644 index 000000000..76a84d9f6 --- /dev/null +++ b/changelog/unreleased/graph-api-user-groups.md @@ -0,0 +1,6 @@ +Enhancement: Introduce User and Group Management capabilities on GraphAPI + +The GraphAPI LDAP Backend is now able to add/modify and delete Users and Groups + +https://github.com/owncloud/ocis/pull/2947 +https://github.com/owncloud/ocis/pull/2996