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
commit 3b07773bcd
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/settings local_settings.py*
include tools/scripts/request_tower_configuration.sh
include tools/scripts/request_tower_configuration.ps1
include tools/scripts/ansible-tower-service
include tools/munin_monitors/*
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()")
OFFICIAL ?= no
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")
TESTEM ?= ./node_modules/.bin/testem
TESTEM_DEBUG_BROWSER ?= Chrome
@ -10,7 +11,7 @@ MOCHA_BIN ?= ./node_modules/.bin/mocha
NODE ?= node
NPM_BIN ?= npm
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
@ -33,8 +34,10 @@ GIT_REMOTE_URL = $(shell git config --get remote.origin.url)
BUILD = 0.git$(DATE)
ifeq ($(OFFICIAL),yes)
RELEASE ?= 1
AW_REPO_URL ?= http://releases.ansible.com/ansible-tower
else
RELEASE ?= $(BUILD)
AW_REPO_URL ?= http://jenkins.testing.ansible.com/ansible-tower_nightlies_RTYUIOPOIUYTYU/$(GIT_BRANCH)
endif
# Allow AMI license customization
@ -57,11 +60,9 @@ endif
ifeq ($(OFFICIAL),yes)
SETUP_TAR_NAME=$(NAME)-setup-$(VERSION)
SDIST_TAR_NAME=$(NAME)-$(VERSION)
PACKER_BUILD_OPTS ?= -var-file=vars-release.json
else
SETUP_TAR_NAME=$(NAME)-setup-$(VERSION)-$(RELEASE)
SDIST_TAR_NAME=$(NAME)-$(VERSION)-$(RELEASE)
PACKER_BUILD_OPTS ?= -var-file=vars-nightly.json
endif
SDIST_TAR_FILE=$(SDIST_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
for COMPONENT in non-free $(VERSION); do \
$(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

View File

@ -630,6 +630,12 @@ class UserSerializer(BaseSerializer):
new_password = None
except AttributeError:
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:
obj.set_password(new_password)
if not obj.password:
@ -2004,11 +2010,12 @@ class ScheduleSerializer(BaseSerializer):
class ActivityStreamSerializer(BaseSerializer):
changes = serializers.SerializerMethodField('get_changes')
object_association = serializers.SerializerMethodField('get_object_association')
class Meta:
model = ActivityStream
fields = ('*', '-name', '-description', '-created', '-modified',
'timestamp', 'operation', 'changes', 'object1', 'object2')
'timestamp', 'operation', 'changes', 'object1', 'object2', 'object_association')
def get_fields(self):
ret = super(ActivityStreamSerializer, self).get_fields()
@ -2033,6 +2040,13 @@ class ActivityStreamSerializer(BaseSerializer):
logger.warn("Error deserializing activity stream json changes")
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):
rel = {}
if obj.actor is not None:

View File

@ -11,6 +11,7 @@ import time
import socket
import sys
import errno
from base64 import b64encode
# Django
from django.conf import settings
@ -526,7 +527,10 @@ class AuthView(APIView):
def get(self, request):
data = SortedDict()
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
not feature_enabled('ldap')) or \
(not feature_enabled('enterprise_auth') and
@ -540,7 +544,7 @@ class AuthView(APIView):
}
if name == 'saml':
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['login_url'] = '%s?idp=%s' % (login_url, 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})
else:
return Response(response_message)
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)
end_line = request.QUERY_PARAMS.get('end_line', None)
dark_val = request.QUERY_PARAMS.get('dark', '')
@ -2883,7 +2889,10 @@ class UnifiedJobStdout(RetrieveAPIView):
if request.accepted_renderer.format == 'api':
return Response(mark_safe(data))
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)
elif request.accepted_renderer.format == 'ansi':
return Response(unified_job.result_stdout_raw)

View File

@ -146,7 +146,7 @@ class BaseAccess(object):
def can_unattach(self, obj, sub_obj, relationship):
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()
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', ''):
@ -154,9 +154,9 @@ class BaseAccess(object):
validation_info['time_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")
if validation_info.get("grace_period_remaining") <= 0:
if check_expiration and validation_info.get("grace_period_remaining") <= 0:
raise PermissionDenied("license has expired")
free_instances = validation_info.get('free_instances', 0)
@ -262,7 +262,7 @@ class OrganizationAccess(BaseAccess):
self.user in obj.admins.all())
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)
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):
super(BaseCommandInstance, self).__init__()
self.enforce_primary_role = False
self.enforce_roles = False
self.enforce_hostname_set = False
self.enforce_unique_find = False

View File

@ -46,43 +46,52 @@ class SocketSession(object):
return bool(not auth_token.is_expired())
class SocketSessionManager(object):
socket_sessions = []
socket_session_token_key_map = {}
@classmethod
def _prune(cls):
if len(cls.socket_sessions) > 1000:
session = cls.socket_session[0]
del cls.socket_session_token_key_map[session.token_key]
cls.sessions = cls.socket_sessions[1:]
def __init__(self):
self.SESSIONS_MAX = 1000
self.socket_sessions = []
self.socket_session_token_key_map = {}
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>
'''
@classmethod
def lookup(cls, token_key=None):
def lookup(self, token_key=None):
if not token_key:
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(cls, session):
cls.socket_sessions.append(session)
entries = cls.socket_session_token_key_map.get(session.token_key, None)
def add_session(self, session):
self.socket_sessions.append(session)
entries = self.socket_session_token_key_map.get(session.token_key, None)
if not 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
cls._prune()
self._prune()
return session
class SocketController(object):
server = None
@classmethod
def broadcast_packet(cls, packet):
def __init__(self, SocketSessionManager):
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
# 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)
if socket_session and socket_session.is_valid():
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 was: " + str(e))
@classmethod
def send_packet(cls, packet, token_key):
def send_packet(self, packet, token_key):
if not token_key:
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
# (it's actually more compliciated than that because of our prune logic)
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 was: " + str(e))
@classmethod
def set_server(cls, server):
cls.server = server
def set_server(self, server):
self.server = server
return server
socketController = SocketController(SocketSessionManager())
#
# Socket session is attached to self.session['socket_session']
# 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)
if socket_session.is_db_token_valid():
self.session['socket_session'] = socket_session
SocketSessionManager.add_session(socket_session)
socketController.add_session(socket_session)
else:
socket_session.invalidate()
@ -240,9 +249,9 @@ def notification_handler(server):
if 'token_key' in message:
# 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:
SocketController.broadcast_packet(packet)
socketController.broadcast_packet(packet)
class Command(NoArgsCommand):
'''
@ -270,7 +279,7 @@ class Command(NoArgsCommand):
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')
SocketController.set_server(server)
socketController.set_server(server)
handler_thread = Thread(target=notification_handler, args=(server,))
handler_thread.daemon = True
handler_thread.start()

View File

@ -170,10 +170,10 @@ def handle_work_error(self, task_id, subtasks=None):
instance_name = ''
if each_task['type'] == 'project_update':
instance = ProjectUpdate.objects.get(id=each_task['id'])
instance_name = instance.project.name
instance_name = instance.name
elif each_task['type'] == 'inventory_update':
instance = InventoryUpdate.objects.get(id=each_task['id'])
instance_name = instance.inventory_source.inventory.name
instance_name = instance.name
elif each_task['type'] == 'job':
instance = Job.objects.get(id=each_task['id'])
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:
instance.status = 'failed'
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)
instance.save()
instance.socketio_emit_status("failed")
@ -614,6 +614,21 @@ class RunJob(BaseTask):
if credential.ssh_key_data not in (None, ''):
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
def build_passwords(self, job, **kwargs):
@ -689,6 +704,8 @@ class RunJob(BaseTask):
env['VMWARE_USER'] = cloud_cred.username
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
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
if job.job_type == PERM_INVENTORY_SCAN:

View File

@ -28,11 +28,11 @@ from django.test.utils import override_settings
# AWX
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_task_system import run_taskmanager
from awx.main.utils import get_ansible_version
from awx.main.task_engine import TaskEngager as LicenseWriter
from awx.sso.backends import LDAPSettings
TEST_PLAYBOOK = '''- hosts: mygroup
gather_facts: false

View File

@ -7,3 +7,6 @@ from .run_fact_cache_receiver import * # noqa
from .commands_monolithic import * # noqa
from .cleanup_facts 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:
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.
group_by_options = [
'group_by_instance_id',
@ -360,7 +366,7 @@ class Ec2Inventory(object):
self.pattern_include = re.compile(pattern_include)
else:
self.pattern_include = None
except configparser.NoOptionError as e:
except configparser.NoOptionError:
self.pattern_include = None
# Do we need to exclude hosts that match a pattern?
@ -370,7 +376,7 @@ class Ec2Inventory(object):
self.pattern_exclude = re.compile(pattern_exclude)
else:
self.pattern_exclude = None
except configparser.NoOptionError as e:
except configparser.NoOptionError:
self.pattern_exclude = None
# Instance filters (see boto and EC2 API docs). Ignore invalid filters.
@ -697,7 +703,8 @@ class Ec2Inventory(object):
self.push(self.inventory, key, dest)
if self.nested_groups:
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
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()
def to_safe(self, word):
''' Converts 'bad' characters in a string to underscores so they can be
used as Ansible groups '''
return re.sub("[^A-Za-z0-9\_]", "_", word)
''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups '''
regex = "[^A-Za-z0-9\_"
if self.replace_dash_in_groups:
regex += "\-"
return re.sub(regex + "]", "_", word)
def json_format_dict(self, data, pretty=False):
''' Converts a dict to a JSON object and dumps it as a formatted
@ -1302,3 +1310,4 @@ class Ec2Inventory(object):
# Run the script
Ec2Inventory()

View File

@ -2,6 +2,7 @@
# All Rights Reserved.
import os
import re # noqa
import sys
import djcelery
from datetime import timedelta
@ -217,13 +218,13 @@ REST_FRAMEWORK = {
}
AUTHENTICATION_BACKENDS = (
'awx.main.backend.LDAPBackend',
'radiusauth.backends.RADIUSBackend',
'awx.sso.backends.LDAPBackend',
'awx.sso.backends.RADIUSBackend',
'social.backends.google.GoogleOAuth2',
'social.backends.github.GithubOAuth2',
'social.backends.github.GithubOrganizationOAuth2',
'social.backends.github.GithubTeamOAuth2',
'social.backends.saml.SAMLAuth',
'awx.sso.backends.SAMLAuth',
'django.contrib.auth.backends.ModelBackend',
)
@ -355,13 +356,15 @@ SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'social.pipeline.social_auth.associate_by_email',
'social.pipeline.mail.mail_validation',
'social.pipeline.user.create_user',
'awx.sso.pipeline.check_user_found_or_created',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'awx.sso.pipeline.set_is_active_for_new_user',
'social.pipeline.user.user_details',
'awx.sso.pipeline.prevent_inactive_login',
'awx.sso.pipeline.update_user_orgs',
'awx.sso.pipeline.update_user_teams',
)
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
@ -370,14 +373,17 @@ SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['profile']
SOCIAL_AUTH_GITHUB_KEY = ''
SOCIAL_AUTH_GITHUB_SECRET = ''
SOCIAL_AUTH_GITHUB_SCOPE = ['user:email', 'read:org']
SOCIAL_AUTH_GITHUB_ORG_KEY = ''
SOCIAL_AUTH_GITHUB_ORG_SECRET = ''
SOCIAL_AUTH_GITHUB_ORG_NAME = ''
SOCIAL_AUTH_GITHUB_ORG_SCOPE = ['user:email', 'read:org']
SOCIAL_AUTH_GITHUB_TEAM_KEY = ''
SOCIAL_AUTH_GITHUB_TEAM_SECRET = ''
SOCIAL_AUTH_GITHUB_TEAM_ID = ''
SOCIAL_AUTH_GITHUB_TEAM_SCOPE = ['user:email', 'read:org']
SOCIAL_AUTH_SAML_SP_ENTITY_ID = ''
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = ''
@ -400,6 +406,9 @@ SOCIAL_AUTH_CLEAN_USERNAMES = True
SOCIAL_AUTH_SANITIZE_REDIRECTS = True
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
# celery task.
@ -572,13 +581,19 @@ VMWARE_EXCLUDE_EMPTY_GROUPS = True
# provide a list here.
# Source: https://developers.google.com/compute/docs/zones
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-b', 'US Central (B)'),
('us-central1-c', 'US Central (C)'),
('us-central1-f', 'US Central (F)'),
('europe-west1-a', 'Europe West (A)'),
('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-b', 'Asia East (B)'),
('asia-east1-c', 'Asia East (C)'),
]
GCE_REGIONS_BLACKLIST = []
@ -715,7 +730,7 @@ LOGGING = {
'level': 'WARNING',
'class':'logging.handlers.RotatingFileHandler',
'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
'backupCount': 5,
'formatter':'simple',
@ -807,7 +822,12 @@ LOGGING = {
'propagate': False,
},
'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',
# '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
###############################################################################

View File

@ -523,8 +523,80 @@ SOCIAL_AUTH_SAML_ENABLED_IDPS = {
# 'url': 'https://myidp.example.com/sso',
# '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
###############################################################################

View File

@ -7,10 +7,10 @@
# settings as needed.
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:
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]):
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_TECHNICAL_CONTACT, SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
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:
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.apps.django_app.middleware import SocialAuthExceptionMiddleware
# Ansible Tower
from awx.main.models import AuthToken
class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
def process_request(self, request):
request.META['SERVER_PORT'] = 80 # FIXME
token_key = request.COOKIES.get('token', '')
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):
return
if isinstance(exception, SocialAuthBaseException):
if isinstance(exception, SocialAuthBaseException) or request.path.startswith('/sso/'):
backend = getattr(request, 'backend', None)
backend_name = getattr(backend, 'name', 'unknown-backend')
full_backend_name = backend_name

View File

@ -1,17 +1,38 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Python
import re
# Python Social Auth
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):
"""Authentication for this user is forbidden"""
def __str__(self):
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):
if kwargs.get('is_new', False):
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):
if user and not user.is_active:
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 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;">
<pre id="cowsay">
________________
@ -19,10 +19,11 @@
<div class="col-xs-12 col-sm-7 text-center">
<img id="about-modal-titlelogo" src="/static/assets/ansible_tower_logo_minimalc.png"><br>
<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>
<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>
</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',
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) {
@ -950,7 +950,8 @@ var tower = angular.module('Tower', [
control_socket.init();
control_socket.on("limit_reached", function(data) {
$log.debug(data.reason);
Timer.expireSession('session_limit');
$rootScope.sessionTimer.expireSession('session_limit');
$location.url('/login');
});
}
openSocket();
@ -999,7 +1000,7 @@ var tower = angular.module('Tower', [
// gets here on timeout
if (next.templateUrl !== (urlPrefix + 'login/loginBackDrop.partial.html')) {
$rootScope.sessionTimer.expireSession('idle');
if (sock) {
if (sock&& sock.socket && sock.socket.socket) {
sock.socket.socket.disconnect();
}
$location.path('/login');
@ -1026,9 +1027,11 @@ var tower = angular.module('Tower', [
$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)
if(!_.contains($location.$$url, '/login')){
$rootScope.sessionTimer = Timer.init();
$rootScope.$emit('OpenSocket');
pendoService.issuePendoIdentity();
Timer.init().then(function(timer){
$rootScope.sessionTimer = timer;
$rootScope.$emit('OpenSocket');
pendoService.issuePendoIdentity();
});
}
}
@ -1063,8 +1066,8 @@ var tower = angular.module('Tower', [
if (!$AnsibleConfig) {
// there may be time lag when loading the config file, so temporarily use what's in local storage
$AnsibleConfig = Store('AnsibleConfig');
// create a promise that will resolve when $AnsibleConfig is loaded
$rootScope.loginConfig = $q.defer();
}
//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,
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();
@ -27,11 +27,33 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
scope.plays = [];
scope.previousTaskFailed = false;
scope.$watch('job_status', function(job_status) {
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));
job_status.explanation = job_status.explanation.split(":")[0] + ". ";
job_status.explanation += "<code>" + taskObj.task_type + "-" + taskObj.task_id + " failed for " + taskObj.task_name + "</code>"
// return a promise from the options request with the permission type choices (including adhoc) as a param
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);
@ -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) {
scope.removeLoadJob();
}
@ -738,6 +705,24 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
scope.verbosity = data.verbosity;
scope.job_tags = data.job_tags;
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++) {
if (verbosity_options[i].value === data.verbosity) {
@ -792,10 +777,6 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
});
//scope.setSearchAll('host');
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) {
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',
'Wait', 'Rest', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList',
'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,
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();
@ -309,29 +309,37 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams,
});
$scope.cancelUpdate = function (id, name) {
// Start the cancel process
var i, project, found = false;
for (i = 0; i < $scope.projects.length; i++) {
if ($scope.projects[i].id === id) {
project = $scope.projects[i];
found = true;
break;
}
}
if (found && project.related.current_update) {
Rest.setUrl(project.related.current_update);
Rest.get()
.success(function (data) {
$scope.$emit('Check_Cancel', data);
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + project.related.current_update + ' failed. GET status: ' + status });
});
} else {
Alert('Update Not Found', 'An SCM update does not appear to be running for project: ' + name + '. Click the <em>Refresh</em> ' +
'button to view the latet status.', 'alert-info');
}
// // Start the cancel process
// var i, project, found = false;
// for (i = 0; i < $scope.projects.length; i++) {
// if ($scope.projects[i].id === id) {
// project = $scope.projects[i];
// found = true;
// break;
// }
// }
Rest.setUrl(GetBasePath("projects") + id);
Rest.get()
.success(function (data) {
if (data.related.current_update) {
Rest.setUrl(data.related.current_update);
Rest.get()
.success(function (data) {
$scope.$emit('Check_Cancel', data);
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + data.related.current_update + ' failed. GET status: ' + status });
});
} 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 () {
@ -384,7 +392,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams,
ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'ProjectList', 'generateList',
'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath',
'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,
LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, LookUpInit, Prompt, GetBasePath, CheckAccess,
OrganizationList, Wait, Stream, permissionsChoices, permissionsLabel, permissionsSearchSelect) {
OrganizationList, Wait, Stream, fieldChoices, fieldLabels, permissionsSearchSelect) {
ClearScope();
@ -192,16 +192,17 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
$scope.permission_search_select = [];
// 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,
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
// scope and be usable by the list form
permissionsChoice.then(function (choices) {
choices =
permissionsLabel({
fieldLabels({
choices: choices
});
_.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
// by the search option for permission_type, you can't inject the
// 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.reset();
$scope.$emit('loadTeam');
});
$scope.team_id = id;
$scope.PermissionAddAllowed = false;
// 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
Wait('start');
Rest.setUrl(defaultUrl + ':id/');
Rest.get({
params: {
id: id
}
})
.success(function (data) {
var fld, related, set;
$scope.team_name = data.name;
for (fld in form.fields) {
if (data[fld]) {
$scope[fld] = data[fld];
master[fld] = $scope[fld];
}
// Retrieve each related set and any lookups
if ($scope.loadTeamRemove) {
$scope.loadTeamRemove();
}
$scope.loadTeamRemove = $scope.$on('loadTeam', function () {
// Retrieve detail record and prepopulate the form
Wait('start');
Rest.setUrl(defaultUrl + ':id/');
Rest.get({
params: {
id: id
}
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 });
});
.success(function (data) {
var fld, related, set;
$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 () {
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',
'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,
ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, GetBasePath,
Prompt, CheckAccess, ResetForm, Wait, Stream, permissionsChoices, permissionsLabel, permissionsSearchSelect) {
Prompt, CheckAccess, ResetForm, Wait, Stream, fieldChoices, fieldLabels, permissionsSearchSelect) {
ClearScope();
@ -224,16 +224,17 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
$scope.permission_search_select = [];
// 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,
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
// scope and be usable by the list form
permissionsChoice.then(function (choices) {
choices =
permissionsLabel({
fieldLabels({
choices: choices
});
_.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) {
$scope.removeFormReady();
}
$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) {
$scope.removePostRefresh();
}
@ -470,54 +472,60 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
// Put form back to its original state
ResetForm();
if ($scope.removeModifyForm) {
$scope.removeModifyForm();
if ($scope.removeLoadForm) {
$scope.removeLoadForm();
}
$scope.removeModifyForm = $scope.$on('modifyForm', function () {
// Modify form based on LDAP settings
Rest.setUrl(GetBasePath('config'));
Rest.get()
.success(function (data) {
var i, fld;
if (data.user_ldap_fields) {
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.removeLoadForm = $scope.$on('loadForm', function () {
if ($scope.removeModifyForm) {
$scope.removeModifyForm();
}
$scope.removeModifyForm = $scope.$on('modifyForm', function () {
// Modify form based on LDAP settings
Rest.setUrl(GetBasePath('config'));
Rest.get()
.success(function (data) {
var i, fld;
if (data.user_ldap_fields) {
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) {
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',
'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',
sourceField: 'name',
ngClick: 'lookUpInventory_script()' ,
addRequired: false,
editRequired: false,
addRequired: true,
editRequired: true,
ngRequired: "source && source.value === 'custom'",
},
extra_vars: {

View File

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

View File

@ -751,9 +751,9 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
$compile(elem)(modal_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 =
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();
@ -1465,6 +1465,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
// Change the lookup and regions when the source changes
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;
SourceChange({ scope: sources_scope, form: SourceForm });
};

View File

@ -25,9 +25,11 @@ export default
.factory('CheckLicense', ['$rootScope', '$compile', 'CreateDialog', 'Store',
'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,
TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location) {
TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location,
pendoService, Authorization, Wait) {
return {
getRemainingDays: function(time_remaining) {
// assumes time_remaining will be in seconds
@ -164,21 +166,32 @@ export default
if (typeof json_data === 'object' && Object.keys(json_data).length > 0) {
Rest.setUrl(url);
Rest.post(json_data)
.success(function () {
try {
$('#license-modal-dialog').dialog('close');
}
catch(e) {
// ignore
}
.success(function (response) {
response.license_info = response;
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;
$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";
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to update license. POST returned: ' + status
ProcessErrors(scope, response.data, response.status, null, { hdr: 'Error!',
msg: 'Failed to update license. POST returned: ' + response.status
});
});
} else {

View File

@ -39,16 +39,25 @@ angular.module('LoadConfigHelper', ['Utilities'])
if(angular.isObject(response.data)){
$AnsibleConfig = _.extend($AnsibleConfig, response.data);
Store('AnsibleConfig', $AnsibleConfig);
if ($rootScope.loginConfig) {
$rootScope.loginConfig.resolve('config loaded');
}
$rootScope.$emit('ConfigReady');
}
else {
$log.info('local_settings.json is not a valid object');
if ($rootScope.loginConfig) {
$rootScope.loginConfig.resolve('config loaded');
}
$rootScope.$emit('ConfigReady');
}
}, function() {
//local_settings.json not found
$log.info('local_settings.json not found');
if ($rootScope.loginConfig) {
$rootScope.loginConfig.resolve('config loaded');
}
$rootScope.$emit('ConfigReady');
});
});
@ -57,15 +66,16 @@ angular.module('LoadConfigHelper', ['Utilities'])
// load config.js
$log.info('attempting to load config.js');
$http({ method:'GET', url: $basePath + 'config.js' })
.success(function(data) {
.then(function(response) {
$log.info('loaded config.js');
$AnsibleConfig = eval(data);
$AnsibleConfig = eval(response.data);
Store('AnsibleConfig', $AnsibleConfig);
$rootScope.$emit('LoadConfig');
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Failed to load ' + $basePath + '/config.js. GET status: ' + status
.catch(function(response) {
response.data = 'Failed to load ' + $basePath + '/config.js';
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 () {
// the following puts our primary scope up for garbage collection, which
// 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();
//$rootScope.$destroy();
@ -78,6 +79,9 @@ export default
$cookieStore.remove('lastPath');
$rootScope.lastPath = '/home';
}
x = Store('sessionTime');
x[$rootScope.current_user.id].loggedIn = false;
Store('sessionTime', x);
$rootScope.lastUser = $cookieStore.get('current_user').id;
$cookieStore.remove('token_expires');
@ -94,7 +98,7 @@ export default
$rootScope.token_expires = null;
$rootScope.login_username = null;
$rootScope.login_password = null;
$rootScope.sessionTimer.expireSession();
$rootScope.sessionTimer.clearTimers();
},
getLicense: function () {
@ -112,6 +116,7 @@ export default
var license = data.license_info;
license.analytics_status = data.analytics_status;
license.version = data.version;
license.ansible_version = data.ansible_version;
license.tested = false;
Store('license', license);
$rootScope.features = Store('license').features;
@ -149,7 +154,6 @@ export default
// store the response values in $rootScope so we can get to them later
$rootScope.current_user = response.results[0];
$cookieStore.put('current_user', response.results[0]); //keep in session cookie in the event of browser refresh
$rootScope.$emit('OpenSocket');
},
restoreUserInfo: function () {

View File

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

View File

@ -23,22 +23,21 @@
*/
export default
['$rootScope', '$cookieStore', 'transitionTo', 'CreateDialog', 'Authorization',
'Store', '$interval',
'Store', '$interval', '$location', '$q',
function ($rootScope, $cookieStore, transitionTo, CreateDialog, Authorization,
Store, $interval) {
Store, $interval, $location, $q) {
return {
sessionTime: null,
timeout: null,
getSessionTime: function () {
if(Store('sessionTime_'+$rootScope.current_user.id)){
return Store('sessionTime_'+$rootScope.current_user.id);
if(Store('sessionTime')){
return Store('sessionTime')[$rootScope.current_user.id].time;
}
else {
return 0;
return 0;
}
},
isExpired: function (increase) {
@ -83,14 +82,24 @@ export default
this.sessionTime = 0;
this.clearTimers();
$cookieStore.put('sessionExpired', true);
transitionTo('signOut');
},
moveForward: function () {
var tm, t;
var tm, t, x, y;
tm = ($AnsibleConfig.session_timeout) ? $AnsibleConfig.session_timeout : 1800;
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;
$cookieStore.put('sessionExpired', false);
this.startTimers();
@ -148,6 +157,14 @@ export default
$('#idle-modal').dialog('close');
}
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);
@ -156,11 +173,15 @@ export default
clearTimers: function(){
$interval.cancel($rootScope.expireTimer);
delete $rootScope.expireTimer;
},
init: function () {
var deferred = $q.defer();
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();
});
if ($AnsibleConfig.custom_logo) {
scope.customLogo = "custom_console_logo.png"
scope.customLogoPresent = true;
} else {
scope.customLogo = "login_modal_logo.png";
scope.customLogoPresent = false;
}
scope.customLoginInfo = $AnsibleConfig.custom_login_info;
scope.customLoginInfoPresent = (scope.customLoginInfo) ? true : false;
$rootScope.loginConfig.promise.then(function () {
scope.customLogo = ($AnsibleConfig.custom_logo) ? "custom_console_logo.png" : "tower_console_logo.png";
scope.customLoginInfo = $AnsibleConfig.custom_login_info;
scope.customLoginInfoPresent = ($AnsibleConfig.customLoginInfo) ? true : false;
});
// Reset the login form
//scope.loginForm.login_username.$setPristine();
@ -171,9 +165,12 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l
Authorization.getUser()
.success(function (data) {
Authorization.setUserInfo(data);
$rootScope.sessionTimer = Timer.init();
$rootScope.user_is_superuser = data.results[0].is_superuser;
scope.$emit('AuthorizationGetLicense');
Timer.init().then(function(timer){
$rootScope.sessionTimer = timer;
$rootScope.$emit('OpenSocket');
$rootScope.user_is_superuser = data.results[0].is_superuser;
scope.$emit('AuthorizationGetLicense');
});
})
.error(function (data, status) {
Authorization.logout();
@ -200,7 +197,7 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l
function (data) {
var key;
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
for (key in data.data) {
scope[key + 'Error'] = data.data[key][0];

View File

@ -25,7 +25,7 @@
var options = [],
error = "";
function parseGoogle(option, key) {
function parseGoogle(option) {
var newOption = {};
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-group" ng-show="job_status.explanation">
<label class="col-lg-2 col-md-12 col-sm-12 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>
<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-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 class="form-group" ng-show="job_status.traceback">

View File

@ -10,8 +10,8 @@
* @description This controller for permissions add
*/
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',
function($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, fieldChoices, fieldLabels) {
ClearScope();
@ -22,13 +22,14 @@ export default
base = $location.path().replace(/^\//, '').split('/')[0],
master = {};
var permissionsChoice = permissionsChoices({
var permissionsChoice = fieldChoices({
scope: $scope,
url: 'api/v1/' + base + '/' + id + '/permissions/'
url: 'api/v1/' + base + '/' + id + '/permissions/',
field: 'permission_type'
});
permissionsChoice.then(function (choices) {
return permissionsLabel({
return fieldLabels({
choices: choices
});
}).then(function (choices) {

View File

@ -10,8 +10,8 @@
* @description This controller for permissions edit
*/
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',
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) {
['$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, fieldChoices, fieldLabels) {
ClearScope();
@ -25,13 +25,14 @@ export default
$scope.permission_label = {};
var permissionsChoice = permissionsChoices({
var permissionsChoice = fieldChoices({
scope: $scope,
url: 'api/v1/' + base + '/' + base_id + '/permissions/'
url: 'api/v1/' + base + '/' + base_id + '/permissions/',
field: 'permission_type'
});
permissionsChoice.then(function (choices) {
return permissionsLabel({
return fieldLabels({
choices: choices
});
}).then(function (choices) {

View File

@ -12,8 +12,8 @@
export default
['$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, 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, fieldChoices, fieldLabels, permissionsSearchSelect) {
ClearScope();
@ -27,16 +27,17 @@ export default
$scope.permission_search_select = [];
// 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,
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
// scope and be usable by the list form
permissionsChoice.then(function (choices) {
choices =
permissionsLabel({
fieldLabels({
choices: choices
});
_.map(choices, function(n, key) {

View File

@ -12,8 +12,6 @@ import list from './shared/permissions.list';
import form from './shared/permissions.form';
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';
export default
@ -25,6 +23,4 @@ export default
.factory('permissionsList', list)
.factory('permissionsForm', form)
.factory('permissionsCategoryChange', permissionsCategoryChange)
.factory('permissionsChoices', permissionsChoices)
.factory('permissionsLabel', permissionsLabel)
.factory('permissionsSearchSelect', permissionsSearchSelect);

View File

@ -16,24 +16,28 @@
['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
return function (params) {
var scope = params.scope,
url = params.url;
url = params.url,
field = params.field;
// Auto populate the field if there is only one result
Rest.setUrl(url);
return Rest.options()
.then(function (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
choices.push(["adhoc",
data.actions.GET.run_ad_hoc_commands.help_text]);
// manually add the adhoc label to the choices object if
// the permission_type field
if (field === "permission_type") {
choices.push(["adhoc",
data.actions.GET.run_ad_hoc_commands.help_text]);
}
return choices;
})
.catch(function (data, status) {
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
[ '$rootScope', '$q',
function ($rootScope, $q) {
[ '$rootScope', '$q', '$injector',
function ($rootScope, $q, $injector) {
return {
response: function(config) {
if(config.headers('auth-token-timeout') !== null){
@ -21,8 +21,10 @@
return config;
},
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');
var location = $injector.get('$location');
location.url('/login');
return $q.reject(rejection);
}
return $q.reject(rejection);

View File

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

View File

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

View File

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

View File

@ -275,7 +275,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
if (/^\-?\d*$/.test(viewValue)) {
// it is valid
ctrl.$setValidity('integer', true);
if ( viewValue === '-' || viewValue === '-0' || viewValue === '' || viewValue === null) {
if ( viewValue === '-' || viewValue === '-0' || viewValue === null) {
ctrl.$setValidity('integer', false);
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)) {
obj2_obj.base = obj2;
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_nolink += obj2 + ' ' + obj2_obj.name + ((activity.operation === 'disassociate') ? ' from ' : ' to ');
descr += obj2 +
" <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) {
name = '';
if (obj2_obj && obj2_obj.name) {
name = ' ' + stripDeleted(obj2_obj.name);
}
descr += obj2 + name + ((activity.operation === 'disassociate') ? ' from ' : ' to ');
descr_nolink += obj2 + name + ((activity.operation === 'disassociate') ? ' from ' : ' to ');
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 ';
}
}
}
if (obj1_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) {
obj1_obj.base = obj1;

View File

@ -31,7 +31,8 @@
window.pendo_options = {
// This is required to be able to load data client side
usePendoAgentAPI: true
usePendoAgentAPI: true,
disableGuides: true
};
</script>
@ -55,7 +56,7 @@
<!-- Password Dialog -->
<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 -->
<div id="form-modal" class="modal fade">
@ -156,7 +157,7 @@
<div id="login-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">
<form name="prompt_for_days_form" id="prompt_for_days_form">

View File

@ -122,7 +122,7 @@ Jenkins
### 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
plugins installed:

View File

@ -38,16 +38,16 @@ successful release.
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>`
* `OFFICIAL=yes`
The following jobs will be triggered:
* [Build_Tower_TAR](http://50.116.42.103/view/Tower/)
* [Build_Tower_DEB](http://50.116.42.103/view/Tower/)
* [Build_Tower_AMI](http://50.116.42.103/view/Tower/)
* [Build_Tower_RPM](http://50.116.42.103/view/Tower/)
* [Build_Tower_Docs](http://50.116.42.103/view/Tower/)
* [Build_Tower_TAR](http://jenkins.testing.ansible.com/view/Tower/)
* [Build_Tower_DEB](http://jenkins.testing.ansible.com/view/Tower/)
* [Build_Tower_AMI](http://jenkins.testing.ansible.com/view/Tower/)
* [Build_Tower_RPM](http://jenkins.testing.ansible.com/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.
@ -66,4 +66,4 @@ While OFFICIAL Tower AMI's are created by jenkins, the process for blessing AMI'
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-extensions==1.3.3
django-polymorphic==0.5.3
django-radius==0.1.1
django-radius==1.0.0
djangorestframework==2.3.13
django-split-settings==0.1.1
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.core==0.4.1
enum34==1.0.4
@ -93,7 +93,7 @@ python-neutronclient==2.3.11
python-novaclient==2.20.0
python-openid==2.2.5
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-swiftclient==2.2.0
python-troveclient==1.0.9

View File

@ -1,6 +1,7 @@
-r requirements.txt
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
pyflakes
pep8

View File

@ -18,4 +18,4 @@ exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inve
[flake8]
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",
"config/awx-httpd-443.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" % munin_plugin_path, ["tools/munin_monitors/tower_jobs",
"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