Updated Blender ID add-on to 1.3.0
This commit is contained in:
parent
6de5ea8376
commit
87b9e91c0c
|
@ -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/).
|
|
@ -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'}
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue