mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-01-22 18:27:42 +00:00
fix: request a password to change sensitive user data (#5629)
This commit is contained in:
parent
943e5340d0
commit
b8151a038a
9 changed files with 103 additions and 26 deletions
|
|
@ -6,22 +6,23 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrEmptyKey = errors.New("empty key")
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
ErrEmptyPassword = errors.New("password is empty")
|
||||
ErrEasyPassword = errors.New("password is too easy")
|
||||
ErrEmptyUsername = errors.New("username is empty")
|
||||
ErrEmptyRequest = errors.New("empty request")
|
||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||
ErrInvalidDataType = errors.New("invalid data type")
|
||||
ErrIsDirectory = errors.New("file is directory")
|
||||
ErrInvalidOption = errors.New("invalid option")
|
||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
||||
ErrEmptyKey = errors.New("empty key")
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
ErrEmptyPassword = errors.New("password is empty")
|
||||
ErrEasyPassword = errors.New("password is too easy")
|
||||
ErrEmptyUsername = errors.New("username is empty")
|
||||
ErrEmptyRequest = errors.New("empty request")
|
||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||
ErrInvalidDataType = errors.New("invalid data type")
|
||||
ErrIsDirectory = errors.New("file is directory")
|
||||
ErrInvalidOption = errors.New("invalid option")
|
||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
||||
ErrCurrentPasswordIncorrect = errors.New("the current password is incorrect")
|
||||
)
|
||||
|
||||
type ErrShortPassword struct {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ export async function get(id: number) {
|
|||
return fetchJSON<IUser>(`/api/users/${id}`, {});
|
||||
}
|
||||
|
||||
export async function create(user: IUser) {
|
||||
export async function create(user: IUser, currentPassword: string) {
|
||||
const res = await fetchURL(`/api/users`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
what: "user",
|
||||
which: [],
|
||||
current_password: currentPassword,
|
||||
data: user,
|
||||
}),
|
||||
});
|
||||
|
|
@ -25,12 +26,17 @@ export async function create(user: IUser) {
|
|||
throw new StatusError(await res.text(), res.status);
|
||||
}
|
||||
|
||||
export async function update(user: Partial<IUser>, which = ["all"]) {
|
||||
export async function update(
|
||||
user: Partial<IUser>,
|
||||
which = ["all"],
|
||||
currentPassword: string | null = null
|
||||
) {
|
||||
await fetchURL(`/api/users/${user.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
what: "user",
|
||||
which: which,
|
||||
...(currentPassword != null ? { current_password: currentPassword } : {}),
|
||||
data: user,
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -258,7 +258,8 @@
|
|||
"userManagement": "User Management",
|
||||
"userUpdated": "User updated!",
|
||||
"username": "Username",
|
||||
"users": "Users"
|
||||
"users": "Users",
|
||||
"currentPassword": "Your Current Password"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Help",
|
||||
|
|
|
|||
1
frontend/src/types/settings.d.ts
vendored
1
frontend/src/types/settings.d.ts
vendored
|
|
@ -5,6 +5,7 @@ interface ISettings {
|
|||
minimumPasswordLength: number;
|
||||
userHomeBasePath: string;
|
||||
defaults: SettingsDefaults;
|
||||
authMethod: string;
|
||||
rules: any[];
|
||||
branding: SettingsBranding;
|
||||
tus: SettingsTus;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,15 @@
|
|||
v-model="passwordConf"
|
||||
name="passwordConf"
|
||||
/>
|
||||
<input
|
||||
v-if="isCurrentPasswordRequired"
|
||||
:class="passwordClass"
|
||||
type="password"
|
||||
:placeholder="t('settings.currentPassword')"
|
||||
v-model="currentPassword"
|
||||
name="current_password"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
|
|
@ -87,7 +96,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { users as api } from "@/api";
|
||||
import { users as api, settings } from "@/api";
|
||||
import AceEditorTheme from "@/components/settings/AceEditorTheme.vue";
|
||||
import Languages from "@/components/settings/Languages.vue";
|
||||
import { computed, inject, onMounted, ref } from "vue";
|
||||
|
|
@ -102,6 +111,8 @@ const $showError = inject<IToastError>("$showError")!;
|
|||
|
||||
const password = ref<string>("");
|
||||
const passwordConf = ref<string>("");
|
||||
const currentPassword = ref<string>("");
|
||||
const isCurrentPasswordRequired = ref<boolean>(false);
|
||||
const hideDotfiles = ref<boolean>(false);
|
||||
const singleClick = ref<boolean>(false);
|
||||
const dateFormat = ref<boolean>(false);
|
||||
|
|
@ -131,6 +142,9 @@ onMounted(async () => {
|
|||
dateFormat.value = authStore.user.dateFormat;
|
||||
aceEditorTheme.value = authStore.user.aceEditorTheme;
|
||||
layoutStore.loading = false;
|
||||
const { authMethod } = await settings.get();
|
||||
isCurrentPasswordRequired.value = authMethod == "json";
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
|
@ -140,6 +154,7 @@ const updatePassword = async (event: Event) => {
|
|||
if (
|
||||
password.value !== passwordConf.value ||
|
||||
password.value === "" ||
|
||||
currentPassword.value === "" ||
|
||||
authStore.user === null
|
||||
) {
|
||||
return;
|
||||
|
|
@ -151,7 +166,7 @@ const updatePassword = async (event: Event) => {
|
|||
id: authStore.user.id,
|
||||
password: password.value,
|
||||
};
|
||||
await api.update(data, ["password"]);
|
||||
await api.update(data, ["password"], currentPassword.value);
|
||||
authStore.updateUser(data);
|
||||
$showSuccess(t("settings.passwordUpdated"));
|
||||
} catch (e: any) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@
|
|||
:isDefault="false"
|
||||
:isNew="isNew"
|
||||
/>
|
||||
|
||||
<p v-if="isCurrentPasswordRequired">
|
||||
<label for="currentPassword">{{
|
||||
t("settings.currentPassword")
|
||||
}}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="password"
|
||||
v-model="currentPassword"
|
||||
id="currentPassword"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
|
|
@ -63,6 +76,8 @@ const error = ref<StatusError>();
|
|||
const originalUser = ref<IUser>();
|
||||
const user = ref<IUser>();
|
||||
const createUserDir = ref<boolean>(false);
|
||||
const currentPassword = ref<string>("");
|
||||
const isCurrentPasswordRequired = ref<boolean>(false);
|
||||
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
const $showSuccess = inject<IToastSuccess>("$showSuccess")!;
|
||||
|
|
@ -90,7 +105,12 @@ const fetchData = async () => {
|
|||
|
||||
try {
|
||||
if (isNew.value) {
|
||||
const { defaults, createUserDir: _createUserDir } = await settings.get();
|
||||
const {
|
||||
authMethod,
|
||||
defaults,
|
||||
createUserDir: _createUserDir,
|
||||
} = await settings.get();
|
||||
isCurrentPasswordRequired.value = authMethod == "json";
|
||||
createUserDir.value = _createUserDir;
|
||||
user.value = {
|
||||
...defaults,
|
||||
|
|
@ -101,6 +121,8 @@ const fetchData = async () => {
|
|||
id: 0,
|
||||
};
|
||||
} else {
|
||||
const { authMethod } = await settings.get();
|
||||
isCurrentPasswordRequired.value = authMethod == "json";
|
||||
const id = Array.isArray(route.params.id)
|
||||
? route.params.id.join("")
|
||||
: route.params.id;
|
||||
|
|
@ -151,11 +173,11 @@ const save = async (event: Event) => {
|
|||
...user.value,
|
||||
};
|
||||
|
||||
const loc = await api.create(newUser);
|
||||
const loc = await api.create(newUser, currentPassword.value);
|
||||
router.push({ path: loc || "/settings/users" });
|
||||
$showSuccess(t("settings.userCreated"));
|
||||
} else {
|
||||
await api.update(user.value);
|
||||
await api.update(user.value, ["all"], currentPassword.value);
|
||||
|
||||
if (user.value.id === authStore.user?.id) {
|
||||
authStore.updateUser(user.value);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ import (
|
|||
)
|
||||
|
||||
type modifyRequest struct {
|
||||
What string `json:"what"` // Answer to: what data type?
|
||||
Which []string `json:"which"` // Answer to: which fields?
|
||||
What string `json:"what"` // Answer to: what data type?
|
||||
Which []string `json:"which"` // Answer to: which fields?
|
||||
CurrentPassword string `json:"current_password"` // Answer to: user logged password
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ type settingsData struct {
|
|||
MinimumPasswordLength uint `json:"minimumPasswordLength"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
Defaults settings.UserDefaults `json:"defaults"`
|
||||
AuthMethod settings.AuthMethod `json:"authMethod"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Branding settings.Branding `json:"branding"`
|
||||
Tus settings.Tus `json:"tus"`
|
||||
|
|
@ -30,6 +31,7 @@ var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
|
|||
MinimumPasswordLength: d.settings.MinimumPasswordLength,
|
||||
UserHomeBasePath: d.settings.UserHomeBasePath,
|
||||
Defaults: d.settings.Defaults,
|
||||
AuthMethod: d.settings.AuthMethod,
|
||||
Rules: d.settings.Rules,
|
||||
Branding: d.settings.Branding,
|
||||
Tus: d.settings.Tus,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
|
@ -117,6 +118,12 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
|||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
if d.settings.AuthMethod == auth.MethodJSONAuth {
|
||||
if !users.CheckPwd(req.CurrentPassword, d.user.Password) {
|
||||
return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Which) != 0 {
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
|
@ -153,6 +160,27 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
|||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
if d.settings.AuthMethod == auth.MethodJSONAuth {
|
||||
var sensibleFields = map[string]struct{}{
|
||||
"all": {},
|
||||
"username": {},
|
||||
"password": {},
|
||||
"scope": {},
|
||||
"lockPassword": {},
|
||||
"commands": {},
|
||||
"perm": {},
|
||||
}
|
||||
|
||||
for _, field := range req.Which {
|
||||
if _, ok := sensibleFields[field]; ok {
|
||||
if !users.CheckPwd(req.CurrentPassword, d.user.Password) {
|
||||
return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Data.ID != d.raw.(uint) {
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue