refactor(config-export): do not include user's password hashes
This commit is contained in:
parent
99ccfa4305
commit
1d49f59ebc
5 changed files with 67 additions and 5 deletions
|
|
@ -50,7 +50,7 @@ services:
|
|||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
environment:
|
||||
- TZ=Europe/Paris # Set your timezone here
|
||||
- TZ=Europe/Paris # Set your timezone here
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
|
|
@ -83,7 +83,7 @@ services:
|
|||
ports:
|
||||
- "4096:4096"
|
||||
environment:
|
||||
- TZ=Europe/Paris # Set your timezone here
|
||||
- TZ=Europe/Paris # Set your timezone here
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
|
|
@ -91,6 +91,7 @@ services:
|
|||
```
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ Improved security by reducing container capabilities
|
||||
- ✅ Support for local directories
|
||||
- ✅ Keep support all repository types (local, S3, GCS, Azure, rclone)
|
||||
|
|
|
|||
|
|
@ -3364,7 +3364,6 @@ export type ExportFullConfigResponses = {
|
|||
createdAt?: number;
|
||||
hasDownloadedResticPassword?: boolean;
|
||||
id?: number;
|
||||
passwordHash?: string;
|
||||
updatedAt?: number;
|
||||
}>;
|
||||
volumes?: Array<unknown>;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ describe("system security", () => {
|
|||
const endpoints: { method: string; path: string }[] = [
|
||||
{ method: "GET", path: "/api/v1/system/info" },
|
||||
{ method: "POST", path: "/api/v1/system/restic-password" },
|
||||
{ method: "POST", path: "/api/v1/system/export" },
|
||||
];
|
||||
|
||||
for (const { method, path } of endpoints) {
|
||||
|
|
@ -86,4 +87,60 @@ describe("system security", () => {
|
|||
expect(body.message).toBe("Incorrect password");
|
||||
});
|
||||
});
|
||||
|
||||
test("should return 400 for invalid payload on full export", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/system/export", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test("should return 401 for incorrect password on full export", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/system/export", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
includeMetadata: true,
|
||||
password: "wrong-password",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Incorrect password");
|
||||
});
|
||||
|
||||
test("full export never exposes password hashes", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
|
||||
const res = await app.request("/api/v1/system/export", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
includeMetadata: true,
|
||||
password: "testpassword",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { users?: Array<Record<string, unknown>> };
|
||||
expect(Array.isArray(body.users)).toBe(true);
|
||||
for (const user of body.users ?? []) {
|
||||
expect(user.passwordHash).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ const exportResponseSchema = type({
|
|||
users: type({
|
||||
id: "number?",
|
||||
username: "string",
|
||||
passwordHash: "string?",
|
||||
createdAt: "number?",
|
||||
updatedAt: "number?",
|
||||
hasDownloadedResticPassword: "boolean?",
|
||||
|
|
|
|||
|
|
@ -187,13 +187,19 @@ const exportConfig = async (params: ExportParams) => {
|
|||
|
||||
const backupSchedules = transformBackupSchedules(backupSchedulesRaw, scheduleNotifications, scheduleMirrors, params);
|
||||
|
||||
const [exportVolumes, exportRepositories, exportNotifications, exportUsers] = await Promise.all([
|
||||
const [exportVolumes, exportRepositories, exportNotifications, exportedUsersWithHash] = await Promise.all([
|
||||
exportEntities(volumes, params),
|
||||
exportEntities(repositories, params),
|
||||
exportEntities(notifications, params),
|
||||
exportEntities(users, params),
|
||||
]);
|
||||
|
||||
const exportUsers = exportedUsersWithHash.map((user) => {
|
||||
const sanitizedUser = { ...user };
|
||||
delete sanitizedUser.passwordHash;
|
||||
return sanitizedUser;
|
||||
});
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue