Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSybren A. Stüvel <sybren@stuvel.eu>2017-06-14 16:10:55 +0300
committerSybren A. Stüvel <sybren@stuvel.eu>2017-06-14 16:23:47 +0300
commit87b9e91c0c8843429ef6712f9c96f423b29d7457 (patch)
treeb4f147094a60e77eb842c3d4ba340fb0fa50daa0
parent6de5ea83768e131421d52f2a2de13bf012e295a2 (diff)
Updated Blender ID add-on to 1.3.0
-rw-r--r--blender_id/CHANGELOG.md16
-rw-r--r--blender_id/__init__.py106
-rw-r--r--blender_id/communication.py65
-rw-r--r--blender_id/profiles.py40
4 files changed, 170 insertions, 57 deletions
diff --git a/blender_id/CHANGELOG.md b/blender_id/CHANGELOG.md
new file mode 100644
index 00000000..201ee1d0
--- /dev/null
+++ b/blender_id/CHANGELOG.md
@@ -0,0 +1,16 @@
+# Blender ID Add-on Changelog
+
+
+## Version 1.3 (released 2017-06-14)
+
+- Show a message after logging out.
+- Store token expiry date in profile JSON.
+- Show "validate" button when the token expiration is unknown.
+- Urge the user to log out & back in again to refresh the auth token if it expires within 2 weeks.
+- Added a method `validate_token()` to the public Blender ID Add-on API.
+
+
+## Older versions
+
+The history of older versions can be found in the
+[Blender ID Add-on Git repository](https://developer.blender.org/diffusion/BIA/).
diff --git a/blender_id/__init__.py b/blender_id/__init__.py
index ae24af86..a7094c7f 100644
--- a/blender_id/__init__.py
+++ b/blender_id/__init__.py
@@ -21,7 +21,7 @@
bl_info = {
'name': 'Blender ID authentication',
'author': 'Francesco Siddi, Inês Almeida and Sybren A. Stüvel',
- 'version': (1, 2, 0),
+ 'version': (1, 3, 0),
'blender': (2, 77, 0),
'location': 'Add-on preferences',
'description':
@@ -32,6 +32,9 @@ bl_info = {
'support': 'OFFICIAL',
}
+import datetime
+import typing
+
import bpy
from bpy.types import AddonPreferences, Operator, PropertyGroup
from bpy.props import PointerProperty, StringProperty
@@ -123,6 +126,53 @@ def get_subclient_user_id(subclient_id: str) -> str:
return BlenderIdProfile.subclients[subclient_id]['subclient_user_id']
+def validate_token() -> typing.Optional[str]:
+ """Validates the current user's token with Blender ID.
+
+ Also refreshes the stored token expiry time.
+
+ :returns: None if everything was ok, otherwise returns an error message.
+ """
+
+ expires, err = communication.blender_id_server_validate(token=BlenderIdProfile.token)
+ if err is not None:
+ return err
+
+ BlenderIdProfile.expires = expires
+ BlenderIdProfile.save_json()
+
+ return None
+
+
+def token_expires() -> typing.Optional[datetime.datetime]:
+ """Returns the token expiry timestamp.
+
+ Returns None if the token expiry is unknown. This can happen when
+ the last login/validation was performed using a version of this
+ add-on that was older than 1.3.
+ """
+
+ exp = BlenderIdProfile.expires
+ if not exp:
+ return None
+
+ # Try parsing as different formats. A new Blender ID is coming,
+ # which may change the format in which timestamps are sent.
+ formats = [
+ '%Y-%m-%dT%H:%M:%S.%fZ', # ISO 8601 with Z-suffix, used by new Blender ID
+ '%a, %d %b %Y %H:%M:%S GMT', # RFC 1123, used by current Blender ID
+ ]
+ for fmt in formats:
+ try:
+ return datetime.datetime.strptime(exp, fmt)
+ except ValueError:
+ # Just use the next format string and try again.
+ pass
+
+ # Unable to parse, may as well not be there then.
+ return None
+
+
class BlenderIdPreferences(AddonPreferences):
bl_idname = __name__
@@ -165,11 +215,41 @@ class BlenderIdPreferences(AddonPreferences):
active_profile = get_active_profile()
if active_profile:
- text = 'You are logged in as {0}'.format(active_profile.username)
- layout.label(text=text, icon='WORLD_DATA')
+ expiry = token_expires()
+ now = datetime.datetime.utcnow()
+ show_validate_button = bpy.app.debug
+
+ if expiry is None:
+ layout.label(text='We do not know when your token expires, please validate it.')
+ show_validate_button = True
+ elif now >= expiry:
+ layout.label(text='Your login has expired! Log out and log in again to refresh it.',
+ icon='ERROR')
+ else:
+ time_left = expiry - now
+ if time_left.days > 14:
+ exp_str = 'on {:%Y-%m-%d}'.format(expiry)
+ elif time_left.days > 1:
+ exp_str = 'in %i days.' % time_left.days
+ elif time_left.seconds >= 7200:
+ exp_str = 'in %i hours.' % round(time_left.seconds / 3600)
+ elif time_left.seconds >= 120:
+ exp_str = 'in %i minutes.' % round(time_left.seconds / 60)
+ else:
+ exp_str = 'within seconds'
+
+ if time_left.days < 14:
+ layout.label('You are logged in as %s.' % active_profile.username,
+ icon='WORLD_DATA')
+ layout.label(text='Your token will expire %s. Please log out and log in again '
+ 'to refresh it.' % exp_str, icon='PREVIEW_RANGE')
+ else:
+ layout.label('You are logged in as %s. Your authentication token expires %s.'
+ % (active_profile.username, exp_str), icon='WORLD_DATA')
+
row = layout.row()
row.operator('blender_id.logout')
- if bpy.app.debug:
+ if show_validate_button:
row.operator('blender_id.validate')
else:
layout.prop(self, 'blender_id_username')
@@ -196,12 +276,12 @@ class BlenderIdLogin(BlenderIdMixin, Operator):
addon_prefs = self.addon_prefs(context)
- resp = communication.blender_id_server_authenticate(
+ auth_result = communication.blender_id_server_authenticate(
username=addon_prefs.blender_id_username,
password=addon_prefs.blender_id_password
)
- if resp['status'] == 'success':
+ if auth_result.success:
# Prevent saving the password in user preferences. Overwrite the password with a
# random string, as just setting to '' might only replace the first byte with 0.
pwlen = len(addon_prefs.blender_id_password)
@@ -211,14 +291,13 @@ class BlenderIdLogin(BlenderIdMixin, Operator):
addon_prefs.blender_id_password = ''
profiles.save_as_active_profile(
- resp['user_id'],
- resp['token'],
+ auth_result,
addon_prefs.blender_id_username,
{}
)
addon_prefs.ok_message = 'Logged in'
else:
- addon_prefs.error_message = resp['error_message']
+ addon_prefs.error_message = auth_result.error_message
if BlenderIdProfile.user_id:
profiles.logout(BlenderIdProfile.user_id)
@@ -234,11 +313,11 @@ class BlenderIdValidate(BlenderIdMixin, Operator):
def execute(self, context):
addon_prefs = self.addon_prefs(context)
- resp = communication.blender_id_server_validate(token=BlenderIdProfile.token)
- if resp is None:
+ err = validate_token()
+ if err is None:
addon_prefs.ok_message = 'Authentication token is valid.'
else:
- addon_prefs.error_message = '%s; you probably want to log out and log in again.' % resp
+ addon_prefs.error_message = '%s; you probably want to log out and log in again.' % err
BlenderIdProfile.read_json()
@@ -250,12 +329,15 @@ class BlenderIdLogout(BlenderIdMixin, Operator):
bl_label = 'Logout'
def execute(self, context):
+ addon_prefs = self.addon_prefs(context)
+
communication.blender_id_server_logout(BlenderIdProfile.user_id,
BlenderIdProfile.token)
profiles.logout(BlenderIdProfile.user_id)
BlenderIdProfile.read_json()
+ addon_prefs.ok_message = 'You have been logged out.'
return {'FINISHED'}
diff --git a/blender_id/communication.py b/blender_id/communication.py
index ee71c553..90ccf9a1 100644
--- a/blender_id/communication.py
+++ b/blender_id/communication.py
@@ -19,12 +19,24 @@
# <pep8 compliant>
import functools
+import typing
class BlenderIdCommError(RuntimeError):
"""Raised when there was an error communicating with Blender ID"""
+class AuthResult:
+ def __init__(self, *, success: bool,
+ user_id: str=None, token: str=None, expires: str=None,
+ error_message: typing.Any=None): # when success=False
+ self.success = success
+ self.user_id = user_id
+ self.token = token
+ self.error_message = str(error_message)
+ self.expires = expires
+
+
@functools.lru_cache(maxsize=None)
def host_label():
import socket
@@ -46,7 +58,7 @@ def blender_id_endpoint(endpoint_path=None):
return urllib.parse.urljoin(base_url, endpoint_path)
-def blender_id_server_authenticate(username, password):
+def blender_id_server_authenticate(username, password) -> AuthResult:
"""Authenticate the user with the server with a single transaction
containing username and password (must happen via HTTPS).
@@ -73,45 +85,33 @@ def blender_id_server_authenticate(username, password):
requests.exceptions.HTTPError,
requests.exceptions.ConnectionError) as e:
print('Exception POSTing to {}: {}'.format(url, e))
- return dict(
- status='fail',
- user_id=None,
- token=None,
- error_message=str(e)
- )
-
- user_id = None
- token = None
- error_message = None
+ return AuthResult(status, error_message=e)
if r.status_code == 200:
resp = r.json()
status = resp['status']
if status == 'success':
- user_id = str(resp['data']['user_id'])
- # We just use the access token for now.
- token = resp['data']['oauth_token']['access_token']
- elif status == 'fail':
- error_message = 'Username and/or password is incorrect'
- else:
- status = 'fail'
- error_message = format('There was a problem communicating with'
- ' the server. Error code is: %s' % r.status_code)
+ return AuthResult(success=True,
+ user_id=str(resp['data']['user_id']),
+ token=resp['data']['oauth_token']['access_token'],
+ expires=resp['data']['oauth_token']['expires'],
+ )
+ if status == 'fail':
+ return AuthResult(success=False, error_message='Username and/or password is incorrect')
- return dict(
- status=status,
- user_id=user_id,
- token=token,
- error_message=error_message
- )
+ return AuthResult(success=False,
+ error_message='There was a problem communicating with'
+ ' the server. Error code is: %s' % r.status_code)
-def blender_id_server_validate(token):
+def blender_id_server_validate(token) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:
"""Validate the auth token with the server.
@param token: the authentication token
@type token: str
- @returns: None if the token is valid, or an error message when it's invalid.
+ @returns: tuple (expiry, error).
+ The expiry is the expiry date of the token if it is valid, else None.
+ The error is None if the token is valid, or an error message when it's invalid.
"""
import requests
@@ -121,12 +121,13 @@ def blender_id_server_validate(token):
r = requests.post(blender_id_endpoint('u/validate_token'),
data={'token': token}, verify=True)
except requests.exceptions.RequestException as e:
- return str(e)
+ return (str(e), None)
- if r.status_code == 200:
- return None
+ if r.status_code != 200:
+ return (None, 'Authentication token invalid')
- return 'Authentication token invalid'
+ response = r.json()
+ return (response['token_expires'], None)
def blender_id_server_logout(user_id, token):
diff --git a/blender_id/profiles.py b/blender_id/profiles.py
index 7dd6e121..2e872a50 100644
--- a/blender_id/profiles.py
+++ b/blender_id/profiles.py
@@ -21,6 +21,8 @@
import os
import bpy
+from . import communication
+
# Set/created upon register.
profiles_path = ''
profiles_file = ''
@@ -44,23 +46,32 @@ class BlenderIdProfile(metaclass=_BIPMeta):
user_id = ''
username = ''
token = ''
+ expires = ''
subclients = {}
@classmethod
+ def reset(cls):
+ cls.user_id = ''
+ cls.username = ''
+ cls.token = ''
+ cls.expires = ''
+ cls.subclients = {}
+
+ @classmethod
def read_json(cls):
"""Updates the active profile information from the JSON file."""
+ cls.reset()
+
active_profile = get_active_profile()
- if active_profile:
- cls.user_id = active_profile['user_id']
- cls.username = active_profile['username']
- cls.token = active_profile['token']
- cls.subclients = active_profile.get('subclients', {})
- else:
- cls.user_id = ''
- cls.username = ''
- cls.token = ''
- cls.subclients = {} # mapping from subclient-ID to user info dict.
+ if not active_profile:
+ return
+
+ for key, value in active_profile.items():
+ if hasattr(cls, key):
+ setattr(cls, key, value)
+ else:
+ print('Skipping key %r from profile JSON' % key)
@classmethod
def save_json(cls, make_active_profile=False):
@@ -70,6 +81,7 @@ class BlenderIdProfile(metaclass=_BIPMeta):
jsonfile['profiles'][cls.user_id] = {
'username': cls.username,
'token': cls.token,
+ 'expires': cls.expires,
'subclients': cls.subclients,
}
@@ -184,11 +196,13 @@ def save_profiles_data(all_profiles: dict):
json.dump(all_profiles, outfile, sort_keys=True)
-def save_as_active_profile(user_id, token, username, subclients):
+def save_as_active_profile(auth_result: communication.AuthResult, username, subclients):
"""Saves the given info as the active profile."""
- BlenderIdProfile.user_id = user_id
- BlenderIdProfile.token = token
+ BlenderIdProfile.user_id = auth_result.user_id
+ BlenderIdProfile.token = auth_result.token
+ BlenderIdProfile.expires = auth_result.expires
+
BlenderIdProfile.username = username
BlenderIdProfile.subclients = subclients