Merge branch 'release_2.4.0' into devel

* release_2.4.0: (70 commits)
  Disallow changing a users password for social auth
  bump django-radius to include new license
  Change Jenkins server references from IP to DNS name
  Social auth and SSO updates:
  allow multi-org expired licenses to delete orgs
  Add social log messages to tower's log on error
  Update default scopes requested for github social auth.
  Switch to matburt's fork for python social auth
  Emit a warning for unmapped SAML paramters
  Fix up some SAML issues
  Remove `environment` play stmt
  Use correct aw_repo_url when building packer
  Add missing libxmlsec1 openssl dependency for ubuntu
  adds socket tests
  added git pre commit hook to run flake8
  fixes remove_instance error
  null pointer exception
  Resolve issue on precise keeping old debs around
  Attach handlers to django_auth_ldap
  Attach handlers to django_auth_ldap
  ...
This commit is contained in:
Matthew Jones
2015-11-16 15:12:37 -05:00
62 changed files with 1334 additions and 537 deletions

View File

@@ -15,6 +15,7 @@ recursive-exclude awx/main/tests *
recursive-exclude awx/ui/client * recursive-exclude awx/ui/client *
recursive-exclude awx/settings local_settings.py* recursive-exclude awx/settings local_settings.py*
include tools/scripts/request_tower_configuration.sh include tools/scripts/request_tower_configuration.sh
include tools/scripts/request_tower_configuration.ps1
include tools/scripts/ansible-tower-service include tools/scripts/ansible-tower-service
include tools/munin_monitors/* include tools/munin_monitors/*
include tools/sosreport/* include tools/sosreport/*

View File

@@ -2,6 +2,7 @@ PYTHON = python
SITELIB=$(shell $(PYTHON) -c "from distutils.sysconfig import get_python_lib; print get_python_lib()") SITELIB=$(shell $(PYTHON) -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")
OFFICIAL ?= no OFFICIAL ?= no
PACKER ?= packer PACKER ?= packer
PACKER_BUILD_OPTS ?= -var 'official=$(OFFICIAL)' -var 'aw_repo_url=$(AW_REPO_URL)'
GRUNT ?= $(shell [ -t 0 ] && echo "grunt" || echo "grunt --no-color") GRUNT ?= $(shell [ -t 0 ] && echo "grunt" || echo "grunt --no-color")
TESTEM ?= ./node_modules/.bin/testem TESTEM ?= ./node_modules/.bin/testem
TESTEM_DEBUG_BROWSER ?= Chrome TESTEM_DEBUG_BROWSER ?= Chrome
@@ -10,7 +11,7 @@ MOCHA_BIN ?= ./node_modules/.bin/mocha
NODE ?= node NODE ?= node
NPM_BIN ?= npm NPM_BIN ?= npm
DEPS_SCRIPT ?= packaging/bundle/deps.py DEPS_SCRIPT ?= packaging/bundle/deps.py
AW_REPO_URL ?= "http://releases.ansible.com/ansible-tower" GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
CLIENT_TEST_DIR ?= build_test CLIENT_TEST_DIR ?= build_test
@@ -33,8 +34,10 @@ GIT_REMOTE_URL = $(shell git config --get remote.origin.url)
BUILD = 0.git$(DATE) BUILD = 0.git$(DATE)
ifeq ($(OFFICIAL),yes) ifeq ($(OFFICIAL),yes)
RELEASE ?= 1 RELEASE ?= 1
AW_REPO_URL ?= http://releases.ansible.com/ansible-tower
else else
RELEASE ?= $(BUILD) RELEASE ?= $(BUILD)
AW_REPO_URL ?= http://jenkins.testing.ansible.com/ansible-tower_nightlies_RTYUIOPOIUYTYU/$(GIT_BRANCH)
endif endif
# Allow AMI license customization # Allow AMI license customization
@@ -57,11 +60,9 @@ endif
ifeq ($(OFFICIAL),yes) ifeq ($(OFFICIAL),yes)
SETUP_TAR_NAME=$(NAME)-setup-$(VERSION) SETUP_TAR_NAME=$(NAME)-setup-$(VERSION)
SDIST_TAR_NAME=$(NAME)-$(VERSION) SDIST_TAR_NAME=$(NAME)-$(VERSION)
PACKER_BUILD_OPTS ?= -var-file=vars-release.json
else else
SETUP_TAR_NAME=$(NAME)-setup-$(VERSION)-$(RELEASE) SETUP_TAR_NAME=$(NAME)-setup-$(VERSION)-$(RELEASE)
SDIST_TAR_NAME=$(NAME)-$(VERSION)-$(RELEASE) SDIST_TAR_NAME=$(NAME)-$(VERSION)-$(RELEASE)
PACKER_BUILD_OPTS ?= -var-file=vars-nightly.json
endif endif
SDIST_TAR_FILE=$(SDIST_TAR_NAME).tar.gz SDIST_TAR_FILE=$(SDIST_TAR_NAME).tar.gz
SETUP_TAR_FILE=$(SETUP_TAR_NAME).tar.gz SETUP_TAR_FILE=$(SETUP_TAR_NAME).tar.gz
@@ -650,7 +651,7 @@ reprepro: deb-build/$(DEB_NVRA).deb reprepro/conf
$(REPREPRO_BIN) $(REPREPRO_OPTS) clearvanished $(REPREPRO_BIN) $(REPREPRO_OPTS) clearvanished
for COMPONENT in non-free $(VERSION); do \ for COMPONENT in non-free $(VERSION); do \
$(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT remove $(DEB_DIST) $(NAME) ; \ $(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT remove $(DEB_DIST) $(NAME) ; \
$(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT --keepunreferencedfiles --ignore=brokenold includedeb $(DEB_DIST) deb-build/$(DEB_NVRA).deb ; \ $(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT --ignore=brokenold includedeb $(DEB_DIST) deb-build/$(DEB_NVRA).deb ; \
done done

View File

@@ -630,6 +630,12 @@ class UserSerializer(BaseSerializer):
new_password = None new_password = None
except AttributeError: except AttributeError:
pass pass
if (getattr(settings, 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', None) or
getattr(settings, 'SOCIAL_AUTH_GITHUB_KEY', None) or
getattr(settings, 'SOCIAL_AUTH_GITHUB_ORG_KEY', None) or
getattr(settings, 'SOCIAL_AUTH_GITHUB_TEAM_KEY', None) or
getattr(settings, 'SOCIAL_AUTH_SAML_ENABLED_IDPS', None)) and obj.social_auth.all():
new_password = None
if new_password: if new_password:
obj.set_password(new_password) obj.set_password(new_password)
if not obj.password: if not obj.password:
@@ -2004,11 +2010,12 @@ class ScheduleSerializer(BaseSerializer):
class ActivityStreamSerializer(BaseSerializer): class ActivityStreamSerializer(BaseSerializer):
changes = serializers.SerializerMethodField('get_changes') changes = serializers.SerializerMethodField('get_changes')
object_association = serializers.SerializerMethodField('get_object_association')
class Meta: class Meta:
model = ActivityStream model = ActivityStream
fields = ('*', '-name', '-description', '-created', '-modified', fields = ('*', '-name', '-description', '-created', '-modified',
'timestamp', 'operation', 'changes', 'object1', 'object2') 'timestamp', 'operation', 'changes', 'object1', 'object2', 'object_association')
def get_fields(self): def get_fields(self):
ret = super(ActivityStreamSerializer, self).get_fields() ret = super(ActivityStreamSerializer, self).get_fields()
@@ -2033,6 +2040,13 @@ class ActivityStreamSerializer(BaseSerializer):
logger.warn("Error deserializing activity stream json changes") logger.warn("Error deserializing activity stream json changes")
return {} return {}
def get_object_association(self, obj):
try:
return obj.object_relationship_type.split(".")[-1].split("_")[1]
except:
pass
return ""
def get_related(self, obj): def get_related(self, obj):
rel = {} rel = {}
if obj.actor is not None: if obj.actor is not None:

View File

@@ -11,6 +11,7 @@ import time
import socket import socket
import sys import sys
import errno import errno
from base64 import b64encode
# Django # Django
from django.conf import settings from django.conf import settings
@@ -526,7 +527,10 @@ class AuthView(APIView):
def get(self, request): def get(self, request):
data = SortedDict() data = SortedDict()
err_backend, err_message = request.session.get('social_auth_error', (None, None)) err_backend, err_message = request.session.get('social_auth_error', (None, None))
for name, backend in load_backends(settings.AUTHENTICATION_BACKENDS).items(): auth_backends = load_backends(settings.AUTHENTICATION_BACKENDS).items()
# Return auth backends in consistent order: Google, GitHub, SAML.
auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else x[0])
for name, backend in auth_backends:
if (not feature_exists('enterprise_auth') and if (not feature_exists('enterprise_auth') and
not feature_enabled('ldap')) or \ not feature_enabled('ldap')) or \
(not feature_enabled('enterprise_auth') and (not feature_enabled('enterprise_auth') and
@@ -540,7 +544,7 @@ class AuthView(APIView):
} }
if name == 'saml': if name == 'saml':
backend_data['metadata_url'] = reverse('sso:saml_metadata') backend_data['metadata_url'] = reverse('sso:saml_metadata')
for idp in settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys(): for idp in sorted(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys()):
saml_backend_data = dict(backend_data.items()) saml_backend_data = dict(backend_data.items())
saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp) saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp)
full_backend_name = '%s:%s' % (name, idp) full_backend_name = '%s:%s' % (name, idp)
@@ -2861,8 +2865,10 @@ class UnifiedJobStdout(RetrieveAPIView):
return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message}) return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message})
else: else:
return Response(response_message) return Response(response_message)
if request.accepted_renderer.format in ('html', 'api', 'json'): if request.accepted_renderer.format in ('html', 'api', 'json'):
content_format = request.QUERY_PARAMS.get('content_format', 'html')
content_encoding = request.QUERY_PARAMS.get('content_encoding', None)
start_line = request.QUERY_PARAMS.get('start_line', 0) start_line = request.QUERY_PARAMS.get('start_line', 0)
end_line = request.QUERY_PARAMS.get('end_line', None) end_line = request.QUERY_PARAMS.get('end_line', None)
dark_val = request.QUERY_PARAMS.get('dark', '') dark_val = request.QUERY_PARAMS.get('dark', '')
@@ -2883,7 +2889,10 @@ class UnifiedJobStdout(RetrieveAPIView):
if request.accepted_renderer.format == 'api': if request.accepted_renderer.format == 'api':
return Response(mark_safe(data)) return Response(mark_safe(data))
if request.accepted_renderer.format == 'json': if request.accepted_renderer.format == 'json':
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body}) if content_encoding == 'base64' and content_format == 'ansi':
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': b64encode(content)})
elif content_format == 'html':
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body})
return Response(data) return Response(data)
elif request.accepted_renderer.format == 'ansi': elif request.accepted_renderer.format == 'ansi':
return Response(unified_job.result_stdout_raw) return Response(unified_job.result_stdout_raw)

View File

@@ -146,7 +146,7 @@ class BaseAccess(object):
def can_unattach(self, obj, sub_obj, relationship): def can_unattach(self, obj, sub_obj, relationship):
return self.can_change(obj, None) return self.can_change(obj, None)
def check_license(self, add_host=False, feature=None): def check_license(self, add_host=False, feature=None, check_expiration=True):
reader = TaskSerializer() reader = TaskSerializer()
validation_info = reader.from_file() validation_info = reader.from_file()
if ('test' in sys.argv or 'jenkins' in sys.argv) and not os.environ.get('SKIP_LICENSE_FIXUP_FOR_TEST', ''): if ('test' in sys.argv or 'jenkins' in sys.argv) and not os.environ.get('SKIP_LICENSE_FIXUP_FOR_TEST', ''):
@@ -154,9 +154,9 @@ class BaseAccess(object):
validation_info['time_remaining'] = 99999999 validation_info['time_remaining'] = 99999999
validation_info['grace_period_remaining'] = 99999999 validation_info['grace_period_remaining'] = 99999999
if validation_info.get('time_remaining', None) is None: if check_expiration and validation_info.get('time_remaining', None) is None:
raise PermissionDenied("license is missing") raise PermissionDenied("license is missing")
if validation_info.get("grace_period_remaining") <= 0: if check_expiration and validation_info.get("grace_period_remaining") <= 0:
raise PermissionDenied("license has expired") raise PermissionDenied("license has expired")
free_instances = validation_info.get('free_instances', 0) free_instances = validation_info.get('free_instances', 0)
@@ -262,7 +262,7 @@ class OrganizationAccess(BaseAccess):
self.user in obj.admins.all()) self.user in obj.admins.all())
def can_delete(self, obj): def can_delete(self, obj):
self.check_license(feature='multiple_organizations') self.check_license(feature='multiple_organizations', check_expiration=False)
return self.can_change(obj, None) return self.can_change(obj, None)
class InventoryAccess(BaseAccess): class InventoryAccess(BaseAccess):

View File

@@ -1,128 +0,0 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Django
from django.dispatch import receiver
# django-auth-ldap
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend
from django_auth_ldap.backend import populate_user
# Ansible Tower
from awx.api.license import feature_enabled
class LDAPSettings(BaseLDAPSettings):
defaults = dict(BaseLDAPSettings.defaults.items() + {
'ORGANIZATION_MAP': {},
'TEAM_MAP': {},
}.items())
class LDAPBackend(BaseLDAPBackend):
'''
Custom LDAP backend for AWX.
'''
settings_prefix = 'AUTH_LDAP_'
def _get_settings(self):
if self._settings is None:
self._settings = LDAPSettings(self.settings_prefix)
return self._settings
def _set_settings(self, settings):
self._settings = settings
settings = property(_get_settings, _set_settings)
def authenticate(self, username, password):
if not self.settings.SERVER_URI or not feature_enabled('ldap'):
return None
return super(LDAPBackend, self).authenticate(username, password)
def get_user(self, user_id):
if not self.settings.SERVER_URI or not feature_enabled('ldap'):
return None
return super(LDAPBackend, self).get_user(user_id)
# Disable any LDAP based authorization / permissions checking.
def has_perm(self, user, perm, obj=None):
return False
def has_module_perms(self, user, app_label):
return False
def get_all_permissions(self, user, obj=None):
return set()
def get_group_permissions(self, user, obj=None):
return set()
def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=False):
'''
Hepler function to update m2m relationship based on LDAP group membership.
'''
should_add = False
if opts is None:
return
elif not opts:
pass
elif opts is True:
should_add = True
else:
if isinstance(opts, basestring):
opts = [opts]
for group_dn in opts:
if not isinstance(group_dn, basestring):
continue
if ldap_user._get_groups().is_member_of(group_dn):
should_add = True
if should_add:
rel.add(user)
elif remove:
rel.remove(user)
@receiver(populate_user)
def on_populate_user(sender, **kwargs):
'''
Handle signal from LDAP backend to populate the user object. Update user
organization/team memberships according to their LDAP groups.
'''
from awx.main.models import Organization, Team
user = kwargs['user']
ldap_user = kwargs['ldap_user']
backend = ldap_user.backend
# Update organization membership based on group memberships.
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
for org_name, org_opts in org_map.items():
org, created = Organization.objects.get_or_create(name=org_name)
remove = bool(org_opts.get('remove', False))
admins_opts = org_opts.get('admins', None)
remove_admins = bool(org_opts.get('remove_admins', remove))
_update_m2m_from_groups(user, ldap_user, org.admins, admins_opts,
remove_admins)
users_opts = org_opts.get('users', None)
remove_users = bool(org_opts.get('remove_users', remove))
_update_m2m_from_groups(user, ldap_user, org.users, users_opts,
remove_users)
# Update team membership based on group memberships.
team_map = getattr(backend.settings, 'TEAM_MAP', {})
for team_name, team_opts in team_map.items():
if 'organization' not in team_opts:
continue
org, created = Organization.objects.get_or_create(name=team_opts['organization'])
team, created = Team.objects.get_or_create(name=team_name, organization=org)
users_opts = team_opts.get('users', None)
remove = bool(team_opts.get('remove', False))
_update_m2m_from_groups(user, ldap_user, team.users, users_opts,
remove)
# Update user profile to store LDAP DN.
profile = user.profile
if profile.ldap_dn != ldap_user.dn:
profile.ldap_dn = ldap_user.dn
profile.save()

View File

@@ -21,6 +21,7 @@ class BaseCommandInstance(BaseCommand):
def __init__(self): def __init__(self):
super(BaseCommandInstance, self).__init__() super(BaseCommandInstance, self).__init__()
self.enforce_primary_role = False
self.enforce_roles = False self.enforce_roles = False
self.enforce_hostname_set = False self.enforce_hostname_set = False
self.enforce_unique_find = False self.enforce_unique_find = False

View File

@@ -46,43 +46,52 @@ class SocketSession(object):
return bool(not auth_token.is_expired()) return bool(not auth_token.is_expired())
class SocketSessionManager(object): class SocketSessionManager(object):
socket_sessions = []
socket_session_token_key_map = {}
@classmethod def __init__(self):
def _prune(cls): self.SESSIONS_MAX = 1000
if len(cls.socket_sessions) > 1000: self.socket_sessions = []
session = cls.socket_session[0] self.socket_session_token_key_map = {}
del cls.socket_session_token_key_map[session.token_key]
cls.sessions = cls.socket_sessions[1:] def _prune(self):
if len(self.socket_sessions) > self.SESSIONS_MAX:
session = self.socket_sessions[0]
entries = self.socket_session_token_key_map[session.token_key]
del entries[session.session_id]
if len(entries) == 0:
del self.socket_session_token_key_map[session.token_key]
self.socket_sessions.pop(0)
''' '''
Returns an dict of sessions <session_id, session> Returns an dict of sessions <session_id, session>
''' '''
@classmethod def lookup(self, token_key=None):
def lookup(cls, token_key=None):
if not token_key: if not token_key:
raise ValueError("token_key required") raise ValueError("token_key required")
return cls.socket_session_token_key_map.get(token_key, None) return self.socket_session_token_key_map.get(token_key, None)
@classmethod def add_session(self, session):
def add_session(cls, session): self.socket_sessions.append(session)
cls.socket_sessions.append(session) entries = self.socket_session_token_key_map.get(session.token_key, None)
entries = cls.socket_session_token_key_map.get(session.token_key, None)
if not entries: if not entries:
entries = {} entries = {}
cls.socket_session_token_key_map[session.token_key] = entries self.socket_session_token_key_map[session.token_key] = entries
entries[session.session_id] = session entries[session.session_id] = session
cls._prune() self._prune()
return session
class SocketController(object): class SocketController(object):
server = None
@classmethod def __init__(self, SocketSessionManager):
def broadcast_packet(cls, packet): self.server = None
self.SocketSessionManager = SocketSessionManager
def add_session(self, session):
return self.SocketSessionManager.add_session(session)
def broadcast_packet(self, packet):
# Broadcast message to everyone at endpoint # Broadcast message to everyone at endpoint
# Loop over the 'raw' list of sockets (don't trust our list) # Loop over the 'raw' list of sockets (don't trust our list)
for session_id, socket in list(cls.server.sockets.iteritems()): for session_id, socket in list(self.server.sockets.iteritems()):
socket_session = socket.session.get('socket_session', None) socket_session = socket.session.get('socket_session', None)
if socket_session and socket_session.is_valid(): if socket_session and socket_session.is_valid():
try: try:
@@ -91,11 +100,10 @@ class SocketController(object):
logger.error("Error sending client packet to %s: %s" % (str(session_id), str(packet))) logger.error("Error sending client packet to %s: %s" % (str(session_id), str(packet)))
logger.error("Error was: " + str(e)) logger.error("Error was: " + str(e))
@classmethod def send_packet(self, packet, token_key):
def send_packet(cls, packet, token_key):
if not token_key: if not token_key:
raise ValueError("token_key is required") raise ValueError("token_key is required")
socket_sessions = SocketSessionManager.lookup(token_key=token_key) socket_sessions = self.SocketSessionManager.lookup(token_key=token_key)
# We may not find the socket_session if the user disconnected # We may not find the socket_session if the user disconnected
# (it's actually more compliciated than that because of our prune logic) # (it's actually more compliciated than that because of our prune logic)
if not socket_sessions: if not socket_sessions:
@@ -112,11 +120,12 @@ class SocketController(object):
logger.error("Error sending client packet to %s: %s" % (str(socket_session.session_id), str(packet))) logger.error("Error sending client packet to %s: %s" % (str(socket_session.session_id), str(packet)))
logger.error("Error was: " + str(e)) logger.error("Error was: " + str(e))
@classmethod def set_server(self, server):
def set_server(cls, server): self.server = server
cls.server = server
return server return server
socketController = SocketController(SocketSessionManager())
# #
# Socket session is attached to self.session['socket_session'] # Socket session is attached to self.session['socket_session']
# self.session and self.socket.session point to the same dict # self.session and self.socket.session point to the same dict
@@ -140,7 +149,7 @@ class TowerBaseNamespace(BaseNamespace):
socket_session = SocketSession(self.socket.sessid, request_token, self.socket) socket_session = SocketSession(self.socket.sessid, request_token, self.socket)
if socket_session.is_db_token_valid(): if socket_session.is_db_token_valid():
self.session['socket_session'] = socket_session self.session['socket_session'] = socket_session
SocketSessionManager.add_session(socket_session) socketController.add_session(socket_session)
else: else:
socket_session.invalidate() socket_session.invalidate()
@@ -240,9 +249,9 @@ def notification_handler(server):
if 'token_key' in message: if 'token_key' in message:
# Best practice not to send the token over the socket # Best practice not to send the token over the socket
SocketController.send_packet(packet, message.pop('token_key')) socketController.send_packet(packet, message.pop('token_key'))
else: else:
SocketController.broadcast_packet(packet) socketController.broadcast_packet(packet)
class Command(NoArgsCommand): class Command(NoArgsCommand):
''' '''
@@ -270,7 +279,7 @@ class Command(NoArgsCommand):
logger.info('Listening on port http://0.0.0.0:' + str(socketio_listen_port)) logger.info('Listening on port http://0.0.0.0:' + str(socketio_listen_port))
server = SocketIOServer(('0.0.0.0', socketio_listen_port), TowerSocket(), resource='socket.io') server = SocketIOServer(('0.0.0.0', socketio_listen_port), TowerSocket(), resource='socket.io')
SocketController.set_server(server) socketController.set_server(server)
handler_thread = Thread(target=notification_handler, args=(server,)) handler_thread = Thread(target=notification_handler, args=(server,))
handler_thread.daemon = True handler_thread.daemon = True
handler_thread.start() handler_thread.start()

View File

@@ -170,10 +170,10 @@ def handle_work_error(self, task_id, subtasks=None):
instance_name = '' instance_name = ''
if each_task['type'] == 'project_update': if each_task['type'] == 'project_update':
instance = ProjectUpdate.objects.get(id=each_task['id']) instance = ProjectUpdate.objects.get(id=each_task['id'])
instance_name = instance.project.name instance_name = instance.name
elif each_task['type'] == 'inventory_update': elif each_task['type'] == 'inventory_update':
instance = InventoryUpdate.objects.get(id=each_task['id']) instance = InventoryUpdate.objects.get(id=each_task['id'])
instance_name = instance.inventory_source.inventory.name instance_name = instance.name
elif each_task['type'] == 'job': elif each_task['type'] == 'job':
instance = Job.objects.get(id=each_task['id']) instance = Job.objects.get(id=each_task['id'])
instance_name = instance.job_template.name instance_name = instance.job_template.name
@@ -191,7 +191,7 @@ def handle_work_error(self, task_id, subtasks=None):
if instance.celery_task_id != task_id: if instance.celery_task_id != task_id:
instance.status = 'failed' instance.status = 'failed'
instance.failed = True instance.failed = True
instance.job_explanation = 'Previous Task Failed: {"task_type": "%s", "task_name": "%s", "task_id": "%s"}' % \ instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \
(first_task_type, first_task_name, first_task_id) (first_task_type, first_task_name, first_task_id)
instance.save() instance.save()
instance.socketio_emit_status("failed") instance.socketio_emit_status("failed")
@@ -614,6 +614,21 @@ class RunJob(BaseTask):
if credential.ssh_key_data not in (None, ''): if credential.ssh_key_data not in (None, ''):
private_data[cred_name] = decrypt_field(credential, 'ssh_key_data') or '' private_data[cred_name] = decrypt_field(credential, 'ssh_key_data') or ''
if job.cloud_credential and job.cloud_credential.kind == 'openstack':
credential = job.cloud_credential
openstack_auth = dict(auth_url=credential.host,
username=credential.username,
password=decrypt_field(credential, "password"),
project_name=credential.project)
openstack_data = {
'clouds': {
'devstack': {
'auth': openstack_auth,
},
},
}
private_data['cloud_credential'] = yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True)
return private_data return private_data
def build_passwords(self, job, **kwargs): def build_passwords(self, job, **kwargs):
@@ -689,6 +704,8 @@ class RunJob(BaseTask):
env['VMWARE_USER'] = cloud_cred.username env['VMWARE_USER'] = cloud_cred.username
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password') env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
env['VMWARE_HOST'] = cloud_cred.host env['VMWARE_HOST'] = cloud_cred.host
elif cloud_cred and cloud_cred.kind == 'openstack':
env['OS_CLIENT_CONFIG_FILE'] = kwargs.get('private_data_files', {}).get('cloud_credential', '')
# Set environment variables related to scan jobs # Set environment variables related to scan jobs
if job.job_type == PERM_INVENTORY_SCAN: if job.job_type == PERM_INVENTORY_SCAN:

View File

@@ -28,11 +28,11 @@ from django.test.utils import override_settings
# AWX # AWX
from awx.main.models import * # noqa from awx.main.models import * # noqa
from awx.main.backend import LDAPSettings
from awx.main.management.commands.run_callback_receiver import CallbackReceiver from awx.main.management.commands.run_callback_receiver import CallbackReceiver
from awx.main.management.commands.run_task_system import run_taskmanager from awx.main.management.commands.run_task_system import run_taskmanager
from awx.main.utils import get_ansible_version from awx.main.utils import get_ansible_version
from awx.main.task_engine import TaskEngager as LicenseWriter from awx.main.task_engine import TaskEngager as LicenseWriter
from awx.sso.backends import LDAPSettings
TEST_PLAYBOOK = '''- hosts: mygroup TEST_PLAYBOOK = '''- hosts: mygroup
gather_facts: false gather_facts: false

View File

@@ -7,3 +7,6 @@ from .run_fact_cache_receiver import * # noqa
from .commands_monolithic import * # noqa from .commands_monolithic import * # noqa
from .cleanup_facts import * # noqa from .cleanup_facts import * # noqa
from .age_deleted import * # noqa from .age_deleted import * # noqa
from .remove_instance import * # noqa
from .run_socketio_service import * # noqa

View File

@@ -0,0 +1,39 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved
# Python
import uuid
# AWX
from awx.main.tests.base import BaseTest
from awx.main.tests.commands.base import BaseCommandMixin
from awx.main.models import * # noqa
__all__ = ['RemoveInstanceCommandFunctionalTest']
class RemoveInstanceCommandFunctionalTest(BaseCommandMixin, BaseTest):
uuids = []
instances = []
def setup_instances(self):
self.uuids = [uuid.uuid4().hex for x in range(0, 3)]
self.instances.append(Instance(uuid=settings.SYSTEM_UUID, primary=True, hostname='127.0.0.1'))
self.instances.append(Instance(uuid=self.uuids[0], primary=False, hostname='127.0.0.2'))
self.instances.append(Instance(uuid=self.uuids[1], primary=False, hostname='127.0.0.3'))
self.instances.append(Instance(uuid=self.uuids[2], primary=False, hostname='127.0.0.4'))
for x in self.instances:
x.save()
def setUp(self):
super(RemoveInstanceCommandFunctionalTest, self).setUp()
self.create_test_license_file()
self.setup_instances()
self.setup_users()
def test_default(self):
self.assertEqual(Instance.objects.filter(hostname="127.0.0.2").count(), 1)
result, stdout, stderr = self.run_command('remove_instance', hostname='127.0.0.2')
self.assertIsNone(result)
self.assertEqual(stdout, 'Successfully removed instance (uuid="%s",hostname="127.0.0.2",role="secondary").\n' % (self.uuids[0]))
self.assertEqual(Instance.objects.filter(hostname="127.0.0.2").count(), 0)

View File

@@ -0,0 +1,116 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved
# Python
from mock import MagicMock, Mock
# Django
from django.test import SimpleTestCase
# AWX
from awx.fact.models.fact import * # noqa
from awx.main.management.commands.run_socketio_service import SocketSessionManager, SocketSession, SocketController
__all__ = ['SocketSessionManagerUnitTest', 'SocketControllerUnitTest',]
class WeakRefable():
pass
class SocketSessionManagerUnitTest(SimpleTestCase):
def setUp(self):
self.session_manager = SocketSessionManager()
super(SocketSessionManagerUnitTest, self).setUp()
def create_sessions(self, count, token_key=None):
self.sessions = []
self.count = count
for i in range(0, count):
self.sessions.append(SocketSession(i, token_key or i, WeakRefable()))
self.session_manager.add_session(self.sessions[i])
def test_multiple_session_diff_token(self):
self.create_sessions(10)
for s in self.sessions:
self.assertIn(s.token_key, self.session_manager.socket_session_token_key_map)
self.assertEqual(s, self.session_manager.socket_session_token_key_map[s.token_key][s.session_id])
def test_multiple_session_same_token(self):
self.create_sessions(10, token_key='foo')
sessions_dict = self.session_manager.lookup("foo")
self.assertEqual(len(sessions_dict), 10)
for s in self.sessions:
self.assertIn(s.session_id, sessions_dict)
self.assertEqual(s, sessions_dict[s.session_id])
def test_prune_sessions_max(self):
self.create_sessions(self.session_manager.SESSIONS_MAX + 10)
self.assertEqual(len(self.session_manager.socket_sessions), self.session_manager.SESSIONS_MAX)
class SocketControllerUnitTest(SimpleTestCase):
def setUp(self):
self.socket_controller = SocketController(SocketSessionManager())
server = Mock()
self.socket_controller.set_server(server)
super(SocketControllerUnitTest, self).setUp()
def create_clients(self, count, token_key=None):
self.sessions = []
self.sockets =[]
self.count = count
self.sockets_dict = {}
for i in range(0, count):
if isinstance(token_key, list):
token_key_actual = token_key[i]
else:
token_key_actual = token_key or i
socket = MagicMock(session=dict())
socket_session = SocketSession(i, token_key_actual, socket)
self.sockets.append(socket)
self.sessions.append(socket_session)
self.sockets_dict[i] = socket
self.socket_controller.add_session(socket_session)
socket.session['socket_session'] = socket_session
socket.send_packet = Mock()
self.socket_controller.server.sockets = self.sockets_dict
def test_broadcast_packet(self):
self.create_clients(10)
packet = {
"hello": "world"
}
self.socket_controller.broadcast_packet(packet)
for s in self.sockets:
s.send_packet.assert_called_with(packet)
def test_send_packet(self):
self.create_clients(5, token_key=[0, 1, 2, 3, 4])
packet = {
"hello": "world"
}
self.socket_controller.send_packet(packet, 2)
self.assertEqual(0, len(self.sockets[0].send_packet.mock_calls))
self.assertEqual(0, len(self.sockets[1].send_packet.mock_calls))
self.sockets[2].send_packet.assert_called_once_with(packet)
self.assertEqual(0, len(self.sockets[3].send_packet.mock_calls))
self.assertEqual(0, len(self.sockets[4].send_packet.mock_calls))
def test_send_packet_multiple_sessions_one_token(self):
self.create_clients(5, token_key=[0, 1, 1, 1, 2])
packet = {
"hello": "world"
}
self.socket_controller.send_packet(packet, 1)
self.assertEqual(0, len(self.sockets[0].send_packet.mock_calls))
self.sockets[1].send_packet.assert_called_once_with(packet)
self.sockets[2].send_packet.assert_called_once_with(packet)
self.sockets[3].send_packet.assert_called_once_with(packet)
self.assertEqual(0, len(self.sockets[4].send_packet.mock_calls))

View File

@@ -327,6 +327,12 @@ class Ec2Inventory(object):
else: else:
self.nested_groups = False self.nested_groups = False
# Replace dash or not in group names
if config.has_option('ec2', 'replace_dash_in_groups'):
self.replace_dash_in_groups = config.getboolean('ec2', 'replace_dash_in_groups')
else:
self.replace_dash_in_groups = True
# Configure which groups should be created. # Configure which groups should be created.
group_by_options = [ group_by_options = [
'group_by_instance_id', 'group_by_instance_id',
@@ -360,7 +366,7 @@ class Ec2Inventory(object):
self.pattern_include = re.compile(pattern_include) self.pattern_include = re.compile(pattern_include)
else: else:
self.pattern_include = None self.pattern_include = None
except configparser.NoOptionError as e: except configparser.NoOptionError:
self.pattern_include = None self.pattern_include = None
# Do we need to exclude hosts that match a pattern? # Do we need to exclude hosts that match a pattern?
@@ -370,7 +376,7 @@ class Ec2Inventory(object):
self.pattern_exclude = re.compile(pattern_exclude) self.pattern_exclude = re.compile(pattern_exclude)
else: else:
self.pattern_exclude = None self.pattern_exclude = None
except configparser.NoOptionError as e: except configparser.NoOptionError:
self.pattern_exclude = None self.pattern_exclude = None
# Instance filters (see boto and EC2 API docs). Ignore invalid filters. # Instance filters (see boto and EC2 API docs). Ignore invalid filters.
@@ -697,7 +703,8 @@ class Ec2Inventory(object):
self.push(self.inventory, key, dest) self.push(self.inventory, key, dest)
if self.nested_groups: if self.nested_groups:
self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k))
self.push_group(self.inventory, self.to_safe("tag_" + k), key) if v:
self.push_group(self.inventory, self.to_safe("tag_" + k), key)
# Inventory: Group by Route53 domain names if enabled # Inventory: Group by Route53 domain names if enabled
if self.route53_enabled and self.group_by_route53_names: if self.route53_enabled and self.group_by_route53_names:
@@ -1285,10 +1292,11 @@ class Ec2Inventory(object):
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower() return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower()
def to_safe(self, word): def to_safe(self, word):
''' Converts 'bad' characters in a string to underscores so they can be ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups '''
used as Ansible groups ''' regex = "[^A-Za-z0-9\_"
if self.replace_dash_in_groups:
return re.sub("[^A-Za-z0-9\_]", "_", word) regex += "\-"
return re.sub(regex + "]", "_", word)
def json_format_dict(self, data, pretty=False): def json_format_dict(self, data, pretty=False):
''' Converts a dict to a JSON object and dumps it as a formatted ''' Converts a dict to a JSON object and dumps it as a formatted
@@ -1302,3 +1310,4 @@ class Ec2Inventory(object):
# Run the script # Run the script
Ec2Inventory() Ec2Inventory()

View File

@@ -2,6 +2,7 @@
# All Rights Reserved. # All Rights Reserved.
import os import os
import re # noqa
import sys import sys
import djcelery import djcelery
from datetime import timedelta from datetime import timedelta
@@ -217,13 +218,13 @@ REST_FRAMEWORK = {
} }
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'awx.main.backend.LDAPBackend', 'awx.sso.backends.LDAPBackend',
'radiusauth.backends.RADIUSBackend', 'awx.sso.backends.RADIUSBackend',
'social.backends.google.GoogleOAuth2', 'social.backends.google.GoogleOAuth2',
'social.backends.github.GithubOAuth2', 'social.backends.github.GithubOAuth2',
'social.backends.github.GithubOrganizationOAuth2', 'social.backends.github.GithubOrganizationOAuth2',
'social.backends.github.GithubTeamOAuth2', 'social.backends.github.GithubTeamOAuth2',
'social.backends.saml.SAMLAuth', 'awx.sso.backends.SAMLAuth',
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
@@ -355,13 +356,15 @@ SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_user', 'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username', 'social.pipeline.user.get_username',
'social.pipeline.social_auth.associate_by_email', 'social.pipeline.social_auth.associate_by_email',
'social.pipeline.mail.mail_validation',
'social.pipeline.user.create_user', 'social.pipeline.user.create_user',
'awx.sso.pipeline.check_user_found_or_created',
'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data', 'social.pipeline.social_auth.load_extra_data',
'awx.sso.pipeline.set_is_active_for_new_user', 'awx.sso.pipeline.set_is_active_for_new_user',
'social.pipeline.user.user_details', 'social.pipeline.user.user_details',
'awx.sso.pipeline.prevent_inactive_login', 'awx.sso.pipeline.prevent_inactive_login',
'awx.sso.pipeline.update_user_orgs',
'awx.sso.pipeline.update_user_teams',
) )
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '' SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
@@ -370,14 +373,17 @@ SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['profile']
SOCIAL_AUTH_GITHUB_KEY = '' SOCIAL_AUTH_GITHUB_KEY = ''
SOCIAL_AUTH_GITHUB_SECRET = '' SOCIAL_AUTH_GITHUB_SECRET = ''
SOCIAL_AUTH_GITHUB_SCOPE = ['user:email', 'read:org']
SOCIAL_AUTH_GITHUB_ORG_KEY = '' SOCIAL_AUTH_GITHUB_ORG_KEY = ''
SOCIAL_AUTH_GITHUB_ORG_SECRET = '' SOCIAL_AUTH_GITHUB_ORG_SECRET = ''
SOCIAL_AUTH_GITHUB_ORG_NAME = '' SOCIAL_AUTH_GITHUB_ORG_NAME = ''
SOCIAL_AUTH_GITHUB_ORG_SCOPE = ['user:email', 'read:org']
SOCIAL_AUTH_GITHUB_TEAM_KEY = '' SOCIAL_AUTH_GITHUB_TEAM_KEY = ''
SOCIAL_AUTH_GITHUB_TEAM_SECRET = '' SOCIAL_AUTH_GITHUB_TEAM_SECRET = ''
SOCIAL_AUTH_GITHUB_TEAM_ID = '' SOCIAL_AUTH_GITHUB_TEAM_ID = ''
SOCIAL_AUTH_GITHUB_TEAM_SCOPE = ['user:email', 'read:org']
SOCIAL_AUTH_SAML_SP_ENTITY_ID = '' SOCIAL_AUTH_SAML_SP_ENTITY_ID = ''
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = '' SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = ''
@@ -400,6 +406,9 @@ SOCIAL_AUTH_CLEAN_USERNAMES = True
SOCIAL_AUTH_SANITIZE_REDIRECTS = True SOCIAL_AUTH_SANITIZE_REDIRECTS = True
SOCIAL_AUTH_REDIRECT_IS_HTTPS = False SOCIAL_AUTH_REDIRECT_IS_HTTPS = False
SOCIAL_AUTH_ORGANIZATION_MAP = {}
SOCIAL_AUTH_TEAM_MAP = {}
# Any ANSIBLE_* settings will be passed to the subprocess environment by the # Any ANSIBLE_* settings will be passed to the subprocess environment by the
# celery task. # celery task.
@@ -572,13 +581,19 @@ VMWARE_EXCLUDE_EMPTY_GROUPS = True
# provide a list here. # provide a list here.
# Source: https://developers.google.com/compute/docs/zones # Source: https://developers.google.com/compute/docs/zones
GCE_REGION_CHOICES = [ GCE_REGION_CHOICES = [
('us-east1-b', 'US East (B)'),
('us-east1-c', 'US East (C)'),
('us-east1-d', 'US East (D)'),
('us-central1-a', 'US Central (A)'), ('us-central1-a', 'US Central (A)'),
('us-central1-b', 'US Central (B)'), ('us-central1-b', 'US Central (B)'),
('us-central1-c', 'US Central (C)'),
('us-central1-f', 'US Central (F)'), ('us-central1-f', 'US Central (F)'),
('europe-west1-a', 'Europe West (A)'),
('europe-west1-b', 'Europe West (B)'), ('europe-west1-b', 'Europe West (B)'),
('europe-west1-c', 'Europe West (C)'),
('europe-west1-d', 'Europe West (D)'),
('asia-east1-a', 'Asia East (A)'), ('asia-east1-a', 'Asia East (A)'),
('asia-east1-b', 'Asia East (B)'), ('asia-east1-b', 'Asia East (B)'),
('asia-east1-c', 'Asia East (C)'),
] ]
GCE_REGIONS_BLACKLIST = [] GCE_REGIONS_BLACKLIST = []
@@ -715,7 +730,7 @@ LOGGING = {
'level': 'WARNING', 'level': 'WARNING',
'class':'logging.handlers.RotatingFileHandler', 'class':'logging.handlers.RotatingFileHandler',
'filters': ['require_debug_false'], 'filters': ['require_debug_false'],
'filename': os.path.join(LOG_ROOT, 'tower_warnings.log'), 'filename': os.path.join(LOG_ROOT, 'tower.log'),
'maxBytes': 1024 * 1024 * 5, # 5 MB 'maxBytes': 1024 * 1024 * 5, # 5 MB
'backupCount': 5, 'backupCount': 5,
'formatter':'simple', 'formatter':'simple',
@@ -807,7 +822,12 @@ LOGGING = {
'propagate': False, 'propagate': False,
}, },
'django_auth_ldap': { 'django_auth_ldap': {
'handlers': ['null'], 'handlers': ['console', 'file', 'tower_warnings'],
'level': 'DEBUG',
},
'social': {
'handlers': ['console', 'file', 'tower_warnings'],
'level': 'DEBUG',
}, },
} }
} }

View File

@@ -525,8 +525,80 @@ SOCIAL_AUTH_SAML_ENABLED_IDPS = {
# 'url': 'https://myidp.example.com/sso', # 'url': 'https://myidp.example.com/sso',
# 'x509cert': '', # 'x509cert': '',
#}, #},
#'onelogin': {
# 'entity_id': 'https://app.onelogin.com/saml/metadata/123456',
# 'url': 'https://example.onelogin.com/trust/saml2/http-post/sso/123456',
# 'x509cert': '',
# 'attr_user_permanent_id': 'name_id',
# 'attr_first_name': 'User.FirstName',
# 'attr_last_name': 'User.LastName',
# 'attr_username': 'User.email',
# 'attr_email': 'User.email',
#},
} }
SOCIAL_AUTH_ORGANIZATION_MAP = {
# Add all users to the default organization.
'Default': {
'users': True,
},
#'Test Org': {
# 'admins': ['admin@example.com'],
# 'users': True,
#},
#'Test Org 2': {
# 'admins': ['admin@example.com', re.compile(r'^tower-[^@]+*?@.*$],
# 'users': re.compile(r'^[^@].*?@example\.com$'),
#},
}
#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {}
#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP = {}
#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP = {}
#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP = {}
#SOCIAL_AUTH_SAML_ORGANIZATION_MAP = {}
SOCIAL_AUTH_TEAM_MAP = {
#'My Team': {
# 'organization': 'Test Org',
# 'users': ['re.compile(r'^[^@]+?@test\.example\.com$')'],
# 'remove': True,
#},
#'Other Team': {
# 'organization': 'Test Org 2',
# 'users': re.compile(r'^[^@]+?@test2\.example\.com$'),
# 'remove': False,
#},
}
#SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {}
#SOCIAL_AUTH_GITHUB_TEAM_MAP = {}
#SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP = {}
#SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP = {}
#SOCIAL_AUTH_SAML_TEAM_MAP = {}
# Uncomment one or more of the lines below to prevent new user accounts from
# being created for the selected social auth providers. 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.
#SOCIAL_AUTH_USER_FIELDS = []
#SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = []
#SOCIAL_AUTH_GITHUB_USER_FIELDS = []
#SOCIAL_AUTH_GITHUB_ORG_USER_FIELDS = []
#SOCIAL_AUTH_GITHUB_TEAM_USER_FIELDS = []
#SOCIAL_AUTH_SAML_USER_FIELDS = []
# It is also possible to add custom functions to the social auth pipeline for
# more advanced organization and team mapping. Use at your own risk.
#def custom_social_auth_pipeline_function(backend, details, user=None, *args, **kwargs):
# print 'custom:', backend, details, user, args, kwargs
#SOCIAL_AUTH_PIPELINE += (
# 'awx.settings.development.custom_social_auth_pipeline_function',
#)
############################################################################### ###############################################################################
# INVENTORY IMPORT TEST SETTINGS # INVENTORY IMPORT TEST SETTINGS
############################################################################### ###############################################################################

View File

@@ -523,8 +523,80 @@ SOCIAL_AUTH_SAML_ENABLED_IDPS = {
# 'url': 'https://myidp.example.com/sso', # 'url': 'https://myidp.example.com/sso',
# 'x509cert': '', # 'x509cert': '',
#}, #},
#'onelogin': {
# 'entity_id': 'https://app.onelogin.com/saml/metadata/123456',
# 'url': 'https://example.onelogin.com/trust/saml2/http-post/sso/123456',
# 'x509cert': '',
# 'attr_user_permanent_id': 'name_id',
# 'attr_first_name': 'User.FirstName',
# 'attr_last_name': 'User.LastName',
# 'attr_username': 'User.email',
# 'attr_email': 'User.email',
#},
} }
SOCIAL_AUTH_ORGANIZATION_MAP = {
# Add all users to the default organization.
'Default': {
'users': True,
},
#'Test Org': {
# 'admins': ['admin@example.com'],
# 'users': True,
#},
#'Test Org 2': {
# 'admins': ['admin@example.com', re.compile(r'^tower-[^@]+*?@.*$],
# 'users': re.compile(r'^[^@].*?@example\.com$'),
#},
}
#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {}
#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP = {}
#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP = {}
#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP = {}
#SOCIAL_AUTH_SAML_ORGANIZATION_MAP = {}
SOCIAL_AUTH_TEAM_MAP = {
#'My Team': {
# 'organization': 'Test Org',
# 'users': ['re.compile(r'^[^@]+?@test\.example\.com$')'],
# 'remove': True,
#},
#'Other Team': {
# 'organization': 'Test Org 2',
# 'users': re.compile(r'^[^@]+?@test2\.example\.com$'),
# 'remove': False,
#},
}
#SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {}
#SOCIAL_AUTH_GITHUB_TEAM_MAP = {}
#SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP = {}
#SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP = {}
#SOCIAL_AUTH_SAML_TEAM_MAP = {}
# Uncomment one or more of the lines below to prevent new user accounts from
# being created for the selected social auth providers. 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.
#SOCIAL_AUTH_USER_FIELDS = []
#SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = []
#SOCIAL_AUTH_GITHUB_USER_FIELDS = []
#SOCIAL_AUTH_GITHUB_ORG_USER_FIELDS = []
#SOCIAL_AUTH_GITHUB_TEAM_USER_FIELDS = []
#SOCIAL_AUTH_SAML_USER_FIELDS = []
# It is also possible to add custom functions to the social auth pipeline for
# more advanced organization and team mapping. Use at your own risk.
#def custom_social_auth_pipeline_function(backend, details, user=None, *args, **kwargs):
# print 'custom:', backend, details, user, args, kwargs
#SOCIAL_AUTH_PIPELINE += (
# 'awx.settings.development.custom_social_auth_pipeline_function',
#)
############################################################################### ###############################################################################
# INVENTORY IMPORT TEST SETTINGS # INVENTORY IMPORT TEST SETTINGS
############################################################################### ###############################################################################

View File

@@ -7,10 +7,10 @@
# settings as needed. # settings as needed.
if not AUTH_LDAP_SERVER_URI: if not AUTH_LDAP_SERVER_URI:
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.main.backend.LDAPBackend'] AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.LDAPBackend']
if not RADIUS_SERVER: if not RADIUS_SERVER:
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'radiusauth.backends.RADIUSBackend'] AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.RADIUSBackend']
if not all([SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET]): if not all([SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET]):
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.google.GoogleOAuth2'] AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.google.GoogleOAuth2']
@@ -28,8 +28,7 @@ if not all([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_SP_PRIVATE_KEY, SOCIAL_AUTH_SAML_ORG_INFO,
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, SOCIAL_AUTH_SAML_SUPPORT_CONTACT, SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
SOCIAL_AUTH_SAML_ENABLED_IDPS]): SOCIAL_AUTH_SAML_ENABLED_IDPS]):
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.saml.SAMLAuth'] AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.SAMLAuth']
if not AUTH_BASIC_ENABLED: if not AUTH_BASIC_ENABLED:
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [x for x in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] if x != 'rest_framework.authentication.BasicAuthentication'] REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [x for x in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] if x != 'rest_framework.authentication.BasicAuthentication']

232
awx/sso/backends.py Normal file
View File

@@ -0,0 +1,232 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Python
import logging
# Django
from django.dispatch import receiver
from django.conf import settings as django_settings
# django-auth-ldap
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend
from django_auth_ldap.backend import populate_user
# radiusauth
from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend
# social
from social.backends.saml import OID_USERID
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
logger = logging.getLogger('awx.sso.backends')
class LDAPSettings(BaseLDAPSettings):
defaults = dict(BaseLDAPSettings.defaults.items() + {
'ORGANIZATION_MAP': {},
'TEAM_MAP': {},
}.items())
class LDAPBackend(BaseLDAPBackend):
'''
Custom LDAP backend for AWX.
'''
settings_prefix = 'AUTH_LDAP_'
def _get_settings(self):
if self._settings is None:
self._settings = LDAPSettings(self.settings_prefix)
return self._settings
def _set_settings(self, settings):
self._settings = settings
settings = property(_get_settings, _set_settings)
def authenticate(self, username, password):
if not self.settings.SERVER_URI:
return None
if not feature_enabled('ldap'):
logger.error("Unable to authenticate, license does not support LDAP authentication")
return None
return super(LDAPBackend, self).authenticate(username, password)
def get_user(self, user_id):
if not self.settings.SERVER_URI:
return None
if not feature_enabled('ldap'):
logger.error("Unable to get_user, license does not support LDAP authentication")
return None
return super(LDAPBackend, self).get_user(user_id)
# Disable any LDAP based authorization / permissions checking.
def has_perm(self, user, perm, obj=None):
return False
def has_module_perms(self, user, app_label):
return False
def get_all_permissions(self, user, obj=None):
return set()
def get_group_permissions(self, user, obj=None):
return set()
class RADIUSBackend(BaseRADIUSBackend):
'''
Custom Radius backend to verify license status
'''
def authenticate(self, username, password):
if not django_settings.RADIUS_SERVER:
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to authenticate, license does not support RADIUS authentication")
return None
return super(RADIUSBackend, self).authenticate(username, password)
def get_user(self, user_id):
if not django_settings.RADIUS_SERVER:
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to get_user, license does not support RADIUS authentication")
return None
return super(RADIUSBackend, self).get_user(user_id)
class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider):
'''
Custom Identity Provider to make attributes to what we expect.
'''
def get_user_permanent_id(self, attributes):
uid = attributes[self.conf.get('attr_user_permanent_id', OID_USERID)]
if isinstance(uid, basestring):
return uid
return uid[0]
def get_attr(self, attributes, conf_key, default_attribute):
"""
Get the attribute 'default_attribute' out of the attributes,
unless self.conf[conf_key] overrides the default by specifying
another attribute to use.
"""
key = self.conf.get(conf_key, default_attribute)
value = attributes[key][0] if key in attributes else None
if conf_key in ('attr_first_name', 'attr_last_name', 'attr_username', 'attr_email') and value is None:
logger.warn("Could not map user detail '%s' from SAML attribute '%s'; "
"update SOCIAL_AUTH_SAML_ENABLED_IDPS['%s']['%s'] with the correct SAML attribute.",
conf_key[5:], key, self.name, conf_key)
return unicode(value) if value is not None else value
class SAMLAuth(BaseSAMLAuth):
'''
Custom SAMLAuth backend to verify license status
'''
def get_idp(self, idp_name):
idp_config = self.setting('ENABLED_IDPS')[idp_name]
return TowerSAMLIdentityProvider(idp_name, **idp_config)
def authenticate(self, *args, **kwargs):
if not all([django_settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID, django_settings.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT,
django_settings.SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, django_settings.SOCIAL_AUTH_SAML_ORG_INFO,
django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS]):
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to authenticate, license does not support SAML authentication")
return None
return super(SAMLAuth, self).authenticate(*args, **kwargs)
def get_user(self, user_id):
if not all([django_settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID, django_settings.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT,
django_settings.SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, django_settings.SOCIAL_AUTH_SAML_ORG_INFO,
django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS]):
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to get_user, license does not support SAML authentication")
return None
return super(SAMLAuth, self).get_user(user_id)
def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=False):
'''
Hepler function to update m2m relationship based on LDAP group membership.
'''
should_add = False
if opts is None:
return
elif not opts:
pass
elif opts is True:
should_add = True
else:
if isinstance(opts, basestring):
opts = [opts]
for group_dn in opts:
if not isinstance(group_dn, basestring):
continue
if ldap_user._get_groups().is_member_of(group_dn):
should_add = True
if should_add:
rel.add(user)
elif remove:
rel.remove(user)
@receiver(populate_user)
def on_populate_user(sender, **kwargs):
'''
Handle signal from LDAP backend to populate the user object. Update user
organization/team memberships according to their LDAP groups.
'''
from awx.main.models import Organization, Team
user = kwargs['user']
ldap_user = kwargs['ldap_user']
backend = ldap_user.backend
# Update organization membership based on group memberships.
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
for org_name, org_opts in org_map.items():
org, created = Organization.objects.get_or_create(name=org_name)
remove = bool(org_opts.get('remove', False))
admins_opts = org_opts.get('admins', None)
remove_admins = bool(org_opts.get('remove_admins', remove))
_update_m2m_from_groups(user, ldap_user, org.admins, admins_opts,
remove_admins)
users_opts = org_opts.get('users', None)
remove_users = bool(org_opts.get('remove_users', remove))
_update_m2m_from_groups(user, ldap_user, org.users, users_opts,
remove_users)
# Update team membership based on group memberships.
team_map = getattr(backend.settings, 'TEAM_MAP', {})
for team_name, team_opts in team_map.items():
if 'organization' not in team_opts:
continue
org, created = Organization.objects.get_or_create(name=team_opts['organization'])
team, created = Team.objects.get_or_create(name=team_name, organization=org)
users_opts = team_opts.get('users', None)
remove = bool(team_opts.get('remove', False))
_update_m2m_from_groups(user, ldap_user, team.users, users_opts,
remove)
# Update user profile to store LDAP DN.
profile = user.profile
if profile.ldap_dn != ldap_user.dn:
profile.ldap_dn = ldap_user.dn
profile.save()

View File

@@ -17,13 +17,13 @@ from social.exceptions import SocialAuthBaseException
from social.utils import social_logger from social.utils import social_logger
from social.apps.django_app.middleware import SocialAuthExceptionMiddleware from social.apps.django_app.middleware import SocialAuthExceptionMiddleware
# Ansible Tower
from awx.main.models import AuthToken from awx.main.models import AuthToken
class SocialAuthMiddleware(SocialAuthExceptionMiddleware): class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
def process_request(self, request): def process_request(self, request):
request.META['SERVER_PORT'] = 80 # FIXME
token_key = request.COOKIES.get('token', '') token_key = request.COOKIES.get('token', '')
token_key = urllib.quote(urllib.unquote(token_key).strip('"')) token_key = urllib.quote(urllib.unquote(token_key).strip('"'))
@@ -63,7 +63,7 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
if strategy is None or self.raise_exception(request, exception): if strategy is None or self.raise_exception(request, exception):
return return
if isinstance(exception, SocialAuthBaseException): if isinstance(exception, SocialAuthBaseException) or request.path.startswith('/sso/'):
backend = getattr(request, 'backend', None) backend = getattr(request, 'backend', None)
backend_name = getattr(backend, 'name', 'unknown-backend') backend_name = getattr(backend, 'name', 'unknown-backend')
full_backend_name = backend_name full_backend_name = backend_name

View File

@@ -1,17 +1,38 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import re
# Python Social Auth # Python Social Auth
from social.exceptions import AuthException from social.exceptions import AuthException
# Tower
from awx.api.license import feature_enabled
class AuthNotFound(AuthException):
def __init__(self, backend, email_or_uid, *args, **kwargs):
self.email_or_uid = email_or_uid
super(AuthNotFound, self).__init__(backend, *args, **kwargs)
def __str__(self):
return 'An account cannot be found for {0}'.format(self.email_or_uid)
class AuthInactive(AuthException): class AuthInactive(AuthException):
"""Authentication for this user is forbidden"""
def __str__(self): def __str__(self):
return 'Your account is inactive' return 'Your account is inactive'
def check_user_found_or_created(backend, details, user=None, *args, **kwargs):
if not user:
email_or_uid = details.get('email') or kwargs.get('email') or kwargs.get('uid') or '???'
raise AuthNotFound(backend, email_or_uid)
def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs): def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs):
if kwargs.get('is_new', False): if kwargs.get('is_new', False):
details['is_active'] = True details['is_active'] = True
@@ -21,3 +42,96 @@ def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs):
def prevent_inactive_login(backend, details, user=None, *args, **kwargs): def prevent_inactive_login(backend, details, user=None, *args, **kwargs):
if user and not user.is_active: if user and not user.is_active:
raise AuthInactive(backend) raise AuthInactive(backend)
def _update_m2m_from_expression(user, rel, expr, remove=False):
'''
Helper function to update m2m relationship based on user matching one or
more expressions.
'''
should_add = False
if expr is None:
return
elif not expr:
pass
elif expr is True:
should_add = True
else:
if isinstance(expr, (basestring, type(re.compile('')))):
expr = [expr]
for ex in expr:
if isinstance(ex, basestring):
if user.username == ex or user.email == ex:
should_add = True
elif isinstance(ex, type(re.compile(''))):
if ex.match(user.username) or ex.match(user.email):
should_add = True
if should_add:
rel.add(user)
elif remove:
rel.remove(user)
def update_user_orgs(backend, details, user=None, *args, **kwargs):
'''
Update organization memberships for the given user based on mapping rules
defined in settings.
'''
if not user:
return
from awx.main.models import Organization
multiple_orgs = feature_enabled('multiple_organizations')
org_map = backend.setting('ORGANIZATION_MAP') or {}
for org_name, org_opts in org_map.items():
# Get or create the org to update. If the license only allows for one
# org, always use the first active org, unless no org exists.
if multiple_orgs:
org = Organization.objects.get_or_create(name=org_name)[0]
else:
try:
org = Organization.objects.filter(active=True).order_by('pk')[0]
except IndexError:
continue
# Update org admins from expression(s).
remove = bool(org_opts.get('remove', False))
admins_expr = org_opts.get('admins', None)
remove_admins = bool(org_opts.get('remove_admins', remove))
_update_m2m_from_expression(user, org.admins, admins_expr, remove_admins)
# Update org users from expression(s).
users_expr = org_opts.get('users', None)
remove_users = bool(org_opts.get('remove_users', remove))
_update_m2m_from_expression(user, org.users, users_expr, remove_users)
def update_user_teams(backend, details, user=None, *args, **kwargs):
'''
Update team memberships for the given user based on mapping rules defined
in settings.
'''
if not user:
return
from awx.main.models import Organization, Team
multiple_orgs = feature_enabled('multiple_organizations')
team_map = backend.setting('TEAM_MAP') or {}
for team_name, team_opts in team_map.items():
# Get or create the org to update. If the license only allows for one
# org, always use the first active org, unless no org exists.
if multiple_orgs:
if 'organization' not in team_opts:
continue
org = Organization.objects.get_or_create(name=team_opts['organization'])[0]
else:
try:
org = Organization.objects.filter(active=True).order_by('pk')[0]
except IndexError:
continue
# Update team members from expression(s).
team = Team.objects.get_or_create(name=team_name, organization=org)[0]
users_expr = team_opts.get('users', None)
remove = bool(team_opts.get('remove', False))
_update_m2m_from_expression(user, team.users, users_expr, remove)

View File

@@ -1,7 +1,7 @@
<div id="about-dialog-body"> <div id="about-dialog-body">
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-5"> <div class="col-xs-12 col-sm-5 About-cowsay">
<div style="width: 340px; margin: 0 auto;"> <div style="width: 340px; margin: 0 auto;">
<pre id="cowsay"> <pre id="cowsay">
________________ ________________
@@ -19,10 +19,11 @@
<div class="col-xs-12 col-sm-7 text-center"> <div class="col-xs-12 col-sm-7 text-center">
<img id="about-modal-titlelogo" src="/static/assets/ansible_tower_logo_minimalc.png"><br> <img id="about-modal-titlelogo" src="/static/assets/ansible_tower_logo_minimalc.png"><br>
<p>Copyright 2015. All rights reserved.</p> <p>Copyright 2015. All rights reserved.</p>
<p>Ansible and Ansible Tower are registered trademarks of Ansible, Inc.</p> <p>Ansible and Ansible Tower are registered trademarks of Red Hat, Inc.</p>
<br>
<img class="About-redhat" src="/static/assets/redhat_ansible_lockup.png">
<br> <br>
<p>Visit <a href="http://www.ansible.com" target="_blank">Ansible.com</a> for more information.</p> <p>Visit <a href="http://www.ansible.com" target="_blank">Ansible.com</a> for more information.</p>
<br>
<p><span id='about-modal-subscription'></span></p> <p><span id='about-modal-subscription'></span></p>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,14 @@
/** @define About */
.About {
height: 309px !important;
}
.About-cowsay {
margin-top: 30px;
}
.About-redhat {
max-width: 100%;
margin-top: -61px;
margin-bottom: -33px;
}

View File

@@ -837,9 +837,9 @@ var tower = angular.module('Tower', [
}]); }]);
}]) }])
.run(['$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'HideStream', 'Socket', .run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'HideStream', 'Socket',
'LoadConfig', 'Store', 'ShowSocketHelp', 'AboutAnsibleHelp', 'pendoService', 'LoadConfig', 'Store', 'ShowSocketHelp', 'AboutAnsibleHelp', 'pendoService',
function ($compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, HideStream, Socket, function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, HideStream, Socket,
LoadConfig, Store, ShowSocketHelp, AboutAnsibleHelp, pendoService) { LoadConfig, Store, ShowSocketHelp, AboutAnsibleHelp, pendoService) {
@@ -950,7 +950,8 @@ var tower = angular.module('Tower', [
control_socket.init(); control_socket.init();
control_socket.on("limit_reached", function(data) { control_socket.on("limit_reached", function(data) {
$log.debug(data.reason); $log.debug(data.reason);
Timer.expireSession('session_limit'); $rootScope.sessionTimer.expireSession('session_limit');
$location.url('/login');
}); });
} }
openSocket(); openSocket();
@@ -999,7 +1000,7 @@ var tower = angular.module('Tower', [
// gets here on timeout // gets here on timeout
if (next.templateUrl !== (urlPrefix + 'login/loginBackDrop.partial.html')) { if (next.templateUrl !== (urlPrefix + 'login/loginBackDrop.partial.html')) {
$rootScope.sessionTimer.expireSession('idle'); $rootScope.sessionTimer.expireSession('idle');
if (sock) { if (sock&& sock.socket && sock.socket.socket) {
sock.socket.socket.disconnect(); sock.socket.socket.disconnect();
} }
$location.path('/login'); $location.path('/login');
@@ -1026,9 +1027,11 @@ var tower = angular.module('Tower', [
$rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser'); $rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser');
// when the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket) // when the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket)
if(!_.contains($location.$$url, '/login')){ if(!_.contains($location.$$url, '/login')){
$rootScope.sessionTimer = Timer.init(); Timer.init().then(function(timer){
$rootScope.$emit('OpenSocket'); $rootScope.sessionTimer = timer;
pendoService.issuePendoIdentity(); $rootScope.$emit('OpenSocket');
pendoService.issuePendoIdentity();
});
} }
} }
@@ -1063,8 +1066,8 @@ var tower = angular.module('Tower', [
if (!$AnsibleConfig) { if (!$AnsibleConfig) {
// there may be time lag when loading the config file, so temporarily use what's in local storage // create a promise that will resolve when $AnsibleConfig is loaded
$AnsibleConfig = Store('AnsibleConfig'); $rootScope.loginConfig = $q.defer();
} }
//the authorization controller redirects to the home page automatcially if there is no last path defined. in order to override //the authorization controller redirects to the home page automatcially if there is no last path defined. in order to override

View File

@@ -13,7 +13,7 @@
export function JobDetailController ($location, $rootScope, $filter, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest, export function JobDetailController ($location, $rootScope, $filter, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest,
ProcessErrors, SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph, LoadHostSummary, ReloadHostSummaryList, JobIsFinished, SetTaskStyles, DigestEvent, ProcessErrors, SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph, LoadHostSummary, ReloadHostSummaryList, JobIsFinished, SetTaskStyles, DigestEvent,
UpdateDOM, EventViewer, DeleteJob, PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts, HostsEdit, ParseVariableString, GetChoices) { UpdateDOM, EventViewer, DeleteJob, PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts, HostsEdit, ParseVariableString, GetChoices, fieldChoices, fieldLabels) {
ClearScope(); ClearScope();
@@ -27,11 +27,33 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
scope.plays = []; scope.plays = [];
scope.previousTaskFailed = false;
scope.$watch('job_status', function(job_status) { scope.$watch('job_status', function(job_status) {
if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") { if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") {
scope.previousTaskFailed = true;
var taskObj = JSON.parse(job_status.explanation.substring(job_status.explanation.split(":")[0].length + 1)); var taskObj = JSON.parse(job_status.explanation.substring(job_status.explanation.split(":")[0].length + 1));
job_status.explanation = job_status.explanation.split(":")[0] + ". "; // return a promise from the options request with the permission type choices (including adhoc) as a param
job_status.explanation += "<code>" + taskObj.task_type + "-" + taskObj.task_id + " failed for " + taskObj.task_name + "</code>" var fieldChoice = fieldChoices({
scope: $scope,
url: 'api/v1/unified_jobs/',
field: 'type'
});
// manipulate the choices from the options request to be set on
// scope and be usable by the list form
fieldChoice.then(function (choices) {
choices =
fieldLabels({
choices: choices
});
scope.explanation_fail_type = choices[taskObj.job_type];
scope.explanation_fail_name = taskObj.job_name;
scope.explanation_fail_id = taskObj.job_id;
scope.task_detail = scope.explanation_fail_type + " failed for " + scope.explanation_fail_name + " with ID " + scope.explanation_fail_id + ".";
});
} else {
scope.previousTaskFailed = false;
} }
}, true); }, true);
@@ -649,61 +671,6 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
}); });
if (scope.removeGetCredentialNames) {
scope.removeGetCredentialNames();
}
scope.removeGetCredentialNames = scope.$on('GetCredentialNames', function(e, data) {
var url;
if (data.credential) {
url = GetBasePath('credentials') + data.credential + '/';
Rest.setUrl(url);
Rest.get()
.success( function(data) {
scope.credential_name = data.name;
})
.error( function(data, status) {
scope.credential_name = '';
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
}
if (data.cloud_credential) {
url = GetBasePath('credentials') + data.cloud_credential + '/';
Rest.setUrl(url);
Rest.get()
.success( function(data) {
scope.cloud_credential_name = data.name;
})
.error( function(data, status) {
scope.credential_name = '';
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
}
});
if (scope.removeGetCreatedByNames) {
scope.removeGetCreatedByNames();
}
scope.removeGetCreatedByNames = scope.$on('GetCreatedByNames', function(e, data) {
var url;
data = data.slice(0, data.length-1);
data = data.slice(data.lastIndexOf('/')+1, data.length);
url = GetBasePath('users') + data + '/';
scope.users_url = '/#/users/' + data;
Rest.setUrl(url);
Rest.get()
.success( function(data) {
scope.created_by = data.username;
})
.error( function(data, status) {
scope.credential_name = '';
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
});
if (scope.removeLoadJob) { if (scope.removeLoadJob) {
scope.removeLoadJob(); scope.removeLoadJob();
} }
@@ -738,6 +705,24 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
scope.verbosity = data.verbosity; scope.verbosity = data.verbosity;
scope.job_tags = data.job_tags; scope.job_tags = data.job_tags;
scope.variables = ParseVariableString(data.extra_vars); scope.variables = ParseVariableString(data.extra_vars);
scope.users_url = (data.summary_fields.created_by) ? '/#/users/' + data.summary_fields.created_by.id : '';
scope.created_by = (data.summary_fields.created_by) ? data.summary_fields.created_by.username : '';
if (data.summary_fields.credential) {
scope.credential_name = data.summary_fields.credential.name;
scope.credential_url = data.related.credential
.replace('api/v1', '#');
} else {
scope.credential_name = "";
}
if (data.summary_fields.cloud_credential) {
scope.cloud_credential_name = data.summary_fields.cloud_credential.name;
scope.cloud_credential_url = data.related.cloud_credential
.replace('api/v1', '#');
} else {
scope.cloud_credential_name = "";
}
for (i=0; i < verbosity_options.length; i++) { for (i=0; i < verbosity_options.length; i++) {
if (verbosity_options[i].value === data.verbosity) { if (verbosity_options[i].value === data.verbosity) {
@@ -792,10 +777,6 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
}); });
//scope.setSearchAll('host'); //scope.setSearchAll('host');
scope.$emit('LoadPlays', data.related.job_events); scope.$emit('LoadPlays', data.related.job_events);
scope.$emit('GetCreatedByNames', data.related.created_by);
if (!scope.credential_name) {
scope.$emit('GetCredentialNames', data);
}
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!', ProcessErrors(scope, data, status, null, { hdr: 'Error!',
@@ -1415,5 +1396,5 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
JobDetailController.$inject = [ '$location', '$rootScope', '$filter', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', JobDetailController.$inject = [ '$location', '$rootScope', '$filter', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath',
'Wait', 'Rest', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList', 'Wait', 'Rest', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList',
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'EventViewer', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer', 'LoadPlays', 'LoadTasks', 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'EventViewer', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer', 'LoadPlays', 'LoadTasks',
'LoadHosts', 'HostsEdit', 'ParseVariableString', 'GetChoices' 'LoadHosts', 'HostsEdit', 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels'
]; ];

View File

@@ -13,7 +13,7 @@
export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, ProjectList, GenerateList, LoadBreadCrumbs, export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, ProjectList, GenerateList, LoadBreadCrumbs,
Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate,
Refresh, Wait, Stream, GetChoices, Empty, Find, LogViewer, GetProjectIcon, GetProjectToolTip) { Refresh, Wait, Stream, GetChoices, Empty, Find, LogViewer, GetProjectIcon, GetProjectToolTip, $filter) {
ClearScope(); ClearScope();
@@ -309,29 +309,37 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams,
}); });
$scope.cancelUpdate = function (id, name) { $scope.cancelUpdate = function (id, name) {
// Start the cancel process // // Start the cancel process
var i, project, found = false; // var i, project, found = false;
for (i = 0; i < $scope.projects.length; i++) { // for (i = 0; i < $scope.projects.length; i++) {
if ($scope.projects[i].id === id) { // if ($scope.projects[i].id === id) {
project = $scope.projects[i]; // project = $scope.projects[i];
found = true; // found = true;
break; // break;
} // }
} // }
if (found && project.related.current_update) { Rest.setUrl(GetBasePath("projects") + id);
Rest.setUrl(project.related.current_update); Rest.get()
Rest.get() .success(function (data) {
.success(function (data) { if (data.related.current_update) {
$scope.$emit('Check_Cancel', data); Rest.setUrl(data.related.current_update);
}) Rest.get()
.error(function (data, status) { .success(function (data) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', $scope.$emit('Check_Cancel', data);
msg: 'Call to ' + project.related.current_update + ' failed. GET status: ' + status }); })
}); .error(function (data, status) {
} else { ProcessErrors($scope, data, status, null, { hdr: 'Error!',
Alert('Update Not Found', 'An SCM update does not appear to be running for project: ' + name + '. Click the <em>Refresh</em> ' + msg: 'Call to ' + data.related.current_update + ' failed. GET status: ' + status });
'button to view the latet status.', 'alert-info'); });
} } else {
Alert('Update Not Found', 'An SCM update does not appear to be running for project: ' + $filter('sanitize')(name) + '. Click the <em>Refresh</em> ' +
'button to view the latest status.', 'alert-info',undefined,undefined,undefined,undefined,true);
}
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to get project failed. GET status: ' + status });
})
}; };
$scope.refresh = function () { $scope.refresh = function () {
@@ -384,7 +392,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams,
ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'ProjectList', 'generateList', ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'ProjectList', 'generateList',
'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath',
'SelectionInit', 'ProjectUpdate', 'Refresh', 'Wait', 'Stream', 'GetChoices', 'Empty', 'Find', 'SelectionInit', 'ProjectUpdate', 'Refresh', 'Wait', 'Stream', 'GetChoices', 'Empty', 'Find',
'LogViewer', 'GetProjectIcon', 'GetProjectToolTip' 'LogViewer', 'GetProjectIcon', 'GetProjectToolTip', '$filter'
]; ];

View File

@@ -176,7 +176,7 @@ TeamsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$r
export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeParams, TeamForm, GenerateForm, Rest, Alert, ProcessErrors, export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeParams, TeamForm, GenerateForm, Rest, Alert, ProcessErrors,
LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, LookUpInit, Prompt, GetBasePath, CheckAccess, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, LookUpInit, Prompt, GetBasePath, CheckAccess,
OrganizationList, Wait, Stream, permissionsChoices, permissionsLabel, permissionsSearchSelect) { OrganizationList, Wait, Stream, fieldChoices, fieldLabels, permissionsSearchSelect) {
ClearScope(); ClearScope();
@@ -192,16 +192,17 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
$scope.permission_search_select = []; $scope.permission_search_select = [];
// return a promise from the options request with the permission type choices (including adhoc) as a param // return a promise from the options request with the permission type choices (including adhoc) as a param
var permissionsChoice = permissionsChoices({ var permissionsChoice = fieldChoices({
scope: $scope, scope: $scope,
url: 'api/v1/' + base + '/' + id + '/permissions/' url: 'api/v1/' + base + '/' + id + '/permissions/',
field: 'permission_type'
}); });
// manipulate the choices from the options request to be set on // manipulate the choices from the options request to be set on
// scope and be usable by the list form // scope and be usable by the list form
permissionsChoice.then(function (choices) { permissionsChoice.then(function (choices) {
choices = choices =
permissionsLabel({ fieldLabels({
choices: choices choices: choices
}); });
_.map(choices, function(n, key) { _.map(choices, function(n, key) {
@@ -209,8 +210,6 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
}); });
}); });
$scope.team_id = id;
// manipulate the choices from the options request to be usable // manipulate the choices from the options request to be usable
// by the search option for permission_type, you can't inject the // by the search option for permission_type, you can't inject the
// list until this is done! // list until this is done!
@@ -221,8 +220,11 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
}); });
generator.inject(form, { mode: 'edit', related: true, scope: $scope }); generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset(); generator.reset();
$scope.$emit('loadTeam');
}); });
$scope.team_id = id;
$scope.PermissionAddAllowed = false; $scope.PermissionAddAllowed = false;
// Retrieve each related set and any lookups // Retrieve each related set and any lookups
@@ -253,59 +255,65 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
} }
}); });
// Retrieve detail record and prepopulate the form // Retrieve each related set and any lookups
Wait('start'); if ($scope.loadTeamRemove) {
Rest.setUrl(defaultUrl + ':id/'); $scope.loadTeamRemove();
Rest.get({ }
params: { $scope.loadTeamRemove = $scope.$on('loadTeam', function () {
id: id // Retrieve detail record and prepopulate the form
} Wait('start');
}) Rest.setUrl(defaultUrl + ':id/');
.success(function (data) { Rest.get({
var fld, related, set; params: {
$scope.team_name = data.name; id: id
for (fld in form.fields) {
if (data[fld]) {
$scope[fld] = data[fld];
master[fld] = $scope[fld];
}
} }
related = data.related;
for (set in form.related) {
if (related[set]) {
relatedSets[set] = {
url: related[set],
iterator: form.related[set].iterator
};
}
}
// Initialize related search functions. Doing it here to make sure relatedSets object is populated.
RelatedSearchInit({
scope: $scope,
form: form,
relatedSets: relatedSets
});
RelatedPaginateInit({
scope: $scope,
relatedSets: relatedSets
});
LookUpInit({
scope: $scope,
form: form,
current_item: data.organization,
list: OrganizationList,
field: 'organization',
input_type: 'radio'
});
$scope.organization_url = data.related.organization;
$scope.$emit('teamLoaded');
}) })
.error(function (data, status) { .success(function (data) {
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to retrieve team: ' + $routeParams.team_id + var fld, related, set;
'. GET status: ' + status }); $scope.team_name = data.name;
}); for (fld in form.fields) {
if (data[fld]) {
$scope[fld] = data[fld];
master[fld] = $scope[fld];
}
}
related = data.related;
for (set in form.related) {
if (related[set]) {
relatedSets[set] = {
url: related[set],
iterator: form.related[set].iterator
};
}
}
// Initialize related search functions. Doing it here to make sure relatedSets object is populated.
RelatedSearchInit({
scope: $scope,
form: form,
relatedSets: relatedSets
});
RelatedPaginateInit({
scope: $scope,
relatedSets: relatedSets
});
LookUpInit({
scope: $scope,
form: form,
current_item: data.organization,
list: OrganizationList,
field: 'organization',
input_type: 'radio'
});
$scope.organization_url = data.related.organization;
$scope.$emit('teamLoaded');
})
.error(function (data, status) {
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to retrieve team: ' + $routeParams.team_id +
'. GET status: ' + status });
});
});
$scope.getPermissionText = function () { $scope.getPermissionText = function () {
if (this.permission.permission_type !== "admin" && this.permission.run_ad_hoc_commands) { if (this.permission.permission_type !== "admin" && this.permission.run_ad_hoc_commands) {
@@ -431,5 +439,5 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
TeamsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'TeamForm', TeamsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'TeamForm',
'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit',
'ReturnToCaller', 'ClearScope', 'LookUpInit', 'Prompt', 'GetBasePath', 'CheckAccess', 'OrganizationList', 'Wait', 'Stream', 'permissionsChoices', 'permissionsLabel', 'permissionsSearchSelect' 'ReturnToCaller', 'ClearScope', 'LookUpInit', 'Prompt', 'GetBasePath', 'CheckAccess', 'OrganizationList', 'Wait', 'Stream', 'fieldChoices', 'fieldLabels', 'permissionsSearchSelect'
]; ];

View File

@@ -208,7 +208,7 @@ UsersAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$r
export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeParams, UserForm, GenerateForm, Rest, Alert, export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeParams, UserForm, GenerateForm, Rest, Alert,
ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, GetBasePath, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, GetBasePath,
Prompt, CheckAccess, ResetForm, Wait, Stream, permissionsChoices, permissionsLabel, permissionsSearchSelect) { Prompt, CheckAccess, ResetForm, Wait, Stream, fieldChoices, fieldLabels, permissionsSearchSelect) {
ClearScope(); ClearScope();
@@ -224,16 +224,17 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
$scope.permission_search_select = []; $scope.permission_search_select = [];
// return a promise from the options request with the permission type choices (including adhoc) as a param // return a promise from the options request with the permission type choices (including adhoc) as a param
var permissionsChoice = permissionsChoices({ var permissionsChoice = fieldChoices({
scope: $scope, scope: $scope,
url: 'api/v1/' + base + '/' + id + '/permissions/' url: 'api/v1/' + base + '/' + id + '/permissions/',
field: 'permission_type'
}); });
// manipulate the choices from the options request to be set on // manipulate the choices from the options request to be set on
// scope and be usable by the list form // scope and be usable by the list form
permissionsChoice.then(function (choices) { permissionsChoice.then(function (choices) {
choices = choices =
permissionsLabel({ fieldLabels({
choices: choices choices: choices
}); });
_.map(choices, function(n, key) { _.map(choices, function(n, key) {
@@ -241,22 +242,23 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
}); });
}); });
// manipulate the choices from the options request to be usable
// by the search option for permission_type, you can't inject the
// list until this is done!
permissionsChoice.then(function (choices) {
form.related.permissions.fields.permission_type.searchOptions =
permissionsSearchSelect({
choices: choices
});
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
$scope.$emit("loadForm");
});
if ($scope.removeFormReady) { if ($scope.removeFormReady) {
$scope.removeFormReady(); $scope.removeFormReady();
} }
$scope.removeFormReady = $scope.$on('formReady', function () { $scope.removeFormReady = $scope.$on('formReady', function () {
// manipulate the choices from the options request to be usable
// by the search option for permission_type, you can't inject the
// list until this is done!
permissionsChoice.then(function (choices) {
form.related.permissions.fields.permission_type.searchOptions =
permissionsSearchSelect({
choices: choices
});
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
});
if ($scope.removePostRefresh) { if ($scope.removePostRefresh) {
$scope.removePostRefresh(); $scope.removePostRefresh();
} }
@@ -470,54 +472,60 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
// Put form back to its original state // Put form back to its original state
ResetForm(); ResetForm();
if ($scope.removeLoadForm) {
if ($scope.removeModifyForm) { $scope.removeLoadForm();
$scope.removeModifyForm();
} }
$scope.removeModifyForm = $scope.$on('modifyForm', function () { $scope.removeLoadForm = $scope.$on('loadForm', function () {
// Modify form based on LDAP settings
Rest.setUrl(GetBasePath('config'));
Rest.get() if ($scope.removeModifyForm) {
.success(function (data) { $scope.removeModifyForm();
var i, fld; }
if (data.user_ldap_fields) { $scope.removeModifyForm = $scope.$on('modifyForm', function () {
for (i = 0; i < data.user_ldap_fields.length; i++) { // Modify form based on LDAP settings
fld = data.user_ldap_fields[i]; Rest.setUrl(GetBasePath('config'));
if (form.fields[fld]) { Rest.get()
form.fields[fld].readonly = true; .success(function (data) {
form.fields[fld].editRequired = false; var i, fld;
if (form.fields[fld].awRequiredWhen) { if (data.user_ldap_fields) {
delete form.fields[fld].awRequiredWhen; for (i = 0; i < data.user_ldap_fields.length; i++) {
fld = data.user_ldap_fields[i];
if (form.fields[fld]) {
form.fields[fld].readonly = true;
form.fields[fld].editRequired = false;
if (form.fields[fld].awRequiredWhen) {
delete form.fields[fld].awRequiredWhen;
}
} }
} }
} }
$scope.$emit('formReady');
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve application config. GET status: ' + status });
});
});
Wait('start');
Rest.setUrl(defaultUrl + id + '/');
Rest.get()
.success(function (data) {
if (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') {
//this is an LDAP user
$scope.$emit('modifyForm');
} else {
$scope.$emit('formReady');
} }
$scope.$emit('formReady');
}) })
.error(function (data, status) { .error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve application config. GET status: ' + status }); msg: 'Failed to retrieve user: ' + id + '. GET status: ' + status });
}); });
}); });
Wait('start');
Rest.setUrl(defaultUrl + id + '/');
Rest.get()
.success(function (data) {
if (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') {
//this is an LDAP user
$scope.$emit('modifyForm');
} else {
$scope.$emit('formReady');
}
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve user: ' + id + '. GET status: ' + status });
});
} }
UsersEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'UserForm', 'GenerateForm', UsersEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'UserForm', 'GenerateForm',
'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope',
'GetBasePath', 'Prompt', 'CheckAccess', 'ResetForm', 'Wait', 'Stream', 'permissionsChoices', 'permissionsLabel', 'permissionsSearchSelect' 'GetBasePath', 'Prompt', 'CheckAccess', 'ResetForm', 'Wait', 'Stream', 'fieldChoices', 'fieldLabels', 'permissionsSearchSelect'
]; ];

View File

@@ -118,8 +118,8 @@ export default
sourceModel: 'inventory_script', sourceModel: 'inventory_script',
sourceField: 'name', sourceField: 'name',
ngClick: 'lookUpInventory_script()' , ngClick: 'lookUpInventory_script()' ,
addRequired: false, addRequired: true,
editRequired: false, editRequired: true,
ngRequired: "source && source.value === 'custom'", ngRequired: "source && source.value === 'custom'",
}, },
extra_vars: { extra_vars: {

View File

@@ -75,7 +75,7 @@ export default
scope: scope, scope: scope,
// buttons: [], // buttons: [],
width: 710, width: 710,
height: 400, height: 450,
minWidth: 300, minWidth: 300,
resizable: false, resizable: false,
callback: 'DialogReady', callback: 'DialogReady',

View File

@@ -751,9 +751,9 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
$compile(elem)(modal_scope); $compile(elem)(modal_scope);
var form_scope = var form_scope =
generator.inject(GroupForm, { mode: 'edit', id: 'properties-tab', breadCrumbs: false, related: false, scope: properties_scope }); generator.inject(GroupForm, { mode: mode, id: 'properties-tab', breadCrumbs: false, related: false, scope: properties_scope });
var source_form_scope = var source_form_scope =
generator.inject(SourceForm, { mode: 'edit', id: 'sources-tab', breadCrumbs: false, related: false, scope: sources_scope }); generator.inject(SourceForm, { mode: mode, id: 'sources-tab', breadCrumbs: false, related: false, scope: sources_scope });
//generator.reset(); //generator.reset();
@@ -1465,6 +1465,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
// Change the lookup and regions when the source changes // Change the lookup and regions when the source changes
sources_scope.sourceChange = function () { sources_scope.sourceChange = function () {
sources_scope.credential_name = "";
sources_scope.credential = "";
if (sources_scope.credential_name_api_error) {
delete sources_scope.credential_name_api_error;
}
parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=="manual") ? true : false; parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=="manual") ? true : false;
SourceChange({ scope: sources_scope, form: SourceForm }); SourceChange({ scope: sources_scope, form: SourceForm });
}; };

View File

@@ -25,9 +25,11 @@ export default
.factory('CheckLicense', ['$rootScope', '$compile', 'CreateDialog', 'Store', .factory('CheckLicense', ['$rootScope', '$compile', 'CreateDialog', 'Store',
'LicenseUpdateForm', 'GenerateForm', 'TextareaResize', 'ToJSON', 'GetBasePath', 'LicenseUpdateForm', 'GenerateForm', 'TextareaResize', 'ToJSON', 'GetBasePath',
'Rest', 'ProcessErrors', 'Alert', 'IsAdmin', '$location', 'Rest', 'ProcessErrors', 'Alert', 'IsAdmin', '$location', 'pendoService',
'Authorization', 'Wait',
function($rootScope, $compile, CreateDialog, Store, LicenseUpdateForm, GenerateForm, function($rootScope, $compile, CreateDialog, Store, LicenseUpdateForm, GenerateForm,
TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location) { TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location,
pendoService, Authorization, Wait) {
return { return {
getRemainingDays: function(time_remaining) { getRemainingDays: function(time_remaining) {
// assumes time_remaining will be in seconds // assumes time_remaining will be in seconds
@@ -164,21 +166,32 @@ export default
if (typeof json_data === 'object' && Object.keys(json_data).length > 0) { if (typeof json_data === 'object' && Object.keys(json_data).length > 0) {
Rest.setUrl(url); Rest.setUrl(url);
Rest.post(json_data) Rest.post(json_data)
.success(function () { .success(function (response) {
try { response.license_info = response;
$('#license-modal-dialog').dialog('close');
}
catch(e) {
// ignore
}
Alert('License Accepted', 'The Ansible Tower license was updated. To review or update the license, choose View License from the Setup menu.','alert-info'); Alert('License Accepted', 'The Ansible Tower license was updated. To review or update the license, choose View License from the Setup menu.','alert-info');
$rootScope.features = undefined; $rootScope.features = undefined;
$location.path('/home');
Authorization.getLicense()
.success(function (data) {
Authorization.setLicense(data);
pendoService.issuePendoIdentity();
Wait("stop");
$location.path('/home');
})
.error(function () {
Wait('stop');
Alert('Error', 'Failed to access license information. GET returned status: ' + status, 'alert-danger',
$location.path('/logout'));
});
}) })
.error(function (data, status) { .catch(function (response) {
scope.license_json_api_error = "A valid license key in JSON format is required"; scope.license_json_api_error = "A valid license key in JSON format is required";
ProcessErrors(scope, data, status, null, { hdr: 'Error!', ProcessErrors(scope, response.data, response.status, null, { hdr: 'Error!',
msg: 'Failed to update license. POST returned: ' + status msg: 'Failed to update license. POST returned: ' + response.status
}); });
}); });
} else { } else {

View File

@@ -39,16 +39,25 @@ angular.module('LoadConfigHelper', ['Utilities'])
if(angular.isObject(response.data)){ if(angular.isObject(response.data)){
$AnsibleConfig = _.extend($AnsibleConfig, response.data); $AnsibleConfig = _.extend($AnsibleConfig, response.data);
Store('AnsibleConfig', $AnsibleConfig); Store('AnsibleConfig', $AnsibleConfig);
if ($rootScope.loginConfig) {
$rootScope.loginConfig.resolve('config loaded');
}
$rootScope.$emit('ConfigReady'); $rootScope.$emit('ConfigReady');
} }
else { else {
$log.info('local_settings.json is not a valid object'); $log.info('local_settings.json is not a valid object');
if ($rootScope.loginConfig) {
$rootScope.loginConfig.resolve('config loaded');
}
$rootScope.$emit('ConfigReady'); $rootScope.$emit('ConfigReady');
} }
}, function() { }, function() {
//local_settings.json not found //local_settings.json not found
$log.info('local_settings.json not found'); $log.info('local_settings.json not found');
if ($rootScope.loginConfig) {
$rootScope.loginConfig.resolve('config loaded');
}
$rootScope.$emit('ConfigReady'); $rootScope.$emit('ConfigReady');
}); });
}); });
@@ -57,15 +66,16 @@ angular.module('LoadConfigHelper', ['Utilities'])
// load config.js // load config.js
$log.info('attempting to load config.js'); $log.info('attempting to load config.js');
$http({ method:'GET', url: $basePath + 'config.js' }) $http({ method:'GET', url: $basePath + 'config.js' })
.success(function(data) { .then(function(response) {
$log.info('loaded config.js'); $log.info('loaded config.js');
$AnsibleConfig = eval(data); $AnsibleConfig = eval(response.data);
Store('AnsibleConfig', $AnsibleConfig); Store('AnsibleConfig', $AnsibleConfig);
$rootScope.$emit('LoadConfig'); $rootScope.$emit('LoadConfig');
}) })
.error(function(data, status) { .catch(function(response) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', response.data = 'Failed to load ' + $basePath + '/config.js';
msg: 'Failed to load ' + $basePath + '/config.js. GET status: ' + status ProcessErrors($rootScope, response, response.status, null, { hdr: 'Error!',
msg: 'Failed to load ' + $basePath + '/config.js.'
}); });
}); });
}; };

View File

@@ -60,7 +60,8 @@ export default
logout: function () { logout: function () {
// the following puts our primary scope up for garbage collection, which // the following puts our primary scope up for garbage collection, which
// should prevent content flash from the prior user. // should prevent content flash from the prior user.
var scope = angular.element(document.getElementById('main-view')).scope();
var x, scope = angular.element(document.getElementById('main-view')).scope();
scope.$destroy(); scope.$destroy();
//$rootScope.$destroy(); //$rootScope.$destroy();
@@ -78,6 +79,9 @@ export default
$cookieStore.remove('lastPath'); $cookieStore.remove('lastPath');
$rootScope.lastPath = '/home'; $rootScope.lastPath = '/home';
} }
x = Store('sessionTime');
x[$rootScope.current_user.id].loggedIn = false;
Store('sessionTime', x);
$rootScope.lastUser = $cookieStore.get('current_user').id; $rootScope.lastUser = $cookieStore.get('current_user').id;
$cookieStore.remove('token_expires'); $cookieStore.remove('token_expires');
@@ -94,7 +98,7 @@ export default
$rootScope.token_expires = null; $rootScope.token_expires = null;
$rootScope.login_username = null; $rootScope.login_username = null;
$rootScope.login_password = null; $rootScope.login_password = null;
$rootScope.sessionTimer.expireSession(); $rootScope.sessionTimer.clearTimers();
}, },
getLicense: function () { getLicense: function () {
@@ -112,6 +116,7 @@ export default
var license = data.license_info; var license = data.license_info;
license.analytics_status = data.analytics_status; license.analytics_status = data.analytics_status;
license.version = data.version; license.version = data.version;
license.ansible_version = data.ansible_version;
license.tested = false; license.tested = false;
Store('license', license); Store('license', license);
$rootScope.features = Store('license').features; $rootScope.features = Store('license').features;
@@ -149,7 +154,6 @@ export default
// store the response values in $rootScope so we can get to them later // store the response values in $rootScope so we can get to them later
$rootScope.current_user = response.results[0]; $rootScope.current_user = response.results[0];
$cookieStore.put('current_user', response.results[0]); //keep in session cookie in the event of browser refresh $cookieStore.put('current_user', response.results[0]); //keep in session cookie in the event of browser refresh
$rootScope.$emit('OpenSocket');
}, },
restoreUserInfo: function () { restoreUserInfo: function () {

View File

@@ -12,7 +12,8 @@ export default
Store, $log) { Store, $log) {
return { return {
setPendoOptions: function (config) { setPendoOptions: function (config) {
var options = { var tower_version = config.version.split('-')[0],
options = {
visitor: { visitor: {
id: null, id: null,
role: null, role: null,
@@ -23,7 +24,9 @@ export default
planLevel: config.license_type, planLevel: config.license_type,
planPrice: config.instance_count, planPrice: config.instance_count,
creationDate: config.license_date, creationDate: config.license_date,
trial: config.trial trial: config.trial,
tower_version: tower_version,
ansible_version: config.ansible_version
} }
}; };
if(config.analytics_status === 'detailed'){ if(config.analytics_status === 'detailed'){

View File

@@ -23,22 +23,21 @@
*/ */
export default export default
['$rootScope', '$cookieStore', 'transitionTo', 'CreateDialog', 'Authorization', ['$rootScope', '$cookieStore', 'transitionTo', 'CreateDialog', 'Authorization',
'Store', '$interval', 'Store', '$interval', '$location', '$q',
function ($rootScope, $cookieStore, transitionTo, CreateDialog, Authorization, function ($rootScope, $cookieStore, transitionTo, CreateDialog, Authorization,
Store, $interval) { Store, $interval, $location, $q) {
return { return {
sessionTime: null, sessionTime: null,
timeout: null, timeout: null,
getSessionTime: function () { getSessionTime: function () {
if(Store('sessionTime_'+$rootScope.current_user.id)){ if(Store('sessionTime')){
return Store('sessionTime_'+$rootScope.current_user.id); return Store('sessionTime')[$rootScope.current_user.id].time;
} }
else { else {
return 0; return 0;
} }
}, },
isExpired: function (increase) { isExpired: function (increase) {
@@ -83,14 +82,24 @@ export default
this.sessionTime = 0; this.sessionTime = 0;
this.clearTimers(); this.clearTimers();
$cookieStore.put('sessionExpired', true); $cookieStore.put('sessionExpired', true);
transitionTo('signOut');
}, },
moveForward: function () { moveForward: function () {
var tm, t; var tm, t, x, y;
tm = ($AnsibleConfig.session_timeout) ? $AnsibleConfig.session_timeout : 1800; tm = ($AnsibleConfig.session_timeout) ? $AnsibleConfig.session_timeout : 1800;
t = new Date().getTime() + (tm * 1000); t = new Date().getTime() + (tm * 1000);
Store('sessionTime_'+$rootScope.current_user.id, t); x = {
time: t,
loggedIn: true
};
if(Store('sessionTime')){
y = Store('sessionTime');
}
else {
y = {};
}
y[$rootScope.current_user.id] = x;
Store('sessionTime' , y);
$rootScope.sessionExpired = false; $rootScope.sessionExpired = false;
$cookieStore.put('sessionExpired', false); $cookieStore.put('sessionExpired', false);
this.startTimers(); this.startTimers();
@@ -148,6 +157,14 @@ export default
$('#idle-modal').dialog('close'); $('#idle-modal').dialog('close');
} }
that.expireSession('idle'); that.expireSession('idle');
$location.url('/login');
}
if(Store('sessionTime') &&
Store('sessionTime')[$rootScope.current_user.id] &&
Store('sessionTime')[$rootScope.current_user.id].loggedIn === false){
that.expireSession();
$location.url('/login');
} }
}, 1000); }, 1000);
@@ -156,11 +173,15 @@ export default
clearTimers: function(){ clearTimers: function(){
$interval.cancel($rootScope.expireTimer); $interval.cancel($rootScope.expireTimer);
delete $rootScope.expireTimer;
}, },
init: function () { init: function () {
var deferred = $q.defer();
this.moveForward(); this.moveForward();
return this; deferred.resolve(this);
return deferred.promise;
} }
}; };
} }

View File

@@ -110,17 +110,11 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l
setLoginFocus(); setLoginFocus();
}); });
$rootScope.loginConfig.promise.then(function () {
if ($AnsibleConfig.custom_logo) { scope.customLogo = ($AnsibleConfig.custom_logo) ? "custom_console_logo.png" : "tower_console_logo.png";
scope.customLogo = "custom_console_logo.png" scope.customLoginInfo = $AnsibleConfig.custom_login_info;
scope.customLogoPresent = true; scope.customLoginInfoPresent = ($AnsibleConfig.customLoginInfo) ? true : false;
} else { });
scope.customLogo = "login_modal_logo.png";
scope.customLogoPresent = false;
}
scope.customLoginInfo = $AnsibleConfig.custom_login_info;
scope.customLoginInfoPresent = (scope.customLoginInfo) ? true : false;
// Reset the login form // Reset the login form
//scope.loginForm.login_username.$setPristine(); //scope.loginForm.login_username.$setPristine();
@@ -171,9 +165,12 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l
Authorization.getUser() Authorization.getUser()
.success(function (data) { .success(function (data) {
Authorization.setUserInfo(data); Authorization.setUserInfo(data);
$rootScope.sessionTimer = Timer.init(); Timer.init().then(function(timer){
$rootScope.user_is_superuser = data.results[0].is_superuser; $rootScope.sessionTimer = timer;
scope.$emit('AuthorizationGetLicense'); $rootScope.$emit('OpenSocket');
$rootScope.user_is_superuser = data.results[0].is_superuser;
scope.$emit('AuthorizationGetLicense');
});
}) })
.error(function (data, status) { .error(function (data, status) {
Authorization.logout(); Authorization.logout();
@@ -200,7 +197,7 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l
function (data) { function (data) {
var key; var key;
Wait('stop'); Wait('stop');
if (data.data.non_field_errors && data.data.non_field_errors.length === 0) { if (data && data.data && data.data.non_field_errors && data.data.non_field_errors.length === 0) {
// show field specific errors returned by the API // show field specific errors returned by the API
for (key in data.data) { for (key in data.data) {
scope[key + 'Error'] = data.data[key][0]; scope[key + 'Error'] = data.data[key][0];

View File

@@ -25,7 +25,7 @@
var options = [], var options = [],
error = ""; error = "";
function parseGoogle(option, key) { function parseGoogle(option) {
var newOption = {}; var newOption = {};
newOption.type = "google"; newOption.type = "google";

View File

@@ -1,14 +0,0 @@
<div class="row">
<div class="left-side col-sm-4 col-xs-12">
<img id="about-modal-logo" src="static/assets/tower_console_bug.png">
</div>
<div class="right-side col-sm-8 col-xs-12">
<img id="about-modal-titlelogo" src="static/assets/tower_login_logo.png"><br>
<p>Tower Version <span id='about-modal-version'></span></p>
<textarea class="form-control" rows="2" readonly>Copyright 2015. All rights reserved.&#10Ansible and Ansible Tower are registered trademarks of Ansible, Inc.&#10</textarea>
<br>
<p>Visit <a href="http://www.ansible.com" target="_blank">Ansible.com</a> for more information!</p>
</div>
</div>

View File

@@ -33,8 +33,24 @@
<div class="form-horizontal" role="form" id="job-status-form"> <div class="form-horizontal" role="form" id="job-status-form">
<div class="form-group" ng-show="job_status.explanation"> <div class="form-group" ng-show="job_status.explanation">
<label class="col-lg-2 col-md-12 col-sm-12 col-xs-12">Explanation</label> <label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 col-xs-12">Explanation</label>
<div class="col-lg-10 col-md-12 col-sm-12 col-xs-12 job_status_explanation" ng-bind-html="job_status.explanation"></div> <div class="col-lg-10 col-md-10 col-sm-10 col-xs-9 job_status_explanation"
ng-show="!previousTaskFailed" ng-bind-html="job_status.explanation"></div>
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-9 job_status_explanation"
ng-show="previousTaskFailed">Previous Task Failed
<a
href=""
id="explanation_help"
aw-pop-over="{{ task_detail }}"
aw-pop-over-watch="task_detail"
data-placement="bottom"
data-container="body" class="help-link" over-title="Failure Detail"
title=""
tabindex="-1">
<i class="fa fa-question-circle">
</i>
</a>
</div>
</div> </div>
<div class="form-group" ng-show="job_status.traceback"> <div class="form-group" ng-show="job_status.traceback">

View File

@@ -10,8 +10,8 @@
* @description This controller for permissions add * @description This controller for permissions add
*/ */
export default export default
['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'permissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'permissionsCategoryChange', 'permissionsChoices', 'permissionsLabel', ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'permissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'permissionsCategoryChange', 'fieldChoices', 'fieldLabels',
function($scope, $rootScope, $compile, $location, $log, $routeParams, permissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ClearScope, GetBasePath, ReturnToCaller, InventoryList, ProjectList, LookUpInit, CheckAccess, Wait, permissionsCategoryChange, permissionsChoices, permissionsLabel) { function($scope, $rootScope, $compile, $location, $log, $routeParams, permissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ClearScope, GetBasePath, ReturnToCaller, InventoryList, ProjectList, LookUpInit, CheckAccess, Wait, permissionsCategoryChange, fieldChoices, fieldLabels) {
ClearScope(); ClearScope();
@@ -22,13 +22,14 @@ export default
base = $location.path().replace(/^\//, '').split('/')[0], base = $location.path().replace(/^\//, '').split('/')[0],
master = {}; master = {};
var permissionsChoice = permissionsChoices({ var permissionsChoice = fieldChoices({
scope: $scope, scope: $scope,
url: 'api/v1/' + base + '/' + id + '/permissions/' url: 'api/v1/' + base + '/' + id + '/permissions/',
field: 'permission_type'
}); });
permissionsChoice.then(function (choices) { permissionsChoice.then(function (choices) {
return permissionsLabel({ return fieldLabels({
choices: choices choices: choices
}); });
}).then(function (choices) { }).then(function (choices) {

View File

@@ -10,8 +10,8 @@
* @description This controller for permissions edit * @description This controller for permissions edit
*/ */
export default export default
['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'permissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'permissionsCategoryChange', 'permissionsChoices', 'permissionsLabel', ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'permissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'permissionsCategoryChange', 'fieldChoices', 'fieldLabels',
function($scope, $rootScope, $compile, $location, $log, $routeParams, permissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, Prompt, GetBasePath, InventoryList, ProjectList, LookUpInit, CheckAccess, Wait, permissionsCategoryChange, permissionsChoices, permissionsLabel) { function($scope, $rootScope, $compile, $location, $log, $routeParams, permissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, Prompt, GetBasePath, InventoryList, ProjectList, LookUpInit, CheckAccess, Wait, permissionsCategoryChange, fieldChoices, fieldLabels) {
ClearScope(); ClearScope();
@@ -25,13 +25,14 @@ export default
$scope.permission_label = {}; $scope.permission_label = {};
var permissionsChoice = permissionsChoices({ var permissionsChoice = fieldChoices({
scope: $scope, scope: $scope,
url: 'api/v1/' + base + '/' + base_id + '/permissions/' url: 'api/v1/' + base + '/' + base_id + '/permissions/',
field: 'permission_type'
}); });
permissionsChoice.then(function (choices) { permissionsChoice.then(function (choices) {
return permissionsLabel({ return fieldLabels({
choices: choices choices: choices
}); });
}).then(function (choices) { }).then(function (choices) {

View File

@@ -12,8 +12,8 @@
export default export default
['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'permissionsList', 'generateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'CheckAccess', 'Wait', 'permissionsChoices', 'permissionsLabel', 'permissionsSearchSelect', ['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'permissionsList', 'generateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'CheckAccess', 'Wait', 'fieldChoices', 'fieldLabels', 'permissionsSearchSelect',
function ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, permissionsList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, CheckAccess, Wait, permissionsChoices, permissionsLabel, permissionsSearchSelect) { function ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, permissionsList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, CheckAccess, Wait, fieldChoices, fieldLabels, permissionsSearchSelect) {
ClearScope(); ClearScope();
@@ -27,16 +27,17 @@ export default
$scope.permission_search_select = []; $scope.permission_search_select = [];
// return a promise from the options request with the permission type choices (including adhoc) as a param // return a promise from the options request with the permission type choices (including adhoc) as a param
var permissionsChoice = permissionsChoices({ var permissionsChoice = fieldChoices({
scope: $scope, scope: $scope,
url: 'api/v1/' + base + '/' + base_id + '/permissions/' url: 'api/v1/' + base + '/' + base_id + '/permissions/',
field: 'permission_type'
}); });
// manipulate the choices from the options request to be set on // manipulate the choices from the options request to be set on
// scope and be usable by the list form // scope and be usable by the list form
permissionsChoice.then(function (choices) { permissionsChoice.then(function (choices) {
choices = choices =
permissionsLabel({ fieldLabels({
choices: choices choices: choices
}); });
_.map(choices, function(n, key) { _.map(choices, function(n, key) {

View File

@@ -12,8 +12,6 @@ import list from './shared/permissions.list';
import form from './shared/permissions.form'; import form from './shared/permissions.form';
import permissionsCategoryChange from './shared/category-change.factory'; import permissionsCategoryChange from './shared/category-change.factory';
import permissionsChoices from './shared/get-choices.factory';
import permissionsLabel from './shared/get-labels.factory';
import permissionsSearchSelect from './shared/get-search-select.factory'; import permissionsSearchSelect from './shared/get-search-select.factory';
export default export default
@@ -25,6 +23,4 @@ export default
.factory('permissionsList', list) .factory('permissionsList', list)
.factory('permissionsForm', form) .factory('permissionsForm', form)
.factory('permissionsCategoryChange', permissionsCategoryChange) .factory('permissionsCategoryChange', permissionsCategoryChange)
.factory('permissionsChoices', permissionsChoices)
.factory('permissionsLabel', permissionsLabel)
.factory('permissionsSearchSelect', permissionsSearchSelect); .factory('permissionsSearchSelect', permissionsSearchSelect);

View File

@@ -16,24 +16,28 @@
['Rest', 'ProcessErrors', function(Rest, ProcessErrors) { ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
return function (params) { return function (params) {
var scope = params.scope, var scope = params.scope,
url = params.url; url = params.url,
field = params.field;
// Auto populate the field if there is only one result // Auto populate the field if there is only one result
Rest.setUrl(url); Rest.setUrl(url);
return Rest.options() return Rest.options()
.then(function (data) { .then(function (data) {
data = data.data; data = data.data;
var choices = data.actions.GET.permission_type.choices; var choices = data.actions.GET[field].choices;
// manually add the adhoc label to the choices object // manually add the adhoc label to the choices object if
choices.push(["adhoc", // the permission_type field
data.actions.GET.run_ad_hoc_commands.help_text]); if (field === "permission_type") {
choices.push(["adhoc",
data.actions.GET.run_ad_hoc_commands.help_text]);
}
return choices; return choices;
}) })
.catch(function (data, status) { .catch(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!', ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get permission type labels. Options requrest returned status: ' + status }); msg: 'Failed to get ' + field + ' labels. Options requrest returned status: ' + status });
}); });
}; };
}]; }];

View File

@@ -11,8 +11,8 @@
*************************************************/ *************************************************/
export default export default
[ '$rootScope', '$q', [ '$rootScope', '$q', '$injector',
function ($rootScope, $q) { function ($rootScope, $q, $injector) {
return { return {
response: function(config) { response: function(config) {
if(config.headers('auth-token-timeout') !== null){ if(config.headers('auth-token-timeout') !== null){
@@ -21,8 +21,10 @@
return config; return config;
}, },
responseError: function(rejection){ responseError: function(rejection){
if( rejection.data && !_.isEmpty(rejection.data.detail) && rejection.data.detail === "Maximum per-user sessions reached"){ if(rejection && rejection.data && rejection.data.detail && rejection.data.detail === "Maximum per-user sessions reached"){
$rootScope.sessionTimer.expireSession('session_limit'); $rootScope.sessionTimer.expireSession('session_limit');
var location = $injector.get('$location');
location.url('/login');
return $q.reject(rejection); return $q.reject(rejection);
} }
return $q.reject(rejection); return $q.reject(rejection);

View File

@@ -6,6 +6,8 @@
import restServicesFactory from './restServices.factory'; import restServicesFactory from './restServices.factory';
import interceptors from './interceptors.service'; import interceptors from './interceptors.service';
import fieldChoices from './get-choices.factory';
import fieldLabels from './get-labels.factory';
export default export default
angular.module('RestServices', []) angular.module('RestServices', [])
@@ -13,4 +15,6 @@ export default
$httpProvider.interceptors.push('RestInterceptor'); $httpProvider.interceptors.push('RestInterceptor');
}]) }])
.factory('Rest', restServicesFactory) .factory('Rest', restServicesFactory)
.service('RestInterceptor', interceptors); .service('RestInterceptor', interceptors)
.factory('fieldChoices', fieldChoices)
.factory('fieldLabels', fieldLabels);

View File

@@ -53,8 +53,8 @@
</p> </p>
</div> </div>
</a> </a>
<a link-to="managementJobsList" class="SetupItem SetupItem--aside HoverIcon Media"> <a link-to="managementJobsList" class="SetupItem SetupItem--aside HoverIcon Media" ng-if="user_is_superuser">
<i class="HoverIcon-icon HoverIcon-icon--opacity HoverIcon-icon--color Media-figure SetupItem-icon SetupItem-icon--aside" ng-if="user_is_superuser"> <i class="HoverIcon-icon HoverIcon-icon--opacity HoverIcon-icon--color Media-figure SetupItem-icon SetupItem-icon--aside">
<aw-icon name="ManagementJobs"></aw-icon> <aw-icon name="ManagementJobs"></aw-icon>
</i> </i>
<div class="Media-block"> <div class="Media-block">

View File

@@ -252,15 +252,20 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
if ((!fieldErrors) && defaultMsg) { if ((!fieldErrors) && defaultMsg) {
Alert(defaultMsg.hdr, defaultMsg.msg); Alert(defaultMsg.hdr, defaultMsg.msg);
} }
} else if (typeof data === 'object' && Object.keys(data).length > 0) { } else if (typeof data === 'object' && data !== null){
keys = Object.keys(data); if(Object.keys(data).length > 0) {
if (Array.isArray(data[keys[0]])) { keys = Object.keys(data);
msg = data[keys[0]][0]; if (Array.isArray(data[keys[0]])) {
msg = data[keys[0]][0];
}
else {
msg = data[keys[0]];
}
Alert(defaultMsg.hdr, msg);
} }
else { else {
msg = data[keys[0]]; Alert(defaultMsg.hdr, defaultMsg.msg);
} }
Alert(defaultMsg.hdr, msg);
} else { } else {
Alert(defaultMsg.hdr, defaultMsg.msg); Alert(defaultMsg.hdr, defaultMsg.msg);
} }

View File

@@ -275,7 +275,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
if (/^\-?\d*$/.test(viewValue)) { if (/^\-?\d*$/.test(viewValue)) {
// it is valid // it is valid
ctrl.$setValidity('integer', true); ctrl.$setValidity('integer', true);
if ( viewValue === '-' || viewValue === '-0' || viewValue === '' || viewValue === null) { if ( viewValue === '-' || viewValue === '-0' || viewValue === null) {
ctrl.$setValidity('integer', false); ctrl.$setValidity('integer', false);
return viewValue; return viewValue;
} }

View File

@@ -221,15 +221,68 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
if (obj2_obj && obj2_obj.name && !/^_delete/.test(obj2_obj.name)) { if (obj2_obj && obj2_obj.name && !/^_delete/.test(obj2_obj.name)) {
obj2_obj.base = obj2; obj2_obj.base = obj2;
obj2_obj.name = $filter('sanitize')(obj2_obj.name); obj2_obj.name = $filter('sanitize')(obj2_obj.name);
descr += obj2 + " <a href=\"" + BuildUrl(obj2_obj) + "\">" + obj2_obj.name + '</a>' + ((activity.operation === 'disassociate') ? ' from ' : ' to '); descr += obj2 +
descr_nolink += obj2 + ' ' + obj2_obj.name + ((activity.operation === 'disassociate') ? ' from ' : ' to '); " <a href=\"" + BuildUrl(obj2_obj) + "\">" +
obj2_obj.name + '</a>';
if (activity.object_association === 'admins') {
if (activity.operation === 'disassociate') {
descr += ' from being an admin of ';
} else {
descr += ' as an admin of ';
}
} else {
if (activity.operation === 'disassociate') {
descr += ' from ';
} else {
descr += ' to ';
}
}
descr_nolink += obj2 + ' ' + obj2_obj.name;
if (activity.object_association === 'admins') {
if (activity.operation === 'disassociate') {
descr_nolink += ' from being an admin of ';
} else {
descr_nolink += ' as an admin of ';
}
} else {
if (activity.operation === 'disassociate') {
descr_nolink += ' from ';
} else {
descr_nolink += ' to ';
}
}
} else if (obj2) { } else if (obj2) {
name = ''; name = '';
if (obj2_obj && obj2_obj.name) { if (obj2_obj && obj2_obj.name) {
name = ' ' + stripDeleted(obj2_obj.name); name = ' ' + stripDeleted(obj2_obj.name);
} }
descr += obj2 + name + ((activity.operation === 'disassociate') ? ' from ' : ' to '); if (activity.object_association === 'admins') {
descr_nolink += obj2 + name + ((activity.operation === 'disassociate') ? ' from ' : ' to '); if (activity.operation === 'disassociate') {
descr += ' from being an admin of ';
} else {
descr += ' as an admin of ';
}
} else {
if (activity.operation === 'disassociate') {
descr += ' from ';
} else {
descr += ' to ';
}
}
descr_nolink += obj2 + ' ' + obj2_obj.name;
if (activity.object_association === 'admins') {
if (activity.operation === 'disassociate') {
descr_nolink += ' from being an admin of ';
} else {
descr_nolink += ' as an admin of ';
}
} else {
if (activity.operation === 'disassociate') {
descr_nolink += ' from ';
} else {
descr_nolink += ' to ';
}
}
} }
if (obj1_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) { if (obj1_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) {
obj1_obj.base = obj1; obj1_obj.base = obj1;

View File

@@ -31,7 +31,8 @@
window.pendo_options = { window.pendo_options = {
// This is required to be able to load data client side // This is required to be able to load data client side
usePendoAgentAPI: true usePendoAgentAPI: true,
disableGuides: true
}; };
</script> </script>
@@ -55,7 +56,7 @@
<!-- Password Dialog --> <!-- Password Dialog -->
<div id="password-modal" style="display: none;"></div> <div id="password-modal" style="display: none;"></div>
<div id="idle-modal" style="display:none">Your session will expire in <span id="remaining_seconds"></span> seconds, would you like to continue?</div> <div id="idle-modal" style="display:none">Your session will expire in <span id="remaining_seconds">60</span> seconds, would you like to continue?</div>
<!-- Generic Form dialog --> <!-- Generic Form dialog -->
<div id="form-modal" class="modal fade"> <div id="form-modal" class="modal fade">
@@ -156,7 +157,7 @@
<div id="login-modal-dialog" style="display: none;"></div> <div id="login-modal-dialog" style="display: none;"></div>
<div id="help-modal-dialog" style="display: none;"></div> <div id="help-modal-dialog" style="display: none;"></div>
<div id="about-modal-dialog" style="display: none;" ng-include=" '{{ STATIC_URL }}assets/cowsay-about.html ' "></div> <div class="About" id="about-modal-dialog" style="display: none;" ng-include=" '{{ STATIC_URL }}assets/cowsay-about.html ' "></div>
<div id="prompt-for-days" style="display:none"> <div id="prompt-for-days" style="display:none">
<form name="prompt_for_days_form" id="prompt_for_days_form"> <form name="prompt_for_days_form" id="prompt_for_days_form">

View File

@@ -122,7 +122,7 @@ Jenkins
### Server Information ### ### Server Information ###
The AnsibleWorks Jenkins server can be found at http://50.116.42.103:8080/ The Ansible Jenkins server can be found at http://jenkins.testing.ansible.com
This is a standard Jenkins installation, with the following additional This is a standard Jenkins installation, with the following additional
plugins installed: plugins installed:

View File

@@ -38,16 +38,16 @@ successful release.
Monitor Jenkins Monitor Jenkins
--------------- ---------------
Once tagged, one must launch the [Release_Tower](http://50.116.42.103/view/Tower/job/Release_Tower/) with the following parameters: Once tagged, one must launch the [Release_Tower](http://jenkins.testing.ansible.com/view/Tower/job/Release_Tower/) with the following parameters:
* `GIT_BRANCH=origin/tags/<X.Y.Z>` * `GIT_BRANCH=origin/tags/<X.Y.Z>`
* `OFFICIAL=yes` * `OFFICIAL=yes`
The following jobs will be triggered: The following jobs will be triggered:
* [Build_Tower_TAR](http://50.116.42.103/view/Tower/) * [Build_Tower_TAR](http://jenkins.testing.ansible.com/view/Tower/)
* [Build_Tower_DEB](http://50.116.42.103/view/Tower/) * [Build_Tower_DEB](http://jenkins.testing.ansible.com/view/Tower/)
* [Build_Tower_AMI](http://50.116.42.103/view/Tower/) * [Build_Tower_AMI](http://jenkins.testing.ansible.com/view/Tower/)
* [Build_Tower_RPM](http://50.116.42.103/view/Tower/) * [Build_Tower_RPM](http://jenkins.testing.ansible.com/view/Tower/)
* [Build_Tower_Docs](http://50.116.42.103/view/Tower/) * [Build_Tower_Docs](http://jenkins.testing.ansible.com/view/Tower/)
Should any build step fail, Jenkins will emit a message in IRC and set the build status to failed. Should any build step fail, Jenkins will emit a message in IRC and set the build status to failed.
@@ -66,4 +66,4 @@ While OFFICIAL Tower AMI's are created by jenkins, the process for blessing AMI'
Publishing Documentation Publishing Documentation
------------------------ ------------------------
Tower documentation is available in the [product-docs](https://github.com/ansible/product-docs) repository. The [Build_Tower_Docs](http://50.116.42.103/view/Tower/) job builds and publishes PDF, and HTML, documentation. Tower documentation is available in the [product-docs](https://github.com/ansible/product-docs) repository. The [Build_Tower_Docs](http://jenkins.testing.ansible.com/view/Tower/) job builds and publishes PDF, and HTML, documentation.

View File

@@ -20,11 +20,11 @@ django-celery==3.1.10
django-crum==0.6.1 django-crum==0.6.1
django-extensions==1.3.3 django-extensions==1.3.3
django-polymorphic==0.5.3 django-polymorphic==0.5.3
django-radius==0.1.1 django-radius==1.0.0
djangorestframework==2.3.13 djangorestframework==2.3.13
django-split-settings==0.1.1 django-split-settings==0.1.1
django-taggit==0.11.2 django-taggit==0.11.2
dm.xmlsec.binding==1.3.2 git+https://github.com/matburt/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding
dogpile.cache==0.5.6 dogpile.cache==0.5.6
dogpile.core==0.4.1 dogpile.core==0.4.1
enum34==1.0.4 enum34==1.0.4
@@ -93,7 +93,7 @@ python-neutronclient==2.3.11
python-novaclient==2.20.0 python-novaclient==2.20.0
python-openid==2.2.5 python-openid==2.2.5
python-radius==1.0 python-radius==1.0
python_social_auth==0.2.13 git+https://github.com/matburt/python-social-auth.git@master#egg=python-social-auth
python-saml==2.1.4 python-saml==2.1.4
python-swiftclient==2.2.0 python-swiftclient==2.2.0
python-troveclient==1.0.9 python-troveclient==1.0.9

View File

@@ -1,6 +1,7 @@
-r requirements.txt -r requirements.txt
ansible ansible
django-jenkins # Based on django-jenkins==0.16.3, with a fix for properly importing coverage
git+https://github.com/jlaska/django-jenkins.git@release_0.16.4#egg=django-jenkins
coverage coverage
pyflakes pyflakes
pep8 pep8

View File

@@ -18,4 +18,4 @@ exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inve
[flake8] [flake8]
ignore=E201,E203,E221,E225,E231,E241,E251,E261,E265,E302,E303,E501,W291,W391,W293,E731 ignore=E201,E203,E221,E225,E231,E241,E251,E261,E265,E302,E303,E501,W291,W391,W293,E731
exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inventory/gce.py,awx/plugins/inventory/vmware.py,awx/plugins/inventory/windows_azure.py,awx/plugins/inventory/openstack.py,awx/plugins/inventory/rax.py,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data,node_modules/,awx/projects/,tools/docker exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inventory/gce.py,awx/plugins/inventory/vmware.py,awx/plugins/inventory/windows_azure.py,awx/plugins/inventory/openstack.py,awx/plugins/inventory/rax.py,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data,node_modules/,awx/projects/,tools/docker,awx/settings/local_settings.py

View File

@@ -122,7 +122,7 @@ setup(
("%s" % webconfig, ["config/awx-httpd-80.conf", ("%s" % webconfig, ["config/awx-httpd-80.conf",
"config/awx-httpd-443.conf", "config/awx-httpd-443.conf",
"config/awx-munin.conf"]), "config/awx-munin.conf"]),
("%s" % sharedir, ["tools/scripts/request_tower_configuration.sh",]), ("%s" % sharedir, ["tools/scripts/request_tower_configuration.sh","tools/scripts/request_tower_configuration.ps1"]),
("%s" % docdir, ["docs/licenses/*",]), ("%s" % docdir, ["docs/licenses/*",]),
("%s" % munin_plugin_path, ["tools/munin_monitors/tower_jobs", ("%s" % munin_plugin_path, ["tools/munin_monitors/tower_jobs",
"tools/munin_monitors/callbackr_alive", "tools/munin_monitors/callbackr_alive",

2
tools/git_hooks/pre-commit Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
ansible-playbook -i "127.0.0.1," tools/git_hooks/pre_commit.yml

View File

@@ -0,0 +1,8 @@
- hosts: all
connection: local
gather_facts: false
tasks:
- name: lint check w/ flake8
command: 'flake8 {{ playbook_dir }}/../../'
register: result

View File

@@ -0,0 +1,40 @@
Param(
[string]$tower_url,
[string]$host_config_key,
[string]$job_template_id
)
Set-StrictMode -Version 2
$ErrorActionPreference = "Stop"
If(-not $tower_url -or -not $host_config_key -or -not $job_template_id)
{
Write-Host "Requests server configuration from Ansible Tower"
Write-Host "Usage: $($MyInvocation.MyCommand.Name) <server address>[:server port] <host config key> <job template id>"
Write-Host "Example: $($MyInvocation.MyCommand.Name) example.towerhost.net 44d7507f2ead49af5fca80aa18fd24bc 38"
Exit 1
}
$retry_attempts = 10
$attempt = 0
$data = @{
host_config_key=$host_config_key
}
While ($attempt -lt $retry_attempts) {
Try {
$resp = Invoke-WebRequest -Method POST -Body $data -Uri http://$tower_url/api/v1/job_templates/$job_template_id/callback/ -UseBasicParsing
If($resp.StatusCode -eq 202) {
Exit 0
}
}
Catch {
$ex = $_
$attempt++
Write-Host "$([int]$ex.Exception.Response.StatusCode) received... retrying in 1 minute (Attempt $attempt)"
}
Start-Sleep -Seconds 60
}
Exit 1