Cura/cura/OAuth2/KeyringAttribute.py
Kostas Karmas 07594f17a7 Fix setting empty auth tokens in the keyring on startup
When Cura is starting up, it reads the authentication data from the preferences (cura.cfg). If
the auth tokens have previously been stored in the keyring, it means that their values will be null
in the cura.cfg file. Therefore, on startup, Cura reads the tokens as none from the preferences and
then sets the empty values in the keyring as tokens. This leads to the user being signed off every
time Cura restarts on Mac.

On Windows, the access token was still stored in the preferences, so on startup it was safe. The
refresh token, on the other hand, had the same issue as on Mac, which means that on startup it was
read as None from the cura.cfg and was stored in the keyring as an empty string. This meant that,
even though on startup (on windows) the user was kept signed in, the next time Cura was attempting
to refresh the access token (after 7-8 minutes), it wouldn't be able, since its refresh token was
read as "" from the keyring. Also, if the user would close Cura and reopen it after 10 minutes
(so after the access token had expired) then they would be signed off on windows too.

This commit fixes that by making sure that if the given value of the refresh and access tokens are
empty, then they will not be stored in the keyring.

CURA-8178
2021-04-16 17:05:08 +02:00

80 lines
3.2 KiB
Python

# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Type, TYPE_CHECKING, Optional, List
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())
if Platform.isOSX() and hasattr(sys, "frozen"):
from keyring.backends.macOS import Keyring
keyring.set_keyring(Keyring())
# Even if errors happen, we don't want this stored locally:
DONT_EVER_STORE_LOCALLY: List[str] = ["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: "BaseModel", owner: type) -> Optional[str]:
if self._store_secure: # type: ignore
try:
value = keyring.get_password("cura", self._keyring_name)
return value if value != "" else None
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: "BaseModel", value: Optional[str]):
if self._store_secure:
setattr(instance, self._name, None)
if value is not 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)