import logging import requests from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from django.core.exceptions import FieldDoesNotExist from django.utils.module_loading import import_string from rest_framework import authentication, exceptions, status from hobo import signature from hobo.requests_wrapper import Requests try: from mellon.models import UserSAMLIdentifier except ImportError: UserSAMLIdentifier = None class AnonymousAuthenticServiceUser(AnonymousUser): '''This virtual user hold permissions for other publik services''' is_anonymous = True is_authenticated = True ou = None is_publik_service = True def has_perm(self, *args, **kwargs): return True def has_ou_perm(self, *args, **kwargs): return True def has_perm_any(self, *args, **kwargs): return True def filter_by_perm(self, perm_or_perms, qs): # all objects are reachable return qs def __unicode__(self): return 'Publik Service User' class AnonymousAdminServiceUser(AnonymousUser): '''This virtual user hold permissions for other publik services''' is_staff = True is_anonymous = True is_authenticated = True ou = None is_publik_service = True def __unicode__(self): return 'Publik Service Admin' class APIClientUser: is_active = True is_anonymous = False is_authenticated = True is_superuser = False roles = [] def __init__(self, is_active, is_anonymous, is_authenticated, is_superuser, roles): self.is_active = is_active self.is_anonymous = is_anonymous self.is_authenticated = is_authenticated self.is_superuser = is_superuser self.roles = roles @classmethod def from_dict(cls, data): return cls( is_active=data['is_active'], is_anonymous=data['is_anonymous'], is_authenticated=data['is_authenticated'], is_superuser=data['is_superuser'], roles=data['roles'], ) class PublikAuthenticationFailed(exceptions.APIException): status_code = status.HTTP_401_UNAUTHORIZED default_code = 'invalid-signature' def __init__(self, code): self.detail = {'err': 1, 'err_desc': code} class PublikAuthentication(authentication.BaseAuthentication): def __init__(self, *args, **kwargs): self.logger = logging.getLogger(__name__) super().__init__(*args, **kwargs) def resolve_user(self, request): User = get_user_model() if 'NameID' in request.GET: name_id = request.GET['NameID'] is_authentic = True try: User._meta.get_field('uuid') except FieldDoesNotExist: is_authentic = False if is_authentic: try: return User.objects.get(uuid=name_id) except User.DoesNotExist: raise PublikAuthenticationFailed('user-not-found') elif UserSAMLIdentifier: try: return UserSAMLIdentifier.objects.get(name_id=name_id).user except UserSAMLIdentifier.DoesNotExist: raise PublikAuthenticationFailed('user-not-found') else: raise PublikAuthenticationFailed('no-usable-model') else: orig = request.GET['orig'] try: return User.objects.get(username=orig) except User.DoesNotExist: pass if hasattr(settings, 'HOBO_ANONYMOUS_SERVICE_USER_CLASS'): klass = import_string(settings.HOBO_ANONYMOUS_SERVICE_USER_CLASS) self.logger.info('anonymous signature validated') return klass() raise PublikAuthenticationFailed('no-user-for-orig') def get_orig_key(self, orig): if not hasattr(settings, 'KNOWN_SERVICES'): self.logger.warning('no known services') raise PublikAuthenticationFailed('no-known-services-setting') for service_id in settings.KNOWN_SERVICES: for slug, service in settings.KNOWN_SERVICES[service_id].items(): if service.get('verif_orig') == orig and service.get('secret'): return service['secret'] self.logger.warning('no secret found for origin %r', orig) raise PublikAuthenticationFailed('no-secret-found-for-orig') def authenticate(self, request): full_path = request.get_full_path() if not request.GET.get('orig') or not request.GET.get('signature'): return None key = self.get_orig_key(request.GET['orig']) try: assert signature.check_url( full_path, key, raise_on_error=True ), 'signature.check_url should never return False with raise_on_error' except signature.SignatureError as e: self.logger.warning('publik rest-framework-authentication failed: %s', e) raise PublikAuthenticationFailed(str(e)) user = self.resolve_user(request) self.logger.info('user authenticated with signature %s', user) return (user, None) class APIClientAuthenticationUnavailable(exceptions.APIException): status_code = status.HTTP_503_SERVICE_UNAVAILABLE default_code = 'IDP temporarily unavailable, try again later.' def __init__(self): self.detail = {'err': 1, 'err_desc': self.default_code} class APIClientAuthentication(authentication.BasicAuthentication): def authenticate_credentials(self, identifier, password, request=None): idp_services = list(getattr(settings, 'KNOWN_SERVICES', {}).get('authentic', {}).values()) if not idp_services: return None authentic = idp_services[0] url = authentic['url'] + 'api/check-api-client/' try: response = Requests().post(url, json={'identifier': identifier, 'password': password}) except requests.Timeout: raise APIClientAuthenticationUnavailable() except requests.RequestException as err: raise APIClientAuthenticationUnavailable() try: response.raise_for_status() except requests.exceptions.RequestException: return None result = response.json() if 'err' not in result or 'data' not in result or result['err'] == 1: return None try: api_client = APIClientUser.from_dict(result['data']) except Exception: return None return api_client, None