Commit 589d66e2 by Mushtaq Ali

Encrypt credentials - EDUCATOR-1392

parent ed7896fb
PEP8_THRESHOLD=50 PEP8_THRESHOLD=49
PYLINT_THRESHOLD=855 PYLINT_THRESHOLD=761
production-requirements: production-requirements:
pip install -r requirements.txt pip install -r requirements.txt
......
...@@ -19,6 +19,9 @@ MANAGERS = ADMINS ...@@ -19,6 +19,9 @@ MANAGERS = ADMINS
# Make this unique, and don't share it with anybody. # Make this unique, and don't share it with anybody.
SECRET_KEY = 'insecure-secret-key' SECRET_KEY = 'insecure-secret-key'
# Set this value in the environment-specific files (e.g. local.py, production.py, test.py)
FERNET_KEYS = ['insecure-ferent-key']
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
......
"""
Local environment settings.
"""
from VEDA.settings.base import * from VEDA.settings.base import *
from VEDA.settings.utils import get_logger_config from VEDA.settings.utils import get_logger_config
......
import yaml """
from os import environ Production environment settings.
"""
from VEDA.settings.base import * from VEDA.settings.base import *
from VEDA.utils import get_config from VEDA.utils import get_config
from VEDA.settings.utils import get_logger_config from VEDA.settings.utils import get_logger_config
......
...@@ -12,4 +12,6 @@ DATABASES = { ...@@ -12,4 +12,6 @@ DATABASES = {
} }
} }
FERNET_KEYS = ['test-ferent-key']
LOGGING = get_logger_config(debug=False, dev_env=True, local_loglevel='DEBUG') LOGGING = get_logger_config(debug=False, dev_env=True, local_loglevel='DEBUG')
"""
Veda Admin.
"""
from django.contrib import admin from django.contrib import admin
from VEDA_OS01.models import ( from VEDA_OS01.models import (
...@@ -7,6 +10,9 @@ from VEDA_OS01.models import ( ...@@ -7,6 +10,9 @@ from VEDA_OS01.models import (
class CourseAdmin(admin.ModelAdmin): class CourseAdmin(admin.ModelAdmin):
"""
Course Admin.
"""
ordering = ['institution'] ordering = ['institution']
list_display = [ list_display = [
'course_name', 'course_name',
...@@ -27,6 +33,9 @@ class CourseAdmin(admin.ModelAdmin): ...@@ -27,6 +33,9 @@ class CourseAdmin(admin.ModelAdmin):
class VideoAdmin(admin.ModelAdmin): class VideoAdmin(admin.ModelAdmin):
"""
Admin for Video model.
"""
model = Video model = Video
list_display = [ list_display = [
'edx_id', 'edx_id',
...@@ -49,6 +58,9 @@ class VideoAdmin(admin.ModelAdmin): ...@@ -49,6 +58,9 @@ class VideoAdmin(admin.ModelAdmin):
class EncodeAdmin(admin.ModelAdmin): class EncodeAdmin(admin.ModelAdmin):
"""
Admin for Encode model.
"""
model = Encode model = Encode
ordering = ['encode_name'] ordering = ['encode_name']
list_display = [ list_display = [
...@@ -69,6 +81,9 @@ class EncodeAdmin(admin.ModelAdmin): ...@@ -69,6 +81,9 @@ class EncodeAdmin(admin.ModelAdmin):
class URLAdmin(admin.ModelAdmin): class URLAdmin(admin.ModelAdmin):
"""
Admin for URL model.
"""
model = URL model = URL
list_display = [ list_display = [
'video_id_get', 'video_id_get',
...@@ -93,16 +108,25 @@ class URLAdmin(admin.ModelAdmin): ...@@ -93,16 +108,25 @@ class URLAdmin(admin.ModelAdmin):
class DestinationAdmin(admin.ModelAdmin): class DestinationAdmin(admin.ModelAdmin):
"""
Admin for Destination model.
"""
model = Destination model = Destination
list_display = ['destination_name', 'destination_active'] list_display = ['destination_name', 'destination_active']
class InstitutionAdmin(admin.ModelAdmin): class InstitutionAdmin(admin.ModelAdmin):
"""
Admin for Institution model.
"""
model = Institution model = Institution
list_display = ['institution_name', 'institution_code'] list_display = ['institution_name', 'institution_code']
class VideoUploadAdmin(admin.ModelAdmin): class VideoUploadAdmin(admin.ModelAdmin):
"""
Admin for VedaUpload model.
"""
model = VedaUpload model = VedaUpload
list_display = [ list_display = [
'client_information', 'client_information',
...@@ -114,10 +138,17 @@ class VideoUploadAdmin(admin.ModelAdmin): ...@@ -114,10 +138,17 @@ class VideoUploadAdmin(admin.ModelAdmin):
class TranscriptCredentialsAdmin(admin.ModelAdmin): class TranscriptCredentialsAdmin(admin.ModelAdmin):
"""
Admin for TranscriptCredentials model.
"""
model = TranscriptCredentials model = TranscriptCredentials
exclude = ('api_key', 'api_secret')
class TranscriptProcessMetadataAdmin(admin.ModelAdmin): class TranscriptProcessMetadataAdmin(admin.ModelAdmin):
"""
Admin for TranscriptProcessMetadata model.
"""
model = TranscriptProcessMetadata model = TranscriptProcessMetadata
......
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2017-10-19 13:17
from __future__ import unicode_literals
from django.db import migrations
import fernet_fields.fields
class Migration(migrations.Migration):
dependencies = [
('VEDA_OS01', '0002_auto_20171016_1211'),
]
operations = [
migrations.AlterField(
model_name='transcriptcredentials',
name='api_key',
field=fernet_fields.fields.EncryptedTextField(max_length=255, verbose_name=b'API key'),
),
migrations.AlterField(
model_name='transcriptcredentials',
name='api_secret',
field=fernet_fields.fields.EncryptedTextField(max_length=255, verbose_name=b'API secret'),
),
]
...@@ -4,11 +4,13 @@ Models for Video Pipeline ...@@ -4,11 +4,13 @@ Models for Video Pipeline
import json import json
import uuid import uuid
from django.db import models from django.db import models
from fernet_fields import EncryptedTextField
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
def _createHex(): def _createHex():
return uuid.uuid1().hex return uuid.uuid1().hex
...@@ -216,7 +218,7 @@ class ListField(models.TextField): ...@@ -216,7 +218,7 @@ class ListField(models.TextField):
return value return value
class Institution (models.Model): class Institution(models.Model):
institution_code = models.CharField(max_length=4) institution_code = models.CharField(max_length=4)
institution_name = models.CharField(max_length=50) institution_name = models.CharField(max_length=50)
...@@ -227,7 +229,10 @@ class Institution (models.Model): ...@@ -227,7 +229,10 @@ class Institution (models.Model):
) )
class Course (models.Model): class Course(models.Model):
"""
Model for Course.
"""
course_name = models.CharField('Course Name', max_length=100) course_name = models.CharField('Course Name', max_length=100)
# TODO: Change Name (this is reversed) # TODO: Change Name (this is reversed)
...@@ -407,7 +412,10 @@ class Course (models.Model): ...@@ -407,7 +412,10 @@ class Course (models.Model):
) )
class Video (models.Model): class Video(models.Model):
"""
Model for Video.
"""
# TODO: Change field name # TODO: Change field name
inst_class = models.ForeignKey(Course) inst_class = models.ForeignKey(Course)
video_active = models.BooleanField('Video Active?', default=True) video_active = models.BooleanField('Video Active?', default=True)
...@@ -513,7 +521,10 @@ class Video (models.Model): ...@@ -513,7 +521,10 @@ class Video (models.Model):
return u'{edx_id}'.format(edx_id=self.edx_id) return u'{edx_id}'.format(edx_id=self.edx_id)
class Destination (models.Model): class Destination(models.Model):
"""
Model for Destination.
"""
destination_name = models.CharField('Destination', max_length=200, null=True, blank=True) destination_name = models.CharField('Destination', max_length=200, null=True, blank=True)
destination_active = models.BooleanField('Destination Active', default=False) destination_active = models.BooleanField('Destination Active', default=False)
destination_nick = models.CharField('Nickname (3 Char.)', max_length=3, null=True, blank=True) destination_nick = models.CharField('Nickname (3 Char.)', max_length=3, null=True, blank=True)
...@@ -522,7 +533,10 @@ class Destination (models.Model): ...@@ -522,7 +533,10 @@ class Destination (models.Model):
return u'%s'.format(self.destination_name) or u'' return u'%s'.format(self.destination_name) or u''
class Encode (models.Model): class Encode(models.Model):
"""
Model for Encode.
"""
encode_destination = models.ForeignKey(Destination) encode_destination = models.ForeignKey(Destination)
encode_name = models.CharField('Encode Name', max_length=100, null=True, blank=True) encode_name = models.CharField('Encode Name', max_length=100, null=True, blank=True)
profile_active = models.BooleanField('Encode Profile Active', default=False) profile_active = models.BooleanField('Encode Profile Active', default=False)
...@@ -570,7 +584,10 @@ class Encode (models.Model): ...@@ -570,7 +584,10 @@ class Encode (models.Model):
return u'{encode_profile}'.format(encode_profile=self.encode_name) return u'{encode_profile}'.format(encode_profile=self.encode_name)
class URL (models.Model): class URL(models.Model):
"""
Model for URL.
"""
encode_profile = models.ForeignKey(Encode) encode_profile = models.ForeignKey(Encode)
videoID = models.ForeignKey(Video) videoID = models.ForeignKey(Video)
encode_url = models.CharField('Destination URL', max_length=500, null=True, blank=True) encode_url = models.CharField('Destination URL', max_length=500, null=True, blank=True)
...@@ -597,7 +614,7 @@ class URL (models.Model): ...@@ -597,7 +614,7 @@ class URL (models.Model):
) )
class VedaUpload (models.Model): class VedaUpload(models.Model):
""" """
Internal Upload Tool Internal Upload Tool
""" """
...@@ -664,8 +681,8 @@ class TranscriptCredentials(TimeStampedModel): ...@@ -664,8 +681,8 @@ class TranscriptCredentials(TimeStampedModel):
help_text='This value must match the value of organization in studio/edx-platform.' help_text='This value must match the value of organization in studio/edx-platform.'
) )
provider = models.CharField('Transcript provider', max_length=50, choices=TranscriptProvider.CHOICES) provider = models.CharField('Transcript provider', max_length=50, choices=TranscriptProvider.CHOICES)
api_key = models.CharField('API key', max_length=255) api_key = EncryptedTextField('API key', max_length=255)
api_secret = models.CharField('API secret', max_length=255, null=True, blank=True) api_secret = EncryptedTextField('API secret', max_length=255)
class Meta: class Meta:
unique_together = ('org', 'provider') unique_together = ('org', 'provider')
......
""" Model tests """
from django.conf import settings
from django.test import override_settings
from django.test.testcases import TransactionTestCase
from cryptography.fernet import InvalidToken
from VEDA_OS01.models import TranscriptCredentials, TranscriptProvider
class TranscriptCredentialsModelTest(TransactionTestCase):
"""
Transcript credentials model tests
"""
def setUp(self):
"""
Test setup.
"""
self.credentials_data = {
'org': 'MAx',
'provider': TranscriptProvider.THREE_PLAY,
'api_key': 'test-key',
'api_secret': 'test-secret'
}
TranscriptCredentials.objects.create(**self.credentials_data)
def tearDown(self):
"""
Test teardown.
"""
# Invalidate here so that evry new test would have FERNET KEYS from tests.py initially.
self.invalidate_fernet_cached_properties()
def invalidate_fernet_cached_properties(self):
"""
Invalidates transcript credential fernet field's cached properties.
"""
def invalidate_fernet_cached_property(field_name):
"""
Invalidates fernet fields cached properties.
"""
field = TranscriptCredentials._meta.get_field(field_name)
if field.keys:
del field.keys
if field.fernet_keys:
del field.fernet_keys
if field.fernet:
del field.fernet
invalidate_fernet_cached_property('api_key')
invalidate_fernet_cached_property('api_secret')
def test_decrypt(self):
"""
Tests transcript credential fields are correctly decrypted.
"""
# Verify that api key is correctly fetched.
transcript_credentials = TranscriptCredentials.objects.get(
org=self.credentials_data['org'], provider=self.credentials_data['provider']
)
self.assertEqual(transcript_credentials.api_key, self.credentials_data['api_key'])
self.assertEqual(transcript_credentials.api_secret, self.credentials_data['api_secret'])
def test_decrypt_different_key(self):
"""
Tests decryption with one more key pre-pended. Note that we still have the old key with which value was
encrypted so we should be able to decrypt it again.
"""
old_keys_set = ['test-ferent-key']
self.assertEqual(settings.FERNET_KEYS, old_keys_set)
new_keys_set = ['new-fernet-key'] + settings.FERNET_KEYS
# Invalidate cached properties so that we get the latest keys
self.invalidate_fernet_cached_properties()
with override_settings(FERNET_KEYS=new_keys_set):
self.assertEqual(settings.FERNET_KEYS, new_keys_set)
transcript_credentials = TranscriptCredentials.objects.get(
org=self.credentials_data['org'], provider=self.credentials_data['provider']
)
self.assertEqual(transcript_credentials.api_key, self.credentials_data['api_key'])
self.assertEqual(transcript_credentials.api_secret, self.credentials_data['api_secret'])
def test_decrypt_different_key_set(self):
"""
Tests decryption with different fernet key set. Note that now we don't have the old fernet key with which
value was encrypted so we would not be able to decrypt it and we should get an Invalid Token.
"""
old_keys_set = ['test-ferent-key']
self.assertEqual(settings.FERNET_KEYS, old_keys_set)
new_keys_set = ['new-fernet-key']
# Invalidate cached properties so that we get the latest keys
self.invalidate_fernet_cached_properties()
with override_settings(FERNET_KEYS=new_keys_set):
self.assertEqual(settings.FERNET_KEYS, new_keys_set)
with self.assertRaises(InvalidToken):
TranscriptCredentials.objects.get(
org=self.credentials_data['org'], provider=self.credentials_data['provider']
)
...@@ -21,6 +21,9 @@ DATABASES: ...@@ -21,6 +21,9 @@ DATABASES:
SECRET_KEY: "" SECRET_KEY: ""
# Fernet keys
FERNET_KEYS: []
# Django DEBUG global # Django DEBUG global
# (set to false in prod) # (set to false in prod)
debug: false debug: false
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
# #
# ------------------------------ # ------------------------------
[MASTER] [MASTER]
ignore = pavelib_test_code ignore = pavelib_test_code, migrations
persistent = yes persistent = yes
load-plugins = edx_lint.pylint,pylint_django,pylint_celery load-plugins = edx_lint.pylint,pylint_django,pylint_celery
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment