From 023a1a335109176c35e2fd823ed0c15cb666821e Mon Sep 17 00:00:00 2001 From: Brian Jinwright Date: Fri, 27 Apr 2018 15:45:56 -0400 Subject: [PATCH 1/7] removed support for python 2 and Django 1.x --- .travis.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 34d9022..68188b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "2.7" + - "3.6" # command to install dependencies install: - "pip install -r requirements.txt" diff --git a/setup.py b/setup.py index e2dc3c2..47ef0e3 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def parse_requirements(filename): long_description=README, classifiers=[ 'Framework :: Django', - 'Framework :: Django :: 1.10', + 'Framework :: Django :: 2.0', "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Environment :: Web Environment", From 961378d5766295f74dea1f3ab655ccb127ce8b5b Mon Sep 17 00:00:00 2001 From: Brian Jinwright Date: Mon, 30 Apr 2018 12:27:10 -0400 Subject: [PATCH 2/7] fixed Python 3.x iteration issue --- django_warrant/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_warrant/backend.py b/django_warrant/backend.py index 114aef5..650b2a5 100644 --- a/django_warrant/backend.py +++ b/django_warrant/backend.py @@ -27,7 +27,7 @@ def get_user_obj(self,username=None,attribute_list=[],metadata={},attr_map={}): user_attrs = cognito_to_dict(attribute_list,CognitoUser.COGNITO_ATTR_MAPPING) django_fields = [f.name for f in CognitoUser.user_class._meta.get_fields()] extra_attrs = {} - for k, v in user_attrs.items(): + for k in list(user_attrs): if k not in django_fields: extra_attrs.update({k: user_attrs.pop(k, None)}) if getattr(settings, 'COGNITO_CREATE_UNKNOWN_USERS', True): From 1bf32490c0010752efeb0265176a5aa0cad3e035 Mon Sep 17 00:00:00 2001 From: Brian Jinwright Date: Tue, 29 May 2018 16:42:27 -0400 Subject: [PATCH 3/7] removed all Warrant references, converted all views to use WarrantLite, added new views in the admin and on the user side --- cdu/settings.py | 8 ++ django_warrant/backend.py | 126 +++++++++++------- django_warrant/forms.py | 3 + django_warrant/models.py | 115 ++++++++++++++++ .../templates/warrant/admin-list-users.html | 2 + .../templates/warrant/admin-profile.html | 3 + .../warrant/admin-update-profile.html | 3 + django_warrant/templates/warrant/base.html | 32 +++-- django_warrant/templates/warrant/profile.html | 4 +- .../templates/warrant/subscriptions.html | 2 + .../templates/warrant/update-profile.html | 4 +- django_warrant/templatetags/__init__.py | 0 django_warrant/templatetags/cognito_tags.py | 7 - django_warrant/tests.py | 40 +++++- django_warrant/urls.py | 10 +- django_warrant/utils.py | 26 ++-- django_warrant/views/__init__.py | 1 + django_warrant/views/admin.py | 63 +++++++++ django_warrant/views/mixins.py | 48 +++++++ django_warrant/views/profile.py | 35 ++--- django_warrant/views/subscriptions.py | 94 +------------ requirements.txt | 3 +- 22 files changed, 425 insertions(+), 204 deletions(-) create mode 100644 django_warrant/templates/warrant/admin-profile.html create mode 100644 django_warrant/templates/warrant/admin-update-profile.html delete mode 100644 django_warrant/templatetags/__init__.py delete mode 100644 django_warrant/templatetags/cognito_tags.py create mode 100644 django_warrant/views/admin.py create mode 100644 django_warrant/views/mixins.py diff --git a/cdu/settings.py b/cdu/settings.py index 0803bd6..b2032f8 100644 --- a/cdu/settings.py +++ b/cdu/settings.py @@ -41,12 +41,20 @@ COGNITO_APP_ID = env('COGNITO_APP_ID') +COGNITO_CLIENT_SECRET = env('COGNITO_CLIENT_SECRET') + COGNITO_ATTR_MAPPING = env( 'COGNITO_ATTR_MAPPING', { 'email': 'email', 'given_name': 'first_name', 'family_name': 'last_name', + 'username':'username', + 'address':'address', + 'gender':'gender', + 'preferred_username':'preferred_username', + 'phone_number':'phone_number', + 'phone_number_verified':'phone_number_verified', 'custom:api_key': 'api_key', 'custom:api_key_id': 'api_key_id' }, diff --git a/django_warrant/backend.py b/django_warrant/backend.py index 650b2a5..e0657d3 100644 --- a/django_warrant/backend.py +++ b/django_warrant/backend.py @@ -6,48 +6,11 @@ from django.conf import settings from django.contrib.auth.backends import ModelBackend from django.contrib.auth import get_user_model -from django.utils.six import iteritems +from warrant_lite import WarrantLite -from warrant import Cognito from .utils import cognito_to_dict -class CognitoUser(Cognito): - user_class = get_user_model() - # Mapping of Cognito User attribute name to Django User attribute name - COGNITO_ATTR_MAPPING = getattr(settings, 'COGNITO_ATTR_MAPPING', - { - 'email': 'email', - 'given_name': 'first_name', - 'family_name': 'last_name', - } - ) - - def get_user_obj(self,username=None,attribute_list=[],metadata={},attr_map={}): - user_attrs = cognito_to_dict(attribute_list,CognitoUser.COGNITO_ATTR_MAPPING) - django_fields = [f.name for f in CognitoUser.user_class._meta.get_fields()] - extra_attrs = {} - for k in list(user_attrs): - if k not in django_fields: - extra_attrs.update({k: user_attrs.pop(k, None)}) - if getattr(settings, 'COGNITO_CREATE_UNKNOWN_USERS', True): - user, created = CognitoUser.user_class.objects.update_or_create( - username=username, - defaults=user_attrs) - else: - try: - user = CognitoUser.user_class.objects.get(username=username) - for k, v in iteritems(user_attrs): - setattr(user, k, v) - user.save() - except CognitoUser.user_class.DoesNotExist: - user = None - if user: - for k, v in extra_attrs.items(): - setattr(user, k, v) - return user - - class AbstractCognitoBackend(ModelBackend): __metaclass__ = abc.ABCMeta @@ -55,7 +18,6 @@ class AbstractCognitoBackend(ModelBackend): USER_NOT_FOUND_ERROR_CODE = 'UserNotFoundException' - COGNITO_USER_CLASS = CognitoUser @abc.abstractmethod def authenticate(self, username=None, password=None): @@ -65,24 +27,58 @@ def authenticate(self, username=None, password=None): :param password: Cognito password :return: returns User instance of AUTH_USER_MODEL or None """ - cognito_user = CognitoUser( - settings.COGNITO_USER_POOL_ID, - settings.COGNITO_APP_ID, - access_key=getattr(settings, 'AWS_ACCESS_KEY_ID', None), - secret_key=getattr(settings, 'AWS_SECRET_ACCESS_KEY', None), - username=username) + wl = WarrantLite(username=username, password=password, + pool_id=settings.COGNITO_USER_POOL_ID, + client_id=settings.COGNITO_APP_ID, + client_secret=settings.COGNITO_CLIENT_SECRET) + try: - cognito_user.authenticate(password) + tokens = wl.authenticate_user() except (Boto3Error, ClientError) as e: return self.handle_error_response(e) - user = cognito_user.get_user() + + access_token = tokens['AuthenticationResult']['AccessToken'] + refresh_token = tokens['AuthenticationResult']['RefreshToken'] + id_token = tokens['AuthenticationResult']['IdToken'] + wl.verify_token(access_token, 'access_token', 'access') + wl.verify_token(id_token, 'id_token', 'id') + + cognito_user = wl.client.get_user( + AccessToken=access_token + ) + user = self.get_user_obj(username,cognito_user) if user: - user.access_token = cognito_user.access_token - user.id_token = cognito_user.id_token - user.refresh_token = cognito_user.refresh_token + user.access_token = access_token + user.id_token = id_token + user.refresh_token = refresh_token return user + def get_user_obj(self,username,cognito_user): + user_attrs = cognito_to_dict(cognito_user.get('UserAttributes'), + settings.COGNITO_ATTR_MAPPING or { + 'email':'email', + 'given_name':'first_name', + 'family_name':'last_name' + }) + User = get_user_model() + django_fields = [f.name for f in User._meta.get_fields()] + extra_attrs = {} + new_user_attrs = user_attrs.copy() + + for k, v in user_attrs.items(): + if k not in django_fields: + extra_attrs.update({k: new_user_attrs.pop(k, None)}) + user_attrs = new_user_attrs + try: + u = User.objects.get(username=username) + except User.DoesNotExist: + u = None + if u: + for k, v in extra_attrs.items(): + setattr(u, k, v) + return u + def handle_error_response(self, error): error_code = error.response['Error']['Code'] if error_code in [ @@ -107,3 +103,33 @@ def authenticate(self, request, username=None, password=None): request.session['REFRESH_TOKEN'] = user.refresh_token request.session.save() return user + + +class CognitoNoModelBackend(object): + + def authenticate(self, request, username=None, password=None): + wl = WarrantLite(username=username, password=password, + pool_id=settings.COGNITO_USER_POOL_ID, + client_id=settings.COGNITO_APP_ID, + client_secret=settings.COGNITO_CLIENT_SECRET) + + try: + tokens = wl.authenticate_user() + except (Boto3Error, ClientError) as e: + return self.handle_error_response(e) + + access_token = tokens['AuthenticationResult']['AccessToken'] + refresh_token = tokens['AuthenticationResult']['RefreshToken'] + id_token = tokens['AuthenticationResult']['IdToken'] + wl.verify_token(access_token, 'access_token', 'access') + wl.verify_token(id_token, 'id_token', 'id') + + user = wl.client.get_user( + AccessToken=access_token + ) + if user: + request.session['ACCESS_TOKEN'] = user.access_token + request.session['ID_TOKEN'] = user.id_token + request.session['REFRESH_TOKEN'] = user.refresh_token + request.session.save() + return user diff --git a/django_warrant/forms.py b/django_warrant/forms.py index cfd66c1..31f52b2 100644 --- a/django_warrant/forms.py +++ b/django_warrant/forms.py @@ -9,6 +9,9 @@ class ProfileForm(forms.Form): gender = forms.ChoiceField(choices=(('female','Female'),('male','Male')),required=True) address = forms.CharField(max_length=200,required=True) preferred_username = forms.CharField(max_length=200,required=True) + + +class AdminProfileForm(ProfileForm): api_key = forms.CharField(max_length=200, required=False) api_key_id = forms.CharField(max_length=200, required=False) diff --git a/django_warrant/models.py b/django_warrant/models.py index e69de29..9274de1 100644 --- a/django_warrant/models.py +++ b/django_warrant/models.py @@ -0,0 +1,115 @@ +import datetime + +from django.conf import settings +from jose import jwt + +from django_warrant.utils import cognito_to_dict, dict_to_cognito, cog_client + + +class UserObj(object): + + def __init__(self, attribute_list, metadata=None, request=None): + """ + :param attribute_list: + :param metadata: Dictionary of User metadata + """ + + self._attr_map = settings.COGNITO_ATTR_MAPPING + self.username = attribute_list.get('Username') + self._data = cognito_to_dict( + attribute_list.get('UserAttributes') + or attribute_list.get('Attributes'),self._attr_map) + + self.sub = self._data.pop('sub',None) + self.email_verified = self._data.pop('email_verified',None) + self.phone_number_verified = self._data.pop('phone_number_verified',None) + self._metadata = {} if metadata is None else metadata + if request: + self.access_token = request.session['ACCESS_TOKEN'] + self.refresh_token = request.session['REFRESH_TOKEN'] + self.id_token = request.session['ID_TOKEN'] + + def __repr__(self): + return '<{class_name}: {uni}>'.format( + class_name=self.__class__.__name__, uni=self.__unicode__()) + + def __unicode__(self): + return self.username + + def __getattr__(self, name): + if name in list(self.__dict__.get('_data',{}).keys()): + return self._data.get(name) + if name in list(self.__dict__.get('_metadata',{}).keys()): + return self._metadata.get(name) + + def __setattr__(self, name, value): + if name in list(self.__dict__.get('_data',{}).keys()): + self._data[name] = value + else: + super(UserObj, self).__setattr__(name, value) + + def check_token(self, renew=True): + """ + Checks the exp attribute of the access_token and either refreshes + the tokens by calling the renew_access_tokens method or does nothing + :param renew: bool indicating whether to refresh on expiration + :return: bool indicating whether access_token has expired + """ + if not self.access_token: + raise AttributeError('Access Token Required to Check Token') + now = datetime.datetime.now() + dec_access_token = jwt.get_unverified_claims(self.access_token) + + if now > datetime.datetime.fromtimestamp(dec_access_token['exp']): + expired = True + if renew: + self.renew_access_token() + else: + expired = False + return expired + + def renew_access_token(self): + """ + Sets a new access token on the User using the refresh token. + """ + auth_params = {'REFRESH_TOKEN': self.refresh_token} + self._add_secret_hash(auth_params, 'SECRET_HASH') + refresh_response = cog_client.initiate_auth( + ClientId=settings.COGNITO_APP_ID, + AuthFlow='REFRESH_TOKEN', + AuthParameters=auth_params, + ) + + self.access_token = refresh_response['AuthenticationResult']['AccessToken'] + self.id_token = refresh_response['AuthenticationResult']['IdToken'] + self.token_type = refresh_response['AuthenticationResult']['TokenType'] + + + def save(self,admin=False,create=False,password=None): + if not create: + if admin: + cog_client.admin_update_user_attributes( + UserPoolId=settings.COGNITO_USER_POOL_ID, + Username=self.username, + UserAttributes=dict_to_cognito(self._data,self._attr_map)) + return + cog_client.update_user_attributes( + UserAttributes=dict_to_cognito(self._data, self._attr_map), + AccessToken=self.access_token + ) + return + else: + cog_client.sign_up( + ClientId=settings.COGNITO_APP_ID, + Username=self.username, + Password=password, + UserAttributes=dict_to_cognito(self._data, self._attr_map) + ) + return + + def delete(self,admin=False): + cog_client.admin_disable_user( + UserPoolId=settings.COGNITO_USER_POOL_ID, + Username=self.username + ) + return diff --git a/django_warrant/templates/warrant/admin-list-users.html b/django_warrant/templates/warrant/admin-list-users.html index f778597..f931eb7 100644 --- a/django_warrant/templates/warrant/admin-list-users.html +++ b/django_warrant/templates/warrant/admin-list-users.html @@ -7,6 +7,7 @@ First Name Last Name + {% for obj in object_list %} @@ -14,6 +15,7 @@ {{ obj.first_name }} {{ obj.last_name }} View + Update {% endfor %} diff --git a/django_warrant/templates/warrant/admin-profile.html b/django_warrant/templates/warrant/admin-profile.html new file mode 100644 index 0000000..a9fdf4d --- /dev/null +++ b/django_warrant/templates/warrant/admin-profile.html @@ -0,0 +1,3 @@ +{% extends 'warrant/profile.html' %} + +{% block h1_title %}{{ user.username }}'s Profile{% endblock h1_title %} \ No newline at end of file diff --git a/django_warrant/templates/warrant/admin-update-profile.html b/django_warrant/templates/warrant/admin-update-profile.html new file mode 100644 index 0000000..2f82738 --- /dev/null +++ b/django_warrant/templates/warrant/admin-update-profile.html @@ -0,0 +1,3 @@ +{% extends 'warrant/update-profile.html' %} + +{% block h1_title %}Update {{ user.username }}'s Profile{% endblock h1_title %} \ No newline at end of file diff --git a/django_warrant/templates/warrant/base.html b/django_warrant/templates/warrant/base.html index 10c00ff..9722a78 100644 --- a/django_warrant/templates/warrant/base.html +++ b/django_warrant/templates/warrant/base.html @@ -43,23 +43,37 @@ {% block nav_title %}Warrant{% endblock %} + {% if messages %} +{% endif %} {% block body %} {% block content %}
+ {% for message in messages %} +
+ × + {{message}} +
+{% endfor %} +
+
+

{% block h1_title %}{% endblock h1_title %}

+
+
diff --git a/django_warrant/templates/warrant/profile.html b/django_warrant/templates/warrant/profile.html index a33a3a5..c9a3445 100644 --- a/django_warrant/templates/warrant/profile.html +++ b/django_warrant/templates/warrant/profile.html @@ -1,8 +1,8 @@ {% extends 'warrant/base.html' %} -{% load cognito_tags %} +{% block h1_title %}{{ user.username }}{% endblock h1_title %} {% block main_content %} -

{{ user|username }}

+ Update your Profile
First Name:
diff --git a/django_warrant/templates/warrant/subscriptions.html b/django_warrant/templates/warrant/subscriptions.html index 428d6ac..8a91c29 100644 --- a/django_warrant/templates/warrant/subscriptions.html +++ b/django_warrant/templates/warrant/subscriptions.html @@ -1,5 +1,7 @@ {% extends 'warrant/base.html' %} +{% block h1_title %}My Subscriptions{% endblock h1_title %} + {% block main_content %} diff --git a/django_warrant/templates/warrant/update-profile.html b/django_warrant/templates/warrant/update-profile.html index 3ec407f..30335f2 100644 --- a/django_warrant/templates/warrant/update-profile.html +++ b/django_warrant/templates/warrant/update-profile.html @@ -1,8 +1,8 @@ {% extends 'warrant/base.html' %} {% load crispy_forms_tags %} - +{% block h1_title %}Update Your Profile{% endblock h1_title %} {% block main_content %} - + {% csrf_token %} {{ form|crispy }} diff --git a/django_warrant/templatetags/__init__.py b/django_warrant/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/django_warrant/templatetags/cognito_tags.py b/django_warrant/templatetags/cognito_tags.py deleted file mode 100644 index 9f30226..0000000 --- a/django_warrant/templatetags/cognito_tags.py +++ /dev/null @@ -1,7 +0,0 @@ -from django import template - -register = template.Library() - -@register.filter('username') -def username(user): - return user._metadata.get('username') diff --git a/django_warrant/tests.py b/django_warrant/tests.py index 5a58b51..87e7f52 100644 --- a/django_warrant/tests.py +++ b/django_warrant/tests.py @@ -15,15 +15,51 @@ from django.test.client import RequestFactory from django.utils.six import iteritems -from django_warrant.backend import CognitoBackend, CognitoUser +from django_warrant.backend import CognitoBackend from django_warrant.middleware import APIKeyMiddleware -from warrant import Cognito + def set_tokens(cls, *args, **kwargs): cls.access_token = 'accesstoken' cls.id_token = 'idtoken' cls.refresh_token = 'refreshtoken' +def authenticate_user(cls,*args, **kwargs): + return { + 'ChallengeParameters': {}, + 'AuthenticationResult': { + 'AccessToken': 'fake.access.token', + 'ExpiresIn': 3600, + 'TokenType': 'Bearer', + 'RefreshToken': 'fake.refresh.token', + 'IdToken': 'fake.id.token'}, + 'ResponseMetadata': { + 'RequestId': 'abc123-1234-4567-789-9101112', + 'HTTPStatusCode': 200, + 'HTTPHeaders': { + 'date': 'Thu, 10 May 2018 15:23:12 GMT', + 'content-type': 'application/x-amz-json-1.1', + 'content-length': '4056', + 'connection': 'keep-alive', + 'x-amzn-requestid': 'abc123-f233-sfsdf-k342334'}, + 'RetryAttempts': 0} + } + +def verify_tokens(cls,*args, **kwargs): + return { + 'sub': 'asfadfadf-3323sd-sdt4-adf22-5dfgdfsaddf', + 'event_id': 'sdf44sdsd-3234-9540-8a21-234sdfdsff', + 'token_use': 'access', + 'scope': 'aws.cognito.signin.user.admin', + 'auth_time': 1525966559, + 'iss': 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Bgbf9cyLt', + 'exp': 1525970159, + 'iat': 1525966559, + 'jti': '23ssdfsdf-tt44-5678-9fgv-345dfgdfgfdfgg', + 'client_id': '7dsfsdfdsfdfgkdfkkd', + 'username': 'fakeusername' + } + def get_user(cls, *args, **kwargs): user = { 'user_status': kwargs.pop('user_status', 'CONFIRMED'), diff --git a/django_warrant/urls.py b/django_warrant/urls.py index 25d7fd8..5f6841d 100644 --- a/django_warrant/urls.py +++ b/django_warrant/urls.py @@ -2,8 +2,9 @@ from django.contrib.auth import views as auth_views from django.urls import re_path -from .views import ProfileView,UpdateProfileView,MySubsriptions,\ - AdminListUsers,AdminSubscriptions,LogoutView +from django_warrant.views import SubscriptionsView, AdminProfileView +from .views import ProfileView,UpdateProfileView,\ + AdminListUsers,LogoutView,AdminUpdateProfileView app_name = 'dw' @@ -11,8 +12,9 @@ re_path(r'^login/$', auth_views.login, {'template_name': 'warrant/login.html'}, name='login'), re_path(r'^logout/$', LogoutView.as_view(), {'template_name': 'warrant/logout.html'}, name='logout'), re_path(r'^profile/$', ProfileView.as_view(),name='profile'), + re_path(r'^subscriptions/$', SubscriptionsView.as_view(),name='subscriptions'), re_path(r'^profile/update/$', UpdateProfileView.as_view(),name='update-profile'), - re_path(r'^profile/subscriptions/$', MySubsriptions.as_view(),name='subscriptions'), re_path(r'^admin/cognito-users/$', AdminListUsers.as_view(),name='admin-cognito-users'), - re_path(r'^admin/cognito-users/(?P[-\w]+)$', AdminSubscriptions.as_view(),name='admin-cognito-user') + re_path(r'^admin/cognito-users/(?P[-\w]+)/$', AdminProfileView.as_view(),name='admin-cognito-user'), + re_path(r'^admin/cognito-users/(?P[-\w]+)/update/$', AdminUpdateProfileView.as_view(),name='admin-cognito-update-user') ) \ No newline at end of file diff --git a/django_warrant/utils.py b/django_warrant/utils.py index d4f5e29..a0d5ff7 100644 --- a/django_warrant/utils.py +++ b/django_warrant/utils.py @@ -1,7 +1,10 @@ +import boto3 from django.conf import settings -from warrant import Cognito +apigw_client = boto3.client('apigateway') +cog_client = boto3.client('cognito-idp') + def cognito_to_dict(attr_list,mapping): user_attrs = dict() for i in attr_list: @@ -11,6 +14,14 @@ def cognito_to_dict(attr_list,mapping): user_attrs[name] = value return user_attrs +def dict_to_cognito(attr_dict,mapping): + cognito_list = list() + inv_map = {v: k for k, v in mapping.items()} + for k,v in attr_dict.items(): + name = inv_map.get(k) + cognito_list.append({'Name':name,'Value':v}) + return cognito_list + def user_obj_to_django(user_obj): c_attrs = settings.COGNITO_ATTR_MAPPING user_attrs = dict() @@ -20,17 +31,4 @@ def user_obj_to_django(user_obj): user_attrs[dk] = v return user_attrs -def get_cognito(request): - - c = Cognito(settings.COGNITO_USER_POOL_ID,settings.COGNITO_APP_ID, - access_token=request.session.get('ACCESS_TOKEN'), - id_token=request.session.get('ID_TOKEN'), - refresh_token=request.session.get('REFRESH_TOKEN')) - changed = c.check_token() - if changed: - request.session['ACCESS_TOKEN'] = c.access_token - request.session['REFRESH_TOKEN'] = c.refresh_token - request.session['ID_TOKEN'] = c.id_token - request.session.save() - return c diff --git a/django_warrant/views/__init__.py b/django_warrant/views/__init__.py index f939f4c..60020ae 100644 --- a/django_warrant/views/__init__.py +++ b/django_warrant/views/__init__.py @@ -1,2 +1,3 @@ +from .admin import * from .profile import * from .subscriptions import * \ No newline at end of file diff --git a/django_warrant/views/admin.py b/django_warrant/views/admin.py new file mode 100644 index 0000000..92e08f1 --- /dev/null +++ b/django_warrant/views/admin.py @@ -0,0 +1,63 @@ +from django.contrib import messages +from django.views.generic import FormView, TemplateView + +from django_warrant.forms import AdminProfileForm +from django_warrant.utils import cog_client + +try: + from django.urls import reverse_lazy +except ImportError: + from django.core.urlresolvers import reverse_lazy +from django.views.generic.list import ListView + +from django.conf import settings +from ..models import UserObj +from .mixins import AdminMixin, GetUserMixin + + +class AdminListUsers(AdminMixin,ListView): + template_name = 'warrant/admin-list-users.html' + + def test_func(self): + return self.request.user.is_staff + + def get_queryset(self): + ul = cog_client.list_users(UserPoolId=settings.COGNITO_USER_POOL_ID).get('Users') + response = [UserObj(i) for i in ul] + return response + + +class AdminProfileView(AdminMixin,GetUserMixin,TemplateView): + template_name = 'warrant/admin-profile.html' + + def get_context_data(self, **kwargs): + context = super(AdminProfileView, self).get_context_data(**kwargs) + context['user'] = self.admin_get_user(self.kwargs.get('username')) + return context + + +class AdminUpdateProfileView(AdminMixin,GetUserMixin,FormView): + template_name = 'warrant/admin-update-profile.html' + form_class = AdminProfileForm + + def test_func(self): + return self.request.user.is_staff + + def get_success_url(self): + return reverse_lazy('dw:admin-cognito-users') + + def get_initial(self): + self.user = self.admin_get_user( + self.kwargs.get('username')) + return self.user._data + + def form_valid(self, form): + if not self.user: + self.user = self.admin_get_user( + self.kwargs.get('username')) + self.user._data = form.cleaned_data + self.user.save(admin=True) + messages.success(self.request, + "You have successfully updated {}'s profile.".format( + self.kwargs.get('username'))) + return super(AdminUpdateProfileView, self).form_valid(form) \ No newline at end of file diff --git a/django_warrant/views/mixins.py b/django_warrant/views/mixins.py new file mode 100644 index 0000000..0bb82df --- /dev/null +++ b/django_warrant/views/mixins.py @@ -0,0 +1,48 @@ +from django.conf import settings +from django.contrib.auth.mixins import LoginRequiredMixin, \ + UserPassesTestMixin, AccessMixin + +from django_warrant.models import UserObj +from django_warrant.utils import cog_client, apigw_client + + +class AdminMixin(LoginRequiredMixin,UserPassesTestMixin): + + def test_func(self): + return self.request.user.is_staff + + +class GetUserMixin(object): + + def get_user(self): + return UserObj(cog_client.get_user( + AccessToken=self.request.session['ACCESS_TOKEN']), + request=self.request) + + def admin_get_user(self,username): + return UserObj(cog_client.admin_get_user( + UserPoolId=settings.COGNITO_USER_POOL_ID, + Username=username)) + + +class TokenMixin(AccessMixin): + + def dispatch(self, request, *args, **kwargs): + if not request.session.get('REFRESH_TOKEN'): + return self.handle_no_permission() + return super(TokenMixin, self).dispatch( + request, *args, **kwargs) + + +class GetSubscriptionsMixin(GetUserMixin): + + def get_subscriptions(self,api_key_id): + return apigw_client.get_usage_plans( + keyId=api_key_id).get('items', []) + + def get_user_subscriptions(self): + return self.get_subscriptions(self.get_user().api_key_id) + + def get_admin_subscriptions(self): + return self.get_subscriptions(self.admin_get_user( + self.kwargs.get('username')).api_key_id) \ No newline at end of file diff --git a/django_warrant/views/profile.py b/django_warrant/views/profile.py index 53ab205..d25605c 100644 --- a/django_warrant/views/profile.py +++ b/django_warrant/views/profile.py @@ -1,7 +1,9 @@ -from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin +from django.contrib.auth.mixins import LoginRequiredMixin from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache +from django_warrant.views.mixins import GetUserMixin, TokenMixin + try: from django.urls import reverse_lazy except ImportError: @@ -9,25 +11,9 @@ from django.views.generic import FormView, TemplateView from django.contrib import messages from django.contrib.auth.views import LogoutView as DJLogoutView -from django.conf import settings - -from django_warrant.utils import get_cognito -from django_warrant.forms import ProfileForm -class TokenMixin(AccessMixin): - - def dispatch(self, request, *args, **kwargs): - if not request.session.get('REFRESH_TOKEN'): - return self.handle_no_permission() - return super(TokenMixin, self).dispatch( - request, *args, **kwargs) - -class GetUserMixin(object): - - def get_user(self): - c = get_cognito(self.request) - return c.get_user(attr_map=settings.COGNITO_ATTR_MAPPING) +from django_warrant.forms import ProfileForm class ProfileView(LoginRequiredMixin,TokenMixin,GetUserMixin,TemplateView): @@ -47,12 +33,14 @@ def get_success_url(self): return reverse_lazy('dw:profile') def get_initial(self): - u = self.get_user() - return u.__dict__.get('_data') + self.user = self.get_user() + return self.user._data def form_valid(self, form): - c = get_cognito(self.request) - c.update_profile(form.cleaned_data,settings.COGNITO_ATTR_MAPPING) + if not self.user: + self.user = self.get_user() + self.user._data = form.cleaned_data + self.user.save() messages.success(self.request,'You have successfully updated your profile.') return super(UpdateProfileView, self).form_valid(form) @@ -62,5 +50,4 @@ class LogoutView(DJLogoutView): @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): request.session.delete() - return super(LogoutView, self).dispatch(request, *args, **kwargs) - + return super(LogoutView, self).dispatch(request, *args, **kwargs) \ No newline at end of file diff --git a/django_warrant/views/subscriptions.py b/django_warrant/views/subscriptions.py index 7c98a83..dd6c577 100644 --- a/django_warrant/views/subscriptions.py +++ b/django_warrant/views/subscriptions.py @@ -1,95 +1,11 @@ -import boto3 -from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import ListView -from django.contrib.auth.mixins import LoginRequiredMixin, \ - UserPassesTestMixin -from django.http import Http404 -try: - from django.urls import reverse_lazy -except ImportError: - from django.core.urlresolvers import reverse_lazy -from django.views.generic import FormView -from django.views.generic.list import MultipleObjectMixin, ListView +from django_warrant.views.mixins import GetSubscriptionsMixin -from django.conf import settings -from warrant import UserObj, Cognito -from django_warrant.forms import APIKeySubscriptionForm - -class GetCognitoUserMixin(object): - client = boto3.client('apigateway') - - def get_user_object(self): - cog_client = boto3.client('cognito-idp') - user = cog_client.get_user( - AccessToken=self.request.session['ACCESS_TOKEN']) - u = UserObj(username=user.get('UserAttributes')[0].get('username'), - attribute_list=user.get('UserAttributes'), - attr_map=settings.COGNITO_ATTR_MAPPING) - return u - - def get_queryset(self): - try: - u = self.get_user_object() - except KeyError: - raise Http404 - my_plans = self.client.get_usage_plans(keyId=u.api_key_id) - return my_plans.get('items',[]) - - -class MySubsriptions(LoginRequiredMixin,GetCognitoUserMixin,ListView): +class SubscriptionsView(LoginRequiredMixin,GetSubscriptionsMixin,ListView): template_name = 'warrant/subscriptions.html' - -class AdminListUsers(UserPassesTestMixin,ListView): - template_name = 'warrant/admin-list-users.html' - - def test_func(self): - return self.request.user.is_staff - def get_queryset(self): - response = Cognito(settings.COGNITO_USER_POOL_ID,settings.COGNITO_APP_ID)\ - .get_users(attr_map=settings.COGNITO_ATTR_MAPPING) - return response - - -class AdminSubscriptions(UserPassesTestMixin,GetCognitoUserMixin, - FormView): - template_name = 'warrant/admin-subscriptions.html' - form_class = APIKeySubscriptionForm - - def get_success_url(self): - return reverse_lazy('dw:admin-cognito-user', - args=[self.kwargs.get('username')]) - - def test_func(self): - return self.request.user.has_perm('can_edit') - - def get_user_object(self): - return Cognito(settings.COGNITO_USER_POOL_ID,settings.COGNITO_APP_ID, - username=self.kwargs.get('username')).admin_get_user( - attr_map=settings.COGNITO_ATTR_MAPPING) - - def get_context_data(self, **kwargs): - kwargs['object_list'] = self.object_list = self.get_queryset() - context = super(AdminSubscriptions, self).get_context_data(**kwargs) - return context - - def get_form_kwargs(self): - kwargs = super(AdminSubscriptions, self).get_form_kwargs() - kwargs.update({'plans':self.client.get_usage_plans().get('items',[]), - 'users_plans':[p.get('id') for p in self.get_queryset()]}) - return kwargs - - def form_invalid(self, form): - - return super(AdminSubscriptions, self).form_invalid(form) - - def form_valid(self, form): - self.client.create_usage_plan_key( - usagePlanId=form.cleaned_data['plan'], - keyId=self.get_user_object().api_key_id, - keyType='API_KEY' - ) - messages.success(self.request,'Addedd subscription successfully.') - return super(AdminSubscriptions, self).form_valid(form) \ No newline at end of file + return self.get_user_subscriptions() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d65c599..3f60b36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ Django>=2.0 django-braces>=1.11.0 django-crispy-forms>=1.6.1 django-extensions>=1.7.7 -warrant>=0.2.0 +python-jose>=3.0.0 +warrant-lite>=1.0.2 From 58f8f99e2e159287a830e9053b0d24d720ce912f Mon Sep 17 00:00:00 2001 From: Brian Jinwright Date: Wed, 30 May 2018 14:40:43 -0400 Subject: [PATCH 4/7] Added forgot password views and forms, added get_user util --- cdu/settings.py | 4 +- cdu/urls.py | 2 +- django_warrant/forms.py | 20 +++++++ django_warrant/models.py | 15 ++++- .../warrant/confirm-forgot-password.html | 3 + .../templates/warrant/forgot-password.html | 3 + django_warrant/templates/warrant/form.html | 11 ++++ django_warrant/templates/warrant/login.html | 14 +---- .../templates/warrant/update-profile.html | 11 +--- django_warrant/urls.py | 6 +- django_warrant/utils.py | 28 +++++++++ django_warrant/views/mixins.py | 6 +- django_warrant/views/profile.py | 58 ++++++++++++++++++- 13 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 django_warrant/templates/warrant/confirm-forgot-password.html create mode 100644 django_warrant/templates/warrant/forgot-password.html create mode 100644 django_warrant/templates/warrant/form.html diff --git a/cdu/settings.py b/cdu/settings.py index b2032f8..c6cb306 100644 --- a/cdu/settings.py +++ b/cdu/settings.py @@ -140,8 +140,8 @@ }, ] -LOGIN_REDIRECT_URL = '/accounts/profile' - +LOGIN_REDIRECT_URL = '/' +LOGIN_URL = '/login/' # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ diff --git a/cdu/urls.py b/cdu/urls.py index 9414425..9dc1bdb 100644 --- a/cdu/urls.py +++ b/cdu/urls.py @@ -18,5 +18,5 @@ urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^accounts/', include('django_warrant.urls',namespace='dw')) + url(r'^', include('django_warrant.urls',namespace='dw')) ] diff --git a/django_warrant/forms.py b/django_warrant/forms.py index 31f52b2..6d71041 100644 --- a/django_warrant/forms.py +++ b/django_warrant/forms.py @@ -1,4 +1,6 @@ from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ class ProfileForm(forms.Form): @@ -22,3 +24,21 @@ class APIKeySubscriptionForm(forms.Form): def __init__(self, plans=[], users_plans=[], *args, **kwargs): self.base_fields['plan'].choices = [(p.get('id'),p.get('name')) for p in plans if not p.get('id') in users_plans] super(APIKeySubscriptionForm, self).__init__(*args, **kwargs) + + +class ForgotPasswordForm(forms.Form): + username = forms.CharField(max_length=200,required=True) + + +class ConfirmForgotPasswordForm(forms.Form): + username = forms.CharField(max_length=200,required=True) + verification_code = forms.CharField(max_length=6) + password = forms.CharField(widget=forms.PasswordInput,required=True,max_length=200) + confirm_password = forms.CharField(widget=forms.PasswordInput, required=True, max_length=200) + + def clean_confirm_password(self): + password = self.cleaned_data['password'] + confirm_password = self.cleaned_data['confirm_password'] + if password != confirm_password: + raise ValidationError(_('The passwords entered do not match.')) + return confirm_password diff --git a/django_warrant/models.py b/django_warrant/models.py index 9274de1..b5482b8 100644 --- a/django_warrant/models.py +++ b/django_warrant/models.py @@ -3,8 +3,8 @@ from django.conf import settings from jose import jwt -from django_warrant.utils import cognito_to_dict, dict_to_cognito, cog_client - +from django_warrant.utils import cognito_to_dict, dict_to_cognito, cog_client, refresh_access_token +from botocore.exceptions import BotoCoreError class UserObj(object): @@ -113,3 +113,14 @@ def delete(self,admin=False): Username=self.username ) return + +def get_user(request): + try: + return UserObj(cog_client.get_user( + AccessToken=request.session['ACCESS_TOKEN']), + request=request) + except Exception: + refresh_access_token(request) + return UserObj(cog_client.get_user( + AccessToken=request.session['ACCESS_TOKEN']), + request=request) diff --git a/django_warrant/templates/warrant/confirm-forgot-password.html b/django_warrant/templates/warrant/confirm-forgot-password.html new file mode 100644 index 0000000..3eebcc6 --- /dev/null +++ b/django_warrant/templates/warrant/confirm-forgot-password.html @@ -0,0 +1,3 @@ +{% extends 'warrant/form.html' %} + +{% block h1_title %}Confirm Verification Code{% endblock h1_title %} \ No newline at end of file diff --git a/django_warrant/templates/warrant/forgot-password.html b/django_warrant/templates/warrant/forgot-password.html new file mode 100644 index 0000000..3397fc5 --- /dev/null +++ b/django_warrant/templates/warrant/forgot-password.html @@ -0,0 +1,3 @@ +{% extends 'warrant/form.html' %} + +{% block h1_title %}Forgot Password{% endblock h1_title %} \ No newline at end of file diff --git a/django_warrant/templates/warrant/form.html b/django_warrant/templates/warrant/form.html new file mode 100644 index 0000000..f3e9180 --- /dev/null +++ b/django_warrant/templates/warrant/form.html @@ -0,0 +1,11 @@ +{% extends 'warrant/base.html' %} +{% load crispy_forms_tags %} +{% block h1_title %}Update Your Profile{% endblock h1_title %} +{% block main_content %} + + {% csrf_token %} + {{ form|crispy }} + + + {% block extra_form_info %}{% endblock extra_form_info %} +{% endblock %} \ No newline at end of file diff --git a/django_warrant/templates/warrant/login.html b/django_warrant/templates/warrant/login.html index b54997e..c2dd77a 100644 --- a/django_warrant/templates/warrant/login.html +++ b/django_warrant/templates/warrant/login.html @@ -1,13 +1,5 @@ -{% extends 'warrant/base.html' %} +{% extends 'warrant/form.html' %} {% block title %}Login{% endblock title %} -{% block main_content %} -
-
- {% csrf_token %} -
- {{ form.as_table }} -
- - -
+{% block extra_form_info %} + Forgot Password | Sign Up {% endblock %} diff --git a/django_warrant/templates/warrant/update-profile.html b/django_warrant/templates/warrant/update-profile.html index 30335f2..431cc3e 100644 --- a/django_warrant/templates/warrant/update-profile.html +++ b/django_warrant/templates/warrant/update-profile.html @@ -1,10 +1,3 @@ -{% extends 'warrant/base.html' %} -{% load crispy_forms_tags %} +{% extends 'warrant/form.html' %} + {% block h1_title %}Update Your Profile{% endblock h1_title %} -{% block main_content %} -
- {% csrf_token %} - {{ form|crispy }} - -
-{% endblock %} \ No newline at end of file diff --git a/django_warrant/urls.py b/django_warrant/urls.py index 5f6841d..b23986e 100644 --- a/django_warrant/urls.py +++ b/django_warrant/urls.py @@ -2,7 +2,7 @@ from django.contrib.auth import views as auth_views from django.urls import re_path -from django_warrant.views import SubscriptionsView, AdminProfileView +from django_warrant.views import SubscriptionsView, AdminProfileView, ForgotPasswordView, ConfirmForgotPasswordView from .views import ProfileView,UpdateProfileView,\ AdminListUsers,LogoutView,AdminUpdateProfileView @@ -11,7 +11,9 @@ urlpatterns = ( re_path(r'^login/$', auth_views.login, {'template_name': 'warrant/login.html'}, name='login'), re_path(r'^logout/$', LogoutView.as_view(), {'template_name': 'warrant/logout.html'}, name='logout'), - re_path(r'^profile/$', ProfileView.as_view(),name='profile'), + re_path(r'^forgot-password/$', ForgotPasswordView.as_view(), name='forgot-password'), + re_path(r'^confirm-forgot-password/$', ConfirmForgotPasswordView.as_view(), name='confirm-forgot-password'), + re_path(r'^$', ProfileView.as_view(),name='profile'), re_path(r'^subscriptions/$', SubscriptionsView.as_view(),name='subscriptions'), re_path(r'^profile/update/$', UpdateProfileView.as_view(),name='update-profile'), re_path(r'^admin/cognito-users/$', AdminListUsers.as_view(),name='admin-cognito-users'), diff --git a/django_warrant/utils.py b/django_warrant/utils.py index a0d5ff7..4b1a32e 100644 --- a/django_warrant/utils.py +++ b/django_warrant/utils.py @@ -1,10 +1,12 @@ import boto3 from django.conf import settings +from warrant_lite import WarrantLite apigw_client = boto3.client('apigateway') cog_client = boto3.client('cognito-idp') + def cognito_to_dict(attr_list,mapping): user_attrs = dict() for i in attr_list: @@ -14,6 +16,7 @@ def cognito_to_dict(attr_list,mapping): user_attrs[name] = value return user_attrs + def dict_to_cognito(attr_dict,mapping): cognito_list = list() inv_map = {v: k for k, v in mapping.items()} @@ -22,6 +25,7 @@ def dict_to_cognito(attr_dict,mapping): cognito_list.append({'Name':name,'Value':v}) return cognito_list + def user_obj_to_django(user_obj): c_attrs = settings.COGNITO_ATTR_MAPPING user_attrs = dict() @@ -32,3 +36,27 @@ def user_obj_to_django(user_obj): return user_attrs +def refresh_access_token(request): + """ + Sets a new access token on the User using the refresh token. + """ + username = request.user.username + refresh_token = request.session['REFRESH_TOKEN'] + auth_params = {'REFRESH_TOKEN': refresh_token} + if settings.COGNITO_CLIENT_SECRET: + auth_params['SECRET_HASH'] = WarrantLite.get_secret_hash(username, + settings.COGNITO_APP_ID, + settings.COGNITO_CLIENT_SECRET) + refresh_response = cog_client.initiate_auth( + ClientId=settings.COGNITO_APP_ID, + AuthFlow='REFRESH_TOKEN', + AuthParameters=auth_params, + ) + request.session['ACCESS_TOKEN'] = refresh_response['AuthenticationResult']['AccessToken'] + request.session['ID_TOKEN'] = refresh_response['AuthenticationResult']['IdToken'] + request.session.save() + return { + 'access_token': refresh_response['AuthenticationResult']['AccessToken'], + 'id_token': refresh_response['AuthenticationResult']['IdToken'], + 'token_type': refresh_response['AuthenticationResult']['TokenType'] + } \ No newline at end of file diff --git a/django_warrant/views/mixins.py b/django_warrant/views/mixins.py index 0bb82df..7d3b59e 100644 --- a/django_warrant/views/mixins.py +++ b/django_warrant/views/mixins.py @@ -2,7 +2,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, \ UserPassesTestMixin, AccessMixin -from django_warrant.models import UserObj +from django_warrant.models import UserObj, get_user from django_warrant.utils import cog_client, apigw_client @@ -15,9 +15,7 @@ def test_func(self): class GetUserMixin(object): def get_user(self): - return UserObj(cog_client.get_user( - AccessToken=self.request.session['ACCESS_TOKEN']), - request=self.request) + return get_user(self.request) def admin_get_user(self,username): return UserObj(cog_client.admin_get_user( diff --git a/django_warrant/views/profile.py b/django_warrant/views/profile.py index d25605c..f07e988 100644 --- a/django_warrant/views/profile.py +++ b/django_warrant/views/profile.py @@ -1,7 +1,12 @@ +from botocore.exceptions import ClientError + +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.utils.decorators import method_decorator +from django.utils.translation import gettext as _ from django.views.decorators.cache import never_cache +from django_warrant.utils import cog_client from django_warrant.views.mixins import GetUserMixin, TokenMixin try: @@ -13,7 +18,7 @@ from django.contrib.auth.views import LogoutView as DJLogoutView -from django_warrant.forms import ProfileForm +from django_warrant.forms import ProfileForm, ForgotPasswordForm, ConfirmForgotPasswordForm class ProfileView(LoginRequiredMixin,TokenMixin,GetUserMixin,TemplateView): @@ -41,7 +46,7 @@ def form_valid(self, form): self.user = self.get_user() self.user._data = form.cleaned_data self.user.save() - messages.success(self.request,'You have successfully updated your profile.') + messages.success(self.request,_('You have successfully updated your profile.')) return super(UpdateProfileView, self).form_valid(form) @@ -50,4 +55,51 @@ class LogoutView(DJLogoutView): @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): request.session.delete() - return super(LogoutView, self).dispatch(request, *args, **kwargs) \ No newline at end of file + return super(LogoutView, self).dispatch(request, *args, **kwargs) + + +class ForgotPasswordView(FormView): + template_name = 'warrant/forgot-password.html' + form_class = ForgotPasswordForm + success_url = reverse_lazy('dw:confirm-forgot-password') + + def form_valid(self, form): + try: + resp = cog_client.forgot_password( + ClientId=settings.COGNITO_APP_ID, + Username=form.cleaned_data['username'] + )['CodeDeliveryDetails'] + + messages.success(self.request, + _('Confirmation code delivered to {} by {}'.format( + resp['Destination'],resp['DeliveryMedium']))) + return super(ForgotPasswordView, self).form_valid(form) + except ClientError: + form.add_error('username',_('That user does not exist')) + return self.form_invalid(form) + + +class ConfirmForgotPasswordView(FormView): + template_name = 'warrant/confirm-forgot-password.html' + form_class = ConfirmForgotPasswordForm + success_url = reverse_lazy('dw:profile') + + def form_valid(self, form): + try: + resp = cog_client.confirm_forgot_password( + ClientId=settings.COGNITO_APP_ID, + Username=form.cleaned_data['username'], + ConfirmationCode=form.cleaned_data['verification_code'], + Password=form.cleaned_data['password'] + ) + if resp['ResponseMetadata']['HTTPStatusCode'] != 200: + messages.error(self.request, + _('We could not verify either your verification code or username')) + else: + messages.success(self.request, + _('You have successfully changed your password.')) + return super(ConfirmForgotPasswordView, self).form_valid(form) + except ClientError as e: + + form.add_error('verification_code',e.response['Error']['Message']) + return self.form_invalid(form) \ No newline at end of file From 7284cf5de7ad4ba186a4cf884302dd8727c74055 Mon Sep 17 00:00:00 2001 From: Brian Jinwright Date: Sat, 2 Jun 2018 16:36:27 -0400 Subject: [PATCH 5/7] condensed alot of the views in profiles using the CognitoFormView class, updated registration workflow, and FINALLY got a working Django auth backend without a RDBMS using just Cognito --- cdu/settings.py | 8 +- django_warrant/__init__.py | 3 +- django_warrant/backend.py | 31 +++- django_warrant/forms.py | 27 +++- django_warrant/middleware.py | 23 +++ django_warrant/models.py | 48 ++++++- django_warrant/templates/warrant/login.html | 3 +- .../templates/warrant/registration.html | 3 + django_warrant/urls.py | 6 +- django_warrant/views/profile.py | 133 +++++++++++++----- django_warrant/views/subscriptions.py | 4 +- 11 files changed, 231 insertions(+), 58 deletions(-) create mode 100644 django_warrant/templates/warrant/registration.html diff --git a/cdu/settings.py b/cdu/settings.py index c6cb306..d1cc60f 100644 --- a/cdu/settings.py +++ b/cdu/settings.py @@ -29,8 +29,7 @@ ALLOWED_HOSTS = [] AUTHENTICATION_BACKENDS = [ - 'django_warrant.backend.CognitoBackend', - 'django.contrib.auth.backends.ModelBackend' + 'django_warrant.backend.CognitoNoModelBackend' ] COGNITO_TEST_USERNAME = env('COGNITO_TEST_USERNAME') @@ -49,6 +48,7 @@ 'email': 'email', 'given_name': 'first_name', 'family_name': 'last_name', + 'name':'name', 'username':'username', 'address':'address', 'gender':'gender', @@ -83,7 +83,7 @@ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django_warrant.middleware.CognitoAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -162,3 +162,5 @@ STATIC_URL = '/static/' CRISPY_TEMPLATE_PACK = 'bootstrap3' + +# AUTH_USER_MODEL = 'django_warrant.UserObj' \ No newline at end of file diff --git a/django_warrant/__init__.py b/django_warrant/__init__.py index 1945ad5..39a4597 100644 --- a/django_warrant/__init__.py +++ b/django_warrant/__init__.py @@ -6,7 +6,8 @@ def add_user_tokens(sender, user, **kwargs): """ Add Cognito tokens to the session upon login """ - if user.backend == 'django_warrant.backend.CognitoBackend': + if user.backend in ['django_warrant.backend.CognitoBackend', + 'django_warrant.backend.CognitoNoModelBackend']: request = kwargs['request'] request.session['ACCESS_TOKEN'] = user.access_token request.session['ID_TOKEN'] = user.id_token diff --git a/django_warrant/backend.py b/django_warrant/backend.py index e0657d3..16ec4d7 100644 --- a/django_warrant/backend.py +++ b/django_warrant/backend.py @@ -6,8 +6,10 @@ from django.conf import settings from django.contrib.auth.backends import ModelBackend from django.contrib.auth import get_user_model +from django.utils.crypto import salted_hmac from warrant_lite import WarrantLite +from django_warrant.models import UserObj from .utils import cognito_to_dict @@ -105,7 +107,7 @@ def authenticate(self, request, username=None, password=None): return user -class CognitoNoModelBackend(object): +class CognitoNoModelBackend(ModelBackend): def authenticate(self, request, username=None, password=None): wl = WarrantLite(username=username, password=password, @@ -124,12 +126,35 @@ def authenticate(self, request, username=None, password=None): wl.verify_token(access_token, 'access_token', 'access') wl.verify_token(id_token, 'id_token', 'id') - user = wl.client.get_user( + user = UserObj(wl.client.get_user( AccessToken=access_token - ) + ),access_token=access_token,is_authenticated=True) + user.refresh_token = refresh_token + user.access_token = access_token + user.id_token = id_token + user.session_auth_hash = get_session_auth_hash(password) if user: request.session['ACCESS_TOKEN'] = user.access_token request.session['ID_TOKEN'] = user.id_token request.session['REFRESH_TOKEN'] = user.refresh_token request.session.save() return user + + def user_can_authenticate(self, user): + """ + Reject users with is_active=False. Custom user models that don't have + that attribute are allowed. + """ + is_active = getattr(user, 'is_active', None) + return is_active or is_active is None + + def get_user(self,user_id): + pass + + +def get_session_auth_hash(password): + """ + Return an HMAC of the password field. + """ + key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash" + return salted_hmac(key_salt, password).hexdigest() \ No newline at end of file diff --git a/django_warrant/forms.py b/django_warrant/forms.py index 6d71041..3586fde 100644 --- a/django_warrant/forms.py +++ b/django_warrant/forms.py @@ -7,7 +7,7 @@ class ProfileForm(forms.Form): first_name = forms.CharField(max_length=200,required=True) last_name = forms.CharField(max_length=200,required=True) email = forms.EmailField(required=True) - phone_number = forms.CharField(max_length=30,required=True) + phone_number = forms.CharField(max_length=30,required=True,help_text='ex. +14325551212') gender = forms.ChoiceField(choices=(('female','Female'),('male','Male')),required=True) address = forms.CharField(max_length=200,required=True) preferred_username = forms.CharField(max_length=200,required=True) @@ -30,10 +30,8 @@ class ForgotPasswordForm(forms.Form): username = forms.CharField(max_length=200,required=True) -class ConfirmForgotPasswordForm(forms.Form): - username = forms.CharField(max_length=200,required=True) - verification_code = forms.CharField(max_length=6) - password = forms.CharField(widget=forms.PasswordInput,required=True,max_length=200) +class PasswordForm(forms.Form): + password = forms.CharField(widget=forms.PasswordInput, required=True, max_length=200) confirm_password = forms.CharField(widget=forms.PasswordInput, required=True, max_length=200) def clean_confirm_password(self): @@ -42,3 +40,22 @@ def clean_confirm_password(self): if password != confirm_password: raise ValidationError(_('The passwords entered do not match.')) return confirm_password + + +class VerificationCodeForm(forms.Form): + username = forms.CharField(max_length=200,required=True) + verification_code = forms.CharField(max_length=6) + + +class ConfirmForgotPasswordForm(VerificationCodeForm,PasswordForm): + pass + + +class RegistrationForm(ProfileForm,PasswordForm,ForgotPasswordForm): + pass + + +class UpdatePasswordForm(PasswordForm): + previous_password = forms.CharField(max_length=200, + widget=forms.PasswordInput, + required=True) \ No newline at end of file diff --git a/django_warrant/middleware.py b/django_warrant/middleware.py index 8fb1475..17215cd 100644 --- a/django_warrant/middleware.py +++ b/django_warrant/middleware.py @@ -1,3 +1,7 @@ +from django.conf import settings +from django.utils.deprecation import MiddlewareMixin +from django.utils.functional import SimpleLazyObject +from django_warrant.models import get_user as gu class APIKeyMiddleware(object): """ @@ -22,3 +26,22 @@ def process_request(request): request.api_key = request.META['HTTP_AUTHORIZATION_ID'] return None + + +def get_user(request): + if not hasattr(request, '_cached_user'): + + request._cached_user = gu(request) + + return request._cached_user + + +class CognitoAuthenticationMiddleware(MiddlewareMixin): + def process_request(self, request): + assert hasattr(request, 'session'), ( + "The Django authentication middleware requires session middleware " + "to be installed. Edit your MIDDLEWARE%s setting to insert " + "'django.contrib.sessions.middleware.SessionMiddleware' before " + "'django.contrib.auth.middleware.AuthenticationMiddleware'." + ) % ("_CLASSES" if settings.MIDDLEWARE is None else "") + request.user = get_user(request) diff --git a/django_warrant/models.py b/django_warrant/models.py index b5482b8..fba4823 100644 --- a/django_warrant/models.py +++ b/django_warrant/models.py @@ -4,11 +4,33 @@ from jose import jwt from django_warrant.utils import cognito_to_dict, dict_to_cognito, cog_client, refresh_access_token -from botocore.exceptions import BotoCoreError + + +class Meta(object): + + def __init__(self,pk): + self.pk = pk + +class PK(object): + + def __init__(self,sub): + self.value = sub + + def value_to_string(self,obj): + return self.value + + def to_python(self,obj): + return obj + + +class AnonUserObj(object): + is_authenticated = False + class UserObj(object): - def __init__(self, attribute_list, metadata=None, request=None): + def __init__(self, attribute_list, metadata=None, request=None, + password_hash=None,access_token=None,is_authenticated=False): """ :param attribute_list: :param metadata: Dictionary of User metadata @@ -19,15 +41,20 @@ def __init__(self, attribute_list, metadata=None, request=None): self._data = cognito_to_dict( attribute_list.get('UserAttributes') or attribute_list.get('Attributes'),self._attr_map) - + self.is_authenticated = is_authenticated self.sub = self._data.pop('sub',None) + self.pk = self.sub + self.id = self.sub self.email_verified = self._data.pop('email_verified',None) self.phone_number_verified = self._data.pop('phone_number_verified',None) self._metadata = {} if metadata is None else metadata + self._meta = Meta(PK(self._data.get('sub'))) if request: self.access_token = request.session['ACCESS_TOKEN'] self.refresh_token = request.session['REFRESH_TOKEN'] self.id_token = request.session['ID_TOKEN'] + elif access_token: + self.access_token = access_token def __repr__(self): return '<{class_name}: {uni}>'.format( @@ -48,6 +75,13 @@ def __setattr__(self, name, value): else: super(UserObj, self).__setattr__(name, value) + @property + def is_active(self): + return self._data.get('email') + + def get_session_auth_hash(self): + return self.session_auth_hash + def check_token(self, renew=True): """ Checks the exp attribute of the access_token and either refreshes @@ -85,7 +119,7 @@ def renew_access_token(self): self.token_type = refresh_response['AuthenticationResult']['TokenType'] - def save(self,admin=False,create=False,password=None): + def save(self,admin=False,create=False,password=None,update_fields=None): if not create: if admin: cog_client.admin_update_user_attributes( @@ -115,12 +149,14 @@ def delete(self,admin=False): return def get_user(request): + if not request.session.get('ACCESS_TOKEN'): + return AnonUserObj() try: return UserObj(cog_client.get_user( AccessToken=request.session['ACCESS_TOKEN']), - request=request) + request=request,is_authenticated=True) except Exception: refresh_access_token(request) return UserObj(cog_client.get_user( AccessToken=request.session['ACCESS_TOKEN']), - request=request) + request=request,is_authenticated=True) diff --git a/django_warrant/templates/warrant/login.html b/django_warrant/templates/warrant/login.html index c2dd77a..1577187 100644 --- a/django_warrant/templates/warrant/login.html +++ b/django_warrant/templates/warrant/login.html @@ -1,5 +1,6 @@ {% extends 'warrant/form.html' %} {% block title %}Login{% endblock title %} +{% block h1_title %}Login{% endblock %} {% block extra_form_info %} - Forgot Password | Sign Up + Forgot Password | Sign Up {% endblock %} diff --git a/django_warrant/templates/warrant/registration.html b/django_warrant/templates/warrant/registration.html new file mode 100644 index 0000000..b984030 --- /dev/null +++ b/django_warrant/templates/warrant/registration.html @@ -0,0 +1,3 @@ +{% extends 'warrant/form.html' %} + +{% block h1_title %}Registration{% endblock h1_title %} diff --git a/django_warrant/urls.py b/django_warrant/urls.py index b23986e..88d1da7 100644 --- a/django_warrant/urls.py +++ b/django_warrant/urls.py @@ -2,7 +2,8 @@ from django.contrib.auth import views as auth_views from django.urls import re_path -from django_warrant.views import SubscriptionsView, AdminProfileView, ForgotPasswordView, ConfirmForgotPasswordView +from django_warrant.views import SubscriptionsView, AdminProfileView, ForgotPasswordView, ConfirmForgotPasswordView, \ + RegistrationView, ConfirmRegistrationView, UpdatePasswordView from .views import ProfileView,UpdateProfileView,\ AdminListUsers,LogoutView,AdminUpdateProfileView @@ -12,6 +13,9 @@ re_path(r'^login/$', auth_views.login, {'template_name': 'warrant/login.html'}, name='login'), re_path(r'^logout/$', LogoutView.as_view(), {'template_name': 'warrant/logout.html'}, name='logout'), re_path(r'^forgot-password/$', ForgotPasswordView.as_view(), name='forgot-password'), + re_path(r'^register/$', RegistrationView.as_view(), name='register'), + re_path(r'^confirm-register/$', ConfirmRegistrationView.as_view(), name='confirm-register'), + re_path(r'^update-password/$', UpdatePasswordView.as_view(), name='update-password'), re_path(r'^confirm-forgot-password/$', ConfirmForgotPasswordView.as_view(), name='confirm-forgot-password'), re_path(r'^$', ProfileView.as_view(),name='profile'), re_path(r'^subscriptions/$', SubscriptionsView.as_view(),name='subscriptions'), diff --git a/django_warrant/views/profile.py b/django_warrant/views/profile.py index f07e988..45d9811 100644 --- a/django_warrant/views/profile.py +++ b/django_warrant/views/profile.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext as _ from django.views.decorators.cache import never_cache -from django_warrant.utils import cog_client +from django_warrant.utils import cog_client, dict_to_cognito from django_warrant.views.mixins import GetUserMixin, TokenMixin try: @@ -18,7 +18,8 @@ from django.contrib.auth.views import LogoutView as DJLogoutView -from django_warrant.forms import ProfileForm, ForgotPasswordForm, ConfirmForgotPasswordForm +from django_warrant.forms import ProfileForm, ForgotPasswordForm, ConfirmForgotPasswordForm, RegistrationForm, \ + VerificationCodeForm, UpdatePasswordForm class ProfileView(LoginRequiredMixin,TokenMixin,GetUserMixin,TemplateView): @@ -58,48 +59,108 @@ def dispatch(self, request, *args, **kwargs): return super(LogoutView, self).dispatch(request, *args, **kwargs) -class ForgotPasswordView(FormView): - template_name = 'warrant/forgot-password.html' - form_class = ForgotPasswordForm - success_url = reverse_lazy('dw:confirm-forgot-password') +class CognitoFormView(FormView): + success_message = None + client_error_field = None + + def get_success_message(self,resp): + return self.success_message + + def cognito_command(self,form): + return {} def form_valid(self, form): try: - resp = cog_client.forgot_password( - ClientId=settings.COGNITO_APP_ID, - Username=form.cleaned_data['username'] - )['CodeDeliveryDetails'] - - messages.success(self.request, - _('Confirmation code delivered to {} by {}'.format( - resp['Destination'],resp['DeliveryMedium']))) - return super(ForgotPasswordView, self).form_valid(form) - except ClientError: - form.add_error('username',_('That user does not exist')) + resp = self.cognito_command(form) + messages.success(self.request,_(self.get_success_message(resp))) + return super(CognitoFormView, self).form_valid(form) + except ClientError as e: + form.add_error(self.client_error_field, e.response['Error']['Message']) return self.form_invalid(form) -class ConfirmForgotPasswordView(FormView): +class ForgotPasswordView(CognitoFormView): + template_name = 'warrant/forgot-password.html' + form_class = ForgotPasswordForm + success_url = reverse_lazy('dw:confirm-forgot-password') + success_message = 'Confirmation code delivered to {} by {}' + client_error_field = 'username' + + def cognito_command(self,form): + return cog_client.forgot_password( + ClientId=settings.COGNITO_APP_ID, + Username=form.cleaned_data['username'] + ) + + def get_success_message(self,resp): + return _(self.success_message.format( + resp['CodeDeliveryDetails']['Destination'], + resp['CodeDeliveryDetails']['DeliveryMedium'] + )) + + +class ConfirmForgotPasswordView(CognitoFormView): template_name = 'warrant/confirm-forgot-password.html' form_class = ConfirmForgotPasswordForm success_url = reverse_lazy('dw:profile') + success_message = 'You have successfully changed your password.' + + def cognito_command(self,form): + return cog_client.confirm_forgot_password( + ClientId=settings.COGNITO_APP_ID, + Username=form.cleaned_data['username'], + ConfirmationCode=form.cleaned_data['verification_code'], + Password=form.cleaned_data['password'] + ) + + +class RegistrationView(CognitoFormView): + template_name = 'warrant/registration.html' + form_class = RegistrationForm + success_message = 'Confirmation code delivered to {} by {}' + success_url = reverse_lazy('dw:confirm-register') + def get_success_message(self,resp): + return _(self.success_message.format( + resp['CodeDeliveryDetails']['Destination'], + resp['CodeDeliveryDetails']['DeliveryMedium'] + )) + + def cognito_command(self,form): + cv = form.cleaned_data.copy() + cv.pop('confirm_password') + cv['name'] = '{} {}'.format(cv['first_name'],cv['last_name']) + return cog_client.sign_up( + ClientId=settings.COGNITO_APP_ID, + Username=cv.pop('username'), + Password=cv.pop('password'), + UserAttributes=dict_to_cognito(cv, + settings.COGNITO_ATTR_MAPPING) + ) + + +class ConfirmRegistrationView(CognitoFormView): + template_name = 'warrant/registration.html' + form_class = VerificationCodeForm + success_message = 'You have successfully registered.' + success_url = reverse_lazy('dw:profile') - def form_valid(self, form): - try: - resp = cog_client.confirm_forgot_password( - ClientId=settings.COGNITO_APP_ID, - Username=form.cleaned_data['username'], - ConfirmationCode=form.cleaned_data['verification_code'], - Password=form.cleaned_data['password'] - ) - if resp['ResponseMetadata']['HTTPStatusCode'] != 200: - messages.error(self.request, - _('We could not verify either your verification code or username')) - else: - messages.success(self.request, - _('You have successfully changed your password.')) - return super(ConfirmForgotPasswordView, self).form_valid(form) - except ClientError as e: + def cognito_command(self,form): + return cog_client.confirm_sign_up( + ClientId=settings.COGNITO_APP_ID, + Username=form.cleaned_data['username'], + ConfirmationCode=form.cleaned_data['verification_code'] + ) + + +class UpdatePasswordView(LoginRequiredMixin,TokenMixin,CognitoFormView): + template_name = 'warrant/update-profile.html' + form_class = UpdatePasswordForm + success_message = 'You have successfully changed your password.' + success_url = reverse_lazy('dw:profile') - form.add_error('verification_code',e.response['Error']['Message']) - return self.form_invalid(form) \ No newline at end of file + def cognito_command(self,form): + return cog_client.change_password( + PreviousPassword=form.cleaned_data['previous_password'], + ProposedPassword=form.cleaned_data['proposed_password'], + AccessToken=self.request.session['ACCESS_TOKEN'] + ) \ No newline at end of file diff --git a/django_warrant/views/subscriptions.py b/django_warrant/views/subscriptions.py index dd6c577..b0b165e 100644 --- a/django_warrant/views/subscriptions.py +++ b/django_warrant/views/subscriptions.py @@ -1,10 +1,10 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import ListView -from django_warrant.views.mixins import GetSubscriptionsMixin +from django_warrant.views.mixins import GetSubscriptionsMixin, TokenMixin -class SubscriptionsView(LoginRequiredMixin,GetSubscriptionsMixin,ListView): +class SubscriptionsView(LoginRequiredMixin,TokenMixin,GetSubscriptionsMixin,ListView): template_name = 'warrant/subscriptions.html' def get_queryset(self): From 27cc7a0a5ba7951081afb296957ff56ba9160f08 Mon Sep 17 00:00:00 2001 From: Brian Jinwright Date: Mon, 4 Jun 2018 12:04:58 -0400 Subject: [PATCH 6/7] refactored the __setattr__ method on the UserObj class, added new util attr_map_inverse, add new method on CognitoFormView named extra_command for used it to create a new api_key and attach it to the user on registration --- django_warrant/models.py | 12 ++++++++---- django_warrant/utils.py | 10 +++++++++- django_warrant/views/mixins.py | 2 +- django_warrant/views/profile.py | 19 +++++++++++++++++-- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/django_warrant/models.py b/django_warrant/models.py index fba4823..b01dbcb 100644 --- a/django_warrant/models.py +++ b/django_warrant/models.py @@ -3,7 +3,7 @@ from django.conf import settings from jose import jwt -from django_warrant.utils import cognito_to_dict, dict_to_cognito, cog_client, refresh_access_token +from django_warrant.utils import cognito_to_dict, dict_to_cognito, cog_client, refresh_access_token, attr_map_inverse class Meta(object): @@ -37,7 +37,7 @@ def __init__(self, attribute_list, metadata=None, request=None, """ self._attr_map = settings.COGNITO_ATTR_MAPPING - self.username = attribute_list.get('Username') + self.username = attribute_list['Username'] self._data = cognito_to_dict( attribute_list.get('UserAttributes') or attribute_list.get('Attributes'),self._attr_map) @@ -70,8 +70,12 @@ def __getattr__(self, name): return self._metadata.get(name) def __setattr__(self, name, value): - if name in list(self.__dict__.get('_data',{}).keys()): - self._data[name] = value + if name in list(attr_map_inverse().keys()): + try: + self._data[name] = value + except TypeError: + self._data = {} + self._data[name] = value else: super(UserObj, self).__setattr__(name, value) diff --git a/django_warrant/utils.py b/django_warrant/utils.py index 4b1a32e..0c1d072 100644 --- a/django_warrant/utils.py +++ b/django_warrant/utils.py @@ -26,6 +26,13 @@ def dict_to_cognito(attr_dict,mapping): return cognito_list +def attr_map_inverse(): + attr_dict = settings.COGNITO_ATTR_MAPPING.copy() + attr_dict.pop('username') + attr_dict.pop('phone_number_verified') + return {v: k for k, v in attr_dict.items()} + + def user_obj_to_django(user_obj): c_attrs = settings.COGNITO_ATTR_MAPPING user_attrs = dict() @@ -40,10 +47,11 @@ def refresh_access_token(request): """ Sets a new access token on the User using the refresh token. """ - username = request.user.username + refresh_token = request.session['REFRESH_TOKEN'] auth_params = {'REFRESH_TOKEN': refresh_token} if settings.COGNITO_CLIENT_SECRET: + username = request.user.username auth_params['SECRET_HASH'] = WarrantLite.get_secret_hash(username, settings.COGNITO_APP_ID, settings.COGNITO_CLIENT_SECRET) diff --git a/django_warrant/views/mixins.py b/django_warrant/views/mixins.py index 7d3b59e..f420219 100644 --- a/django_warrant/views/mixins.py +++ b/django_warrant/views/mixins.py @@ -20,7 +20,7 @@ def get_user(self): def admin_get_user(self,username): return UserObj(cog_client.admin_get_user( UserPoolId=settings.COGNITO_USER_POOL_ID, - Username=username)) + Username=username),is_authenticated=False) class TokenMixin(AccessMixin): diff --git a/django_warrant/views/profile.py b/django_warrant/views/profile.py index 45d9811..f445249 100644 --- a/django_warrant/views/profile.py +++ b/django_warrant/views/profile.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext as _ from django.views.decorators.cache import never_cache -from django_warrant.utils import cog_client, dict_to_cognito +from django_warrant.utils import cog_client, dict_to_cognito, apigw_client from django_warrant.views.mixins import GetUserMixin, TokenMixin try: @@ -69,9 +69,13 @@ def get_success_message(self,resp): def cognito_command(self,form): return {} + def extra_command(self,form): + pass + def form_valid(self, form): try: resp = self.cognito_command(form) + self.extra_command(form) messages.success(self.request,_(self.get_success_message(resp))) return super(CognitoFormView, self).form_valid(form) except ClientError as e: @@ -138,7 +142,7 @@ def cognito_command(self,form): ) -class ConfirmRegistrationView(CognitoFormView): +class ConfirmRegistrationView(GetUserMixin,CognitoFormView): template_name = 'warrant/registration.html' form_class = VerificationCodeForm success_message = 'You have successfully registered.' @@ -151,6 +155,17 @@ def cognito_command(self,form): ConfirmationCode=form.cleaned_data['verification_code'] ) + def extra_command(self,form): + username = form.cleaned_data['username'] + resp = apigw_client.create_api_key( + name=username, + description='Created by during registration by django-warrant' + ) + u = self.admin_get_user(username) + u.api_key = resp['value'] + u.api_key_id = resp['id'] + u.save(admin=True) + class UpdatePasswordView(LoginRequiredMixin,TokenMixin,CognitoFormView): template_name = 'warrant/update-profile.html' From 5ad9e1540a0c9930a2328b2334543a32d543aa4b Mon Sep 17 00:00:00 2001 From: Brian Jinwright Date: Mon, 4 Jun 2018 15:07:01 -0400 Subject: [PATCH 7/7] added Group class and methods to the UserObj to tell whether the user is an admin or not --- cdu/settings.py | 2 ++ django_warrant/middleware.py | 4 +--- django_warrant/models.py | 44 ++++++++++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/cdu/settings.py b/cdu/settings.py index d1cc60f..a7839b7 100644 --- a/cdu/settings.py +++ b/cdu/settings.py @@ -38,6 +38,8 @@ COGNITO_USER_POOL_ID = env('COGNITO_USER_POOL_ID') +COGNITO_ADMIN_GROUP = env('COGNITO_ADMIN_GROUP','Admins') + COGNITO_APP_ID = env('COGNITO_APP_ID') COGNITO_CLIENT_SECRET = env('COGNITO_CLIENT_SECRET') diff --git a/django_warrant/middleware.py b/django_warrant/middleware.py index 17215cd..99e09f6 100644 --- a/django_warrant/middleware.py +++ b/django_warrant/middleware.py @@ -1,8 +1,8 @@ from django.conf import settings from django.utils.deprecation import MiddlewareMixin -from django.utils.functional import SimpleLazyObject from django_warrant.models import get_user as gu + class APIKeyMiddleware(object): """ A simple middleware to pull the users API key from the headers and @@ -30,9 +30,7 @@ def process_request(request): def get_user(request): if not hasattr(request, '_cached_user'): - request._cached_user = gu(request) - return request._cached_user diff --git a/django_warrant/models.py b/django_warrant/models.py index b01dbcb..5566e38 100644 --- a/django_warrant/models.py +++ b/django_warrant/models.py @@ -3,7 +3,25 @@ from django.conf import settings from jose import jwt -from django_warrant.utils import cognito_to_dict, dict_to_cognito, cog_client, refresh_access_token, attr_map_inverse +from django_warrant.utils import cognito_to_dict, dict_to_cognito, cog_client, \ + refresh_access_token, attr_map_inverse + + +class Group(object): + + def __init__(self,attr_dict): + self._data = attr_dict + + def __repr__(self): + return '<{class_name}: {uni}>'.format( + class_name=self.__class__.__name__, uni=self.__unicode__()) + + def __unicode__(self): + return self.username + + def __getattr__(self, name): + if name in list(self.__dict__.get('_data',{}).keys()): + return self._data.get(name) class Meta(object): @@ -11,6 +29,7 @@ class Meta(object): def __init__(self,pk): self.pk = pk + class PK(object): def __init__(self,sub): @@ -48,6 +67,7 @@ def __init__(self, attribute_list, metadata=None, request=None, self.email_verified = self._data.pop('email_verified',None) self.phone_number_verified = self._data.pop('phone_number_verified',None) self._metadata = {} if metadata is None else metadata + self._cached_groups = None self._meta = Meta(PK(self._data.get('sub'))) if request: self.access_token = request.session['ACCESS_TOKEN'] @@ -79,6 +99,26 @@ def __setattr__(self, name, value): else: super(UserObj, self).__setattr__(name, value) + @property + def groups(self): + if not self._cached_groups: + self._cached_groups = [Group(i) for i in + cog_client.admin_list_groups_for_user( + Username=self.username, + UserPoolId=settings.COGNITO_USER_POOL_ID, + ).get('Groups')] + return self._cached_groups + else: + return self._cached_groups + + @property + def group_names(self): + return [i.GroupName for i in self.groups] + + @property + def is_staff(self): + return settings.COGNITO_ADMIN_GROUP in self.group_names + @property def is_active(self): return self._data.get('email') @@ -122,7 +162,6 @@ def renew_access_token(self): self.id_token = refresh_response['AuthenticationResult']['IdToken'] self.token_type = refresh_response['AuthenticationResult']['TokenType'] - def save(self,admin=False,create=False,password=None,update_fields=None): if not create: if admin: @@ -152,6 +191,7 @@ def delete(self,admin=False): ) return + def get_user(request): if not request.session.get('ACCESS_TOKEN'): return AnonUserObj()