hobo/hobo/rest_authentication.py

195 lines
6.5 KiB
Python

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