diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index 62d9ccd131..6d7f652e20 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 36e6108bd3..1b40eb92c2 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 04c8d7e97c..1d5221fce1 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 0598126368..fdbfab3f28 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 05cb9306cd..627a9784d0 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 6a29c3ef82..eec57f9954 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 870ea8786e..f29982a019 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 59e9197c73..1f58944c25 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)