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

This commit is contained in:
Akita Noek 2016-06-01 14:09:51 -04:00
commit 7f9174f4ea
72 changed files with 1333 additions and 925 deletions

View File

@ -23,6 +23,7 @@ from django.db import models
# from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.forms.models import model_to_dict
# Django REST Framework
from rest_framework.exceptions import ValidationError
@ -1766,7 +1767,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,)),
@ -1783,19 +1784,21 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
request = self.context.get('request', None)
if request is not None and request.user is not None and obj.inventory is not None and obj.project is not None:
d['can_copy'] = request.user.can_access(JobTemplate, 'add',
{'inventory': obj.inventory.pk,
'project': obj.project.pk})
d['can_edit'] = request.user.can_access(JobTemplate, 'change', obj,
{'inventory': obj.inventory.pk,
'project': obj.project.pk})
elif request is not None and request.user is not None and request.user.is_superuser:
d['can_copy'] = True
d['can_edit'] = True
else:
# Check for conditions that would create a validation error if coppied
validation_errors, resources_needed_to_start = obj.resource_validation_data()
if request is None or request.user is None:
d['can_copy'] = False
d['can_edit'] = False
elif request.user.is_superuser:
d['can_copy'] = not validation_errors
d['can_edit'] = True
else:
jt_data = model_to_dict(obj)
d['can_copy'] = (not validation_errors) and request.user.can_access(JobTemplate, 'add', jt_data)
d['can_edit'] = request.user.can_access(JobTemplate, 'change', obj, jt_data)
d['recent_jobs'] = self._recent_jobs(obj)
return d
@ -2259,12 +2262,14 @@ class JobLaunchSerializer(BaseSerializer):
obj = self.context.get('obj')
data = self.context.get('data')
for field in obj.resources_needed_to_start:
if not (field in attrs and obj._ask_for_vars_dict().get(field, False)):
errors[field] = "Job Template '%s' is missing or undefined." % field
if (not obj.ask_credential_on_launch) or (not attrs.get('credential', None)):
credential = obj.credential
else:
credential = attrs.get('credential', None)
if not credential:
errors['credential'] = 'Credential not provided'
# fill passwords dict with request data passwords
if credential and credential.passwords_needed:
@ -2295,11 +2300,6 @@ class JobLaunchSerializer(BaseSerializer):
if validation_errors:
errors['variables_needed_to_start'] = validation_errors
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None):
errors['project'] = 'Job Template Project is missing or undefined.'
if (obj.inventory is None) and not attrs.get('inventory', None):
errors['inventory'] = 'Job Template Inventory is missing or undefined.'
# Special prohibited cases for scan jobs
if 'job_type' in data and obj.ask_job_type_on_launch:
if ((obj.job_type == PERM_INVENTORY_SCAN and not data['job_type'] == PERM_INVENTORY_SCAN) or

View File

@ -817,17 +817,76 @@ 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
class JobAccess(BaseAccess):
'''
I can see jobs when:
- I am a superuser.
- I can see its job template
- I am an admin or auditor of the organization which contains its inventory
- I am an admin or auditor of the organization which contains its project
I can delete jobs when:
- I am an admin of the organization which contains its inventory
- I am an admin of the organization which contains its project
'''
model = Job
@ -839,10 +898,20 @@ class JobAccess(BaseAccess):
if self.user.is_superuser:
return qs.all()
return qs.filter(
qs_jt = qs.filter(
job_template__in=JobTemplate.accessible_objects(self.user, 'read_role')
)
org_access_qs = Organization.objects.filter(
Q(admin_role__members=self.user) | Q(auditor_role__members=self.user))
if not org_access_qs.exists():
return qs_jt
return qs.filter(
Q(job_template__in=JobTemplate.accessible_objects(self.user, 'read_role')) |
Q(inventory__organization__in=org_access_qs) |
Q(project__organization__in=org_access_qs)).distinct()
def can_add(self, data):
if not data or '_method' in data: # So the browseable API will work?
return True
@ -871,7 +940,11 @@ class JobAccess(BaseAccess):
@check_superuser
def can_delete(self, obj):
return self.user in obj.inventory.admin_role
if obj.inventory is not None and self.user in obj.inventory.organization.admin_role:
return True
if obj.project is not None and self.user in obj.project.organization.admin_role:
return True
return False
def can_start(self, obj):
self.check_license()

View File

@ -242,15 +242,44 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled',
'labels',]
def resource_validation_data(self):
'''
Process consistency errors and need-for-launch related fields.
'''
resources_needed_to_start = []
validation_errors = {}
# Inventory and Credential related checks
if self.inventory is None:
resources_needed_to_start.append('inventory')
if not self.ask_inventory_on_launch:
validation_errors['inventory'] = ["Job Template must provide 'inventory' or allow prompting for it.",]
if self.credential is None:
resources_needed_to_start.append('credential')
if not self.ask_credential_on_launch:
validation_errors['credential'] = ["Job Template must provide 'credential' or allow prompting for it.",]
# Job type dependent checks
if self.job_type == 'scan':
if self.inventory is None or self.ask_inventory_on_launch:
validation_errors['inventory'] = ["Scan jobs must be assigned a fixed inventory.",]
elif self.project is None:
resources_needed_to_start.append('project')
validation_errors['project'] = ["Job types 'run' and 'check' must have assigned a project.",]
return (validation_errors, resources_needed_to_start)
def clean(self):
if self.job_type == 'scan' and (self.inventory is None or self.ask_inventory_on_launch):
raise ValidationError({"inventory": ["Scan jobs must be assigned a fixed inventory.",]})
if (not self.ask_inventory_on_launch) and self.inventory is None:
raise ValidationError({"inventory": ["Job Template must provide 'inventory' or allow prompting for it.",]})
if (not self.ask_credential_on_launch) and self.credential is None:
raise ValidationError({"credential": ["Job Template must provide 'credential' or allow prompting for it.",]})
validation_errors, resources_needed_to_start = self.resource_validation_data()
if validation_errors:
raise ValidationError(validation_errors)
return super(JobTemplate, self).clean()
@property
def resources_needed_to_start(self):
validation_errors, resources_needed_to_start = self.resource_validation_data()
return resources_needed_to_start
def create_job(self, **kwargs):
'''
Create a new job based on this template.
@ -265,9 +294,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
Return whether job template can be used to start a new job without
requiring any user input.
'''
return bool(self.credential and not len(self.passwords_needed_to_start) and
not len(self.variables_needed_to_start) and
self.inventory)
return (not self.resources_needed_to_start and
not self.passwords_needed_to_start and
not self.variables_needed_to_start)
@property
def variables_needed_to_start(self):

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

@ -107,7 +107,7 @@ 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)
jt = JobTemplate(name=name, job_type=job_type, playbook='mocked')
jt.inventory = inventory
if jt.inventory is None:

View File

@ -27,6 +27,17 @@ from .fixtures import (
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
@ -167,15 +178,19 @@ class _Mapped(object):
# or encapsulated by specific factory fixtures in a conftest
#
def create_job_template(name, **kwargs):
Objects = namedtuple("Objects", "job_template, inventory, project, credential, job_type")
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')
persisted = kwargs.get('persisted', True)
if 'organization' in kwargs:
org = kwargs['organization']
@ -202,24 +217,38 @@ def create_job_template(name, **kwargs):
job_type=job_type, persisted=persisted)
role_objects = generate_role_objects([org, proj, inv, cred])
apply_roles(kwargs.get('roles'), role_objects, persisted)
apply_roles(roles, role_objects, persisted)
return Objects(job_template=jt,
project=proj,
inventory=inv,
credential=cred,
job_type=job_type)
job_type=job_type,
organization=org,)
def create_organization(name, **kwargs):
Objects = namedtuple("Objects", "organization,teams,users,superusers,projects,labels,notification_templates")
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 = {}
persisted = kwargs.get('persisted', True)
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:
@ -246,20 +275,24 @@ def create_organization(name, **kwargs):
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(kwargs.get('roles'), role_objects, persisted)
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))
notification_templates=_Mapped(notification_templates),
inventories=_Mapped(inventories))
def create_notification_template(name, **kwargs):
Objects = namedtuple("Objects", "notification_template,organization,users,superusers,teams")
def create_notification_template(name, roles=None, persisted=True, **kwargs):
Objects = generate_objects(["notification_template",
"organization",
"users",
"superusers",
"teams",], kwargs)
organization = None
persisted = kwargs.get('persisted', True)
if 'organization' in kwargs:
org = kwargs['organization']
@ -272,7 +305,7 @@ def create_notification_template(name, **kwargs):
users = generate_users(organization, teams, False, persisted, users=kwargs.get('users'))
role_objects = generate_role_objects([organization, notification_template])
apply_roles(kwargs.get('roles'), role_objects, persisted)
apply_roles(roles, role_objects, persisted)
return Objects(notification_template=notification_template,
organization=organization,
users=_Mapped(users),

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

@ -178,7 +178,7 @@ def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, user):
args=[deploy_jobtemplate.pk]), {}, user('admin', True))
assert response.status_code == 400
assert response.data['inventory'] == ['Job Template Inventory is missing or undefined.']
assert response.data['inventory'] == ["Job Template 'inventory' is missing or undefined."]
@pytest.mark.django_db
@pytest.mark.job_runtime_vars

View File

@ -0,0 +1,179 @@
import pytest
import mock
# AWX
from awx.api.serializers import JobTemplateSerializer
from awx.main.models.jobs import JobTemplate
from awx.main.models.projects import ProjectOptions
# Django
from django.test.client import RequestFactory
from django.core.urlresolvers import reverse
@pytest.fixture
def jt_copy_edit(job_template_factory, project):
objects = job_template_factory(
'copy-edit-job-template',
project=project)
return objects.job_template
@property
def project_playbooks(self):
return ['mocked', 'mocked.yml', 'alt-mocked.yml']
@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
# Test protection against limited set of validation problems
@pytest.mark.django_db
def test_bad_data_copy_edit(admin_user, project):
"""
If a required resource (inventory here) was deleted, copying not allowed
because doing so would caues a validation error
"""
jt_res = JobTemplate.objects.create(
job_type='run',
project=project,
inventory=None, ask_inventory_on_launch=False, # not allowed
credential=None, ask_credential_on_launch=True,
name='deploy-job-template'
)
serializer = JobTemplateSerializer(jt_res)
request = RequestFactory().get('/api/v1/job_templates/12/')
request.user = admin_user
serializer.context['request'] = request
response = serializer.to_representation(jt_res)
assert not response['summary_fields']['can_copy']
assert response['summary_fields']['can_edit']
# Tests for correspondence between view info and actual access
@pytest.mark.django_db
def test_admin_copy_edit(jt_copy_edit, admin_user):
"Absent a validation error, system admins can do everything"
# Serializer can_copy/can_edit fields
serializer = JobTemplateSerializer(jt_copy_edit)
request = RequestFactory().get('/api/v1/job_templates/12/')
request.user = admin_user
serializer.context['request'] = request
response = serializer.to_representation(jt_copy_edit)
assert response['summary_fields']['can_copy']
assert response['summary_fields']['can_edit']
@pytest.mark.django_db
def test_org_admin_copy_edit(jt_copy_edit, org_admin):
"Organization admins SHOULD be able to copy a JT firmly in their org"
# Serializer can_copy/can_edit fields
serializer = JobTemplateSerializer(jt_copy_edit)
request = RequestFactory().get('/api/v1/job_templates/12/')
request.user = org_admin
serializer.context['request'] = request
response = serializer.to_representation(jt_copy_edit)
assert response['summary_fields']['can_copy']
assert response['summary_fields']['can_edit']
@pytest.mark.django_db
def test_org_admin_foreign_cred_no_copy_edit(jt_copy_edit, org_admin, machine_credential):
"""
Organization admins without access to the 3 related resources:
SHOULD NOT be able to copy JT
SHOULD NOT be able to edit that job template
"""
# Attach credential to JT that org admin can not use
jt_copy_edit.credential = machine_credential
jt_copy_edit.save()
# Serializer can_copy/can_edit fields
serializer = JobTemplateSerializer(jt_copy_edit)
request = RequestFactory().get('/api/v1/job_templates/12/')
request.user = org_admin
serializer.context['request'] = request
response = serializer.to_representation(jt_copy_edit)
assert not response['summary_fields']['can_copy']
assert not response['summary_fields']['can_edit']
@pytest.mark.django_db
def test_jt_admin_copy_edit(jt_copy_edit, rando):
"JT admins wihout access to associated resources SHOULD NOT be able to copy"
# random user given JT admin access only
jt_copy_edit.admin_role.members.add(rando)
jt_copy_edit.save()
# Serializer can_copy/can_edit fields
serializer = JobTemplateSerializer(jt_copy_edit)
request = RequestFactory().get('/api/v1/job_templates/12/')
request.user = rando
serializer.context['request'] = request
response = serializer.to_representation(jt_copy_edit)
assert not response['summary_fields']['can_copy']
assert not response['summary_fields']['can_edit']
@pytest.mark.django_db
def test_proj_jt_admin_copy_edit(jt_copy_edit, rando):
"JT admins with access to associated resources SHOULD be able to copy"
# random user given JT and project admin abilities
jt_copy_edit.admin_role.members.add(rando)
jt_copy_edit.save()
jt_copy_edit.project.admin_role.members.add(rando)
jt_copy_edit.project.save()
# Serializer can_copy/can_edit fields
serializer = JobTemplateSerializer(jt_copy_edit)
request = RequestFactory().get('/api/v1/job_templates/12/')
request.user = rando
serializer.context['request'] = request
response = serializer.to_representation(jt_copy_edit)
assert response['summary_fields']['can_copy']
assert response['summary_fields']['can_edit']
# Functional tests - create new JT with all returned fields, as the UI does
@pytest.mark.django_db
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
def test_org_admin_copy_edit_functional(jt_copy_edit, org_admin, get, post):
get_response = get(reverse('api:job_template_detail', args=[jt_copy_edit.pk]), user=org_admin)
assert get_response.status_code == 200
assert get_response.data['summary_fields']['can_copy']
post_data = get_response.data
post_data['name'] = '%s @ 12:19:47 pm' % post_data['name']
post_response = post(reverse('api:job_template_list', args=[]), user=org_admin, data=post_data)
assert post_response.status_code == 201
assert post_response.data['name'] == 'copy-edit-job-template @ 12:19:47 pm'
@pytest.mark.django_db
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
def test_jt_admin_copy_edit_functional(jt_copy_edit, rando, get, post):
# Grant random user JT admin access only
jt_copy_edit.admin_role.members.add(rando)
jt_copy_edit.save()
get_response = get(reverse('api:job_template_detail', args=[jt_copy_edit.pk]), user=rando)
assert get_response.status_code == 200
assert not get_response.data['summary_fields']['can_copy']
post_data = get_response.data
post_data['name'] = '%s @ 12:19:47 pm' % post_data['name']
post_response = post(reverse('api:job_template_list', args=[]), user=rando, data=post_data)
assert post_response.status_code == 403

View File

@ -0,0 +1,109 @@
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', '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.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

@ -215,6 +215,13 @@ def org_admin(user, organization):
organization.member_role.members.add(ret)
return ret
@pytest.fixture
def org_auditor(user, organization):
ret = user('org-auditor', False)
organization.auditor_role.members.add(ret)
organization.member_role.members.add(ret)
return ret
@pytest.fixture
def org_member(user, organization):
ret = user('org-member', False)

View File

@ -0,0 +1,72 @@
import pytest
from awx.main.access import JobAccess
from awx.main.models import Job
@pytest.fixture
def normal_job(deploy_jobtemplate):
return Job.objects.create(
job_template=deploy_jobtemplate,
project=deploy_jobtemplate.project,
inventory=deploy_jobtemplate.inventory
)
# Read permissions testing
@pytest.mark.django_db
def test_superuser_sees_orphans(normal_job, admin_user):
normal_job.job_template = None
access = JobAccess(admin_user)
assert access.can_read(normal_job)
@pytest.mark.django_db
def test_org_member_does_not_see_orphans(normal_job, org_member, project):
normal_job.job_template = None
# Check that privledged access to project still does not grant access
project.admin_role.members.add(org_member)
access = JobAccess(org_member)
assert not access.can_read(normal_job)
@pytest.mark.django_db
def test_org_admin_sees_orphans(normal_job, org_admin):
normal_job.job_template = None
access = JobAccess(org_admin)
assert access.can_read(normal_job)
@pytest.mark.django_db
def test_org_auditor_sees_orphans(normal_job, org_auditor):
normal_job.job_template = None
access = JobAccess(org_auditor)
assert access.can_read(normal_job)
# Delete permissions testing
@pytest.mark.django_db
def test_JT_admin_delete_denied(normal_job, rando):
normal_job.job_template.admin_role.members.add(rando)
access = JobAccess(rando)
assert not access.can_delete(normal_job)
@pytest.mark.django_db
def test_inventory_admin_delete_denied(normal_job, rando):
normal_job.job_template.inventory.admin_role.members.add(rando)
access = JobAccess(rando)
assert not access.can_delete(normal_job)
@pytest.mark.django_db
def test_null_related_delete_denied(normal_job, rando):
normal_job.project = None
normal_job.inventory = None
access = JobAccess(rando)
assert not access.can_delete(normal_job)
@pytest.mark.django_db
def test_inventory_org_admin_delete_allowed(normal_job, org_admin):
normal_job.project = None # do this so we test job->inventory->org->admin connection
access = JobAccess(org_admin)
assert access.can_delete(normal_job)
@pytest.mark.django_db
def test_project_org_admin_delete_allowed(normal_job, org_admin):
normal_job.inventory = None # do this so we test job->project->org->admin connection
access = JobAccess(org_admin)
assert access.can_delete(normal_job)

View File

@ -9,9 +9,14 @@ from awx.main.models import Label, Job
#DRF
from rest_framework import serializers
def mock_JT_resource_data():
return ({}, [])
@pytest.fixture
def job_template(mocker):
return mocker.MagicMock(pk=5)
mock_jt = mocker.MagicMock(pk=5)
mock_jt.resource_validation_data = mock_JT_resource_data
return mock_jt
@pytest.fixture
def job(mocker, job_template):

View File

@ -0,0 +1,35 @@
from awx.main.tests.factories import create_job_template
def test_missing_project_error():
objects = create_job_template(
'missing-project-jt',
organization='org1',
inventory='inventory1',
credential='cred1',
persisted=False)
obj = objects.job_template
assert 'project' in obj.resources_needed_to_start
validation_errors, resources_needed_to_start = obj.resource_validation_data()
assert 'project' in validation_errors
def test_inventory_credential_need_to_start():
objects = create_job_template(
'job-template-few-resources',
project='project1',
persisted=False)
obj = objects.job_template
assert 'inventory' in obj.resources_needed_to_start
assert 'credential' in obj.resources_needed_to_start
def test_inventory_credential_contradictions():
objects = create_job_template(
'job-template-paradox',
project='project1',
persisted=False)
obj = objects.job_template
obj.ask_inventory_on_launch = False
obj.ask_credential_on_launch = False
validation_errors, resources_needed_to_start = obj.resource_validation_data()
assert 'inventory' in validation_errors
assert 'credential' in validation_errors

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,14 @@
.Form-formGroup--checkbox{
display: flex;
align-items: flex-end;
align-items: flex-start;
}
.Form-textUneditable {
.Form-textInput {
border: none;
padding: 0;
}
}
.Form-subForm {
@ -324,6 +331,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{
@ -427,6 +440,10 @@ input[type='radio']:checked:before {
outline:none;
}
.Form-inputLabelContainer {
width: 100%;
display: block !important;
}
.Form-inputLabel{
text-transform: uppercase;
color: @default-interface-txt;
@ -437,6 +454,16 @@ input[type='radio']:checked:before {
.noselect;
}
.Form-labelAction {
text-transform: uppercase;
font-weight: normal;
font-size: 0.8em;
padding-left:5px;
float: right;
margin-top: 3px;
.noselect;
}
.Form-buttons{
height: 30px;
display: flex;

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',
@ -495,6 +495,35 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l
$scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false;
$scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch';
}
// Dynamically update popover values
if($scope.scm_type.value) {
switch ($scope.scm_type.value) {
case 'git':
$scope.urlPopover = '<p>Example URLs for GIT SCM include:</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
'<p><strong>Note:</strong> When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
'do not enter a username (other than git). Additionally, GitHub and Bitbucket do not support password authentication when using ' +
'SSH. GIT read only protocol (git://) does not use username or password information.';
break;
case 'svn':
$scope.urlPopover = '<p>Example URLs for Subversion SCM include:</p>' +
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
'<li>svn+ssh://servername.example.com/path</li></ul>';
break;
case 'hg':
$scope.urlPopover = '<p>Example URLs for Mercurial SCM include:</p>' +
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
'<li>ssh://server.example.com/path</li></ul>' +
'<p><strong>Note:</strong> Mercurial does not support password authentication for SSH. ' +
'Do not put the username and key in the URL. ' +
'If using Bitbucket and SSH, do not supply your Bitbucket username.';
break;
default:
$scope.urlPopover = '<p> URL popover text';
}
}
};
$scope.formCancel = function () {

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

@ -81,6 +81,11 @@ export default
},
project: {
label: 'Project',
labelAction: {
label: 'RESET',
ngClick: 'resetProjectToDefault()',
'class': "{{!(job_type.value === 'scan' && project_name !== 'Default') ? 'hidden' : ''}}",
},
type: 'lookup',
sourceModel: 'project',
sourceField: 'name',
@ -99,6 +104,7 @@ export default
label: 'Playbook',
type:'select',
ngOptions: 'book for book in playbook_options track by book',
ngDisabled: "job_type.value === 'scan' && project_name === 'Default'",
id: 'playbook-select',
awRequiredWhen: {
reqExpression: "playbookrequired",
@ -110,12 +116,6 @@ export default
dataPlacement: 'right',
dataContainer: "body",
},
default_scan: {
type: 'custom',
column: 1,
ngShow: 'job_type.value === "scan" && project_name !== "Default"',
control: '<a href="" ng-click="toggleScanInfo()">Reset to default project and playbook</a>'
},
credential: {
label: 'Machine Credential',
type: 'lookup',
@ -204,7 +204,7 @@ export default
},
job_tags: {
label: 'Job Tags',
type: 'textarea',
type: 'text',
rows: 1,
addRequired: false,
editRequired: false,

View File

@ -64,11 +64,10 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
ngChange: 'scmChange()',
addRequired: true,
editRequired: true,
hasSubForm: true
hasSubForm: true,
},
missing_path_alert: {
type: 'alertblock',
"class": 'alert-info',
ngShow: "showMissingPlaybooksAlert && scm_type.value == 'manual'",
alertTxt: '<p class=\"text-justify\"><strong>WARNING:</strong> There are no available playbook directories in {{ base_dir }}. ' +
'Either that directory is empty, or all of the contents are already assigned to other projects. ' +
@ -79,7 +78,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
base_dir: {
label: 'Project Base Path',
type: 'text',
//"class": 'col-lg-6',
class: 'Form-textUneditable',
showonly: true,
ngShow: "scm_type.value == 'manual' " ,
awPopOver: '<p>Base path used for locating playbooks. Directories found inside this path will be listed in the playbook directory drop-down. ' +
@ -115,30 +114,12 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
init: false
},
subForm: 'sourceSubForm',
helpCollapse: [{
hdr: 'GIT URLs',
content: '<p>Example URLs for GIT SCM include:</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
'<p><strong>Note:</strong> When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
'do not enter a username (other than git). Additionally, GitHub and Bitbucket do not support password authentication when using ' +
'SSH. GIT read only protocol (git://) does not use username or password information.',
show: "scm_type.value == 'git'"
}, {
hdr: 'SVN URLs',
content: '<p>Example URLs for Subversion SCM include:</p>' +
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
'<li>svn+ssh://servername.example.com/path</li></ul>',
show: "scm_type.value == 'svn'"
}, {
hdr: 'Mercurial URLs',
content: '<p>Example URLs for Mercurial SCM include:</p>' +
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
'<li>ssh://server.example.com/path</li></ul>' +
'<p><strong>Note:</strong> Mercurial does not support password authentication for SSH. ' +
'Do not put the username and key in the URL. ' +
'If using Bitbucket and SSH, do not supply your Bitbucket username.',
show: "scm_type.value == 'hg'"
}],
hideSubForm: "scm_type.value === 'manual'",
awPopOverWatch: "urlPopover",
awPopOver: "set in controllers/projects",
dataTitle: 'SCM URL',
dataContainer: 'body',
dataPlacement: 'right'
},
scm_branch: {
labelBind: "scmBranchLabel",
@ -174,7 +155,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 +167,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 +178,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 +254,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,7 +138,6 @@ export default
iterator: 'team',
open: false,
index: false,
actions: {},
fields: {

View File

@ -784,7 +784,6 @@ export default
url, play;
scope.tasks = [];
if (scope.selectedPlay) {
url = scope.job.url + 'job_tasks/?event_id=' + scope.selectedPlay;
url += (scope.search_task_name) ? '&task__icontains=' + scope.search_task_name : '';
@ -912,16 +911,25 @@ export default
scope.tasks[idx].taskActiveClass = '';
}
});
params = {
parent: scope.selectedTask,
event__startswith: 'runner',
page_size: scope.hostResultsMaxRows,
order: 'host_name,counter',
};
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
scope.hostResults = JobDetailService.processHostEvents(res.results);
if (scope.selectedTask !== null){
params = {
parent: scope.selectedTask,
event__startswith: 'runner',
page_size: scope.hostResultsMaxRows,
order: 'host_name,counter',
};
if (scope.search_host_status === 'failed'){
params.failed = true;
}
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
scope.hostResults = JobDetailService.processHostEvents(res.results);
scope.hostResultsLoading = false;
});
}
else{
scope.hostResults = [];
scope.hostResultsLoading = false;
});
}
};
}])
@ -934,7 +942,7 @@ export default
graph_data.push({
label: 'OK',
value: count.ok.length,
color: '#60D66F'
color: '#5CB85C'
});
}
if (count.changed.length > 0) {
@ -979,18 +987,19 @@ export default
job_detail_chart = nv.models.pieChart()
.margin({bottom: 15})
.x(function(d) {
return d.label +': '+ Math.round((d.value/total)*100) + "%";
return d.label +': '+ Math.floor((d.value/total)*100) + "%";
})
.y(function(d) { return d.value; })
.showLabels(true)
.showLegend(false)
.showLabels(false)
.showLegend(true)
.growOnHover(false)
.labelThreshold(0.01)
.tooltipContent(function(x, y) {
return '<p>'+x+'</p>'+ '<p>' + Math.floor(y.replace(',','')) + ' HOSTS ' + '</p>';
})
.color(colors);
job_detail_chart.legend.rightAlign(false);
job_detail_chart.legend.margin({top: 5, right: 450, left:0, bottom: 0});
d3.select(element.find('svg')[0])
.datum(dataset)
.transition().duration(350)
@ -1000,19 +1009,15 @@ export default
"font-style": "normal",
"font-weight":400,
"src": "url(/static/assets/OpenSans-Regular.ttf)",
"width": 500,
"width": 600,
"height": 300,
"color": '#848992'
});
d3.select(element.find(".nv-label text")[0])
.attr("class", "HostSummary-graph--successful")
d3.select(element.find(".nv-noData")[0])
.style({
"font-family": 'Open Sans',
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[0],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
"text-anchor": 'start'
});
/*
d3.select(element.find(".nv-label text")[1])
.attr("class", "HostSummary-graph--changed")
.style({
@ -1040,6 +1045,7 @@ export default
"fill" : colors[3],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
*/
return job_detail_chart;
};
}])

View File

@ -177,7 +177,7 @@ angular.module('JobTemplatesHelper', ['Utilities'])
});
if(scope.project === "" && scope.playbook === ""){
if (scope.project === "" && scope.playbook === "") {
scope.toggleScanInfo();
}

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

@ -1,6 +1,6 @@
<div id="hosts-summary-section" class="section">
<div class="JobDetail-instructions"><span class="badge">4</span> Please select a host below to view a summary of all associated tasks.</div>
<div class="JobDetail-instructions" ng-hide="hosts.length == 0"><span class="badge">4</span> Please select a host below to view a summary of all associated tasks.</div>
<div class="JobDetail-searchHeaderRow" ng-hide="hosts.length == 0">
<div class="JobDetail-searchContainer form-group">
<div class="search-name">
@ -13,7 +13,7 @@
</div>
<div class="JobDetail-tableToggleContainer form-group">
<div class="btn-group" >
<button
<button
ng-click="setFilter('all')"
class="JobDetail-tableToggle btn btn-xs" ng-class="{'btn-default': filter === 'failed', 'btn-primary': filter === 'all'}">All</button>
<button ng-click="setFilter('failed')"
@ -34,7 +34,7 @@
</div>
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500">
<table class="table">
<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,9 +13,15 @@
.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);
.OnePlusOne-container(100%, @breakpoint-md);
}
.JobDetail-leftSide{
@ -151,8 +157,25 @@
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;
font-size: 12px;
text-transform: uppercase;
font-family: 'Open Sans', sans-serif;
}
.JobDetail .nv-series{
padding-right: 30px;
display: block;
}
.JobDetail-instructions .badge{
background-color: @default-list-header-bg;
color: @default-interface-txt;
}
.JobDetail-tableToggle--left{
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
@ -185,6 +208,12 @@
margin-left: -5px;
}
.JobDetails-table--noResults {
tr > td {
border-top: none !important;
}
}
.JobDetail-statusIcon--results{
padding-left: 0px;
padding-right: 10px;
@ -196,7 +225,13 @@
}
.JobDetail-stdoutActionButton--active{
display: none;
visibility: hidden;
flex:none;
width:0px;
padding-right: 0px;
}
.JobDetail-leftSide.JobDetail-stdoutActionButton--active {
margin-right: 0px;
}

View File

@ -673,21 +673,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');
};
@ -776,6 +764,15 @@ export default
}
};
scope.filterTaskStatus = function() {
scope.search_task_status = (scope.search_task_status === 'all') ? 'failed' : 'all';
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
LoadTasks({
scope: scope
});
}
};
scope.filterPlayStatus = function() {
scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all';
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
@ -785,6 +782,26 @@ export default
}
};
scope.filterHostStatus = function(){
scope.search_host_status = (scope.search_host_status === 'all') ? 'failed' : 'all';
if (!scope.liveEventProcessing || scope.pauseLiveEvents){
if (scope.selectedTask !== null && scope.selectedPlay !== null){
var params = {
parent: scope.selectedTask,
page_size: scope.hostResultsMaxRows,
order: 'host_name,counter',
};
if (scope.search_host_status === 'failed'){
params.failed = true;
}
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
scope.hostResults = JobDetailService.processHostEvents(res.results);
scope.hostResultsLoading = false;
});
}
}
};
scope.searchPlays = function() {
if (scope.search_play_name) {
scope.searchPlaysEnabled = false;

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,
@ -196,12 +197,20 @@
});
});
function sync_playbook_select2() {
CreateSelect2({
element:'#playbook-select',
multiple: false
});
}
// Update playbook select whenever project value changes
selectPlaybook = function (oldValue, newValue) {
var url;
if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){
$scope.playbook_options = ['Default'];
$scope.playbook = 'Default';
sync_playbook_select2();
Wait('stop');
}
else if (oldValue !== newValue) {
@ -216,6 +225,7 @@
opts.push(data[i]);
}
$scope.playbook_options = opts;
sync_playbook_select2();
Wait('stop');
})
.error(function (data, status) {
@ -226,32 +236,37 @@
}
};
$scope.jobTypeChange = function(){
if($scope.job_type){
if($scope.job_type.value === 'scan'){
// If the job_type is 'scan' then we don't want the user to be
// able to prompt for job type or inventory
$scope.ask_job_type_on_launch = false;
$scope.ask_inventory_on_launch = false;
$scope.toggleScanInfo();
}
else if($scope.project_name === "Default"){
$scope.project_name = null;
$scope.playbook_options = [];
// $scope.playbook = 'null';
$scope.job_templates_form.playbook.$setPristine();
}
}
let last_non_scan_project_name = null;
let last_non_scan_playbook = "";
let last_non_scan_playbook_options = [];
$scope.jobTypeChange = function() {
if ($scope.job_type) {
if ($scope.job_type.value === 'scan') {
if ($scope.project_name !== "Default") {
last_non_scan_project_name = $scope.project_name;
last_non_scan_playbook = $scope.playbook;
last_non_scan_playbook_options = $scope.playbook_options;
}
// If the job_type is 'scan' then we don't want the user to be
// able to prompt for job type or inventory
$scope.ask_job_type_on_launch = false;
$scope.ask_inventory_on_launch = false;
$scope.resetProjectToDefault();
}
else if ($scope.project_name === "Default") {
$scope.project_name = last_non_scan_project_name;
$scope.playbook_options = last_non_scan_playbook_options;
$scope.playbook = last_non_scan_playbook;
$scope.job_templates_form.playbook.$setPristine();
}
}
sync_playbook_select2();
};
$scope.toggleScanInfo = function() {
$scope.resetProjectToDefault = function() {
$scope.project_name = 'Default';
if($scope.project === null){
selectPlaybook();
}
else {
$scope.project = null;
}
$scope.project = null;
selectPlaybook('force_load');
};
// Detect and alert user to potential SCM status issues

View File

@ -76,6 +76,13 @@ export default
$scope.playbook = null;
generator.reset();
function sync_playbook_select2() {
CreateSelect2({
element:'#playbook-select',
multiple: false
});
}
getPlaybooks = function (project) {
var url;
if ($scope.playbook) {
@ -85,6 +92,7 @@ export default
if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){
$scope.playbook_options = ['Default'];
$scope.playbook = 'Default';
sync_playbook_select2();
Wait('stop');
}
else if (!Empty(project)) {
@ -93,14 +101,14 @@ export default
Rest.setUrl(url);
Rest.get()
.success(function (data) {
var i;
$scope.playbook_options = [];
for (i = 0; i < data.length; i++) {
for (var i = 0; i < data.length; i++) {
$scope.playbook_options.push(data[i]);
if (data[i] === $scope.playbook) {
$scope.job_templates_form.playbook.$setValidity('required', true);
}
}
sync_playbook_select2();
if ($scope.playbook) {
$scope.$emit('jobTemplateLoadFinished');
} else {
@ -108,7 +116,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 ' +
@ -122,23 +130,31 @@ export default
}
};
$scope.jobTypeChange = function(){
if($scope.job_type){
if($scope.job_type.value === 'scan'){
// If the job_type is 'scan' then we don't want the user to be
// able to prompt for job type or inventory
$scope.ask_job_type_on_launch = false;
$scope.ask_inventory_on_launch = false;
$scope.toggleScanInfo();
}
else if($scope.project_name === "Default"){
$scope.project_name = null;
$scope.playbook_options = [];
// $scope.playbook = 'null';
$scope.job_templates_form.playbook.$setPristine();
}
}
let last_non_scan_project_name = null;
let last_non_scan_playbook = "";
let last_non_scan_playbook_options = [];
$scope.jobTypeChange = function() {
if ($scope.job_type) {
if ($scope.job_type.value === 'scan') {
if ($scope.project_name !== "Default") {
last_non_scan_project_name = $scope.project_name;
last_non_scan_playbook = $scope.playbook;
last_non_scan_playbook_options = $scope.playbook_options;
}
// If the job_type is 'scan' then we don't want the user to be
// able to prompt for job type or inventory
$scope.ask_job_type_on_launch = false;
$scope.ask_inventory_on_launch = false;
$scope.resetProjectToDefault();
}
else if ($scope.project_name === "Default") {
$scope.project_name = last_non_scan_project_name;
$scope.playbook_options = last_non_scan_playbook_options;
$scope.playbook = last_non_scan_playbook;
$scope.job_templates_form.playbook.$setPristine();
}
}
sync_playbook_select2();
};
$scope.toggleNotification = function(event, notifier_id, column) {
@ -159,14 +175,10 @@ export default
});
};
$scope.toggleScanInfo = function() {
$scope.resetProjectToDefault = function() {
$scope.project_name = 'Default';
if($scope.project === null){
getPlaybooks();
}
else {
$scope.project = null;
}
$scope.project = null;
getPlaybooks();
};
// Detect and alert user to potential SCM status issues
@ -198,7 +210,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 +300,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

@ -36,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

@ -714,13 +714,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
function label() {
var html = '';
if (field.label || field.labelBind) {
html += "<label ";
if (horizontal || field.labelClass) {
html += "class=\"";
html += (field.labelClass) ? field.labelClass : "";
html += (horizontal) ? " " + getLabelWidth() : "";
html += "\" ";
}
html += "<label class=\"";
html += (field.labelClass) ? field.labelClass : "";
html += (horizontal) ? " " + getLabelWidth() : "Form-inputLabelContainer ";
html += "\" ";
html += (field.labelNGClass) ? "ng-class=\"" + field.labelNGClass + "\" " : "";
html += "for=\"" + fld + '">\n';
html += (field.icon) ? Icon(field.icon) : "";
@ -742,6 +739,14 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "\" value=\"json\" ng-change=\"parseTypeChange()\"> <span class=\"parse-label\">JSON</span>\n";
html += "</div>\n";
}
if (field.labelAction) {
let action = field.labelAction;
let href = action.href || "";
let ngClick = action.ngClick || "";
let cls = action["class"] || "";
html += `<a class="Form-labelAction ${cls}" href="${href}" ng-click="${ngClick}">${action.label}</a>`;
}
html += "\n\t</label>\n";
}
return html;
@ -787,7 +792,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if ((!field.readonly) || (field.readonly && options.mode === 'edit')) {
if((field.excludeMode === undefined || field.excludeMode !== options.mode)) {
if((field.excludeMode === undefined || field.excludeMode !== options.mode) && field.type !== 'alertblock') {
html += "<div class='form-group Form-formGroup ";
@ -1612,7 +1617,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
currentSubForm = field.subForm;
var subFormTitle = this.form.subFormTitles[field.subForm];
html += '<div class="Form-subForm '+ currentSubForm + '" ng-hide="'+ hasSubFormField + '.value === undefined"> ';
html += '<div class="Form-subForm '+ currentSubForm + '" ng-hide="'+ hasSubFormField + '.value === undefined || ' + field.hideSubForm + '"> ';
html += '<span class="Form-subForm--title">'+ subFormTitle +'</span>';
}
else if (!field.subForm && currentSubForm !== undefined) {
@ -1838,7 +1843,12 @@ 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
var emptyListText = (collection.emptyListText) ? collection.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
html += '<div ng-hide="is_superuser">';
html += "<div class=\"List-noItems\" ng-hide=\"is_superuser\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == false && " + collection.iterator + "_total_rows < 1\">" + emptyListText + "</div>";
html += '</div>';
//}
html += `
<div class=\"List-noItems\" ng-show=\"is_superuser\">

View File

@ -8,7 +8,6 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
function ($log, $rootScope, $scope, $state, $stateParams, ProcessErrors, Rest, Wait) {
var api_complete = false,
stdout_url,
current_range,
loaded_sections = [],
event_queue = 0,
@ -87,7 +86,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 +144,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