Skip to content

draft: add endpoints to create and modify users - #38894

Open
viadanna wants to merge 4 commits into
openedx:masterfrom
open-craft:viadanna/create-user-endpoint
Open

viadanna wants to merge 4 commits into
openedx:masterfrom
open-craft:viadanna/create-user-endpoint

Conversation

@viadanna

Copy link
Copy Markdown
Contributor

Description

This PR adds endpoints to create and modify users.

Supporting information

Private Refs:

Testing instructions

  1. Make a POST request to /api/user/v1/modify to create a user, e.g.
$ http POST http://local.openedx.io:8000/api/user/v1/modify --auth-type bearer --auth TOKEN username=test email=test@example.com password=12345678 name=Test
HTTP/1.1 201 Created
Allow: POST, PATCH, OPTIONS
Content-Language: en
Content-Length: 31
Content-Type: application/json
Date: Wed, 15 Jul 2026 14:49:26 GMT
Server: WSGIServer/0.2 CPython/3.11.8
Set-Cookie: openedx-language-preference=en; Domain=local.openedx.io; expires=Wed, 29 Jul 2026 14:49:26 GMT; Max-Age=1209600; Path=/; SameSite=Lax
Set-Cookie: csrftoken=uVKV5sKqW6wkR7bzxV6Avju4QLpYIMuw; expires=Wed, 14 Jul 2027 14:49:26 GMT; Max-Age=31449600; Path=/; SameSite=Lax
Set-Cookie: lms_sessionid=1|2dh9womlw77dgrx1pivuj0sg9i9u5ybq|WWtn1GLVCf45|IjRjOTNkMjg0ZjVlOGY2NGUzMDQ4NjY1YzYxNzY2YjBmNTI2MmI5NWI4ZjY5ZWMzZTk4YWVjNTYwZjZlOWVkZWYi:1wk0vO:YUj--lO47gIYgyE3ECpwA9H3ats72cPfhsaAMISRph4; Domain=local.openedx.io; HttpOnly; Path=/; SameSite=Lax
Vary: Accept, Accept-Language, Cookie, origin
X-Frame-Options: SAMEORIGIN

{
    "user_id": 6,
    "username": "test"
}
  1. Make a PATCH request to modify an existing user, e.g.
$ http PATCH http://local.openedx.io:8000/api/user/v1/modify --auth-type bearer --auth TOKEN username=test first_name=Test last_name=User
HTTP/1.1 200 OK
Allow: POST, PATCH, OPTIONS
Content-Language: en
Content-Length: 31
Content-Type: application/json
Date: Wed, 15 Jul 2026 15:14:34 GMT
Server: WSGIServer/0.2 CPython/3.11.8
Set-Cookie: openedx-language-preference=en; Domain=local.openedx.io; expires=Wed, 29 Jul 2026 15:14:34 GMT; Max-Age=1209600; Path=/; SameSite=Lax
Set-Cookie: lms_sessionid=1|w489j3st009e06ej1fuqk52mbtjviq2r|ZXnG9qPRsD9K|ImI5ZTJlMjBkMDE3NmJkM2Q0YzM1YmU3Nzg3OGMyMjUwODQ1NWM4Zjc2NWRjNDZhMTc5Y2Q1YjAzMjhiMGRiNzIi:1wk1Ji:E-vJBuc200cn9TzEhbD3k3fTpUkuESZ6M7A8TzVq0HM; Domain=local.openedx.io; expires=Wed, 29 Jul 2026 15:14:34 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
Vary: Accept, Accept-Language, origin, Cookie
X-Frame-Options: SAMEORIGIN

{
    "user_id": 6,
    "username": "test"
}

Deadline

None, still a draft

Copilot AI review requested due to automatic review settings July 15, 2026 19:47
@viadanna
viadanna requested a review from a team as a code owner July 15, 2026 19:47
@openedx-webhooks

openedx-webhooks commented Jul 15, 2026 •

Copy link
Copy Markdown

Thanks for the pull request, @viadanna!

This repository is currently maintained by @openedx/wg-maintenance-openedx-platform-oncall.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Jul 15, 2026
@github-project-automation github-project-automation Bot moved this to Needs Triage in Contributions Jul 15, 2026
@viadanna
viadanna requested a review from xitij2000 July 15, 2026 19:50
@viadanna viadanna self-assigned this Jul 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new admin-only “modify user” API endpoint in user_api to support creating users (POST) and updating user attributes (PATCH) via /api/user/v1/modify.

Changes:

  • Added UserModifyView with JWT/Bearer/Session authentication and IsAdminUser permission, implementing POST (create) and PATCH (update).
  • Wired the new endpoint into user_api URL routing.
  • Added initial HTTP-level tests covering user creation success and common validation failures.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
openedx/core/djangoapps/user_api/views.py Adds UserModifyView implementing POST/PATCH behavior for user creation and updates.
openedx/core/djangoapps/user_api/urls.py Registers the new v1/modify route.
openedx/core/djangoapps/user_api/tests/test_views.py Adds tests for the new create-user endpoint behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/tests/test_views.py Outdated
Comment thread openedx/core/djangoapps/user_api/tests/test_views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py
@viadanna viadanna changed the title Viadanna/create user endpoint draft: add endpoints to create and modify users Jul 15, 2026
@samuelallan72

Copy link
Copy Markdown
Contributor

@viadanna could you rebase this on latest master please, and resolve the conflicts? :)

@viadanna
viadanna force-pushed the viadanna/create-user-endpoint branch from 2b862ab to 6d230bd Compare July 16, 2026 14:18
@viadanna

Copy link
Copy Markdown
Contributor Author

@viadanna could you rebase this on latest master please, and resolve the conflicts? :)

Done @samuelallan72

Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py

@samuelallan72 samuelallan72 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@viadanna I tested this out locally; please see comments inline 🙂

@xitij2000 xitij2000 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this PR has some lint issues that need to be fixed, and other issues I've highlighted. In addition could you also add swagger specs?

Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
@mphilbrick211 mphilbrick211 moved this from Needs Triage to In Eng Review in Contributions Jul 20, 2026
@viadanna
viadanna force-pushed the viadanna/create-user-endpoint branch from adbefc9 to ee1884a Compare September 24, 2026 12:10
@viadanna
viadanna requested a lite review from Copilot September 24, 2026 12:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread docs/lms-openapi.yaml Outdated
Comment thread docs/lms-openapi.yaml Outdated
Comment thread openedx/core/djangoapps/user_api/serializers.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py
Comment thread openedx/core/djangoapps/user_api/serializers.py
Comment thread openedx/core/djangoapps/user_api/views.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread docs/lms-openapi.yaml Outdated
Comment thread openedx/core/djangoapps/user_api/serializers.py
Comment thread openedx/core/djangoapps/user_api/views.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical authorization and plaintext-password handling issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 1 Low severity

Open (3)
Resolved since last review (9)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Profile is not created for account-only requests

openedx/​core/​djangoapps/​user_api/​serializers.py:172

A UserProfile is only created when at least one profile field is supplied. Since ordinary User creation does not automatically create a profile in this codebase, a valid request containing only account fields leaves user.profile missing; a later PATCH of name or another profile field then raises RelatedObjectDoesNotExist instead of updating the user. Always create the profile here.

Medium severity Password update omits required normalization

openedx/​core/​djangoapps/​user_api/​serializers.py:260

Create normalizes passwords before hashing, but update hashes the unnormalized value. For passwords whose NFKC form differs, login normalizes the entered password while the stored hash was made from the raw value, so the new password cannot authenticate consistently. Normalize before set_password here as well.

Low severity Schema incorrectly allows unsupported additional properties

docs/​lms-openapi.yaml:10083

additionalProperties: true contradicts the endpoint implementation, which rejects every field not in UserProfileSerializer.Meta.fields with a 400 response. Clients generated from this schema may send arbitrary fields believing they are supported; set this to false (and make the same correction in the PATCH schema below).

Comment thread openedx/core/djangoapps/user_api/serializers.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread docs/lms-openapi.yaml Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved security, password handling, validation, persistence, testing, and documentation issues remain.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 1 Medium severity

Open (3)
Resolved since last review (3)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Missing PATCH lookup identifier returns 404 instead of 400

openedx/​core/​djangoapps/​user_api/​views.py:318

username_or_email is optional here, so a PATCH body without the required lookup field passes None to get_user_by_username_or_email and is reported as a 404 User not found. The OpenAPI schema marks this field as required and the request is invalid rather than a missing resource; reject the missing/blank identifier with a 400 validation error before performing the lookup.

Comment thread openedx/core/djangoapps/user_api/serializers.py Outdated
Comment thread openedx/core/djangoapps/user_api/views.py Outdated
Comment thread openedx/core/djangoapps/user_api/tests/test_views.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved moderate issues affect password policy, profile updates, request handling, authorization, and input validation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity · 2 Low severity

Open (3)
Resolved since last review (3)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Pass target user to password validation

openedx/​core/​djangoapps/​user_api/​serializers.py:214

validate_password is called without the existing user, so PATCH password changes skip Django's UserAttributeSimilarityValidator checks against the target user's username/email. This lets an admin set a password equal to the account identifier even though the repository's password policy enables that validator; pass user=self.instance when validating updates.

Medium severity Create missing profile before applying profile updates

openedx/​core/​djangoapps/​user_api/​serializers.py:258

Not every existing User is guaranteed to have a related profile (the codebase explicitly handles users without one), so PATCHing any profile field for such a user raises UserProfile.DoesNotExist and becomes a 500. Create or fetch the profile before applying profile_data so this endpoint can modify all existing accounts.

Medium severity Read QueryDict identifier value before removing it

openedx/​core/​djangoapps/​user_api/​views.py:306

When form-encoded data is used, request.data is a QueryDict and QueryDict.pop() returns a list. The list is passed to get_user_by_username_or_email, so the normal form-encoded PATCH request can fail to find the user or raise instead of updating it. Read the identifier with request.data.get(...) before removing it from the copied payload.

Comment thread openedx/core/djangoapps/user_api/views.py
Comment thread docs/lms-openapi.yaml Outdated
Comment thread docs/lms-openapi.yaml Outdated

@samuelallan72 samuelallan72 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@viadanna I think in general this works well, and I was able to test this locally. Please see some inline nits though, mostly around docs and the serialiser. :)

Comment thread docs/lms-openapi.yaml Outdated
Comment thread openedx/core/djangoapps/user_api/views.py
Comment thread openedx/core/djangoapps/user_api/serializers.py Outdated
Comment thread openedx/core/djangoapps/user_api/serializers.py
Comment thread openedx/core/djangoapps/user_api/serializers.py Outdated
Comment thread docs/lms-openapi.yaml Outdated
@viadanna
viadanna force-pushed the viadanna/create-user-endpoint branch from 294af36 to 9975945 Compare October 6, 2026 22:43
@viadanna
viadanna requested a review from samuelallan72 October 8, 2026 14:23

@samuelallan72 samuelallan72 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@viadanna 👍

Please see some questions inline, but nothing blocking from my end. Feel free to resolve the review threads from my last review; you've addressed them, but github doesn't let me resolve them.

  • I tested this: I didn't retest this functionally since I last reviewed, but the changes look good to me.
  • I read through the code
  • I checked for accessibility issues
  • Includes documentation

Comment on lines +356 to +358
is_superuser = serializer.validated_data.get("is_superuser")
if is_superuser and not request.user.is_superuser:
raise PermissionDenied("You must be a superuser to perform this action.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm would this let a non-superuser remove the is_superuser permission from an existing superuser? Not sure if that's desired.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I improved the check and added tests to catch this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@viadanna nice :) I'm not sure about the new logic though. Wouldn't we be checking for is_setting_superuser_flag = "is_superuser" in serializer.validated_data? I might be misinterpreting. Would you mind adding a comment about the purpose of the logic here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored it and commented on each gate to make it clearer. What do you think?

Comment on lines +219 to +247
def validate_year_of_birth(self, value: int) -> int:
"""Validate the year of birth.

Ensures that the year of birth is within the valid range defined in UserProfile.VALID_YEARS.

:param value: Year of birth to validate.
:return: Validated year of birth.
:raises serializers.ValidationError: If the year of birth is not within the valid range.
"""
if value not in UserProfile.VALID_YEARS:
raise serializers.ValidationError(
f"Year of birth must be within {UserProfile.VALID_YEARS}"
)
return value

def validate_gender(self, value: str) -> str:
"""Validate the gender.

Ensures that the gender is one of the valid choices defined in UserProfile.GENDER_CHOICES.

:param value: Gender to validate.
:return: Validated gender.
:raises serializers.ValidationError: If the gender is not one of the valid choices.
"""
if value not in dict(UserProfile.GENDER_CHOICES):
choices = list(dict(UserProfile.GENDER_CHOICES).keys())
raise serializers.ValidationError(f"Gender must be one of {choices}")
return value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would these (year of birth, gender, country, level of education) be validated on save anyway?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they will. Do you think we should remove this validation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@viadanna I think we should, to avoid too much duplicated validation logic. But I'd be curious what upstream think of this. Perhaps this is useful to have user friendly error messages.

@viadanna viadanna Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed these but I'll leave the commit so it can be reverted if necessary.

@viadanna
viadanna force-pushed the viadanna/create-user-endpoint branch from 2f83587 to b8940fb Compare October 12, 2026 18:51
@viadanna
viadanna force-pushed the viadanna/create-user-endpoint branch from b8940fb to d3c4759 Compare October 22, 2026 20:55
@viadanna
viadanna force-pushed the viadanna/create-user-endpoint branch 2 times, most recently from fdd0f7f to 4b5cb95 Compare October 22, 2026 21:05
@viadanna
viadanna force-pushed the viadanna/create-user-endpoint branch from 6726cf1 to e2c0d15 Compare October 22, 2026 22:07

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: In Eng Review

Development

Successfully merging this pull request may close these issues.

6 participants