From cbdc38b4c34c7d92ed506843be9cbd9e8369f1a1 Mon Sep 17 00:00:00 2001 From: Justin Hynes Date: Wed, 4 Dec 2024 08:38:31 -0500 Subject: [PATCH 01/10] chore: remove `lxml` constraint (#2649) The `edx-i18n-tools` package has been upgraded and we no longer need this constraint. Relevant `edx-i18n-tools` PR: https://github.com/openedx/i18n-tools/pull/146. --- requirements/all.txt | 13 +++++++++---- requirements/base.txt | 6 ++++-- requirements/constraints.txt | 5 ----- requirements/dev.txt | 8 ++++++-- requirements/production.txt | 12 ++++++++---- requirements/test.txt | 8 ++++++-- requirements/translations.txt | 6 ++++-- 7 files changed, 37 insertions(+), 21 deletions(-) diff --git a/requirements/all.txt b/requirements/all.txt index 22b873387..f5ef09b2d 100644 --- a/requirements/all.txt +++ b/requirements/all.txt @@ -33,11 +33,11 @@ bleach==6.2.0 # via # -r requirements/dev.txt # -r requirements/production.txt -boto3==1.35.73 +boto3==1.35.74 # via # -r requirements/production.txt # django-ses -botocore==1.35.73 +botocore==1.35.74 # via # -r requirements/production.txt # boto3 @@ -492,13 +492,18 @@ jmespath==1.0.1 # -r requirements/production.txt # boto3 # botocore -lxml[html-clean]==5.1.1 +lxml[html-clean]==5.3.0 # via - # -c requirements/constraints.txt # -r requirements/dev.txt # -r requirements/production.txt # edx-credentials-themes # edx-i18n-tools + # lxml-html-clean +lxml-html-clean==0.4.1 + # via + # -r requirements/dev.txt + # -r requirements/production.txt + # lxml markdown==3.7 # via # -r requirements/dev.txt diff --git a/requirements/base.txt b/requirements/base.txt index 0df02af82..282c42f53 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -239,11 +239,13 @@ jinja2==3.1.4 # via # code-annotations # coreschema -lxml[html-clean,html_clean]==5.1.1 +lxml[html-clean,html_clean]==5.3.0 # via - # -c requirements/constraints.txt # edx-credentials-themes # edx-i18n-tools + # lxml-html-clean +lxml-html-clean==0.4.1 + # via lxml markdown==3.7 # via -r requirements/base.in markupsafe==3.0.2 diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 56ce69d9c..844ea2aa9 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -11,11 +11,6 @@ # Common constraints for edx repos -c common_constraints.txt -# Pinning lxml to < 5.2 as edx-i18n-tools package needs to be updated. -# Release notes: https://pypi.org/project/lxml/5.2.0/ -# Github issue: https://github.com/openedx/i18n-tools/issues/144 -lxml<5.2 - # Pinning edx-django-utils to <6 # v6 drops support for python versions <3.12 # Changelog: https://github.com/openedx/edx-django-utils/blob/master/CHANGELOG.rst#600---2024-10-09 diff --git a/requirements/dev.txt b/requirements/dev.txt index 18be4e96b..ea4defa4a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -372,12 +372,16 @@ jinja2==3.1.4 # -r requirements/test.txt # code-annotations # coreschema -lxml[html-clean]==5.1.1 +lxml[html-clean]==5.3.0 # via - # -c requirements/constraints.txt # -r requirements/test.txt # edx-credentials-themes # edx-i18n-tools + # lxml-html-clean +lxml-html-clean==0.4.1 + # via + # -r requirements/test.txt + # lxml markdown==3.7 # via -r requirements/test.txt markupsafe==3.0.2 diff --git a/requirements/production.txt b/requirements/production.txt index 63b7c21cd..e393aa400 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -20,9 +20,9 @@ backoff==2.2.1 # segment-analytics-python bleach==6.2.0 # via -r requirements/base.txt -boto3==1.35.73 +boto3==1.35.74 # via django-ses -botocore==1.35.73 +botocore==1.35.74 # via # boto3 # s3transfer @@ -321,12 +321,16 @@ jmespath==1.0.1 # via # boto3 # botocore -lxml[html-clean]==5.1.1 +lxml[html-clean]==5.3.0 # via - # -c requirements/constraints.txt # -r requirements/base.txt # edx-credentials-themes # edx-i18n-tools + # lxml-html-clean +lxml-html-clean==0.4.1 + # via + # -r requirements/base.txt + # lxml markdown==3.7 # via -r requirements/base.txt markupsafe==3.0.2 diff --git a/requirements/test.txt b/requirements/test.txt index 66453cc43..7164fca0f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -345,12 +345,16 @@ jinja2==3.1.4 # -r requirements/base.txt # code-annotations # coreschema -lxml[html-clean]==5.1.1 +lxml[html-clean]==5.3.0 # via - # -c requirements/constraints.txt # -r requirements/base.txt # edx-credentials-themes # edx-i18n-tools + # lxml-html-clean +lxml-html-clean==0.4.1 + # via + # -r requirements/base.txt + # lxml markdown==3.7 # via -r requirements/base.txt markupsafe==3.0.2 diff --git a/requirements/translations.txt b/requirements/translations.txt index 57bd527c9..b93ea3576 100644 --- a/requirements/translations.txt +++ b/requirements/translations.txt @@ -12,10 +12,12 @@ django==4.2.16 # edx-i18n-tools edx-i18n-tools==1.6.3 # via -r requirements/translations.in -lxml[html-clean,html_clean]==5.1.1 +lxml[html-clean,html_clean]==5.3.0 # via - # -c requirements/constraints.txt # edx-i18n-tools + # lxml-html-clean +lxml-html-clean==0.4.1 + # via lxml path==16.16.0 # via edx-i18n-tools polib==1.2.0 From a6ebd4d838d33d5cf1ec80cac9ad601ddde0e4dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:39:15 -0500 Subject: [PATCH 02/10] fix(deps): update dependency sass-loader to v16.0.4 (#2653) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a941ca32f..3923cad28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "file-loader": "6.2.0", "mini-css-extract-plugin": "2.9.2", "sass": "1.82.0", - "sass-loader": "16.0.3", + "sass-loader": "16.0.4", "url-loader": "4.1.1", "webpack": "5.97.0", "webpack-bundle-tracker": "3.1.1" @@ -9816,9 +9816,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.3.tgz", - "integrity": "sha512-gosNorT1RCkuCMyihv6FBRR7BMV06oKRAs+l4UMp1mlcVg9rWN6KMmUj3igjQwmYys4mDP3etEYJgiHRbgHCHA==", + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", + "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "license": "MIT", "dependencies": { "neo-async": "^2.6.2" diff --git a/package.json b/package.json index 94ad7bcf4..97f825258 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "file-loader": "6.2.0", "mini-css-extract-plugin": "2.9.2", "sass": "1.82.0", - "sass-loader": "16.0.3", + "sass-loader": "16.0.4", "url-loader": "4.1.1", "webpack": "5.97.0", "webpack-bundle-tracker": "3.1.1" From 62fdf3d609f7e0c36483bef32a545da133a4a4c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:39:23 -0500 Subject: [PATCH 03/10] chore(deps): update dependency @babel/plugin-transform-modules-commonjs to v7.26.3 (#2654) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 27 ++++++--------------------- package.json | 2 +- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3923cad28..47fa68c4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@babel/core": "7.26.0", "@babel/eslint-parser": "7.25.9", "@babel/plugin-proposal-object-rest-spread": "7.20.7", - "@babel/plugin-transform-modules-commonjs": "7.25.9", + "@babel/plugin-transform-modules-commonjs": "7.26.3", "@babel/plugin-transform-object-assign": "7.25.9", "@babel/preset-env": "7.26.0", "@edx/eslint-config": "4.3.0", @@ -363,20 +363,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", - "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", @@ -1029,15 +1015,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", - "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" diff --git a/package.json b/package.json index 97f825258..b52a2aefc 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@babel/core": "7.26.0", "@babel/eslint-parser": "7.25.9", "@babel/plugin-proposal-object-rest-spread": "7.20.7", - "@babel/plugin-transform-modules-commonjs": "7.25.9", + "@babel/plugin-transform-modules-commonjs": "7.26.3", "@babel/plugin-transform-object-assign": "7.25.9", "@babel/preset-env": "7.26.0", "@edx/eslint-config": "4.3.0", From 311f38e009fefaad8c3dd0bed33ec114dea397d9 Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Thu, 5 Dec 2024 11:52:27 -0800 Subject: [PATCH 04/10] feat: management command allowing you to revoke certificates (#2652) this allows revocation of certificates. It defaults to program_certificates, which is the most likely use case, but because some UserCredentials might not be synchronized from the LMS, does allow override of the credential type. * Adds a new model and django admin so this script can be run in an automated fashion * this required bringing in config_models FIXES: APER-3799 --- credentials/apps/credentials/admin.py | 7 + .../commands/revoke_certificates.py | 153 +++++++++++ .../tests/test_revoke_certificates.py | 242 ++++++++++++++++++ ..._revoke_certificates_management_command.py | 45 ++++ credentials/apps/credentials/models.py | 22 ++ credentials/settings/base.py | 2 + requirements/all.txt | 19 +- requirements/base.in | 1 + requirements/base.txt | 13 +- requirements/dev.txt | 13 +- requirements/django.txt | 2 +- requirements/production.txt | 17 +- requirements/test.txt | 11 +- requirements/translations.txt | 2 +- 14 files changed, 524 insertions(+), 25 deletions(-) create mode 100644 credentials/apps/credentials/management/commands/revoke_certificates.py create mode 100644 credentials/apps/credentials/management/commands/tests/test_revoke_certificates.py create mode 100644 credentials/apps/credentials/migrations/0030_revoke_certificates_management_command.py diff --git a/credentials/apps/credentials/admin.py b/credentials/apps/credentials/admin.py index 0235abee2..bc7cf39bb 100644 --- a/credentials/apps/credentials/admin.py +++ b/credentials/apps/credentials/admin.py @@ -1,3 +1,4 @@ +from config_models.admin import ConfigurationModelAdmin from django.contrib import admin from django.db.models import Q @@ -6,6 +7,7 @@ CourseCertificate, ProgramCertificate, ProgramCompletionEmailConfiguration, + RevokeCertificatesConfig, Signatory, UserCredential, UserCredentialAttribute, @@ -113,3 +115,8 @@ class SignatoryAdmin(TimeStampedModelAdminMixin, admin.ModelAdmin): class ProgramCompletionEmailConfigurationAdmin(TimeStampedModelAdminMixin, admin.ModelAdmin): list_display = ("identifier", "enabled") search_fields = ("identifier",) + + +@admin.register(RevokeCertificatesConfig) +class RevokeCertificatesConfigAdmin(ConfigurationModelAdmin): + pass diff --git a/credentials/apps/credentials/management/commands/revoke_certificates.py b/credentials/apps/credentials/management/commands/revoke_certificates.py new file mode 100644 index 000000000..c49564811 --- /dev/null +++ b/credentials/apps/credentials/management/commands/revoke_certificates.py @@ -0,0 +1,153 @@ +"""Management command to revoke certificates given a certificate ID and a list of users""" + +import logging +import shlex +from typing import TYPE_CHECKING, Any + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError + +from credentials.apps.credentials.models import RevokeCertificatesConfig, UserCredential + + +if TYPE_CHECKING: + from argparse import ArgumentParser + + from django.db.models import QuerySet + + +logger = logging.getLogger(__name__) +User = get_user_model() + + +class Command(BaseCommand): + """ + Management command to revoke certificates. + + Given a certificate ID and a list of users, revoke that certificate ID + for those users. + + Example usage: + + $ ./manage.py revoke_certificates --lms_user_ids 867 5309 925 --credential_id 90210 + """ + + help = "Revoke certificates for a list of LMS user IDs. Defaults to program certificates." + + def add_arguments(self, parser: "ArgumentParser") -> None: + """Arguments for the command.""" + parser.add_argument( + "--dry-run", + action="store_true", + help="Just show a preview of what would happen.", + ) + parser.add_argument( + "--args-from-database", + action="store_true", + help="Use arguments from the RevokeCertificates model instead of the command line.", + ) + parser.add_argument( + "--verbose", + action="store_true", + help="log each update", + ) + parser.add_argument( + "--lms_user_ids", + default=None, + nargs="+", + help="Users for whom this certificate should be revoked. Required.", + ) + parser.add_argument( + "--credential_id", + default=None, + help="ID of the certificate to be revoked. Required.", + ) + parser.add_argument( + "--credential_type", + default="programcertificate", + choices=["coursecertificate", "programcertificate", "credlybadgetemplate"], + help="Type of credential to revoke. Defaults to 'programcertificate'", + ) + + def get_usernames_from_lms_user_ids(self, lms_user_ids: list[str]) -> "QuerySet": + """ + Generate Users from a list of usernames from a list of user IDs + + Because a UserCredential stores a username, not a foreign key, it's most + efficient to convert the list of user IDs to users directly, before + starting the query. Returning a QuerySet of the User objects (instead of + usernames) allows us to do verbose logging and error reporting. + + Arguments: + + lms_user_ids: list(str): a list of LMS user IDs + + Returns: + + a QuerySet of User objects. + """ + users = User.objects.filter(lms_user_id__in=lms_user_ids) + missing_users = set(lms_user_ids).difference({str(i.lms_user_id) for i in users}) + if missing_users: + logger.warning(f"The following user IDs don't match existing users: {missing_users}") + return users + + def get_args_from_database(self) -> dict[str, Any]: + """Returns an options dictionary from the current NotifyCredentialsConfig model.""" + config = RevokeCertificatesConfig.current() + if not config.enabled: + raise CommandError("RevokeCertificatesConfig is disabled, but --args-from-database was requested.") + + argv = shlex.split(config.arguments) + parser = self.create_parser("manage.py", "revoke_certificates") + return parser.parse_args(argv).__dict__ # we want a dictionary, not a non-iterable Namespace object + + def handle(self, *args, **options): + if options["args_from_database"]: + options = self.get_args_from_database() + credential_id = options.get("credential_id") + verbosity = options.get("verbose") + credential_type = options.get("credential_type") + dry_run = options.get("dry_run") + lms_user_ids = options.get("lms_user_ids") + + logger.info( + f"revoke_certificates starting, dry-run={dry_run}, credential_id={credential_id}, " + f"credential_type={credential_type}, lms_user_ids={lms_user_ids}, verbosity={verbosity}" + ) + + # Because we allow args_from_database, we cannot rely on marking arguments as required, + # so we validate our arguments here. + if not credential_id: + raise CommandError("You must specify a credential_id") + if not lms_user_ids: + raise CommandError("You must specify list of lms_user_ids") + users = self.get_usernames_from_lms_user_ids(lms_user_ids) + if not users: + raise CommandError("None of the given lms_user_ids maps to a real user") + + # We use usernames here, not foreign keys, so just make a list. + # This is not going to be a huge set of users, run from a management command. + usernames = [i.username for i in users] # type: list[str] + + user_creds_to_revoke = UserCredential.objects.filter( + username__in=usernames, + status=UserCredential.AWARDED, + credential_content_type__model=credential_type, + credential_id=credential_id, + ) + if not user_creds_to_revoke: + raise CommandError("No active certificates match the given criteria") + + # as a manually input list, this should be small enough to do in a single bulk_update + for user_cred in user_creds_to_revoke: + if verbosity: + # It's not worth doing an extra query to annotate the verbose logging message with + # user ID, and username isn't PII safe. If the person reading the logs wants more + # info about the affected users, this log message includes enough to look them up. + logger.info(f"Revoking UserCredential {user_cred.id} ({credential_type} {credential_id})") + user_cred.status = UserCredential.REVOKED + if not dry_run: + user_creds_to_revoke.bulk_update(user_creds_to_revoke, ["status"]) + + logger.info("Done revoking certificates") diff --git a/credentials/apps/credentials/management/commands/tests/test_revoke_certificates.py b/credentials/apps/credentials/management/commands/tests/test_revoke_certificates.py new file mode 100644 index 000000000..ba86e0592 --- /dev/null +++ b/credentials/apps/credentials/management/commands/tests/test_revoke_certificates.py @@ -0,0 +1,242 @@ +""" +Tests for the revoke_certificates management command +""" + +from unittest import mock + +from django.contrib.contenttypes.models import ContentType +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase + +from credentials.apps.catalog.tests.factories import ( + CourseFactory, + CourseRunFactory, + OrganizationFactory, + ProgramFactory, +) +from credentials.apps.core.tests.factories import UserFactory +from credentials.apps.core.tests.mixins import SiteMixin +from credentials.apps.credentials.management.commands.revoke_certificates import Command +from credentials.apps.credentials.models import RevokeCertificatesConfig, UserCredential +from credentials.apps.credentials.tests.factories import ( + CourseCertificateFactory, + ProgramCertificateFactory, + UserCredentialFactory, +) + + +class RevokeCertificatesTests(SiteMixin, TestCase): + def setUp(self): + """ + Create several users with multiple UserCredentials in order to verify + bulk operations. + """ + super().setUp() + self.users = UserFactory.create_batch(3) + + # Set up org, courses, and certificate configs + self.orgs = [OrganizationFactory.create(name=name, site=self.site) for name in ["TestOrg1", "TestOrg2"]] + self.course_credential_content_type = ContentType.objects.get( + app_label="credentials", model="coursecertificate" + ) + self.program_credential_content_type = ContentType.objects.get( + app_label="credentials", model="programcertificate" + ) + + self.course = CourseFactory.create(site=self.site) + self.course_runs = CourseRunFactory.create_batch(2, course=self.course) + self.course_certs = [ + CourseCertificateFactory.create( + course_id=course_run.key, + course_run=course_run, + site=self.site, + ) + for course_run in self.course_runs + ] + + self.program = ProgramFactory( + title="TestProgram1", course_runs=self.course_runs, authoring_organizations=self.orgs, site=self.site + ) + self.program_cert = ProgramCertificateFactory.create(program_uuid=self.program.uuid, site=self.site) + + # Set up course and program UserCredentials for each test user + for user in self.users: + UserCredentialFactory.create( + username=user.username, + credential_content_type=self.program_credential_content_type, + credential=self.program_cert, + ) + for course_cert in self.course_certs: + UserCredentialFactory.create( + username=user.username, + credential_content_type=self.course_credential_content_type, + credential=course_cert, + ) + + def test_default_behavior_deletes_progam_certs(self): + """verify default behavior revokes expected and ONLY expected program certificates""" + # pick a subset of users to revoke + users_to_revoke = self.users[:2] + + call_command( + Command(), + "--lms_user_ids", + users_to_revoke[0].lms_user_id, + users_to_revoke[1].lms_user_id, + f"--credential_id={self.program_cert.id}", + ) + + revoked_creds = list(UserCredential.objects.filter(status=UserCredential.REVOKED)) + expected_revoked_creds = list( + UserCredential.objects.filter( + username__in=[u.username for u in users_to_revoke], + credential_content_type__model="programcertificate", + ) + ) + + self.assertListEqual(expected_revoked_creds, revoked_creds) + + def test_credential_type_specify(self): + """verify credential type can be specified""" + # pick a subset of users to revoke + users_to_revoke = self.users[:2] + cred_type_to_revoke = "coursecertificate" + cred_to_revoke = self.course_certs[0] + + call_command( + Command(), + "--lms_user_ids", + users_to_revoke[0].lms_user_id, + users_to_revoke[1].lms_user_id, + f"--credential_id={cred_to_revoke.id}", + f"--credential_type={cred_type_to_revoke}", + ) + + revoked_creds = list(UserCredential.objects.filter(status=UserCredential.REVOKED)) + expected_revoked_creds = list( + UserCredential.objects.filter( + username__in=[u.username for u in users_to_revoke], + credential_content_type__model=cred_type_to_revoke, + credential_id=cred_to_revoke.id, + ) + ) + self.assertListEqual(expected_revoked_creds, revoked_creds) + + def test_dry_run(self): + """verify dry_run makes no changes""" + # pick a subset of users to revoke + users_to_revoke = self.users[:2] + + call_command( + Command(), + "--lms_user_ids", + users_to_revoke[0].lms_user_id, + users_to_revoke[1].lms_user_id, + f"--credential_id={self.program_cert.id}", + "--dry-run", + ) + + revoked_creds = list(UserCredential.objects.filter(status=UserCredential.REVOKED)) + + self.assertFalse(revoked_creds) + + def test_verbosity_enabled(self): + """verify the verbose flag works when enabled""" + # pick a subset of users to revoke + users_to_revoke = self.users[:2] + expected_substring = "Revoking UserCredential" + + with self.assertLogs(level="INFO") as cm: + call_command( + Command(), + "--lms_user_ids", + users_to_revoke[0].lms_user_id, + users_to_revoke[1].lms_user_id, + f"--credential_id={self.program_cert.id}", + "--verbose", + ) + self.assertTrue(any(expected_substring in s for s in cm.output)) + + def test_verbosity_disabled(self): + """verify the verbose flag's absence works as expected""" + # pick a subset of users to revoke + users_to_revoke = self.users[:2] + expected_substring = "Revoking UserCredential" + + with self.assertLogs(level="INFO") as cm: + call_command( + Command(), + "--lms_user_ids", + users_to_revoke[0].lms_user_id, + users_to_revoke[1].lms_user_id, + f"--credential_id={self.program_cert.id}", + ) + self.assertFalse(any(expected_substring in s for s in cm.output)) + + def test_invalid_users(self): + """verify that the inclusion of invalid user IDs results in a warning""" + # pick a subset of users to revoke + users_to_revoke = self.users[:2] + expected_substring = "The following user IDs don't match existing users" + + with self.assertLogs(level="WARNING") as cm: + call_command( + Command(), + "--lms_user_ids", + users_to_revoke[0].lms_user_id, + users_to_revoke[1].lms_user_id, + 8675309, + f"--credential_id={self.program_cert.id}", + "--verbose", + ) + self.assertTrue(any(expected_substring in s for s in cm.output)) + + def test_validation(self): + """Test that the input validators are correct""" + # fake user + expected = "None of the given lms_user_ids maps to a real user*" + with self.assertRaisesRegex(CommandError, expected): + call_command(Command(), "--lms_user_ids", "8675309", "--credential_id", "123") + + # real user, doesn't mapped to a real credential + expected = "No active certificates match the given criteria" + with self.assertRaisesRegex(CommandError, expected): + call_command(Command(), "--lms_user_ids", self.users[0].lms_user_id, "--credential_id", "123") + + # missing credential_id + expected = "You must specify a credential_id" + with self.assertRaisesRegex(CommandError, expected): + call_command(Command(), "--lms_user_ids", self.users[0].lms_user_id) + + # missing lms_user_ids + expected = "You must specify list of lms_user_ids" + with self.assertRaisesRegex(CommandError, expected): + call_command(Command(), "--credential_id", "123") + + def test_args_from_database(self): + """Correctly parse args from database at the correct times""" + # Nothing in the database, should default to disabled + with self.assertRaisesRegex(CommandError, "RevokeCertificatesConfig is disabled.*"): + call_command(Command(), "--lms_user_ids", "8675309", "--credential_id", "123", "--args-from-database") + + # Add a config + config = RevokeCertificatesConfig.current() + config.arguments = f"--lms_user_ids {self.users[0].lms_user_id} --credential_id 90210" + config.enabled = True + config.save() + + # Not told to use config, should ignore it + with self.assertRaisesRegex(CommandError, "None of the given lms_user_ids maps to a real user*"): + call_command(Command(), "--lms_user_ids", "8675309", "--credential_id", "123") + + # Told to use it, and enabled. Should use config in preference of command line + with self.assertRaisesRegex(CommandError, "No active certificates match the given criteria"): + call_command(Command(), "--lms_user_ids", "8675309", "--credential_id", "123", "--args-from-database") + + config.enabled = False + config.save() + + # Explicitly disabled + with self.assertRaisesRegex(CommandError, "RevokeCertificatesConfig is disabled.*"): + call_command(Command(), "--lms_user_ids", "8675309", "--credential_id", "123", "--args-from-database") diff --git a/credentials/apps/credentials/migrations/0030_revoke_certificates_management_command.py b/credentials/apps/credentials/migrations/0030_revoke_certificates_management_command.py new file mode 100644 index 000000000..70c41cf38 --- /dev/null +++ b/credentials/apps/credentials/migrations/0030_revoke_certificates_management_command.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.16 on 2024-12-04 20:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("credentials", "0029_alter_usercredential_credential_content_type"), + ] + + operations = [ + migrations.CreateModel( + name="RevokeCertificatesConfig", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("change_date", models.DateTimeField(auto_now_add=True, verbose_name="Change date")), + ("enabled", models.BooleanField(default=False, verbose_name="Enabled")), + ( + "arguments", + models.TextField( + blank=True, + default="", + help_text='Arguments for a management command, eg. "--certificate_id 222 --lms_user_ids 867 5309 925".', + ), + ), + ( + "changed_by", + models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + verbose_name="Changed by", + ), + ), + ], + options={ + "verbose_name": "revoke_certificates argument", + }, + ), + ] diff --git a/credentials/apps/credentials/models.py b/credentials/apps/credentials/models.py index 0f6fd98bf..85e1dac9e 100644 --- a/credentials/apps/credentials/models.py +++ b/credentials/apps/credentials/models.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING import bleach +from config_models.models import ConfigurationModel from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType @@ -427,3 +428,24 @@ class UserCredentialDateOverride(TimeStampedModel): date = models.DateTimeField( help_text="The date to override a course certificate with. This is set in the LMS Django Admin.", ) + + +class RevokeCertificatesConfig(ConfigurationModel): + """ + Manages configuration for a run of the revoke_certificates management command. + + .. no_pii: + """ + + class Meta: + app_label = "credentials" + verbose_name = "revoke_certificates argument" + + arguments = models.TextField( + blank=True, + help_text='Arguments for a management command, eg. "--certificate_id 222 --lms_user_ids 867 5309 925".', + default="", + ) + + def __str__(self): + return str(self.arguments) diff --git a/credentials/settings/base.py b/credentials/settings/base.py index 3a344d559..e046ee85e 100644 --- a/credentials/settings/base.py +++ b/credentials/settings/base.py @@ -42,6 +42,8 @@ "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", + # Database-backed configuration + "config_models", ] THIRD_PARTY_APPS = [ diff --git a/requirements/all.txt b/requirements/all.txt index f5ef09b2d..d59c68d51 100644 --- a/requirements/all.txt +++ b/requirements/all.txt @@ -33,11 +33,11 @@ bleach==6.2.0 # via # -r requirements/dev.txt # -r requirements/production.txt -boto3==1.35.74 +boto3==1.35.76 # via # -r requirements/production.txt # django-ses -botocore==1.35.74 +botocore==1.35.76 # via # -r requirements/production.txt # boto3 @@ -135,12 +135,13 @@ distlib==0.3.9 # via # -r requirements/dev.txt # virtualenv -django==4.2.16 +django==4.2.17 # via # -c requirements/common_constraints.txt # -r requirements/dev.txt # -r requirements/production.txt # django-appconf + # django-config-models # django-cors-headers # django-crum # django-debug-toolbar @@ -177,6 +178,10 @@ django-appconf==1.0.6 # -r requirements/dev.txt # -r requirements/production.txt # django-statici18n +django-config-models==2.7.0 + # via + # -r requirements/dev.txt + # -r requirements/production.txt django-cors-headers==4.6.0 # via # -r requirements/dev.txt @@ -253,6 +258,7 @@ djangorestframework==3.15.2 # via # -r requirements/dev.txt # -r requirements/production.txt + # django-config-models # django-rest-swagger # drf-jwt # drf-yasg @@ -301,6 +307,7 @@ edx-django-utils==5.16.0 # -c requirements/constraints.txt # -r requirements/dev.txt # -r requirements/production.txt + # django-config-models # edx-ace # edx-drf-extensions # edx-event-bus-kafka @@ -411,7 +418,7 @@ google-cloud-firestore==2.19.0 # -r requirements/dev.txt # -r requirements/production.txt # firebase-admin -google-cloud-storage==2.18.2 +google-cloud-storage==2.19.0 # via # -r requirements/dev.txt # -r requirements/production.txt @@ -612,7 +619,7 @@ proto-plus==1.25.0 # -r requirements/production.txt # google-api-core # google-cloud-firestore -protobuf==5.29.0 +protobuf==5.29.1 # via # -r requirements/dev.txt # -r requirements/production.txt @@ -804,7 +811,7 @@ simplejson==3.19.3 # -r requirements/production.txt # django-rest-swagger # sailthru-client -six==1.16.0 +six==1.17.0 # via # -r requirements/dev.txt # -r requirements/production.txt diff --git a/requirements/base.in b/requirements/base.in index 1fd279ca1..28bcd596a 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -13,6 +13,7 @@ bleach coreapi didkit django +django-config-models # Configuration models for Django allowing config management with auditing django-cors-headers django-extensions django-filter diff --git a/requirements/base.txt b/requirements/base.txt index 282c42f53..4966bfa2b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -51,11 +51,12 @@ defusedxml==0.8.0rc2 # social-auth-core didkit==0.3.3 # via -r requirements/base.in -django==4.2.16 +django==4.2.17 # via # -c requirements/common_constraints.txt # -r requirements/base.in # django-appconf + # django-config-models # django-cors-headers # django-crum # django-extensions @@ -85,6 +86,8 @@ django==4.2.16 # xss-utils django-appconf==1.0.6 # via django-statici18n +django-config-models==2.7.0 + # via -r requirements/base.in django-cors-headers==4.6.0 # via -r requirements/base.in django-crum==0.7.9 @@ -122,6 +125,7 @@ django-webpack-loader==3.1.1 djangorestframework==3.15.2 # via # -r requirements/base.in + # django-config-models # django-rest-swagger # drf-jwt # drf-yasg @@ -148,6 +152,7 @@ edx-django-utils==5.16.0 # via # -c requirements/constraints.txt # -r requirements/base.in + # django-config-models # edx-ace # edx-drf-extensions # edx-event-bus-kafka @@ -207,7 +212,7 @@ google-cloud-core==2.4.1 # google-cloud-storage google-cloud-firestore==2.19.0 # via firebase-admin -google-cloud-storage==2.18.2 +google-cloud-storage==2.19.0 # via firebase-admin google-crc32c==1.6.0 # via @@ -285,7 +290,7 @@ proto-plus==1.25.0 # via # google-api-core # google-cloud-firestore -protobuf==5.29.0 +protobuf==5.29.1 # via # google-api-core # google-cloud-firestore @@ -372,7 +377,7 @@ simplejson==3.19.3 # via # django-rest-swagger # sailthru-client -six==1.16.0 +six==1.17.0 # via # edx-ace # edx-auth-backends diff --git a/requirements/dev.txt b/requirements/dev.txt index ea4defa4a..4d3ca1c99 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -108,11 +108,12 @@ distlib==0.3.9 # via # -r requirements/test.txt # virtualenv -django==4.2.16 +django==4.2.17 # via # -c requirements/common_constraints.txt # -r requirements/test.txt # django-appconf + # django-config-models # django-cors-headers # django-crum # django-debug-toolbar @@ -147,6 +148,8 @@ django-appconf==1.0.6 # via # -r requirements/test.txt # django-statici18n +django-config-models==2.7.0 + # via -r requirements/test.txt django-cors-headers==4.6.0 # via -r requirements/test.txt django-crum==0.7.9 @@ -193,6 +196,7 @@ django-webpack-loader==3.1.1 djangorestframework==3.15.2 # via # -r requirements/test.txt + # django-config-models # django-rest-swagger # drf-jwt # drf-yasg @@ -225,6 +229,7 @@ edx-django-utils==5.16.0 # via # -c requirements/constraints.txt # -r requirements/test.txt + # django-config-models # edx-ace # edx-drf-extensions # edx-event-bus-kafka @@ -313,7 +318,7 @@ google-cloud-firestore==2.19.0 # via # -r requirements/test.txt # firebase-admin -google-cloud-storage==2.18.2 +google-cloud-storage==2.19.0 # via # -r requirements/test.txt # firebase-admin @@ -468,7 +473,7 @@ proto-plus==1.25.0 # -r requirements/test.txt # google-api-core # google-cloud-firestore -protobuf==5.29.0 +protobuf==5.29.1 # via # -r requirements/test.txt # google-api-core @@ -624,7 +629,7 @@ simplejson==3.19.3 # -r requirements/test.txt # django-rest-swagger # sailthru-client -six==1.16.0 +six==1.17.0 # via # -r requirements/test.txt # edx-ace diff --git a/requirements/django.txt b/requirements/django.txt index 64aaf996f..ebf97308f 100644 --- a/requirements/django.txt +++ b/requirements/django.txt @@ -1 +1 @@ -django==4.2.16 +django==4.2.17 diff --git a/requirements/production.txt b/requirements/production.txt index e393aa400..042ec4e67 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -20,9 +20,9 @@ backoff==2.2.1 # segment-analytics-python bleach==6.2.0 # via -r requirements/base.txt -boto3==1.35.74 +boto3==1.35.76 # via django-ses -botocore==1.35.74 +botocore==1.35.76 # via # boto3 # s3transfer @@ -77,11 +77,12 @@ defusedxml==0.8.0rc2 # social-auth-core didkit==0.3.3 # via -r requirements/base.txt -django==4.2.16 +django==4.2.17 # via # -c requirements/common_constraints.txt # -r requirements/base.txt # django-appconf + # django-config-models # django-cors-headers # django-crum # django-extensions @@ -114,6 +115,8 @@ django-appconf==1.0.6 # via # -r requirements/base.txt # django-statici18n +django-config-models==2.7.0 + # via -r requirements/base.txt django-cors-headers==4.6.0 # via -r requirements/base.txt django-crum==0.7.9 @@ -156,6 +159,7 @@ django-webpack-loader==3.1.1 djangorestframework==3.15.2 # via # -r requirements/base.txt + # django-config-models # django-rest-swagger # drf-jwt # drf-yasg @@ -188,6 +192,7 @@ edx-django-utils==5.16.0 # via # -c requirements/constraints.txt # -r requirements/base.txt + # django-config-models # edx-ace # edx-drf-extensions # edx-event-bus-kafka @@ -264,7 +269,7 @@ google-cloud-firestore==2.19.0 # via # -r requirements/base.txt # firebase-admin -google-cloud-storage==2.18.2 +google-cloud-storage==2.19.0 # via # -r requirements/base.txt # firebase-admin @@ -390,7 +395,7 @@ proto-plus==1.25.0 # -r requirements/base.txt # google-api-core # google-cloud-firestore -protobuf==5.29.0 +protobuf==5.29.1 # via # -r requirements/base.txt # google-api-core @@ -513,7 +518,7 @@ simplejson==3.19.3 # -r requirements/base.txt # django-rest-swagger # sailthru-client -six==1.16.0 +six==1.17.0 # via # -r requirements/base.txt # edx-ace diff --git a/requirements/test.txt b/requirements/test.txt index 7164fca0f..e9f3b8264 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -101,6 +101,7 @@ distlib==0.3.9 # -c requirements/common_constraints.txt # -r requirements/base.txt # django-appconf + # django-config-models # django-cors-headers # django-crum # django-extensions @@ -132,6 +133,8 @@ django-appconf==1.0.6 # via # -r requirements/base.txt # django-statici18n +django-config-models==2.7.0 + # via -r requirements/base.txt django-cors-headers==4.6.0 # via -r requirements/base.txt django-crum==0.7.9 @@ -172,6 +175,7 @@ django-webpack-loader==3.1.1 djangorestframework==3.15.2 # via # -r requirements/base.txt + # django-config-models # django-rest-swagger # drf-jwt # drf-yasg @@ -204,6 +208,7 @@ edx-django-utils==5.16.0 # via # -c requirements/constraints.txt # -r requirements/base.txt + # django-config-models # edx-ace # edx-drf-extensions # edx-event-bus-kafka @@ -288,7 +293,7 @@ google-cloud-firestore==2.19.0 # via # -r requirements/base.txt # firebase-admin -google-cloud-storage==2.18.2 +google-cloud-storage==2.19.0 # via # -r requirements/base.txt # firebase-admin @@ -430,7 +435,7 @@ proto-plus==1.25.0 # -r requirements/base.txt # google-api-core # google-cloud-firestore -protobuf==5.29.0 +protobuf==5.29.1 # via # -r requirements/base.txt # google-api-core @@ -576,7 +581,7 @@ simplejson==3.19.3 # -r requirements/base.txt # django-rest-swagger # sailthru-client -six==1.16.0 +six==1.17.0 # via # -r requirements/base.txt # edx-ace diff --git a/requirements/translations.txt b/requirements/translations.txt index b93ea3576..bafb98caa 100644 --- a/requirements/translations.txt +++ b/requirements/translations.txt @@ -6,7 +6,7 @@ # asgiref==3.8.1 # via django -django==4.2.16 +django==4.2.17 # via # -c requirements/common_constraints.txt # edx-i18n-tools From 0a52fe64e095fee24ec7b95facef1b240b33edb2 Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Thu, 5 Dec 2024 12:38:47 -0800 Subject: [PATCH 05/10] chore: fixing the stubs in the settings file for the log formatter (#2656) this is an attempt to fix the overrides for the log formatter by putting stubs in the settings files. --- credentials/settings/base.py | 3 ++- credentials/settings/devstack.py | 3 ++- credentials/settings/local.py | 3 ++- credentials/settings/test.py | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/credentials/settings/base.py b/credentials/settings/base.py index e046ee85e..dda8f1370 100644 --- a/credentials/settings/base.py +++ b/credentials/settings/base.py @@ -406,7 +406,8 @@ ACE_CHANNEL_SAILTHRU_TEMPLATE_NAME = "" # unused, but required to be set or we see an exception # Set up logging for development use (logging to stdout) -LOGGING = get_logger_config(debug=DEBUG, dev_env=True, local_loglevel="DEBUG") +LOGGING_FORMAT_STRING = "" +LOGGING = get_logger_config(debug=DEBUG, dev_env=True, local_loglevel="DEBUG", format_string=LOGGING_FORMAT_STRING) # DRF Settings REST_FRAMEWORK = { diff --git a/credentials/settings/devstack.py b/credentials/settings/devstack.py index 0f54b7ecf..ae4026a81 100644 --- a/credentials/settings/devstack.py +++ b/credentials/settings/devstack.py @@ -8,7 +8,8 @@ ALLOWED_HOSTS = ["*"] -LOGGING = get_logger_config(debug=True, dev_env=True, local_loglevel="DEBUG") +LOGGING_FORMAT_STRING = "" +LOGGING = get_logger_config(debug=True, dev_env=True, local_loglevel="DEBUG", format_string=LOGGING_FORMAT_STRING) del LOGGING["handlers"]["local"] SECRET_KEY = os.environ.get("SECRET_KEY", "change-me") diff --git a/credentials/settings/local.py b/credentials/settings/local.py index 06101bdae..9e41dcdd5 100644 --- a/credentials/settings/local.py +++ b/credentials/settings/local.py @@ -56,7 +56,8 @@ USER_CACHE_TTL = 60 # LOGGING -LOGGING = get_logger_config(debug=True, dev_env=True, local_loglevel="DEBUG") +LOGGING_FORMAT_STRING = "" +LOGGING = get_logger_config(debug=True, dev_env=True, local_loglevel="DEBUG", format_string=LOGGING_FORMAT_STRING) ##################################################################### # Lastly, see if the developer has any local overrides. diff --git a/credentials/settings/test.py b/credentials/settings/test.py index 3efd26246..d01b8754a 100644 --- a/credentials/settings/test.py +++ b/credentials/settings/test.py @@ -10,7 +10,8 @@ "credentials.apps.edx_credentials_extensions", ] -LOGGING = get_logger_config(debug=False, dev_env=True, local_loglevel="DEBUG") +LOGGING_FORMAT_STRING = "" +LOGGING = get_logger_config(debug=False, dev_env=True, local_loglevel="DEBUG", format_string=LOGGING_FORMAT_STRING) ALLOWED_HOSTS = ["*"] DATABASES = { From df9cdd5534fb183535b8bb2d165f8f4510614b2e Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Thu, 5 Dec 2024 12:54:53 -0800 Subject: [PATCH 06/10] feat: public dockerfiles are deprecated (#2657) This removal supports the [deprecation of public dockerfiles](https://github.com/openedx/public-engineering/issues/263). * this removes the public dockerfile * the corresponding workflow action was removed in #2618 FIXES: APER-3435 --- Dockerfile | 111 ----------------------------------------------------- 1 file changed, 111 deletions(-) delete mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e57c142d0..000000000 --- a/Dockerfile +++ /dev/null @@ -1,111 +0,0 @@ -FROM ubuntu:focal as base - -# System requirements -# - git; Used to pull in particular requirements from github rather than pypi, -# and to check the sha of the code checkout. -# - language-pack-en locales; ubuntu locale support so that system utilities have a consistent -# language and time zone. -# - python; ubuntu doesnt ship with python, so this is the python we will use to run the application -# - python3-pip; install pip to install application requirements.txt files -# - libssl-dev; # mysqlclient wont install without this. -# - libmysqlclient-dev; to install header files needed to use native C implementation for -# MySQL-python for performance gains. -# - wget; to download a watchman binary archive -# - unzip; to unzip a watchman binary archive -# - pkg-config; mysqlclient>=2.2.0 requires pkg-config (https://github.com/PyMySQL/mysqlclient/issues/620) - -# If you add a package here please include a comment above describing what it is used for -RUN apt-get update && \ - apt-get install -y software-properties-common && \ - apt-add-repository -y ppa:deadsnakes/ppa && apt-get update && \ - apt-get upgrade -qy && apt-get install language-pack-en locales gettext git \ - python3.11-dev python3.11-venv libmysqlclient-dev libssl-dev build-essential wget unzip pkg-config -qy && \ - rm -rf /var/lib/apt/lists/* - -# Create Python env -ENV VIRTUAL_ENV=/edx/app/credentials/venvs/credentials -RUN python3.11 -m venv $VIRTUAL_ENV -ENV PATH="$VIRTUAL_ENV/bin:$PATH" - -# Create Node env -RUN pip install nodeenv -ENV NODE_ENV=/edx/app/credentials/nodeenvs/credentials -RUN nodeenv $NODE_ENV --node=18.17.1 --prebuilt -ENV PATH="$NODE_ENV/bin:$PATH" -RUN npm install -g npm@9.x.x - -RUN locale-gen en_US.UTF-8 -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 -ENV DJANGO_SETTINGS_MODULE credentials.settings.production -ENV OPENEDX_ATLAS_PULL true -ENV CREDENTIALS_CFG "minimal.yml" - -EXPOSE 18150 -RUN useradd -m --shell /bin/false app - -# Install watchman -RUN wget https://github.com/facebook/watchman/releases/download/v2023.11.20.00/watchman-v2023.11.20.00-linux.zip -RUN unzip watchman-v2023.11.20.00-linux.zip -RUN mkdir -p /usr/local/{bin,lib} /usr/local/var/run/watchman -RUN cp watchman-v2023.11.20.00-linux/bin/* /usr/local/bin -RUN cp watchman-v2023.11.20.00-linux/lib/* /usr/local/lib -RUN chmod 755 /usr/local/bin/watchman -RUN chmod 2777 /usr/local/var/run/watchman - -# Now install credentials -WORKDIR /edx/app/credentials/credentials - -# Copy the requirements explicitly even though we copy everything below -# this prevents the image cache from busting unless the dependencies have changed. -COPY requirements/production.txt /edx/app/credentials/credentials/requirements/production.txt -COPY requirements/pip_tools.txt /edx/app/credentials/credentials/requirements/pip_tools.txt - -# Dependencies are installed as root so they cannot be modified by the application user. -RUN pip install -r requirements/pip_tools.txt -RUN pip install -r requirements/production.txt - -RUN mkdir -p /edx/var/log - -# This line is after the python requirements so that changes to the code will not -# bust the image cache -COPY . /edx/app/credentials/credentials - -# Fetch the translations into the image once the Makefile's in place -RUN make pull_translations - -# Install dependencies in node_modules directory -RUN npm install --no-save -ENV NODE_BIN=/edx/app/credentials/credentials/node_modules -ENV PATH="$NODE_BIN/.bin:$PATH" -# Run webpack -RUN webpack --config webpack.config.js - -# Change static folder owner to application user. -RUN chown -R app:app /edx/app/credentials/credentials/credentials/static - -# Code is owned by root so it cannot be modified by the application user. -# So we copy it before changing users. -USER app - -# Gunicorn 19 does not log to stdout or stderr by default. Once we are past gunicorn 19, the logging to STDOUT need not be specified. -CMD gunicorn --workers=2 --name credentials -c /edx/app/credentials/credentials/credentials/docker_gunicorn_configuration.py --log-file - --max-requests=1000 credentials.wsgi:application - -# We don't switch back to the app user for devstack because we need devstack users to be -# able to update requirements and generally run things as root. -FROM base as dev -USER root -ENV DJANGO_SETTINGS_MODULE credentials.settings.devstack -RUN pip install -r /edx/app/credentials/credentials/requirements/dev.txt -RUN make pull_translations - -# Temporary compatibility hack while devstack is supporting -# both the old `edxops/credentials` image and this image: -# Add in a dummy ../credentials_env file. -# The credentials_env file was originally needed for sourcing to get -# environment variables like DJANGO_SETTINGS_MODULE, but now we just set -# those variables right in the Dockerfile. -RUN touch ../credentials_env - -CMD while true; do python ./manage.py runserver 0.0.0.0:18150; sleep 2; done From 28c5b3c88c4a3732f4b033d5257f7c3b01c4c189 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 00:21:43 +0000 Subject: [PATCH 07/10] fix(deps): update dependency webpack to v5.97.1 (#2659) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47fa68c4d..d185e5ea5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "sass": "1.82.0", "sass-loader": "16.0.4", "url-loader": "4.1.1", - "webpack": "5.97.0", + "webpack": "5.97.1", "webpack-bundle-tracker": "3.1.1" }, "devDependencies": { @@ -11011,9 +11011,9 @@ } }, "node_modules/webpack": { - "version": "5.97.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.0.tgz", - "integrity": "sha512-CWT8v7ShSfj7tGs4TLRtaOLmOCPWhoKEvp+eA7FVx8Xrjb3XfT0aXdxDItnRZmE8sHcH+a8ayDrJCOjXKxVFfQ==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", diff --git a/package.json b/package.json index b52a2aefc..5e500dc39 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "sass": "1.82.0", "sass-loader": "16.0.4", "url-loader": "4.1.1", - "webpack": "5.97.0", + "webpack": "5.97.1", "webpack-bundle-tracker": "3.1.1" }, "devDependencies": { From 49df0b63faa6ac4a8e9ab11e088dc678f4512cb4 Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Fri, 6 Dec 2024 06:12:28 -0800 Subject: [PATCH 08/10] feat: attempting a new fix for the local log format problem (#2658) The previous attempt to set a local log format failed because first the base configuration files are applied, in which the logging format is set via a function call, and then the overrides come in from edx-internal. Basically, there's a timing problem. This is an attempt to solve that problem in the hopes that the environment variables are set before the configuration files for the application are run. This change will be made in conjunction with a PR on at ex-internal. FIXES: APER-3805 --- credentials/settings/base.py | 2 +- credentials/settings/devstack.py | 2 +- credentials/settings/local.py | 2 +- credentials/settings/test.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/credentials/settings/base.py b/credentials/settings/base.py index dda8f1370..6fdbbde9e 100644 --- a/credentials/settings/base.py +++ b/credentials/settings/base.py @@ -406,7 +406,7 @@ ACE_CHANNEL_SAILTHRU_TEMPLATE_NAME = "" # unused, but required to be set or we see an exception # Set up logging for development use (logging to stdout) -LOGGING_FORMAT_STRING = "" +LOGGING_FORMAT_STRING = os.environ.get("LOGGING_FORMAT_STRING", "") LOGGING = get_logger_config(debug=DEBUG, dev_env=True, local_loglevel="DEBUG", format_string=LOGGING_FORMAT_STRING) # DRF Settings diff --git a/credentials/settings/devstack.py b/credentials/settings/devstack.py index ae4026a81..a36b5939e 100644 --- a/credentials/settings/devstack.py +++ b/credentials/settings/devstack.py @@ -8,7 +8,7 @@ ALLOWED_HOSTS = ["*"] -LOGGING_FORMAT_STRING = "" +LOGGING_FORMAT_STRING = os.environ.get("LOGGING_FORMAT_STRING", "") LOGGING = get_logger_config(debug=True, dev_env=True, local_loglevel="DEBUG", format_string=LOGGING_FORMAT_STRING) del LOGGING["handlers"]["local"] diff --git a/credentials/settings/local.py b/credentials/settings/local.py index 9e41dcdd5..5df12a5fb 100644 --- a/credentials/settings/local.py +++ b/credentials/settings/local.py @@ -56,7 +56,7 @@ USER_CACHE_TTL = 60 # LOGGING -LOGGING_FORMAT_STRING = "" +LOGGING_FORMAT_STRING = os.environ.get("LOGGING_FORMAT_STRING", "") LOGGING = get_logger_config(debug=True, dev_env=True, local_loglevel="DEBUG", format_string=LOGGING_FORMAT_STRING) ##################################################################### diff --git a/credentials/settings/test.py b/credentials/settings/test.py index d01b8754a..71b55c701 100644 --- a/credentials/settings/test.py +++ b/credentials/settings/test.py @@ -10,7 +10,7 @@ "credentials.apps.edx_credentials_extensions", ] -LOGGING_FORMAT_STRING = "" +LOGGING_FORMAT_STRING = os.environ.get("LOGGING_FORMAT_STRING", "") LOGGING = get_logger_config(debug=False, dev_env=True, local_loglevel="DEBUG", format_string=LOGGING_FORMAT_STRING) ALLOWED_HOSTS = ["*"] From e3f24fd3d6e1d15bfd638f089107c2cc46c4e21b Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Fri, 6 Dec 2024 09:50:03 -0800 Subject: [PATCH 09/10] chore: fix timing issue with logging overrides (#2660) Make sure that the call to `get_logger` happens after the configuration file overrides have a chance to set `LOGGING_FORMAT_STRING`. FIXES: APER-3805 --- credentials/settings/production.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/credentials/settings/production.py b/credentials/settings/production.py index 43c3e92cd..8b3ad8828 100644 --- a/credentials/settings/production.py +++ b/credentials/settings/production.py @@ -12,9 +12,6 @@ ALLOWED_HOSTS = ["*"] -LOGGING_FORMAT_STRING = "" -LOGGING = get_logger_config(format_string=LOGGING_FORMAT_STRING) - # Keep track of the names of settings that represent dicts. Instead of overriding the values in base.py, # the values read from disk should UPDATE the pre-configured dicts. DICT_UPDATE_KEYS = ("JWT_AUTH",) @@ -50,6 +47,9 @@ # Load the files storage backend settings for django storages vars().update(FILE_STORAGE_BACKEND) +# make sure this happens after the configuration file overrides so format string can be overridden +LOGGING = get_logger_config(format_string=LOGGING_FORMAT_STRING) + if "EXTRA_APPS" in locals(): INSTALLED_APPS += EXTRA_APPS From f9971637eee5c1625f6750618a6b6921152819c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 02:16:54 +0000 Subject: [PATCH 10/10] fix(deps): update dependency @openedx/paragon to v22.11.0 (#2661) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d185e5ea5..128824883 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2067,9 +2067,9 @@ } }, "node_modules/@openedx/paragon": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.10.0.tgz", - "integrity": "sha512-uwH/vN6PM9v77NIJ0MUyREdF+3LY/kXIVaOAN+TJKi6JexKoqM7jR30wGuI83YGymwthXDc8T4J54O/wXDoxrQ==", + "version": "22.11.0", + "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.11.0.tgz", + "integrity": "sha512-Bvh2o6ZeTLNtqYVr/ajKI29v//M/mEddaoOqjkzobE4JpS0muOJyQSEu2ju7dha90Sjjk/zy8Dr2enZ7phpkkQ==", "license": "Apache-2.0", "workspaces": [ "example",