Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added fields for holding encrypted data in database for blackboard #1907

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5eebfbe
feat: added fields for holding encrypted data in database for blackboard
MueezKhan246 Oct 11, 2023
6ad3968
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
MueezKhan246 Apr 22, 2024
313b04b
refactor: removing conflict issues from utils file
MueezKhan246 Apr 22, 2024
4ddea10
refactor: removed unused import
MueezKhan246 Apr 22, 2024
5f3cb08
refactor: removing unnecessary pass statement
MueezKhan246 Apr 22, 2024
e440722
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
MueezKhan246 Apr 22, 2024
4f1aa45
refactor: resolved migration issues
MueezKhan246 Apr 23, 2024
df42385
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
MueezKhan246 Apr 23, 2024
48b4f06
refactor: removed redundant line at the end of file
MueezKhan246 Apr 23, 2024
0b46f2c
Merge branch 'master' of github.com:openedx/edx-enterprise into Mueez…
MueezKhan246 Apr 24, 2024
fa3b473
Merge branch 'master' of github.com:openedx/edx-enterprise into Mueez…
MueezKhan246 Apr 25, 2024
1c8198f
Merge branch 'master' into MueezKhan/Encryption-Fields-Added-To-Black…
MueezKhan246 Apr 25, 2024
f1c37bd
Merge branch 'master' of github.com:openedx/edx-enterprise into Mueez…
MueezKhan246 May 2, 2024
6a4813b
test: updating test case for blackboard global config model
MueezKhan246 May 2, 2024
41357fa
Merge branch 'MueezKhan/Encryption-Fields-Added-To-BlackboardConfig' …
MueezKhan246 May 2, 2024
44eb27a
refactor: removing trailing space
MueezKhan246 May 2, 2024
8ab0a56
refactor: disabling lint warning for unused schema editor
MueezKhan246 May 2, 2024
5859701
refactor: disabling unused-argument
MueezKhan246 May 2, 2024
633f51e
Merge branch 'master' of github.com:openedx/edx-enterprise into Mueez…
MueezKhan246 May 7, 2024
4d6eb66
refactor: updated changelog file
MueezKhan246 May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.17.2]
--------
* feat: added fields for holding encrypted data in database for blackboard

[4.17.1]
--------
* revert: revert async task functionality implemented in 4.15.6
Expand Down
5 changes: 5 additions & 0 deletions integrated_channels/api/v1/blackboard/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ class Meta:
extra_fields = (
'client_id',
'client_secret',
Comment on lines 19 to 20
Copy link
Member

Choose a reason for hiding this comment

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

can we remove these two fields from here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sameenfatima78 i planned to create a separate PR for removing these and feature flag, like done for other integrated channels

'encrypted_client_id',
'encrypted_client_secret',
'blackboard_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)

sameenfatima78 marked this conversation as resolved.
Show resolved Hide resolved

class BlackboardGlobalConfigSerializer(serializers.ModelSerializer):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.23 on 2024-04-22 17:09

from django.db import migrations
import fernet_fields.fields


class Migration(migrations.Migration):

dependencies = [
('blackboard', '0019_delete_historicalblackboardenterprisecustomerconfiguration'),
]

operations = [
migrations.AddField(
model_name='blackboardenterprisecustomerconfiguration',
name='decrypted_client_id',
field=fernet_fields.fields.EncryptedCharField(blank=True, default='', help_text='The API Client ID (encrypted at db level) provided to edX by the enterprise customer to be used to make API calls to Degreed on behalf of the customer.', max_length=255, verbose_name='API Client ID encrypted at db level'),
),
migrations.AddField(
model_name='blackboardenterprisecustomerconfiguration',
name='decrypted_client_secret',
field=fernet_fields.fields.EncryptedCharField(blank=True, default='', help_text='The API Client Secret (encrypted at db level) provided to edX by the enterprise customer to be used to make API calls to Degreed on behalf of the customer.', max_length=255, verbose_name='API Client Secret encrypted at db level'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Generated by Django 3.2.23 on 2024-04-23 10:57

from django.db import migrations
from integrated_channels.blackboard.utils import populate_decrypted_fields_blackboard


class Migration(migrations.Migration):

dependencies = [
('blackboard', '0020_auto_20240422_1709'),
]

operations = [
migrations.RunPython(populate_decrypted_fields_blackboard, reverse_code=migrations.RunPython.noop),
]
68 changes: 68 additions & 0 deletions integrated_channels/blackboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from logging import getLogger

from config_models.models import ConfigurationModel
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 integrated_channels.blackboard.exporters.content_metadata import BlackboardContentMetadataExporter
from integrated_channels.blackboard.exporters.learner_data import BlackboardLearnerExporter
Expand Down Expand Up @@ -108,6 +110,39 @@ class BlackboardEnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfigur
)
)

decrypted_client_id = EncryptedCharField(
max_length=255,
blank=True,
default='',
verbose_name="API Client ID encrypted at db level",
help_text=(
"The API Client ID (encrypted at db level) provided to edX by the enterprise customer to be used"
" to make API calls to Degreed on behalf of the customer."
)
)

@property
def encrypted_client_id(self):
"""
Return encrypted client_id as a string.
The data is encrypted in the DB at rest, but is unencrypted in the app when retrieved through the
decrypted_client_id field. 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 encrypted client_id.
"""
self.decrypted_client_id = value

client_secret = models.CharField(
max_length=255,
blank=True,
Expand All @@ -119,6 +154,39 @@ class BlackboardEnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfigur
)
)

decrypted_client_secret = EncryptedCharField(
max_length=255,
blank=True,
default='',
verbose_name="API Client Secret encrypted at db level",
help_text=(
"The API Client Secret (encrypted at db level) provided to edX by the enterprise customer to be "
"used to make API calls to Degreed on behalf of the customer."
),
)

@property
def encrypted_client_secret(self):
"""
Return encrypted client_secret as a string.
The data is encrypted in the DB at rest, but is unencrypted in the app when retrieved through the
decrypted_client_secret field. 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 encrypted client_secret.
"""
self.decrypted_client_secret = value

blackboard_base_url = models.CharField(
max_length=255,
blank=True,
Expand Down
17 changes: 17 additions & 0 deletions integrated_channels/blackboard/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Utilities for Blackboard integrated channels.
"""


def populate_decrypted_fields_blackboard(apps, schema_editor=None): # pylint: disable=unused-argument
"""
Populates the encryption fields in Blackboard config with the data previously stored in database.
"""
BlackboardEnterpriseCustomerConfiguration = apps.get_model(
'blackboard', 'BlackboardEnterpriseCustomerConfiguration'
)

for blackboard_enterprise_configuration in BlackboardEnterpriseCustomerConfiguration.objects.all():
blackboard_enterprise_configuration.decrypted_client_id = blackboard_enterprise_configuration.client_id
blackboard_enterprise_configuration.decrypted_client_secret = blackboard_enterprise_configuration.client_secret
blackboard_enterprise_configuration.save()
2 changes: 2 additions & 0 deletions test_utils/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,8 @@ class Meta:
blackboard_base_url = factory.LazyAttribute(lambda x: FAKER.url())
client_id = factory.LazyAttribute(lambda x: FAKER.random_int(min=1))
client_secret = factory.LazyAttribute(lambda x: FAKER.uuid4())
decrypted_client_id = factory.LazyAttribute(lambda x: FAKER.random_int(min=1))
decrypted_client_secret = factory.LazyAttribute(lambda x: FAKER.uuid4())
refresh_token = factory.LazyAttribute(lambda x: FAKER.uuid4())


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import json
from unittest import mock

from django.apps import apps
from django.urls import reverse

from enterprise.constants import ENTERPRISE_ADMIN_ROLE
from enterprise.utils import localized_utcnow
from integrated_channels.blackboard.models import BlackboardEnterpriseCustomerConfiguration
from integrated_channels.blackboard.utils import populate_decrypted_fields_blackboard
from test_utils import FAKE_UUIDS, APITest, factories


Expand Down Expand Up @@ -129,16 +131,39 @@ def test_update(self, mock_current_request):
payload = {
'client_secret': 1000,
'client_id': 1001,
'encrypted_client_secret': 1000,
'encrypted_client_id': 1001,
'blackboard_base_url': 'http://testing2',
'enterprise_customer': FAKE_UUIDS[0],
}
response = self.client.put(url, payload)
self.enterprise_customer_conf.refresh_from_db()
self.assertEqual(self.enterprise_customer_conf.client_secret, '1000')
self.assertEqual(self.enterprise_customer_conf.client_id, '1001')
self.assertEqual(self.enterprise_customer_conf.decrypted_client_secret, '1000')
self.assertEqual(self.enterprise_customer_conf.decrypted_client_id, '1001')
self.assertEqual(self.enterprise_customer_conf.blackboard_base_url, 'http://testing2')
self.assertEqual(response.status_code, 200)

@mock.patch('enterprise.rules.crum.get_current_request')
def test_populate_decrypted_fields(self, mock_current_request):
mock_current_request.return_value = self.get_request_with_jwt_cookie(
system_wide_role=ENTERPRISE_ADMIN_ROLE,
context=self.enterprise_customer.uuid,
)
url = reverse('api:v1:blackboard:configuration-detail', args=[self.enterprise_customer_conf.id])
client_secret = self.enterprise_customer_conf.client_secret
payload = {
'encrypted_client_secret': '1000',
'enterprise_customer': FAKE_UUIDS[0],
}
self.client.put(url, payload)
self.enterprise_customer_conf.refresh_from_db()
self.assertEqual(self.enterprise_customer_conf.decrypted_client_secret, '1000')
populate_decrypted_fields_blackboard(apps)
self.enterprise_customer_conf.refresh_from_db()
self.assertEqual(self.enterprise_customer_conf.decrypted_client_secret, client_secret)

@mock.patch('enterprise.rules.crum.get_current_request')
def test_partial_update(self, mock_current_request):
mock_current_request.return_value = self.get_request_with_jwt_cookie(
Expand Down
Loading