Skip to content

Commit

Permalink
Merge branch 'master' into bilalqamar95/node20-upgrade-1
Browse files Browse the repository at this point in the history
  • Loading branch information
BilalQamar95 authored Sep 2, 2024
2 parents 53d37e4 + 93d2200 commit 183ba80
Show file tree
Hide file tree
Showing 19 changed files with 328 additions and 65 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ Unreleased
----------
* nothing unreleased

[4.23.16]
----------
* feat: remove references to client_id and client_secret from CanvasEnterpriseCustomerConfiguration

[4.23.15]
----------
* feat: altered decrypted_secret to be encrypted and made credentials nullable

[4.23.14]
----------
* feat: populate encrypted client id and secret for canvas integration and remove references to unencrypted fields

[4.23.13]
----------
* feat: added encrypted columns for user credentials for SAP config

[4.23.12]
----------
* feat: feat: added encrypted client id and secret for canvas integration

[4.23.11]
----------
* feat: implement back-off and retry for SAP SuccessFactors
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.23.11"
__version__ = "4.23.16"
11 changes: 11 additions & 0 deletions enterprise/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,14 @@ def generate_default_orchestration_record_display_name(sender, instance, **kwarg

if COURSE_ENROLLMENT_CHANGED is not None:
COURSE_ENROLLMENT_CHANGED.connect(course_enrollment_changed_receiver)


@receiver(pre_save, sender=SAPSuccessFactorsEnterpriseCustomerConfiguration)
def update_decrypted_credentials(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Ensure that the decrypted credentials have same values as unencrypted credentials.
"""
if instance.key != instance.decrypted_key:
instance.decrypted_key = instance.key
if instance.secret != instance.decrypted_secret:
instance.decrypted_secret = instance.secret
9 changes: 7 additions & 2 deletions integrated_channels/api/v1/canvas/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Serializers for Canvas.
"""
from rest_framework import serializers

from integrated_channels.api.serializers import EnterpriseCustomerPluginConfigSerializer
from integrated_channels.canvas.models import CanvasEnterpriseCustomerConfiguration

Expand All @@ -9,12 +11,15 @@ class CanvasEnterpriseCustomerConfigurationSerializer(EnterpriseCustomerPluginCo
class Meta:
model = CanvasEnterpriseCustomerConfiguration
extra_fields = (
'client_id',
'client_secret',
'encrypted_client_id',
'encrypted_client_secret',
'canvas_account_id',
'canvas_base_url',
'refresh_token',
'uuid',
'oauth_authorization_url',
)
fields = EnterpriseCustomerPluginConfigSerializer.Meta.fields + extra_fields

encrypted_client_id = serializers.CharField(required=False, allow_blank=False, read_only=False)
encrypted_client_secret = serializers.CharField(required=False, allow_blank=False, read_only=False)
4 changes: 2 additions & 2 deletions integrated_channels/canvas/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class CanvasEnterpriseCustomerConfigurationAdmin(DjangoObjectActions, admin.Mode
"""
list_display = (
"enterprise_customer_name",
"client_id",
"client_secret",
"decrypted_client_id",
"decrypted_client_secret",
"canvas_account_id",
"canvas_base_url",
)
Expand Down
4 changes: 2 additions & 2 deletions integrated_channels/canvas/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,8 +959,8 @@ def _get_oauth_access_token(self):
HTTPError: If we received a failure response code from Canvas.
ClientError: If an unexpected response format was received that we could not parse.
"""
client_id = self.enterprise_configuration.client_id
client_secret = self.enterprise_configuration.client_secret
client_id = self.enterprise_configuration.decrypted_client_id
client_secret = self.enterprise_configuration.decrypted_client_secret

if not client_id:
raise ClientError(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.15 on 2024-08-26 07:03

from django.db import migrations
import fernet_fields.fields


class Migration(migrations.Migration):

dependencies = [
('canvas', '0035_canvaslearnerassessmentdatatransmissionaudit_is_transmitted_and_more'),
]

operations = [
migrations.AddField(
model_name='canvasenterprisecustomerconfiguration',
name='decrypted_client_id',
field=fernet_fields.fields.EncryptedCharField(blank=True, default='', help_text='The encrypted API Client ID provided to edX by the enterprise customer to be used to make API calls to Canvas on behalf of the customer. It will be encrypted when stored in the database.', max_length=255, null=True, verbose_name='Encrypted API Client ID'),
),
migrations.AddField(
model_name='canvasenterprisecustomerconfiguration',
name='decrypted_client_secret',
field=fernet_fields.fields.EncryptedCharField(blank=True, default='', help_text='The encrypted API Client Secret provided to edX by the enterprise customer to be used to make API calls to Canvas on behalf of the customer. It will be encrypted when stored in the database.', max_length=255, null=True, verbose_name='Encrypted API Client Secret'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.15 on 2024-08-26 13:57

from django.db import migrations


def populate_decrypted_client_id_and_secret(apps, schema_editor): # pragma: no cover
"""
Populate the decrypted_client_id and decrypted_client_secret fields for all
existing CanvasEnterpriseCustomerConfiguration
"""
CanvasEnterpriseCustomerConfiguration = apps.get_model('canvas', 'CanvasEnterpriseCustomerConfiguration')
for config in CanvasEnterpriseCustomerConfiguration.objects.all():
config.decrypted_client_id = config.client_id
config.decrypted_client_secret = config.client_secret
config.save()


class Migration(migrations.Migration):

dependencies = [
('canvas', '0036_canvasenterprisecustomerconfiguration_decrypted_client_id_and_more'),
]

operations = [
migrations.RunPython(populate_decrypted_client_id_and_secret, reverse_code=migrations.RunPython.noop),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.15 on 2024-09-02 08:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('canvas', '0037_canvasenterprisecustomerconfiguration_copy_id_and_secret_and_more'),
]

operations = [
migrations.AlterField(
model_name='canvasenterprisecustomerconfiguration',
name='client_id',
field=models.CharField(blank=True, default='', help_text='The API Client ID provided to edX by the enterprise customer to be used to make API calls to Canvas on behalf of the customer.', max_length=255, null=True, verbose_name='API Client ID'),
),
migrations.AlterField(
model_name='canvasenterprisecustomerconfiguration',
name='client_secret',
field=models.CharField(blank=True, default='', help_text='The API Client Secret provided to edX by the enterprise customer to be used to make API calls to Canvas on behalf of the customer.', max_length=255, null=True, verbose_name='API Client Secret'),
),
]
80 changes: 64 additions & 16 deletions integrated_channels/canvas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import uuid
from logging import getLogger

from fernet_fields import EncryptedCharField
from six.moves.urllib.parse import urljoin

from django.conf import settings
from django.db import models
from django.utils.encoding import force_bytes, force_str
from django.utils.translation import gettext_lazy as _

from integrated_channels.canvas.exporters.content_metadata import CanvasContentMetadataExporter
Expand All @@ -33,28 +35,74 @@ class CanvasEnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfiguratio
Based on: https://canvas.instructure.com/doc/api/file.oauth.html#oauth2-flow-3
"""

client_id = models.CharField(
decrypted_client_id = EncryptedCharField(
max_length=255,
blank=True,
default='',
verbose_name="API Client ID",
help_text=_(
"The API Client ID provided to edX by the enterprise customer to be used to make API "
"calls to Canvas on behalf of the customer."
)
verbose_name="Encrypted API Client ID",
help_text=(
"The encrypted API Client ID provided to edX by the enterprise customer to be used to make API "
"calls to Canvas on behalf of the customer. It will be encrypted when stored in the database."
),
null=True,
)

client_secret = models.CharField(
@property
def encrypted_client_id(self):
"""
Returns encrypted client_id as a string.
The data is encrypted in the DB at rest, but unencrypted in the app when retrieved through
`decrypted_client_id`. This method will encrypt the `client_id` again before sending.
"""
if self.decrypted_client_id:
return force_str(
self._meta.get_field('decrypted_client_id').fernet.encrypt(
force_bytes(self.decrypted_client_id)
)
)
return self.decrypted_client_id

@encrypted_client_id.setter
def encrypted_client_id(self, value):
"""
Set the `decrypted_client_id` from the encrypted value.
"""
self.decrypted_client_id = value

decrypted_client_secret = EncryptedCharField(
max_length=255,
blank=True,
default='',
verbose_name="API Client Secret",
help_text=_(
"The API Client Secret provided to edX by the enterprise customer to be used to make "
" API calls to Canvas on behalf of the customer."
)
verbose_name="Encrypted API Client Secret",
help_text=(
"The encrypted API Client Secret provided to edX by the enterprise customer to be used to make "
" API calls to Canvas on behalf of the customer. It will be encrypted when stored in the database."
),
null=True,
)

@property
def encrypted_client_secret(self):
"""
Returns encrypted client_secret as a string.
The data is encrypted in the DB at rest, but unencrypted in the app when retrieved through
`decrypted_client_secret`. This method will encrypt the `client_secret` again before sending.
"""
if self.decrypted_client_secret:
return force_str(
self._meta.get_field('decrypted_client_secret').fernet.encrypt(
force_bytes(self.decrypted_client_secret)
)
)
return self.decrypted_client_secret

@encrypted_client_secret.setter
def encrypted_client_secret(self, value):
"""
Set the `decrypted_client_secret` from the encrypted value.
"""
self.decrypted_client_secret = value

canvas_account_id = models.BigIntegerField(
null=True,
blank=True,
Expand Down Expand Up @@ -107,11 +155,11 @@ def oauth_authorization_url(self):
obj: The instance of CanvasEnterpriseCustomerConfiguration
being rendered with this admin form.
"""
if self.canvas_base_url and self.client_id:
if self.canvas_base_url and self.decrypted_client_id:
return (f'{self.canvas_base_url}/login/oauth2/auth'
f'?redirect_uri={LMS_OAUTH_REDIRECT_URL}&'
f'response_type=code&'
f'client_id={self.client_id}&state={self.uuid}')
f'client_id={self.decrypted_client_id}&state={self.uuid}')
else:
return None

Expand All @@ -126,9 +174,9 @@ def is_valid(self):
"""
missing_items = {'missing': []}
incorrect_items = {'incorrect': []}
if not self.client_id:
if not self.decrypted_client_id:
missing_items.get('missing').append('client_id')
if not self.client_secret:
if not self.decrypted_client_secret:
missing_items.get('missing').append('client_secret')
if not self.canvas_base_url:
missing_items.get('missing').append('canvas_base_url')
Expand Down
4 changes: 2 additions & 2 deletions integrated_channels/canvas/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ def get(self, request, *args, **kwargs):

access_token_request_params = {
'grant_type': 'authorization_code',
'client_id': enterprise_config.client_id,
'client_secret': enterprise_config.client_secret,
'client_id': enterprise_config.decrypted_client_id,
'client_secret': enterprise_config.decrypted_client_secret,
'redirect_uri': settings.LMS_INTERNAL_ROOT_URL + "/canvas/oauth-complete",
'code': client_code,
}
Expand Down
2 changes: 2 additions & 0 deletions integrated_channels/sap_success_factors/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ class SAPSuccessFactorsEnterpriseCustomerConfigurationAdmin(DjangoObjectActions,
"sapsf_base_url",
"sapsf_company_id",
"key",
"decrypted_key",
"secret",
"decrypted_secret",
"sapsf_user_id",
"user_type",
"has_access_token",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.23 on 2024-08-23 09:36

from django.db import migrations, models
import fernet_fields.fields


class Migration(migrations.Migration):

dependencies = [
('sap_success_factors', '0016_sapsuccessfactorslearnerdatatransmissionaudit_is_transmitted'),
]

operations = [
migrations.AddField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='decrypted_key',
field=fernet_fields.fields.EncryptedCharField(blank=True, default='', help_text='The encrypted OAuth client identifier. It will be encrypted when stored in the database.', max_length=255, null=True, verbose_name='Encrypted Client ID'),
),
migrations.AddField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='decrypted_secret',
field=models.CharField(blank=True, default='', help_text='The encrypted OAuth client secret. It will be encrypted when stored in the database.', max_length=255, null=True, verbose_name='Encrypted Client Secret'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 3.2.23 on 2024-08-29 09:10

from django.db import migrations, models
import fernet_fields.fields


class Migration(migrations.Migration):

dependencies = [
('sap_success_factors', '0017_auto_20240823_0936'),
]

operations = [
migrations.AlterField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='decrypted_secret',
field=fernet_fields.fields.EncryptedCharField(blank=True, default='', help_text='The encrypted OAuth client secret. It will be encrypted when stored in the database.', max_length=255, null=True, verbose_name='Encrypted Client Secret'),
),
migrations.AlterField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='key',
field=models.CharField(blank=True, default='', help_text='OAuth client identifier.', max_length=255, null=True, verbose_name='Client ID'),
),
migrations.AlterField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='secret',
field=models.CharField(blank=True, default='', help_text='OAuth client secret.', max_length=255, null=True, verbose_name='Client Secret'),
),
]
Loading

0 comments on commit 183ba80

Please sign in to comment.