Skip to content

Commit

Permalink
feat: populate encrypted client id and secret
Browse files Browse the repository at this point in the history
* add migration to populate encrypted client id and secret in CanvasEnterpriseCustomerConfiguration
* removes references to old client id and secret in CanvasEnterpriseCustomerConfiguration
  • Loading branch information
sameeramin committed Aug 27, 2024
1 parent 08ca882 commit 3cb417d
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Unreleased
----------
* nothing unreleased

[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
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.13"
__version__ = "4.23.14"
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/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,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),
]
45 changes: 45 additions & 0 deletions integrated_channels/canvas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

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 Down Expand Up @@ -57,6 +58,28 @@ class CanvasEnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfiguratio
null=True,
)

@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

client_secret = models.CharField(
max_length=255,
blank=True,
Expand All @@ -80,6 +103,28 @@ class CanvasEnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfiguratio
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
16 changes: 8 additions & 8 deletions tests/test_integrated_channels/test_canvas/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ def setUp(self):

self.course_api_path = "/api/v1/provider/content/course"
self.course_url = urljoin(self.url_base, self.course_api_path)
self.client_id = "client_id"
self.client_secret = "client_secret"
self.decrypted_client_id = "client_id"
self.decrypted_client_secret = "client_secret"
self.access_token = "access_token"
self.refresh_token = "refresh_token"
self.enterprise_config = factories.CanvasEnterpriseCustomerConfigurationFactory(
client_id=self.client_id,
client_secret=self.client_secret,
decrypted_client_id=self.decrypted_client_id,
decrypted_client_secret=self.decrypted_client_secret,
canvas_account_id=self.account_id,
canvas_base_url=self.url_base,
refresh_token=self.refresh_token,
Expand Down Expand Up @@ -426,14 +426,14 @@ def test_create_client_session_with_oauth_access_key(self):

def test_client_instantiation_fails_without_client_id(self):
with pytest.raises(ClientError) as client_error:
self.enterprise_config.client_id = None
self.enterprise_config.decrypted_client_id = None
canvas_api_client = CanvasAPIClient(self.enterprise_config)
canvas_api_client._create_session() # pylint: disable=protected-access
assert client_error.value.message == "Failed to generate oauth access token: Client ID required."

def test_client_instantiation_fails_without_client_secret(self):
with pytest.raises(ClientError) as client_error:
self.enterprise_config.client_secret = None
self.enterprise_config.decrypted_client_secret = None
canvas_api_client = CanvasAPIClient(self.enterprise_config)
canvas_api_client._create_session() # pylint: disable=protected-access
assert client_error.value.message == "Failed to generate oauth access token: Client secret required."
Expand Down Expand Up @@ -1083,8 +1083,8 @@ def test_health_check_invalid_config(self):
Test the client health check with invalid config
"""
bad_enterprise_config = factories.CanvasEnterpriseCustomerConfigurationFactory(
client_id=self.client_id,
client_secret=self.client_secret,
decrypted_client_id=self.decrypted_client_id,
decrypted_client_secret=self.decrypted_client_secret,
canvas_account_id=self.account_id,
canvas_base_url='Not a valid url',
refresh_token=self.refresh_token,
Expand Down
8 changes: 4 additions & 4 deletions tests/test_integrated_channels/test_canvas/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ def setUp(self):
self.account_id = random.randint(9223372036854775800, 9223372036854775807)
self.course_id = "edx+111"
self.url_base = "http://betatest.instructure.com"
self.client_id = "client_id"
self.client_secret = "client_secret"
self.decrypted_client_id = "client_id"
self.decrypted_client_secret = "client_secret"
self.access_token = "access_token"
self.refresh_token = "refresh_token"
self.enterprise_config = factories.CanvasEnterpriseCustomerConfigurationFactory(
client_id=self.client_id,
client_secret=self.client_secret,
decrypted_client_id=self.decrypted_client_id,
decrypted_client_secret=self.decrypted_client_secret,
canvas_account_id=self.account_id,
canvas_base_url=self.url_base,
refresh_token=self.refresh_token,
Expand Down
8 changes: 4 additions & 4 deletions tests/test_integrated_channels/test_canvas/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ def setUp(self):

CanvasEnterpriseCustomerConfiguration.objects.get_or_create(
uuid=SINGLE_CANVAS_CONFIG['uuid'],
client_id=SINGLE_CANVAS_CONFIG['client_id'],
client_secret=SINGLE_CANVAS_CONFIG['client_secret'],
decrypted_client_id=SINGLE_CANVAS_CONFIG['client_id'],
decrypted_client_secret=SINGLE_CANVAS_CONFIG['client_secret'],
canvas_account_id=SINGLE_CANVAS_CONFIG['canvas_account_id'],
canvas_base_url=SINGLE_CANVAS_CONFIG['canvas_base_url'],
enterprise_customer=self.enterprise_customer,
Expand All @@ -69,8 +69,8 @@ def setUp(self):

CanvasEnterpriseCustomerConfiguration.objects.get_or_create(
uuid=SECOND_CANVAS_CONFIG['uuid'],
client_id=SECOND_CANVAS_CONFIG['client_id'],
client_secret=SECOND_CANVAS_CONFIG['client_secret'],
decrypted_client_id=SECOND_CANVAS_CONFIG['client_id'],
decrypted_client_secret=SECOND_CANVAS_CONFIG['client_secret'],
canvas_account_id=SECOND_CANVAS_CONFIG['canvas_account_id'],
canvas_base_url=SECOND_CANVAS_CONFIG['canvas_base_url'],
enterprise_customer=self.enterprise_customer,
Expand Down

0 comments on commit 3cb417d

Please sign in to comment.