Skip to content

Commit

Permalink
Data migration for encrypted credentials column in sap configuration (#…
Browse files Browse the repository at this point in the history
…2215)

* feat: added data migration to populate encrypted columns

* feat: replaced references from unencrypted to encrypted columns
  • Loading branch information
MueezKhan246 authored Sep 9, 2024
1 parent cba67ec commit b60023c
Show file tree
Hide file tree
Showing 19 changed files with 157 additions and 61 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Unreleased
----------
* nothing unreleased

[4.25.4]
----------
* feat: replaced references from unencrypted to encrypted columns.
* feat: added data migration to populate encrypted columns.

[4.25.3]
----------
* feat: added encrypted client secret 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.25.3"
__version__ = "4.25.4"
11 changes: 0 additions & 11 deletions enterprise/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,14 +464,3 @@ 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/sap_success_factors/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Serializer for Success Factors configuration.
"""
from rest_framework import serializers

from integrated_channels.api.serializers import EnterpriseCustomerPluginConfigSerializer
from integrated_channels.sap_success_factors.models import SAPSuccessFactorsEnterpriseCustomerConfiguration

Expand All @@ -9,15 +11,18 @@ class SAPSuccessFactorsConfigSerializer(EnterpriseCustomerPluginConfigSerializer
class Meta:
model = SAPSuccessFactorsEnterpriseCustomerConfiguration
extra_fields = (
'key',
'encrypted_key',
'sapsf_base_url',
'sapsf_company_id',
'sapsf_user_id',
'secret',
'encrypted_secret',
'user_type',
'additional_locales',
'show_course_price',
'transmit_total_hours',
'prevent_self_submit_grades',
)
fields = EnterpriseCustomerPluginConfigSerializer.Meta.fields + extra_fields

encrypted_key = serializers.CharField(required=False, allow_blank=False, read_only=False)
encrypted_secret = serializers.CharField(required=False, allow_blank=False, read_only=False)
6 changes: 2 additions & 4 deletions integrated_channels/sap_success_factors/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ class SAPSuccessFactorsEnterpriseCustomerConfigurationAdmin(DjangoObjectActions,
"active",
"sapsf_base_url",
"sapsf_company_id",
"key",
"decrypted_key",
"secret",
"decrypted_secret",
"sapsf_user_id",
"user_type",
Expand Down Expand Up @@ -111,8 +109,8 @@ def has_access_token(self, obj):
try:
access_token, expires_at = SAPSuccessFactorsAPIClient.get_oauth_access_token(
obj.sapsf_base_url,
obj.key,
obj.secret,
obj.decrypted_key,
obj.decrypted_secret,
obj.sapsf_company_id,
obj.sapsf_user_id,
obj.user_type,
Expand Down
8 changes: 4 additions & 4 deletions integrated_channels/sap_success_factors/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ def _create_session(self):
self.session.close()

oauth_access_token, expires_at = self.get_oauth_access_token(
self.enterprise_configuration.key,
self.enterprise_configuration.secret,
self.enterprise_configuration.decrypted_key,
self.enterprise_configuration.decrypted_secret,
self.enterprise_configuration.sapsf_company_id,
self.enterprise_configuration.sapsf_user_id,
self.enterprise_configuration.user_type,
Expand Down Expand Up @@ -301,8 +301,8 @@ def _call_post_with_user_override(self, sap_user_id, url, payload):
'SAPSuccessFactorsEnterpriseCustomerConfiguration'
)
oauth_access_token, _ = self.get_oauth_access_token(
self.enterprise_configuration.key,
self.enterprise_configuration.secret,
self.enterprise_configuration.decrypted_key,
self.enterprise_configuration.decrypted_secret,
self.enterprise_configuration.sapsf_company_id,
sap_user_id,
SAPSuccessFactorsEnterpriseCustomerConfiguration.USER_TYPE_USER,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Generated by Django 3.2.23 on 2024-08-26 09:54

from django.db import migrations
from integrated_channels.sap_success_factors.utils import populate_decrypted_fields_sap_success_factors


class Migration(migrations.Migration):

dependencies = [
('sap_success_factors', '0021_sapsuccessfactorsenterprisecustomerconfiguration_decrypted_secret'),
]

operations = [
migrations.RunPython(populate_decrypted_fields_sap_success_factors, reverse_code=migrations.RunPython.noop),
]
50 changes: 47 additions & 3 deletions integrated_channels/sap_success_factors/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from fernet_fields import EncryptedCharField

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.exceptions import ClientError
Expand Down Expand Up @@ -73,7 +74,6 @@ class SAPSuccessFactorsEnterpriseCustomerConfiguration(EnterpriseCustomerPluginC
(USER_TYPE_USER, 'User'),
(USER_TYPE_ADMIN, 'Admin'),
)

key = models.CharField(
max_length=255,
blank=True,
Expand All @@ -94,6 +94,28 @@ class SAPSuccessFactorsEnterpriseCustomerConfiguration(EnterpriseCustomerPluginC
null=True
)

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

@encrypted_key.setter
def encrypted_key(self, value):
"""
Set the encrypted key.
"""
self.decrypted_key = value

sapsf_base_url = models.CharField(
max_length=255,
blank=True,
Expand Down Expand Up @@ -135,6 +157,28 @@ class SAPSuccessFactorsEnterpriseCustomerConfiguration(EnterpriseCustomerPluginC
null=True
)

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

@encrypted_secret.setter
def encrypted_secret(self, value):
"""
Set the encrypted secret.
"""
self.decrypted_secret = value

user_type = models.CharField(
max_length=20,
choices=USER_TYPE_CHOICES,
Expand Down Expand Up @@ -205,15 +249,15 @@ def is_valid(self):
"""
missing_items = {'missing': []}
incorrect_items = {'incorrect': []}
if not self.key:
if not self.decrypted_key:
missing_items.get('missing').append('key')
if not self.sapsf_base_url:
missing_items.get('missing').append('sapsf_base_url')
if not self.sapsf_company_id:
missing_items.get('missing').append('sapsf_company_id')
if not self.sapsf_user_id:
missing_items.get('missing').append('sapsf_user_id')
if not self.secret:
if not self.decrypted_secret:
missing_items.get('missing').append('secret')
if not is_valid_url(self.sapsf_base_url):
incorrect_items.get('incorrect').append('sapsf_base_url')
Expand Down
19 changes: 19 additions & 0 deletions integrated_channels/sap_success_factors/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Utilities for SAP Success Factors integrated channel.
"""


def populate_decrypted_fields_sap_success_factors(apps, schema_editor=None): # pylint: disable=unused-argument
"""
Populates the encryption fields in SAP Success Factors config with the data previously stored in database.
"""
SAPSuccessFactorsEnterpriseCustomerConfiguration = apps.get_model(
'sap_success_factors', 'SAPSuccessFactorsEnterpriseCustomerConfiguration'
)

for sap_success_factors_enterprise_configuration in SAPSuccessFactorsEnterpriseCustomerConfiguration.objects.all():
sap_success_factors_enterprise_configuration.decrypted_key = getattr(
sap_success_factors_enterprise_configuration, 'key', '')
sap_success_factors_enterprise_configuration.decrypted_secret = getattr(
sap_success_factors_enterprise_configuration, 'secret', '')
sap_success_factors_enterprise_configuration.save()
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
from unittest import mock
from uuid import uuid4

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.sap_success_factors.models import SAPSuccessFactorsEnterpriseCustomerConfiguration
from integrated_channels.sap_success_factors.utils import populate_decrypted_fields_sap_success_factors
from test_utils import APITest, factories

ENTERPRISE_ID = str(uuid4())
Expand Down Expand Up @@ -70,10 +72,6 @@ def test_get(self, mock_current_request):
self.sap_config.sapsf_company_id)
self.assertEqual(int(data.get('sapsf_user_id')),
self.sap_config.sapsf_user_id)
self.assertEqual(data.get('key'),
self.sap_config.key)
self.assertEqual(data.get('secret'),
self.sap_config.secret)
self.assertEqual(data.get('user_type'),
self.sap_config.user_type)
self.assertEqual(data.get('enterprise_customer'),
Expand All @@ -91,20 +89,43 @@ def test_update(self, mock_current_request):
'sapsf_company_id': 'test',
'enterprise_customer': ENTERPRISE_ID,
'sapsf_user_id': 893489,
'key': 'testing',
'secret': 'secret',
'encrypted_key': '',
'encrypted_secret': '',
'user_type': 'user',
}
response = self.client.put(url, payload)
self.sap_config.refresh_from_db()
self.assertEqual(self.sap_config.sapsf_base_url, 'http://testing2')
self.assertEqual(self.sap_config.sapsf_company_id, 'test')
self.assertEqual(self.sap_config.key, 'testing')
self.assertEqual(self.sap_config.secret, 'secret')
self.assertEqual(self.sap_config.decrypted_key, '')
self.assertEqual(self.sap_config.decrypted_secret, '')
self.assertEqual(self.sap_config.user_type, 'user')
self.assertEqual(self.sap_config.sapsf_user_id, '893489')
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:sap_success_factors:configuration-detail', args=[self.sap_config.id])
client_secret = getattr(self.sap_config, 'secret', '')
payload = {
'sapsf_base_url': 'http://testing2',
'sapsf_company_id': 'test',
'enterprise_customer': ENTERPRISE_ID,
'sapsf_user_id': 893489,
'user_type': 'user',
'encrypted_secret': '1000',
}
self.client.put(url, payload)
self.sap_config.refresh_from_db()
self.assertEqual(self.sap_config.decrypted_secret, '1000')
populate_decrypted_fields_sap_success_factors(apps)
self.sap_config.refresh_from_db()
self.assertEqual(self.sap_config.encrypted_secret, client_secret)

@mock.patch('enterprise.rules.crum.get_current_request')
def test_patch(self, mock_current_request):
mock_current_request.return_value = self.get_request_with_jwt_cookie(
Expand Down Expand Up @@ -164,8 +185,8 @@ def test_is_valid_field(self, mock_current_request):
missing, _ = data[0].get('is_valid')
assert missing.get('missing') == ['key', 'sapsf_base_url', 'sapsf_company_id', 'sapsf_user_id', 'secret']

self.sap_config.key = 'ayy'
self.sap_config.secret = 'lmao'
self.sap_config.decrypted_key = 'ayy'
self.sap_config.decrypted_secret = 'lmao'
self.sap_config.sapsf_company_id = '1'
self.sap_config.sapsf_user_id = '1'
self.sap_config.sapsf_base_url = 'http://happy.com'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,8 @@ def test_learner_data_instructor_paced_no_certificate_null_sso_id(
self.config = factories.SAPSuccessFactorsEnterpriseCustomerConfigurationFactory(
enterprise_customer=self.enterprise_customer,
sapsf_base_url='enterprise.successfactors.com',
key='key',
secret='secret',
decrypted_key='key',
decrypted_secret='secret',
active=True,
)
self.exporter = self.config.get_learner_data_exporter('dummy-user')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ def setUp(self):
# so it's okay for it to be any arbitrary channel. We randomly choose SAPSF.
self.enterprise_config = factories.SAPSuccessFactorsEnterpriseCustomerConfigurationFactory(
enterprise_customer=self.enterprise_customer,
key="client_id",
decrypted_key="client_id",
sapsf_base_url="http://test.successfactors.com/",
sapsf_company_id="company_id",
sapsf_user_id="user_id",
secret="client_secret",
decrypted_secret="client_secret",
transmission_chunk_size=5,
)
self.enterprise_catalog = factories.EnterpriseCustomerCatalogFactory(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ def setUp(self):
# so it's okay for it to be any arbitrary channel. We randomly choose SAPSF.
self.enterprise_config = factories.SAPSuccessFactorsEnterpriseCustomerConfigurationFactory(
enterprise_customer=enterprise_customer,
key="client_id",
decrypted_key="client_id",
sapsf_base_url="http://test.successfactors.com/",
sapsf_company_id="company_id",
sapsf_user_id="user_id",
secret="client_secret",
decrypted_secret="client_secret",
)

self.learner_transmitter = LearnerTransmitter(self.enterprise_config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ def setUp(self):

self.expected_token_response_body = {"expires_in": self.expires_in, "access_token": self.access_token}
self.enterprise_config = SAPSuccessFactorsEnterpriseCustomerConfiguration(
key=self.client_id,
encrypted_key=self.client_id,
sapsf_base_url=self.url_base,
sapsf_company_id=self.company_id,
sapsf_user_id=self.user_id,
secret=self.client_secret
encrypted_secret=self.client_secret
)
self.enterprise_config.enterprise_customer = EnterpriseCustomerFactory()
self.completion_payload = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ def setUp(self):
)
self.enterprise_config = SAPSuccessFactorsEnterpriseCustomerConfigurationFactory(
enterprise_customer=self.enterprise_customer,
key="client_id",
decrypted_key="client_id",
sapsf_base_url="http://test.successfactors.com/",
sapsf_company_id="company_id",
sapsf_user_id="user_id",
secret="client_secret"
decrypted_secret="client_secret"
)

def test_unique_enrollment_id_course_id_constraint(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ def setUp(self):
)
self.enterprise_config = factories.SAPSuccessFactorsEnterpriseCustomerConfigurationFactory(
enterprise_customer=enterprise_customer,
key='client_id',
decrypted_key='client_id',
sapsf_base_url=self.url_base,
sapsf_company_id='company_id',
sapsf_user_id='user_id',
secret='client_secret',
decrypted_secret='client_secret',
)
factories.SAPSuccessFactorsGlobalConfiguration.objects.create(
completion_status_api_path=self.completion_status_api_path,
Expand Down
Loading

0 comments on commit b60023c

Please sign in to comment.