diff --git a/awx/api/views.py b/awx/api/views.py index f6f1d7f0d1..7d3cb123b2 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -103,8 +103,11 @@ class ApiRootView(APIView): current_version = current, available_versions = dict( v1 = current - ) + ), ) + if feature_enabled('rebranding'): + data['custom_logo'] = settings.CUSTOM_LOGO + data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO return Response(data) diff --git a/awx/conf/management/commands/migrate_to_database_settings.py b/awx/conf/management/commands/migrate_to_database_settings.py index 618c8cf725..30bb922704 100644 --- a/awx/conf/management/commands/migrate_to_database_settings.py +++ b/awx/conf/management/commands/migrate_to_database_settings.py @@ -2,6 +2,7 @@ # All Rights Reserved. # Python +import base64 import collections import difflib import json @@ -141,12 +142,85 @@ class Command(BaseCommand): os.remove(license_file) return diff_lines + def _get_local_settings_file(self): + if MODE == 'development': + static_root = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'ui', 'static') + else: + static_root = settings.STATIC_ROOT + return os.path.join(static_root, 'local_settings.json') + + def _comment_local_settings_file(self, dry_run=True): + local_settings_file = self._get_local_settings_file() + diff_lines = [] + if os.path.exists(local_settings_file): + try: + raw_local_settings_data = open(local_settings_file).read() + json.loads(raw_local_settings_data) + except Exception as e: + if not self.skip_errors: + raise CommandError('Error reading local settings from {0}: {1!r}'.format(local_settings_file, e)) + return diff_lines + if self.backup_suffix: + backup_local_settings_file = '{}{}'.format(local_settings_file, self.backup_suffix) + else: + backup_local_settings_file = '{}.old'.format(local_settings_file) + diff_lines = list(difflib.unified_diff( + raw_local_settings_data.splitlines(), + [], + fromfile=backup_local_settings_file, + tofile=local_settings_file, + lineterm='', + )) + if not dry_run: + if self.backup_suffix: + shutil.copy2(local_settings_file, backup_local_settings_file) + os.remove(local_settings_file) + return diff_lines + + def _get_custom_logo_file(self): + if MODE == 'development': + static_root = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'ui', 'static') + else: + static_root = settings.STATIC_ROOT + return os.path.join(static_root, 'assets', 'custom_console_logo.png') + + def _comment_custom_logo_file(self, dry_run=True): + custom_logo_file = self._get_custom_logo_file() + diff_lines = [] + if os.path.exists(custom_logo_file): + try: + raw_custom_logo_data = open(custom_logo_file).read() + except Exception as e: + if not self.skip_errors: + raise CommandError('Error reading custom logo from {0}: {1!r}'.format(custom_logo_file, e)) + return diff_lines + if self.backup_suffix: + backup_custom_logo_file = '{}{}'.format(custom_logo_file, self.backup_suffix) + else: + backup_custom_logo_file = '{}.old'.format(custom_logo_file) + diff_lines = list(difflib.unified_diff( + [''.format(len(raw_custom_logo_data))], + [], + fromfile=backup_custom_logo_file, + tofile=custom_logo_file, + lineterm='', + )) + if not dry_run: + if self.backup_suffix: + shutil.copy2(custom_logo_file, backup_custom_logo_file) + os.remove(custom_logo_file) + return diff_lines + def _check_if_needs_comment(self, patterns, setting): files_to_comment = [] # If any diffs are returned, this setting needs to be commented. diffs = comment_assignments(patterns, setting, dry_run=True) if setting == 'LICENSE': diffs.extend(self._comment_license_file(dry_run=True)) + elif setting == 'CUSTOM_LOGIN_INFO': + diffs.extend(self._comment_local_settings_file(dry_run=True)) + elif setting == 'CUSTOM_LOGO': + diffs.extend(self._comment_custom_logo_file(dry_run=True)) for diff in diffs: for line in diff.splitlines(): if line.startswith('+++ '): @@ -163,6 +237,27 @@ class Command(BaseCommand): except SkipField: pass current_value = getattr(settings, setting, empty) + if setting == 'CUSTOM_LOGIN_INFO' and current_value in {empty, ''}: + local_settings_file = self._get_local_settings_file() + try: + if os.path.exists(local_settings_file): + local_settings = json.load(open(local_settings_file)) + current_value = local_settings.get('custom_login_info', '') + except Exception as e: + if not self.skip_errors: + raise CommandError('Error reading custom login info from {0}: {1!r}'.format(local_settings_file, e)) + if setting == 'CUSTOM_LOGO' and current_value in {empty, ''}: + custom_logo_file = self._get_custom_logo_file() + try: + if os.path.exists(custom_logo_file): + custom_logo_data = open(custom_logo_file).read() + if custom_logo_data: + current_value = 'data:image/png;base64,{}'.format(base64.b64encode(custom_logo_data)) + else: + current_value = '' + except Exception as e: + if not self.skip_errors: + raise CommandError('Error reading custom logo from {0}: {1!r}'.format(custom_logo_file, e)) if current_value != default_value: if current_value is empty: current_value = None @@ -338,10 +433,16 @@ class Command(BaseCommand): if to_comment: to_comment_patterns = [] license_file_to_comment = None + local_settings_file_to_comment = None + custom_logo_file_to_comment = None for files_to_comment in to_comment.values(): for file_to_comment in files_to_comment: if file_to_comment == self._get_license_file(): license_file_to_comment = file_to_comment + elif file_to_comment == self._get_local_settings_file(): + local_settings_file_to_comment = file_to_comment + elif file_to_comment == self._get_custom_logo_file(): + custom_logo_file_to_comment = file_to_comment elif file_to_comment not in to_comment_patterns: to_comment_patterns.append(file_to_comment) # Run once in dry-run mode to catch any errors from updating the files. @@ -351,4 +452,8 @@ class Command(BaseCommand): diffs = comment_assignments(to_comment_patterns, to_comment.keys(), dry_run=False, backup_suffix=self.backup_suffix) if license_file_to_comment: diffs.extend(self._comment_license_file(dry_run=False)) + if local_settings_file_to_comment: + diffs.extend(self._comment_local_settings_file(dry_run=False)) + if custom_logo_file_to_comment: + diffs.extend(self._comment_custom_logo_file(dry_run=False)) self._display_comment(diffs) diff --git a/awx/main/tests/functional/api/test_settings.py b/awx/main/tests/functional/api/test_settings.py index 5125f1b13f..55fc45a334 100644 --- a/awx/main/tests/functional/api/test_settings.py +++ b/awx/main/tests/functional/api/test_settings.py @@ -11,6 +11,10 @@ from django.core.urlresolvers import reverse # AWX from awx.conf.models import Setting +TEST_GIF_LOGO = 'data:image/gif;base64,R0lGODlhIQAjAPIAAP//////AP8AAMzMAJmZADNmAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAHACwAAAAAIQAjAAADo3i63P4wykmrvTjrzZsxXfR94WMQBFh6RECuixHMLyzPQ13ewZCvow9OpzEAjIBj79cJJmU+FceIVEZ3QRozxBttmyOBwPBtisdX4Bha3oxmS+llFIPHQXQKkiSEXz9PeklHBzx3hYNyEHt4fmmAhHp8Nz45KgV5FgWFOFEGmwWbGqEfniChohmoQZ+oqRiZDZhEgk81I4mwg4EKVbxzrDHBEAkAIfkECQoABwAsAAAAACEAIwAAA6V4utz+MMpJq724GpP15p1kEAQYQmOwnWjgrmxjuMEAx8rsDjZ+fJvdLWQAFAHGWo8FRM54JqIRmYTigDrDMqZTbbbMj0CgjTLHZKvPQH6CTx+a2vKR0XbbOsoZ7SphG057gjl+c0dGgzeGNiaBiSgbBQUHBV08NpOVlkMSk0FKjZuURHiiOJxQnSGfQJuoEKREejK0dFRGjoiQt7iOuLx0rgxYEQkAIfkECQoABwAsAAAAACEAIwAAA7h4utxnxslJDSGR6nrz/owxYB64QUEwlGaVqlB7vrAJscsd3Lhy+wBArGEICo3DUFH4QDqK0GMy51xOgcGlEAfJ+iAFie62chR+jYKaSAuQGOqwJp7jGQRDuol+F/jxZWsyCmoQfwYwgoM5Oyg1i2w0A2WQIW2TPYOIkleQmy+UlYygoaIPnJmapKmqKiusMmSdpjxypnALtrcHioq3ury7hGm3dnVosVpMWFmwREZbddDOSsjVswcJACH5BAkKAAcALAAAAAAhACMAAAOxeLrc/jDKSZUxNS9DCNYV54HURQwfGRlDEFwqdLVuGjOsW9/Odb0wnsUAKBKNwsMFQGwyNUHckVl8bqI4o43lA26PNkv1S9DtNuOeVirw+aTI3qWAQwnud1vhLSnQLS0GeFF+GoVKNF0fh4Z+LDQ6Bn5/MTNmL0mAl2E3j2aclTmRmYCQoKEDiaRDKFhJez6UmbKyQowHtzy1uEl8DLCnEktrQ2PBD1NxSlXKIW5hz6cJACH5BAkKAAcALAAAAAAhACMAAAOkeLrc/jDKSau9OOvNlTFd9H3hYxAEWDJfkK5LGwTq+g0zDR/GgM+10A04Cm56OANgqTRmkDTmSOiLMgFOTM9AnFJHuexzYBAIijZf2SweJ8ttbbXLmd5+wBiJosSCoGF/fXEeS1g8gHl9hxODKkh4gkwVIwUekESIhA4FlgV3PyCWG52WI2oGnR2lnUWpqhqVEF4Xi7QjhpsshpOFvLosrnpoEAkAIfkECQoABwAsAAAAACEAIwAAA6l4utz+MMpJq71YGpPr3t1kEAQXQltQnk8aBCa7bMMLy4wx1G8s072PL6SrGQDI4zBThCU/v50zCVhidIYgNPqxWZkDg0AgxB2K4vEXbBSvr1JtZ3uOext0x7FqovF6OXtfe1UzdjAxhINPM013ChtJER8FBQeVRX8GlpggFZWWfjwblTiigGZnfqRmpUKbljKxDrNMeY2eF4R8jUiSur6/Z8GFV2WBtwwJACH5BAkKAAcALAAAAAAhACMAAAO6eLrcZi3KyQwhkGpq8f6ONWQgaAxB8JTfg6YkO50pzD5xhaurhCsGAKCnEw6NucNDCAkyI8ugdAhFKpnJJdMaeiofBejowUseCr9GYa0j1GyMdVgjBxoEuPSZXWKf7gKBeHtzMms0gHgGfDIVLztmjScvNZEyk28qjT40b5aXlHCbDgOhnzedoqOOlKeopaqrCy56sgtotbYKhYW6e7e9tsHBssO6eSTIm1peV0iuFUZDyU7NJnmcuQsJACH5BAkKAAcALAAAAAAhACMAAAOteLrc/jDKSZsxNS9DCNYV54Hh4H0kdAXBgKaOwbYX/Miza1vrVe8KA2AoJL5gwiQgeZz4GMXlcHl8xozQ3kW3KTajL9zsBJ1+sV2fQfALem+XAlRApxu4ioI1UpC76zJ4fRqDBzI+LFyFhH1iiS59fkgziW07jjRAG5QDeECOLk2Tj6KjnZafW6hAej6Smgevr6yysza2tiCuMasUF2Yov2gZUUQbU8YaaqjLpQkAOw==' +TEST_PNG_LOGO = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAjCAYAAAAaLGNkAAAAAXNSR0IB2cksfwAAAdVpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj4xPC90aWZmOkNvbXByZXNzaW9uPgogICAgICAgICA8dGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPjI8L3RpZmY6UGhvdG9tZXRyaWNJbnRlcnByZXRhdGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cjl0tmoAAAHVSURBVFgJ7VZRsoMgDNTOu5E9U+/Ud6Z6JssGNg2oNKD90xkHCNnNkgTbYbieKwNXBn6bgSXQ4+16xi5UDiqDN3Pecr6+1fM5DHh7n1NEIPjjoRLKzOjG3qQ5dRtEy2LCjh/Gz2wDZE2nZYKkrxdn/kY9XQQkGCGqqDY5IgJFkEKgBCzDNGXhTKEye7boFRH6IPJj5EshiNCSjV4R4eSx7zhmR2tcdIuwmWiMeao7e0JHViZEWUI5aP8a9O+rx74D6sGEiJftiX3YeueIiFXg2KrhpqzjVC3dPZFYJZ7NOwwtNwM8R0UkLfH0sT5qck+OlkMq0BucKr0iWG7gpAQksD9esM1z3Lnf6SHjLh67nnKEGxC/iomWhByTeXOQJGHHcKxwHhHKnt1HIdYtmexkIb/HOURWTSJqn2gKMDG0bDUc/D0iAseovxUBoylmQCug6IVhSv+4DIeKI94jAr4AjiSEgQ25JYB+YWT9BZ94AM8erwgFkRifaArA6U0G5KT0m//z26REZuK9okgrT6VwE1jTHjbVzyNAyRwTEPOtuiex9FVBNZCkruaA4PZqFp1u8Rpww9/6rcK5y0EkAxRiZJt79PWOVYWGRE9pbJhavMengMflGyumk0akMsQnAAAAAElFTkSuQmCC' +TEST_JPEG_LOGO = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBkRXhpZgAATU0AKgAAAAgAAwEGAAMAAAABAAIAAAESAAMAAAABAAEAAIdpAAQAAAABAAAAMgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIaADAAQAAAABAAAAIwAAAAD/4QkhaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiLz4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8P3hwYWNrZXQgZW5kPSJ3Ij8+AP/tADhQaG90b3Nob3AgMy4wADhCSU0EBAAAAAAAADhCSU0EJQAAAAAAENQdjNmPALIE6YAJmOz4Qn7/wAARCAAjACEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwAGBgYGBgYKBgYKDgoKCg4SDg4ODhIXEhISEhIXHBcXFxcXFxwcHBwcHBwcIiIiIiIiJycnJycsLCwsLCwsLCws/9sAQwEHBwcLCgsTCgoTLh8aHy4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4u/90ABAAD/9oADAMBAAIRAxEAPwD6poormvFfivSvB2lHVtWLGMtsRE2hnYKzlVLsi52oxALDdjauWKqQCXQfFXh7xP8Aaf7AvYrz7HL5U3lk/K3YjIGVODtcZVsHBODXQV806bcT+E9L03XbCOS2udMsLQanbB4po72xYMfOQpKYyV2zPEwcNwVK7WAr6WriwWMWIUvdcZRdmnuu33rVFSjYKKKK7ST/0PqmuF8Vv4X8S+HNZ0+e/gIsYJvtEsL+bJZsI3UuyxNvBA3gpxvXchyCRXdV8ta3bW667DoloW1y10tLLTJxZWP2hoLSGYzNHclGZpJC0ESk8IAZcRB8is61T2cHK1/1DrY526h8YXHh691vxCz6dafY5Q0U7yGSeQxSxohNzJLcbUeQ4VnVNxBRCWL19b2eraVqE9xa2F3BcS2jbJ0ikV2ibJG1wpJU5UjBx0PpXzrrniy4k17TrrWrGex022ufMijvd9m11PGH8naXKqsUcgR3MhB5U7MA16x4L8F3vhq2sY9Ru4rg6day2tusEAhCrcOkknmEMRI2Y1AcLGT8xYMzZHjZFGu6cquKjaUnt2XS76vv/SN8RVjOdoKyXY9Cooor3TA//9H6pr4gfxRrMvxJ0/whLJE+maVrcVnZRtBCzwQQ3SIipMU80fKignflgPmJr7fr4A/5rf8A9zJ/7eUAdX8SfGviPwl8TtaPh6eK1eTyN0n2eCSUg28OV8ySNn2/KDtztzzjNfZVhY2umWMGm2KeXb2sSQxJknakYCqMkknAHUnNfBXxt/5Kdq//AG7/APpPFX3/AEAFFFFAH//Z' + @pytest.fixture def mock_no_license_file(mocker): @@ -38,3 +42,40 @@ def test_license_cannot_be_removed_via_system_settings(mock_no_license_file, get delete(url, user=admin, expect=204) response = get(url, user=admin, expect=200) assert response.data['LICENSE'] + + +@pytest.mark.django_db +def test_ui_settings(get, put, patch, delete, admin, enterprise_license): + url = reverse('api:setting_singleton_detail', args=('ui',)) + response = get(url, user=admin, expect=200) + assert 'CUSTOM_LOGO' not in response.data + assert 'CUSTOM_LOGIN_INFO' not in response.data + Setting.objects.create(key='LICENSE', value=enterprise_license) + response = get(url, user=admin, expect=200) + assert not response.data['CUSTOM_LOGO'] + assert not response.data['CUSTOM_LOGIN_INFO'] + put(url, user=admin, data=response.data, expect=200) + patch(url, user=admin, data={'CUSTOM_LOGO': 'data:text/plain;base64,'}, expect=400) + patch(url, user=admin, data={'CUSTOM_LOGO': 'data:image/png;base64,00'}, expect=400) + patch(url, user=admin, data={'CUSTOM_LOGO': TEST_GIF_LOGO}, expect=200) + response = get(url, user=admin, expect=200) + assert response.data['CUSTOM_LOGO'] == TEST_GIF_LOGO + patch(url, user=admin, data={'CUSTOM_LOGO': TEST_PNG_LOGO}, expect=200) + response = get(url, user=admin, expect=200) + assert response.data['CUSTOM_LOGO'] == TEST_PNG_LOGO + patch(url, user=admin, data={'CUSTOM_LOGO': TEST_JPEG_LOGO}, expect=200) + response = get(url, user=admin, expect=200) + assert response.data['CUSTOM_LOGO'] == TEST_JPEG_LOGO + patch(url, user=admin, data={'CUSTOM_LOGO': ''}, expect=200) + response = get(url, user=admin, expect=200) + assert not response.data['CUSTOM_LOGO'] + patch(url, user=admin, data={'CUSTOM_LOGIN_INFO': 'Customize Me!'}, expect=200) + response = get(url, user=admin, expect=200) + assert response.data['CUSTOM_LOGIN_INFO'] + patch(url, user=admin, data={'CUSTOM_LOGIN_INFO': ''}, expect=200) + response = get(url, user=admin, expect=200) + assert not response.data['CUSTOM_LOGIN_INFO'] + delete(url, user=admin, expect=204) + response = get(url, user=admin, expect=200) + assert not response.data['CUSTOM_LOGO'] + assert not response.data['CUSTOM_LOGIN_INFO'] diff --git a/awx/ui/conf.py b/awx/ui/conf.py index a92eeea35c..bac1709253 100644 --- a/awx/ui/conf.py +++ b/awx/ui/conf.py @@ -5,16 +5,8 @@ from django.utils.translation import ugettext_lazy as _ # Tower -from awx.conf import fields, register - - -class PendoTrackingStateField(fields.ChoiceField): - - def to_internal_value(self, data): - # Any false/null values get converted to 'off'. - if data in fields.NullBooleanField.FALSE_VALUES or data in fields.NullBooleanField.NULL_VALUES: - return 'off' - return super(PendoTrackingStateField, self).to_internal_value(data) +from awx.conf import register, fields +from awx.ui.fields import * # noqa register( @@ -30,3 +22,35 @@ register( category=_('UI'), category_slug='ui', ) + +register( + 'CUSTOM_LOGIN_INFO', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('Custom Login Info'), + help_text=_('If needed, you can add specific information (such as a legal ' + 'notice or a disclaimer) to a text box in the login modal using ' + 'this setting. Any content added must be in plain text, as ' + 'custom HTML or other markup languages are not supported. If ' + 'multiple paragraphs of text are needed, new lines (paragraphs) ' + 'must be escaped as `\\n` within the block of text.'), + category=_('UI'), + category_slug='ui', + feature_required='rebranding', +) + +register( + 'CUSTOM_LOGO', + field_class=CustomLogoField, + allow_blank=True, + default='', + label=_('Custom Logo'), + help_text=_('To set up a custom logo, provide a file that you create. For ' + 'the custom logo to look its best, use a `.png` file with a ' + 'transparent background. GIF, PNG and JPEG formats are supported.'), + placeholder='data:image/gif;base64,R0lGODlhAQABAAAAADs=', + category=_('UI'), + category_slug='ui', + feature_required='rebranding', +) diff --git a/awx/ui/fields.py b/awx/ui/fields.py new file mode 100644 index 0000000000..20bddb39f1 --- /dev/null +++ b/awx/ui/fields.py @@ -0,0 +1,43 @@ +# Copyright (c) 2016 Ansible, Inc. +# All Rights Reserved. + +# Python +import base64 +import re + +# Django +from django.utils.translation import ugettext_lazy as _ + +# Tower +from awx.conf import fields, register + + +class PendoTrackingStateField(fields.ChoiceField): + + def to_internal_value(self, data): + # Any false/null values get converted to 'off'. + if data in fields.NullBooleanField.FALSE_VALUES or data in fields.NullBooleanField.NULL_VALUES: + return 'off' + return super(PendoTrackingStateField, self).to_internal_value(data) + + +class CustomLogoField(fields.CharField): + + CUSTOM_LOGO_RE = re.compile(r'^data:image/(?:png|jpeg|gif);base64,([A-Za-z0-9+/=]+?)$') + + default_error_messages = { + 'invalid_format': _('Invalid format for custom logo. Must be a data URL with a base64-encoded GIF, PNG or JPEG image.'), + 'invalid_data': _('Invalid base64-encoded data in data URL.'), + } + + def to_internal_value(self, data): + data = super(CustomLogoField, self).to_internal_value(data) + match = self.CUSTOM_LOGO_RE.match(data) + if not match: + self.fail('invalid_format') + b64data = match.group(1) + try: + base64.b64decode(b64data) + except TypeError: + self.fail('invalid_data') + return data