From aec013b93cc52b5d6b19779c85c8ffcb9fe7b9e8 Mon Sep 17 00:00:00 2001 From: Basraah Date: Fri, 5 May 2017 00:31:57 +1000 Subject: [PATCH] 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 --- services/modules/seat/manager.py | 200 +++++++++++++++++-------------- services/modules/seat/views.py | 24 ++++ 2 files changed, 133 insertions(+), 91 deletions(-) diff --git a/services/modules/seat/manager.py b/services/modules/seat/manager.py index 453ff546..f75acd65 100644 --- a/services/modules/seat/manager.py +++ b/services/modules/seat/manager.py @@ -2,9 +2,11 @@ from __future__ import unicode_literals import random import string import requests +import hashlib from eveonline.managers import EveManager from django.conf import settings from django.core.exceptions import ObjectDoesNotExist +from django.core.cache import cache from six import iteritems @@ -20,7 +22,7 @@ class SeatManager: RESPONSE_OK = 'ok' @staticmethod - def __santatize_username(username): + def __sanitize_username(username): sanatized = username.replace(" ", "_") return sanatized.lower() @@ -33,7 +35,7 @@ class SeatManager: return cls.RESPONSE_OK in response @staticmethod - def exec_request(endpoint, func, **kwargs): + def exec_request(endpoint, func, raise_for_status=False, **kwargs): """ Send an https api request """ try: 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.raise_for_status() 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)) return {} @classmethod def add_user(cls, username, email): """ Add user to service """ - sanatized = str(SeatManager.__santatize_username(username)) - logger.debug("Adding user to SeAT with username %s" % sanatized) - password = SeatManager.__generate_random_pass() - ret = SeatManager.exec_request('user', 'post', username=sanatized, email=str(email), password=password) + sanitized = str(cls.__sanitize_username(username)) + logger.debug("Adding user to SeAT with username %s" % sanitized) + password = cls.__generate_random_pass() + ret = cls.exec_request('user', 'post', username=sanitized, email=str(email), password=password) logger.debug(ret) if cls._response_ok(ret): - logger.info("Added SeAT user with username %s" % sanatized) - return sanatized, password - logger.info("Failed to add SeAT user with username %s" % sanatized) + logger.info("Added SeAT user with username %s" % sanitized) + return sanitized, password + logger.info("Failed to add SeAT user with username %s" % sanitized) return None @classmethod @@ -93,7 +97,7 @@ class SeatManager: @classmethod def enable_user(cls, username): """ 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) if cls._response_ok(ret): logger.info("Enabled SeAT user with username %s" % username) @@ -104,13 +108,12 @@ class SeatManager: @classmethod def update_user(cls, username, email, password): """ Edit user info """ - logger.debug("Updating SeAT username %s with email %s and password hash starting with %s" % (username, email, - password[0:5])) - ret = SeatManager.exec_request('user/{}'.format(username), 'put', email=email) + logger.debug("Updating SeAT username %s with email %s and password" % (username, email)) + ret = cls.exec_request('user/{}'.format(username), 'put', email=email) logger.debug(ret) if not cls._response_ok(ret): 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) if not cls._response_ok(ret): 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) return username - @staticmethod - def update_user_password(username, email, plain_password=None): + @classmethod + def update_user_password(cls, username, email, plain_password=None): logger.debug("Settings new SeAT password for user %s" % username) if not plain_password: - plain_password = SeatManager.__generate_random_pass() - if SeatManager.update_user(username, email, plain_password): + plain_password = cls.__generate_random_pass() + if cls.update_user(username, email, plain_password): return plain_password - @staticmethod - def check_user_status(username): - sanatized = str(SeatManager.__santatize_username(username)) - logger.debug("Checking SeAT status for user %s" % sanatized) - ret = SeatManager.exec_request('user/{}'.format(sanatized), 'get') + @classmethod + def check_user_status(cls, username): + sanitized = str(cls.__sanitize_username(username)) + logger.debug("Checking SeAT status for user %s" % sanitized) + ret = cls.exec_request('user/{}'.format(sanitized), 'get') logger.debug(ret) return ret - @staticmethod - def get_all_seat_eveapis(): - seat_all_keys = SeatManager.exec_request('key', 'get') + @classmethod + def get_all_seat_eveapis(cls): + seat_all_keys = cls.exec_request('key', 'get') seat_keys = {} for key in seat_all_keys: try: @@ -145,117 +148,132 @@ class SeatManager: seat_keys[key["key_id"]] = None 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 if user: - keypars = EveManager.get_api_key_pairs(user) - try: - userinfo = SeatManager.check_user_status(user.seat.username) - except ObjectDoesNotExist: - pass + keypairs = EveManager.get_api_key_pairs(user) else: # retrieve all api keys instead - keypars = EveManager.get_all_api_key_pairs() - if keypars: - for keypar in keypars: - if keypar.api_id not in seat_all_keys.keys(): - #Add new keys - logger.debug("Adding Api Key with ID %s" % keypar.api_id) - ret = SeatManager.exec_request('key', 'post', key_id=keypar.api_id, v_code=keypar.api_key) - logger.debug(ret) - else: - # remove it from the list so it doesn't get deleted in the last step - seat_all_keys.pop(keypar.api_id) - if not userinfo: # TODO: should the following be done only for new keys? - # Check the key's user status - logger.debug("Retrieving user name from Auth's SeAT users database") - try: - if keypar.user.seat.username: - logger.debug("Retrieving user %s info from SeAT users database" % keypar.user.seat.username) - userinfo = SeatManager.check_user_status(keypar.user.seat.username) - except ObjectDoesNotExist: - pass - if userinfo: - try: - # If the user has activated seat, assign the key to him. - logger.debug("Transferring Api Key with ID %s to user %s with ID %s " % ( - keypar.api_id, - keypar.user.seat.username, - userinfo['id'])) - ret = SeatManager.exec_request('key/transfer/{}/{}'.format(keypar.api_id, userinfo['id']), - 'get') - logger.debug(ret) - except ObjectDoesNotExist: - logger.debug("User does not have SeAT activated, could not assign key to user") + keypairs = EveManager.get_all_api_key_pairs() - if bool(seat_all_keys) and not user and hasattr(settings, 'SEAT_PURGE_DELETED') and settings.SEAT_PURGE_DELETED: + for keypair in keypairs: + # Transfer the key if it isn't already in SeAT + if keypair.api_id not in seat_all_keys.keys(): + # Add new keys + logger.debug("Adding Api Key with ID %s" % keypair.api_id) + try: + ret = cls.exec_request('key', 'post', + key_id=keypair.api_id, + v_code=keypair.api_key, + raise_for_status=True) + 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: + # remove it from the list so it doesn't get deleted in the last step + seat_all_keys.pop(keypair.api_id) + + # Attach API key to the users SeAT account, if possible + try: + userinfo = cache.get_or_set('seat_user_status_' + cls.username_hash(keypair.user.seat.username), + lambda: cls.check_user_status(keypair.user.seat.username), + 300) # Cache for 5 minutes + + if not bool(userinfo): + # No SeAT account, skip + logger.debug("Could not find users SeAT id, cannot assign key to them") + 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 " % ( + keypair.api_id, + keypair.user.seat.username, + userinfo['id'])) + ret = cls.exec_request('key/transfer/{}/{}'.format(keypair.api_id, userinfo['id']), + 'get') + logger.debug(ret) + except ObjectDoesNotExist: + logger.debug("User does not have SeAT activated, could not assign key to user") + + if bool(seat_all_keys) and not user and getattr(settings, 'SEAT_PURGE_DELETED', False): # remove from SeAT keys that were removed from Auth for key, key_user in iteritems(seat_all_keys): # 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) try: if (ret['info']['type'] == "Account") or (ret['info']['type'] == "Character"): 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) except KeyError: pass - @staticmethod - def get_all_roles(): + @classmethod + def get_all_roles(cls): groups = {} - ret = SeatManager.exec_request('role', 'get') + ret = cls.exec_request('role', 'get') logger.debug(ret) for group in ret: groups[group["title"]] = group["id"] logger.debug("Retrieved role list from SeAT: %s" % str(groups)) return groups - @staticmethod - def add_role(role): - ret = SeatManager.exec_request('role/new', 'post', name=role) + @classmethod + def add_role(cls, role): + ret = cls.exec_request('role/new', 'post', name=role) logger.debug(ret) 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) return role_info["id"] - @staticmethod - def add_role_to_user(user_id, role_id): - ret = SeatManager.exec_request('role/grant-user-role/{}/{}'.format(user_id, role_id), 'get') + @classmethod + def add_role_to_user(cls, user_id, role_id): + 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)) return ret - @staticmethod - def revoke_role_from_user(user_id, role_id): - ret = SeatManager.exec_request('role/revoke-user-role/{}/{}'.format(user_id, role_id), 'get') + @classmethod + def revoke_role_from_user(cls, user_id, role_id): + 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)) return ret - @staticmethod - def update_roles(seat_user, roles): + @classmethod + def update_roles(cls, 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 = {} if type(user_info["roles"]) is list: for role in user_info["roles"]: user_roles[role["title"]] = role["id"] 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()) remroles = set(user_roles.keys()) - set(roles) logger.info("Updating SeAT roles for user %s - adding %s, removing %s" % (seat_user, addroles, remroles)) for r in addroles: 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)) - 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: 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() diff --git a/services/modules/seat/views.py b/services/modules/seat/views.py index 6a1e6a51..d110d95b 100644 --- a/services/modules/seat/views.py +++ b/services/modules/seat/views.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals from django.shortcuts import render, redirect 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 @@ -15,6 +17,7 @@ import logging logger = logging.getLogger(__name__) ACCESS_PERM = 'seat.access_seat' +SERVICE_NAME = {'service': 'SeAT'} @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) SeatTasks.update_roles.delay(request.user.pk) 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) credentials = { 'username': request.user.seat.username, @@ -46,6 +51,9 @@ def activate_seat(request): } return render(request, 'registered/service_credentials.html', 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) return redirect("auth_services") @@ -56,10 +64,15 @@ def deactivate_seat(request): logger.debug("deactivate_seat called by user %s" % request.user) # false we failed 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) return redirect("auth_services") else: 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) return redirect("auth_services") @@ -76,10 +89,15 @@ def reset_seat_password(request): 'username': request.user.seat.username, '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) return render(request, 'registered/service_credentials.html', context={'credentials': credentials, 'service': 'SeAT'}) 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") @@ -98,11 +116,17 @@ def set_seat_password(request): request.user.email, plain_password=password) 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) return redirect("auth_services") 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) else: + messages.add_message(request, messages.ERROR, _('Invalid password.')) logger.error("Invalid SeAT password provided") else: logger.debug("Request is not type POST - providing empty form.")