mirror of
https://github.com/ansible/awx.git
synced 2026-03-20 10:27:34 -02:30
Configure Tower in Tower:
* Add separate Django app for configuration: awx.conf. * Migrate from existing main.TowerSettings model to conf.Setting. * Add settings wrapper to allow get/set/del via django.conf.settings. * Update existing references to tower_settings to use django.conf.settings. * Add a settings registry to allow for each Django app to register configurable settings. * Support setting validation and conversion using Django REST Framework fields. * Add /api/v1/settings/ to display a list of setting categories. * Add /api/v1/settings/<slug>/ to display all settings in a category as a single object. * Allow PUT/PATCH to update setting singleton, DELETE to reset to defaults. * Add "all" category to display all settings across categories. * Add "changed" category to display only settings configured in the database. * Support per-user settings via "user" category (/api/v1/settings/user/). * Support defaults for user settings via "user-defaults" category (/api/v1/settings/user-defaults/). * Update serializer metadata to support category, category_slug and placeholder on OPTIONS responses. * Update serializer metadata to handle child fields of a list/dict. * Hide raw data form in browsable API for OPTIONS and DELETE. * Combine existing licensing code into single "TaskEnhancer" class. * Move license helper functions from awx.api.license into awx.conf.license. * Update /api/v1/config/ to read/verify/update license using TaskEnhancer and settings wrapper. * Add support for caching settings accessed via settings wrapper. * Invalidate cached settings when Setting model changes or is deleted. * Preload all database settings into cache on first access via settings wrapper. * Add support for read-only settings than can update their value depending on other settings. * Use setting_changed signal whenever a setting changes. * Register configurable authentication, jobs, system and ui settings. * Register configurable LDAP, RADIUS and social auth settings. * Add custom fields and validators for URL, LDAP, RADIUS and social auth settings. * Rewrite existing validator for Credential ssh_private_key to support validating private keys, certs or combinations of both. * Get all unit/functional tests working with above changes. * Add "migrate_to_database_settings" command to determine settings to be migrated into the database and comment them out when set in Python settings files. * Add support for migrating license key from file to database. * Remove database-configuable settings from local_settings.py example files. * Update setup role to no longer install files for database-configurable settings. f 94ff6ee More settings work. f af4c4e0 Even more db settings stuff. f 96ea9c0 More settings, attempt at singleton serializer for settings. f 937c760 More work on singleton/category views in API, add code to comment out settings in Python files, work on command to migrate settings to database. f 425b0d3 Minor fixes for sprint demo. f ea402a4 Add support for read-only settings, cleanup license engine, get license support working with DB settings. f ec289e4 Rename migration, minor fixmes, update setup role. f 603640b Rewrite key/cert validator, finish adding social auth fields, hook up signals for setting_changed, use None to imply a setting is not set. f 67d1b5a Get functional/unit tests passing. f 2919b62 Flake8 fixes. f e62f421 Add redbaron to requirements, get file to database migration working (except for license). f c564508 Add support for migrating license file. f 982f767 Add support for regex in social map fields.
This commit is contained in:
168
awx/main/validators.py
Normal file
168
awx/main/validators.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import base64
|
||||
import re
|
||||
|
||||
# Django
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
def validate_pem(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None):
|
||||
"""
|
||||
Validate the given PEM data is valid and contains the required numbers of
|
||||
keys and certificates.
|
||||
|
||||
Return a list of PEM objects, where each object is a dict with the following
|
||||
keys:
|
||||
- 'all': The entire string for the PEM object including BEGIN/END lines.
|
||||
- 'type': The type of PEM object ('PRIVATE KEY' or 'CERTIFICATE').
|
||||
- 'data': The string inside the BEGIN/END lines.
|
||||
- 'b64': Key/certificate as a base64-encoded string.
|
||||
- 'bin': Key/certificate as bytes.
|
||||
- 'key_type': Only when type == 'PRIVATE KEY', one of 'rsa', 'dsa',
|
||||
'ecdsa', 'ed25519' or 'rsa1'.
|
||||
- 'key_enc': Only when type == 'PRIVATE KEY', boolean indicating if key is
|
||||
encrypted.
|
||||
"""
|
||||
|
||||
# Map the X in BEGIN X PRIVATE KEY to the key type (ssh-keygen -t).
|
||||
# Tower jobs using OPENSSH format private keys may still fail if the
|
||||
# system SSH implementation lacks support for this format.
|
||||
private_key_types = {
|
||||
'RSA': 'rsa',
|
||||
'DSA': 'dsa',
|
||||
'EC': 'ecdsa',
|
||||
'OPENSSH': 'ed25519',
|
||||
'': 'rsa1',
|
||||
}
|
||||
|
||||
# Build regular expressions for matching each object in the PEM file.
|
||||
pem_obj_re = re.compile(
|
||||
r'^(-{4,}) *BEGIN ([A-Z ]+?) *\1[\r\n]+' +
|
||||
r'(.+?)[\r\n]+\1 *END \2 *\1[\r\n]?(.*?)$', re.DOTALL,
|
||||
)
|
||||
pem_obj_header_re = re.compile(r'^(.+?):\s*?(.+?)(\\??)$')
|
||||
|
||||
pem_objects = []
|
||||
key_count, cert_count = 0, 0
|
||||
data = data.lstrip()
|
||||
while data:
|
||||
match = pem_obj_re.match(data)
|
||||
if not match:
|
||||
raise ValidationError(_('Invalid certificate or key: %r...') % data[:100])
|
||||
data = match.group(4).lstrip()
|
||||
|
||||
# Check PEM object type, check key type if private key.
|
||||
pem_obj_info = {}
|
||||
pem_obj_info['all'] = match.group(0)
|
||||
pem_obj_info['type'] = pem_obj_type = match.group(2)
|
||||
if pem_obj_type.endswith('PRIVATE KEY'):
|
||||
key_count += 1
|
||||
pem_obj_info['type'] = 'PRIVATE KEY'
|
||||
key_type = pem_obj_type.replace('PRIVATE KEY', '').strip()
|
||||
try:
|
||||
pem_obj_info['key_type'] = private_key_types[key_type]
|
||||
except KeyError:
|
||||
raise ValidationError(_('Invalid private key: unsupported type "%s"') % key_type)
|
||||
elif pem_obj_type == 'CERTIFICATE':
|
||||
cert_count += 1
|
||||
else:
|
||||
raise ValidationError(_('Unsupported PEM object type: "%s"') % pem_obj_type)
|
||||
|
||||
# Ensure that this PEM object is valid base64 data.
|
||||
pem_obj_info['data'] = match.group(3)
|
||||
base64_data = ''
|
||||
line_continues = False
|
||||
for line in pem_obj_info['data'].splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line_continues:
|
||||
line_continues = line.endswith('\\')
|
||||
continue
|
||||
line_match = pem_obj_header_re.match(line)
|
||||
if line_match:
|
||||
line_continues = line.endswith('\\')
|
||||
continue
|
||||
base64_data += line
|
||||
try:
|
||||
decoded_data = base64.b64decode(base64_data)
|
||||
if not decoded_data:
|
||||
raise TypeError
|
||||
pem_obj_info['b64'] = base64_data
|
||||
pem_obj_info['bin'] = decoded_data
|
||||
except TypeError:
|
||||
raise ValidationError(_('Invalid base64-encoded data'))
|
||||
|
||||
# If private key, check whether it is encrypted.
|
||||
if pem_obj_info.get('key_type', '') == 'ed25519':
|
||||
# See https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L3218
|
||||
# Decoded key data starts with magic string (null-terminated), four byte
|
||||
# length field, followed by the ciphername -- if ciphername is anything
|
||||
# other than 'none' the key is encrypted.
|
||||
pem_obj_info['key_enc'] = not bool(pem_obj_info['bin'].startswith('openssh-key-v1\x00\x00\x00\x00\x04none'))
|
||||
elif pem_obj_info.get('key_type', ''):
|
||||
pem_obj_info['key_enc'] = bool('ENCRYPTED' in pem_obj_info['data'])
|
||||
|
||||
pem_objects.append(pem_obj_info)
|
||||
|
||||
# Validate that the number of keys and certs provided are within the limits.
|
||||
key_count_dict = dict(min_keys=min_keys, max_keys=max_keys, key_count=key_count)
|
||||
if key_count < min_keys:
|
||||
if min_keys == 1:
|
||||
if max_keys == min_keys:
|
||||
raise ValidationError(_('Exactly one private key is required.'))
|
||||
else:
|
||||
raise ValidationError(_('At least one private key is required.'))
|
||||
else:
|
||||
raise ValidationError(_('At least %(min_keys)d private keys are required, only %(key_count)d provided.') % key_count_dict)
|
||||
elif max_keys is not None and key_count > max_keys:
|
||||
if max_keys == 1:
|
||||
raise ValidationError(_('Only one private key is allowed, %(key_count)d provided.') % key_count_dict)
|
||||
else:
|
||||
raise ValidationError(_('No more than %(max_keys)d private keys are allowed, %(key_count)d provided.') % key_count_dict)
|
||||
cert_count_dict = dict(min_certs=min_certs, max_certs=max_certs, cert_count=cert_count)
|
||||
if cert_count < min_certs:
|
||||
if min_certs == 1:
|
||||
if max_certs == min_certs:
|
||||
raise ValidationError(_('Exactly one certificate is required.'))
|
||||
else:
|
||||
raise ValidationError(_('At least one certificate is required.'))
|
||||
else:
|
||||
raise ValidationError(_('At least %(min_certs)d certificates are required, only %(cert_count)d provided.') % cert_count_dict)
|
||||
elif max_certs is not None and cert_count > max_certs:
|
||||
if max_certs == 1:
|
||||
raise ValidationError(_('Only one certificate is allowed, %(cert_count)d provided.') % cert_count_dict)
|
||||
else:
|
||||
raise ValidationError(_('No more than %(max_certs)d certificates are allowed, %(cert_count)d provided.') % cert_count_dict)
|
||||
|
||||
return pem_objects
|
||||
|
||||
|
||||
def validate_private_key(data):
|
||||
"""
|
||||
Validate that data contains exactly one private key.
|
||||
"""
|
||||
return validate_pem(data, min_keys=1, max_keys=1, max_certs=0)
|
||||
|
||||
|
||||
def validate_certificate(data):
|
||||
"""
|
||||
Validate that data contains one or more certificates. Adds BEGIN/END lines
|
||||
if necessary.
|
||||
"""
|
||||
if 'BEGIN CERTIFICATE' not in data:
|
||||
data = '-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n'.format(data)
|
||||
return validate_pem(data, max_keys=0, min_certs=1)
|
||||
|
||||
|
||||
def validate_ssh_private_key(data):
|
||||
"""
|
||||
Validate that data contains at least one private key and optionally
|
||||
certificates; should handle any valid options for ssh_private_key on a
|
||||
credential.
|
||||
"""
|
||||
return validate_pem(data, min_keys=1)
|
||||
Reference in New Issue
Block a user