mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-25 23:53:56 -06:00
Merge branch 'big_merge_v49' into 4.9
This commit is contained in:
commit
368c0310a5
6 changed files with 143 additions and 9 deletions
|
@ -5,6 +5,7 @@ import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
from copy import deepcopy
|
||||||
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
||||||
from typing import Dict, Optional, TYPE_CHECKING
|
from typing import Dict, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -27,6 +28,9 @@ class Backup:
|
||||||
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
|
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
|
||||||
"""These files should be ignored when making a backup."""
|
"""These files should be ignored when making a backup."""
|
||||||
|
|
||||||
|
SECRETS_SETTINGS = ["general/ultimaker_auth_data"]
|
||||||
|
"""Secret preferences that need to obfuscated when making a backup of Cura"""
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
"""Re-use translation catalog"""
|
"""Re-use translation catalog"""
|
||||||
|
|
||||||
|
@ -43,6 +47,9 @@ class Backup:
|
||||||
|
|
||||||
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
|
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
|
||||||
|
|
||||||
|
# obfuscate sensitive secrets
|
||||||
|
secrets = self._obfuscate()
|
||||||
|
|
||||||
# Ensure all current settings are saved.
|
# Ensure all current settings are saved.
|
||||||
self._application.saveSettings()
|
self._application.saveSettings()
|
||||||
|
|
||||||
|
@ -78,6 +85,8 @@ class Backup:
|
||||||
"profile_count": str(profile_count),
|
"profile_count": str(profile_count),
|
||||||
"plugin_count": str(plugin_count)
|
"plugin_count": str(plugin_count)
|
||||||
}
|
}
|
||||||
|
# Restore the obfuscated settings
|
||||||
|
self._illuminate(**secrets)
|
||||||
|
|
||||||
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
|
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
|
||||||
"""Make a full archive from the given root path with the given name.
|
"""Make a full archive from the given root path with the given name.
|
||||||
|
@ -134,6 +143,9 @@ class Backup:
|
||||||
"Tried to restore a Cura backup that is higher than the current version."))
|
"Tried to restore a Cura backup that is higher than the current version."))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Get the current secrets and store since the back-up doesn't contain those
|
||||||
|
secrets = self._obfuscate()
|
||||||
|
|
||||||
version_data_dir = Resources.getDataStoragePath()
|
version_data_dir = Resources.getDataStoragePath()
|
||||||
archive = ZipFile(io.BytesIO(self.zip_file), "r")
|
archive = ZipFile(io.BytesIO(self.zip_file), "r")
|
||||||
extracted = self._extractArchive(archive, version_data_dir)
|
extracted = self._extractArchive(archive, version_data_dir)
|
||||||
|
@ -146,6 +158,9 @@ class Backup:
|
||||||
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
|
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
|
||||||
shutil.move(backup_preferences_file, preferences_file)
|
shutil.move(backup_preferences_file, preferences_file)
|
||||||
|
|
||||||
|
# Restore the obfuscated settings
|
||||||
|
self._illuminate(**secrets)
|
||||||
|
|
||||||
return extracted
|
return extracted
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -173,3 +188,28 @@ class Backup:
|
||||||
Logger.logException("e", "Unable to extract the backup due to permission or file system errors.")
|
Logger.logException("e", "Unable to extract the backup due to permission or file system errors.")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _obfuscate(self) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Obfuscate and remove the secret preferences that are specified in SECRETS_SETTINGS
|
||||||
|
|
||||||
|
:return: a dictionary of the removed secrets. Note: the '/' is replaced by '__'
|
||||||
|
"""
|
||||||
|
preferences = self._application.getPreferences()
|
||||||
|
secrets = {}
|
||||||
|
for secret in self.SECRETS_SETTINGS:
|
||||||
|
secrets[secret.replace("/", "__")] = deepcopy(preferences.getValue(secret))
|
||||||
|
preferences.setValue(secret, None)
|
||||||
|
self._application.savePreferences()
|
||||||
|
return secrets
|
||||||
|
|
||||||
|
def _illuminate(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Restore the obfuscated settings
|
||||||
|
|
||||||
|
:param kwargs: a dict of obscured preferences. Note: the '__' of the keys will be replaced by '/'
|
||||||
|
"""
|
||||||
|
preferences = self._application.getPreferences()
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
preferences.setValue(key.replace("__", "/"), value)
|
||||||
|
self._application.savePreferences()
|
||||||
|
|
|
@ -255,10 +255,9 @@ class AuthorizationService:
|
||||||
self._auth_data = auth_data
|
self._auth_data = auth_data
|
||||||
if auth_data:
|
if auth_data:
|
||||||
self._user_profile = self.getUserProfile()
|
self._user_profile = self.getUserProfile()
|
||||||
self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(vars(auth_data)))
|
self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(auth_data.dump()))
|
||||||
else:
|
else:
|
||||||
self._user_profile = None
|
self._user_profile = None
|
||||||
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
||||||
|
|
||||||
self.accessTokenChanged.emit()
|
self.accessTokenChanged.emit()
|
||||||
|
|
||||||
|
|
73
cura/OAuth2/KeyringAttribute.py
Normal file
73
cura/OAuth2/KeyringAttribute.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
# Copyright (c) 2021 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import Type, TYPE_CHECKING
|
||||||
|
|
||||||
|
import keyring
|
||||||
|
from keyring.backend import KeyringBackend
|
||||||
|
from keyring.errors import NoKeyringError, PasswordSetError
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.OAuth2.Models import BaseModel
|
||||||
|
|
||||||
|
# Need to do some extra workarounds on windows:
|
||||||
|
import sys
|
||||||
|
from UM.Platform import Platform
|
||||||
|
if Platform.isWindows() and hasattr(sys, "frozen"):
|
||||||
|
import win32timezone
|
||||||
|
from keyring.backends.Windows import WinVaultKeyring
|
||||||
|
keyring.set_keyring(WinVaultKeyring())
|
||||||
|
|
||||||
|
# Even if errors happen, we don't want this stored locally:
|
||||||
|
DONT_EVER_STORE_LOCALLY = ["refresh_token"]
|
||||||
|
|
||||||
|
class KeyringAttribute:
|
||||||
|
"""
|
||||||
|
Descriptor for attributes that need to be stored in the keyring. With Fallback behaviour to the preference cfg file
|
||||||
|
"""
|
||||||
|
def __get__(self, instance: Type["BaseModel"], owner: type) -> str:
|
||||||
|
if self._store_secure:
|
||||||
|
try:
|
||||||
|
return keyring.get_password("cura", self._keyring_name)
|
||||||
|
except NoKeyringError:
|
||||||
|
self._store_secure = False
|
||||||
|
Logger.logException("w", "No keyring backend present")
|
||||||
|
return getattr(instance, self._name)
|
||||||
|
else:
|
||||||
|
return getattr(instance, self._name)
|
||||||
|
|
||||||
|
def __set__(self, instance: Type["BaseModel"], value: str):
|
||||||
|
if self._store_secure:
|
||||||
|
setattr(instance, self._name, None)
|
||||||
|
try:
|
||||||
|
keyring.set_password("cura", self._keyring_name, value)
|
||||||
|
except PasswordSetError:
|
||||||
|
self._store_secure = False
|
||||||
|
if self._name not in DONT_EVER_STORE_LOCALLY:
|
||||||
|
setattr(instance, self._name, value)
|
||||||
|
Logger.logException("w", "Keyring access denied")
|
||||||
|
except NoKeyringError:
|
||||||
|
self._store_secure = False
|
||||||
|
if self._name not in DONT_EVER_STORE_LOCALLY:
|
||||||
|
setattr(instance, self._name, value)
|
||||||
|
Logger.logException("w", "No keyring backend present")
|
||||||
|
except BaseException as e:
|
||||||
|
# A BaseException can occur in Windows when the keyring attempts to write a token longer than 1024
|
||||||
|
# characters in the Windows Credentials Manager.
|
||||||
|
self._store_secure = False
|
||||||
|
if self._name not in DONT_EVER_STORE_LOCALLY:
|
||||||
|
setattr(instance, self._name, value)
|
||||||
|
Logger.log("w", "Keyring failed: {}".format(e))
|
||||||
|
else:
|
||||||
|
setattr(instance, self._name, value)
|
||||||
|
|
||||||
|
def __set_name__(self, owner: type, name: str):
|
||||||
|
self._name = "_{}".format(name)
|
||||||
|
self._keyring_name = name
|
||||||
|
self._store_secure = False
|
||||||
|
try:
|
||||||
|
self._store_secure = KeyringBackend.viable
|
||||||
|
except NoKeyringError:
|
||||||
|
Logger.logException("w", "Could not use keyring")
|
||||||
|
setattr(owner, self._name, None)
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright (c) 2020 Ultimaker B.V.
|
# Copyright (c) 2021 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List, Union
|
||||||
|
from copy import deepcopy
|
||||||
|
from cura.OAuth2.KeyringAttribute import KeyringAttribute
|
||||||
|
|
||||||
|
|
||||||
class BaseModel:
|
class BaseModel:
|
||||||
|
@ -37,12 +39,29 @@ class AuthenticationResponse(BaseModel):
|
||||||
# Data comes from the token response with success flag and error message added.
|
# Data comes from the token response with success flag and error message added.
|
||||||
success = True # type: bool
|
success = True # type: bool
|
||||||
token_type = None # type: Optional[str]
|
token_type = None # type: Optional[str]
|
||||||
access_token = None # type: Optional[str]
|
|
||||||
refresh_token = None # type: Optional[str]
|
|
||||||
expires_in = None # type: Optional[str]
|
expires_in = None # type: Optional[str]
|
||||||
scope = None # type: Optional[str]
|
scope = None # type: Optional[str]
|
||||||
err_message = None # type: Optional[str]
|
err_message = None # type: Optional[str]
|
||||||
received_at = None # type: Optional[str]
|
received_at = None # type: Optional[str]
|
||||||
|
access_token = KeyringAttribute()
|
||||||
|
refresh_token = KeyringAttribute()
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
self.access_token = kwargs.pop("access_token", None)
|
||||||
|
self.refresh_token = kwargs.pop("refresh_token", None)
|
||||||
|
super(AuthenticationResponse, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def dump(self) -> Dict[str, Union[bool, Optional[str]]]:
|
||||||
|
"""
|
||||||
|
Dumps the dictionary of Authentication attributes. KeyringAttributes are transformed to public attributes
|
||||||
|
If the keyring was used, these will have a None value, otherwise they will have the secret value
|
||||||
|
|
||||||
|
:return: Dictionary of Authentication attributes
|
||||||
|
"""
|
||||||
|
dumped = deepcopy(vars(self))
|
||||||
|
dumped["access_token"] = dumped.pop("_access_token")
|
||||||
|
dumped["refresh_token"] = dumped.pop("_refresh_token")
|
||||||
|
return dumped
|
||||||
|
|
||||||
|
|
||||||
class ResponseStatus(BaseModel):
|
class ResponseStatus(BaseModel):
|
||||||
|
|
|
@ -32,3 +32,5 @@ urllib3==1.25.6
|
||||||
PyYAML==5.1.2
|
PyYAML==5.1.2
|
||||||
zeroconf==0.24.1
|
zeroconf==0.24.1
|
||||||
comtypes==1.1.7
|
comtypes==1.1.7
|
||||||
|
pywin32==300
|
||||||
|
keyring==23.0.1
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2020 Ultimaker B.V.
|
// Copyright (c) 2021 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
@ -15,7 +15,7 @@ UM.Dialog
|
||||||
title: catalog.i18nc("@title:window The argument is the application name.", "About %1").arg(CuraApplication.applicationDisplayName)
|
title: catalog.i18nc("@title:window The argument is the application name.", "About %1").arg(CuraApplication.applicationDisplayName)
|
||||||
|
|
||||||
minimumWidth: 500 * screenScaleFactor
|
minimumWidth: 500 * screenScaleFactor
|
||||||
minimumHeight: 650 * screenScaleFactor
|
minimumHeight: 700 * screenScaleFactor
|
||||||
width: minimumWidth
|
width: minimumWidth
|
||||||
height: minimumHeight
|
height: minimumHeight
|
||||||
|
|
||||||
|
@ -158,7 +158,8 @@ UM.Dialog
|
||||||
projectsModel.append({ name: "Sentry", description: catalog.i18nc("@Label", "Python Error tracking library"), license: "BSD 2-Clause 'Simplified'", url: "https://sentry.io/for/python/" });
|
projectsModel.append({ name: "Sentry", description: catalog.i18nc("@Label", "Python Error tracking library"), license: "BSD 2-Clause 'Simplified'", url: "https://sentry.io/for/python/" });
|
||||||
projectsModel.append({ name: "libnest2d", description: catalog.i18nc("@label", "Polygon packing library, developed by Prusa Research"), license: "LGPL", url: "https://github.com/tamasmeszaros/libnest2d" });
|
projectsModel.append({ name: "libnest2d", description: catalog.i18nc("@label", "Polygon packing library, developed by Prusa Research"), license: "LGPL", url: "https://github.com/tamasmeszaros/libnest2d" });
|
||||||
projectsModel.append({ name: "pynest2d", description: catalog.i18nc("@label", "Python bindings for libnest2d"), license: "LGPL", url: "https://github.com/Ultimaker/pynest2d" });
|
projectsModel.append({ name: "pynest2d", description: catalog.i18nc("@label", "Python bindings for libnest2d"), license: "LGPL", url: "https://github.com/Ultimaker/pynest2d" });
|
||||||
|
projectsModel.append({ name: "keyring", description: catalog.i18nc("@label", "Support library for system keyring access"), license: "MIT", url: "https://github.com/jaraco/keyring" });
|
||||||
|
projectsModel.append({ name: "pywin32", description: catalog.i18nc("@label", "Python extensions for Microsoft Windows"), license: "PSF", url: "https://github.com/mhammond/pywin32" });
|
||||||
projectsModel.append({ name: "Noto Sans", description: catalog.i18nc("@label", "Font"), license: "Apache 2.0", url: "https://www.google.com/get/noto/" });
|
projectsModel.append({ name: "Noto Sans", description: catalog.i18nc("@label", "Font"), license: "Apache 2.0", url: "https://www.google.com/get/noto/" });
|
||||||
projectsModel.append({ name: "Font-Awesome-SVG-PNG", description: catalog.i18nc("@label", "SVG icons"), license: "SIL OFL 1.1", url: "https://github.com/encharm/Font-Awesome-SVG-PNG" });
|
projectsModel.append({ name: "Font-Awesome-SVG-PNG", description: catalog.i18nc("@label", "SVG icons"), license: "SIL OFL 1.1", url: "https://github.com/encharm/Font-Awesome-SVG-PNG" });
|
||||||
projectsModel.append({ name: "AppImageKit", description: catalog.i18nc("@label", "Linux cross-distribution application deployment"), license: "MIT", url: "https://github.com/AppImage/AppImageKit" });
|
projectsModel.append({ name: "AppImageKit", description: catalog.i18nc("@label", "Linux cross-distribution application deployment"), license: "MIT", url: "https://github.com/AppImage/AppImageKit" });
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue