diff --git a/config-raw.json b/config-raw.json index b9ea8cd9b..bfe2fd37e 100644 --- a/config-raw.json +++ b/config-raw.json @@ -775,6 +775,11 @@ "default_value": "(&(objectclass=*)(|(objectclass=group)(objectclass=groupOfNames)))", "comment": "The filter to search for group objects in the ldap directory. Only used when `groupsyncenabled` is set to `true`." }, + { + "key": "avatarsyncattribute", + "default_value": "", + "comment": "The LDAP attribute where an image, decoded as raw bytes, can be found. If provided, Vikunja will use the value as avatar." + }, { "key": "attribute", "default_value": "", diff --git a/pkg/config/config.go b/pkg/config/config.go index c5c5bc040..460b1282e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -90,6 +90,7 @@ const ( AuthLdapBindPassword Key = `auth.ldap.bindpassword` AuthLdapGroupSyncEnabled Key = `auth.ldap.groupsyncenabled` AuthLdapGroupSyncFilter Key = `auth.ldap.groupsyncfilter` + AuthLdapAvatarSyncAttribute Key = `auth.ldap.avatarsyncattribute` AuthLdapAttributeUsername Key = `auth.ldap.attribute.username` AuthLdapAttributeEmail Key = `auth.ldap.attribute.email` AuthLdapAttributeDisplayname Key = `auth.ldap.attribute.displayname` diff --git a/pkg/modules/auth/ldap/ldap.go b/pkg/modules/auth/ldap/ldap.go index d5c302892..df7ec454b 100644 --- a/pkg/modules/auth/ldap/ldap.go +++ b/pkg/modules/auth/ldap/ldap.go @@ -17,6 +17,7 @@ package ldap import ( + "bytes" "crypto/tls" "errors" "fmt" @@ -27,6 +28,7 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" + "code.vikunja.io/api/pkg/modules/avatar/upload" "code.vikunja.io/api/pkg/user" "github.com/go-ldap/ldap/v3" @@ -108,7 +110,7 @@ func sanitizedUserQuery(username string) (string, bool) { return fmt.Sprintf(config.AuthLdapUserFilter.GetString(), username), true } -func AuthenticateUserInLDAP(s *xorm.Session, username, password string, syncGroups bool) (u *user.User, err error) { +func AuthenticateUserInLDAP(s *xorm.Session, username, password string, syncGroups bool, avatarSyncAttribute string) (u *user.User, err error) { if password == "" || username == "" { return nil, user.ErrNoUsernamePassword{} } @@ -137,6 +139,7 @@ func AuthenticateUserInLDAP(s *xorm.Session, username, password string, syncGrou config.AuthLdapAttributeUsername.GetString(), config.AuthLdapAttributeEmail.GetString(), config.AuthLdapAttributeDisplayname.GetString(), + "jpegPhoto", }, nil, ) @@ -169,6 +172,15 @@ func AuthenticateUserInLDAP(s *xorm.Session, username, password string, syncGrou return nil, err } + if avatarSyncAttribute != "" { + raw := sr.Entries[0].GetRawAttributeValue(avatarSyncAttribute) + u.AvatarProvider = "ldap" + err = upload.StoreAvatarFile(s, u, bytes.NewReader(raw)) + if err != nil { + return nil, err + } + } + if !syncGroups { return } diff --git a/pkg/modules/auth/ldap/ldap_test.go b/pkg/modules/auth/ldap/ldap_test.go index 633bca6a1..1a329ba1f 100644 --- a/pkg/modules/auth/ldap/ldap_test.go +++ b/pkg/modules/auth/ldap/ldap_test.go @@ -40,7 +40,7 @@ func TestLdapLogin(t *testing.T) { s := db.NewSession() defer s.Close() - user, err := AuthenticateUserInLDAP(s, "professor", "professor", false) + user, err := AuthenticateUserInLDAP(s, "professor", "professor", false, "") require.NoError(t, err) assert.Equal(t, "professor", user.Username) @@ -58,7 +58,7 @@ func TestLdapLogin(t *testing.T) { s := db.NewSession() defer s.Close() - _, err := AuthenticateUserInLDAP(s, "professor", "wrongpassword", false) + _, err := AuthenticateUserInLDAP(s, "professor", "wrongpassword", false, "") require.Error(t, err) assert.True(t, user2.IsErrWrongUsernameOrPassword(err)) @@ -69,7 +69,7 @@ func TestLdapLogin(t *testing.T) { s := db.NewSession() defer s.Close() - _, err := AuthenticateUserInLDAP(s, "gnome", "professor", false) + _, err := AuthenticateUserInLDAP(s, "gnome", "professor", false, "") require.Error(t, err) assert.True(t, user2.IsErrWrongUsernameOrPassword(err)) @@ -80,7 +80,7 @@ func TestLdapLogin(t *testing.T) { s := db.NewSession() defer s.Close() - user, err := AuthenticateUserInLDAP(s, "professor", "professor", true) + user, err := AuthenticateUserInLDAP(s, "professor", "professor", true, "") require.NoError(t, err) assert.Equal(t, "professor", user.Username) @@ -99,4 +99,20 @@ func TestLdapLogin(t *testing.T) { "external_id": "cn=git,ou=people,dc=planetexpress,dc=com", }, false) }) + + t.Run("should sync avatar when enabled", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + user, err := AuthenticateUserInLDAP(s, "professor", "professor", false, "jpegPhoto") + + require.NoError(t, err) + assert.Equal(t, "professor", user.Username) + db.AssertExists(t, "users", map[string]interface{}{ + "username": "professor", + "issuer": "ldap", + "avatar_provider": "ldap", + }, false) + }) } diff --git a/pkg/modules/avatar/ldap/ldap.go b/pkg/modules/avatar/ldap/ldap.go new file mode 100644 index 000000000..56a812749 --- /dev/null +++ b/pkg/modules/avatar/ldap/ldap.go @@ -0,0 +1,30 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package ldap + +import ( + "code.vikunja.io/api/pkg/modules/avatar/upload" + "code.vikunja.io/api/pkg/user" +) + +type Provider struct{} + +func (p *Provider) GetAvatar(user *user.User, size int64) (avatar []byte, mimeType string, err error) { + up := upload.Provider{} + + return up.GetAvatar(user, size) +} diff --git a/pkg/routes/api/v1/login.go b/pkg/routes/api/v1/login.go index b83857307..a6e65b715 100644 --- a/pkg/routes/api/v1/login.go +++ b/pkg/routes/api/v1/login.go @@ -55,7 +55,7 @@ func Login(c echo.Context) (err error) { var user *user2.User if config.AuthLdapEnabled.GetBool() { - user, err = ldap.AuthenticateUserInLDAP(s, u.Username, u.Password, config.AuthLdapGroupSyncEnabled.GetBool()) + user, err = ldap.AuthenticateUserInLDAP(s, u.Username, u.Password, config.AuthLdapGroupSyncEnabled.GetBool(), config.AuthLdapAvatarSyncAttribute.GetString()) if err != nil && !user2.IsErrWrongUsernameOrPassword(err) { _ = s.Rollback() return handler.HandleHTTPError(err) diff --git a/pkg/user/user.go b/pkg/user/user.go index c052b4c1f..38fb88ff0 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -550,7 +550,8 @@ func UpdateUser(s *xorm.Session, user *User, forceOverride bool) (updatedUser *U user.AvatarProvider != "gravatar" && user.AvatarProvider != "initials" && user.AvatarProvider != "upload" && - user.AvatarProvider != "marble" { + user.AvatarProvider != "marble" && + user.AvatarProvider != "ldap" { return updatedUser, &ErrInvalidAvatarProvider{AvatarProvider: user.AvatarProvider} } }