SeAT service cleanup (#796)

Manager function tidyup
Hopefully improved the key sync function, at least it should be easier to follow whats happening now.
Remove partial logging of unhashed passwords
Added user feedback
This commit is contained in:
Basraah 2017-05-05 00:31:57 +10:00 committed by Adarnof
parent 4556a0e740
commit aec013b93c
2 changed files with 133 additions and 91 deletions

View File

@ -2,9 +2,11 @@ from __future__ import unicode_literals
import random import random
import string import string
import requests import requests
import hashlib
from eveonline.managers import EveManager from eveonline.managers import EveManager
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.cache import cache
from six import iteritems from six import iteritems
@ -20,7 +22,7 @@ class SeatManager:
RESPONSE_OK = 'ok' RESPONSE_OK = 'ok'
@staticmethod @staticmethod
def __santatize_username(username): def __sanitize_username(username):
sanatized = username.replace(" ", "_") sanatized = username.replace(" ", "_")
return sanatized.lower() return sanatized.lower()
@ -33,7 +35,7 @@ class SeatManager:
return cls.RESPONSE_OK in response return cls.RESPONSE_OK in response
@staticmethod @staticmethod
def exec_request(endpoint, func, **kwargs): def exec_request(endpoint, func, raise_for_status=False, **kwargs):
""" Send an https api request """ """ Send an https api request """
try: try:
endpoint = '{0}/api/v1/{1}'.format(settings.SEAT_URL, endpoint) endpoint = '{0}/api/v1/{1}'.format(settings.SEAT_URL, endpoint)
@ -43,22 +45,24 @@ class SeatManager:
ret = getattr(requests, func)(endpoint, headers=headers, data=kwargs) ret = getattr(requests, func)(endpoint, headers=headers, data=kwargs)
ret.raise_for_status() ret.raise_for_status()
return ret.json() return ret.json()
except: except requests.HTTPError as e:
if raise_for_status:
raise e
logger.exception("Error encountered while performing API request to SeAT with url {}".format(endpoint)) logger.exception("Error encountered while performing API request to SeAT with url {}".format(endpoint))
return {} return {}
@classmethod @classmethod
def add_user(cls, username, email): def add_user(cls, username, email):
""" Add user to service """ """ Add user to service """
sanatized = str(SeatManager.__santatize_username(username)) sanitized = str(cls.__sanitize_username(username))
logger.debug("Adding user to SeAT with username %s" % sanatized) logger.debug("Adding user to SeAT with username %s" % sanitized)
password = SeatManager.__generate_random_pass() password = cls.__generate_random_pass()
ret = SeatManager.exec_request('user', 'post', username=sanatized, email=str(email), password=password) ret = cls.exec_request('user', 'post', username=sanitized, email=str(email), password=password)
logger.debug(ret) logger.debug(ret)
if cls._response_ok(ret): if cls._response_ok(ret):
logger.info("Added SeAT user with username %s" % sanatized) logger.info("Added SeAT user with username %s" % sanitized)
return sanatized, password return sanitized, password
logger.info("Failed to add SeAT user with username %s" % sanatized) logger.info("Failed to add SeAT user with username %s" % sanitized)
return None return None
@classmethod @classmethod
@ -93,7 +97,7 @@ class SeatManager:
@classmethod @classmethod
def enable_user(cls, username): def enable_user(cls, username):
""" Enable user """ """ Enable user """
ret = SeatManager.exec_request('user/{}'.format(username), 'put', active=1) ret = cls.exec_request('user/{}'.format(username), 'put', active=1)
logger.debug(ret) logger.debug(ret)
if cls._response_ok(ret): if cls._response_ok(ret):
logger.info("Enabled SeAT user with username %s" % username) logger.info("Enabled SeAT user with username %s" % username)
@ -104,13 +108,12 @@ class SeatManager:
@classmethod @classmethod
def update_user(cls, username, email, password): def update_user(cls, username, email, password):
""" Edit user info """ """ Edit user info """
logger.debug("Updating SeAT username %s with email %s and password hash starting with %s" % (username, email, logger.debug("Updating SeAT username %s with email %s and password" % (username, email))
password[0:5])) ret = cls.exec_request('user/{}'.format(username), 'put', email=email)
ret = SeatManager.exec_request('user/{}'.format(username), 'put', email=email)
logger.debug(ret) logger.debug(ret)
if not cls._response_ok(ret): if not cls._response_ok(ret):
logger.warn("Failed to update email for username {}".format(username)) logger.warn("Failed to update email for username {}".format(username))
ret = SeatManager.exec_request('user/{}'.format(username), 'put', password=password) ret = cls.exec_request('user/{}'.format(username), 'put', password=password)
logger.debug(ret) logger.debug(ret)
if not cls._response_ok(ret): if not cls._response_ok(ret):
logger.warn("Failed to update password for username {}".format(username)) logger.warn("Failed to update password for username {}".format(username))
@ -118,25 +121,25 @@ class SeatManager:
logger.info("Updated SeAT user with username %s" % username) logger.info("Updated SeAT user with username %s" % username)
return username return username
@staticmethod @classmethod
def update_user_password(username, email, plain_password=None): def update_user_password(cls, username, email, plain_password=None):
logger.debug("Settings new SeAT password for user %s" % username) logger.debug("Settings new SeAT password for user %s" % username)
if not plain_password: if not plain_password:
plain_password = SeatManager.__generate_random_pass() plain_password = cls.__generate_random_pass()
if SeatManager.update_user(username, email, plain_password): if cls.update_user(username, email, plain_password):
return plain_password return plain_password
@staticmethod @classmethod
def check_user_status(username): def check_user_status(cls, username):
sanatized = str(SeatManager.__santatize_username(username)) sanitized = str(cls.__sanitize_username(username))
logger.debug("Checking SeAT status for user %s" % sanatized) logger.debug("Checking SeAT status for user %s" % sanitized)
ret = SeatManager.exec_request('user/{}'.format(sanatized), 'get') ret = cls.exec_request('user/{}'.format(sanitized), 'get')
logger.debug(ret) logger.debug(ret)
return ret return ret
@staticmethod @classmethod
def get_all_seat_eveapis(): def get_all_seat_eveapis(cls):
seat_all_keys = SeatManager.exec_request('key', 'get') seat_all_keys = cls.exec_request('key', 'get')
seat_keys = {} seat_keys = {}
for key in seat_all_keys: for key in seat_all_keys:
try: try:
@ -145,117 +148,132 @@ class SeatManager:
seat_keys[key["key_id"]] = None seat_keys[key["key_id"]] = None
return seat_keys return seat_keys
@classmethod
def synchronize_eveapis(cls, user=None):
# Fetch all of the API keys stored in SeAT already
seat_all_keys = cls.get_all_seat_eveapis()
@staticmethod
def synchronize_eveapis(user=None):
seat_all_keys = SeatManager.get_all_seat_eveapis()
userinfo = None
# retrieve only user-specific api keys if user is specified # retrieve only user-specific api keys if user is specified
if user: if user:
keypars = EveManager.get_api_key_pairs(user) keypairs = EveManager.get_api_key_pairs(user)
try:
userinfo = SeatManager.check_user_status(user.seat.username)
except ObjectDoesNotExist:
pass
else: else:
# retrieve all api keys instead # retrieve all api keys instead
keypars = EveManager.get_all_api_key_pairs() keypairs = EveManager.get_all_api_key_pairs()
if keypars:
for keypar in keypars: for keypair in keypairs:
if keypar.api_id not in seat_all_keys.keys(): # Transfer the key if it isn't already in SeAT
if keypair.api_id not in seat_all_keys.keys():
# Add new keys # Add new keys
logger.debug("Adding Api Key with ID %s" % keypar.api_id) logger.debug("Adding Api Key with ID %s" % keypair.api_id)
ret = SeatManager.exec_request('key', 'post', key_id=keypar.api_id, v_code=keypar.api_key) try:
ret = cls.exec_request('key', 'post',
key_id=keypair.api_id,
v_code=keypair.api_key,
raise_for_status=True)
logger.debug(ret) logger.debug(ret)
except requests.HTTPError as e:
if e.response.status_code == 400:
logger.debug("API key already exists")
else:
logger.exception("API key sync failed")
continue # Skip the rest of the key processing
else: else:
# remove it from the list so it doesn't get deleted in the last step # remove it from the list so it doesn't get deleted in the last step
seat_all_keys.pop(keypar.api_id) seat_all_keys.pop(keypair.api_id)
if not userinfo: # TODO: should the following be done only for new keys?
# Check the key's user status # Attach API key to the users SeAT account, if possible
logger.debug("Retrieving user name from Auth's SeAT users database")
try: try:
if keypar.user.seat.username: userinfo = cache.get_or_set('seat_user_status_' + cls.username_hash(keypair.user.seat.username),
logger.debug("Retrieving user %s info from SeAT users database" % keypar.user.seat.username) lambda: cls.check_user_status(keypair.user.seat.username),
userinfo = SeatManager.check_user_status(keypar.user.seat.username) 300) # Cache for 5 minutes
except ObjectDoesNotExist:
pass if not bool(userinfo):
if userinfo: # No SeAT account, skip
try: logger.debug("Could not find users SeAT id, cannot assign key to them")
# If the user has activated seat, assign the key to him. continue
# If the user has activated seat, assign the key to them
logger.debug("Transferring Api Key with ID %s to user %s with ID %s " % ( logger.debug("Transferring Api Key with ID %s to user %s with ID %s " % (
keypar.api_id, keypair.api_id,
keypar.user.seat.username, keypair.user.seat.username,
userinfo['id'])) userinfo['id']))
ret = SeatManager.exec_request('key/transfer/{}/{}'.format(keypar.api_id, userinfo['id']), ret = cls.exec_request('key/transfer/{}/{}'.format(keypair.api_id, userinfo['id']),
'get') 'get')
logger.debug(ret) logger.debug(ret)
except ObjectDoesNotExist: except ObjectDoesNotExist:
logger.debug("User does not have SeAT activated, could not assign key to user") logger.debug("User does not have SeAT activated, could not assign key to user")
if bool(seat_all_keys) and not user and hasattr(settings, 'SEAT_PURGE_DELETED') and settings.SEAT_PURGE_DELETED: if bool(seat_all_keys) and not user and getattr(settings, 'SEAT_PURGE_DELETED', False):
# remove from SeAT keys that were removed from Auth # remove from SeAT keys that were removed from Auth
for key, key_user in iteritems(seat_all_keys): for key, key_user in iteritems(seat_all_keys):
# Remove the key only if it is an account or character key # Remove the key only if it is an account or character key
ret = SeatManager.exec_request('key/{}'.format(key), 'get') ret = cls.exec_request('key/{}'.format(key), 'get')
logger.debug(ret) logger.debug(ret)
try: try:
if (ret['info']['type'] == "Account") or (ret['info']['type'] == "Character"): if (ret['info']['type'] == "Account") or (ret['info']['type'] == "Character"):
logger.debug("Removing api key %s from SeAT database" % key) logger.debug("Removing api key %s from SeAT database" % key)
ret = SeatManager.exec_request('key/{}'.format(key), 'delete') ret = cls.exec_request('key/{}'.format(key), 'delete')
logger.debug(ret) logger.debug(ret)
except KeyError: except KeyError:
pass pass
@staticmethod @classmethod
def get_all_roles(): def get_all_roles(cls):
groups = {} groups = {}
ret = SeatManager.exec_request('role', 'get') ret = cls.exec_request('role', 'get')
logger.debug(ret) logger.debug(ret)
for group in ret: for group in ret:
groups[group["title"]] = group["id"] groups[group["title"]] = group["id"]
logger.debug("Retrieved role list from SeAT: %s" % str(groups)) logger.debug("Retrieved role list from SeAT: %s" % str(groups))
return groups return groups
@staticmethod @classmethod
def add_role(role): def add_role(cls, role):
ret = SeatManager.exec_request('role/new', 'post', name=role) ret = cls.exec_request('role/new', 'post', name=role)
logger.debug(ret) logger.debug(ret)
logger.info("Added Seat group %s" % role) logger.info("Added Seat group %s" % role)
role_info = SeatManager.exec_request('role/detail/{}'.format(role), 'get') role_info = cls.exec_request('role/detail/{}'.format(role), 'get')
logger.debug(role_info) logger.debug(role_info)
return role_info["id"] return role_info["id"]
@staticmethod @classmethod
def add_role_to_user(user_id, role_id): def add_role_to_user(cls, user_id, role_id):
ret = SeatManager.exec_request('role/grant-user-role/{}/{}'.format(user_id, role_id), 'get') ret = cls.exec_request('role/grant-user-role/{}/{}'.format(user_id, role_id), 'get')
logger.info("Added role %s to user %s" % (role_id, user_id)) logger.info("Added role %s to user %s" % (role_id, user_id))
return ret return ret
@staticmethod @classmethod
def revoke_role_from_user(user_id, role_id): def revoke_role_from_user(cls, user_id, role_id):
ret = SeatManager.exec_request('role/revoke-user-role/{}/{}'.format(user_id, role_id), 'get') ret = cls.exec_request('role/revoke-user-role/{}/{}'.format(user_id, role_id), 'get')
logger.info("Revoked role %s from user %s" % (role_id, user_id)) logger.info("Revoked role %s from user %s" % (role_id, user_id))
return ret return ret
@staticmethod @classmethod
def update_roles(seat_user, roles): def update_roles(cls, seat_user, roles):
logger.debug("Updating SeAT user %s with roles %s" % (seat_user, roles)) logger.debug("Updating SeAT user %s with roles %s" % (seat_user, roles))
user_info = SeatManager.check_user_status(seat_user) user_info = cls.check_user_status(seat_user)
user_roles = {} user_roles = {}
if type(user_info["roles"]) is list: if type(user_info["roles"]) is list:
for role in user_info["roles"]: for role in user_info["roles"]:
user_roles[role["title"]] = role["id"] user_roles[role["title"]] = role["id"]
logger.debug("Got user %s SeAT roles %s" % (seat_user, user_roles)) logger.debug("Got user %s SeAT roles %s" % (seat_user, user_roles))
seat_roles = SeatManager.get_all_roles() seat_roles = cls.get_all_roles()
addroles = set(roles) - set(user_roles.keys()) addroles = set(roles) - set(user_roles.keys())
remroles = set(user_roles.keys()) - set(roles) remroles = set(user_roles.keys()) - set(roles)
logger.info("Updating SeAT roles for user %s - adding %s, removing %s" % (seat_user, addroles, remroles)) logger.info("Updating SeAT roles for user %s - adding %s, removing %s" % (seat_user, addroles, remroles))
for r in addroles: for r in addroles:
if r not in seat_roles: if r not in seat_roles:
seat_roles[r] = SeatManager.add_role(r) seat_roles[r] = cls.add_role(r)
logger.debug("Adding role %s to SeAT user %s" % (r, seat_user)) logger.debug("Adding role %s to SeAT user %s" % (r, seat_user))
SeatManager.add_role_to_user(user_info["id"], seat_roles[r]) cls.add_role_to_user(user_info["id"], seat_roles[r])
for r in remroles: for r in remroles:
logger.debug("Removing role %s from user %s" % (r, seat_user)) logger.debug("Removing role %s from user %s" % (r, seat_user))
SeatManager.revoke_role_from_user(user_info["id"], seat_roles[r]) cls.revoke_role_from_user(user_info["id"], seat_roles[r])
@staticmethod
def username_hash(username):
m = hashlib.sha1()
m.update(username)
return m.hexdigest()

View File

@ -1,6 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _
from .manager import SeatManager from .manager import SeatManager
@ -15,6 +17,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ACCESS_PERM = 'seat.access_seat' ACCESS_PERM = 'seat.access_seat'
SERVICE_NAME = {'service': 'SeAT'}
@login_required @login_required
@ -39,6 +42,8 @@ def activate_seat(request):
logger.debug("Updated SeatUser for user %s with SeAT credentials. Adding eve-apis..." % request.user) logger.debug("Updated SeatUser for user %s with SeAT credentials. Adding eve-apis..." % request.user)
SeatTasks.update_roles.delay(request.user.pk) SeatTasks.update_roles.delay(request.user.pk)
logger.info("Successfully activated SeAT for user %s" % request.user) logger.info("Successfully activated SeAT for user %s" % request.user)
messages.add_message(request, messages.SUCCESS, _('Successfully activated your %(service)s account.') %
SERVICE_NAME)
SeatManager.synchronize_eveapis(request.user) SeatManager.synchronize_eveapis(request.user)
credentials = { credentials = {
'username': request.user.seat.username, 'username': request.user.seat.username,
@ -46,6 +51,9 @@ def activate_seat(request):
} }
return render(request, 'registered/service_credentials.html', return render(request, 'registered/service_credentials.html',
context={'credentials': credentials, 'service': 'SeAT'}) context={'credentials': credentials, 'service': 'SeAT'})
messages.add_message(request, messages.ERROR,
_('Failed to activate your %(service)s account, please contact your administrator.') %
SERVICE_NAME)
logger.error("Unsuccessful attempt to activate seat for user %s" % request.user) logger.error("Unsuccessful attempt to activate seat for user %s" % request.user)
return redirect("auth_services") return redirect("auth_services")
@ -56,10 +64,15 @@ def deactivate_seat(request):
logger.debug("deactivate_seat called by user %s" % request.user) logger.debug("deactivate_seat called by user %s" % request.user)
# false we failed # false we failed
if SeatTasks.delete_user(request.user): if SeatTasks.delete_user(request.user):
messages.add_message(request, messages.SUCCESS,
_('Successfully deactivated your %(service)s account.') % SERVICE_NAME)
logger.info("Successfully deactivated SeAT for user %s" % request.user) logger.info("Successfully deactivated SeAT for user %s" % request.user)
return redirect("auth_services") return redirect("auth_services")
else: else:
logging.error("User does not have a SeAT account") logging.error("User does not have a SeAT account")
messages.add_message(request, messages.ERROR,
_('Failed to deactivate your %(service)s account, please contact your administrator.') %
SERVICE_NAME)
logger.error("Unsuccessful attempt to activate SeAT for user %s" % request.user) logger.error("Unsuccessful attempt to activate SeAT for user %s" % request.user)
return redirect("auth_services") return redirect("auth_services")
@ -76,10 +89,15 @@ def reset_seat_password(request):
'username': request.user.seat.username, 'username': request.user.seat.username,
'password': result, 'password': result,
} }
messages.add_message(request, messages.SUCCESS,
_('Successfully reset your %(service)s password.') % {'service': 'SeAT'})
logger.info("Succesfully reset SeAT password for user %s" % request.user) logger.info("Succesfully reset SeAT password for user %s" % request.user)
return render(request, 'registered/service_credentials.html', return render(request, 'registered/service_credentials.html',
context={'credentials': credentials, 'service': 'SeAT'}) context={'credentials': credentials, 'service': 'SeAT'})
logger.error("Unsuccessful attempt to reset SeAT password for user %s" % request.user) logger.error("Unsuccessful attempt to reset SeAT password for user %s" % request.user)
messages.add_message(request, messages.ERROR,
_('Failed to reset your %(service)s password, please contact your administrator.') %
{'service': 'SeAT'})
return redirect("auth_services") return redirect("auth_services")
@ -98,11 +116,17 @@ def set_seat_password(request):
request.user.email, request.user.email,
plain_password=password) plain_password=password)
if result: if result:
messages.add_message(request, messages.SUCCESS,
_('Successfully set your %(service)s password.') % SERVICE_NAME)
logger.info("Succesfully reset SeAT password for user %s" % request.user) logger.info("Succesfully reset SeAT password for user %s" % request.user)
return redirect("auth_services") return redirect("auth_services")
else: else:
messages.add_message(request, messages.ERROR,
_('Failed to set your %(service)s password, please contact your administrator.') %
SERVICE_NAME)
logger.error("Failed to install custom SeAT password for user %s" % request.user) logger.error("Failed to install custom SeAT password for user %s" % request.user)
else: else:
messages.add_message(request, messages.ERROR, _('Invalid password.'))
logger.error("Invalid SeAT password provided") logger.error("Invalid SeAT password provided")
else: else:
logger.debug("Request is not type POST - providing empty form.") logger.debug("Request is not type POST - providing empty form.")