diff --git a/core/ldapauth/ldapauth.go b/core/ldapauth/ldapauth.go
index 2be1831ad..33f79f90e 100644
--- a/core/ldapauth/ldapauth.go
+++ b/core/ldapauth/ldapauth.go
@@ -244,6 +244,7 @@ func authLDAP(ctx context.Context, ds model.DataStore, src Source, username, pas
u.IsAdmin = memberAny(du.Groups, src.AdminGroupDNs)
u.AuthSource = "ldap"
u.AuthSourceID = first(src.ID, src.Name)
+ u.ExternalSync = true
if err := repo.Put(u); err != nil {
return nil, err
}
diff --git a/model/user.go b/model/user.go
index 71a4caf9e..95e30a6d8 100644
--- a/model/user.go
+++ b/model/user.go
@@ -27,6 +27,9 @@ type User struct {
NewPassword string `structs:"password,omitempty" json:"password,omitempty"` //nolint:gosec
// If changing the password, this is also required
CurrentPassword string `structs:"current_password,omitempty" json:"currentPassword,omitempty"`
+
+ // ExternalSync allows external auth providers to update read-only identity fields for managed users.
+ ExternalSync bool `structs:"-" json:"-"`
}
func (u User) HasLibraryAccess(libraryID int) bool {
diff --git a/persistence/user_repository.go b/persistence/user_repository.go
index 37f7bf448..abf762f95 100644
--- a/persistence/user_repository.go
+++ b/persistence/user_repository.go
@@ -182,8 +182,24 @@ func (r *userRepository) preserveExternalAuthSource(u *model.User) error {
if existing.AuthSource == "" {
return nil
}
+ if u.ExternalSync {
+ return nil
+ }
+ validation := &rest.ValidationError{Errors: map[string]string{}}
if u.NewPassword != "" {
- return &rest.ValidationError{Errors: map[string]string{"password": "resources.user.validation.externalPasswordReadOnly"}}
+ validation.Errors["password"] = "resources.user.validation.externalFieldReadOnly"
+ }
+ if !strings.EqualFold(u.UserName, existing.UserName) {
+ validation.Errors["userName"] = "resources.user.validation.externalFieldReadOnly"
+ }
+ if u.Name != existing.Name {
+ validation.Errors["name"] = "resources.user.validation.externalFieldReadOnly"
+ }
+ if u.Email != existing.Email {
+ validation.Errors["email"] = "resources.user.validation.externalFieldReadOnly"
+ }
+ if len(validation.Errors) > 0 {
+ return validation
}
if u.AuthSource == "" {
u.AuthSource = existing.AuthSource
diff --git a/persistence/user_repository_test.go b/persistence/user_repository_test.go
index 69ca64abf..e30b92e5c 100644
--- a/persistence/user_repository_test.go
+++ b/persistence/user_repository_test.go
@@ -70,26 +70,45 @@ var _ = Describe("UserRepository", func() {
Expect(err).ToNot(HaveOccurred())
Expect(actual.Password).To(Equal("newpass"))
})
- It("does not update password for LDAP-sourced users", func() {
+ It("does not update managed fields for LDAP-sourced users", func() {
ldapUser := model.User{
ID: "ldap-user",
UserName: "ldap_user",
Name: "LDAP User",
+ Email: "ldap@example.com",
NewPassword: "generated",
AuthSource: "ldap",
AuthSourceID: "ldap01",
}
Expect(repo.Put(&ldapUser)).To(Succeed())
+ ldapUser.UserName = "renamed"
+ ldapUser.Name = "Renamed User"
+ ldapUser.Email = "renamed@example.com"
ldapUser.NewPassword = "newpass"
err := repo.Put(&ldapUser)
var verr *rest.ValidationError
Expect(errors.As(err, &verr)).To(BeTrue())
- Expect(verr.Errors).To(HaveKeyWithValue("password", "resources.user.validation.externalPasswordReadOnly"))
+ Expect(verr.Errors).To(HaveKeyWithValue("userName", "resources.user.validation.externalFieldReadOnly"))
+ Expect(verr.Errors).To(HaveKeyWithValue("name", "resources.user.validation.externalFieldReadOnly"))
+ Expect(verr.Errors).To(HaveKeyWithValue("email", "resources.user.validation.externalFieldReadOnly"))
+ Expect(verr.Errors).To(HaveKeyWithValue("password", "resources.user.validation.externalFieldReadOnly"))
actual, err := repo.FindByUsernameWithPassword("ldap_user")
Expect(err).ToNot(HaveOccurred())
+ Expect(actual.UserName).To(Equal("ldap_user"))
+ Expect(actual.Name).To(Equal("LDAP User"))
+ Expect(actual.Email).To(Equal("ldap@example.com"))
Expect(actual.Password).To(Equal("generated"))
+
+ actual.Name = "Synced User"
+ actual.Email = "synced@example.com"
+ actual.ExternalSync = true
+ Expect(repo.Put(actual)).To(Succeed())
+ actual, err = repo.FindByUsername("ldap_user")
+ Expect(err).ToNot(HaveOccurred())
+ Expect(actual.Name).To(Equal("Synced User"))
+ Expect(actual.Email).To(Equal("synced@example.com"))
})
})
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index 25852c589..36dfa7343 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -168,14 +168,14 @@
},
"validation": {
"librariesRequired": "At least one library must be selected for non-admin users",
- "externalPasswordReadOnly": "Password cannot be changed for users managed by an external authentication source."
+ "externalFieldReadOnly": "This field is managed by an external authentication source."
},
"message": {
"listenBrainzToken": "Enter your ListenBrainz user token.",
"clickHereForToken": "Click here to get your token",
"selectAllLibraries": "Select all libraries",
"adminAutoLibraries": "Admin users automatically have access to all libraries",
- "externalPasswordReadOnly": "Password changes are disabled because this user is managed by an external authentication source."
+ "externalPasswordReadOnly": "Username, display name, email, and password changes are disabled because this user is managed by an external authentication source."
}
},
"player": {
diff --git a/ui/src/user/UserEdit.jsx b/ui/src/user/UserEdit.jsx
index aeabd1f0e..92fb56aae 100644
--- a/ui/src/user/UserEdit.jsx
+++ b/ui/src/user/UserEdit.jsx
@@ -121,18 +121,37 @@ const UserEdit = (props) => {
validate={validateForm}
>
{permissions === 'admin' && (
-
+
+ {({ formData }) => (
+
+ )}
+
)}
-
-
+
+ {({ formData }) => (
+
+ )}
+
+
+ {({ formData }) => (
+
+ )}
+
{({ formData }) =>
formData.authSource ? (