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

Encryption fields added to moodle config #1889

Merged
merged 17 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
48a4adf
feat: added fields for holding encrypted data in database (ENT 5613)
MueezKhan246 Oct 3, 2023
6a8a7b7
refactor: removing "null=true" constraint from encrypted fields
MueezKhan246 Oct 10, 2023
2c3b179
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
MueezKhan246 Oct 11, 2023
aa27513
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
MueezKhan246 Oct 25, 2023
405ee57
refactor: changing version for pull request merge
MueezKhan246 Oct 25, 2023
9bf58d4
fix: used null=True to avoid decryption of empty values
MueezKhan246 Nov 6, 2023
a184835
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
MueezKhan246 Nov 6, 2023
af0866c
refactor: removed unused import
MueezKhan246 Nov 8, 2023
618e72a
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
MueezKhan246 Nov 8, 2023
253cbfd
refactor: D000 Title underline too short
MueezKhan246 Nov 8, 2023
fd0e6ce
refactor: added more meaningful help text for encryption fields
MueezKhan246 Nov 14, 2023
5d6ecfb
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
MueezKhan246 Nov 14, 2023
61c1f39
fix: added missing updated migration
MueezKhan246 Nov 14, 2023
59cb36a
fix: corrected change log file
MueezKhan246 Nov 15, 2023
abe2b2e
Merge branch 'master' into MueezKhan/Encryption-Fields-Added-To-Moodl…
MueezKhan246 Nov 20, 2023
38433ea
Merge branch 'master' into MueezKhan/Encryption-Fields-Added-To-Moodl…
MueezKhan246 Nov 20, 2023
3ac6fb8
Merge branch 'master' into MueezKhan/Encryption-Fields-Added-To-Moodl…
MueezKhan246 Nov 21, 2023
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
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Change Log

Unreleased
----------
[4.6.9]
-------
refactor: removing "null=true" constraint from encrypted fields
MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved
feat: added fields for holding encrypted data in database (ENT 5613)

[4.6.8]
-------
feat: truncate API Response before writing to the APIResponseRecord
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.6.8"
__version__ = "4.6.9"
59 changes: 59 additions & 0 deletions integrated_channels/moodle/migrations/0028_auto_20230928_1530.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 3.2.20 on 2023-09-28 15:30
MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved

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.
"""
MoodleEnterpriseCustomerConfiguration = apps.get_model('moodle', 'MoodleEnterpriseCustomerConfiguration')

for moodle_enterprise_configuration in MoodleEnterpriseCustomerConfiguration.objects.all():
moodle_enterprise_configuration.decrypted_username = moodle_enterprise_configuration.username
moodle_enterprise_configuration.decrypted_password = moodle_enterprise_configuration.password
moodle_enterprise_configuration.decrypted_token = moodle_enterprise_configuration.token
sameenfatima78 marked this conversation as resolved.
Show resolved Hide resolved
moodle_enterprise_configuration.save()
MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved


class Migration(migrations.Migration):

dependencies = [
('moodle', '0027_alter_historicalmoodleenterprisecustomerconfiguration_options'),
]

operations = [
migrations.AddField(
model_name='historicalmoodleenterprisecustomerconfiguration',
name='decrypted_password',
field=fernet_fields.fields.EncryptedCharField(blank=True, help_text="The encrypted API user's password used to obtain new tokens.", max_length=255, default="", verbose_name='Encrypted Webservice Password'),
),
migrations.AddField(
model_name='historicalmoodleenterprisecustomerconfiguration',
name='decrypted_token',
field=fernet_fields.fields.EncryptedCharField(blank=True, help_text="The encrypted API user's token used to obtain new tokens.", max_length=255, default="", verbose_name='Encrypted Webservice Token'),
),
migrations.AddField(
model_name='historicalmoodleenterprisecustomerconfiguration',
name='decrypted_username',
field=fernet_fields.fields.EncryptedCharField(blank=True, help_text="The encrypted API user's username used to obtain new tokens.", max_length=255, default="", verbose_name='Encrypted Webservice Username'),
),
migrations.AddField(
model_name='moodleenterprisecustomerconfiguration',
name='decrypted_password',
field=fernet_fields.fields.EncryptedCharField(blank=True, help_text="The encrypted API user's password used to obtain new tokens.", max_length=255, default="", verbose_name='Encrypted Webservice Password'),
),
migrations.AddField(
model_name='moodleenterprisecustomerconfiguration',
name='decrypted_token',
field=fernet_fields.fields.EncryptedCharField(blank=True, help_text="The encrypted API user's token used to obtain new tokens.", max_length=255, default="", verbose_name='Encrypted Webservice Token'),
),
migrations.AddField(
model_name='moodleenterprisecustomerconfiguration',
name='decrypted_username',
field=fernet_fields.fields.EncryptedCharField(blank=True, help_text="The encrypted API user's username used to obtain new tokens.", max_length=255, default="", verbose_name='Encrypted Webservice Username'),
),
migrations.RunPython(populate_decrypted_fields, dummy_reverse),
]
98 changes: 98 additions & 0 deletions integrated_channels/moodle/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,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 django.utils.translation import gettext_lazy as _

from integrated_channels.integrated_channel.models import (
Expand Down Expand Up @@ -64,6 +66,38 @@ class MoodleEnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfiguratio
)
)

decrypted_username = EncryptedCharField(
max_length=255,
verbose_name="Encrypted Webservice Username",
blank=True,
help_text=_(
"The encrypted API user's username used to obtain new tokens."),
default="",
)
MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved

@property
def encrypted_username(self):
"""
Return encrypted username as a string.

The data is encrypted in the DB at rest, but is unencrypted in the app when retrieved through the
decrypted_username field. This method will encrypt the username again before sending.
"""
if self.decrypted_username:
return force_str(
self._meta.get_field('decrypted_username').fernet.encrypt(
force_bytes(self.decrypted_username)
)
)
return self.decrypted_username

@encrypted_username.setter
def encrypted_username(self, value):
"""
Set the encrypted username.
"""
self.decrypted_username = value

MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved
password = models.CharField(
max_length=255,
blank=True,
Expand All @@ -73,6 +107,38 @@ class MoodleEnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfiguratio
)
)

decrypted_password = EncryptedCharField(
max_length=255,
verbose_name="Encrypted Webservice Password",
blank=True,
help_text=_(
"The encrypted API user's password used to obtain new tokens."),
default="",
)
MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved

@property
def encrypted_password(self):
"""
Return encrypted password as a string.

The data is encrypted in the DB at rest, but is unencrypted in the app when retrieved through the
decrypted_password field. This method will encrypt the password again before sending.
"""
if self.decrypted_password:
return force_str(
self._meta.get_field('decrypted_password').fernet.encrypt(
force_bytes(self.decrypted_password)
)
)
return self.decrypted_password

@encrypted_password.setter
def encrypted_password(self, value):
"""
Set the encrypted password.
"""
self.decrypted_password = value

MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved
token = models.CharField(
max_length=255,
blank=True,
Expand All @@ -82,6 +148,38 @@ class MoodleEnterpriseCustomerConfiguration(EnterpriseCustomerPluginConfiguratio
)
)

decrypted_token = EncryptedCharField(
max_length=255,
verbose_name="Encrypted Webservice Token",
blank=True,
help_text=_(
"The encrypted API user's token used to obtain new tokens."),
default="",
)
MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved

@property
def encrypted_token(self):
"""
Return encrypted token as a string.

The data is encrypted in the DB at rest, but is unencrypted in the app when retrieved through the
decrypted_token field. This method will encrypt the token again before sending.
"""
if self.decrypted_token:
return force_str(
self._meta.get_field('decrypted_token').fernet.encrypt(
force_bytes(self.decrypted_token)
)
)
return self.decrypted_token

@encrypted_token.setter
def encrypted_token(self, value):
"""
Set the encrypted token.
"""
self.decrypted_token = value

MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved
transmission_chunk_size = models.IntegerField(
default=1,
help_text=_("The maximum number of data items to transmit to the integrated channel with each request.")
Expand Down
10 changes: 10 additions & 0 deletions integrated_channels/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,3 +501,13 @@ 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:
MueezKhan246 marked this conversation as resolved.
Show resolved Hide resolved
"""
Loading