Skip to content

Commit 1b1f380

Browse files
authored
Merge pull request #172 from open-craft/kshitij/bb-3899/pii-flag-cleanup
refactor: Rename CourseEditLTIFieldsEnabledFlag to CourseAllowPIISharingInLTIFlag and use it for LTI1.3 [BD-38] [BB-3899] [TNL-8104]
2 parents 5579620 + 6fb8679 commit 1b1f380

12 files changed

Lines changed: 112 additions & 83 deletions

File tree

README.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,21 @@ Please do not report security issues in public. Send security concerns via email
318318
Changelog
319319
=========
320320

321+
3.0.0 - 2021-06-16
322+
-------------------
323+
324+
* Rename `CourseEditLTIFieldsEnabledFlag` to `CourseAllowPIISharingInLTIFlag`
325+
to highlight its increased scope.
326+
* Use `CourseAllowPIISharingInLTIFlag` for LTI1.3 in lieu of the current
327+
`CourseWaffleFlag`.
328+
329+
321330
2.11.0 - 2021-06-10
322331
-------------------
323332

333+
* NOTE: This release requires a corresponding change in edx-platform that was
334+
implemented in https://github.com/edx/edx-platform/pull/27529
335+
As such, this release cannot be installed in releases before Maple.
324336
* Move ``CourseEditLTIFieldsEnabledFlag`` from ``edx-platform`` to this repo
325337
while retaining data from existing model.
326338

lti_consumer/admin.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from config_models.admin import KeyedConfigurationModelAdmin
55
from django.contrib import admin
66

7-
from lti_consumer.forms import CourseEditLTIFieldsEnabledAdminForm
7+
from lti_consumer.forms import CourseAllowPIISharingInLTIAdminForm
88
from lti_consumer.models import (
9-
CourseEditLTIFieldsEnabledFlag,
9+
CourseAllowPIISharingInLTIFlag,
1010
LtiAgsLineItem,
1111
LtiAgsScore,
1212
LtiConfiguration,
@@ -23,12 +23,12 @@ class LtiConfigurationAdmin(admin.ModelAdmin):
2323
readonly_fields = ('location', 'config_id')
2424

2525

26-
class CourseEditLTIFieldsEnabledFlagAdmin(KeyedConfigurationModelAdmin):
26+
class CourseAllowPIISharingInLTIFlagAdmin(KeyedConfigurationModelAdmin):
2727
"""
28-
Admin for LTI Fields Editing feature on course-by-course basis.
28+
Admin for enabling PII Sharing in LTI on course-by-course basis.
2929
Allows searching by course id.
3030
"""
31-
form = CourseEditLTIFieldsEnabledAdminForm
31+
form = CourseAllowPIISharingInLTIAdminForm
3232
search_fields = ['course_id']
3333
fieldsets = (
3434
(None, {
@@ -38,7 +38,7 @@ class CourseEditLTIFieldsEnabledFlagAdmin(KeyedConfigurationModelAdmin):
3838
)
3939

4040

41-
admin.site.register(CourseEditLTIFieldsEnabledFlag, CourseEditLTIFieldsEnabledFlagAdmin)
41+
admin.site.register(CourseAllowPIISharingInLTIFlag, CourseAllowPIISharingInLTIFlagAdmin)
4242
admin.site.register(LtiConfiguration, LtiConfigurationAdmin)
4343
admin.site.register(LtiAgsLineItem)
4444
admin.site.register(LtiAgsScore)

lti_consumer/api.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
import json
99

10+
from opaque_keys.edx.keys import CourseKey
11+
1012
from .exceptions import LtiError
11-
from .models import LtiConfiguration, LtiDlContentItem
13+
from .models import CourseAllowPIISharingInLTIFlag, LtiConfiguration, LtiDlContentItem
1214
from .utils import (
1315
get_lti_deeplinking_content_url,
1416
get_lms_lti_keyset_link,
@@ -189,7 +191,20 @@ def get_deep_linking_data(deep_linking_id, config_id=None, block=None):
189191
# Retrieve LTI Configuration
190192
lti_config = _get_lti_config(config_id, block)
191193
# Only filter DL content item from content item set in the same LTI configuration.
192-
# This avoid a malicious user to input a random LTI id and perform LTI DL
193-
# content launches outsite the scope of it's configuration.
194+
# This avoids a malicious user to input a random LTI id and perform LTI DL
195+
# content launches outside the scope of its configuration.
194196
content_item = lti_config.ltidlcontentitem_set.get(pk=deep_linking_id)
195197
return content_item.attributes
198+
199+
200+
def get_lti_pii_sharing_state_for_course(course_key: CourseKey) -> bool:
201+
"""
202+
Returns the status of PII sharing for the provided course.
203+
204+
Args:
205+
course_key (CourseKey): Course key for the course to check for PII sharing
206+
207+
Returns:
208+
bool: The state of PII sharing for this course for LTI.
209+
"""
210+
return CourseAllowPIISharingInLTIFlag.current(course_key).enabled

lti_consumer/forms.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@
77

88
from django import forms
99

10-
from lti_consumer.models import CourseEditLTIFieldsEnabledFlag
10+
from lti_consumer.models import CourseAllowPIISharingInLTIFlag
1111
from lti_consumer.plugin.compat import clean_course_id
1212

1313
log = logging.getLogger(__name__)
1414

1515

16-
class CourseEditLTIFieldsEnabledAdminForm(forms.ModelForm):
16+
class CourseAllowPIISharingInLTIAdminForm(forms.ModelForm):
1717
"""
1818
Form for LTI consumer course-specific configuration to verify the course id.
1919
"""
2020

2121
class Meta:
22-
model = CourseEditLTIFieldsEnabledFlag
22+
model = CourseAllowPIISharingInLTIFlag
2323
fields = '__all__'
2424

2525
def clean_course_id(self):
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 2.2.24 on 2021-06-16 12:26
2+
3+
from django.conf import settings
4+
from django.db import migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11+
('lti_consumer', '0011_courseeditltifieldsenabledflag'),
12+
]
13+
14+
operations = [
15+
migrations.RenameModel(
16+
old_name='CourseEditLTIFieldsEnabledFlag',
17+
new_name='CourseAllowPIISharingInLTIFlag',
18+
),
19+
]

lti_consumer/models.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from jsonfield import JSONField
1212
from Cryptodome.PublicKey import RSA
1313
from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
14+
from opaque_keys.edx.keys import CourseKey
1415
from config_models.models import ConfigurationModel
1516

1617
# LTI 1.1
@@ -329,6 +330,22 @@ def get_lti_consumer(self):
329330

330331
return self._get_lti_1p1_consumer()
331332

333+
@property
334+
def pii_share_username(self):
335+
return self.lti_config.get('pii_share_username', False) # pylint: disable=no-member
336+
337+
@pii_share_username.setter
338+
def pii_share_username(self, value):
339+
self.lti_config['pii_share_username'] = value # pylint: disable=unsupported-assignment-operation
340+
341+
@property
342+
def pii_share_email(self):
343+
return self.lti_config.get('pii_share_email', False) # pylint: disable=no-member
344+
345+
@pii_share_email.setter
346+
def pii_share_email(self, value):
347+
self.lti_config['pii_share_email'] = value # pylint: disable=unsupported-assignment-operation
348+
332349
def __str__(self):
333350
return f"[{self.config_store}] {self.version} - {self.location}"
334351

@@ -535,10 +552,14 @@ class Meta:
535552
app_label = 'lti_consumer'
536553

537554

538-
class CourseEditLTIFieldsEnabledFlag(ConfigurationModel):
555+
class CourseAllowPIISharingInLTIFlag(ConfigurationModel):
539556
"""
540-
Enables the editing of "request username" and "request email" fields
541-
of LTI consumer for a specific course.
557+
Enables the sharing of PII via LTI for the specific course.
558+
559+
Depending on where it's used, further action might be needed to actually
560+
enable sharing PII. For instance, in the LTI XBlock setting this flag
561+
will allow editing the "request username" and "request email" fields, which
562+
will also need to be set to actually share PII.
542563
543564
.. no_pii:
544565
"""
@@ -548,7 +569,7 @@ class CourseEditLTIFieldsEnabledFlag(ConfigurationModel):
548569

549570
@classmethod
550571
@request_cached
551-
def lti_access_to_learners_editable(cls, course_id, is_already_sharing_learner_info):
572+
def lti_access_to_learners_editable(cls, course_id: CourseKey, is_already_sharing_learner_info: bool) -> bool:
552573
"""
553574
Looks at the currently active configuration model to determine whether
554575
the feature that enables editing of "request username" and "request email"
@@ -563,13 +584,13 @@ def lti_access_to_learners_editable(cls, course_id, is_already_sharing_learner_i
563584
is_already_sharing_learner_info (bool): indicates whether LTI consumer is
564585
already sharing edX learner username/email.
565586
"""
566-
course_specific_config = (CourseEditLTIFieldsEnabledFlag.objects
587+
course_specific_config = (CourseAllowPIISharingInLTIFlag.objects
567588
.filter(course_id=course_id)
568589
.order_by('-change_date')
569590
.first())
570591

571592
if is_already_sharing_learner_info and not course_specific_config:
572-
CourseEditLTIFieldsEnabledFlag.objects.create(course_id=course_id, enabled=True)
593+
CourseAllowPIISharingInLTIFlag.objects.create(course_id=course_id, enabled=True)
573594
return True
574595

575596
return course_specific_config.enabled if course_specific_config else False

lti_consumer/plugin/compat.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,6 @@
1414
log = logging.getLogger(__name__)
1515

1616

17-
# Waffle flags configuration
18-
19-
# Namespace
20-
WAFFLE_NAMESPACE = 'lti_consumer'
21-
22-
# Course Waffle Flags
23-
# .. toggle_name: lti_consumer.lti_nrps_transmit_pii
24-
# .. toggle_implementation: CourseWaffleFlag
25-
# .. toggle_default: False
26-
# .. toggle_description: When enabled, the LTI NRPS endpoint will include learner PII
27-
# in the response (username, email, etc).
28-
# .. toggle_use_cases: open_edx
29-
# .. toggle_creation_date: 2021-06-03
30-
# .. toggle_tickets: TNL-7974, https://github.com/edx/xblock-lti-consumer/pull/124
31-
# .. toggle_warnings: None.
32-
LTI_NRPS_TRANSMIT_PII = 'lti_nrps_transmit_pii'
33-
34-
3517
def run_xblock_handler(*args, **kwargs):
3618
"""
3719
Import and run `handle_xblock_callback` from LMS
@@ -187,15 +169,6 @@ def get_course_members(course_key):
187169
raise LtiError('NRPS is not available for {}'.format(course_key)) from ex
188170

189171

190-
def get_lti_pii_course_waffle_flag():
191-
"""
192-
Returns Course Waffle Flag Override for PII information exposure.
193-
"""
194-
# pylint: disable=import-error,import-outside-toplevel
195-
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
196-
return CourseWaffleFlag(WAFFLE_NAMESPACE, LTI_NRPS_TRANSMIT_PII, __name__)
197-
198-
199172
def request_cached(func) -> Callable[[Callable], Callable]:
200173
"""
201174
Import the `request_cached` decorator from LMS and apply it if available.

lti_consumer/plugin/views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from rest_framework.response import Response
2020
from rest_framework.status import HTTP_403_FORBIDDEN
2121

22+
from lti_consumer.api import get_lti_pii_sharing_state_for_course
2223
from lti_consumer.exceptions import LtiError
2324
from lti_consumer.models import (
2425
LtiConfiguration,
@@ -56,7 +57,7 @@
5657
)
5758
from lti_consumer.lti_1p3.extensions.rest_framework.utils import IgnoreContentNegotiation
5859
from lti_consumer.plugin import compat
59-
from lti_consumer.utils import _, expose_pii_fields
60+
from lti_consumer.utils import _
6061

6162

6263
log = logging.getLogger(__name__)
@@ -464,7 +465,7 @@ def get_serializer_class(self):
464465
Overrides ModelViewSet's `get_serializer_class` method.
465466
Checks if PII fields can be exposed and returns appropiate serializer.
466467
"""
467-
if expose_pii_fields(self.request.lti_configuration.location.course_key):
468+
if get_lti_pii_sharing_state_for_course(self.request.lti_configuration.location.course_key):
468469
return LtiNrpsContextMembershipPIISerializer
469470
else:
470471
return LtiNrpsContextMembershipBasicSerializer

lti_consumer/tests/unit/plugin/test_views_lti_nrps.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Tests for LTI Names and Role Provisioning Service views.
33
"""
4-
from mock import patch, PropertyMock
4+
from mock import Mock, patch, PropertyMock
55
from Cryptodome.PublicKey import RSA
66
from jwkest.jwk import RSAKey
77
from rest_framework.test import APITransactionTestCase
@@ -226,12 +226,12 @@ def test_token_with_incorrect_scope(self):
226226
response = self.client.get(self.context_membership_endpoint)
227227
self.assertEqual(response.status_code, 403)
228228

229-
@patch('lti_consumer.plugin.views.expose_pii_fields', return_value=False)
229+
@patch('lti_consumer.plugin.views.get_lti_pii_sharing_state_for_course', Mock(return_value=False))
230230
@patch(
231231
'lti_consumer.plugin.views.compat.get_course_members',
232-
side_effect=patch_get_memberships()
232+
Mock(side_effect=patch_get_memberships()),
233233
)
234-
def test_token_with_correct_scope(self, get_course_members_patcher, expose_pii_fields_patcher): # pylint: disable=unused-argument
234+
def test_token_with_correct_scope(self):
235235
"""
236236
Test if context membership returns correct response when token has correct scope
237237
"""
@@ -240,14 +240,14 @@ def test_token_with_correct_scope(self, get_course_members_patcher, expose_pii_f
240240
self.assertEqual(response.status_code, 200)
241241
self.assertEqual(response['content-type'], 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json')
242242

243-
@patch('lti_consumer.plugin.views.expose_pii_fields', return_value=False)
243+
@patch('lti_consumer.plugin.views.get_lti_pii_sharing_state_for_course', return_value=False)
244244
@patch(
245245
'lti_consumer.plugin.views.compat.get_course_members',
246-
side_effect=patch_get_memberships({
246+
Mock(side_effect=patch_get_memberships({
247247
'student': 4
248-
})
248+
})),
249249
)
250-
def test_get_without_pii(self, get_course_members_patcher, expose_pii_fields_patcher): # pylint: disable=unused-argument
250+
def test_get_without_pii(self, expose_pii_fields_patcher):
251251
"""
252252
Test context membership endpoint response structure with PII not exposed.
253253
"""
@@ -267,14 +267,14 @@ def test_get_without_pii(self, get_course_members_patcher, expose_pii_fields_pat
267267
self.assertNotIn('email', member_fields)
268268
self.assertNotIn('name', member_fields)
269269

270-
@patch('lti_consumer.plugin.views.expose_pii_fields', return_value=True)
270+
@patch('lti_consumer.plugin.views.get_lti_pii_sharing_state_for_course', return_value=True)
271271
@patch(
272272
'lti_consumer.plugin.views.compat.get_course_members',
273-
side_effect=patch_get_memberships({
273+
Mock(side_effect=patch_get_memberships({
274274
'student': 4
275-
})
275+
})),
276276
)
277-
def test_get_with_pii(self, get_course_members_patcher, expose_pii_fields_patcher): # pylint: disable=unused-argument
277+
def test_get_with_pii(self, expose_pii_fields_patcher):
278278
"""
279279
Test context membership endpoint response structure with PII exposed.
280280
"""
@@ -295,14 +295,14 @@ def test_get_with_pii(self, get_course_members_patcher, expose_pii_fields_patche
295295
self.assertIn('email', member_fields)
296296
self.assertIn('name', member_fields)
297297

298-
@patch('lti_consumer.plugin.views.expose_pii_fields', return_value=False)
298+
@patch('lti_consumer.plugin.views.get_lti_pii_sharing_state_for_course', Mock(return_value=False))
299299
@patch(
300300
'lti_consumer.plugin.views.compat.get_course_members',
301-
side_effect=patch_get_memberships({
301+
Mock(side_effect=patch_get_memberships({
302302
'exception': True
303-
})
303+
})),
304304
)
305-
def test_enrollment_limit_gate(self, get_course_members_patcher, expose_pii_fields_patcher): # pylint: disable=unused-argument
305+
def test_enrollment_limit_gate(self):
306306
"""
307307
Test if number of enrolled user is larger than the limit, api returns 404 response.
308308
"""

0 commit comments

Comments
 (0)