New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Encrypted Backups #14126
feat: Encrypted Backups #14126
Conversation
Codecov Report
@@ Coverage Diff @@
## develop #14126 +/- ##
===========================================
- Coverage 56.25% 56.22% -0.03%
===========================================
Files 499 503 +4
Lines 41290 41536 +246
===========================================
+ Hits 23226 23352 +126
- Misses 18064 18184 +120 |
Create Docs and paste link in description. |
@@ -47,6 +47,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin | |||
|
|||
@click.command('restore') | |||
@click.argument('sql-file-path') | |||
@click.option('--backup-encryption-key', help='Backup encryption Key') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@click.option('--backup-encryption-key', help='Backup encryption Key') | |
@click.option('--encryption-key', help='Backup encryption Key') |
Backup seems a bit redundant here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already have "encryption key" for passwords. Won't it cause confusion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we kept a separate key was that we didn't want customers to have access to the site encryption key.
But we let them download the site config which has the key anyway.
So replace this key with the site encryption key. Reuse that, revert changes.
@@ -56,7 +57,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin | |||
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file') | |||
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended') | |||
@pass_context | |||
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): | |||
def restore(context, sql_file_path, backup_encryption_key=None, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Best to add new kwargs at the end. Don't know if someone is using these commands programmatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 agree @Anurag0911
|
||
if not os.path.exists(sql_file_path): | ||
print("Invalid path", sql_file_path) | ||
sys.exit(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sys.exit(1) | ||
|
||
# Check if file is encrypted | ||
if "enc" in sql_file_path: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a very weak check to figure out if a backup is encrypted
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can try reading the file and decrypt it in except block but adding a header in an encrypted file is not an option it causes issues with the decrypted files. Any other suggestions for identifying these files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We tried having a header in the tar, but it didn't work out as this encrypts the whole thing.
We should have a try-catch if this fails along with this.
@@ -66,10 +67,61 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas | |||
is_partial, | |||
validate_database_sql | |||
) | |||
from frappe.utils.backups import backup_decryption, decryption_rollback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Encryption and Decryption methods should be a part of the BackupGenerator
. Either that or a separate class that represents a Backup
alone.
frappe/utils/backups.py
Outdated
def backup_encryption_key(): | ||
""" | ||
Checks if backup encryption key exists | ||
else create one | ||
Return: | ||
Backup encryption key | ||
""" | ||
from frappe.installer import update_site_config | ||
if 'backup_encryption_key' not in frappe.local.conf: | ||
confirm = click.confirm( | ||
"No encrytion key found. Generate new Key?" | ||
) | ||
if not confirm: | ||
sys.exit(1) | ||
backup_encryption_key = Fernet.generate_key().decode() | ||
update_site_config('backup_encryption_key', backup_encryption_key) | ||
frappe.local.conf.backup_encryption_key = backup_encryption_key | ||
return frappe.local.conf.backup_encryption_key |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function does too many things; break it down to either get user input, set encryption key in config or change it. If ever invoked from anywhere except the CLI, it would cause it to stall and/or crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need more information regarding this. User input is required once and there should be a warning... it's harmless to remove, is it good idea to create even it's required once?
decryptedfile = file_path[:-4], | ||
) | ||
|
||
frappe.utils.execute_in_shell(command) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if file_path
does not exist, NameError
?
frappe/utils/backups.py
Outdated
def get_backup_encryption_key(): | ||
if 'backup_encryption_key' in frappe.local.conf: | ||
message = frappe.local.conf.backup_encryption_key | ||
else: | ||
message = "No key found." | ||
return frappe.msgprint(message,'Backup Encryption Key') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API endpoints should respond with some specific data, just returning frappe.conf.backup_encryption_key
would do here. The user message should be handled at the frontend.
Also, this API should have a restriction such as only for System Managers, or the Administrator user.
file_type_slugs = { | ||
"database": "*-{{}}-{}database.sql.gz".format('*' if partial else ''), | ||
"public": "*-{}-files.tar", | ||
"private": "*-{}-private-files.tar", | ||
"config": "*-{}-site_config_backup.json", | ||
} | ||
else: | ||
file_type_slugs = { | ||
"database": "*-{{}}-{}database.enc.sql.gz".format('*' if partial else ''), | ||
"public": "*-{}-files.enc.tar", | ||
"private": "*-{}-private-files.enc.tar", | ||
"config": "*-{}-site_config_backup.json", | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be DRY-er
frappe/commands/site.py
Outdated
|
||
#Get the key from site config | ||
site = get_site(context) | ||
frappe.init(site) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not do this whole block right before calling partial_restore
? There doesn't seem to be any reason to initialize werkzeug locals and release them twice here.
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed within 3 days if no further activity occurs, but it only takes a comment to keep a contribution alive :) Also, even if it is closed, you can always reopen the PR when you're ready. Thank you for contributing. |
Problem: Backups were not encrypted.
Changes:
Approach
For Encryption
For Decryption
PR.Summary.mp4
CheckBox to enable/disable backup encryption
Backup Downloads page
Updates-
Backup encryption with files.
Use
--backup-encryption-key
flag to decrypt the file at the given path.No need to provide the encryption key on the same website as it is stored in the site config during the encryption.
Compressed files encryption.
Partial files encryption
Rollback
Examples:
Providing invalid key.
Providing wrong root password.