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:
|
devices:
|
||||||
- /dev/fuse:/dev/fuse
|
- /dev/fuse:/dev/fuse
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris # Set your timezone here
|
- TZ=Europe/Paris # Set your timezone here
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||||
|
|
@ -83,7 +83,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "4096:4096"
|
- "4096:4096"
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris # Set your timezone here
|
- TZ=Europe/Paris # Set your timezone here
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||||
|
|
@ -91,6 +91,7 @@ services:
|
||||||
```
|
```
|
||||||
|
|
||||||
**Trade-offs:**
|
**Trade-offs:**
|
||||||
|
|
||||||
- ✅ Improved security by reducing container capabilities
|
- ✅ Improved security by reducing container capabilities
|
||||||
- ✅ Support for local directories
|
- ✅ Support for local directories
|
||||||
- ✅ Keep support all repository types (local, S3, GCS, Azure, rclone)
|
- ✅ Keep support all repository types (local, S3, GCS, Azure, rclone)
|
||||||
|
|
|
||||||
|
|
@ -3364,7 +3364,6 @@ export type ExportFullConfigResponses = {
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
hasDownloadedResticPassword?: boolean;
|
hasDownloadedResticPassword?: boolean;
|
||||||
id?: number;
|
id?: number;
|
||||||
passwordHash?: string;
|
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
}>;
|
}>;
|
||||||
volumes?: Array<unknown>;
|
volumes?: Array<unknown>;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ describe("system security", () => {
|
||||||
const endpoints: { method: string; path: string }[] = [
|
const endpoints: { method: string; path: string }[] = [
|
||||||
{ method: "GET", path: "/api/v1/system/info" },
|
{ method: "GET", path: "/api/v1/system/info" },
|
||||||
{ method: "POST", path: "/api/v1/system/restic-password" },
|
{ method: "POST", path: "/api/v1/system/restic-password" },
|
||||||
|
{ method: "POST", path: "/api/v1/system/export" },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { method, path } of endpoints) {
|
for (const { method, path } of endpoints) {
|
||||||
|
|
@ -86,4 +87,60 @@ describe("system security", () => {
|
||||||
expect(body.message).toBe("Incorrect password");
|
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({
|
users: type({
|
||||||
id: "number?",
|
id: "number?",
|
||||||
username: "string",
|
username: "string",
|
||||||
passwordHash: "string?",
|
|
||||||
createdAt: "number?",
|
createdAt: "number?",
|
||||||
updatedAt: "number?",
|
updatedAt: "number?",
|
||||||
hasDownloadedResticPassword: "boolean?",
|
hasDownloadedResticPassword: "boolean?",
|
||||||
|
|
|
||||||
|
|
@ -187,13 +187,19 @@ const exportConfig = async (params: ExportParams) => {
|
||||||
|
|
||||||
const backupSchedules = transformBackupSchedules(backupSchedulesRaw, scheduleNotifications, scheduleMirrors, params);
|
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(volumes, params),
|
||||||
exportEntities(repositories, params),
|
exportEntities(repositories, params),
|
||||||
exportEntities(notifications, params),
|
exportEntities(notifications, params),
|
||||||
exportEntities(users, params),
|
exportEntities(users, params),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const exportUsers = exportedUsersWithHash.map((user) => {
|
||||||
|
const sanitizedUser = { ...user };
|
||||||
|
delete sanitizedUser.passwordHash;
|
||||||
|
return sanitizedUser;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue