From 5f01d26224eb81400e5c8db44404b0d879926272 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 3 Apr 2018 16:51:34 -0400 Subject: [PATCH] automatically encrypt/decrypt main_oauth2application.client_secret see: https://github.com/ansible/awx/issues/1416 --- awx/main/fields.py | 14 +++++++++++ .../0029_v330_encrypt_oauth2_secret.py | 22 ++++++++++++++++++ awx/main/models/oauth.py | 7 ++++++ awx/main/tests/functional/api/test_oauth.py | 23 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 awx/main/migrations/0029_v330_encrypt_oauth2_secret.py diff --git a/awx/main/fields.py b/awx/main/fields.py index 29ccb71609..3d541d524a 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -42,6 +42,7 @@ from rest_framework import serializers # AWX from awx.main.utils.filters import SmartFilter +from awx.main.utils.encryption import encrypt_value, decrypt_value, get_encryption_key from awx.main.validators import validate_ssh_private_key from awx.main.models.rbac import batch_role_ancestor_rebuilding, Role from awx.main import utils @@ -821,3 +822,16 @@ class AskForField(models.BooleanField): # self.name will be set by the model metaclass, not this field raise Exception('Corresponding allows_field cannot be accessed until model is initialized.') return self._allows_field + + +class OAuth2ClientSecretField(models.CharField): + + def get_db_prep_value(self, value, connection, prepared=False): + return super(OAuth2ClientSecretField, self).get_db_prep_value( + encrypt_value(value), connection, prepared + ) + + def from_db_value(self, value, expression, connection, context): + if value.startswith('$encrypted$'): + return decrypt_value(get_encryption_key('value', pk=None), value) + return value diff --git a/awx/main/migrations/0029_v330_encrypt_oauth2_secret.py b/awx/main/migrations/0029_v330_encrypt_oauth2_secret.py new file mode 100644 index 0000000000..e49372144b --- /dev/null +++ b/awx/main/migrations/0029_v330_encrypt_oauth2_secret.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-04-03 20:48 +from __future__ import unicode_literals + +import awx.main.fields +from django.db import migrations +import oauth2_provider.generators + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0028_v330_modify_application'), + ] + + operations = [ + migrations.AlterField( + model_name='oauth2application', + name='client_secret', + field=awx.main.fields.OAuth2ClientSecretField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=1024), + ), + ] diff --git a/awx/main/models/oauth.py b/awx/main/models/oauth.py index 8626ca51c3..c905aad3f4 100644 --- a/awx/main/models/oauth.py +++ b/awx/main/models/oauth.py @@ -9,6 +9,9 @@ from django.utils.translation import ugettext_lazy as _ # Django OAuth Toolkit from oauth2_provider.models import AbstractApplication, AbstractAccessToken +from oauth2_provider.generators import generate_client_secret + +from awx.main.fields import OAuth2ClientSecretField DATA_URI_RE = re.compile(r'.*') # FIXME @@ -39,6 +42,10 @@ class OAuth2Application(AbstractApplication): null=True, ) + client_secret = OAuth2ClientSecretField( + max_length=1024, blank=True, default=generate_client_secret, db_index=True + ) + class OAuth2AccessToken(AbstractAccessToken): diff --git a/awx/main/tests/functional/api/test_oauth.py b/awx/main/tests/functional/api/test_oauth.py index a417eb539f..3666165c6b 100644 --- a/awx/main/tests/functional/api/test_oauth.py +++ b/awx/main/tests/functional/api/test_oauth.py @@ -1,6 +1,9 @@ import pytest import base64 +from django.db import connection + +from awx.main.utils.encryption import decrypt_value, get_encryption_key from awx.api.versioning import reverse, drf_reverse from awx.main.models.oauth import (OAuth2Application as Application, OAuth2AccessToken as AccessToken, @@ -65,6 +68,26 @@ def test_oauth_application_update(oauth_application, organization, patch, admin, assert updated_app.organization == organization +@pytest.mark.django_db +def test_oauth_application_encryption(admin, organization, post): + response = post( + reverse('api:o_auth2_application_list'), { + 'name': 'test app', + 'organization': organization.pk, + 'client_type': 'confidential', + 'authorization_grant_type': 'password', + }, admin, expect=201 + ) + pk = response.data.get('id') + secret = response.data.get('client_secret') + with connection.cursor() as cursor: + encrypted = cursor.execute( + 'SELECT client_secret FROM main_oauth2application WHERE id={}'.format(pk) + ).fetchone()[0] + assert encrypted.startswith('$encrypted$') + assert decrypt_value(get_encryption_key('value', pk=None), encrypted) == secret + + @pytest.mark.django_db def test_oauth_token_create(oauth_application, get, post, admin): response = post(