Skip to content

Commit

Permalink
OpenConceptLab/ocl_issues#1338 | user signup and mark verified views
Browse files Browse the repository at this point in the history
  • Loading branch information
snyaggarwal committed Aug 2, 2022
1 parent 6be9848 commit 8abcf5e
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 12 deletions.
105 changes: 101 additions & 4 deletions core/common/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import connection
from django.http import Http404
from pydash import get

from core.settings import REDIS_HOST, REDIS_PORT, REDIS_DB
Expand Down Expand Up @@ -248,11 +249,13 @@ def last_value(seq_name):


class AbstractAuthService:
def __init__(self, username, password=None, user=None):
def __init__(self, username=None, password=None, user=None):
self.username = username
self.password = password
self.user = user
if self.username and not self.user:
if self.user:
self.username = self.user.username
elif self.username:
self.set_user()

def set_user(self):
Expand All @@ -262,18 +265,112 @@ def set_user(self):
def get_token(self):
pass

def mark_verified(self, **kwargs):
return self.user.mark_verified(**kwargs)


class DjangoAuthService(AbstractAuthService):
token_type = 'Token'

def get_token(self, check_password=True):
if check_password:
if not self.user.check_password(self.password):
return False
return self.user.get_token()
return self.token_type + ' ' + self.user.get_token()

@staticmethod
def create_user(_):
return True


class OIDCAuthService(AbstractAuthService):
token_type = 'Bearer'

def get_token(self):
return self.user.get_oidc_token(self.password)
token = self.user.get_oidc_token(self.password)
if token is False:
return token
return self.token_type + ' ' + get(token, 'access_token')

@staticmethod
def get_admin_token():
response = requests.post(
settings.OIDC_SERVER_INTERNAL_URL + '/realms/master/protocol/openid-connect/token/',
data=dict(
grant_type='password',
username=settings.KEYCLOAK_ADMIN,
password=settings.KEYCLOAK_ADMIN_PASSWORD,
client_id='admin-cli'
),
verify=False,
)
return response.json().get('access_token')

@staticmethod
def get_admin_headers():
return dict(Authorization=f'Bearer {OIDCAuthService.get_admin_token()}')

def get_user_headers(self):
return dict(Authorization=self.get_token())

@staticmethod
def create_user(data):
response = requests.post(
settings.OIDC_SERVER_INTERNAL_URL + '/admin/realms/ocl/users',
json=dict(
enabled=True,
emailVerified=False,
firstName=data.get('first_name'),
lastName=data.get('last_name'),
email=data.get('email'),
username=data.get('username'),
credentials=[dict(type='password', value=data.get('password'), temporary=False)]
),
verify=False,
headers=OIDCAuthService.get_admin_headers()
)
if response.status_code == 201:
return True

return response.json()

def mark_verified(self, **kwargs):
admin_headers = self.get_admin_headers()
response = requests.get(
settings.OIDC_SERVER_INTERNAL_URL + '/admin/realms/ocl/users',
verify=False,
headers=admin_headers
)

users = response.json()
user_info = get([user for user in users if user['username'] == self.username], '0')
oid_user_id = get(user_info, 'id')
if not oid_user_id:
raise Http404()

response = requests.put(
settings.OIDC_SERVER_INTERNAL_URL + f'/admin/realms/ocl/users/{oid_user_id}',
json=dict(emailVerified=True),
verify=False,
headers=admin_headers
)
if response.status_code < 300:
return super().mark_verified(**kwargs)
return response.json()

def reset_password(self, **kwargs):
# PUT /{realm}/users/{id}/disable-credential-types
# Body
# credentialTypes : < string > array

# PUT /admin/realms/{realm}/users/{id}/reset-password
# {
# "type": "password",
# "temporary": false,
# "value": "my-new-password"
# }

pass


class AuthService:
Expand Down
2 changes: 2 additions & 0 deletions core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,5 @@
OIDC_RP_SCOPES = 'openid profile email roles role_list'
OIDC_STORE_ACCESS_TOKEN = True
LOGIN_REDIRECT_URL = '/'
KEYCLOAK_ADMIN = os.environ.get('KEYCLOAK_ADMIN', 'root')
KEYCLOAK_ADMIN_PASSWORD = os.environ.get('KEYCLOAK_ADMIN_PASSWORD', 'Root123')
37 changes: 29 additions & 8 deletions core/users/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import uuid

from django.contrib.auth.models import update_last_login
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.http import Http404
from drf_yasg.utils import swagger_auto_schema
from pydash import get
Expand Down Expand Up @@ -38,12 +40,10 @@ def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')

auth_service = AuthService.get(username=username, password=password)
token = auth_service.get_token()

if token is False:
raise Http400(dict(non_field_errors=["Unable to log in with provided credentials."]))
if not username or not password:
raise Http400(dict(non_field_errors=['Must include "username" and "password".']))

auth_service = AuthService.get(username=username, password=password)
user = auth_service.user

if not user.is_active:
Expand All @@ -57,12 +57,17 @@ def post(self, request, *args, **kwargs):
{'detail': VERIFY_EMAIL_MESSAGE, 'email': user.email}, status=status.HTTP_401_UNAUTHORIZED
)

token = auth_service.get_token()

if token is False:
raise Http400(dict(non_field_errors=["Unable to log in with provided credentials."]))

try:
update_last_login(None, user)
except: # pylint: disable=bare-except
pass

return Response(token)
return Response(dict(token=token))


class UserBaseView(BaseAPIView):
Expand Down Expand Up @@ -177,6 +182,20 @@ def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
headers = self.get_success_headers(serializer.data)
return Response({'detail': VERIFY_EMAIL_MESSAGE}, status=status.HTTP_201_CREATED, headers=headers)

def perform_create(self, serializer):
data = self.request.data
try:
validate_password(data.get('password'))
except ValidationError as ex:
serializer._errors['password'] = ex.messages
return

response = AuthService.get().create_user(data)
if response is True:
super().perform_create(serializer)
else:
serializer._errors = response


class UserEmailVerificationView(UserBaseView):
permission_classes = (AllowAny, )
Expand All @@ -186,8 +205,10 @@ def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
if not user:
return Response(status=status.HTTP_404_NOT_FOUND)

result = user.mark_verified(token=kwargs.get('verification_token'), force=get(request.user, 'is_staff'))
if result:
result = AuthService.get(user=user).mark_verified(
token=kwargs.get('verification_token'), force=get(request.user, 'is_staff'))

if result is True:
return Response(status=status.HTTP_200_OK)

return Response(dict(detail=VERIFICATION_TOKEN_MISMATCH), status=status.HTTP_401_UNAUTHORIZED)
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ services:
- OIDC_RP_CLIENT_SECRET
- OIDC_SERVER_URL=${OIDC_SERVER_URL-http://localhost:8080}
- OIDC_REALM=${OIDC_REALM-ocl}
- KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN-root}
- KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD-Root123}
healthcheck:
test: "curl --silent --fail http://localhost:8000/version/ || exit 1"
celery:
Expand Down

0 comments on commit 8abcf5e

Please sign in to comment.