Merge branch 'big_merge_v49' into 4.9

This commit is contained in:
Remco Burema 2021-03-30 22:51:50 +02:00
commit 368c0310a5
No known key found for this signature in database
GPG key ID: 215C49431D43F98C
6 changed files with 143 additions and 9 deletions

View file

@ -5,6 +5,7 @@ import io
import os
import re
import shutil
from copy import deepcopy
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
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"]
"""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")
"""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)
# obfuscate sensitive secrets
secrets = self._obfuscate()
# Ensure all current settings are saved.
self._application.saveSettings()
@ -78,6 +85,8 @@ class Backup:
"profile_count": str(profile_count),
"plugin_count": str(plugin_count)
}
# Restore the obfuscated settings
self._illuminate(**secrets)
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
"""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."))
return False
# Get the current secrets and store since the back-up doesn't contain those
secrets = self._obfuscate()
version_data_dir = Resources.getDataStoragePath()
archive = ZipFile(io.BytesIO(self.zip_file), "r")
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)
shutil.move(backup_preferences_file, preferences_file)
# Restore the obfuscated settings
self._illuminate(**secrets)
return extracted
@staticmethod
@ -173,3 +188,28 @@ class Backup:
Logger.logException("e", "Unable to extract the backup due to permission or file system errors.")
return False
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()

View file

@ -255,10 +255,9 @@ class AuthorizationService:
self._auth_data = auth_data
if auth_data:
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:
self._user_profile = None
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
self.accessTokenChanged.emit()

View 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)

View file

@ -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.
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:
@ -37,12 +39,29 @@ class AuthenticationResponse(BaseModel):
# Data comes from the token response with success flag and error message added.
success = True # type: bool
token_type = None # type: Optional[str]
access_token = None # type: Optional[str]
refresh_token = None # type: Optional[str]
expires_in = None # type: Optional[str]
scope = None # type: Optional[str]
err_message = 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):

View file

@ -32,3 +32,5 @@ urllib3==1.25.6
PyYAML==5.1.2
zeroconf==0.24.1
comtypes==1.1.7
pywin32==300
keyring==23.0.1

View file

@ -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.
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)
minimumWidth: 500 * screenScaleFactor
minimumHeight: 650 * screenScaleFactor
minimumHeight: 700 * screenScaleFactor
width: minimumWidth
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: "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: "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: "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" });