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.
This commit is contained in:
Ralf Haferkamp
2022-01-19 15:46:22 +01:00
parent c6d2fdeb04
commit dd7ae9cb47
8 changed files with 170 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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