mirror of
https://github.com/LogicLabs-OU/OpenArchiver.git
synced 2026-04-06 00:31:57 +02:00
Add option to disable deletions
This commit introduces a new feature that allows admins to disable the deletion of emails and ingestion sources for the entire instance. This is a critical feature for compliance and data retention, as it prevents accidental or unauthorized deletions. Changes: - **Configuration**: Added an `ENABLE_DELETION` environment variable. If this variable is not set to `true`, all deletion operations will be disabled. - **Deletion Guard**: A centralized `checkDeletionEnabled` guard has been implemented to enforce this setting at both the controller and service levels, ensuring a robust and secure implementation. - **Documentation**: The installation guide has been updated to include the new `ENABLE_DELETION` environment variable and its behavior. - **Refactor**: The `IngestionService`'s `create` method was refactored to remove unnecessary calls to the `delete` method, simplifying the code and improving its robustness.
This commit is contained in:
@@ -64,6 +64,9 @@ STORAGE_ENCRYPTION_KEY=
|
||||
|
||||
# --- Security & Authentication ---
|
||||
|
||||
# Enable or disable deletion of emails and ingestion sources. Defaults to false.
|
||||
ENABLE_DELETION=false
|
||||
|
||||
# Rate Limiting
|
||||
# The window in milliseconds for which API requests are checked. Defaults to 60000 (1 minute).
|
||||
RATE_LIMIT_WINDOW_MS=60000
|
||||
|
||||
@@ -132,14 +132,15 @@ These variables are used by `docker-compose.yml` to configure the services.
|
||||
|
||||
#### Security & Authentication
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| `JWT_SECRET` | A secret key for signing JWT tokens. | `a-very-secret-key-that-you-should-change` |
|
||||
| `JWT_EXPIRES_IN` | The expiration time for JWT tokens. | `7d` |
|
||||
| ~~`SUPER_API_KEY`~~ (Deprecated) | An API key with super admin privileges. (The SUPER_API_KEY is deprecated since v0.3.0 after we roll out the role-based access control system.) | |
|
||||
| `RATE_LIMIT_WINDOW_MS` | The window in milliseconds for which API requests are checked. | `900000` (15 minutes) |
|
||||
| `RATE_LIMIT_MAX_REQUESTS` | The maximum number of API requests allowed from an IP within the window. | `100` |
|
||||
| `ENCRYPTION_KEY` | A 32-byte hex string for encrypting sensitive data in the database. | |
|
||||
| Variable | Description | Default Value |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| `ENABLE_DELETION` | Enable or disable deletion of emails and ingestion sources. If this option is not set, or is set to any value other than `true`, deletion will be disabled for the entire instance. | `false` |
|
||||
| `JWT_SECRET` | A secret key for signing JWT tokens. | `a-very-secret-key-that-you-should-change` |
|
||||
| `JWT_EXPIRES_IN` | The expiration time for JWT tokens. | `7d` |
|
||||
| ~~`SUPER_API_KEY`~~ (Deprecated) | An API key with super admin privileges. (The SUPER_API_KEY is deprecated since v0.3.0 after we roll out the role-based access control system.) | |
|
||||
| `RATE_LIMIT_WINDOW_MS` | The window in milliseconds for which API requests are checked. | `900000` (15 minutes) |
|
||||
| `RATE_LIMIT_MAX_REQUESTS` | The maximum number of API requests allowed from an IP within the window. | `100` |
|
||||
| `ENCRYPTION_KEY` | A 32-byte hex string for encrypting sensitive data in the database. | |
|
||||
|
||||
#### Apache Tika Integration
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
|
||||
import { ArchivedEmailService } from '../../services/ArchivedEmailService';
|
||||
import { config } from '../../config';
|
||||
import { UserService } from '../../services/UserService';
|
||||
import { checkDeletionEnabled } from '../../helpers/deletionGuard';
|
||||
|
||||
export class ArchivedEmailController {
|
||||
private userService = new UserService();
|
||||
@@ -63,6 +64,7 @@ export class ArchivedEmailController {
|
||||
return res.status(403).json({ message: req.t('errors.demoMode') });
|
||||
}
|
||||
try {
|
||||
checkDeletionEnabled();
|
||||
const { id } = req.params;
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { logger } from '../../config/logger';
|
||||
import { config } from '../../config';
|
||||
import { UserService } from '../../services/UserService';
|
||||
import { checkDeletionEnabled } from '../../helpers/deletionGuard';
|
||||
|
||||
export class IngestionController {
|
||||
private userService = new UserService();
|
||||
@@ -111,6 +112,7 @@ export class IngestionController {
|
||||
return res.status(403).json({ message: req.t('errors.demoMode') });
|
||||
}
|
||||
try {
|
||||
checkDeletionEnabled();
|
||||
const { id } = req.params;
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
@@ -126,6 +128,8 @@ export class IngestionController {
|
||||
console.error(`Delete ingestion source ${req.params.id} error:`, error);
|
||||
if (error instanceof Error && error.message === 'Ingestion source not found') {
|
||||
return res.status(404).json({ message: req.t('ingestion.notFound') });
|
||||
} else if (error instanceof Error) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
return res.status(500).json({ message: req.t('errors.internalServerError') });
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ export const app = {
|
||||
encryptionKey: process.env.ENCRYPTION_KEY,
|
||||
isDemo: process.env.IS_DEMO === 'true',
|
||||
syncFrequency: process.env.SYNC_FREQUENCY || '* * * * *', //default to 1 minute
|
||||
enableDeletion: process.env.ENABLE_DELETION === 'true',
|
||||
};
|
||||
|
||||
9
packages/backend/src/helpers/deletionGuard.ts
Normal file
9
packages/backend/src/helpers/deletionGuard.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { config } from '../config';
|
||||
import i18next from 'i18next';
|
||||
|
||||
export function checkDeletionEnabled() {
|
||||
if (!config.app.enableDeletion) {
|
||||
const errorMessage = i18next.t('Deletion is disabled for this instance.');
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@
|
||||
"demoMode": "Dieser Vorgang ist im Demo-Modus nicht zulässig.",
|
||||
"unauthorized": "Unbefugt",
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten",
|
||||
"noPermissionToAction": "Sie haben keine Berechtigung, die aktuelle Aktion auszuführen."
|
||||
"noPermissionToAction": "Sie haben keine Berechtigung, die aktuelle Aktion auszuführen.",
|
||||
"deletion_disabled": "Das Löschen ist für diese Instanz deaktiviert."
|
||||
},
|
||||
"user": {
|
||||
"notFound": "Benutzer nicht gefunden",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"demoMode": "This operation is not allowed in demo mode.",
|
||||
"unauthorized": "Unauthorized",
|
||||
"unknown": "An unknown error occurred",
|
||||
"noPermissionToAction": "You don't have the permission to perform the current action."
|
||||
"noPermissionToAction": "You don't have the permission to perform the current action.",
|
||||
"deletion_disabled": "Deletion is disabled for this instance."
|
||||
},
|
||||
"user": {
|
||||
"notFound": "User not found",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { SearchService } from './SearchService';
|
||||
import type { Readable } from 'stream';
|
||||
import { AuditService } from './AuditService';
|
||||
import { User } from '@open-archiver/types';
|
||||
import { checkDeletionEnabled } from '../helpers/deletionGuard';
|
||||
|
||||
interface DbRecipients {
|
||||
to: { name: string; address: string }[];
|
||||
@@ -198,6 +199,7 @@ export class ArchivedEmailService {
|
||||
actor: User,
|
||||
actorIp: string
|
||||
): Promise<void> {
|
||||
checkDeletionEnabled();
|
||||
const [email] = await db
|
||||
.select()
|
||||
.from(archivedEmails)
|
||||
|
||||
@@ -27,6 +27,7 @@ import { config } from '../config/index';
|
||||
import { FilterBuilder } from './FilterBuilder';
|
||||
import { AuditService } from './AuditService';
|
||||
import { User } from '@open-archiver/types';
|
||||
import { checkDeletionEnabled } from '../helpers/deletionGuard';
|
||||
|
||||
export class IngestionService {
|
||||
private static auditService = new AuditService();
|
||||
@@ -205,6 +206,7 @@ export class IngestionService {
|
||||
}
|
||||
|
||||
public static async delete(id: string, actor: User, actorIp: string): Promise<IngestionSource> {
|
||||
checkDeletionEnabled();
|
||||
const source = await this.findById(id);
|
||||
if (!source) {
|
||||
throw new Error('Ingestion source not found');
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
duration: 5000,
|
||||
show: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
ingestionSources = ingestionSources.filter((s) => !selectedIds.includes(s.id));
|
||||
|
||||
Reference in New Issue
Block a user