mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-06 04:09:40 -06:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user