Merge branch 'release_3.0.0' of github.com:ansible/ansible-tower into JobDetailsAudit

This commit is contained in:
Akita Noek 2016-06-01 09:13:29 -04:00
commit cc507cee1f
83 changed files with 1653 additions and 1152 deletions

View File

@ -219,7 +219,7 @@ class FieldLookupBackend(BaseFilterBackend):
else:
q = Q(**{k:v})
queryset = queryset.filter(q)
queryset = queryset.filter(*args)
queryset = queryset.filter(*args).distinct()
return queryset
except (FieldError, FieldDoesNotExist, ValueError), e:
raise ParseError(e.args[0])

View File

@ -1766,7 +1766,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
notification_templates_any = reverse('api:job_template_notification_templates_any_list', args=(obj.pk,)),
notification_templates_success = reverse('api:job_template_notification_templates_success_list', args=(obj.pk,)),
notification_templates_error = reverse('api:job_template_notification_templates_error_list', args=(obj.pk,)),
access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
survey_spec = reverse('api:job_template_survey_spec', args=(obj.pk,)),
labels = reverse('api:job_template_label_list', args=(obj.pk,)),
roles = reverse('api:job_template_roles_list', args=(obj.pk,)),

View File

@ -2212,6 +2212,13 @@ class JobTemplateList(ListCreateAPIView):
serializer_class = JobTemplateSerializer
always_allow_superuser = False
def post(self, request, *args, **kwargs):
ret = super(JobTemplateList, self).post(request, *args, **kwargs)
if ret.status_code == 201:
job_template = JobTemplate.objects.get(id=ret.data['id'])
job_template.admin_role.members.add(request.user)
return ret
class JobTemplateDetail(RetrieveUpdateDestroyAPIView):
model = JobTemplate

View File

@ -773,7 +773,9 @@ class JobTemplateAccess(BaseAccess):
inventory_pk = get_pk_from_dict(data, 'inventory')
inventory = Inventory.objects.filter(id=inventory_pk)
if not inventory.exists() and not data.get('ask_inventory_on_launch', False):
return False # Does this make sense? Maybe should check read access
return False
if inventory.exists() and self.user not in inventory[0].use_role:
return False
project_pk = get_pk_from_dict(data, 'project')
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
@ -786,10 +788,8 @@ class JobTemplateAccess(BaseAccess):
# If the user has admin access to the project (as an org admin), should
# be able to proceed without additional checks.
project = get_object_or_400(Project, pk=project_pk)
if self.user in project.admin_role:
return True
return self.user in project.admin_role and self.user in inventory.read_role
return self.user in project.use_role
def can_start(self, obj, validate_license=True):
# Check license.
@ -817,13 +817,62 @@ class JobTemplateAccess(BaseAccess):
if self.user not in obj.admin_role:
return False
if data is not None:
data_for_change = dict(data)
data = dict(data)
if self.changes_are_non_sensitive(obj, data):
if 'job_type' in data and obj.job_type != data['job_type'] and data['job_type'] == PERM_INVENTORY_SCAN:
self.check_license(feature='system_tracking')
if 'survey_enabled' in data and obj.survey_enabled != data['survey_enabled'] and data['survey_enabled']:
self.check_license(feature='surveys')
return True
for required_field in ('credential', 'cloud_credential', 'inventory', 'project'):
required_obj = getattr(obj, required_field, None)
if required_field not in data_for_change and required_obj is not None:
data_for_change[required_field] = required_obj.pk
return self.can_read(obj) and self.can_add(data_for_change)
def changes_are_non_sensitive(self, obj, data):
'''
Returne true if the changes being made are considered nonsensitive, and
thus can be made by a job template administrator which may not have access
to the any inventory, project, or credentials associated with the template.
'''
# We are white listing fields that can
field_whitelist = [
'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars',
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_inventory_on_launch',
'ask_credential_on_launch', 'survey_enabled'
]
for k, v in data.items():
if hasattr(obj, k) and getattr(obj, k) != v:
if k not in field_whitelist:
return False
return True
def can_update_sensitive_fields(self, obj, data):
project_id = data.get('project', obj.project.id if obj.project else None)
inventory_id = data.get('inventory', obj.inventory.id if obj.inventory else None)
credential_id = data.get('credential', obj.credential.id if obj.credential else None)
cloud_credential_id = data.get('cloud_credential', obj.cloud_credential.id if obj.cloud_credential else None)
network_credential_id = data.get('network_credential', obj.network_credential.id if obj.network_credential else None)
if project_id and self.user not in Project.objects.get(pk=project_id).use_role:
return False
if inventory_id and self.user not in Inventory.objects.get(pk=inventory_id).use_role:
return False
if credential_id and self.user not in Credential.objects.get(pk=credential_id).use_role:
return False
if cloud_credential_id and self.user not in Credential.objects.get(pk=cloud_credential_id).use_role:
return False
if network_credential_id and self.user not in Credential.objects.get(pk=network_credential_id).use_role:
return False
return True
def can_delete(self, obj):
return self.user in obj.admin_role

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from awx.main.migrations import _rbac as rbac
from awx.main.migrations import _ask_for_variables as ask_for_variables
from awx.main.migrations import _migration_utils as migration_utils
from django.db import migrations
@ -15,4 +16,5 @@ class Migration(migrations.Migration):
operations = [
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
migrations.RunPython(ask_for_variables.migrate_credential),
migrations.RunPython(rbac.rebuild_role_hierarchy),
]

View File

@ -215,7 +215,7 @@ def migrate_inventory(apps, schema_editor):
Inventory = apps.get_model('main', 'Inventory')
Permission = apps.get_model('main', 'Permission')
def role_from_permission():
def role_from_permission(perm):
if perm.permission_type == 'admin':
return inventory.admin_role
elif perm.permission_type == 'read':
@ -233,7 +233,7 @@ def migrate_inventory(apps, schema_editor):
role = None
execrole = None
role = role_from_permission()
role = role_from_permission(perm)
if role is None:
raise Exception(smart_text(u'Unhandled permission type for inventory: {}'.format( perm.permission_type)))
@ -320,24 +320,30 @@ def migrate_projects(apps, schema_editor):
logger.warn(smart_text(u'adding Project({}) admin: {}'.format(project.name, project.created_by.username)))
for team in project.deprecated_teams.all():
team.member_role.children.add(project.use_role)
team.member_role.children.add(project.read_role)
logger.info(smart_text(u'adding Team({}) access for Project({})'.format(team.name, project.name)))
if project.organization is not None:
for user in project.organization.deprecated_users.all():
project.use_role.members.add(user)
logger.info(smart_text(u'adding Organization({}) member access to Project({})'.format(project.organization.name, project.name)))
for perm in Permission.objects.filter(project=project):
# All perms at this level just imply a user or team can read
if perm.permission_type == 'create':
role = project.use_role
else:
role = project.read_role
if perm.team:
perm.team.member_role.children.add(project.use_role)
perm.team.member_role.children.add(role)
logger.info(smart_text(u'adding Team({}) access for Project({})'.format(perm.team.name, project.name)))
if perm.user:
project.use_role.members.add(perm.user)
role.members.add(perm.user)
logger.info(smart_text(u'adding User({}) access for Project({})'.format(perm.user.username, project.name)))
if project.organization is not None:
for user in project.organization.deprecated_users.all():
if not (project.use_role.members.filter(pk=user.id).exists() or project.admin_role.members.filter(pk=user.id).exists()):
project.read_role.members.add(user)
logger.info(smart_text(u'adding Organization({}) member access to Project({})'.format(project.organization.name, project.name)))
@log_migration
def migrate_job_templates(apps, schema_editor):
@ -403,7 +409,7 @@ def migrate_job_templates(apps, schema_editor):
team_create_permissions = set(
jt_permission_qs
.filter(permission_type__in=['create'] if jt.job_type == 'check' else ['create'])
.filter(permission_type__in=['create'])
.values_list('team__id', flat=True)
)
team_run_permissions = set(
@ -413,12 +419,12 @@ def migrate_job_templates(apps, schema_editor):
)
user_create_permissions = set(
jt_permission_qs
.filter(permission_type__in=['create'] if jt.job_type == 'check' else ['run'])
.filter(permission_type__in=['create'])
.values_list('user__id', flat=True)
)
user_run_permissions = set(
jt_permission_qs
.filter(permission_type__in=['check', 'run'] if jt.job_type == 'check' else ['create'])
.filter(permission_type__in=['check', 'run'] if jt.job_type == 'check' else ['run'])
.values_list('user__id', flat=True)
)
@ -446,17 +452,20 @@ def migrate_job_templates(apps, schema_editor):
logger.info(smart_text(u'transfering execute access on JobTemplate({}) to Team({})'.format(jt.name, team.name)))
for user in User.objects.filter(id__in=user_create_permissions).iterator():
cred = jt.credential or jt.cloud_credential
if (jt.inventory.id in user_inv_permissions[user.id] or
any([jt.inventory.id in team_inv_permissions[team.id] for team in user.deprecated_teams.all()])) and \
((not jt.credential and not jt.cloud_credential) or
Credential.objects.filter(Q(deprecated_user=user) | Q(deprecated_team__deprecated_users=user), jobtemplates=jt).exists()):
(not cred or cred.deprecated_user == user or
(cred.deprecated_team and cred.deprecated_team.deprecated_users.filter(pk=user.id).exists())):
jt.admin_role.members.add(user)
logger.info(smart_text(u'transfering admin access on JobTemplate({}) to User({})'.format(jt.name, user.username)))
for user in User.objects.filter(id__in=user_run_permissions).iterator():
cred = jt.credential or jt.cloud_credential
if (jt.inventory.id in user_inv_permissions[user.id] or
any([jt.inventory.id in team_inv_permissions[team.id] for team in user.deprecated_teams.all()])) and \
((not jt.credential and not jt.cloud_credential) or
Credential.objects.filter(Q(deprecated_user=user) | Q(deprecated_team__deprecated_users=user), jobtemplates=jt).exists()):
(not cred or cred.deprecated_user == user or
(cred.deprecated_team and cred.deprecated_team.deprecated_users.filter(pk=user.id).exists())):
jt.execute_role.members.add(user)
logger.info(smart_text(u'transfering execute access on JobTemplate({}) to User({})'.format(jt.name, user.username)))
@ -468,8 +477,6 @@ def rebuild_role_hierarchy(apps, schema_editor):
start = time()
roots = Role.objects \
.all() \
.exclude(pk__in=Role.parents.through.objects.all()
.values_list('from_role_id', flat=True).distinct()) \
.values_list('id', flat=True)
stop = time()
logger.info('Found %d roots in %f seconds, rebuilding ancestry map' % (len(roots), stop - start))

View File

@ -379,11 +379,16 @@ def activity_stream_associate(sender, instance, **kwargs):
obj1 = instance
object1=camelcase_to_underscore(obj1.__class__.__name__)
obj_rel = sender.__module__ + "." + sender.__name__
for entity_acted in kwargs['pk_set']:
obj2 = kwargs['model']
obj2_id = entity_acted
obj2_actual = obj2.objects.get(id=obj2_id)
object2 = camelcase_to_underscore(obj2.__name__)
if isinstance(obj2_actual, Role) and obj2_actual.content_object is not None:
obj2_actual = obj2_actual.content_object
object2 = camelcase_to_underscore(obj2_actual.__class__.__name__)
else:
object2 = camelcase_to_underscore(obj2.__name__)
# Skip recording any inventory source, or system job template changes here.
if isinstance(obj1, InventorySource) or isinstance(obj2_actual, InventorySource):
continue
@ -409,7 +414,7 @@ def activity_stream_associate(sender, instance, **kwargs):
# If the m2m is from the User side we need to
# set the content_object of the Role for our entry.
if type(instance) == User and role.content_object is not None:
getattr(activity_entry, role.content_type.name).add(role.content_object)
getattr(activity_entry, role.content_type.name.replace(' ', '_')).add(role.content_object)
activity_entry.role.add(role)
activity_entry.object_relationship_type = obj_rel

View File

@ -441,7 +441,12 @@ class BaseTask(Task):
if settings.ANSIBLE_USE_VENV:
env['VIRTUAL_ENV'] = settings.ANSIBLE_VENV_PATH
env['PATH'] = os.path.join(settings.ANSIBLE_VENV_PATH, "bin") + ":" + env['PATH']
env['PYTHONPATH'] = os.path.join(settings.ANSIBLE_VENV_PATH, "lib/python2.7/site-packages/") + ":"
venv_libdir = os.path.join(settings.ANSIBLE_VENV_PATH, "lib")
env.pop('PYTHONPATH', None) # default to none if no python_ver matches
for python_ver in ["python2.7", "python2.6"]:
if os.path.isdir(os.path.join(venv_libdir, python_ver)):
env['PYTHONPATH'] = os.path.join(venv_libdir, python_ver, "site-packages") + ":"
break
if self.should_use_proot(instance, **kwargs):
env['PROOT_TMP_DIR'] = tower_settings.AWX_PROOT_BASE_PATH
return env

View File

@ -0,0 +1,65 @@
factories
=========
This is a module for defining stand-alone factories and fixtures. Ideally a fixture will implement a single item.
DO NOT decorate fixtures in this module with the @pytest.fixture. These fixtures are to be combined
with fixture factories and composition using the `conftest.py` convention. Those composed fixtures
will be decorated for usage and discovery.
Use the fixtures directly in factory methods to build up the desired set of components and relationships.
Each fixture should create exactly one object and should support the option for that object to be persisted
or not.
A factory should create at a minimum a single object for that factory type. The creation of any
associated objects should be explicit. For example, the `create_organization` factory when given only
a `name` parameter will create an Organization but it will not implicitly create any other objects.
teams
-----
There is some special handling for users when adding teams. There is a short hand that allows you to
assign a user to the member\_role of a team using the string notation of `team_name:user_name`. There is
no shortcut for adding a user to the admin\_role of a team. See the roles section for more information
about how to do that.
roles
-----
The roles helper allows you pass in roles to a factory. These roles assignments will happen after
the objects are created. Using the roles parameter required that persisted=True (default).
You can use a string notation of `object_name.role_name:user` OR `object_name.role_name:object_name.child_role`
obj.parent_role:user # This will make the user a member of parent_role
obj1.role:obj2.role # This will make obj2 a child role of obj1
team1.admin_role:joe
team1.admin_role:project1.admin_role
examples
--------
objects = create_organization('test-org')
assert objects.organization.name == 'test-org'
objects = create_organization('test-org', projects=['test-proj'])
assert objects.projects.test-proj.organization == objects.organization
objects = create_organization('test-org', persisted=False)
assert not objects.organization.pk
patterns
--------
`mk` functions are single object fixtures. They should create only a single object with the minimum deps.
They should also accept a `persited` flag, if they must be persisted to work, they raise an error if persisted=False
`generate` and `apply` functions are helpers that build up the various parts of a `create` functions objects. These
should be useful for more than one create function to use and should explicitly accept all of the values needed
to execute. These functions should also be robust and have very speciifc error reporting about constraints and/or
bad values.
`create` functions compose many of the `mk` and `generate` functions to make different object
factories. These functions when giving the minimum set of arguments should only produce a
single artifact (or the minimum needed for that object). These should be wrapped by discoverable
fixtures in various conftest.py files.

View File

@ -0,0 +1,16 @@
from .tower import (
create_organization,
create_job_template,
create_notification_template,
)
from .exc import (
NotUnique,
)
__all__ = [
'create_organization',
'create_job_template',
'create_notification_template',
'NotUnique',
]

View File

@ -0,0 +1,5 @@
class NotUnique(Exception):
def __init__(self, name, objects):
msg = '{} is not a unique key, found {}={}'.format(name, name, objects[name])
super(Exception, self).__init__(msg)

View File

@ -0,0 +1,124 @@
from django.contrib.auth.models import User
from awx.main.models import (
Organization,
Project,
Team,
Instance,
JobTemplate,
NotificationTemplate,
Credential,
Inventory,
Label,
)
# mk methods should create only a single object of a single type.
# they should also have the option of being persisted or not.
# if the object must be persisted an error should be raised when
# persisted=False
#
def mk_instance(persisted=True):
if not persisted:
raise RuntimeError('creating an Instance requires persisted=True')
from django.conf import settings
return Instance.objects.get_or_create(uuid=settings.SYSTEM_UUID, primary=True, hostname="instance.example.org")
def mk_organization(name, description=None, persisted=True):
description = description or '{}-description'.format(name)
org = Organization(name=name, description=description)
if persisted:
mk_instance(persisted)
org.save()
return org
def mk_label(name, organization=None, description=None, persisted=True):
description = description or '{}-description'.format(name)
label = Label(name=name, description=description)
if organization is not None:
label.organization = organization
if persisted:
label.save()
return label
def mk_team(name, organization=None, persisted=True):
team = Team(name=name)
if organization is not None:
team.organization = organization
if persisted:
mk_instance(persisted)
team.save()
return team
def mk_user(name, is_superuser=False, organization=None, team=None, persisted=True):
user = User(username=name, is_superuser=is_superuser)
if persisted:
user.save()
if organization is not None:
organization.member_role.members.add(user)
if team is not None:
team.member_role.members.add(user)
return user
def mk_project(name, organization=None, description=None, persisted=True):
description = description or '{}-description'.format(name)
project = Project(name=name, description=description)
if organization is not None:
project.organization = organization
if persisted:
project.save()
return project
def mk_credential(name, cloud=False, kind='ssh', persisted=True):
cred = Credential(name=name, cloud=cloud, kind=kind)
if persisted:
cred.save()
return cred
def mk_notification_template(name, notification_type='webhook', configuration=None, organization=None, persisted=True):
nt = NotificationTemplate(name=name)
nt.notification_type = notification_type
nt.notification_configuration = configuration or dict(url="http://localhost", headers={"Test": "Header"})
if organization is not None:
nt.organization = organization
if persisted:
nt.save()
return nt
def mk_inventory(name, organization=None, persisted=True):
inv = Inventory(name=name)
if organization is not None:
inv.organization = organization
if persisted:
inv.save()
return inv
def mk_job_template(name, job_type='run',
organization=None, inventory=None,
credential=None, persisted=True,
project=None):
jt = JobTemplate(name=name, job_type=job_type, playbook='mocked')
jt.inventory = inventory
if jt.inventory is None:
jt.ask_inventory_on_launch = True
jt.credential = credential
if jt.credential is None:
jt.ask_credential_on_launch = True
jt.project = project
if persisted:
jt.save()
return jt

View File

@ -0,0 +1,313 @@
from collections import namedtuple
from django.contrib.auth.models import User
from awx.main.models import (
Organization,
Project,
Team,
NotificationTemplate,
Credential,
Inventory,
Label,
)
from .fixtures import (
mk_organization,
mk_team,
mk_user,
mk_job_template,
mk_credential,
mk_inventory,
mk_project,
mk_label,
mk_notification_template,
)
from .exc import NotUnique
def generate_objects(artifacts, kwargs):
'''generate_objects takes a list of artifacts that are supported by
a create function and compares it to the kwargs passed in to the create
function. If a kwarg is found that is not in the artifacts list a RuntimeError
is raised.
'''
for k in kwargs.keys():
if k not in artifacts:
raise RuntimeError('{} is not a valid argument'.format(k))
return namedtuple("Objects", ",".join(artifacts))
def generate_role_objects(objects):
'''generate_role_objects assembles a dictionary of all possible objects by name.
It will raise an exception if any of the objects share a name due to the fact that
it is to be used with apply_roles, which expects unique object names.
roles share a common name e.g. admin_role, member_role. This ensures that the
roles short hand used for mapping Roles and Users in apply_roles will function as desired.
'''
combined_objects = {}
for o in objects:
if type(o) is dict:
for k,v in o.iteritems():
if combined_objects.get(k) is not None:
raise NotUnique(k, combined_objects)
combined_objects[k] = v
elif hasattr(o, 'name'):
if combined_objects.get(o.name) is not None:
raise NotUnique(o.name, combined_objects)
combined_objects[o.name] = o
else:
if o is not None:
raise RuntimeError('expected a list of dict or list of list, got a type {}'.format(type(o)))
return combined_objects
def apply_roles(roles, objects, persisted):
'''apply_roles evaluates a list of Role relationships represented as strings.
The format of this string is 'role:[user|role]'. When a user is provided, they will be
made a member of the role on the LHS. When a role is provided that role will be added to
the children of the role on the LHS.
This function assumes that objects is a dictionary that contains a unique set of key to value
mappings for all possible "Role objects". See the example below:
Mapping Users
-------------
roles = ['org1.admin_role:user1', 'team1.admin_role:user1']
objects = {'org1': Organization, 'team1': Team, 'user1': User]
Mapping Roles
-------------
roles = ['org1.admin_role:team1.admin_role']
objects = {'org1': Organization, 'team1': Team}
Invalid Mapping
---------------
roles = ['org1.admin_role:team1.admin_role']
objects = {'org1': Organization', 'user1': User} # Exception, no team1 entry
'''
if roles is None:
return None
if not persisted:
raise RuntimeError('roles can not be used when persisted=False')
for role in roles:
obj_role, sep, member_role = role.partition(':')
if not member_role:
raise RuntimeError('you must provide an assignment role, got None')
obj_str, o_role_str = obj_role.split('.')
member_str, m_sep, m_role_str = member_role.partition('.')
obj = objects[obj_str]
obj_role = getattr(obj, o_role_str)
member = objects[member_str]
if m_role_str:
if hasattr(member, m_role_str):
member_role = getattr(member, m_role_str)
obj_role.children.add(member_role)
else:
raise RuntimeError('unable to find {} role for {}'.format(m_role_str, member_str))
else:
if type(member) is User:
obj_role.members.add(member)
else:
raise RuntimeError('unable to add non-user {} for members list of {}'.format(member_str, obj_str))
def generate_users(organization, teams, superuser, persisted, **kwargs):
'''generate_users evaluates a mixed list of User objects and strings.
If a string is encountered a user with that username is created and added to the lookup dict.
If a User object is encountered the User.username is used as a key for the lookup dict.
A short hand for assigning a user to a team is available in the following format: "team_name:username".
If a string in that format is encounted an attempt to lookup the team by the key team_name from the teams
argumnent is made, a KeyError will be thrown if the team does not exist in the dict. The teams argument should
be a dict of {Team.name:Team}
'''
users = {}
key = 'superusers' if superuser else 'users'
if key in kwargs and kwargs.get(key) is not None:
for u in kwargs[key]:
if type(u) is User:
users[u.username] = u
else:
p1, sep, p2 = u.partition(':')
if p2:
t = teams[p1]
users[p2] = mk_user(p2, organization=organization, team=t, is_superuser=superuser, persisted=persisted)
else:
users[p1] = mk_user(p1, organization=organization, team=None, is_superuser=superuser, persisted=persisted)
return users
def generate_teams(organization, persisted, **kwargs):
'''generate_teams evalutes a mixed list of Team objects and strings.
If a string is encountered a team with that string name is created and added to the lookup dict.
If a Team object is encounted the Team.name is used as a key for the lookup dict.
'''
teams = {}
if 'teams' in kwargs and kwargs.get('teams') is not None:
for t in kwargs['teams']:
if type(t) is Team:
teams[t.name] = t
else:
teams[t] = mk_team(t, organization=organization, persisted=persisted)
return teams
class _Mapped(object):
'''_Mapped is a helper class that replaces spaces and dashes
in the name of an object and assigns the object as an attribute
input: {'my org': Organization}
output: instance.my_org = Organization
'''
def __init__(self, d):
self.d = d
for k,v in d.items():
k = k.replace(' ', '_')
k = k.replace('-', '_')
setattr(self, k.replace(' ','_'), v)
def all(self):
return self.d.values()
# create methods are intended to be called directly as needed
# or encapsulated by specific factory fixtures in a conftest
#
def create_job_template(name, roles=None, persisted=True, **kwargs):
Objects = generate_objects(["job_template",
"organization",
"inventory",
"project",
"credential",
"job_type",], kwargs)
org = None
proj = None
inv = None
cred = None
job_type = kwargs.get('job_type', 'run')
if 'organization' in kwargs:
org = kwargs['organization']
if type(org) is not Organization:
org = mk_organization(org, '%s-desc'.format(org), persisted=persisted)
if 'credential' in kwargs:
cred = kwargs['credential']
if type(cred) is not Credential:
cred = mk_credential(cred, persisted=persisted)
if 'project' in kwargs:
proj = kwargs['project']
if type(proj) is not Project:
proj = mk_project(proj, organization=org, persisted=persisted)
if 'inventory' in kwargs:
inv = kwargs['inventory']
if type(inv) is not Inventory:
inv = mk_inventory(inv, organization=org, persisted=persisted)
jt = mk_job_template(name, project=proj,
inventory=inv, credential=cred,
job_type=job_type, persisted=persisted)
role_objects = generate_role_objects([org, proj, inv, cred])
apply_roles(roles, role_objects, persisted)
return Objects(job_template=jt,
project=proj,
inventory=inv,
credential=cred,
job_type=job_type,
organization=org,)
def create_organization(name, roles=None, persisted=True, **kwargs):
Objects = generate_objects(["organization",
"teams", "users",
"superusers",
"projects",
"labels",
"notification_templates",
"inventories",], kwargs)
projects = {}
inventories = {}
labels = {}
notification_templates = {}
org = mk_organization(name, '%s-desc'.format(name), persisted=persisted)
if 'inventories' in kwargs:
for i in kwargs['inventories']:
if type(i) is Inventory:
inventories[i.name] = i
else:
inventories[i] = mk_inventory(i, organization=org, persisted=persisted)
if 'projects' in kwargs:
for p in kwargs['projects']:
if type(p) is Project:
projects[p.name] = p
else:
projects[p] = mk_project(p, organization=org, persisted=persisted)
teams = generate_teams(org, persisted, teams=kwargs.get('teams'))
superusers = generate_users(org, teams, True, persisted, superusers=kwargs.get('superusers'))
users = generate_users(org, teams, False, persisted, users=kwargs.get('users'))
if 'labels' in kwargs:
for l in kwargs['labels']:
if type(l) is Label:
labels[l.name] = l
else:
labels[l] = mk_label(l, organization=org, persisted=persisted)
if 'notification_templates' in kwargs:
for nt in kwargs['notification_templates']:
if type(nt) is NotificationTemplate:
notification_templates[nt.name] = nt
else:
notification_templates[nt] = mk_notification_template(nt, organization=org, persisted=persisted)
role_objects = generate_role_objects([org, superusers, users, teams, projects, labels, notification_templates])
apply_roles(roles, role_objects, persisted)
return Objects(organization=org,
superusers=_Mapped(superusers),
users=_Mapped(users),
teams=_Mapped(teams),
projects=_Mapped(projects),
labels=_Mapped(labels),
notification_templates=_Mapped(notification_templates),
inventories=_Mapped(inventories))
def create_notification_template(name, roles=None, persisted=True, **kwargs):
Objects = generate_objects(["notification_template",
"organization",
"users",
"superusers",
"teams",], kwargs)
organization = None
if 'organization' in kwargs:
org = kwargs['organization']
organization = mk_organization(org, '{}-desc'.format(org), persisted=persisted)
notification_template = mk_notification_template(name, organization=organization, persisted=persisted)
teams = generate_teams(organization, persisted, teams=kwargs.get('teams'))
superusers = generate_users(organization, teams, True, persisted, superusers=kwargs.get('superusers'))
users = generate_users(organization, teams, False, persisted, users=kwargs.get('users'))
role_objects = generate_role_objects([organization, notification_template])
apply_roles(roles, role_objects, persisted)
return Objects(notification_template=notification_template,
organization=organization,
users=_Mapped(users),
superusers=_Mapped(superusers),
teams=teams)

View File

@ -131,3 +131,24 @@ def test_stream_queryset_hides_shows_items(
assert queryset.filter(host__pk=host.pk, operation='create').count() == 1
assert queryset.filter(team__pk=team.pk, operation='create').count() == 1
assert queryset.filter(notification_template__pk=notification_template.pk, operation='create').count() == 1
@pytest.mark.django_db
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
def test_stream_user_direct_role_updates(get, post, organization_factory):
objects = organization_factory('test_org',
superusers=['admin'],
users=['test'],
inventories=['inv1'])
url = reverse('api:user_roles_list', args=(objects.users.test.pk,))
post(url, dict(id=objects.inventories.inv1.read_role.pk), objects.superusers.admin)
activity_stream = ActivityStream.objects.filter(
inventory__pk=objects.inventories.inv1.pk,
user__pk=objects.users.test.pk,
role__pk=objects.inventories.inv1.read_role.pk).first()
url = reverse('api:activity_stream_detail', args=(activity_stream.pk,))
response = get(url, objects.users.test)
assert response.data['object1'] == 'user'
assert response.data['object2'] == 'inventory'

View File

@ -0,0 +1,148 @@
import mock # noqa
import pytest
from django.core.urlresolvers import reverse
"""
def run_test_ad_hoc_command(self, **kwargs):
# Post to list to start a new ad hoc command.
expect = kwargs.pop('expect', 201)
url = kwargs.pop('url', reverse('api:ad_hoc_command_list'))
data = {
'inventory': self.inventory.pk,
'credential': self.credential.pk,
'module_name': 'command',
'module_args': 'uptime',
}
data.update(kwargs)
for k,v in data.items():
if v is None:
del data[k]
return self.post(url, data, expect=expect)
"""
@pytest.fixture
def post_adhoc(post, inventory, machine_credential):
def f(url, data, user, expect=201):
if not url:
url = reverse('api:ad_hoc_command_list')
if 'module_name' not in data:
data['module_name'] = 'command'
if 'module_args' not in data:
data['module_args'] = 'uptime'
if 'inventory' not in data:
data['inventory'] = inventory.id
if 'credential' not in data:
data['credential'] = machine_credential.id
for k,v in data.items():
if v is None:
del data[k]
return post(url, data, user, expect=expect)
return f
@pytest.mark.django_db
def test_admin_post_ad_hoc_command_list(admin, post_adhoc, inventory, machine_credential):
res = post_adhoc(reverse('api:ad_hoc_command_list'), {}, admin, expect=201)
assert res.data['job_type'] == 'run'
assert res.data['inventory'], inventory.id
assert res.data['credential'] == machine_credential.id
assert res.data['module_name'] == 'command'
assert res.data['module_args'] == 'uptime'
assert res.data['limit'] == ''
assert res.data['forks'] == 0
assert res.data['verbosity'] == 0
assert res.data['become_enabled'] is False
@pytest.mark.django_db
def test_empty_post_403(admin, post):
post(reverse('api:ad_hoc_command_list'), {}, admin, expect=400)
@pytest.mark.django_db
def test_empty_put_405(admin, put):
put(reverse('api:ad_hoc_command_list'), {}, admin, expect=405)
@pytest.mark.django_db
def test_empty_patch_405(admin, patch):
patch(reverse('api:ad_hoc_command_list'), {}, admin, expect=405)
@pytest.mark.django_db
def test_empty_delete_405(admin, delete):
delete(reverse('api:ad_hoc_command_list'), admin, expect=405)
@pytest.mark.django_db
def test_user_post_ad_hoc_command_list(alice, post_adhoc, inventory, machine_credential):
inventory.adhoc_role.members.add(alice)
machine_credential.use_role.members.add(alice)
post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=201)
@pytest.mark.django_db
def test_user_post_ad_hoc_command_list_xfail(alice, post_adhoc, inventory, machine_credential):
inventory.read_role.members.add(alice) # just read access? no dice.
machine_credential.use_role.members.add(alice)
post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=403)
@pytest.mark.django_db
def test_user_post_ad_hoc_command_list_without_creds(alice, post_adhoc, inventory, machine_credential):
inventory.adhoc_role.members.add(alice)
post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=403)
@pytest.mark.django_db
def test_user_post_ad_hoc_command_list_without_inventory(alice, post_adhoc, inventory, machine_credential):
machine_credential.use_role.members.add(alice)
post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=403)
@pytest.mark.django_db
def test_admin_post_inventory_ad_hoc_command_list(admin, post_adhoc, inventory):
post_adhoc(reverse('api:inventory_ad_hoc_commands_list', args=(inventory.id,)), {'inventory': None}, admin, expect=201)
post_adhoc(reverse('api:inventory_ad_hoc_commands_list', args=(inventory.id,)), {}, admin, expect=201)
@pytest.mark.django_db
def test_get_inventory_ad_hoc_command_list(admin, alice, post_adhoc, get, inventory_factory, machine_credential):
inv1 = inventory_factory('inv1')
inv2 = inventory_factory('inv2')
post_adhoc(reverse('api:ad_hoc_command_list'), {'inventory': inv1.id}, admin, expect=201)
post_adhoc(reverse('api:ad_hoc_command_list'), {'inventory': inv2.id}, admin, expect=201)
res = get(reverse('api:ad_hoc_command_list'), admin, expect=200)
assert res.data['count'] == 2
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), admin, expect=200)
assert res.data['count'] == 1
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv2.id,)), admin, expect=200)
assert res.data['count'] == 1
inv1.adhoc_role.members.add(alice)
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), alice, expect=200)
assert res.data['count'] == 0
machine_credential.use_role.members.add(alice)
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), alice, expect=200)
assert res.data['count'] == 1
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv2.id,)), alice, expect=403)
@pytest.mark.django_db
def test_bad_data1(admin, post_adhoc):
post_adhoc(reverse('api:ad_hoc_command_list'), {'module_name': 'command', 'module_args': None}, admin, expect=400)
@pytest.mark.django_db
def test_bad_data2(admin, post_adhoc):
post_adhoc(reverse('api:ad_hoc_command_list'), {'job_type': 'baddata'}, admin, expect=400)
@pytest.mark.django_db
def test_bad_data3(admin, post_adhoc):
post_adhoc(reverse('api:ad_hoc_command_list'), {'verbosity': -1}, admin, expect=400)
@pytest.mark.django_db
def test_bad_data4(admin, post_adhoc):
post_adhoc(reverse('api:ad_hoc_command_list'), {'forks': -1}, admin, expect=400)

View File

@ -0,0 +1,18 @@
import pytest
from django.core.urlresolvers import reverse
@pytest.mark.django_db
def test_job_template_role_user(post, organization_factory, job_template_factory):
objects = organization_factory("org",
superusers=['admin'],
users=['test'])
jt_objects = job_template_factory("jt",
organization=objects.organization,
inventory='test_inv',
project='test_proj')
url = reverse('api:user_roles_list', args=(objects.users.test.pk,))
response = post(url, dict(id=jt_objects.job_template.execute_role.pk), objects.superusers.admin)
assert response.status_code == 204

View File

@ -0,0 +1,111 @@
import mock # noqa
import pytest
from awx.main.models.projects import ProjectOptions
from django.core.urlresolvers import reverse
@property
def project_playbooks(self):
return ['mocked.yml', 'alt-mocked.yml']
@pytest.mark.django_db
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
@pytest.mark.parametrize(
"grant_project, grant_credential, grant_inventory, expect", [
(True, True, True, 201),
(True, True, False, 403),
(True, False, True, 403),
(False, True, True, 403),
]
)
def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_credential, grant_inventory, expect):
if grant_project:
project.use_role.members.add(alice)
if grant_credential:
machine_credential.use_role.members.add(alice)
if grant_inventory:
inventory.use_role.members.add(alice)
post(reverse('api:job_template_list'), {
'name': 'Some name',
'project': project.id,
'credential': machine_credential.id,
'inventory': inventory.id,
'playbook': 'mocked.yml',
}, alice, expect=expect)
@pytest.mark.django_db
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
@pytest.mark.parametrize(
"grant_project, grant_credential, grant_inventory, expect", [
(True, True, True, 200),
(True, True, False, 403),
(True, False, True, 403),
(False, True, True, 403),
]
)
def test_edit_sensitive_fields(patch, job_template_factory, alice, grant_project, grant_credential, grant_inventory, expect):
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
objs.job_template.admin_role.members.add(alice)
if grant_project:
objs.project.use_role.members.add(alice)
if grant_credential:
objs.credential.use_role.members.add(alice)
if grant_inventory:
objs.inventory.use_role.members.add(alice)
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
'name': 'Some name',
'project': objs.project.id,
'credential': objs.credential.id,
'inventory': objs.inventory.id,
'playbook': 'alt-mocked.yml',
}, alice, expect=expect)
@pytest.mark.django_db
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
def test_edit_playbook(patch, job_template_factory, alice):
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
objs.job_template.admin_role.members.add(alice)
objs.project.use_role.members.add(alice)
objs.credential.use_role.members.add(alice)
objs.inventory.use_role.members.add(alice)
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
'playbook': 'alt-mocked.yml',
}, alice, expect=200)
objs.inventory.use_role.members.remove(alice)
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
'playbook': 'mocked.yml',
}, alice, expect=403)
@pytest.mark.django_db
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
def test_edit_nonsenstive(patch, job_template_factory, alice):
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
jt = objs.job_template
jt.playbook = 'mocked.yml'
jt.save()
jt.admin_role.members.add(alice)
res = patch(reverse('api:job_template_detail', args=(jt.id,)), {
'name': 'updated',
'description': 'bar',
'forks': 14,
'limit': 'something',
'verbosity': 5,
'extra_vars': '--',
'job_tags': 'sometags',
'force_handlers': True,
'skip_tags': True,
'ask_variables_on_launch':True,
'ask_tags_on_launch':True,
'ask_job_type_on_launch':True,
'ask_inventory_on_launch':True,
'ask_credential_on_launch': True,
}, alice, expect=200)
print(res.data)
assert res.data['name'] == 'updated'

View File

@ -38,6 +38,12 @@ from awx.main.models.organization import (
from awx.main.models.notifications import NotificationTemplate
from awx.main.tests.factories import (
create_organization,
create_job_template,
create_notification_template,
)
'''
Disable all django model signals.
'''
@ -147,18 +153,6 @@ def instance(settings):
def organization(instance):
return Organization.objects.create(name="test-org", description="test-org-desc")
@pytest.fixture
def organization_factory(instance):
def factory(name):
try:
org = Organization.objects.get(name=name)
except Organization.DoesNotExist:
org = Organization.objects.create(name=name,
description="description for " + name,
)
return org
return factory
@pytest.fixture
def credential():
return Credential.objects.create(kind='aws', name='test-cred')
@ -282,24 +276,9 @@ def permissions():
'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,},
}
@pytest.fixture
def notification_template_factory(organization):
def n(name="test-notification_template"):
try:
notification_template = NotificationTemplate.objects.get(name=name)
except NotificationTemplate.DoesNotExist:
notification_template = NotificationTemplate(name=name,
organization=organization,
notification_type="webhook",
notification_configuration=dict(url="http://localhost",
headers={"Test": "Header"}))
notification_template.save()
return notification_template
return n
@pytest.fixture
def post():
def rf(url, data, user=None, middleware=None, **kwargs):
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
view, view_args, view_kwargs = resolve(urlparse(url)[2])
if 'format' not in kwargs:
kwargs['format'] = 'json'
@ -311,12 +290,16 @@ def post():
response = view(request, *view_args, **view_kwargs)
if middleware:
middleware.process_response(request, response)
if expect:
if response.status_code != expect:
print(response.data)
assert response.status_code == expect
return response
return rf
@pytest.fixture
def get():
def rf(url, user=None, middleware=None, **kwargs):
def rf(url, user=None, middleware=None, expect=None, **kwargs):
view, view_args, view_kwargs = resolve(urlparse(url)[2])
if 'format' not in kwargs:
kwargs['format'] = 'json'
@ -328,12 +311,16 @@ def get():
response = view(request, *view_args, **view_kwargs)
if middleware:
middleware.process_response(request, response)
if expect:
if response.status_code != expect:
print(response.data)
assert response.status_code == expect
return response
return rf
@pytest.fixture
def put():
def rf(url, data, user=None, middleware=None, **kwargs):
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
view, view_args, view_kwargs = resolve(urlparse(url)[2])
if 'format' not in kwargs:
kwargs['format'] = 'json'
@ -345,12 +332,16 @@ def put():
response = view(request, *view_args, **view_kwargs)
if middleware:
middleware.process_response(request, response)
if expect:
if response.status_code != expect:
print(response.data)
assert response.status_code == expect
return response
return rf
@pytest.fixture
def patch():
def rf(url, data, user=None, middleware=None, **kwargs):
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
view, view_args, view_kwargs = resolve(urlparse(url)[2])
if 'format' not in kwargs:
kwargs['format'] = 'json'
@ -362,12 +353,16 @@ def patch():
response = view(request, *view_args, **view_kwargs)
if middleware:
middleware.process_response(request, response)
if expect:
if response.status_code != expect:
print(response.data)
assert response.status_code == expect
return response
return rf
@pytest.fixture
def delete():
def rf(url, user=None, middleware=None, **kwargs):
def rf(url, user=None, middleware=None, expect=None, **kwargs):
view, view_args, view_kwargs = resolve(urlparse(url)[2])
if 'format' not in kwargs:
kwargs['format'] = 'json'
@ -379,12 +374,16 @@ def delete():
response = view(request, *view_args, **view_kwargs)
if middleware:
middleware.process_response(request, response)
if expect:
if response.status_code != expect:
print(response.data)
assert response.status_code == expect
return response
return rf
@pytest.fixture
def head():
def rf(url, user=None, middleware=None, **kwargs):
def rf(url, user=None, middleware=None, expect=None, **kwargs):
view, view_args, view_kwargs = resolve(urlparse(url)[2])
if 'format' not in kwargs:
kwargs['format'] = 'json'
@ -396,12 +395,16 @@ def head():
response = view(request, *view_args, **view_kwargs)
if middleware:
middleware.process_response(request, response)
if expect:
if response.status_code != expect:
print(response.data)
assert response.status_code == expect
return response
return rf
@pytest.fixture
def options():
def rf(url, data, user=None, middleware=None, **kwargs):
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
view, view_args, view_kwargs = resolve(urlparse(url)[2])
if 'format' not in kwargs:
kwargs['format'] = 'json'
@ -413,6 +416,10 @@ def options():
response = view(request, *view_args, **view_kwargs)
if middleware:
middleware.process_response(request, response)
if expect:
if response.status_code != expect:
print(response.data)
assert response.status_code == expect
return response
return rf
@ -474,3 +481,16 @@ def job_template_labels(organization, job_template):
job_template.labels.create(name="label-2", organization=organization)
return job_template
@pytest.fixture
def job_template_factory():
return create_job_template
@pytest.fixture
def organization_factory():
return create_organization
@pytest.fixture
def notification_template_factory():
return create_notification_template

View File

@ -0,0 +1,85 @@
import pytest
from awx.main.tests.factories import NotUnique
def test_roles_exc_not_persisted(organization_factory):
with pytest.raises(RuntimeError) as exc:
organization_factory('test-org', roles=['test-org.admin_role:user1'], persisted=False)
assert 'persisted=False' in str(exc.value)
@pytest.mark.django_db
def test_roles_exc_bad_object(organization_factory):
with pytest.raises(KeyError):
organization_factory('test-org', roles=['test-project.admin_role:user'])
@pytest.mark.django_db
def test_roles_exc_not_unique(organization_factory):
with pytest.raises(NotUnique) as exc:
organization_factory('test-org', projects=['foo'], teams=['foo'], roles=['foo.admin_role:user'])
assert 'not a unique key' in str(exc.value)
@pytest.mark.django_db
def test_roles_exc_not_assignment(organization_factory):
with pytest.raises(RuntimeError) as exc:
organization_factory('test-org', projects=['foo'], roles=['foo.admin_role'])
assert 'provide an assignment' in str(exc.value)
@pytest.mark.django_db
def test_roles_exc_not_found(organization_factory):
with pytest.raises(RuntimeError) as exc:
organization_factory('test-org', users=['user'], projects=['foo'], roles=['foo.admin_role:user.bad_role'])
assert 'unable to find' in str(exc.value)
@pytest.mark.django_db
def test_roles_exc_not_user(organization_factory):
with pytest.raises(RuntimeError) as exc:
organization_factory('test-org', projects=['foo'], roles=['foo.admin_role:foo'])
assert 'unable to add non-user' in str(exc.value)
@pytest.mark.django_db
def test_org_factory_roles(organization_factory):
objects = organization_factory('org_roles_test',
teams=['team1', 'team2'],
users=['team1:foo', 'bar'],
projects=['baz', 'bang'],
roles=['team2.member_role:foo',
'team1.admin_role:bar',
'team1.admin_role:team2.admin_role',
'baz.admin_role:foo'])
assert objects.users.bar in objects.teams.team2.admin_role
assert objects.users.foo in objects.projects.baz.admin_role
assert objects.users.foo in objects.teams.team1.member_role
assert objects.teams.team2.admin_role in objects.teams.team1.admin_role.children.all()
@pytest.mark.django_db
def test_org_factory(organization_factory):
objects = organization_factory('organization1',
teams=['team1'],
superusers=['superuser'],
users=['admin', 'alice', 'team1:bob'],
projects=['proj1'])
assert hasattr(objects.users, 'admin')
assert hasattr(objects.users, 'alice')
assert hasattr(objects.superusers, 'superuser')
assert objects.users.bob in objects.teams.team1.member_role.members.all()
assert objects.projects.proj1.organization == objects.organization
@pytest.mark.django_db
def test_job_template_factory(job_template_factory):
jt_objects = job_template_factory('testJT', organization='org1',
project='proj1', inventory='inventory1',
credential='cred1')
assert jt_objects.job_template.name == 'testJT'
assert jt_objects.project.name == 'proj1'
assert jt_objects.inventory.name == 'inventory1'
assert jt_objects.credential.name == 'cred1'
assert jt_objects.inventory.organization.name == 'org1'

View File

@ -1,7 +1,6 @@
import mock # noqa
import pytest
from django.db import transaction
from django.core.urlresolvers import reverse
from awx.main.models import Project
@ -9,62 +8,55 @@ from awx.main.models import Project
#
# Project listing and visibility tests
#
@pytest.fixture
def team_project_list(organization_factory):
objects = organization_factory('org-test',
superusers=['admin'],
users=['team1:alice', 'team2:bob'],
teams=['team1', 'team2'],
projects=['pteam1', 'pteam2', 'pshared'],
roles=['team1.member_role:pteam1.admin_role',
'team2.member_role:pteam2.admin_role',
'team1.member_role:pshared.admin_role',
'team2.member_role:pshared.admin_role'])
return objects
@pytest.mark.django_db
def test_user_project_list(get, project_factory, organization, admin, alice, bob):
def test_user_project_list(get, organization_factory):
'List of projects a user has access to, filtered by projects you can also see'
organization.member_role.members.add(alice, bob)
objects = organization_factory('org1',
projects=['alice project', 'bob project', 'shared project'],
superusers=['admin'],
users=['alice', 'bob'],
roles=['alice project.admin_role:alice',
'bob project.admin_role:bob',
'shared project.admin_role:bob',
'shared project.admin_role:alice'])
alice_project = project_factory('alice project')
alice_project.admin_role.members.add(alice)
bob_project = project_factory('bob project')
bob_project.admin_role.members.add(bob)
shared_project = project_factory('shared project')
shared_project.admin_role.members.add(alice)
shared_project.admin_role.members.add(bob)
# admins can see all projects
assert get(reverse('api:user_projects_list', args=(admin.pk,)), admin).data['count'] == 3
assert get(reverse('api:user_projects_list', args=(objects.superusers.admin.pk,)), objects.superusers.admin).data['count'] == 3
# admins can see everyones projects
assert get(reverse('api:user_projects_list', args=(alice.pk,)), admin).data['count'] == 2
assert get(reverse('api:user_projects_list', args=(bob.pk,)), admin).data['count'] == 2
assert get(reverse('api:user_projects_list', args=(objects.users.alice.pk,)), objects.superusers.admin).data['count'] == 2
assert get(reverse('api:user_projects_list', args=(objects.users.bob.pk,)), objects.superusers.admin).data['count'] == 2
# users can see their own projects
assert get(reverse('api:user_projects_list', args=(alice.pk,)), alice).data['count'] == 2
assert get(reverse('api:user_projects_list', args=(objects.users.alice.pk,)), objects.users.alice).data['count'] == 2
# alice should only be able to see the shared project when looking at bobs projects
assert get(reverse('api:user_projects_list', args=(bob.pk,)), alice).data['count'] == 1
assert get(reverse('api:user_projects_list', args=(objects.users.bob.pk,)), objects.users.alice).data['count'] == 1
# alice should see all projects they can see when viewing an admin
assert get(reverse('api:user_projects_list', args=(admin.pk,)), alice).data['count'] == 2
assert get(reverse('api:user_projects_list', args=(objects.superusers.admin.pk,)), objects.users.alice).data['count'] == 2
def setup_test_team_project_list(project_factory, team_factory, admin, alice, bob):
team1 = team_factory('team1')
team2 = team_factory('team2')
team1_project = project_factory('team1 project')
team1_project.admin_role.parents.add(team1.member_role)
team2_project = project_factory('team2 project')
team2_project.admin_role.parents.add(team2.member_role)
shared_project = project_factory('shared project')
shared_project.admin_role.parents.add(team1.member_role)
shared_project.admin_role.parents.add(team2.member_role)
team1.member_role.members.add(alice)
team2.member_role.members.add(bob)
return team1, team2
@pytest.mark.django_db
def test_team_project_list(get, project_factory, team_factory, admin, alice, bob):
'List of projects a team has access to, filtered by projects you can also see'
team1, team2 = setup_test_team_project_list(project_factory, team_factory, admin, alice, bob)
def test_team_project_list(get, team_project_list):
objects = team_project_list
team1, team2 = objects.teams.team1, objects.teams.team2
alice, bob, admin = objects.users.alice, objects.users.bob, objects.superusers.admin
# admins can see all projects on a team
assert get(reverse('api:team_projects_list', args=(team1.pk,)), admin).data['count'] == 2
@ -78,12 +70,6 @@ def test_team_project_list(get, project_factory, team_factory, admin, alice, bob
assert get(reverse('api:team_projects_list', args=(team2.pk,)), alice).data['count'] == 1
team2.read_role.members.remove(alice)
# Test user endpoints first, very similar tests to test_user_project_list
# but permissions are being derived from team membership instead.
with transaction.atomic():
res = get(reverse('api:user_projects_list', args=(bob.pk,)), alice)
assert res.status_code == 403
# admins can see all projects
assert get(reverse('api:user_projects_list', args=(admin.pk,)), admin).data['count'] == 3
@ -98,17 +84,11 @@ def test_team_project_list(get, project_factory, team_factory, admin, alice, bob
assert get(reverse('api:user_projects_list', args=(admin.pk,)), alice).data['count'] == 2
@pytest.mark.django_db
def test_team_project_list_fail1(get, project_factory, team_factory, admin, alice, bob):
# alice should not be able to see team2 projects because she doesn't have access to team2
team1, team2 = setup_test_team_project_list(project_factory, team_factory, admin, alice, bob)
res = get(reverse('api:team_projects_list', args=(team2.pk,)), alice)
def test_team_project_list_fail1(get, team_project_list):
objects = team_project_list
res = get(reverse('api:team_projects_list', args=(objects.teams.team2.pk,)), objects.users.alice)
assert res.status_code == 403
@pytest.mark.django_db
def test_team_project_list_fail2(get, project_factory, team_factory, admin, alice, bob):
team1, team2 = setup_test_team_project_list(project_factory, team_factory, admin, alice, bob)
# alice should not be able to see bob
@pytest.mark.parametrize("u,expected_status_code", [
('rando', 403),
('org_member', 403),

View File

@ -7,8 +7,11 @@ from awx.main.access import (
)
from awx.main.migrations import _rbac as rbac
from awx.main.models import Permission
from awx.main.models.jobs import JobTemplate
from django.apps import apps
from django.core.urlresolvers import reverse
@pytest.mark.django_db
def test_job_template_migration_check(credential, deploy_jobtemplate, check_jobtemplate, user):
@ -155,3 +158,26 @@ def test_job_template_access_superuser(check_license, user, deploy_jobtemplate):
# THEN all access checks should pass
assert access.can_read(deploy_jobtemplate)
assert access.can_add({})
@pytest.mark.django_db
@pytest.mark.job_permissions
def test_job_template_creator_access(project, rando, post):
project.admin_role.members.add(rando)
with mock.patch(
'awx.main.models.projects.ProjectOptions.playbooks',
new_callable=mock.PropertyMock(return_value=['helloworld.yml'])):
response = post(reverse('api:job_template_list', args=[]), dict(
name='newly-created-jt',
job_type='run',
ask_inventory_on_launch=True,
ask_credential_on_launch=True,
project=project.pk,
playbook='helloworld.yml'
), rando)
assert response.status_code == 201
jt_pk = response.data['id']
jt_obj = JobTemplate.objects.get(pk=jt_pk)
# Creating a JT should place the creator in the admin role
assert rando in jt_obj.admin_role

View File

@ -31,20 +31,22 @@ def test_label_access_superuser(label, user):
assert access.can_delete(label)
@pytest.mark.django_db
def test_label_access_admin(label, user, organization_factory):
def test_label_access_admin(organization_factory):
'''can_change because I am an admin of that org'''
a = user('admin', False)
org_no_members = organization_factory("no_members")
org_members = organization_factory("has_members")
no_members = organization_factory("no_members")
members = organization_factory("has_members",
users=['admin'],
labels=['test'])
label.organization.admin_role.members.add(a)
org_members.admin_role.members.add(a)
label = members.labels.test
admin = members.users.admin
members.organization.admin_role.members.add(admin)
access = LabelAccess(user('admin', False))
assert not access.can_change(label, {'organization': org_no_members.id})
access = LabelAccess(admin)
assert not access.can_change(label, {'organization': no_members.organization.id})
assert access.can_read(label)
assert access.can_change(label, None)
assert access.can_change(label, {'organization': org_members.id})
assert access.can_change(label, {'organization': members.organization.id})
assert access.can_delete(label)
@pytest.mark.django_db

View File

@ -25,35 +25,44 @@ def test_notification_template_get_queryset_orgadmin(notification_template, user
assert access.get_queryset().count() == 1
@pytest.mark.django_db
def test_notification_template_access_superuser(notification_template, user, notification_template_factory):
access = NotificationTemplateAccess(user('admin', True))
assert access.can_read(notification_template)
assert access.can_change(notification_template, None)
assert access.can_delete(notification_template)
nf = notification_template_factory("test-orphaned")
def test_notification_template_access_superuser(notification_template_factory):
nf_objects = notification_template_factory('test-orphaned', organization='test', superusers=['admin'])
admin = nf_objects.superusers.admin
nf = nf_objects.notification_template
access = NotificationTemplateAccess(admin)
assert access.can_read(nf)
assert access.can_change(nf, None)
assert access.can_delete(nf)
nf.organization = None
nf.save()
assert access.can_read(nf)
assert access.can_change(nf, None)
assert access.can_delete(nf)
@pytest.mark.django_db
def test_notification_template_access_admin(notification_template, user, organization_factory, notification_template_factory):
adm = user('admin', False)
other_org = organization_factory('other')
present_org = organization_factory('present')
notification_template.organization.admin_role.members.add(adm)
present_org.admin_role.members.add(adm)
def test_notification_template_access_admin(organization_factory, notification_template_factory):
other_objects = organization_factory('other')
present_objects = organization_factory('present',
users=['admin'],
notification_templates=['test-notification'],
roles=['present.admin_role:admin'])
access = NotificationTemplateAccess(user('admin', False))
notification_template = present_objects.notification_templates.test_notification
other_org = other_objects.organization
present_org = present_objects.organization
admin = present_objects.users.admin
access = NotificationTemplateAccess(admin)
assert not access.can_change(notification_template, {'organization': other_org.id})
assert access.can_read(notification_template)
assert access.can_change(notification_template, None)
assert access.can_change(notification_template, {'organization': present_org.id})
assert access.can_delete(notification_template)
nf = notification_template_factory("test-orphaned")
nf.organization = None
nf.save()
assert not access.can_read(nf)
assert not access.can_change(nf, None)
assert not access.can_delete(nf)

View File

@ -404,164 +404,6 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
del data[k]
return self.post(url, data, expect=expect)
@mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock)
def test_ad_hoc_command_list(self, ignore):
url = reverse('api:ad_hoc_command_list')
# Retrieve the empty list of ad hoc commands.
qs = AdHocCommand.objects.none()
self.check_get_list(url, 'admin', qs)
self.check_get_list(url, 'normal', qs)
self.check_get_list(url, 'other', qs)
self.check_get_list(url, 'nobody', qs)
self.check_get_list(url, None, qs, expect=401)
# Start a new ad hoc command. Only admin and normal user (org admin)
# can run commands by default.
with self.current_user('admin'):
response = self.run_test_ad_hoc_command()
self.assertEqual(response['job_type'], 'run')
self.assertEqual(response['inventory'], self.inventory.pk)
self.assertEqual(response['credential'], self.credential.pk)
self.assertEqual(response['module_name'], 'command')
self.assertEqual(response['module_args'], 'uptime')
self.assertEqual(response['limit'], '')
self.assertEqual(response['forks'], 0)
self.assertEqual(response['verbosity'], 0)
self.assertEqual(response['become_enabled'], False)
self.put(url, {}, expect=405)
self.patch(url, {}, expect=405)
self.delete(url, expect=405)
with self.current_user('normal'):
self.run_test_ad_hoc_command()
self.put(url, {}, expect=405)
self.patch(url, {}, expect=405)
self.delete(url, expect=405)
with self.current_user('other'):
self.run_test_ad_hoc_command(expect=403)
self.put(url, {}, expect=405)
self.patch(url, {}, expect=405)
self.delete(url, expect=405)
with self.current_user('nobody'):
self.run_test_ad_hoc_command(expect=403)
self.put(url, {}, expect=405)
self.patch(url, {}, expect=405)
self.delete(url, expect=405)
with self.current_user(None):
self.run_test_ad_hoc_command(expect=401)
self.put(url, {}, expect=401)
self.patch(url, {}, expect=401)
self.delete(url, expect=401)
# Retrieve the list of ad hoc commands (only admin/normal can see by default).
qs = AdHocCommand.objects.all()
self.assertEqual(qs.count(), 2)
self.check_get_list(url, 'admin', qs)
self.check_get_list(url, 'normal', qs)
qs = AdHocCommand.objects.none()
self.check_get_list(url, 'other', qs)
self.check_get_list(url, 'nobody', qs)
self.check_get_list(url, None, qs, expect=401)
# Explicitly give other user updater permission on the inventory (still
# not allowed to run ad hoc commands).
user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,))
with self.current_user('admin'):
response = self.post(user_roles_list_url, {"id": self.inventory.update_role.id}, expect=204)
with self.current_user('other'):
self.run_test_ad_hoc_command(expect=403)
self.check_get_list(url, 'other', qs)
# Add executor role permissions to other. Fails
# when other user can't read credential.
with self.current_user('admin'):
response = self.post(user_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204)
with self.current_user('other'):
self.run_test_ad_hoc_command(expect=403)
# Succeeds once other user has a readable credential. Other user can
# only see his own ad hoc command (because of credential permissions).
other_cred = self.create_test_credential(user=self.other_django_user)
with self.current_user('other'):
self.run_test_ad_hoc_command(credential=other_cred.pk)
qs = AdHocCommand.objects.filter(created_by=self.other_django_user)
self.assertEqual(qs.count(), 1)
self.check_get_list(url, 'other', qs)
# Explicitly give nobody user read permission on the inventory.
nobody_roles_list_url = reverse('api:user_roles_list', args=(self.nobody_django_user.pk,))
with self.current_user('admin'):
response = self.post(nobody_roles_list_url, {"id": self.inventory.read_role.id}, expect=204)
with self.current_user('nobody'):
self.run_test_ad_hoc_command(credential=other_cred.pk, expect=403)
self.check_get_list(url, 'other', qs)
# Create a cred for the nobody user, run an ad hoc command as the admin
# user with that cred. Nobody user can still not see the ad hoc command
# without the run_ad_hoc_commands permission flag.
nobody_cred = self.create_test_credential(user=self.nobody_django_user)
with self.current_user('admin'):
self.run_test_ad_hoc_command(credential=nobody_cred.pk)
qs = AdHocCommand.objects.none()
self.check_get_list(url, 'nobody', qs)
# Give the nobody user the run_ad_hoc_commands flag, and can now see
# the one ad hoc command previously run.
with self.current_user('admin'):
response = self.post(nobody_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204)
qs = AdHocCommand.objects.filter(credential_id=nobody_cred.pk)
self.assertEqual(qs.count(), 1)
self.check_get_list(url, 'nobody', qs)
# Post without inventory (should fail).
with self.current_user('admin'):
self.run_test_ad_hoc_command(inventory=None, expect=400)
# Post without credential (should fail).
with self.current_user('admin'):
self.run_test_ad_hoc_command(credential=None, expect=400)
# Post with empty or unsupported module name (empty defaults to command).
with self.current_user('admin'):
response = self.run_test_ad_hoc_command(module_name=None)
self.assertEqual(response['module_name'], 'command')
with self.current_user('admin'):
response = self.run_test_ad_hoc_command(module_name='')
self.assertEqual(response['module_name'], 'command')
with self.current_user('admin'):
self.run_test_ad_hoc_command(module_name='transcombobulator', expect=400)
# Post with empty module args for shell/command modules (should fail),
# empty args for other modules ok.
with self.current_user('admin'):
self.run_test_ad_hoc_command(module_args=None, expect=400)
with self.current_user('admin'):
self.run_test_ad_hoc_command(module_name='shell', module_args=None, expect=400)
with self.current_user('admin'):
self.run_test_ad_hoc_command(module_name='shell', module_args='', expect=400)
with self.current_user('admin'):
self.run_test_ad_hoc_command(module_name='ping', module_args=None)
# Post with invalid values for other parameters.
with self.current_user('admin'):
self.run_test_ad_hoc_command(job_type='something', expect=400)
with self.current_user('admin'):
response = self.run_test_ad_hoc_command(job_type='check')
self.assertEqual(response['job_type'], 'check')
with self.current_user('admin'):
self.run_test_ad_hoc_command(verbosity=-1, expect=400)
with self.current_user('admin'):
self.run_test_ad_hoc_command(forks=-1, expect=400)
with self.current_user('admin'):
response = self.run_test_ad_hoc_command(become_enabled=True)
self.assertEqual(response['become_enabled'], True)
# Try to run with expired license.
self.create_expired_license_file()
with self.current_user('admin'):
self.run_test_ad_hoc_command(expect=403)
with self.current_user('normal'):
self.run_test_ad_hoc_command(expect=403)
@mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock)
def test_ad_hoc_command_detail(self, ignore):
@ -953,98 +795,6 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
self.patch(url, {}, expect=401)
self.delete(url, expect=401)
@mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock)
def test_inventory_ad_hoc_commands_list(self, ignore):
with self.current_user('admin'):
response = self.run_test_ad_hoc_command()
response = self.run_test_ad_hoc_command(inventory=self.inventory2.pk)
# Test the ad hoc commands list for an inventory. Should only return
# the ad hoc command(s) run against that inventory. Posting should
# start a new ad hoc command and always set the inventory from the URL.
url = reverse('api:inventory_ad_hoc_commands_list', args=(self.inventory.pk,))
inventory_url = reverse('api:inventory_detail', args=(self.inventory.pk,))
with self.current_user('admin'):
response = self.get(url, expect=200)
self.assertEqual(response['count'], 1)
response = self.run_test_ad_hoc_command(url=url, inventory=None, expect=201)
self.assertEqual(response['inventory'], self.inventory.pk)
response = self.run_test_ad_hoc_command(url=url, inventory=self.inventory2.pk, expect=201)
self.assertEqual(response['inventory'], self.inventory.pk)
self.put(url, {}, expect=405)
self.patch(url, {}, expect=405)
self.delete(url, expect=405)
response = self.get(inventory_url, expect=200)
self.assertTrue(response['can_run_ad_hoc_commands'])
with self.current_user('normal'):
response = self.get(url, expect=200)
self.assertEqual(response['count'], 3)
response = self.run_test_ad_hoc_command(url=url, inventory=None, expect=201)
self.assertEqual(response['inventory'], self.inventory.pk)
self.put(url, {}, expect=405)
self.patch(url, {}, expect=405)
self.delete(url, expect=405)
response = self.get(inventory_url, expect=200)
self.assertTrue(response['can_run_ad_hoc_commands'])
with self.current_user('other'):
self.get(url, expect=403)
self.post(url, {}, expect=403)
self.put(url, {}, expect=405)
self.patch(url, {}, expect=405)
self.delete(url, expect=405)
with self.current_user('nobody'):
self.get(url, expect=403)
self.post(url, {}, expect=403)
self.put(url, {}, expect=405)
self.patch(url, {}, expect=405)
self.delete(url, expect=405)
with self.current_user(None):
self.get(url, expect=401)
self.post(url, {}, expect=401)
self.put(url, {}, expect=401)
self.patch(url, {}, expect=401)
self.delete(url, expect=401)
# Create another unrelated inventory permission with run_ad_hoc_commands
# set; this tests an edge case in the RBAC query where we'll return
# can_run_ad_hoc_commands = True when we shouldn't.
nobody_roles_list_url = reverse('api:user_roles_list', args=(self.nobody_django_user.pk,))
with self.current_user('admin'):
response = self.post(nobody_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204)
# Create a credential for the other user and explicitly give other
# user admin permission on the inventory (still not allowed to run ad
# hoc commands; can get the list but can't see any items).
other_cred = self.create_test_credential(user=self.other_django_user)
user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,))
with self.current_user('admin'):
response = self.post(user_roles_list_url, {"id": self.inventory.update_role.id}, expect=204)
with self.current_user('other'):
response = self.get(url, expect=200)
self.assertEqual(response['count'], 0)
response = self.get(inventory_url, expect=200)
self.assertFalse(response['can_run_ad_hoc_commands'])
self.run_test_ad_hoc_command(url=url, inventory=None, credential=other_cred.pk, expect=403)
# Update permission to allow other user to run ad hoc commands. Can
# only see his own ad hoc commands (because of credential permission).
with self.current_user('admin'):
response = self.post(user_roles_list_url, {"id": self.inventory.adhoc_role.id}, expect=204)
with self.current_user('other'):
response = self.get(url, expect=200)
self.assertEqual(response['count'], 0)
self.run_test_ad_hoc_command(url=url, inventory=None, credential=other_cred.pk, expect=201)
response = self.get(url, expect=200)
self.assertEqual(response['count'], 1)
response = self.get(inventory_url, expect=200)
self.assertTrue(response['can_run_ad_hoc_commands'])
# Try to run with expired license.
self.create_expired_license_file()
with self.current_user('admin'):
self.run_test_ad_hoc_command(url=url, expect=403)
with self.current_user('normal'):
self.run_test_ad_hoc_command(url=url, expect=403)
def test_host_ad_hoc_commands_list(self):
# TODO: Figure out why this test needs pexpect

View File

@ -1770,6 +1770,7 @@ class InventoryUpdatesTest(BaseTransactionTest):
self.assertFalse(inventory_update.name.endswith(inventory_update.inventory_source.name), inventory_update.name)
def test_update_from_rax(self):
self.skipTest('Skipping until we can resolve the CERTIFICATE_VERIFY_FAILED issue: #1706')
source_username = getattr(settings, 'TEST_RACKSPACE_USERNAME', '')
source_password = getattr(settings, 'TEST_RACKSPACE_API_KEY', '')
source_regions = getattr(settings, 'TEST_RACKSPACE_REGIONS', '')

View File

@ -961,7 +961,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.assertEqual(jobs_qs.count(), 7)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name]))
self.assertEqual(job.limit, host.name)
self.assertEqual(job.hosts.count(), 1)
self.assertEqual(job.hosts.all()[0], host)

View File

@ -37,13 +37,13 @@ body .navbar {
border-color: #E8E8E8;
}
body .navbar .navbar-brand {
color: #848992;
color: #707070;
padding: 0;
font-size: 14px;
}
body .navbar .navbar-brand:focus,
body .navbar .navbar-brand:hover {
color: #848992;
color: #707070;
}
body .navbar .navbar-brand img {
display: inline-block;
@ -60,7 +60,7 @@ body .navbar .navbar-brand > span {
body .navbar .navbar-title {
float: left;
height: 50px;
color: #848992;
color: #707070;
padding: 0;
font-size: 14px;
display: none;
@ -74,19 +74,19 @@ body.show-title .navbar .navbar-title {
display: inline-block;
}
body .navbar .navbar-nav > li > a {
color: #848992;
color: #707070;
display: flex;
justify-content: center;
}
body .navbar .navbar-nav > li > a:focus,
body .navbar .navbar-nav > li > a:hover {
color: #848992;
color: #707070;
}
body .navbar .navbar-nav > li > a > span.glyphicon {
font-size: 20px;
padding-right: 5px;
padding-left: 5px;
color: #B7B7B7;
color: #848992;
}
body .page-header {
@ -110,7 +110,7 @@ body .description .hide-description span.glyphicon {
font-size: 20px;
}
body .description .hide-description:hover span.glyphicon {
color: #B7B7B7;
color: #848992;
}
body ul.breadcrumb,
body .description,
@ -167,7 +167,7 @@ body .form-actions button {
body .form-horizontal .control-label {
text-transform: uppercase;
font-weight: normal;
color: #848992;
color: #707070;
}
body textarea.form-control {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
@ -182,22 +182,22 @@ body .description pre {
}
body .tooltip.bottom .tooltip-arrow {
border-bottom-color: #848992;
border-bottom-color: #707070;
}
body .tooltip.top .tooltip-arrow {
border-top-color: #848992;
border-top-color: #707070;
}
body .tooltip.left .tooltip-arrow {
border-left-color: #848992;
border-left-color: #707070;
}
body .tooltip.right .tooltip-arrow {
border-right-color: #848992;
border-right-color: #707070;
}
body .tooltip.in {
opacity: 1;
}
body .tooltip-inner {
background-color: #848992;
background-color: #707070;
}
body .btn {
@ -205,7 +205,7 @@ body .btn {
}
.btn-primary {
background-color: #FFFFFF;
color: #848992;
color: #707070;
border: 1px solid #E8E8E8;
}
.btn-primary:hover,
@ -224,7 +224,7 @@ body .btn {
.open>.dropdown-toggle.btn-primary:hover,
.open>.dropdown-toggle.btn-primary {
background-color: #FAFAFA;
color: #848992;
color: #707070;
border: 1px solid #E8E8E8;
}
@ -283,7 +283,7 @@ body #footer {
overflow: hidden;
margin-bottom: 0;
height: 40px;
color: #848992;
color: #707070;
}
body #footer .footer-logo {
text-align: left;
@ -302,7 +302,7 @@ body #footer .footer-copyright {
padding-top: 10px;
}
body #footer .footer-copyright a {
color: #848992;
color: #707070;
}
@media screen and (min-width: 768px) {
@ -329,7 +329,7 @@ body #footer .footer-copyright a {
border-color: #E8E8E8;
}
body .navbar .navbar-toggle .icon-bar {
background-color: #B7B7B7;
background-color: #848992;
}
body .navbar .tooltip {
visibility: hidden;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -962,6 +962,10 @@ input[type="checkbox"].checkbox-no-label {
.checkbox-inline, .radio-inline {
margin-right: 10px;
}
.checkbox-inline.stack-inline {
display: block;
}
}
.checkbox-options {

View File

@ -178,7 +178,7 @@
.Form-formGroup--checkbox{
display: flex;
align-items: flex-end;
align-items: flex-start;
}
.Form-subForm {
@ -324,6 +324,12 @@
.select2-dropdown{
border:1px solid @field-border;
}
.select2-container--open .select2-dropdown--below {
margin-top: -1px;
border-top: 1px solid @field-border;
}
.Form-dropDown:focus{

View File

@ -214,7 +214,7 @@
}
#job-detail-container {
.well {
overflow: hidden;
}
@ -276,6 +276,8 @@
overflow-x: hidden;
overflow-y: auto;
background-color: @white;
min-height: 40px;
.row {
border-top: 1px solid @grey;
}
@ -318,7 +320,7 @@
#play-section {
.table-detail {
height: 150px;
min-height: 40px;
}
}
@ -421,7 +423,6 @@
table-layout: fixed;
}
#hosts-table-detail {
height: 150px;
background-color: @white;
}
#hosts-table-detail table {

View File

@ -0,0 +1,32 @@
{
"name": "lrInfiniteScroll",
"main": "lrInfiniteScroll.js",
"version": "1.0.0",
"homepage": "https://github.com/lorenzofox3/lrInfiniteScroll",
"authors": [
"lorenzofox3 <laurent34azerty@gmail.com>"
],
"description": "angular directive to handle element scroll",
"keywords": [
"angular",
"scroll",
"inifinite"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"_release": "1.0.0",
"_resolution": {
"type": "version",
"tag": "1.0.0",
"commit": "c833e9d8ff56d6c66e2a21ed7f27ad840f159a8b"
},
"_source": "https://github.com/lorenzofox3/lrInfiniteScroll.git",
"_target": "~1.0.0",
"_originalSource": "lrInfiniteScroll"
}

View File

@ -0,0 +1,2 @@
require('./lrInfiniteScroll');
module.exports = 'lrInfiniteScroll';

View File

@ -2,13 +2,12 @@
'use strict';
var module = ng.module('lrInfiniteScroll', []);
module.directive('lrInfiniteScroll', ['$log', '$timeout', function ($log, timeout) {
module.directive('lrInfiniteScroll', ['$timeout', function (timeout) {
return{
link: function (scope, element, attr) {
var
lengthThreshold = attr.scrollThreshold || 50,
timeThreshold = attr.timeThreshold || 400,
direction = attr.direction || 'down',
handler = scope.$eval(attr.lrInfiniteScroll),
promise = null,
lastRemaining = 9999;
@ -20,14 +19,14 @@
handler = ng.noop;
}
$log.debug('lrInfiniteScroll: ' + attr.lrInfiniteScroll);
element.bind('scroll', function () {
var remaining = (direction === 'down') ? element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop) : element[0].scrollTop;
// if we have reached the threshold and we scroll down
if ((direction === 'down' && remaining < lengthThreshold && (remaining - lastRemaining) < 0) ||
direction === 'up' && remaining < lengthThreshold) {
//if there is already a timer running which has not expired yet we have to cancel it and restart the timer
var
remaining = element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop);
//if we have reached the threshold and we scroll down
if (remaining < lengthThreshold && (remaining - lastRemaining) < 0) {
//if there is already a timer running which has no expired yet we have to cancel it and restart the timer
if (promise !== null) {
timeout.cancel(promise);
}

View File

@ -15,13 +15,13 @@
multiSelectExtended: true,
index: false,
hover: true,
emptyListText : 'No Teams exist',
fields: {
name: {
key: true,
label: 'name'
},
},
}
};
}

View File

@ -16,6 +16,7 @@
multiSelectExtended: true,
index: false,
hover: true,
emptyListText : 'No Users exist',
fields: {
first_name: {

View File

@ -23,7 +23,7 @@ export default
return i.role;
}))
.filter((role) => {
return !!attrs.teamRoleList == !!role.team_id;
return Boolean(attrs.teamRoleList) === Boolean(role.team_id);
})
.sort((a, b) => {
if (a.name

View File

@ -79,6 +79,7 @@ __deferLoadIfEnabled();
var tower = angular.module('Tower', [
//'ngAnimate',
'lrInfiniteScroll',
'ngSanitize',
'ngCookies',
about.name,
@ -269,7 +270,7 @@ var tower = angular.module('Tower', [
}).
state('projects', {
url: '/projects',
url: '/projects?{status}',
templateUrl: urlPrefix + 'partials/projects.html',
controller: ProjectsList,
data: {
@ -297,6 +298,10 @@ var tower = angular.module('Tower', [
controller: ProjectsEdit,
data: {
activityStreamId: 'id'
},
ncyBreadcrumb: {
parent: 'projects',
label: 'EDIT PROJECT'
}
}).
state('projectOrganizations', {
@ -340,6 +345,10 @@ var tower = angular.module('Tower', [
controller: TeamsEdit,
data: {
activityStreamId: 'team_id'
},
ncyBreadcrumb: {
parent: "teams",
label: "EDIT TEAM"
}
}).

View File

@ -74,7 +74,7 @@ export default
var licenseInfo = FeaturesService.getLicenseInfo();
scope.licenseType = licenseInfo ? licenseInfo.license_type : null;
if (!licenseInfo) {
console.warn("License info not loaded correctly");
console.warn("License info not loaded correctly"); // jshint ignore:line
}
})
.catch(function (response) {

View File

@ -22,7 +22,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
Wait('start');
var list = ProjectList,
defaultUrl = GetBasePath('projects'),
defaultUrl = GetBasePath('projects') + ($stateParams.status ? '?status=' + $stateParams.status : ''),
view = GenerateList,
base = $location.path().replace(/^\//, '').split('/')[0],
mode = (base === 'projects') ? 'edit' : 'select',

View File

@ -162,7 +162,7 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
generator.reset();
$scope.user_type_options = user_type_options;
$scope.user_type = user_type_options[0]
$scope.user_type = user_type_options[0];
$scope.$watch('user_type', user_type_sync($scope));
CreateSelect2({
@ -271,7 +271,7 @@ export function UsersEdit($scope, $rootScope, $location,
generator.reset();
$scope.user_type_options = user_type_options;
$scope.user_type = user_type_options[0]
$scope.user_type = user_type_options[0];
$scope.$watch('user_type', user_type_sync($scope));
var setScopeFields = function(data){

View File

@ -49,7 +49,7 @@ export default
label: "Inventories",
},
{
url: "/#/inventories/?inventory_sources_with_failures",
url: "/#/inventories?status=sync-failed",
number: scope.data.inventories.inventory_failed,
label: "Inventory Sync Failures",
isFailureCount: true
@ -60,7 +60,7 @@ export default
label: "Projects"
},
{
url: "/#/projects/?status=failed",
url: "/#/projects?status=failed",
number: scope.data.projects.failed,
label: "Project Sync Failures",
isFailureCount: true

View File

@ -118,6 +118,7 @@
top: auto;
box-shadow: none;
text-transform: uppercase;
cursor: pointer;
}
.DashboardGraphs-periodDropdown,

View File

@ -246,7 +246,7 @@ export default
rows: 10,
awPopOver: "SSH key description",
awPopOverWatch: "key_description",
dataTitle: 'Help',
dataTitle: 'Private Key',
dataPlacement: 'right',
dataContainer: "body",
subForm: "credentialSubForm"

View File

@ -204,7 +204,7 @@ export default
},
job_tags: {
label: 'Job Tags',
type: 'textarea',
type: 'text',
rows: 1,
addRequired: false,
editRequired: false,

View File

@ -174,7 +174,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
dataTitle: 'SCM Clean',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options'
labelClass: 'checkbox-options stack-inline'
}, {
name: 'scm_delete_on_update',
label: 'Delete on Update',
@ -186,7 +186,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
dataTitle: 'SCM Delete',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options'
labelClass: 'checkbox-options stack-inline'
}, {
name: 'scm_update_on_launch',
label: 'Update on Launch',
@ -197,7 +197,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
dataTitle: 'SCM Update',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options'
labelClass: 'checkbox-options stack-inline'
}]
},
scm_update_cache_timeout: {
@ -273,7 +273,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
}
},
notifications: {
include: "NotificationsList"
include: "NotificationsList",
}
},

View File

@ -132,9 +132,10 @@ export default
"delete": {
label: 'Remove',
ngClick: 'deletePermissionFromTeam(team_id, team_obj.name, role.name, role.summary_fields.resource_name, role.related.teams)',
class: "List-actionButton--delete",
'class': "List-actionButton--delete",
iconClass: 'fa fa-times',
awToolTip: 'Dissasociate permission from team'
awToolTip: 'Dissasociate permission from team',
dataPlacement: 'top'
}
},
hideOnSuperuser: true

View File

@ -138,6 +138,7 @@ export default
iterator: 'team',
open: false,
index: false,
suppressEmptyText: true,
actions: {},

View File

@ -20,7 +20,7 @@ export default
angular.module('SchedulesHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', 'SearchHelper', 'PaginationHelpers', listGenerator.name, 'ModalDialog',
'GeneratorHelpers'])
.factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest',
.factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest',
'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state',
function(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors,
GetBasePath, SchedulePost, $state) {
@ -176,7 +176,7 @@ export default
};
}])
.factory('AddSchedule', ['$location', '$rootScope', '$stateParams',
.factory('AddSchedule', ['$location', '$rootScope', '$stateParams',
'SchedulerInit', 'Wait', 'GetBasePath', 'Empty', 'SchedulePost', '$state', 'Rest', 'ProcessErrors',
function($location, $rootScope, $stateParams, SchedulerInit,
Wait, GetBasePath, Empty, SchedulePost, $state, Rest,
@ -295,8 +295,7 @@ export default
}])
.factory('SchedulePost', ['Rest', 'ProcessErrors', 'RRuleToAPI', 'Wait',
'ToJSON',
function(Rest, ProcessErrors, RRuleToAPI, Wait, ToJSON) {
function(Rest, ProcessErrors, RRuleToAPI, Wait) {
return function(params) {
var scope = params.scope,
url = params.url,
@ -326,8 +325,8 @@ export default
schedule.extra_data = JSON.stringify(extra_vars);
}
else if(scope.extraVars){
schedule.extra_data = scope.parseType === 'yaml' ?
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
schedule.extra_data = scope.parseType === 'yaml' ?
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
}
Rest.setUrl(url);
if (mode === 'add') {

View File

@ -17,7 +17,7 @@ function InventoriesList($scope, $rootScope, $location, $log,
Find, Empty, $state) {
var list = InventoryList,
defaultUrl = GetBasePath('inventory'),
defaultUrl = GetBasePath('inventory') + ($stateParams.status === 'sync-failed' ? '?not__inventory_sources_with_failures=0' : ''),
view = generateList,
paths = $location.path().replace(/^\//, '').split('/'),
mode = (paths[0] === 'inventories') ? 'edit' : 'select';

View File

@ -9,7 +9,7 @@ import InventoriesList from './inventory-list.controller';
export default {
name: 'inventories',
route: '/inventories',
route: '/inventories?{status}',
templateUrl: templateUrl('inventories/inventories'),
controller: InventoriesList,
data: {

View File

@ -80,7 +80,7 @@
group_id: id
});
};
$scope.showFailedHosts = function(x, y, z){
$scope.showFailedHosts = function() {
$state.go('inventoryManage', {failed: true}, {reload: true});
};
$scope.scheduleGroup = function(id) {
@ -91,7 +91,7 @@
$scope.$parent.groupsSelected = selection.length > 0 ? true : false;
$scope.$parent.groupsSelectedItems = selection.selectedItems;
});
$scope.$on('PostRefresh', () =>{
$scope.$on('PostRefresh', () => {
$scope.groups.forEach( (group, index) => {
var group_status, hosts_status;
group_status = GetSyncStatusMsg({

View File

@ -8,7 +8,7 @@ import {ManageHostsAdd, ManageHostsEdit} from './hosts.route';
export default
angular.module('manageHosts', [])
.run(['$stateExtender', '$state', function($stateExtender, $state){
.run(['$stateExtender', function($stateExtender){
$stateExtender.addState(ManageHostsAdd);
$stateExtender.addState(ManageHostsEdit);
}]);

View File

@ -61,7 +61,7 @@ export default
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
}
}
$scope.canEdit = data['script'] !== null;
$scope.canEdit = data.script !== null;
if (!$scope.canEdit) {
$scope.script = "Script contents hidden";
}

View File

@ -33,8 +33,8 @@
</table>
</div>
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500" ng-hide="hosts.length == 0">
<table class="table">
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500">
<table class="table" ng-class="{'JobDetails-table--noResults': hosts.length === 0}">
<tbody>
<tr class="List-tableRow" ng-repeat="host in hosts track by $index" id="{{ host.id }}" ng-class-even="'List-tableRow--evenRow'" ng-class-odd="'List-tableRow--oddRow'">
<td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">

View File

@ -13,6 +13,12 @@
.JobDetail-instructions{
color: @default-interface-txt;
margin: 10px 0 10px 0;
.badge {
background-color: @default-list-header-bg;
color: @default-interface-txt;
padding: 5px 7px;
}
}
.JobDetail{
.OnePlusOne-container(100%, @breakpoint-md);
@ -151,6 +157,10 @@
background-color: @default-link;
border: 1px solid @default-link;
color: @default-bg;
&:hover {
background-color: @default-link-hov;
}
}
.JobDetail .nvd3.nv-noData{
color: @default-interface-txt;
@ -198,6 +208,12 @@
margin-left: -5px;
}
.JobDetails-table--noResults {
tr > td {
border-top: none !important;
}
}
.JobDetail-statusIcon--results{
padding-left: 0px;
padding-right: 10px;
@ -215,3 +231,7 @@
width:0px;
padding-right: 0px;
}
.JobDetail-leftSide.JobDetail-stdoutActionButton--active {
margin-right: 0px;
}

View File

@ -667,21 +667,9 @@ export default
scope.lessStatus = false; // close the view more status option
// Detail table height adjusting. First, put page height back to 'normal'.
$('#plays-table-detail').height(80);
//$('#plays-table-detail').mCustomScrollbar("update");
// $('#tasks-table-detail').height(120);
//$('#tasks-table-detail').mCustomScrollbar("update");
$('#hosts-table-detail').height(150);
//$('#hosts-table-detail').mCustomScrollbar("update");
height = $(window).height() - $('#main-menu-container .navbar').outerHeight() -
$('#job-detail-container').outerHeight() - 20;
if (height > 15) {
// there's a bunch of white space at the bottom, let's use it
$('#plays-table-detail').height(80 + (height * 0.10));
$('#tasks-table-detail').height(120 + (height * 0.20));
$('#hosts-table-detail').height(150 + (height * 0.10));
}
scope.$emit('RefreshCompleted');
};

View File

@ -189,7 +189,7 @@
</div>
<div id="plays-table-header" class="table-header">
<div id="plays-table-header" class="table-header" ng-show="plays.length !== 0">
<table class="table table-condensed">
<thead>
<tr>
@ -202,7 +202,7 @@
</div>
<div id="plays-table-detail" class="table-detail" lr-infinite-scroll="playsScrollDown"
scroll-threshold="10" time-threshold="500">
<table class="table">
<table class="table" ng-class="{'JobDetails-table--noResults': plays.length === 0}">
<tbody>
<tr class="List-tableRow cursor-pointer" ng-repeat="play in plays" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-class="play.playActiveClass" ng-click="selectPlay(play.id, $event)">
<td class="List-tableCell col-lg-7 col-md-6 col-sm-6 col-xs-4 status-column" aw-tool-tip="{{ play.status_tip }}" data-tip-watch="play.status_tip" data-placement="top"><i class="JobDetail-statusIcon fa icon-job-{{ play.status }}"></i>{{ play.name }}</td>
@ -248,7 +248,7 @@
</div>
<div class="table-header">
<table id="tasks-table-header" class="table table-condensed">
<table id="tasks-table-header" class="table table-condensed" ng-show="taskList.length !== 0">
<thead>
<tr>
<th class="List-tableHeader col-lg-3 col-md-3 col-sm-6 col-xs-4">Tasks</th>
@ -261,7 +261,7 @@
</div>
<div id="tasks-table-detail" class="table-detail" lr-infinite-scroll="tasksScrollDown"
scroll-threshold="10" time-threshold="500">
<table class="table">
<table class="table" ng-class="{'JobDetails-table--noResults': taskList.length === 0}">
<tbody>
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="task in taskList = (tasks) track by $index" ng-class="task.taskActiveClass" ng-click="selectTask(task.id)">
<td class="List-tableCell col-lg-3 col-md-3 col-sm-6 col-xs-4 status-column" aw-tool-tip="{{ task.status_tip }}"
@ -331,7 +331,7 @@
</div>
</div>
<div class="table-header" id="hosts-table-header">
<table class="table table-condensed">
<table class="table table-condensed" ng-show="results.length !== 0">
<thead>
<tr>
<th class="List-tableHeader col-lg-4 col-md-3 col-sm-3 col-xs-3">Hosts</th>
@ -343,7 +343,7 @@
</div>
<div id="hosts-table-detail" class="table-detail" lr-infinite-scroll="hostResultsScrollDown" scroll-threshold="10" time-threshold="500">
<table class="table">
<table class="table" ng-class="{'JobDetails-table--noResults': results.length === 0}">
<tbody>
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index">
<td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column">
@ -404,7 +404,7 @@
<button class="StandardOut-actionButton" aw-tool-tip="Toggle Output" data-placement="top" ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}" ng-click="toggleStdoutFullscreen()">
<i class="fa fa-arrows-alt"></i>
</button>
<a ng-show="job_status.status === ('failed' || 'successful')" href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
<a ng-show="job_status.status === 'failed' || job_status.status === 'successful' || job_status.status === 'canceled'" href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
<button class="StandardOut-actionButton" aw-tool-tip="Download Output" data-placement="top">
<i class="fa fa-download"></i>
</button>

View File

@ -72,6 +72,7 @@
jQuery.extend(true, CloudCredentialList, CredentialList);
CloudCredentialList.name = 'cloudcredentials';
CloudCredentialList.iterator = 'cloudcredential';
CloudCredentialList.basePath = '/api/v1/credentials?cloud=true';
SurveyControllerInit({
scope: $scope,

View File

@ -108,7 +108,7 @@ export default
}
})
.error(function (ret,status_code) {
if (status_code == 403) {
if (status_code === 403) {
/* user doesn't have access to see the project, no big deal. */
} else {
Alert('Missing Playbooks', 'Unable to retrieve the list of playbooks for this project. Choose a different ' +
@ -198,7 +198,7 @@ export default
}
})
.error(function (data, status) {
if (status == 403) {
if (status === 403) {
/* User doesn't have read access to the project, no problem. */
} else {
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to get project ' + $scope.project +
@ -288,6 +288,7 @@ export default
jQuery.extend(true, CloudCredentialList, CredentialList);
CloudCredentialList.name = 'cloudcredentials';
CloudCredentialList.iterator = 'cloudcredential';
CloudCredentialList.basePath = '/api/v1/credentials?cloud=true';
LookUpInit({
url: GetBasePath('credentials') + '?cloud=true',
scope: $scope,

View File

@ -20,6 +20,7 @@ export default
index: false,
hover: true,
well: false,
emptyListText: 'No completed jobs',
fields: {
status: {

View File

@ -15,6 +15,7 @@ export default
index: true,
hover: true,
well: false,
emptyListText: 'No schedules exist',
fields: {
status: {

View File

@ -157,11 +157,11 @@
ng-href="/#/portal"
ng-if="!licenseMissing"
ng-class="{'is-currentRoute' : isCurrentState('portalMode'), 'is-loggedOut' : !$root.current_user.username}"
aw-tool-tip="Portal Mode"
aw-tool-tip="My View"
data-placement="bottom"
data-trigger="hover"
data-container="body">
<i class="MainMenu-itemImage MainMenu-itemImage--settings fa fa-columns"
<i class="MainMenu-itemImage MainMenu-itemImage--settings fa fa-tasks"
alt="Portal Mode">
</i>
</a>

View File

@ -1,3 +1,4 @@
<div ui-view></div>
<div class="tab-pane Panel" id="management_jobs">
<div class="List-title">
<div class="List-titleText">
@ -7,7 +8,6 @@
{{ mgmtCards.length }}
</span>
</div>
<div ui-view></div>
<div class="MgmtCards">
<div class="MgmtCards-card"
ng-repeat="card in mgmtCards track by card.id">

View File

@ -3,7 +3,8 @@
.MgmtCards {
display: flex;
flex-wrap: wrap;
flex-flow: row wrap;
justify-content: space-between;
}
.MgmtCards-card {
@ -11,15 +12,14 @@
padding: 20px;
border-radius: 5px;
border: 1px solid @default-border;
display: flex;
flex-wrap: wrap;
align-items: baseline;
margin-top: 20px;
width: 32%;
}
.MgmtCards-card--selected {
padding-left: 16px;
border-left: 5px solid #337AB7;
border-left: 5px solid @default-link;
}
.MgmtCards-card--promptElements{
@ -44,7 +44,6 @@
font-weight: bold;
color: @default-interface-txt;
margin-bottom: 25px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@ -86,46 +85,19 @@
margin-right: 10px;
}
@media (min-width: 1179px) {
.MgmtCards-card {
width: ~"calc(25% - 15px)";
margin-right: 20px;
}
.MgmtCards-card:nth-child(4n+4) {
margin-right: 0px;
}
}
@media (min-width: 901px) and (max-width: 1178px) {
.MgmtCards-card {
width: ~"calc(33% - 11px)";
margin-right: 20px;
}
.MgmtCards-card:nth-child(3n+3) {
margin-right: 0px;
}
}
@media (min-width: 616px) and (max-width: 900px) {
.MgmtCards-card {
width: ~"calc(50% - 10px)";
margin-right: 20px;
}
.MgmtCards-card:nth-child(2n+2) {
margin-right: 0px;
}
}
@media (max-width: 615px) {
@media (max-width: 840px) {
.MgmtCards-card {
width: 100%;
margin-right: 0px;
}
}
@media (min-width: 840px) and (max-width: 1240px) {
.MgmtCards-card {
width: ~"calc(50% - 10px)";
}
}
#prompt-for-days-facts, #prompt-for-days {
overflow-x: hidden;
font-family: "Open Sans";

View File

@ -141,7 +141,23 @@ export default function() {
reqExpression: "channel_required",
init: "false"
},
ngShow: "notification_type.value == 'slack' || notification_type.value == 'hipchat'",
ngShow: "notification_type.value == 'slack'",
subForm: 'typeSubForm'
},
rooms: {
label: 'Destination Channels',
type: 'textarea',
rows: 3,
awPopOver: '<p>Type an option on each line. The pound symbol (#) is not required.</p>'+
'<p>For example:<br>engineering<br>\n #support<br>\n',
dataTitle: 'Destination Channels',
dataPlacement: 'right',
dataContainer: "body",
awRequiredWhen: {
reqExpression: "room_required",
init: "false"
},
ngShow: "notification_type.value == 'hipchat'",
subForm: 'typeSubForm'
},
token: {
@ -243,8 +259,9 @@ export default function() {
subForm: 'typeSubForm'
},
api_url: {
label: 'API URL (e.g: https://mycompany.hiptchat.com)',
label: 'API URL',
type: 'text',
placeholder: 'https://mycompany.hipchat.com',
awRequiredWhen: {
reqExpression: "hipchat_required",
init: "false"
@ -264,11 +281,7 @@ export default function() {
},
notify: {
label: 'Notify Channel',
type: 'text',
awRequiredWhen: {
reqExpression: "hipchat_required",
init: "false"
},
type: 'checkbox',
ngShow: "notification_type.value == 'hipchat' ",
subForm: 'typeSubForm'
},

View File

@ -13,6 +13,7 @@ export default function(){
iterator: 'notification_template',
index: false,
hover: false,
emptyListText: 'No notifications exist',
fields: {
status: {

View File

@ -14,6 +14,7 @@ export default function(){
iterator: 'notification',
index: false,
hover: false,
emptyListText: 'No Notifications exist',
basePath: 'notifications',
fields: {
name: {

View File

@ -39,7 +39,7 @@ function () {
case 'hipchat':
obj.tokenLabel = ' Token';
obj.hipchat_required = true;
obj.channel_required = true;
obj.room_required = true;
obj.token_required = true;
break;
case 'twilio':

View File

@ -20,7 +20,7 @@
<div id="scheduled_jobs_link" class="Form-tab"
ng-class="{'is-selected': schedulesSelected }"
ng-click="toggleTab('scheduled')">
Schedule
Schedules
</div>
</div>
<div id="jobs-tab-content" class="Form-tabSection"

View File

@ -8,7 +8,7 @@ export default {
name: 'portalMode',
url: '/portal',
ncyBreadcrumb: {
label: "PORTAL MODE"
label: "MY VIEW"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
@ -30,4 +30,4 @@ export default {
controller: PortalModeJobsController
}
}
};
};

View File

@ -37,19 +37,28 @@ export default
name: 'projectSchedules',
route: '/projects/:id/schedules',
templateUrl: templateUrl("scheduler/scheduler"),
controller: 'schedulerController'
controller: 'schedulerController',
ncyBreadcrumb: {
label: 'PROJECT SCHEDULES'
}
});
$stateExtender.addState({
name: 'projectSchedules.add',
route: '/add',
templateUrl: templateUrl("scheduler/schedulerForm"),
controller: 'schedulerAddController'
controller: 'schedulerAddController',
ncyBreadcrumb: {
label: 'PROJECT SCHEDULES ADD'
}
});
$stateExtender.addState({
name: 'projectSchedules.edit',
route: '/:schedule_id',
templateUrl: templateUrl("scheduler/schedulerForm"),
controller: 'schedulerEditController'
controller: 'schedulerEditController',
ncyBreadcrumb: {
label: 'PROJECT SCHEDULES EDIT'
}
});
$stateExtender.addState({
name: 'inventoryManage.schedules',

View File

@ -509,7 +509,7 @@
Please provide a valid date.
</div>
</div>
<div class="form-group SchedulerForm-formGroup"
<div class="form-group SchedulerForm-formGroup"
ng-if="schedulerEnd && schedulerEnd.value == 'on'">
<label class="Form-inputLabel">
<span class="red-text">*</span>
@ -583,7 +583,7 @@
<div class="SchedulerFormDetail-container"
ng-show="schedulerIsValid">
<label class="SchedulerFormDetail-label">
Description
Schedule Description
</label>
<div class="SchedulerFormDetail-nlp">
{{ rrule_nlp_description }}
@ -651,7 +651,7 @@
<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>"
data-placement="right" data-container="body" over-title="Extra Variables" class="help-link" data-original-title="" title="" tabindex="-1">
<i class="fa fa-question-circle"></i>
</a>
</a>
<div class="parse-selection">
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="yaml"><span class="parse-label">YAML</span>

View File

@ -31,7 +31,6 @@
align-items: center;
max-height: 400px;
width: 120px;
overflow-y: scroll;
cursor: pointer;
text-transform: uppercase;
}

View File

@ -4,7 +4,7 @@ export default {
name: 'setup',
route: '/setup',
ncyBreadcrumb: {
label: "SETUP"
label: "SETTINGS"
},
templateUrl: templateUrl('setup-menu/setup-menu')
};

View File

@ -59,7 +59,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
callback = params.callback,
beforeDestroy = params.beforeDestroy,
closeOnEscape = (params.closeOnEscape === undefined) ? false : params.closeOnEscape,
resizable = (params.resizable === undefined) ? true : params.resizable,
resizable = (params.resizable === undefined) ? false : params.resizable,
draggable = (params.draggable === undefined) ? true : params.draggable,
dialogClass = params.dialogClass,
forms = _.chain([params.form]).flatten().compact().value(),

View File

@ -1,7 +1,7 @@
// default base colors
@default-interface-txt: #848992;
@default-interface-txt: #707070;
@default-data-txt: #161B1F;
@default-icon: #B7B7B7;
@default-icon: #848992;
@default-icon-hov: #D7D7D7; // also selected button
@default-border: #E8E8E8;
@d7grey: #D7D7D7; // used for random things, like the close button on top-right corner of panes
@ -20,6 +20,7 @@
@default-stdout-txt: #707070;
@default-dark: #000000;
@default-warning: #F0AD4E;
@default-warning-hov: #EC971F;
@default-unreachable: #FF0000;
@ -35,7 +36,7 @@
// buttons
@btn-bg: @default-bg;
@btn-bord: @default-border;
@btn-bord: @d7grey;
@btn-txt: @default-interface-txt;
@btn-bg-hov: @default-tertiary-bg;
@btn-bg-sel: @default-icon-hov;

View File

@ -1838,7 +1838,11 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
`;
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
html += "<div class=\"List-noItems\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == false && " + collection.iterator + "_total_rows < 1\">PLEASE ADD ITEMS TO THIS LIST</div>";
// Allow for the suppression of the empty list text to avoid duplication between form generator and list generator
if(!collection.suppressEmptyText || collection.suppressEmptyText === false) {
var emptyListText = (collection.emptyListText) ? collection.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
html += "<div class=\"List-noItems\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == false && " + collection.iterator + "_total_rows < 1\">" + emptyListText + "</div>";
}
html += `
<div class=\"List-noItems\" ng-show=\"is_superuser\">

View File

@ -87,7 +87,7 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
});
function loadStdout() {
Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=-' + page_size);
Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=0&end_line=' + page_size);
Rest.get()
.success(function(data) {
Wait('stop');
@ -145,38 +145,17 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
});
}
$scope.stdOutScrollToTop = function() {
// scroll up or back in time toward the beginning of the file
var start, end, url;
if (loaded_sections.length > 0 && loaded_sections[0].start > 0) {
start = (loaded_sections[0].start - page_size > 0) ? loaded_sections[0].start - page_size : 0;
end = loaded_sections[0].start - 1;
}
else if (loaded_sections.length === 0) {
start = 0;
end = page_size;
}
if (start !== undefined && end !== undefined) {
$('#stdoutMoreRowsTop').fadeIn();
url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + end;
// lrInfiniteScroll handler
// grabs the next stdout section
$scope.stdOutGetNextSection = function(){
if (current_range.absolute_end > current_range.end){
var url = $scope.stdoutEndpoint + '?format=json&start_line=' + current_range.end +
'&end_line=' + (current_range.end + page_size);
Rest.setUrl(url);
Rest.get()
.success( function(data) {
//var currentPos = $('#pre-container').scrollTop();
var newSH, oldSH = $('#pre-container').prop('scrollHeight'),
st = $('#pre-container').scrollTop();
$('#pre-container-content').prepend(data.content);
newSH = $('#pre-container').prop('scrollHeight');
$('#pre-container').scrollTop(newSH - oldSH + st);
loaded_sections.unshift({
start: (data.range.start < 0) ? 0 : data.range.start,
end: data.range.end
});
.success(function(data){
$('#pre-container-content').append(data.content);
current_range = data.range;
$('#stdoutMoreRowsTop').fadeOut(400);
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',

View File

@ -1,6 +1,5 @@
<div class="StandardOut-consoleOutput">
<div id="pre-container" class="body_background body_foreground pre mono-space StandardOut-preContainer"
lr-infinite-scroll="stdOutScrollToTop" scroll-threshold="300" data-direction="up" time-threshold="500">
<div class="StandardOut-consoleOutput" lr-infinite-scroll="stdOutGetNextSection" scroll-threshold="300" time-threshold="500">
<div id="pre-container" class="body_background body_foreground pre mono-space StandardOut-preContainer">
<div id="pre-container-content" class="StandardOut-preContent"></div>
</div>
<div class="scroll-spinner" id="stdoutMoreRowsBottom">

829
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,8 @@ ENV LC_ALL en_US.UTF-8
RUN apt-get update && apt-get install -y software-properties-common python-software-properties curl
RUN add-apt-repository -y ppa:chris-lea/redis-server; add-apt-repository -y ppa:chris-lea/zeromq; add-apt-repository -y ppa:chris-lea/node.js; add-apt-repository -y ppa:ansible/ansible; add-apt-repository -y ppa:jal233/proot;
RUN curl -sL https://deb.nodesource.com/setup_0.12 | bash -
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 && apt-key adv --fetch-keys http://www.postgresql.org/media/keys/ACCC4CF8.asc
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
RUN curl -sL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" | tee /etc/apt/sources.list.d/postgres-9.4.list
RUN apt-get update
RUN apt-get install -y openssh-server ansible mg vim tmux git mercurial subversion python-dev python-psycopg2 make postgresql-client libpq-dev nodejs python-psutil libxml2-dev libxslt-dev lib32z1-dev libsasl2-dev libldap2-dev libffi-dev libzmq-dev proot python-pip libxmlsec1-dev swig redis-server && rm -rf /var/lib/apt/lists/*