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

Replacing encrypted fields with non encrypted degreed2 #1908

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions integrated_channels/api/v1/degreed2/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Serializer for Degreed2 configuration.
"""
from rest_framework import serializers
from integrated_channels.api.serializers import EnterpriseCustomerPluginConfigSerializer
from integrated_channels.degreed2.models import Degreed2EnterpriseCustomerConfiguration

Expand All @@ -9,9 +10,12 @@ class Degreed2ConfigSerializer(EnterpriseCustomerPluginConfigSerializer):
class Meta:
model = Degreed2EnterpriseCustomerConfiguration
extra_fields = (
'client_id',
'client_secret',
'encrypted_client_id',
'encrypted_client_secret',
'degreed_base_url',
'degreed_token_fetch_base_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)
26 changes: 26 additions & 0 deletions integrated_channels/degreed2/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from django_object_actions import DjangoObjectActions

from django import forms
from django.contrib import admin, messages
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
Expand All @@ -16,6 +17,19 @@
from integrated_channels.integrated_channel.admin import BaseLearnerDataTransmissionAuditAdmin


class Degreed2EnterpriseCustomerConfigurationForm(forms.ModelForm):
"""
Django admin form for MoodleEnterpriseCustomerConfiguration.
"""
class Meta:
model = Degreed2EnterpriseCustomerConfiguration
fields = '__all__'
widgets = {
'decrypted_client_id': forms.widgets.PasswordInput(),
'decrypted_client_secret': forms.widgets.PasswordInput(),
}


@admin.register(Degreed2EnterpriseCustomerConfiguration)
class Degreed2EnterpriseCustomerConfigurationAdmin(DjangoObjectActions, admin.ModelAdmin):
"""
Expand All @@ -41,10 +55,22 @@ class Degreed2EnterpriseCustomerConfigurationAdmin(DjangoObjectActions, admin.Mo

list_filter = ("active",)
search_fields = ("enterprise_customer_name",)
form = Degreed2EnterpriseCustomerConfigurationForm
change_actions = ("force_content_metadata_transmission",)

class Meta:
model = Degreed2EnterpriseCustomerConfiguration

def get_fields(self, request, obj=None):
"""
Return the fields that should be displayed on the admin form.
"""
fields = list(super().get_fields(request, obj))
if obj:
# Exclude password fields when we are editing an existing model.
return [f for f in fields if f not in {'decrypted_client_id', 'decrypted_client_secret'}]

return fields

def enterprise_customer_name(self, obj):
"""
Expand Down
4 changes: 2 additions & 2 deletions integrated_channels/degreed2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,8 @@ def _get_oauth_access_token(self, scope):
data={
'grant_type': 'client_credentials',
'scope': scope,
'client_id': config.client_id,
'client_secret': config.client_secret,
'client_id': config.decrypted_client_id,
'client_secret': config.decrypted_client_secret,
},
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
Expand Down
48 changes: 48 additions & 0 deletions integrated_channels/degreed2/migrations/0024_auto_20231011_0853.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 3.2.20 on 2023-10-11 08:53

from django.db import migrations
from integrated_channels.utils import dummy_reverse
import fernet_fields.fields


def populate_decrypted_fields(apps, schema_editor):
"""
Populates the encryption fields with the data previously stored in database.
"""
Degreed2EnterpriseCustomerConfiguration = apps.get_model('degreed2', 'Degreed2EnterpriseCustomerConfiguration')

for degreed2_enterprise_configuration in Degreed2EnterpriseCustomerConfiguration.objects.all():
degreed2_enterprise_configuration.decrypted_client_id = degreed2_enterprise_configuration.client_id
degreed2_enterprise_configuration.decrypted_client_secret = degreed2_enterprise_configuration.client_secret
degreed2_enterprise_configuration.save()


class Migration(migrations.Migration):

dependencies = [
('degreed2', '0023_alter_historicaldegreed2enterprisecustomerconfiguration_options'),
]

operations = [
migrations.AddField(
model_name='degreed2enterprisecustomerconfiguration',
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='degreed2enterprisecustomerconfiguration',
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'),
),
migrations.AddField(
model_name='historicaldegreed2enterprisecustomerconfiguration',
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='historicaldegreed2enterprisecustomerconfiguration',
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'),
),
migrations.RunPython(populate_decrypted_fields, dummy_reverse),
]
68 changes: 68 additions & 0 deletions integrated_channels/degreed2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import json
from logging import getLogger

from fernet_fields import EncryptedCharField
from simple_history.models import HistoricalRecords

from django.db import models
from django.utils.encoding import force_bytes, force_str

from integrated_channels.degreed2.exporters.content_metadata import Degreed2ContentMetadataExporter
from integrated_channels.degreed2.exporters.learner_data import Degreed2LearnerExporter
Expand Down Expand Up @@ -41,6 +43,39 @@ class Degreed2EnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfigurat
)
)

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 @@ -52,6 +87,39 @@ class Degreed2EnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfigurat
)
)

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

degreed_base_url = models.CharField(
max_length=255,
blank=True,
Expand Down
9 changes: 9 additions & 0 deletions integrated_channels/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,12 @@ def get_enterprise_client_by_channel_code(channel_code):
'canvas': CanvasAPIClient,
}
return _enterprise_client_model_by_channel_code[channel_code]


def dummy_reverse(_apps, _schema_editor):
"""
Reverse a data migration but do nothing.
:param _apps:
:param _schema_editor:
:return:
"""
4 changes: 2 additions & 2 deletions test_utils/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,8 +682,8 @@ class Meta:
active = True
degreed_base_url = factory.LazyAttribute(lambda x: FAKER.url())
degreed_token_fetch_base_url = factory.LazyAttribute(lambda x: FAKER.url())
client_id = factory.LazyAttribute(lambda x: FAKER.uuid4())
client_secret = factory.LazyAttribute(lambda x: FAKER.uuid4())
decrypted_client_id = factory.LazyAttribute(lambda x: FAKER.uuid4())
decrypted_client_secret = factory.LazyAttribute(lambda x: FAKER.uuid4())


class DegreedLearnerDataTransmissionAuditFactory(LearnerDataTransmissionAuditFactory):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ def test_get(self, mock_current_request):
response = self.client.get(url)
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(data.get('degreed_base_url'), self.degreed2_config.degreed_base_url)
self.assertEqual(data.get('client_id'), self.degreed2_config.client_id)
self.assertEqual(data.get('client_secret'), self.degreed2_config.client_secret)
self.assertEqual(data.get('decrypted_client_id'), self.degreed2_config.decrypted_client_id)
self.assertEqual(data.get('decrypted_client_secret'), self.degreed2_config.decrypted_client_secret)
self.assertEqual(data.get('degreed_token_fetch_base_url'), self.degreed2_config.degreed_token_fetch_base_url)
self.assertEqual(data.get('enterprise_customer'),
str(self.degreed2_config.enterprise_customer.uuid))
Expand All @@ -82,14 +82,14 @@ def test_update(self, mock_current_request):
'degreed_base_url': 'http://testing2',
'degreed_token_fetch_base_url': 'foobar',
'enterprise_customer': ENTERPRISE_ID,
'client_id': 'testing',
'client_secret': 'secret',
'encrypted_client_id': 'testing',
'encrypted_client_secret': 'secret',
}
response = self.client.put(url, payload)
self.degreed2_config.refresh_from_db()
self.assertEqual(self.degreed2_config.degreed_base_url, 'http://testing2')
self.assertEqual(self.degreed2_config.client_id, 'testing')
self.assertEqual(self.degreed2_config.client_secret, 'secret')
self.assertEqual(self.degreed2_config.decrypted_client_id, 'testing')
self.assertEqual(self.degreed2_config.decrypted_client_secret, 'secret')
self.assertEqual(self.degreed2_config.degreed_token_fetch_base_url, 'foobar')
self.assertEqual(str(self.degreed2_config.enterprise_customer.uuid), ENTERPRISE_ID)
self.assertEqual(response.status_code, 200)
Expand Down
Loading