-
Notifications
You must be signed in to change notification settings - Fork 4.3k
draft: add endpoints to create and modify users #38894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
431a6d4
58161c3
c01b3f2
6d230bd
8606d4a
e80d96a
7e15502
5687729
a3dc11a
7c14842
adbefc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,24 @@ | ||
| """HTTP end-points for the User API. """ | ||
|
|
||
| from django.contrib.auth.models import User # pylint: disable=imported-auth-user | ||
| from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user | ||
| from django.core.exceptions import ValidationError as DjangoValidationError | ||
| from django.db import transaction | ||
| from django.http import HttpResponse | ||
| from django.utils.decorators import method_decorator | ||
| from django.views.decorators.csrf import ensure_csrf_cookie | ||
| from django_filters.rest_framework import DjangoFilterBackend | ||
| from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication | ||
| from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser | ||
| from opaque_keys import InvalidKeyError | ||
| from opaque_keys.edx import locator | ||
| from opaque_keys.edx.keys import CourseKey | ||
| from rest_framework import generics, status, viewsets | ||
| from rest_framework.exceptions import ParseError | ||
| from rest_framework.permissions import IsAuthenticated | ||
| from rest_framework.exceptions import ParseError, ValidationError | ||
| from rest_framework.permissions import IsAdminUser, IsAuthenticated | ||
| from rest_framework.response import Response | ||
| from rest_framework.views import APIView | ||
|
|
||
| from common.djangoapps.student.helpers import AccountValidationError | ||
| from openedx.core.djangoapps.django_comment_common.models import Role | ||
| from openedx.core.djangoapps.user_api.models import UserPreference | ||
| from openedx.core.djangoapps.user_api.preferences.api import get_country_time_zones, update_email_opt_in | ||
|
|
@@ -22,6 +27,8 @@ | |
| UserPreferenceSerializer, | ||
| UserSerializer, | ||
| ) | ||
| from openedx.core.djangoapps.user_authn.views.register import create_account_with_params | ||
| from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser | ||
| from openedx.core.lib.api.permissions import ApiKeyHeaderPermission | ||
| from openedx.core.lib.api.view_utils import require_post_params | ||
|
|
||
|
|
@@ -161,3 +168,128 @@ class CountryTimeZoneListView(generics.ListAPIView): | |
| def get_queryset(self): | ||
| country_code = self.request.GET.get("country_code", None) | ||
| return get_country_time_zones(country_code) | ||
|
|
||
|
|
||
| class UserModifyView(APIView): | ||
| """ | ||
| **Use Cases** | ||
|
|
||
| Creates a user with email and username, or updates user information by | ||
| email or username. | ||
|
|
||
| **Example Requests** | ||
|
|
||
| POST /api/user/v1/modify/ | ||
|
|
||
| PATCH /api/user/v1/modify/ | ||
|
|
||
| **Example POST Response** | ||
|
|
||
| If the request is successful, an HTTP 201 "Created" response is | ||
| returned along with the user id and username, e.g.: | ||
|
|
||
| { | ||
| "user_id": 5, | ||
| "username": "newuser" | ||
| } | ||
|
|
||
| """ | ||
|
|
||
| authentication_classes = ( | ||
| JwtAuthentication, | ||
| BearerAuthenticationAllowInactiveUser, | ||
| SessionAuthenticationAllowInactiveUser, | ||
| ) | ||
| permission_classes = (IsAdminUser,) | ||
|
|
||
| @method_decorator(transaction.non_atomic_requests) | ||
| def dispatch(self, request, *args, **kwargs): | ||
| """ | ||
| Decorate with `non_atomic_requests` to work on newer versions of platform. | ||
| """ | ||
| return super().dispatch(request, *args, **kwargs) | ||
|
Comment on lines
+205
to
+210
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think applying this to patch can result in issues. If the password updates but the user doesn't you'll end with a partial update. |
||
|
|
||
| def post(self, request): | ||
| """ | ||
| Create a user with email and username. | ||
| """ | ||
| try: | ||
| user = create_account_with_params(request, request.data) | ||
| except ( | ||
| AccountValidationError, | ||
| ValueError, | ||
| ValidationError, | ||
| DjangoValidationError, | ||
| ) as e: | ||
| if isinstance(e, ValidationError): | ||
| message = e.detail | ||
| else: | ||
| message = str(e) | ||
| return Response( | ||
| data={"error": message}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
|
|
||
| return Response( | ||
| data={"user_id": user.id, "username": user.username}, | ||
| status=status.HTTP_201_CREATED, | ||
| ) | ||
|
|
||
| def patch(self, request): | ||
| """ | ||
| Update user information by email or username. | ||
| """ | ||
|
viadanna marked this conversation as resolved.
|
||
| try: | ||
| user = self._get_user_by_email_or_username(request) | ||
| if not user: | ||
| return Response( | ||
| data={"error": "User not found."}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
||
| data = request.data.copy() | ||
| # Remove email and username from the data to prevent changing them | ||
| data.pop("email", None) | ||
| data.pop("username", None) | ||
| # Remove password from the data and handle it separately | ||
| password = data.pop("password", None) | ||
| if password: | ||
| user.set_password(password) | ||
| user = UserSerializer().update(user, data) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be better to validate this data before saving. i.e construct the serialiser from the data, check is_valid, and then save. |
||
| except ( | ||
| AccountValidationError, | ||
| ValueError, | ||
| ValidationError, | ||
| DjangoValidationError, | ||
| ) as e: | ||
| if isinstance(e, ValidationError): | ||
| message = e.detail | ||
| else: | ||
| message = str(e) | ||
| return Response( | ||
| data={"error": message}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| return Response( | ||
| data={"user_id": user.id, "username": user.username}, | ||
| status=status.HTTP_200_OK, | ||
| ) | ||
|
|
||
| @staticmethod | ||
| def _get_user_by_email_or_username(request): | ||
| """ | ||
| Get a user by email and/or username. | ||
|
|
||
| Returns None if one doesn't exist. | ||
| """ | ||
|
|
||
| email = request.data.get("email") | ||
| username = request.data.get("username") | ||
| if not email and not username: | ||
| raise ValidationError("email or username must be specified") | ||
| query = {} | ||
| if email: | ||
| query["email"] = email | ||
| if username: | ||
| query["username"] = username | ||
| return User.objects.filter(**query).first() | ||
|
Comment on lines
+278
to
+295
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps you can use the existing |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what permissions we want here, but just flagging to double check since this api is pretty powerful - it can modify arbitrary users.
Also not sure how this fits with the new RBAC system?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was developed before the new RBCA system and will need further changes to adapt to it. But as you can see, it isn't being used by any of the user_api django app views, so it all needs updating, which falls outside the scope of the work.