Updated comments in cura/OAuth2

Used DOX_2_RST.py to convert doxygen style comments to sphinx style comments
This commit is contained in:
jelle Spijker 2020-05-11 13:16:29 +02:00 committed by Jelle Spijker
parent a503149452
commit b032101e55
No known key found for this signature in database
GPG key ID: 6662DC033BE6B99A
6 changed files with 135 additions and 71 deletions

View file

@ -16,23 +16,27 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin
catalog = i18nCatalog("cura")
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
## Class containing several helpers to deal with the authorization flow.
class AuthorizationHelpers:
"""Class containing several helpers to deal with the authorization flow."""
def __init__(self, settings: "OAuth2Settings") -> None:
self._settings = settings
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
@property
## The OAuth2 settings object.
def settings(self) -> "OAuth2Settings":
"""The OAuth2 settings object."""
return self._settings
## Request the access token from the authorization server.
# \param authorization_code: The authorization code from the 1st step.
# \param verification_code: The verification code needed for the PKCE
# extension.
# \return An AuthenticationResponse object.
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
"""Request the access token from the authorization server.
:param authorization_code: The authorization code from the 1st step.
:param verification_code: The verification code needed for the PKCE extension.
:return: An AuthenticationResponse object.
"""
data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
@ -46,10 +50,13 @@ class AuthorizationHelpers:
except requests.exceptions.ConnectionError:
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
## Request the access token from the authorization server using a refresh token.
# \param refresh_token:
# \return An AuthenticationResponse object.
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
"""Request the access token from the authorization server using a refresh token.
:param refresh_token:
:return: An AuthenticationResponse object.
"""
Logger.log("d", "Refreshing the access token.")
data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
@ -64,10 +71,13 @@ class AuthorizationHelpers:
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
@staticmethod
## Parse the token response from the authorization server into an AuthenticationResponse object.
# \param token_response: The JSON string data response from the authorization server.
# \return An AuthenticationResponse object.
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
"""Parse the token response from the authorization server into an AuthenticationResponse object.
:param token_response: The JSON string data response from the authorization server.
:return: An AuthenticationResponse object.
"""
token_data = None
try:
@ -89,10 +99,13 @@ class AuthorizationHelpers:
scope=token_data["scope"],
received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
## Calls the authentication API endpoint to get the token data.
# \param access_token: The encoded JWT token.
# \return Dict containing some profile data.
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
"""Calls the authentication API endpoint to get the token data.
:param access_token: The encoded JWT token.
:return: Dict containing some profile data.
"""
try:
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
"Authorization": "Bearer {}".format(access_token)
@ -115,16 +128,22 @@ class AuthorizationHelpers:
)
@staticmethod
## Generate a verification code of arbitrary length.
# \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to
# leave it at 32
def generateVerificationCode(code_length: int = 32) -> str:
"""Generate a verification code of arbitrary length.
:param code_length:: How long should the code be? This should never be lower than 16, but it's probably
better to leave it at 32
"""
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
@staticmethod
## Generates a base64 encoded sha512 encrypted version of a given string.
# \param verification_code:
# \return The encrypted code in base64 format.
def generateVerificationCodeChallenge(verification_code: str) -> str:
"""Generates a base64 encoded sha512 encrypted version of a given string.
:param verification_code:
:return: The encrypted code in base64 format.
"""
encoded = sha512(verification_code.encode()).digest()
return b64encode(encoded, altchars = b"_-").decode()

View file

@ -14,9 +14,12 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura")
## This handler handles all HTTP requests on the local web server.
# It also requests the access token for the 2nd stage of the OAuth flow.
class AuthorizationRequestHandler(BaseHTTPRequestHandler):
"""This handler handles all HTTP requests on the local web server.
It also requests the access token for the 2nd stage of the OAuth flow.
"""
def __init__(self, request, client_address, server) -> None:
super().__init__(request, client_address, server)
@ -55,10 +58,13 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
# This will cause the server to shut down, so we do it at the very end of the request handling.
self.authorization_callback(token_response)
## Handler for the callback URL redirect.
# \param query Dict containing the HTTP query parameters.
# \return HTTP ResponseData containing a success page to show to the user.
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
"""Handler for the callback URL redirect.
:param query: Dict containing the HTTP query parameters.
:return: HTTP ResponseData containing a success page to show to the user.
"""
code = self._queryGet(query, "code")
state = self._queryGet(query, "state")
if state != self.state:
@ -95,9 +101,10 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
), token_response
## Handle all other non-existing server calls.
@staticmethod
def _handleNotFound() -> ResponseData:
"""Handle all other non-existing server calls."""
return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.")
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
@ -110,7 +117,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
def _sendData(self, data: bytes) -> None:
self.wfile.write(data)
## Convenience helper for getting values from a pre-parsed query string
@staticmethod
def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str] = None) -> Optional[str]:
"""Convenience helper for getting values from a pre-parsed query string"""
return query_data.get(key, [default])[0]

View file

@ -9,21 +9,26 @@ if TYPE_CHECKING:
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
## The authorization request callback handler server.
# This subclass is needed to be able to pass some data to the request handler.
# This cannot be done on the request handler directly as the HTTPServer
# creates an instance of the handler after init.
class AuthorizationRequestServer(HTTPServer):
## Set the authorization helpers instance on the request handler.
"""The authorization request callback handler server.
This subclass is needed to be able to pass some data to the request handler. This cannot be done on the request
handler directly as the HTTPServer creates an instance of the handler after init.
"""
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
"""Set the authorization helpers instance on the request handler."""
self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore
## Set the authorization callback on the request handler.
def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None:
"""Set the authorization callback on the request handler."""
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
## Set the verification code on the request handler.
def setVerificationCode(self, verification_code: str) -> None:
"""Set the verification code on the request handler."""
self.RequestHandlerClass.verification_code = verification_code # type: ignore
def setState(self, state: str) -> None:

View file

@ -25,9 +25,11 @@ if TYPE_CHECKING:
from UM.Preferences import Preferences
## The authorization service is responsible for handling the login flow,
# storing user credentials and providing account information.
class AuthorizationService:
"""The authorization service is responsible for handling the login flow, storing user credentials and providing
account information.
"""
# Emit signal when authentication is completed.
onAuthStateChanged = Signal()
@ -59,11 +61,16 @@ class AuthorizationService:
if self._preferences:
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
## Get the user profile as obtained from the JWT (JSON Web Token).
# If the JWT is not yet parsed, calling this will take care of that.
# \return UserProfile if a user is logged in, None otherwise.
# \sa _parseJWT
def getUserProfile(self) -> Optional["UserProfile"]:
"""Get the user profile as obtained from the JWT (JSON Web Token).
If the JWT is not yet parsed, calling this will take care of that.
:return: UserProfile if a user is logged in, None otherwise.
See also: :py:method:`cura.OAuth2.AuthorizationService.AuthorizationService._parseJWT`
"""
if not self._user_profile:
# If no user profile was stored locally, we try to get it from JWT.
try:
@ -81,9 +88,12 @@ class AuthorizationService:
return self._user_profile
## Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
# \return UserProfile if it was able to parse, None otherwise.
def _parseJWT(self) -> Optional["UserProfile"]:
"""Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
:return: UserProfile if it was able to parse, None otherwise.
"""
if not self._auth_data or self._auth_data.access_token is None:
# If no auth data exists, we should always log in again.
Logger.log("d", "There was no auth data or access token")
@ -106,8 +116,9 @@ class AuthorizationService:
self._storeAuthData(self._auth_data)
return self._auth_helpers.parseJWT(self._auth_data.access_token)
## Get the access token as provided by the repsonse data.
def getAccessToken(self) -> Optional[str]:
"""Get the access token as provided by the repsonse data."""
if self._auth_data is None:
Logger.log("d", "No auth data to retrieve the access_token from")
return None
@ -122,8 +133,9 @@ class AuthorizationService:
return self._auth_data.access_token if self._auth_data else None
## Try to refresh the access token. This should be used when it has expired.
def refreshAccessToken(self) -> None:
"""Try to refresh the access token. This should be used when it has expired."""
if self._auth_data is None or self._auth_data.refresh_token is None:
Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
return
@ -135,14 +147,16 @@ class AuthorizationService:
Logger.log("w", "Failed to get a new access token from the server.")
self.onAuthStateChanged.emit(logged_in = False)
## Delete the authentication data that we have stored locally (eg; logout)
def deleteAuthData(self) -> None:
"""Delete the authentication data that we have stored locally (eg; logout)"""
if self._auth_data is not None:
self._storeAuthData()
self.onAuthStateChanged.emit(logged_in = False)
## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
def startAuthorizationFlow(self) -> None:
"""Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login."""
Logger.log("d", "Starting new OAuth2 flow...")
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
@ -177,8 +191,9 @@ class AuthorizationService:
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
## Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
"""Callback method for the authentication flow."""
if auth_response.success:
self._storeAuthData(auth_response)
self.onAuthStateChanged.emit(logged_in = True)
@ -186,8 +201,9 @@ class AuthorizationService:
self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message)
self._server.stop() # Stop the web server at all times.
## Load authentication data from preferences.
def loadAuthDataFromPreferences(self) -> None:
"""Load authentication data from preferences."""
if self._preferences is None:
Logger.log("e", "Unable to load authentication data, since no preference has been set!")
return
@ -208,8 +224,9 @@ class AuthorizationService:
except ValueError:
Logger.logException("w", "Could not load auth data from preferences")
## Store authentication data in preferences.
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
"""Store authentication data in preferences."""
Logger.log("d", "Attempting to store the auth data")
if self._preferences is None:
Logger.log("e", "Unable to save authentication data, since no preference has been set!")

View file

@ -20,18 +20,23 @@ if TYPE_CHECKING:
class LocalAuthorizationServer:
## The local LocalAuthorizationServer takes care of the oauth2 callbacks.
# Once the flow is completed, this server should be closed down again by
# calling stop()
# \param auth_helpers An instance of the authorization helpers class.
# \param auth_state_changed_callback A callback function to be called when
# the authorization state changes.
# \param daemon Whether the server thread should be run in daemon mode.
# Note: Daemon threads are abruptly stopped at shutdown. Their resources
# (e.g. open files) may never be released.
def __init__(self, auth_helpers: "AuthorizationHelpers",
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
daemon: bool) -> None:
"""The local LocalAuthorizationServer takes care of the oauth2 callbacks.
Once the flow is completed, this server should be closed down again by calling
:py:meth:`cura.OAuth2.LocalAuthorizationServer.LocalAuthorizationServer.stop()`
:param auth_helpers: An instance of the authorization helpers class.
:param auth_state_changed_callback: A callback function to be called when the authorization state changes.
:param daemon: Whether the server thread should be run in daemon mode.
.. note::
Daemon threads are abruptly stopped at shutdown. Their resources (e.g. open files) may never be released.
"""
self._web_server = None # type: Optional[AuthorizationRequestServer]
self._web_server_thread = None # type: Optional[threading.Thread]
self._web_server_port = auth_helpers.settings.CALLBACK_PORT
@ -39,10 +44,13 @@ class LocalAuthorizationServer:
self._auth_state_changed_callback = auth_state_changed_callback
self._daemon = daemon
## Starts the local web server to handle the authorization callback.
# \param verification_code The verification code part of the OAuth2 client identification.
# \param state The unique state code (to ensure that the request we get back is really from the server.
def start(self, verification_code: str, state: str) -> None:
"""Starts the local web server to handle the authorization callback.
:param verification_code: The verification code part of the OAuth2 client identification.
:param state: The unique state code (to ensure that the request we get back is really from the server.
"""
if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
# We still inject the new verification code though.
@ -66,8 +74,9 @@ class LocalAuthorizationServer:
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
self._web_server_thread.start()
## Stops the web server if it was running. It also does some cleanup.
def stop(self) -> None:
"""Stops the web server if it was running. It also does some cleanup."""
Logger.log("d", "Stopping local oauth2 web server...")
if self._web_server:

View file

@ -8,8 +8,9 @@ class BaseModel:
self.__dict__.update(kwargs)
## OAuth OAuth2Settings data template.
class OAuth2Settings(BaseModel):
"""OAuth OAuth2Settings data template."""
CALLBACK_PORT = None # type: Optional[int]
OAUTH_SERVER_URL = None # type: Optional[str]
CLIENT_ID = None # type: Optional[str]
@ -20,16 +21,18 @@ class OAuth2Settings(BaseModel):
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
## User profile data template.
class UserProfile(BaseModel):
"""User profile data template."""
user_id = None # type: Optional[str]
username = None # type: Optional[str]
profile_image_url = None # type: Optional[str]
## Authentication data template.
class AuthenticationResponse(BaseModel):
"""Data comes from the token response with success flag and error message added."""
"""Authentication data template."""
# 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]
@ -40,22 +43,25 @@ class AuthenticationResponse(BaseModel):
received_at = None # type: Optional[str]
## Response status template.
class ResponseStatus(BaseModel):
"""Response status template."""
code = 200 # type: int
message = "" # type: str
## Response data template.
class ResponseData(BaseModel):
"""Response data template."""
status = None # type: ResponseStatus
data_stream = None # type: Optional[bytes]
redirect_uri = None # type: Optional[str]
content_type = "text/html" # type: str
## Possible HTTP responses.
HTTP_STATUS = {
"""Possible HTTP responses."""
"OK": ResponseStatus(code = 200, message = "OK"),
"NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),
"REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")