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