mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
Protect LDAP-managed user credentials
This commit is contained in:
parent
dd1fc43762
commit
3c337767f1
@ -242,6 +242,8 @@ func authLDAP(ctx context.Context, ds model.DataStore, src Source, username, pas
|
||||
u.Name = first(du.Name, du.UserName)
|
||||
u.Email = du.Email
|
||||
u.IsAdmin = memberAny(du.Groups, src.AdminGroupDNs)
|
||||
u.AuthSource = "ldap"
|
||||
u.AuthSourceID = first(src.ID, src.Name)
|
||||
if err := repo.Put(u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
11
db/migrations/20260711130000_add_user_auth_source.sql
Normal file
11
db/migrations/20260711130000_add_user_auth_source.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
alter table user add column auth_source varchar(32) default '' not null;
|
||||
alter table user add column auth_source_id varchar(255) default '' not null;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
alter table user drop column auth_source;
|
||||
alter table user drop column auth_source_id;
|
||||
-- +goose StatementEnd
|
||||
@ -10,6 +10,8 @@ type User struct {
|
||||
Name string `structs:"name" json:"name"`
|
||||
Email string `structs:"email" json:"email"`
|
||||
IsAdmin bool `structs:"is_admin" json:"isAdmin"`
|
||||
AuthSource string `structs:"auth_source" json:"authSource,omitempty"`
|
||||
AuthSourceID string `structs:"auth_source_id" json:"authSourceId,omitempty"`
|
||||
LastLoginAt *time.Time `structs:"last_login_at" json:"lastLoginAt"`
|
||||
LastAccessAt *time.Time `structs:"last_access_at" json:"lastAccessAt"`
|
||||
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
|
||||
|
||||
@ -116,6 +116,9 @@ func (r *userRepository) Put(u *model.User) error {
|
||||
u.ID = id.NewRandom()
|
||||
}
|
||||
u.UpdatedAt = time.Now()
|
||||
if err := r.preserveExternalAuthSource(u); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.NewPassword != "" {
|
||||
_ = r.encryptPassword(u)
|
||||
}
|
||||
@ -165,6 +168,30 @@ func (r *userRepository) Put(u *model.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) preserveExternalAuthSource(u *model.User) error {
|
||||
if u.ID == "" {
|
||||
return nil
|
||||
}
|
||||
existing, err := r.Get(u.ID)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.AuthSource == "" {
|
||||
return nil
|
||||
}
|
||||
if u.NewPassword != "" {
|
||||
return &rest.ValidationError{Errors: map[string]string{"password": "resources.user.validation.externalPasswordReadOnly"}}
|
||||
}
|
||||
if u.AuthSource == "" {
|
||||
u.AuthSource = existing.AuthSource
|
||||
u.AuthSourceID = existing.AuthSourceID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) FindFirstAdmin() (*model.User, error) {
|
||||
sel := r.selectUserWithLibraries(model.QueryOptions{Sort: "updated_at", Max: 1}).Where(Eq{"user.is_admin": true})
|
||||
var usr dbUser
|
||||
|
||||
@ -70,6 +70,27 @@ var _ = Describe("UserRepository", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(actual.Password).To(Equal("newpass"))
|
||||
})
|
||||
It("does not update password for LDAP-sourced users", func() {
|
||||
ldapUser := model.User{
|
||||
ID: "ldap-user",
|
||||
UserName: "ldap_user",
|
||||
Name: "LDAP User",
|
||||
NewPassword: "generated",
|
||||
AuthSource: "ldap",
|
||||
AuthSourceID: "ldap01",
|
||||
}
|
||||
Expect(repo.Put(&ldapUser)).To(Succeed())
|
||||
|
||||
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"))
|
||||
|
||||
actual, err := repo.FindByUsernameWithPassword("ldap_user")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(actual.Password).To(Equal("generated"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("validatePasswordChange", func() {
|
||||
|
||||
@ -167,13 +167,15 @@
|
||||
"deleted": "User deleted"
|
||||
},
|
||||
"validation": {
|
||||
"librariesRequired": "At least one library must be selected for non-admin users"
|
||||
"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."
|
||||
},
|
||||
"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"
|
||||
"adminAutoLibraries": "Admin users automatically have access to all libraries",
|
||||
"externalPasswordReadOnly": "Password changes are disabled because this user is managed by an external authentication source."
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
|
||||
@ -48,14 +48,16 @@ const UserToolbar = ({ showDelete, ...props }) => (
|
||||
|
||||
const CurrentPasswordInput = ({ formData, isMyself, ...rest }) => {
|
||||
const { permissions } = usePermissions()
|
||||
return formData.changePassword && (isMyself || permissions !== 'admin') ? (
|
||||
return formData.changePassword &&
|
||||
!formData.authSource &&
|
||||
(isMyself || permissions !== 'admin') ? (
|
||||
<PasswordInput className="ra-input" source="currentPassword" {...rest} />
|
||||
) : null
|
||||
}
|
||||
|
||||
const NewPasswordInput = ({ formData, ...rest }) => {
|
||||
const translate = useTranslate()
|
||||
return formData.changePassword ? (
|
||||
return formData.changePassword && !formData.authSource ? (
|
||||
<PasswordInput
|
||||
source="password"
|
||||
className="ra-input"
|
||||
@ -131,7 +133,21 @@ const UserEdit = (props) => {
|
||||
{...getNameHelperText()}
|
||||
/>
|
||||
<TextInput spellCheck={false} source="email" validate={[email()]} />
|
||||
<BooleanInput source="changePassword" />
|
||||
<FormDataConsumer>
|
||||
{({ formData }) =>
|
||||
formData.authSource ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 16, marginBottom: 16 }}
|
||||
>
|
||||
{translate('resources.user.message.externalPasswordReadOnly')}
|
||||
</Typography>
|
||||
) : (
|
||||
<BooleanInput source="changePassword" />
|
||||
)
|
||||
}
|
||||
</FormDataConsumer>
|
||||
<FormDataConsumer>
|
||||
{(formDataProps) => (
|
||||
<CurrentPasswordInput
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user