From ad1355d03227049a026108462d6eb43f48113ed2 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 14 Dec 2022 12:16:33 +0100 Subject: [PATCH] Implement LDAP backend for CreateSchool --- services/graph/pkg/identity/ldap_school.go | 76 ++++++++++++++++++- .../graph/pkg/identity/ldap_school_test.go | 70 +++++++++++++++++ services/graph/pkg/identity/ldap_test.go | 4 +- 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 services/graph/pkg/identity/ldap_school_test.go diff --git a/services/graph/pkg/identity/ldap_school.go b/services/graph/pkg/identity/ldap_school.go index db365b92ac..3d256aa51e 100644 --- a/services/graph/pkg/identity/ldap_school.go +++ b/services/graph/pkg/identity/ldap_school.go @@ -2,12 +2,16 @@ package identity import ( "context" + "errors" "fmt" "net/url" "github.com/go-ldap/ldap/v3" + "github.com/gofrs/uuid" libregraph "github.com/owncloud/libre-graph-api-go" + oldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap" "github.com/owncloud/ocis/v2/services/graph/pkg/config" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" ) type educationConfig struct { @@ -57,7 +61,43 @@ func newSchoolAttributeMap() schoolAttributeMap { // CreateSchool creates the supplied school in the identity backend. func (i *LDAP) CreateSchool(ctx context.Context, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) { - return nil, errNotImplemented + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("CreateSchool") + if !i.writeEnabled { + return nil, errReadOnly + } + + dn := fmt.Sprintf("%s=%s,%s", + i.educationConfig.schoolAttributeMap.displayName, + oldap.EscapeDNAttributeValue(school.GetDisplayName()), + i.educationConfig.schoolBaseDN, + ) + ar := ldap.NewAddRequest(dn, nil) + ar.Attribute(i.educationConfig.schoolAttributeMap.displayName, []string{school.GetDisplayName()}) + ar.Attribute(i.educationConfig.schoolAttributeMap.schoolNumber, []string{school.GetSchoolNumber()}) + if !i.useServerUUID { + ar.Attribute(i.educationConfig.schoolAttributeMap.id, []string{uuid.Must(uuid.NewV4()).String()}) + } + objectClasses := []string{"organizationalUnit", i.educationConfig.schoolObjectClass, "top"} + ar.Attribute("objectClass", objectClasses) + + if err := i.conn.Add(ar); err != nil { + var lerr *ldap.Error + logger.Debug().Err(err).Msg("error adding school") + if errors.As(err, &lerr) { + if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists { + err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error()) + } + } + return nil, err + } + + // Read back school from LDAP to get the generated UUID + e, err := i.getSchoolByDN(ar.DN) + if err != nil { + return nil, err + } + return i.createSchoolModelFromLDAP(e), nil } // DeleteSchool deletes a given school, identified by id @@ -89,3 +129,37 @@ func (i *LDAP) AddMembersToSchool(ctx context.Context, schoolID string, memberID func (i *LDAP) RemoveMemberFromSchool(ctx context.Context, schoolID string, memberID string) error { return errNotImplemented } + +func (i *LDAP) getSchoolByDN(dn string) (*ldap.Entry, error) { + attrs := []string{ + i.educationConfig.schoolAttributeMap.displayName, + i.educationConfig.schoolAttributeMap.id, + i.educationConfig.schoolAttributeMap.schoolNumber, + } + filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass) + + if i.educationConfig.schoolFilter != "" { + filter = fmt.Sprintf("(&%s(%s))", filter, i.educationConfig.schoolFilter) + } + return i.getEntryByDN(dn, attrs, filter) +} + +func (i *LDAP) createSchoolModelFromLDAP(e *ldap.Entry) *libregraph.EducationSchool { + if e == nil { + return nil + } + + displayName := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.displayName) + id := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id) + schoolNumber := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.schoolNumber) + + if id != "" && displayName != "" && schoolNumber != "" { + school := libregraph.NewEducationSchool() + school.SetDisplayName(displayName) + school.SetSchoolNumber(schoolNumber) + school.SetId(id) + return school + } + i.logger.Warn().Str("dn", e.DN).Str("id", id).Str("displayName", displayName).Str("schoolNumber", schoolNumber).Msg("Invalid School. Missing required attribute") + return nil +} diff --git a/services/graph/pkg/identity/ldap_school_test.go b/services/graph/pkg/identity/ldap_school_test.go new file mode 100644 index 0000000000..0c3853d69a --- /dev/null +++ b/services/graph/pkg/identity/ldap_school_test.go @@ -0,0 +1,70 @@ +package identity + +import ( + "context" + "testing" + + "github.com/go-ldap/ldap/v3" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/services/graph/mocks" + "github.com/owncloud/ocis/v2/services/graph/pkg/config" + "github.com/test-go/testify/assert" + "github.com/test-go/testify/mock" +) + +var eduConfig = config.LDAP{ + UserBaseDN: "ou=people,dc=test", + UserObjectClass: "inetOrgPerson", + UserSearchScope: "sub", + UserFilter: "", + UserDisplayNameAttribute: "displayname", + UserIDAttribute: "entryUUID", + UserEmailAttribute: "mail", + UserNameAttribute: "uid", + + GroupBaseDN: "ou=groups,dc=test", + GroupObjectClass: "groupOfNames", + GroupSearchScope: "sub", + GroupFilter: "", + GroupNameAttribute: "cn", + GroupIDAttribute: "entryUUID", + + WriteEnabled: true, + EducationResourcesEnabled: true, +} + +var schoolEntry = ldap.NewEntry("ou=Test School", + map[string][]string{ + "ou": {"Test School"}, + "ocEducationSchoolNumber": {"0123"}, + "owncloudUUID": {"abcd-defg"}, + }) + +func TestCreateSchool(t *testing.T) { + lm := &mocks.Client{} + lm.On("Add", mock.Anything). + Return(nil) + + lm.On("Search", mock.Anything). + Return( + &ldap.SearchResult{ + Entries: []*ldap.Entry{schoolEntry}, + }, + nil) + + b, err := getMockedBackend(lm, eduConfig, &logger) + assert.Nil(t, err) + assert.NotEqual(t, "", b.educationConfig.schoolObjectClass) + school := libregraph.NewEducationSchool() + school.SetDisplayName("Test School") + school.SetSchoolNumber("0123") + school.SetId("abcd-defg") + res_school, err := b.CreateSchool(context.Background(), *school) + lm.AssertNumberOfCalls(t, "Add", 1) + lm.AssertNumberOfCalls(t, "Search", 1) + assert.Nil(t, err) + assert.NotNil(t, res_school) + assert.Equal(t, res_school.GetDisplayName(), school.GetDisplayName()) + assert.Equal(t, res_school.GetId(), school.GetId()) + assert.Equal(t, res_school.GetSchoolNumber(), school.GetSchoolNumber()) +} diff --git a/services/graph/pkg/identity/ldap_test.go b/services/graph/pkg/identity/ldap_test.go index 5c206431a3..8f8260aec5 100644 --- a/services/graph/pkg/identity/ldap_test.go +++ b/services/graph/pkg/identity/ldap_test.go @@ -14,7 +14,7 @@ import ( ) func getMockedBackend(l ldap.Client, lc config.LDAP, logger *log.Logger) (*LDAP, error) { - return NewLDAPBackend(l, lconfig, logger) + return NewLDAPBackend(l, lc, logger) } var lconfig = config.LDAP{ @@ -33,6 +33,8 @@ var lconfig = config.LDAP{ GroupFilter: "", GroupNameAttribute: "cn", GroupIDAttribute: "entryUUID", + + WriteEnabled: true, } var userEntry = ldap.NewEntry("uid=user",