mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
Protect LDAP-managed identity fields
This commit is contained in:
parent
3c337767f1
commit
0fbc6946a7
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -121,18 +121,37 @@ const UserEdit = (props) => {
|
||||
validate={validateForm}
|
||||
>
|
||||
{permissions === 'admin' && (
|
||||
<TextInput
|
||||
spellCheck={false}
|
||||
source="userName"
|
||||
validate={[required()]}
|
||||
/>
|
||||
<FormDataConsumer>
|
||||
{({ formData }) => (
|
||||
<TextInput
|
||||
spellCheck={false}
|
||||
source="userName"
|
||||
validate={[required()]}
|
||||
disabled={!!formData.authSource}
|
||||
/>
|
||||
)}
|
||||
</FormDataConsumer>
|
||||
)}
|
||||
<TextInput
|
||||
source="name"
|
||||
validate={[required()]}
|
||||
{...getNameHelperText()}
|
||||
/>
|
||||
<TextInput spellCheck={false} source="email" validate={[email()]} />
|
||||
<FormDataConsumer>
|
||||
{({ formData }) => (
|
||||
<TextInput
|
||||
source="name"
|
||||
validate={[required()]}
|
||||
disabled={!!formData.authSource}
|
||||
{...getNameHelperText()}
|
||||
/>
|
||||
)}
|
||||
</FormDataConsumer>
|
||||
<FormDataConsumer>
|
||||
{({ formData }) => (
|
||||
<TextInput
|
||||
spellCheck={false}
|
||||
source="email"
|
||||
validate={[email()]}
|
||||
disabled={!!formData.authSource}
|
||||
/>
|
||||
)}
|
||||
</FormDataConsumer>
|
||||
<FormDataConsumer>
|
||||
{({ formData }) =>
|
||||
formData.authSource ? (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user