mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 07:17:40 -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:
@@ -19,3 +19,6 @@ def xmlsec_initialize(*args, **kwargs):
|
||||
xmlsec_initialized = True
|
||||
|
||||
dm.xmlsec.binding.initialize = xmlsec_initialize
|
||||
|
||||
|
||||
default_app_config = 'awx.sso.apps.SSOConfig'
|
||||
|
||||
9
awx/sso/apps.py
Normal file
9
awx/sso/apps.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Django
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class SSOConfig(AppConfig):
|
||||
|
||||
name = 'awx.sso'
|
||||
verbose_name = _('Single Sign-On')
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
# Python
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
# Django
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings as django_settings
|
||||
from django.core.signals import setting_changed
|
||||
|
||||
# django-auth-ldap
|
||||
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
|
||||
@@ -23,7 +25,7 @@ from social.backends.saml import SAMLAuth as BaseSAMLAuth
|
||||
from social.backends.saml import SAMLIdentityProvider as BaseSAMLIdentityProvider
|
||||
|
||||
# Ansible Tower
|
||||
from awx.api.license import feature_enabled
|
||||
from awx.conf.license import feature_enabled
|
||||
|
||||
logger = logging.getLogger('awx.sso.backends')
|
||||
|
||||
@@ -43,6 +45,20 @@ class LDAPBackend(BaseLDAPBackend):
|
||||
|
||||
settings_prefix = 'AUTH_LDAP_'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._dispatch_uid = uuid.uuid4()
|
||||
super(LDAPBackend, self).__init__(*args, **kwargs)
|
||||
setting_changed.connect(self._on_setting_changed, dispatch_uid=self._dispatch_uid)
|
||||
|
||||
def __del__(self):
|
||||
setting_changed.disconnect(dispatch_uid=self._dispatch_uid)
|
||||
|
||||
def _on_setting_changed(self, sender, **kwargs):
|
||||
# If any AUTH_LDAP_* setting changes, force settings to be reloaded for
|
||||
# this backend instance.
|
||||
if kwargs.get('setting', '').startswith(self.settings_prefix):
|
||||
self._settings = None
|
||||
|
||||
def _get_settings(self):
|
||||
if self._settings is None:
|
||||
self._settings = LDAPSettings(self.settings_prefix)
|
||||
|
||||
967
awx/sso/conf.py
Normal file
967
awx/sso/conf.py
Normal file
@@ -0,0 +1,967 @@
|
||||
# Python
|
||||
import collections
|
||||
import urlparse
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# Tower
|
||||
from awx.conf import register
|
||||
from awx.sso import fields
|
||||
from awx.main.validators import validate_private_key, validate_certificate
|
||||
from awx.sso.validators import * # noqa
|
||||
|
||||
|
||||
class SocialAuthCallbackURL(object):
|
||||
|
||||
def __init__(self, provider):
|
||||
self.provider = provider
|
||||
|
||||
def __call__(self):
|
||||
path = reverse('social:complete', args=(self.provider,))
|
||||
return urlparse.urljoin(settings.TOWER_URL_BASE, path)
|
||||
|
||||
SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT = _('''\
|
||||
Mapping to organization admins/users from social auth accounts. This setting
|
||||
controls which users are placed into which Tower organizations based on
|
||||
their username and email address. Dictionary keys are organization names.
|
||||
organizations will be created if not present if the license allows for
|
||||
multiple organizations, otherwise the single default organization is used
|
||||
regardless of the key. Values are dictionaries defining the options for
|
||||
each organization's membership. For each organization it is possible to
|
||||
specify which users are automatically users of the organization and also
|
||||
which users can administer the organization.
|
||||
|
||||
- admins: None, True/False, string or list/tuple of strings.
|
||||
If None, organization admins will not be updated.
|
||||
If True, all users using social auth will automatically be added as admins
|
||||
of the organization.
|
||||
If False, no social auth users will be automatically added as admins of
|
||||
the organiation.
|
||||
If a string or list of strings, specifies the usernames and emails for
|
||||
users who will be added to the organization. Strings in the format
|
||||
"/<pattern>/<flags>" will be interpreted as regular expressions and may also
|
||||
be used instead of string literals; only "i" and "m" are supported for flags.
|
||||
- remove_admins: True/False. Defaults to False.
|
||||
If True, a user who does not match will be removed from the organization's
|
||||
administrative list.
|
||||
- users: None, True/False, string or list/tuple of strings. Same rules apply
|
||||
as for admins.
|
||||
- remove_users: True/False. Defaults to False. Same rules as apply for
|
||||
remove_admins.\
|
||||
''')
|
||||
|
||||
# FIXME: /regex/gim (flags)
|
||||
|
||||
SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER = collections.OrderedDict([
|
||||
('Default', collections.OrderedDict([
|
||||
('users', True),
|
||||
])),
|
||||
('Test Org', collections.OrderedDict([
|
||||
('admins', ['admin@example.com']),
|
||||
('users', True),
|
||||
])),
|
||||
('Test Org 2', collections.OrderedDict([
|
||||
('admins', ['admin@example.com', r'/^tower-[^@]+*?@.*$/']),
|
||||
('remove_admins', True),
|
||||
('users', r'/^[^@].*?@example\.com$/i'),
|
||||
('remove_users', True),
|
||||
])),
|
||||
])
|
||||
|
||||
SOCIAL_AUTH_TEAM_MAP_HELP_TEXT = _('''\
|
||||
Mapping of team members (users) from social auth accounts. Keys are team
|
||||
names (will be created if not present). Values are dictionaries of options
|
||||
for each team's membership, where each can contain the following parameters:
|
||||
|
||||
- organization: string. The name of the organization to which the team
|
||||
belongs. The team will be created if the combination of organization and
|
||||
team name does not exist. The organization will first be created if it
|
||||
does not exist. If the license does not allow for multiple organizations,
|
||||
the team will always be assigned to the single default organization.
|
||||
- users: None, True/False, string or list/tuple of strings.
|
||||
If None, team members will not be updated.
|
||||
If True/False, all social auth users will be added/removed as team
|
||||
members.
|
||||
If a string or list of strings, specifies expressions used to match users.
|
||||
User will be added as a team member if the username or email matches.
|
||||
Strings in the format "/<pattern>/<flags>" will be interpreted as regular
|
||||
expressions and may also be used instead of string literals; only "i" and "m"
|
||||
are supported for flags.
|
||||
- remove: True/False. Defaults to False. If True, a user who does not match
|
||||
the rules above will be removed from the team.\
|
||||
''')
|
||||
|
||||
SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER = collections.OrderedDict([
|
||||
('My Team', collections.OrderedDict([
|
||||
('organization', 'Test Org'),
|
||||
('users', [r'/^[^@]+?@test\.example\.com$/']),
|
||||
('remove', True),
|
||||
])),
|
||||
('Other Team', collections.OrderedDict([
|
||||
('organization', 'Test Org 2'),
|
||||
('users', r'/^[^@]+?@test2\.example\.com$/i'),
|
||||
('remove', False),
|
||||
])),
|
||||
])
|
||||
|
||||
###############################################################################
|
||||
# AUTHENTICATION BACKENDS DYNAMIC SETTING
|
||||
###############################################################################
|
||||
|
||||
register(
|
||||
'AUTHENTICATION_BACKENDS',
|
||||
field_class=fields.AuthenticationBackendsField,
|
||||
label=_('Authentication Backends'),
|
||||
help_text=_('List of authentication backends that are enabled based on '
|
||||
'license features and other authentication settings.'),
|
||||
read_only=True,
|
||||
depends_on=fields.AuthenticationBackendsField.get_all_required_settings(),
|
||||
category=_('Authentication'),
|
||||
category_slug='authentication',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_ORGANIZATION_MAP',
|
||||
field_class=fields.SocialOrganizationMapField,
|
||||
default={},
|
||||
label=_('Social Auth Organization Map'),
|
||||
help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT,
|
||||
category=_('Authentication'),
|
||||
category_slug='authentication',
|
||||
placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_TEAM_MAP',
|
||||
field_class=fields.SocialTeamMapField,
|
||||
default={},
|
||||
label=_('Social Auth Team Map'),
|
||||
help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT,
|
||||
category=_('Authentication'),
|
||||
category_slug='authentication',
|
||||
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_USER_FIELDS',
|
||||
field_class=fields.StringListField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('Social Auth User Fields'),
|
||||
help_text=_('When set to an empty list `[]`, this setting prevents new user '
|
||||
'accounts from being created. Only users who have previously '
|
||||
'logged in using social auth or have a user account with a '
|
||||
'matching email address will be able to login.'),
|
||||
category=_('Authentication'),
|
||||
category_slug='authentication',
|
||||
placeholder=['username', 'email'],
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# LDAP AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_SERVER_URI',
|
||||
field_class=fields.URLField,
|
||||
schemes=('ldap', 'ldaps'),
|
||||
allow_blank=True,
|
||||
label=_('LDAP Server URI'),
|
||||
help_text=_('URI to connect to LDAP server, such as "ldap://ldap.example.com:389" '
|
||||
'(non-SSL) or "ldaps://ldap.example.com:636" (SSL). LDAP authentication '
|
||||
'is disabled if this parameter is empty or your license does not '
|
||||
'enable LDAP support.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder='ldaps://ldap.example.com:636',
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_BIND_DN',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
validators=[validate_ldap_bind_dn],
|
||||
label=_('LDAP Bind DN'),
|
||||
help_text=_('DN (Distinguished Name) of user to bind for all search queries. '
|
||||
'Normally in the format "CN=Some User,OU=Users,DC=example,DC=com" '
|
||||
'but may also be specified as "DOMAIN\username" for Active Directory. '
|
||||
'This is the system user account we will use to login to query LDAP '
|
||||
'for other user information.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_BIND_PASSWORD',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('LDAP Bind Password'),
|
||||
help_text=_('Password used to bind LDAP user account.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_START_TLS',
|
||||
field_class=fields.BooleanField,
|
||||
default=False,
|
||||
label=_('LDAP Start TLS'),
|
||||
help_text=_('Whether to enable TLS when the LDAP connection is not using SSL.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_CONNECTION_OPTIONS',
|
||||
field_class=fields.LDAPConnectionOptionsField,
|
||||
default={'OPT_REFERRALS': 0},
|
||||
label=_('LDAP Connection Options'),
|
||||
help_text=_('Additional options to set for the LDAP connection. LDAP '
|
||||
'referrals are disabled by default (to prevent certain LDAP '
|
||||
'queries from hanging with AD). Option names should be strings '
|
||||
'(e.g. "OPT_REFERRALS"). Refer to '
|
||||
'https://www.python-ldap.org/doc/html/ldap.html#options for '
|
||||
'possible options and values that can be set.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder=collections.OrderedDict([
|
||||
('OPT_REFERRALS', 0),
|
||||
]),
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_USER_SEARCH',
|
||||
field_class=fields.LDAPSearchUnionField,
|
||||
default=[],
|
||||
label=_('LDAP User Search'),
|
||||
help_text=_('LDAP search query to find users. Any user that matches the given '
|
||||
'pattern will be able to login to Tower. The user should also be '
|
||||
'mapped into an Tower organization (as defined in the '
|
||||
'AUTH_LDAP_ORGANIZATION_MAP setting). If multiple search queries '
|
||||
'need to be supported use of "LDAPUnion" is possible. See '
|
||||
'python-ldap documentation as linked at the top of this section.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder=(
|
||||
'OU=Users,DC=example,DC=com',
|
||||
'SCOPE_SUBTREE',
|
||||
'(sAMAccountName=%(user)s)',
|
||||
),
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_USER_DN_TEMPLATE',
|
||||
field_class=fields.LDAPDNWithUserField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('LDAP User DN Template'),
|
||||
help_text=_('Alternative to user search, if user DNs are all of the same '
|
||||
'format. This approach will be more efficient for user lookups than '
|
||||
'searching if it is usable in your organizational environment. If '
|
||||
'this setting has a value it will be used instead of '
|
||||
'AUTH_LDAP_USER_SEARCH.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder='uid=%(user)s,OU=Users,DC=example,DC=com',
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_USER_ATTR_MAP',
|
||||
field_class=fields.LDAPUserAttrMapField,
|
||||
default={},
|
||||
label=_('LDAP User Attribute Map'),
|
||||
help_text=_('Mapping of LDAP user schema to Tower API user atrributes (key is '
|
||||
'user attribute name, value is LDAP attribute name). The default '
|
||||
'setting is valid for ActiveDirectory but users with other LDAP '
|
||||
'configurations may need to change the values (not the keys) of '
|
||||
'the dictionary/hash-table.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder=collections.OrderedDict([
|
||||
('first_name', 'givenName'),
|
||||
('last_name', 'sn'),
|
||||
('email', 'mail'),
|
||||
]),
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_GROUP_SEARCH',
|
||||
field_class=fields.LDAPSearchField,
|
||||
default=[],
|
||||
label=_('LDAP Group Search'),
|
||||
help_text=_('Users in Tower are mapped to organizations based on their '
|
||||
'membership in LDAP groups. This setting defines the LDAP search '
|
||||
'query to find groups. Note that this, unlike the user search '
|
||||
'above, does not support LDAPSearchUnion.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder=(
|
||||
'DC=example,DC=com',
|
||||
'SCOPE_SUBTREE',
|
||||
'(objectClass=group)',
|
||||
),
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_GROUP_TYPE',
|
||||
field_class=fields.LDAPGroupTypeField,
|
||||
label=_('LDAP Group Type'),
|
||||
help_text=_('The group type may need to be changed based on the type of the '
|
||||
'LDAP server. Values are listed at: '
|
||||
'http://pythonhosted.org/django-auth-ldap/groups.html#types-of-groups'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_REQUIRE_GROUP',
|
||||
field_class=fields.LDAPDNField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('LDAP Require Group'),
|
||||
help_text=_('Group DN required to login. If specified, user must be a member '
|
||||
'of this group to login via LDAP. If not set, everyone in LDAP '
|
||||
'that matches the user search will be able to login via Tower. '
|
||||
'Only one require group is supported.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder='CN=Tower Users,OU=Users,DC=example,DC=com',
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_DENY_GROUP',
|
||||
field_class=fields.LDAPDNField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('LDAP Deny Group'),
|
||||
help_text=_('Group DN denied from login. If specified, user will not be '
|
||||
'allowed to login if a member of this group. Only one deny group '
|
||||
'is supported.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder='CN=Disabled Users,OU=Users,DC=example,DC=com',
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_USER_FLAGS_BY_GROUP',
|
||||
field_class=fields.LDAPUserFlagsField,
|
||||
default={},
|
||||
label=_('LDAP User Flags By Group'),
|
||||
help_text=_('User profile flags updated from group membership (key is user '
|
||||
'attribute name, value is group DN). These are boolean fields '
|
||||
'that are matched based on whether the user is a member of the '
|
||||
'given group. So far only is_superuser is settable via this '
|
||||
'method. This flag is set both true and false at login time '
|
||||
'based on current LDAP settings.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder=collections.OrderedDict([
|
||||
('is_superuser', 'CN=Domain Admins,CN=Users,DC=example,DC=com'),
|
||||
]),
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_ORGANIZATION_MAP',
|
||||
field_class=fields.LDAPOrganizationMapField,
|
||||
default={},
|
||||
label=_('LDAP Organization Map'),
|
||||
help_text=_('Mapping between organization admins/users and LDAP groups. This '
|
||||
'controls what users are placed into what Tower organizations '
|
||||
'relative to their LDAP group memberships. Keys are organization '
|
||||
'names. Organizations will be created if not present. Values are '
|
||||
'dictionaries defining the options for each organization\'s '
|
||||
'membership. For each organization it is possible to specify '
|
||||
'what groups are automatically users of the organization and also '
|
||||
'what groups can administer the organization.\n\n'
|
||||
' - admins: None, True/False, string or list of strings.\n'
|
||||
' If None, organization admins will not be updated based on '
|
||||
'LDAP values.\n'
|
||||
' If True, all users in LDAP will automatically be added as '
|
||||
'admins of the organization.\n'
|
||||
' If False, no LDAP users will be automatically added as admins '
|
||||
'of the organiation.\n'
|
||||
' If a string or list of strings, specifies the group DN(s) '
|
||||
'that will be added of the organization if they match any of the '
|
||||
'specified groups.\n'
|
||||
' - remove_admins: True/False. Defaults to True.\n'
|
||||
' If True, a user who is not an member of the given groups will '
|
||||
'be removed from the organization\'s administrative list.\n'
|
||||
' - users: None, True/False, string or list/tuple of strings. '
|
||||
'Same rules apply as for admins.\n'
|
||||
' - remove_users: True/False. Defaults to True. Same rules apply '
|
||||
'as for remove_admins.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder=collections.OrderedDict([
|
||||
('Test Org', collections.OrderedDict([
|
||||
('admins', 'CN=Domain Admins,CN=Users,DC=example,DC=com'),
|
||||
('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']),
|
||||
('remove_users', True),
|
||||
('remove_admins', True),
|
||||
])),
|
||||
('Test Org 2', collections.OrderedDict([
|
||||
('admins', 'CN=Administrators,CN=Builtin,DC=example,DC=com'),
|
||||
('users', True),
|
||||
('remove_users', True),
|
||||
('remove_admins', True),
|
||||
])),
|
||||
]),
|
||||
)
|
||||
|
||||
register(
|
||||
'AUTH_LDAP_TEAM_MAP',
|
||||
field_class=fields.LDAPTeamMapField,
|
||||
default={},
|
||||
label=_('LDAP Team Map'),
|
||||
help_text=_('Mapping between team members (users) and LDAP groups. Keys are '
|
||||
'team names (will be created if not present). Values are '
|
||||
'dictionaries of options for each team\'s membership, where each '
|
||||
'can contain the following parameters:\n\n'
|
||||
' - organization: string. The name of the organization to which '
|
||||
'the team belongs. The team will be created if the combination of '
|
||||
'organization and team name does not exist. The organization will '
|
||||
'first be created if it does not exist.\n'
|
||||
' - users: None, True/False, string or list/tuple of strings.\n'
|
||||
' If None, team members will not be updated.\n'
|
||||
' If True/False, all LDAP users will be added/removed as team '
|
||||
'members.\n'
|
||||
' If a string or list of strings, specifies the group DN(s). '
|
||||
'User will be added as a team member if the user is a member of '
|
||||
'ANY of these groups.\n'
|
||||
'- remove: True/False. Defaults to False. If True, a user who is '
|
||||
'not a member of the given groups will be removed from the team.'),
|
||||
category=_('LDAP'),
|
||||
category_slug='ldap',
|
||||
placeholder=collections.OrderedDict([
|
||||
('My Team', collections.OrderedDict([
|
||||
('organization', 'Test Org'),
|
||||
('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']),
|
||||
('remove', True),
|
||||
])),
|
||||
('Other Team', collections.OrderedDict([
|
||||
('organization', 'Test Org 2'),
|
||||
('users', 'CN=Other Users,CN=Users,DC=example,DC=com'),
|
||||
('remove', False),
|
||||
])),
|
||||
]),
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# RADIUS AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
register(
|
||||
'RADIUS_SERVER',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('RADIUS Server'),
|
||||
help_text=_('Hostname/IP of RADIUS server. RADIUS authentication will be '
|
||||
'disabled if this setting is empty.'),
|
||||
category=_('RADIUS'),
|
||||
category_slug='radius',
|
||||
placeholder='radius.example.com',
|
||||
)
|
||||
|
||||
register(
|
||||
'RADIUS_PORT',
|
||||
field_class=fields.IntegerField,
|
||||
min_value=1,
|
||||
max_value=65535,
|
||||
default=1812,
|
||||
label=_('RADIUS Port'),
|
||||
help_text=_('Port of RADIUS server.'),
|
||||
category=_('RADIUS'),
|
||||
category_slug='radius',
|
||||
)
|
||||
|
||||
register(
|
||||
'RADIUS_SECRET',
|
||||
field_class=fields.RADIUSSecretField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('RADIUS Secret'),
|
||||
help_text=_('Shared secret for authenticating to RADIUS server.'),
|
||||
category=_('RADIUS'),
|
||||
category_slug='radius',
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# GOOGLE OAUTH2 AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL',
|
||||
field_class=fields.CharField,
|
||||
read_only=True,
|
||||
default=SocialAuthCallbackURL('google-oauth2'),
|
||||
label=_('Google OAuth2 Callback URL'),
|
||||
help_text=_('Create a project at https://console.developers.google.com/ to '
|
||||
'obtain an OAuth2 key and secret for a web application. Ensure '
|
||||
'that the Google+ API is enabled. Provide this URL as the '
|
||||
'callback URL for your application.'),
|
||||
category=_('Google OAuth2'),
|
||||
category_slug='google-oauth2',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('Google OAuth2 Key'),
|
||||
help_text=_('The OAuth2 key from your web application at https://console.developers.google.com/.'),
|
||||
category=_('Google OAuth2'),
|
||||
category_slug='google-oauth2',
|
||||
placeholder='528620852399-gm2dt4hrl2tsj67fqamk09k1e0ad6gd8.apps.googleusercontent.com',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('Google OAuth2 Secret'),
|
||||
help_text=_('The OAuth2 secret from your web application at https://console.developers.google.com/.'),
|
||||
category=_('Google OAuth2'),
|
||||
category_slug='google-oauth2',
|
||||
placeholder='q2fMVCmEregbg-drvebPp8OW',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS',
|
||||
field_class=fields.StringListField,
|
||||
default=[],
|
||||
label=_('Google OAuth2 Whitelisted Domains'),
|
||||
help_text=_('Update this setting to restrict the domains who are allowed to '
|
||||
'login using Google OAuth2.'),
|
||||
category=_('Google OAuth2'),
|
||||
category_slug='google-oauth2',
|
||||
placeholder=['example.com'],
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS',
|
||||
field_class=fields.DictField,
|
||||
default={},
|
||||
label=_('Google OAuth2 Extra Arguments'),
|
||||
help_text=_('Extra arguments for Google OAuth2 login. When only allowing a '
|
||||
'single domain to authenticate, set to `{"hd": "yourdomain.com"}` '
|
||||
'and Google will not display any other accounts even if the user '
|
||||
'is logged in with multiple Google accounts.'),
|
||||
category=_('Google OAuth2'),
|
||||
category_slug='google-oauth2',
|
||||
placeholder={'hd': 'example.com'},
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP',
|
||||
field_class=fields.SocialOrganizationMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('Google OAuth2 Organization Map'),
|
||||
help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT,
|
||||
category=_('Google OAuth2'),
|
||||
category_slug='google-oauth2',
|
||||
placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP',
|
||||
field_class=fields.SocialTeamMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('Google OAuth2 Team Map'),
|
||||
help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT,
|
||||
category=_('Google OAuth2'),
|
||||
category_slug='google-oauth2',
|
||||
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# GITHUB OAUTH2 AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_CALLBACK_URL',
|
||||
field_class=fields.CharField,
|
||||
read_only=True,
|
||||
default=SocialAuthCallbackURL('github'),
|
||||
label=_('GitHub OAuth2 Callback URL'),
|
||||
help_text=_('Create a developer application at '
|
||||
'https://github.com/settings/developers to obtain an OAuth2 '
|
||||
'key (Client ID) and secret (Client Secret). Provide this URL '
|
||||
'as the callback URL for your application.'),
|
||||
category=_('GitHub OAuth2'),
|
||||
category_slug='github',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_KEY',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('GitHub OAuth2 Key'),
|
||||
help_text=_('The OAuth2 key (Client ID) from your GitHub developer application.'),
|
||||
category=_('GitHub OAuth2'),
|
||||
category_slug='github',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_SECRET',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('GitHub OAuth2 Secret'),
|
||||
help_text=_('The OAuth2 secret (Client Secret) from your GitHub developer application.'),
|
||||
category=_('GitHub OAuth2'),
|
||||
category_slug='github',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP',
|
||||
field_class=fields.SocialOrganizationMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('GitHub OAuth2 Organization Map'),
|
||||
help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT,
|
||||
category=_('GitHub OAuth2'),
|
||||
category_slug='github',
|
||||
placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_MAP',
|
||||
field_class=fields.SocialTeamMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('GitHub OAuth2 Team Map'),
|
||||
help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT,
|
||||
category=_('GitHub OAuth2'),
|
||||
category_slug='github',
|
||||
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# GITHUB ORG OAUTH2 AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL',
|
||||
field_class=fields.CharField,
|
||||
read_only=True,
|
||||
default=SocialAuthCallbackURL('github-org'),
|
||||
label=_('GitHub Organization OAuth2 Callback URL'),
|
||||
help_text=_('Create an organization-owned application at '
|
||||
'https://github.com/organizations/<yourorg>/settings/applications '
|
||||
'and obtain an OAuth2 key (Client ID) and secret (Client Secret). '
|
||||
'Provide this URL as the callback URL for your application.'),
|
||||
category=_('GitHub Organization OAuth2'),
|
||||
category_slug='github-org',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_ORG_KEY',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('GitHub Organization OAuth2 Key'),
|
||||
help_text=_('The OAuth2 key (Client ID) from your GitHub organization application.'),
|
||||
category=_('GitHub Organization OAuth2'),
|
||||
category_slug='github-org',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_ORG_SECRET',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('GitHub Organization OAuth2 Secret'),
|
||||
help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'),
|
||||
category=_('GitHub Organization OAuth2'),
|
||||
category_slug='github-org',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_ORG_NAME',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('GitHub Organization Name'),
|
||||
help_text=_('The name of your GitHub organization, as used in your '
|
||||
'organization\'s URL: https://github.com/<yourorg>/.'),
|
||||
category=_('GitHub Organization OAuth2'),
|
||||
category_slug='github-org',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP',
|
||||
field_class=fields.SocialOrganizationMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('GitHub Organization OAuth2 Organization Map'),
|
||||
help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT,
|
||||
category=_('GitHub Organization OAuth2'),
|
||||
category_slug='github-org',
|
||||
placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP',
|
||||
field_class=fields.SocialTeamMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('GitHub Organization OAuth2 Team Map'),
|
||||
help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT,
|
||||
category=_('GitHub Organization OAuth2'),
|
||||
category_slug='github-org',
|
||||
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# GITHUB TEAM OAUTH2 AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL',
|
||||
field_class=fields.CharField,
|
||||
read_only=True,
|
||||
default=SocialAuthCallbackURL('github-team'),
|
||||
label=_('GitHub Team OAuth2 Callback URL'),
|
||||
help_text=_('Create an organization-owned application at '
|
||||
'https://github.com/organizations/<yourorg>/settings/applications '
|
||||
'and obtain an OAuth2 key (Client ID) and secret (Client Secret). '
|
||||
'Provide this URL as the callback URL for your application.'),
|
||||
category=_('GitHub Team OAuth2'),
|
||||
category_slug='github-team',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_KEY',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('GitHub Team OAuth2 Key'),
|
||||
help_text=_('The OAuth2 key (Client ID) from your GitHub organization application.'),
|
||||
category=_('GitHub Team OAuth2'),
|
||||
category_slug='github-team',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_SECRET',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('GitHub Team OAuth2 Secret'),
|
||||
help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'),
|
||||
category=_('GitHub Team OAuth2'),
|
||||
category_slug='github-team',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_ID',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
label=_('GitHub Team ID'),
|
||||
help_text=_('Find the numeric team ID using the Github API: '
|
||||
'http://fabian-kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/.'),
|
||||
category=_('GitHub Team OAuth2'),
|
||||
category_slug='github-team',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP',
|
||||
field_class=fields.SocialOrganizationMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('GitHub Team OAuth2 Organization Map'),
|
||||
help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT,
|
||||
category=_('GitHub Team OAuth2'),
|
||||
category_slug='github-team',
|
||||
placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP',
|
||||
field_class=fields.SocialTeamMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('GitHub Team OAuth2 Team Map'),
|
||||
help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT,
|
||||
category=_('GitHub Team OAuth2'),
|
||||
category_slug='github-team',
|
||||
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# SAML AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
def get_saml_metadata_url():
|
||||
return urlparse.urljoin(settings.TOWER_URL_BASE, reverse('sso:saml_metadata'))
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_CALLBACK_URL',
|
||||
field_class=fields.CharField,
|
||||
read_only=True,
|
||||
default=SocialAuthCallbackURL('saml'),
|
||||
label=_('SAML Service Provider Callback URL'),
|
||||
help_text=_('Register Tower as a service provider (SP) with each identity '
|
||||
'provider (IdP) you have configured. Provide your SP Entity ID '
|
||||
'and this callback URL for your application.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_METADATA_URL',
|
||||
field_class=fields.CharField,
|
||||
read_only=True,
|
||||
default=get_saml_metadata_url,
|
||||
label=_('SAML Service Provider Metadata URL'),
|
||||
help_text=_('If your identity provider (IdP) allows uploading an XML '
|
||||
'metadata file, you can download one from this URL.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_SP_ENTITY_ID',
|
||||
field_class=fields.URLField,
|
||||
schemes=('http', 'https'),
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('SAML Service Provider Entity ID'),
|
||||
help_text=_('Set to a URL for a domain name you own (does not need to be a '
|
||||
'valid URL; only used as a unique ID).'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
validators=[validate_certificate],
|
||||
label=_('SAML Service Provider Public Certificate'),
|
||||
help_text=_('Create a keypair for Tower to use as a service provider (SP) '
|
||||
'and include the certificate content here.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
validators=[validate_private_key],
|
||||
label=_('SAML Service Provider Private Key'),
|
||||
help_text=_('Create a keypair for Tower to use as a service provider (SP) '
|
||||
'and include the private key content here.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_ORG_INFO',
|
||||
field_class=fields.SAMLOrgInfoField,
|
||||
default={},
|
||||
label=_('SAML Service Provider Organization Info'),
|
||||
help_text=_('Configure this setting with information about your app.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=collections.OrderedDict([
|
||||
('en-US', collections.OrderedDict([
|
||||
('name', 'example'),
|
||||
('displayname', 'Example'),
|
||||
('url', 'http://www.example.com'),
|
||||
])),
|
||||
]),
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_TECHNICAL_CONTACT',
|
||||
field_class=fields.SAMLContactField,
|
||||
default={},
|
||||
label=_('SAML Service Provider Technical Contact'),
|
||||
help_text=_('Configure this setting with your contact information.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=collections.OrderedDict([
|
||||
('givenName', 'Technical Contact'),
|
||||
('emailAddress', 'techsup@example.com'),
|
||||
]),
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_SUPPORT_CONTACT',
|
||||
field_class=fields.SAMLContactField,
|
||||
default={},
|
||||
label=_('SAML Service Provider Support Contact'),
|
||||
help_text=_('Configure this setting with your contact information.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=collections.OrderedDict([
|
||||
('givenName', 'Support Contact'),
|
||||
('emailAddress', 'support@example.com'),
|
||||
]),
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_ENABLED_IDPS',
|
||||
field_class=fields.SAMLEnabledIdPsField,
|
||||
default={},
|
||||
label=_('SAML Enabled Identity Providers'),
|
||||
help_text=_('Configure the Entity ID, SSO URL and certificate for each '
|
||||
'identity provider (IdP) in use. Multiple SAML IdPs are supported. '
|
||||
'Some IdPs may provide user data using attribute names that differ '
|
||||
'from the default OIDs '
|
||||
'(https://github.com/omab/python-social-auth/blob/master/social/backends/saml.py#L16). '
|
||||
'Attribute names may be overridden for each IdP.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=collections.OrderedDict([
|
||||
('Okta', collections.OrderedDict([
|
||||
('entity_id', 'http://www.okta.com/HHniyLkaxk9e76wD0Thh'),
|
||||
('url', 'https://dev-123456.oktapreview.com/app/ansibletower/HHniyLkaxk9e76wD0Thh/sso/saml'),
|
||||
('x509cert', 'MIIDpDCCAoygAwIBAgIGAVVZ4rPzMA0GCSqGSIb3...'),
|
||||
('attr_user_permanent_id', 'username'),
|
||||
('attr_first_name', 'first_name'),
|
||||
('attr_last_name', 'last_name'),
|
||||
('attr_username', 'username'),
|
||||
('attr_email', 'email'),
|
||||
])),
|
||||
('OneLogin', collections.OrderedDict([
|
||||
('entity_id', 'https://app.onelogin.com/saml/metadata/123456'),
|
||||
('url', 'https://example.onelogin.com/trust/saml2/http-post/sso/123456'),
|
||||
('x509cert', 'MIIEJjCCAw6gAwIBAgIUfuSD54OPSBhndDHh3gZo...'),
|
||||
('attr_user_permanent_id', 'name_id'),
|
||||
('attr_first_name', 'User.FirstName'),
|
||||
('attr_last_name', 'User.LastName'),
|
||||
('attr_username', 'User.email'),
|
||||
('attr_email', 'User.email'),
|
||||
])),
|
||||
]),
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_ORGANIZATION_MAP',
|
||||
field_class=fields.SocialOrganizationMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('SAML Organization Map'),
|
||||
help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT,
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_TEAM_MAP',
|
||||
field_class=fields.SocialTeamMapField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('SAML Team Map'),
|
||||
help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT,
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
|
||||
)
|
||||
598
awx/sso/fields.py
Normal file
598
awx/sso/fields.py
Normal file
@@ -0,0 +1,598 @@
|
||||
# Python LDAP
|
||||
import ldap
|
||||
|
||||
# Django
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# Django Auth LDAP
|
||||
import django_auth_ldap.config
|
||||
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
|
||||
|
||||
# Tower
|
||||
from awx.conf import fields
|
||||
from awx.conf.fields import * # noqa
|
||||
from awx.conf.license import feature_enabled
|
||||
from awx.main.validators import validate_certificate
|
||||
from awx.sso.validators import * # noqa
|
||||
|
||||
|
||||
def get_subclasses(cls):
|
||||
for subclass in cls.__subclasses__():
|
||||
for subsubclass in get_subclasses(subclass):
|
||||
yield subsubclass
|
||||
yield subclass
|
||||
|
||||
|
||||
class AuthenticationBackendsField(fields.StringListField):
|
||||
|
||||
# Mapping of settings that must be set in order to enable each
|
||||
# authentication backend.
|
||||
REQUIRED_BACKEND_SETTINGS = collections.OrderedDict([
|
||||
('awx.sso.backends.LDAPBackend', [
|
||||
'AUTH_LDAP_SERVER_URI',
|
||||
]),
|
||||
('awx.sso.backends.RADIUSBackend', [
|
||||
'RADIUS_SERVER',
|
||||
]),
|
||||
('social.backends.google.GoogleOAuth2', [
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY',
|
||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET',
|
||||
]),
|
||||
('social.backends.github.GithubOAuth2', [
|
||||
'SOCIAL_AUTH_GITHUB_KEY',
|
||||
'SOCIAL_AUTH_GITHUB_SECRET',
|
||||
]),
|
||||
('social.backends.github.GithubOrganizationOAuth2', [
|
||||
'SOCIAL_AUTH_GITHUB_ORG_KEY',
|
||||
'SOCIAL_AUTH_GITHUB_ORG_SECRET',
|
||||
'SOCIAL_AUTH_GITHUB_ORG_NAME',
|
||||
]),
|
||||
('social.backends.github.GithubTeamOAuth2', [
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_KEY',
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_SECRET',
|
||||
'SOCIAL_AUTH_GITHUB_TEAM_ID',
|
||||
]),
|
||||
('awx.sso.backends.SAMLAuth', [
|
||||
'SOCIAL_AUTH_SAML_SP_ENTITY_ID',
|
||||
'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT',
|
||||
'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY',
|
||||
'SOCIAL_AUTH_SAML_ORG_INFO',
|
||||
'SOCIAL_AUTH_SAML_TECHNICAL_CONTACT',
|
||||
'SOCIAL_AUTH_SAML_SUPPORT_CONTACT',
|
||||
'SOCIAL_AUTH_SAML_ENABLED_IDPS',
|
||||
]),
|
||||
('django.contrib.auth.backends.ModelBackend', []),
|
||||
])
|
||||
|
||||
REQUIRED_BACKEND_FEATURE = {
|
||||
'awx.sso.backends.LDAPBackend': 'ldap',
|
||||
'awx.sso.backends.RADIUSBackend': 'enterprise_auth',
|
||||
'awx.sso.backends.SAMLAuth': 'enterprise_auth',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_all_required_settings(cls):
|
||||
all_required_settings = set(['LICENSE'])
|
||||
for required_settings in cls.REQUIRED_BACKEND_SETTINGS.values():
|
||||
all_required_settings.update(required_settings)
|
||||
return all_required_settings
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('default', self._default_from_required_settings)
|
||||
super(AuthenticationBackendsField, self).__init__(*args, **kwargs)
|
||||
|
||||
def _default_from_required_settings(self):
|
||||
from django.conf import settings
|
||||
try:
|
||||
backends = settings._awx_conf_settings._get_default('AUTHENTICATION_BACKENDS')
|
||||
except AttributeError:
|
||||
backends = self.REQUIRED_BACKEND_SETTINGS.keys()
|
||||
# Filter which authentication backends are enabled based on their
|
||||
# required settings being defined and non-empty. Also filter available
|
||||
# backends based on license features.
|
||||
for backend, required_settings in self.REQUIRED_BACKEND_SETTINGS.items():
|
||||
if backend not in backends:
|
||||
continue
|
||||
required_feature = self.REQUIRED_BACKEND_FEATURE.get(backend, '')
|
||||
if not required_feature or feature_enabled(required_feature):
|
||||
if all([getattr(settings, rs, None) for rs in required_settings]):
|
||||
continue
|
||||
backends = filter(lambda x: x != backend, backends)
|
||||
return backends
|
||||
|
||||
|
||||
class LDAPConnectionOptionsField(fields.DictField):
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_options': _('Invalid connection option(s): {invalid_options}.'),
|
||||
}
|
||||
|
||||
def to_representation(self, value):
|
||||
value = value or {}
|
||||
opt_names = ldap.OPT_NAMES_DICT
|
||||
# Convert integer options to their named constants.
|
||||
repr_value = {}
|
||||
for opt, opt_value in value.items():
|
||||
if opt in opt_names:
|
||||
repr_value[opt_names[opt]] = opt_value
|
||||
return repr_value
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(LDAPConnectionOptionsField, self).to_internal_value(data)
|
||||
valid_options = dict([(v, k) for k, v in ldap.OPT_NAMES_DICT.items()])
|
||||
invalid_options = set(data.keys()) - set(valid_options.keys())
|
||||
if invalid_options:
|
||||
options_display = json.dumps(list(invalid_options)).lstrip('[').rstrip(']')
|
||||
self.fail('invalid_options', invalid_options=options_display)
|
||||
# Convert named options to their integer constants.
|
||||
internal_data = {}
|
||||
for opt_name, opt_value in data.items():
|
||||
internal_data[valid_options[opt_name]] = opt_value
|
||||
return internal_data
|
||||
|
||||
|
||||
class LDAPDNField(fields.CharField):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(LDAPDNField, self).__init__(**kwargs)
|
||||
self.validators.append(validate_ldap_dn)
|
||||
|
||||
|
||||
class LDAPDNWithUserField(fields.CharField):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(LDAPDNWithUserField, self).__init__(**kwargs)
|
||||
self.validators.append(validate_ldap_dn_with_user)
|
||||
|
||||
|
||||
class LDAPFilterField(fields.CharField):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(LDAPFilterField, self).__init__(**kwargs)
|
||||
self.validators.append(validate_ldap_filter)
|
||||
|
||||
|
||||
class LDAPFilterWithUserField(fields.CharField):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(LDAPFilterWithUserField, self).__init__(**kwargs)
|
||||
self.validators.append(validate_ldap_filter_with_user)
|
||||
|
||||
|
||||
class LDAPScopeField(fields.ChoiceField):
|
||||
|
||||
def __init__(self, choices=None, **kwargs):
|
||||
choices = choices or [
|
||||
('SCOPE_BASE', _('Base')),
|
||||
('SCOPE_ONELEVEL', _('One Level')),
|
||||
('SCOPE_SUBTREE', _('Subtree')),
|
||||
]
|
||||
super(LDAPScopeField, self).__init__(choices, **kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
for choice in self.choices.keys():
|
||||
if value == getattr(ldap, choice):
|
||||
return choice
|
||||
return super(LDAPScopeField, self).to_representation(value)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
value = super(LDAPScopeField, self).to_internal_value(data)
|
||||
return getattr(ldap, value)
|
||||
|
||||
|
||||
class LDAPSearchField(fields.ListField):
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_length': _('Expected a list of three items but got {length} instead.'),
|
||||
'type_error': _('Expected an instance of LDAPSearch but got {input_type} instead.'),
|
||||
}
|
||||
ldap_filter_field_class = LDAPFilterField
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value:
|
||||
return []
|
||||
if not isinstance(value, LDAPSearch):
|
||||
self.fail('type_error', input_type=type(value))
|
||||
return [
|
||||
LDAPDNField().to_representation(value.base_dn),
|
||||
LDAPScopeField().to_representation(value.scope),
|
||||
self.ldap_filter_field_class().to_representation(value.filterstr),
|
||||
]
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(LDAPSearchField, self).to_internal_value(data)
|
||||
if len(data) == 0:
|
||||
return None
|
||||
if len(data) != 3:
|
||||
self.fail('invalid_length', length=len(data))
|
||||
return LDAPSearch(
|
||||
LDAPDNField().run_validation(data[0]),
|
||||
LDAPScopeField().run_validation(data[1]),
|
||||
self.ldap_filter_field_class().run_validation(data[2]),
|
||||
)
|
||||
|
||||
|
||||
class LDAPSearchWithUserField(LDAPSearchField):
|
||||
|
||||
ldap_filter_field_class = LDAPFilterWithUserField
|
||||
|
||||
|
||||
class LDAPSearchUnionField(fields.ListField):
|
||||
|
||||
default_error_messages = {
|
||||
'type_error': _('Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} instead.'),
|
||||
}
|
||||
ldap_search_field_class = LDAPSearchWithUserField
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value:
|
||||
return []
|
||||
elif isinstance(value, LDAPSearchUnion):
|
||||
return [self.ldap_search_field_class().to_representation(s) for s in value.searches]
|
||||
elif isinstance(value, LDAPSearch):
|
||||
return self.ldap_search_field_class().to_representation(value)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(value))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(LDAPSearchUnionField, self).to_internal_value(data)
|
||||
if len(data) == 0:
|
||||
return None
|
||||
if len(data) == 3 and isinstance(data[0], basestring):
|
||||
return self.ldap_search_field_class().run_validation(data)
|
||||
else:
|
||||
return LDAPSearchUnion(*[self.ldap_search_field_class().run_validation(x) for x in data])
|
||||
|
||||
|
||||
class LDAPUserAttrMapField(fields.DictField):
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_attrs': _('Invalid user attribute(s): {invalid_attrs}.'),
|
||||
}
|
||||
valid_user_attrs = {'first_name', 'last_name', 'email'}
|
||||
child = fields.CharField()
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(LDAPUserAttrMapField, self).to_internal_value(data)
|
||||
invalid_attrs = (set(data.keys()) - self.valid_user_attrs)
|
||||
if invalid_attrs:
|
||||
attrs_display = json.dumps(list(invalid_attrs)).lstrip('[').rstrip(']')
|
||||
self.fail('invalid_attrs', invalid_attrs=attrs_display)
|
||||
return data
|
||||
|
||||
|
||||
class LDAPGroupTypeField(fields.ChoiceField):
|
||||
|
||||
default_error_messages = {
|
||||
'type_error': _('Expected an instance of LDAPGroupType but got {input_type} instead.'),
|
||||
}
|
||||
|
||||
def __init__(self, choices=None, **kwargs):
|
||||
group_types = get_subclasses(django_auth_ldap.config.LDAPGroupType)
|
||||
choices = choices or [(x.__name__, x.__name__) for x in group_types]
|
||||
super(LDAPGroupTypeField, self).__init__(choices, **kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value:
|
||||
return ''
|
||||
if not isinstance(value, django_auth_ldap.config.LDAPGroupType):
|
||||
self.fail('type_error', input_type=type(value))
|
||||
return value.__class__.__name__
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(LDAPGroupTypeField, self).to_internal_value(data)
|
||||
if not data:
|
||||
return None
|
||||
return getattr(django_auth_ldap.config, data)()
|
||||
|
||||
|
||||
class LDAPUserFlagsField(fields.DictField):
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_flag': _('Invalid user flag: "{invalid_flag}".'),
|
||||
}
|
||||
valid_user_flags = {'is_superuser'}
|
||||
child = LDAPDNField()
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(LDAPUserFlagsField, self).to_internal_value(data)
|
||||
invalid_flags = (set(data.keys()) - self.valid_user_flags)
|
||||
if invalid_flags:
|
||||
self.fail('invalid_flag', invalid_flag=list(invalid_flags)[0])
|
||||
return data
|
||||
|
||||
|
||||
class LDAPDNMapField(fields.ListField):
|
||||
|
||||
default_error_messages = {
|
||||
'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.'),
|
||||
}
|
||||
child = LDAPDNField()
|
||||
|
||||
def to_representation(self, value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return super(LDAPDNMapField, self).to_representation(value)
|
||||
elif value in fields.NullBooleanField.TRUE_VALUES:
|
||||
return True
|
||||
elif value in fields.NullBooleanField.FALSE_VALUES:
|
||||
return False
|
||||
elif value in fields.NullBooleanField.NULL_VALUES:
|
||||
return None
|
||||
elif isinstance(value, basestring):
|
||||
return self.child.to_representation(value)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(value))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, (list, tuple)):
|
||||
return super(LDAPDNMapField, self).to_internal_value(data)
|
||||
elif data in fields.NullBooleanField.TRUE_VALUES:
|
||||
return True
|
||||
elif data in fields.NullBooleanField.FALSE_VALUES:
|
||||
return False
|
||||
elif data in fields.NullBooleanField.NULL_VALUES:
|
||||
return None
|
||||
elif isinstance(data, basestring):
|
||||
return self.child.run_validation(data)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(data))
|
||||
|
||||
|
||||
class BaseDictWithChildField(fields.DictField):
|
||||
|
||||
default_error_messages = {
|
||||
'missing_keys': _('Missing key(s): {missing_keys}.'),
|
||||
'invalid_keys': _('Invalid key(s): {invalid_keys}.'),
|
||||
}
|
||||
child_fields = {
|
||||
# 'key': fields.ChildField(),
|
||||
}
|
||||
allow_unknown_keys = False
|
||||
|
||||
def to_representation(self, value):
|
||||
value = super(BaseDictWithChildField, self).to_representation(value)
|
||||
for k, v in value.items():
|
||||
child_field = self.child_fields.get(k, None)
|
||||
if child_field:
|
||||
value[k] = child_field.to_representation(v)
|
||||
elif allow_unknown_keys:
|
||||
value[k] = v
|
||||
return value
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(BaseDictWithChildField, self).to_internal_value(data)
|
||||
missing_keys = set()
|
||||
for key, child_field in self.child_fields.items():
|
||||
if not child_field.required:
|
||||
continue
|
||||
elif key not in data:
|
||||
missing_keys.add(key)
|
||||
if missing_keys:
|
||||
keys_display = json.dumps(list(missing_keys)).lstrip('[').rstrip(']')
|
||||
self.fail('missing_keys', missing_keys=keys_display)
|
||||
if not self.allow_unknown_keys:
|
||||
invalid_keys = set(data.keys()) - set(self.child_fields.keys())
|
||||
if invalid_keys:
|
||||
keys_display = json.dumps(list(invalid_keys)).lstrip('[').rstrip(']')
|
||||
self.fail('invalid_keys', invalid_keys=keys_display)
|
||||
for k, v in data.items():
|
||||
child_field = self.child_fields.get(k, None)
|
||||
if child_field:
|
||||
data[k] = child_field.run_validation(v)
|
||||
elif self.allow_unknown_keys:
|
||||
data[k] = v
|
||||
return data
|
||||
|
||||
|
||||
class LDAPSingleOrganizationMapField(BaseDictWithChildField):
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_keys': _('Invalid key(s) for organization map: {invalid_keys}.'),
|
||||
}
|
||||
child_fields = {
|
||||
'admins': LDAPDNMapField(allow_null=True, required=False),
|
||||
'users': LDAPDNMapField(allow_null=True, required=False),
|
||||
'remove_admins': fields.BooleanField(required=False),
|
||||
'remove_users': fields.BooleanField(required=False),
|
||||
}
|
||||
|
||||
|
||||
class LDAPOrganizationMapField(fields.DictField):
|
||||
|
||||
child = LDAPSingleOrganizationMapField()
|
||||
|
||||
|
||||
class LDAPSingleTeamMapField(BaseDictWithChildField):
|
||||
|
||||
default_error_messages = {
|
||||
'missing_keys': _('Missing required key for team map: {invalid_keys}.'),
|
||||
'invalid_keys': _('Invalid key(s) for team map: {invalid_keys}.'),
|
||||
}
|
||||
child_fields = {
|
||||
'organization': fields.CharField(),
|
||||
'users': LDAPDNMapField(allow_null=True, required=False),
|
||||
'remove': fields.BooleanField(required=False),
|
||||
}
|
||||
|
||||
|
||||
class LDAPTeamMapField(fields.DictField):
|
||||
|
||||
child = LDAPSingleTeamMapField()
|
||||
|
||||
|
||||
class RADIUSSecretField(fields.CharField):
|
||||
|
||||
def to_internal_value(self, value):
|
||||
value = super(RADIUSSecretField, self).to_internal_value(value)
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode('utf-8')
|
||||
return value
|
||||
|
||||
|
||||
class SocialMapStringRegexField(fields.CharField):
|
||||
|
||||
def to_representation(self, value):
|
||||
if isinstance(value, type(re.compile(''))):
|
||||
flags = []
|
||||
if value.flags & re.I:
|
||||
flags.append('i')
|
||||
if value.flags & re.M:
|
||||
flags.append('m')
|
||||
return '/{}/{}'.format(value.pattern, ''.join(flags))
|
||||
else:
|
||||
return super(SocialMapStringRegexField, self).to_representation(value)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(SocialMapStringRegexField, self).to_internal_value(data)
|
||||
match = re.match(r'^/(?P<pattern>.*)/(?P<flags>[im]+)?$', data)
|
||||
if match:
|
||||
flags = 0
|
||||
if match.group('flags'):
|
||||
if 'i' in match.group('flags'):
|
||||
flags |= re.I
|
||||
if 'm' in match.group('flags'):
|
||||
flags |= re.M
|
||||
try:
|
||||
return re.compile(match.group('pattern'), flags)
|
||||
except re.error as e:
|
||||
raise ValidationError('{}: {}'.format(e, data))
|
||||
return data
|
||||
|
||||
|
||||
class SocialMapField(fields.ListField):
|
||||
|
||||
default_error_messages = {
|
||||
'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.'),
|
||||
}
|
||||
child = SocialMapStringRegexField()
|
||||
|
||||
def to_representation(self, value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return super(SocialMapField, self).to_representation(value)
|
||||
elif value in fields.NullBooleanField.TRUE_VALUES:
|
||||
return True
|
||||
elif value in fields.NullBooleanField.FALSE_VALUES:
|
||||
return False
|
||||
elif value in fields.NullBooleanField.NULL_VALUES:
|
||||
return None
|
||||
elif isinstance(value, (basestring, type(re.compile('')))):
|
||||
return self.child.to_representation(value)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(value))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, (list, tuple)):
|
||||
return super(SocialMapField, self).to_internal_value(data)
|
||||
elif data in fields.NullBooleanField.TRUE_VALUES:
|
||||
return True
|
||||
elif data in fields.NullBooleanField.FALSE_VALUES:
|
||||
return False
|
||||
elif data in fields.NullBooleanField.NULL_VALUES:
|
||||
return None
|
||||
elif isinstance(data, basestring):
|
||||
return self.child.run_validation(data)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(data))
|
||||
|
||||
|
||||
class SocialSingleOrganizationMapField(BaseDictWithChildField):
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_keys': _('Invalid key(s) for organization map: {invalid_keys}.'),
|
||||
}
|
||||
child_fields = {
|
||||
'admins': SocialMapField(allow_null=True, required=False),
|
||||
'users': SocialMapField(allow_null=True, required=False),
|
||||
'remove_admins': fields.BooleanField(required=False),
|
||||
'remove_users': fields.BooleanField(required=False),
|
||||
}
|
||||
|
||||
|
||||
class SocialOrganizationMapField(fields.DictField):
|
||||
|
||||
child = SocialSingleOrganizationMapField()
|
||||
|
||||
|
||||
class SocialSingleTeamMapField(BaseDictWithChildField):
|
||||
|
||||
default_error_messages = {
|
||||
'missing_keys': _('Missing required key for team map: {missing_keys}.'),
|
||||
'invalid_keys': _('Invalid key(s) for team map: {invalid_keys}.'),
|
||||
}
|
||||
child_fields = {
|
||||
'organization': fields.CharField(),
|
||||
'users': SocialMapField(allow_null=True, required=False),
|
||||
'remove': fields.BooleanField(required=False),
|
||||
}
|
||||
|
||||
|
||||
class SocialTeamMapField(fields.DictField):
|
||||
|
||||
child = SocialSingleTeamMapField()
|
||||
|
||||
|
||||
class SAMLOrgInfoValueField(BaseDictWithChildField):
|
||||
|
||||
default_error_messages = {
|
||||
'missing_keys': _('Missing required key(s) for org info record: {missing_keys}.'),
|
||||
}
|
||||
child_fields = {
|
||||
'name': fields.CharField(),
|
||||
'displayname': fields.CharField(),
|
||||
'url': fields.URLField(),
|
||||
}
|
||||
allow_unknown_keys = True
|
||||
|
||||
|
||||
class SAMLOrgInfoField(fields.DictField):
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_lang_code': _('Invalid language code(s) for org info: {invalid_lang_codes}.'),
|
||||
}
|
||||
child = SAMLOrgInfoValueField()
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(SAMLOrgInfoField, self).to_internal_value(data)
|
||||
invalid_keys = set()
|
||||
for key in data.keys():
|
||||
if not re.match(r'^[a-z]{2}(?:-[a-z]{2})??$', key, re.I):
|
||||
invalid_keys.add(key)
|
||||
if invalid_keys:
|
||||
keys_display = json.dumps(list(invalid_keys)).lstrip('[').rstrip(']')
|
||||
self.fail('invalid_lang_code', invalid_lang_codes=keys_display)
|
||||
return data
|
||||
|
||||
|
||||
class SAMLContactField(BaseDictWithChildField):
|
||||
|
||||
default_error_messages = {
|
||||
'missing_keys': _('Missing required key(s) for contact: {missing_keys}.'),
|
||||
}
|
||||
child_fields = {
|
||||
'givenName': fields.CharField(),
|
||||
'emailAddress': fields.EmailField(),
|
||||
}
|
||||
allow_unknown_keys = True
|
||||
|
||||
|
||||
class SAMLIdPField(BaseDictWithChildField):
|
||||
|
||||
default_error_messages = {
|
||||
'missing_keys': _('Missing required key(s) for IdP: {missing_keys}.'),
|
||||
}
|
||||
child_fields = {
|
||||
'entity_id': fields.URLField(),
|
||||
'url': fields.URLField(),
|
||||
'x509cert': fields.CharField(validators=[validate_certificate]),
|
||||
'attr_user_permanent_id': fields.CharField(required=False),
|
||||
'attr_first_name': fields.CharField(required=False),
|
||||
'attr_last_name': fields.CharField(required=False),
|
||||
'attr_username': fields.CharField(required=False),
|
||||
'attr_email': fields.CharField(required=False),
|
||||
}
|
||||
allow_unknown_keys = True
|
||||
|
||||
|
||||
class SAMLEnabledIdPsField(fields.DictField):
|
||||
|
||||
child = SAMLIdPField()
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
from social.exceptions import AuthException
|
||||
|
||||
# Tower
|
||||
from awx.api.license import feature_enabled
|
||||
from awx.conf.license import feature_enabled
|
||||
|
||||
|
||||
class AuthNotFound(AuthException):
|
||||
|
||||
60
awx/sso/validators.py
Normal file
60
awx/sso/validators.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Python
|
||||
import re
|
||||
|
||||
# Python-LDAP
|
||||
import ldap
|
||||
|
||||
# Django
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
__all__ = ['validate_ldap_dn', 'validate_ldap_dn_with_user',
|
||||
'validate_ldap_bind_dn', 'validate_ldap_filter',
|
||||
'validate_ldap_filter_with_user']
|
||||
|
||||
|
||||
def validate_ldap_dn(value, with_user=False):
|
||||
if with_user:
|
||||
if '%(user)s' not in value:
|
||||
raise ValidationError(_('DN must include "%%(user)s" placeholder for username: %s') % value)
|
||||
dn_value = value.replace('%(user)s', 'USER')
|
||||
else:
|
||||
dn_value = value
|
||||
try:
|
||||
ldap.dn.str2dn(dn_value)
|
||||
except ldap.DECODING_ERROR:
|
||||
raise ValidationError(_('Invalid DN: %s') % value)
|
||||
|
||||
|
||||
def validate_ldap_dn_with_user(value):
|
||||
validate_ldap_dn(value, with_user=True)
|
||||
|
||||
|
||||
def validate_ldap_bind_dn(value):
|
||||
if not re.match(r'^[A-Za-z][A-Za-z0-9._-]*?\\[A-Za-z0-9 ._-]+?$', value.strip()):
|
||||
validate_ldap_dn(value)
|
||||
|
||||
|
||||
def validate_ldap_filter(value, with_user=False):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return
|
||||
if with_user:
|
||||
if '%(user)s' not in value:
|
||||
raise ValidationError(_('DN must include "%%(user)s" placeholder for username: %s') % value)
|
||||
dn_value = value.replace('%(user)s', 'USER')
|
||||
else:
|
||||
dn_value = value
|
||||
if re.match(r'^\([A-Za-z0-9]+?=[^()]+?\)$', dn_value):
|
||||
return
|
||||
elif re.match(r'^\([&|!]\(.*?\)\)$', dn_value):
|
||||
try:
|
||||
map(validate_ldap_filter, ['(%s)' % x for x in dn_value[3:-2].split(')(')])
|
||||
return
|
||||
except ValidationError:
|
||||
pass
|
||||
raise ValidationError(_('Invalid filter: %s') % value)
|
||||
|
||||
|
||||
def validate_ldap_filter_with_user(value):
|
||||
validate_ldap_filter(value, with_user=True)
|
||||
Reference in New Issue
Block a user