Merge branch 'devel' of github.com:ansible/ansible-tower into devel

This commit is contained in:
Leigh Johnson 2016-05-05 10:20:27 -04:00
commit 1f12a89b00
252 changed files with 4498 additions and 2890 deletions

View File

@ -249,7 +249,19 @@ rebase:
push:
git push origin master
virtualenv:
virtualenv: virtualenv_ansible virtualenv_tower
virtualenv_ansible:
if [ "$(VENV_BASE)" ]; then \
if [ ! -d "$(VENV_BASE)" ]; then \
mkdir $(VENV_BASE); \
fi; \
if [ ! -d "$(VENV_BASE)/ansible" ]; then \
virtualenv --system-site-packages $(VENV_BASE)/ansible; \
fi; \
fi
virtualenv_tower:
if [ "$(VENV_BASE)" ]; then \
if [ ! -d "$(VENV_BASE)" ]; then \
mkdir $(VENV_BASE); \
@ -257,12 +269,9 @@ virtualenv:
if [ ! -d "$(VENV_BASE)/tower" ]; then \
virtualenv --system-site-packages $(VENV_BASE)/tower; \
fi; \
if [ ! -d "$(VENV_BASE)/ansible" ]; then \
virtualenv --system-site-packages $(VENV_BASE)/ansible; \
fi; \
fi
requirements_ansible:
requirements_ansible: virtualenv_ansible
if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/ansible/bin/activate; \
$(VENV_BASE)/ansible/bin/pip install -U pip==8.1.1; \
@ -273,7 +282,7 @@ requirements_ansible:
fi
# Install third-party requirements needed for Tower's environment.
requirements_tower:
requirements_tower: virtualenv_tower
if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
$(VENV_BASE)/tower/bin/pip install -U pip==8.1.1; \
@ -299,7 +308,7 @@ requirements_jenkins:
fi && \
$(NPM_BIN) install csslint jshint
requirements: virtualenv requirements_ansible requirements_tower
requirements: requirements_ansible requirements_tower
requirements_dev: requirements requirements_tower_dev
@ -640,7 +649,7 @@ tar-build/$(SETUP_TAR_FILE):
@cp -a setup tar-build/$(SETUP_TAR_NAME)
@rsync -az docs/licenses tar-build/$(SETUP_TAR_NAME)/
@cd tar-build/$(SETUP_TAR_NAME) && sed -e 's#%NAME%#$(NAME)#;s#%VERSION%#$(VERSION)#;s#%RELEASE%#$(RELEASE)#;' group_vars/all.in > group_vars/all
@cd tar-build && tar -czf $(SETUP_TAR_FILE) --exclude "*/all.in" $(SETUP_TAR_NAME)/
@cd tar-build && tar -czf $(SETUP_TAR_FILE) --exclude "*/all.in" --exclude "**/test/*" $(SETUP_TAR_NAME)/
@ln -sf $(SETUP_TAR_FILE) tar-build/$(SETUP_TAR_LINK)
tar-build/$(SETUP_TAR_CHECKSUM):

View File

@ -131,6 +131,8 @@ class FieldLookupBackend(BaseFilterBackend):
value = to_python_boolean(value)
elif new_lookup.endswith('__in'):
items = []
if not value:
raise ValueError('cannot provide empty value for __in')
for item in value.split(','):
items.append(self.value_to_python_for_field(field, item))
value = items
@ -218,7 +220,7 @@ class FieldLookupBackend(BaseFilterBackend):
q = Q(**{k:v})
queryset = queryset.filter(q)
queryset = queryset.filter(*args)
return queryset.distinct()
return queryset
except (FieldError, FieldDoesNotExist, ValueError), e:
raise ParseError(e.args[0])
except ValidationError, e:

View File

@ -25,6 +25,7 @@ from rest_framework import views
# AWX
from awx.main.models import * # noqa
from awx.main.models import Label
from awx.main.utils import * # noqa
from awx.api.serializers import ResourceAccessListElementSerializer
@ -35,7 +36,8 @@ __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView',
'SubDetailAPIView',
'ResourceAccessList',
'ParentMixin',]
'ParentMixin',
'DeleteLastUnattachLabelMixin',]
logger = logging.getLogger('awx.api.generics')
@ -399,12 +401,15 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
else:
return Response(status=status.HTTP_204_NO_CONTENT)
def unattach(self, request, *args, **kwargs):
def unattach_validate(self, request):
sub_id = request.data.get('id', None)
res = None
if not sub_id:
data = dict(msg='"id" is required to disassociate')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
res = Response(data, status=status.HTTP_400_BAD_REQUEST)
return (sub_id, res)
def unattach_by_id(self, request, sub_id):
parent = self.get_parent_object()
parent_key = getattr(self, 'parent_key', None)
relationship = getattrd(parent, self.relationship)
@ -421,6 +426,12 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
def unattach(self, request, *args, **kwargs):
(sub_id, res) = self.unattach_validate(request)
if res:
return res
return self.unattach_by_id(request, sub_id)
def post(self, request, *args, **kwargs):
if not isinstance(request.data, dict):
return Response('invalid type for post data',
@ -430,6 +441,21 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
else:
return self.attach(request, *args, **kwargs)
class DeleteLastUnattachLabelMixin(object):
def unattach(self, request, *args, **kwargs):
(sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request, *args, **kwargs)
if res:
return res
res = super(DeleteLastUnattachLabelMixin, self).unattach_by_id(request, sub_id)
label = Label.objects.get(id=sub_id)
if label.is_detached():
label.delete()
return res
class SubDetailAPIView(generics.RetrieveAPIView, GenericAPIView, ParentMixin):
pass
@ -474,7 +500,7 @@ class ResourceAccessList(ListAPIView):
def get_queryset(self):
self.object_id = self.kwargs['pk']
resource_model = getattr(self, 'resource_model')
obj = resource_model.objects.get(pk=self.object_id)
obj = get_object_or_404(resource_model, pk=self.object_id)
content_type = ContentType.objects.get_for_model(obj)
roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id))
@ -483,4 +509,3 @@ class ResourceAccessList(ListAPIView):
for r in roles:
ancestors.update(set(r.ancestors.all()))
return User.objects.filter(roles__in=list(ancestors))

View File

@ -38,7 +38,7 @@ from polymorphic import PolymorphicModel
from awx.main.constants import SCHEDULEABLE_PROVIDERS
from awx.main.models import * # noqa
from awx.main.fields import ImplicitRoleField
from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat, camelcase_to_underscore
from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat, camelcase_to_underscore, getattrd
from awx.main.redact import REPLACE_STR
from awx.main.conf import tower_settings
@ -78,7 +78,6 @@ SUMMARIZABLE_FK_FIELDS = {
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
'network_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'net'),
'permission': DEFAULT_SUMMARY_FIELDS,
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
'job_template': DEFAULT_SUMMARY_FIELDS,
'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',),
@ -90,6 +89,7 @@ SUMMARIZABLE_FK_FIELDS = {
'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
'inventory_source': ('source', 'last_updated', 'status'),
'source_script': ('name', 'description'),
'role': ('id', 'role_field')
}
@ -340,16 +340,18 @@ class BaseSerializer(serializers.ModelSerializer):
return None
elif isinstance(obj, User):
return obj.date_joined
else:
elif hasattr(obj, 'created'):
return obj.created
return None
def get_modified(self, obj):
if obj is None:
return None
elif isinstance(obj, User):
return obj.last_login # Not actually exposed for User.
else:
elif hasattr(obj, 'modified'):
return obj.modified
return None
def build_standard_field(self, field_name, model_field):
# DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits
@ -699,7 +701,7 @@ class UserSerializer(BaseSerializer):
def validate_password(self, value):
if not self.instance and value in (None, ''):
raise serializers.ValidationError('Password required for new User')
raise serializers.ValidationError('Password required for new User.')
return value
def _update_password(self, obj, new_password):
@ -763,7 +765,7 @@ class UserSerializer(BaseSerializer):
ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
if field_name in ldap_managed_fields:
if value != getattr(self.instance, field_name):
raise serializers.ValidationError('Unable to change %s on user managed by LDAP' % field_name)
raise serializers.ValidationError('Unable to change %s on user managed by LDAP.' % field_name)
return value
def validate_username(self, value):
@ -796,6 +798,7 @@ class OrganizationSerializer(BaseSerializer):
users = reverse('api:organization_users_list', args=(obj.pk,)),
admins = reverse('api:organization_admins_list', args=(obj.pk,)),
teams = reverse('api:organization_teams_list', args=(obj.pk,)),
credentials = reverse('api:organization_credential_list', args=(obj.pk,)),
activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,)),
notifiers = reverse('api:organization_notifiers_list', args=(obj.pk,)),
notifiers_any = reverse('api:organization_notifiers_any_list', args=(obj.pk,)),
@ -961,7 +964,7 @@ class BaseSerializerWithVariables(BaseSerializer):
try:
yaml.safe_load(value)
except yaml.YAMLError:
raise serializers.ValidationError('Must be valid JSON or YAML')
raise serializers.ValidationError('Must be valid JSON or YAML.')
return value
@ -1113,7 +1116,7 @@ class HostSerializer(BaseSerializerWithVariables):
vars_dict['ansible_ssh_port'] = port
attrs['variables'] = yaml.dump(vars_dict)
except (yaml.YAMLError, TypeError):
raise serializers.ValidationError('Must be valid JSON or YAML')
raise serializers.ValidationError('Must be valid JSON or YAML.')
return super(HostSerializer, self).validate(attrs)
@ -1170,7 +1173,7 @@ class GroupSerializer(BaseSerializerWithVariables):
def validate_name(self, value):
if value in ('all', '_meta'):
raise serializers.ValidationError('Invalid group name')
raise serializers.ValidationError('Invalid group name.')
return value
def to_representation(self, obj):
@ -1303,10 +1306,10 @@ class InventorySourceOptionsSerializer(BaseSerializer):
else:
try:
if source_script.organization != self.instance.inventory.organization:
errors['source_script'] = 'source_script does not belong to the same organization as the inventory'
errors['source_script'] = 'source_script does not belong to the same organization as the inventory.'
except Exception:
# TODO: Log
errors['source_script'] = 'source_script doesn\'t exist'
errors['source_script'] = 'source_script doesn\'t exist.'
if errors:
raise serializers.ValidationError(errors)
@ -1441,8 +1444,26 @@ class RoleSerializer(BaseSerializer):
class Meta:
model = Role
fields = ('*', 'description', 'name')
read_only_fields = ('description', 'name')
read_only_fields = ('id', 'role_field', 'description', 'name')
def to_representation(self, obj):
ret = super(RoleSerializer, self).to_representation(obj)
def spacify_type_name(cls):
return re.sub(r'([a-z])([A-Z])', '\g<1> \g<2>', cls.__name__)
if obj.object_id:
content_object = obj.content_object
if hasattr(content_object, 'username'):
ret['summary_fields']['resource_name'] = obj.content_object.username
if hasattr(content_object, 'name'):
ret['summary_fields']['resource_name'] = obj.content_object.name
ret['summary_fields']['resource_type'] = obj.content_type.name
ret['summary_fields']['resource_type_display_name'] = spacify_type_name(obj.content_type.model_class())
ret.pop('created')
ret.pop('modified')
return ret
def get_related(self, obj):
ret = super(RoleSerializer, self).get_related(obj)
@ -1524,6 +1545,15 @@ class ResourceAccessListElementSerializer(UserSerializer):
.filter(content_type=team_content_type,
members=user,
children__in=direct_permissive_role_ids)
if content_type == team_content_type:
# When looking at the access list for a team, exclude the entries
# for that team. This exists primarily so we don't list the read role
# as a direct role when a user is a member or admin of a team
direct_team_roles = direct_team_roles.exclude(
children__content_type=team_content_type,
children__object_id=obj.id
)
indirect_team_roles = Role.objects \
.filter(content_type=team_content_type,
@ -1553,6 +1583,18 @@ class ResourceAccessListElementSerializer(UserSerializer):
class CredentialSerializer(BaseSerializer):
# FIXME: may want to make some fields filtered based on user accessing
user = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), required=False, default=None, write_only=True,
help_text='Write-only field used to add user to owner role. If provided, '
'do not give either team or organization. Only valid for creation.')
team = serializers.PrimaryKeyRelatedField(
queryset=Team.objects.all(), required=False, default=None, write_only=True,
help_text='Write-only field used to add team to owner role. If provided, '
'do not give either user or organization. Only valid for creation.')
organization = serializers.PrimaryKeyRelatedField(
queryset=Organization.objects.all(), required=False, default=None, write_only=True,
help_text='Write-only field used to add organization to owner role. If provided, '
'do not give either team or team. Only valid for creation.')
class Meta:
model = Credential
@ -1561,7 +1603,14 @@ class CredentialSerializer(BaseSerializer):
'ssh_key_data', 'ssh_key_unlock',
'become_method', 'become_username', 'become_password',
'vault_password', 'subscription', 'tenant', 'secret', 'client',
'authorize', 'authorize_password')
'authorize', 'authorize_password',
'user', 'team', 'organization')
def create(self, validated_data):
# Remove the user, team, and organization processed in view
for field in ['user', 'team', 'organization']:
validated_data.pop(field, None)
return super(CredentialSerializer, self).create(validated_data)
def build_standard_field(self, field_name, model_field):
field_class, field_kwargs = super(CredentialSerializer, self).build_standard_field(field_name, model_field)
@ -1657,9 +1706,9 @@ class JobOptionsSerializer(BaseSerializer):
if not project and job_type != PERM_INVENTORY_SCAN:
raise serializers.ValidationError({'project': 'This field is required.'})
if project and playbook and force_text(playbook) not in project.playbooks:
raise serializers.ValidationError({'playbook': 'Playbook not found for project'})
raise serializers.ValidationError({'playbook': 'Playbook not found for project.'})
if project and not playbook:
raise serializers.ValidationError({'playbook': 'Must select playbook for project'})
raise serializers.ValidationError({'playbook': 'Must select playbook for project.'})
return super(JobOptionsSerializer, self).validate(attrs)
@ -1720,7 +1769,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
survey_enabled = attrs.get('survey_enabled', self.instance and self.instance.survey_enabled or False)
job_type = attrs.get('job_type', self.instance and self.instance.job_type or None)
if survey_enabled and job_type == PERM_INVENTORY_SCAN:
raise serializers.ValidationError({'survey_enabled': 'Survey Enabled can not be used with scan jobs'})
raise serializers.ValidationError({'survey_enabled': 'Survey Enabled can not be used with scan jobs.'})
return super(JobTemplateSerializer, self).validate(attrs)
@ -1733,12 +1782,13 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
ask_tags_on_launch = serializers.ReadOnlyField()
ask_job_type_on_launch = serializers.ReadOnlyField()
ask_inventory_on_launch = serializers.ReadOnlyField()
ask_credential_on_launch = serializers.ReadOnlyField()
class Meta:
model = Job
fields = ('*', 'job_template', 'passwords_needed_to_start', 'ask_variables_on_launch',
'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch',
'ask_inventory_on_launch')
'ask_inventory_on_launch', 'ask_credential_on_launch')
def get_related(self, obj):
res = super(JobSerializer, self).get_related(obj)
@ -1865,9 +1915,9 @@ class JobRelaunchSerializer(JobSerializer):
if not obj.credential:
raise serializers.ValidationError(dict(credential=["Credential not found or deleted."]))
if obj.job_type != PERM_INVENTORY_SCAN and obj.project is None:
raise serializers.ValidationError(dict(errors=["Job Template Project is missing or undefined"]))
raise serializers.ValidationError(dict(errors=["Job Template Project is missing or undefined."]))
if obj.inventory is None:
raise serializers.ValidationError(dict(errors=["Job Template Inventory is missing or undefined"]))
raise serializers.ValidationError(dict(errors=["Job Template Inventory is missing or undefined."]))
attrs = super(JobRelaunchSerializer, self).validate(attrs)
return attrs
@ -1876,7 +1926,7 @@ class AdHocCommandSerializer(UnifiedJobSerializer):
class Meta:
model = AdHocCommand
fields = ('*', 'job_type', 'inventory', 'limit', 'credential',
'module_name', 'module_args', 'forks', 'verbosity',
'module_name', 'module_args', 'forks', 'verbosity', 'extra_vars',
'become_enabled', '-unified_job_template', '-description')
extra_kwargs = {
'name': {
@ -2104,6 +2154,8 @@ class JobLaunchSerializer(BaseSerializer):
inventory_needed_to_start = serializers.SerializerMethodField()
survey_enabled = serializers.SerializerMethodField()
extra_vars = VerbatimField(required=False, write_only=True)
job_template_data = serializers.SerializerMethodField()
defaults = serializers.SerializerMethodField()
class Meta:
model = JobTemplate
@ -2113,7 +2165,8 @@ class JobLaunchSerializer(BaseSerializer):
'ask_job_type_on_launch', 'ask_limit_on_launch',
'ask_inventory_on_launch', 'ask_credential_on_launch',
'survey_enabled', 'variables_needed_to_start',
'credential_needed_to_start', 'inventory_needed_to_start',)
'credential_needed_to_start', 'inventory_needed_to_start',
'job_template_data', 'defaults')
read_only_fields = ('ask_variables_on_launch', 'ask_limit_on_launch',
'ask_tags_on_launch', 'ask_job_type_on_launch',
'ask_inventory_on_launch', 'ask_credential_on_launch')
@ -2137,6 +2190,21 @@ class JobLaunchSerializer(BaseSerializer):
return obj.survey_enabled and 'spec' in obj.survey_spec
return False
def get_defaults(self, obj):
ask_for_vars_dict = obj._ask_for_vars_dict()
defaults_dict = {}
for field in ask_for_vars_dict:
if field in ('inventory', 'credential'):
defaults_dict[field] = dict(
name=getattrd(obj, '%s.name' % field, None),
id=getattrd(obj, '%s.pk' % field, None))
else:
defaults_dict[field] = getattr(obj, field)
return defaults_dict
def get_job_template_data(self, obj):
return dict(name=obj.name, id=obj.id, description=obj.description)
def validate(self, attrs):
errors = {}
obj = self.context.get('obj')
@ -2166,8 +2234,9 @@ class JobLaunchSerializer(BaseSerializer):
except (ValueError, TypeError):
try:
extra_vars = yaml.safe_load(extra_vars)
except (yaml.YAMLError, TypeError, AttributeError):
errors['extra_vars'] = 'Must be valid JSON or YAML'
assert isinstance(extra_vars, dict)
except (yaml.YAMLError, TypeError, AttributeError, AssertionError):
errors['extra_vars'] = 'Must be a valid JSON or YAML dictionary'
if not isinstance(extra_vars, dict):
extra_vars = {}
@ -2178,9 +2247,9 @@ class JobLaunchSerializer(BaseSerializer):
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'
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'
errors['inventory'] = 'Job Template Inventory is missing or undefined.'
if errors:
raise serializers.ValidationError(errors)
@ -2318,7 +2387,7 @@ class ScheduleSerializer(BaseSerializer):
def validate_unified_job_template(self, value):
if type(value) == InventorySource and value.source not in SCHEDULEABLE_PROVIDERS:
raise serializers.ValidationError('Inventory Source must be a cloud resource')
raise serializers.ValidationError('Inventory Source must be a cloud resource.')
return value
# We reject rrules if:
@ -2510,7 +2579,7 @@ class TowerSettingsSerializer(BaseSerializer):
def validate(self, attrs):
manifest = settings.TOWER_SETTINGS_MANIFEST
if attrs['key'] not in manifest:
raise serializers.ValidationError(dict(key=["Key {0} is not a valid settings key".format(attrs['key'])]))
raise serializers.ValidationError(dict(key=["Key {0} is not a valid settings key.".format(attrs['key'])]))
if attrs['value_type'] == 'json':
attrs['value'] = json.dumps(attrs['value'])
@ -2544,7 +2613,7 @@ class AuthTokenSerializer(serializers.Serializer):
else:
raise serializers.ValidationError('Unable to login with provided credentials.')
else:
raise serializers.ValidationError('Must include "username" and "password"')
raise serializers.ValidationError('Must include "username" and "password".')
class FactVersionSerializer(BaseFactSerializer):

View File

@ -1,20 +1,3 @@
TOWER SOFTWARE END USER LICENSE AGREEMENT
Unless otherwise agreed to, and executed in a definitive agreement, between
Ansible, Inc. (“Ansible”) and the individual or entity (“Customer”) signing or
electronically accepting these terms of use for the Tower Software (“EULA”),
all Tower Software, including any and all versions released or made available
by Ansible, shall be subject to the Ansible Software Subscription and Services
Agreement found at www.ansible.com/subscription-agreement (“Agreement”).
Ansible is not responsible for any additional obligations, conditions or
warranties agreed to between Customer and an authorized distributor, or
reseller, of the Tower Software. BY DOWNLOADING AND USING THE TOWER SOFTWARE,
OR BY CLICKING ON THE “YES” BUTTON OR OTHER BUTTON OR MECHANISM DESIGNED TO
ACKNOWLEDGE CONSENT TO THE TERMS OF AN ELECTRONIC COPY OF THIS EULA, THE
CUSTOMER HEREBY ACKNOWLEDGES THAT CUSTOMER HAS READ, UNDERSTOOD, AND AGREES TO
BE BOUND BY THE TERMS OF THIS EULA AND AGREEMENT, INCLUDING ALL TERMS
INCORPORATED HEREIN BY REFERENCE, AND THAT THIS EULA AND AGREEMENT IS
EQUIVALENT TO ANY WRITTEN NEGOTIATED AGREEMENT BETWEEN CUSTOMER AND ANSIBLE.
THIS EULA AND AGREEMENT IS ENFORCEABLE AGAINST ANY PERSON OR ENTITY THAT USES
OR AVAILS ITSELF OF THE TOWER SOFTWARE OR ANY PERSON OR ENTITY THAT USES THE OR
AVAILS ITSELF OF THE TOWER SOFTWARE ON ANOTHER PERSONS OR ENTITYS BEHALF.
Unless otherwise agreed to, and executed in a definitive agreement, between Ansible, Inc. (“Ansible”) and the individual or entity (“Customer”) signing or electronically accepting these terms of use for the Tower Software (“EULA”), all Tower Software, including any and all versions released or made available by Ansible, shall be subject to the Ansible Software Subscription and Services Agreement found at www.ansible.com/subscription-agreement (“Agreement”). Ansible is not responsible for any additional obligations, conditions or warranties agreed to between Customer and an authorized distributor, or reseller, of the Tower Software. BY DOWNLOADING AND USING THE TOWER SOFTWARE, OR BY CLICKING ON THE “YES” BUTTON OR OTHER BUTTON OR MECHANISM DESIGNED TO ACKNOWLEDGE CONSENT TO THE TERMS OF AN ELECTRONIC COPY OF THIS EULA, THE CUSTOMER HEREBY ACKNOWLEDGES THAT CUSTOMER HAS READ, UNDERSTOOD, AND AGREES TO BE BOUND BY THE TERMS OF THIS EULA AND AGREEMENT, INCLUDING ALL TERMS INCORPORATED HEREIN BY REFERENCE, AND THAT THIS EULA AND AGREEMENT IS EQUIVALENT TO ANY WRITTEN NEGOTIATED AGREEMENT BETWEEN CUSTOMER AND ANSIBLE. THIS EULA AND AGREEMENT IS ENFORCEABLE AGAINST ANY PERSON OR ENTITY THAT USES OR AVAILS ITSELF OF THE TOWER SOFTWARE OR ANY PERSON OR ENTITY THAT USES THE OR AVAILS ITSELF OF THE TOWER SOFTWARE ON ANOTHER PERSONS OR ENTITYS BEHALF.

View File

@ -30,6 +30,8 @@ from django.views.decorators.csrf import csrf_exempt
from django.template.loader import render_to_string
from django.core.servers.basehttp import FileWrapper
from django.http import HttpResponse
from django.contrib.contenttypes.models import ContentType
# Django REST Framework
from rest_framework.exceptions import PermissionDenied, ParseError
@ -616,9 +618,14 @@ class OrganizationList(ListCreateAPIView):
JT_reference = 'project__organization'
db_results['job_templates'] = JobTemplate.accessible_objects(
self.request.user, 'read_role').values(JT_reference).annotate(
self.request.user, 'read_role').exclude(job_type='scan').values(JT_reference).annotate(
Count(JT_reference)).order_by(JT_reference)
JT_scan_reference = 'inventory__organization'
db_results['job_templates_scan'] = JobTemplate.accessible_objects(
self.request.user, 'read_role').filter(job_type='scan').values(JT_scan_reference).annotate(
Count(JT_scan_reference)).order_by(JT_scan_reference)
db_results['projects'] = project_qs\
.values('organization').annotate(Count('organization')).order_by('organization')
@ -638,6 +645,8 @@ class OrganizationList(ListCreateAPIView):
for res in db_results:
if res == 'job_templates':
org_reference = JT_reference
elif res == 'job_templates_scan':
org_reference = JT_scan_reference
elif res == 'users':
org_reference = 'id'
else:
@ -651,6 +660,12 @@ class OrganizationList(ListCreateAPIView):
continue
count_context[org_id][res] = entry['%s__count' % org_reference]
# Combine the counts for job templates with scan job templates
for org in org_id_list:
org_id = org['id']
if 'job_templates_scan' in count_context[org_id]:
count_context[org_id]['job_templates'] += count_context[org_id].pop('job_templates_scan')
full_context['related_field_counts'] = count_context
return full_context
@ -684,8 +699,10 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView):
organization__id=org_id).count()
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count()
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(
project__organization__id=org_id).count()
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).exclude(
job_type='scan').filter(project__organization__id=org_id).count()
org_counts['job_templates'] += JobTemplate.accessible_objects(**access_kwargs).filter(
job_type='scan').filter(inventory__organization__id=org_id).count()
full_context['related_field_counts'] = {}
full_context['related_field_counts'][org_id] = org_counts
@ -814,10 +831,11 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
relationship='member_role.children'
def get_queryset(self):
team = Team.objects.get(pk=self.kwargs['pk'])
return team.member_role.children.filter(id__in=Role.visible_roles(self.request.user))
team = get_object_or_404(Team, pk=self.kwargs['pk'])
if not self.request.user.can_access(Team, 'read', team):
raise PermissionDenied()
return Role.filter_visible_roles(self.request.user, team.member_role.children.all())
# XXX: Need to enforce permissions
def post(self, request, *args, **kwargs):
# Forbid implicit role creation here
sub_id = request.data.get('id', None)
@ -1081,8 +1099,12 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
#u = User.objects.get(pk=self.kwargs['pk'])
return Role.visible_roles(self.request.user).filter(members__in=[int(self.kwargs['pk']), ])
u = get_object_or_404(User, pk=self.kwargs['pk'])
if not self.request.user.can_access(User, 'read', u):
raise PermissionDenied()
content_type = ContentType.objects.get_for_model(User)
return Role.filter_visible_roles(self.request.user, u.roles.all()) \
.exclude(content_type=content_type, object_id=u.id)
def post(self, request, *args, **kwargs):
# Forbid implicit role creation here
@ -1090,6 +1112,10 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
if not sub_id:
data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
if sub_id == self.request.user.admin_role.pk:
raise PermissionDenied('You may not remove your own admin_role')
return super(UserRolesList, self).post(request, *args, **kwargs)
def check_parent_access(self, parent=None):
@ -1205,6 +1231,10 @@ class CredentialList(ListCreateAPIView):
serializer_class = CredentialSerializer
def post(self, request, *args, **kwargs):
for field in [x for x in ['user', 'team', 'organization'] if x in request.data and request.data[x] in ('', None)]:
request.data.pop(field)
kwargs.pop(field, None)
if not any([x in request.data for x in ['user', 'team', 'organization']]):
return Response({'detail': 'Missing user, team, or organization'}, status=status.HTTP_400_BAD_REQUEST)
@ -1213,15 +1243,15 @@ class CredentialList(ListCreateAPIView):
if 'user' in request.data:
user = User.objects.get(pk=request.data['user'])
obj = user
can_add_params = {'user': user.id}
if 'team' in request.data:
team = Team.objects.get(pk=request.data['team'])
obj = team
can_add_params = {'team': team.id}
if 'organization' in request.data:
organization = Organization.objects.get(pk=request.data['organization'])
obj = organization
can_add_params = {'organization': organization.id}
if self.request.user not in obj.admin_role:
if not self.request.user.can_access(Credential, 'add', can_add_params):
raise PermissionDenied()
ret = super(CredentialList, self).post(request, *args, **kwargs)
@ -1251,8 +1281,7 @@ class UserCredentialsList(CredentialList):
return user_creds & visible_creds
def post(self, request, *args, **kwargs):
user = User.objects.get(pk=self.kwargs['pk'])
request.data['user'] = user.id
request.data['user'] = self.kwargs['pk']
# The following post takes care of ensuring the current user can add a cred to this user
return super(UserCredentialsList, self).post(request, args, kwargs)
@ -1271,8 +1300,7 @@ class TeamCredentialsList(CredentialList):
return team_creds & visible_creds
def post(self, request, *args, **kwargs):
team = Team.objects.get(pk=self.kwargs['pk'])
request.data['team'] = team.id
request.data['team'] = self.kwargs['pk']
# The following post takes care of ensuring the current user can add a cred to this user
return super(TeamCredentialsList, self).post(request, args, kwargs)
@ -1479,7 +1507,7 @@ class HostAllGroupsList(SubListAPIView):
def get_queryset(self):
parent = self.get_parent_object()
self.check_parent_access(parent)
qs = self.request.user.get_queryset(self.model)
qs = self.request.user.get_queryset(self.model).distinct()
sublist_qs = parent.all_groups.distinct()
return qs & sublist_qs
@ -2263,7 +2291,7 @@ class JobTemplateNotifiersSuccessList(SubListCreateAttachDetachAPIView):
parent_model = JobTemplate
relationship = 'notifiers_success'
class JobTemplateLabelList(SubListCreateAttachDetachAPIView):
class JobTemplateLabelList(SubListCreateAttachDetachAPIView, DeleteLastUnattachLabelMixin):
model = Label
serializer_class = LabelSerializer
@ -2454,7 +2482,7 @@ class SystemJobTemplateList(ListAPIView):
def get(self, request, *args, **kwargs):
if not request.user.is_superuser:
return Response(status=status.HTTP_404_NOT_FOUND)
raise PermissionDenied("Superuser privileges needed")
return super(SystemJobTemplateList, self).get(request, *args, **kwargs)
class SystemJobTemplateDetail(RetrieveAPIView):
@ -3184,7 +3212,7 @@ class SystemJobList(ListCreateAPIView):
def get(self, request, *args, **kwargs):
if not request.user.is_superuser:
return Response(status=status.HTTP_404_NOT_FOUND)
raise PermissionDenied("Superuser privileges needed")
return super(SystemJobList, self).get(request, *args, **kwargs)
@ -3573,7 +3601,7 @@ class RoleChildrenList(SubListAPIView):
# XXX: This should be the intersection between the roles of the user
# and the roles that the requesting user has access to see
role = Role.objects.get(pk=self.kwargs['pk'])
return role.children
return role.children.all()

View File

@ -110,6 +110,18 @@ def check_user_access(user, model_class, action, *args, **kwargs):
return result
return False
def check_superuser(func):
'''
check_superuser is a decorator that provides a simple short circuit
for access checks. If the User object is a superuser, return True, otherwise
execute the logic of the can_access method.
'''
def wrapper(self, *args, **kwargs):
if self.user.is_superuser:
return True
return func(self, *args, **kwargs)
return wrapper
class BaseAccess(object):
'''
Base class for checking user access to a given model. Subclasses should
@ -242,10 +254,8 @@ class UserAccess(BaseAccess):
# that a user should be able to edit for themselves.
return bool(self.user == obj or self.can_admin(obj, data))
@check_superuser
def can_admin(self, obj, data):
# Admin implies changing all user fields.
if self.user.is_superuser:
return True
return Organization.objects.filter(member_role__members=obj, admin_role__members=self.user).exists()
def can_delete(self, obj):
@ -277,9 +287,8 @@ class OrganizationAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by').all()
@check_superuser
def can_change(self, obj, data):
if self.user.is_superuser:
return True
return self.user in obj.admin_role
def can_delete(self, obj):
@ -312,27 +321,25 @@ class InventoryAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by', 'organization').all()
@check_superuser
def can_read(self, obj):
if self.user.is_superuser:
return True
return self.user in obj.read_role
@check_superuser
def can_use(self, obj):
if self.user.is_superuser:
return True
return self.user in obj.use_role
@check_superuser
def can_add(self, data):
# If no data is specified, just checking for generic add permission?
if not data:
return Organization.accessible_objects(self.user, 'admin_role').exists()
if self.user.is_superuser:
return True
org_pk = get_pk_from_dict(data, 'organization')
org = get_object_or_400(Organization, pk=org_pk)
return self.user in org.admin_role
@check_superuser
def can_change(self, obj, data):
# Verify that the user has access to the new organization if moving an
# inventory to a new organization.
@ -342,8 +349,9 @@ class InventoryAccess(BaseAccess):
if self.user not in org.admin_role:
return False
# Otherwise, just check for write permission.
return self.user in obj.admin_role
return self.user in obj.update_role
@check_superuser
def can_admin(self, obj, data):
# Verify that the user has access to the new organization if moving an
# inventory to a new organization.
@ -371,12 +379,16 @@ class HostAccess(BaseAccess):
def get_queryset(self):
inv_qs = Inventory.accessible_objects(self.user, 'read_role')
group_qs = Group.accessible_objects(self.user, 'read_role')
qs = (self.model.objects.filter(inventory=inv_qs) | self.model.objects.filter(groups=group_qs)).distinct()
#qs = qs.select_related('created_by', 'modified_by', 'inventory',
# 'last_job__job_template',
# 'last_job_host_summary__job')
#return qs.prefetch_related('groups').all()
group_qs = Group.accessible_objects(self.user, 'read_role').exclude(inventory__in=inv_qs)
if group_qs.count():
qs = self.model.objects.filter(Q(inventory__in=inv_qs) | Q(groups__in=group_qs))
else:
qs = self.model.objects.filter(inventory__in=inv_qs)
qs = qs.select_related('created_by', 'modified_by', 'inventory',
'last_job__job_template',
'last_job_host_summary__job')
qs =qs.prefetch_related('groups').all()
return qs
def can_read(self, obj):
@ -389,7 +401,7 @@ class HostAccess(BaseAccess):
# Checks for admin or change permission on inventory.
inventory_pk = get_pk_from_dict(data, 'inventory')
inventory = get_object_or_400(Inventory, pk=inventory_pk)
if self.user not in inventory.admin_role:
if self.user not in inventory.update_role:
return False
# Check to see if we have enough licenses
@ -403,7 +415,7 @@ class HostAccess(BaseAccess):
raise PermissionDenied('Unable to change inventory on a host')
# Checks for admin or change permission on inventory, controls whether
# the user can edit variable data.
return obj and self.user in obj.inventory.admin_role
return obj and self.user in obj.inventory.update_role
def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False):
@ -440,7 +452,7 @@ class GroupAccess(BaseAccess):
# Checks for admin or change permission on inventory.
inventory_pk = get_pk_from_dict(data, 'inventory')
inventory = get_object_or_400(Inventory, pk=inventory_pk)
return self.user in inventory.admin_role
return self.user in inventory.update_role
def can_change(self, obj, data):
# Prevent moving a group to a different inventory.
@ -449,7 +461,7 @@ class GroupAccess(BaseAccess):
raise PermissionDenied('Unable to change inventory on a group')
# Checks for admin or change permission on inventory, controls whether
# the user can attach subgroups or edit variable data.
return obj and self.user in obj.inventory.admin_role
return obj and self.user in obj.inventory.update_role
def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False):
@ -483,7 +495,7 @@ class InventorySourceAccess(BaseAccess):
def get_queryset(self):
qs = self.model.objects.all()
qs = qs.select_related('created_by', 'modified_by', 'group', 'inventory')
inventory_ids = set(self.user.get_queryset(Inventory).values_list('id', flat=True))
inventory_ids = self.user.get_queryset(Inventory)
return qs.filter(Q(inventory_id__in=inventory_ids) |
Q(group__inventory_id__in=inventory_ids))
@ -502,7 +514,7 @@ class InventorySourceAccess(BaseAccess):
def can_change(self, obj, data):
# Checks for admin or change permission on group.
if obj and obj.group:
return self.user in obj.group.admin_role
return self.user in obj.group.update_role
# Can't change inventory sources attached to only the inventory, since
# these are created automatically from the management command.
else:
@ -555,21 +567,36 @@ class CredentialAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by').all()
@check_superuser
def can_read(self, obj):
return self.user in obj.read_role
def can_add(self, data):
# Access enforced in our view where we have context enough to make a decision
return True
def can_use(self, obj):
if self.user.is_superuser:
return True
user_pk = get_pk_from_dict(data, 'user')
if user_pk:
user_obj = get_object_or_400(User, pk=user_pk)
return check_user_access(self.user, User, 'change', user_obj, None)
team_pk = get_pk_from_dict(data, 'team')
if team_pk:
team_obj = get_object_or_400(Team, pk=team_pk)
return check_user_access(self.user, Team, 'change', team_obj, None)
organization_pk = get_pk_from_dict(data, 'organization')
if organization_pk:
organization_obj = get_object_or_400(Organization, pk=organization_pk)
return check_user_access(self.user, Organization, 'change', organization_obj, None)
return False
@check_superuser
def can_use(self, obj):
return self.user in obj.use_role
@check_superuser
def can_change(self, obj, data):
if self.user.is_superuser:
return True
if not self.can_add(data):
return False
return self.user in obj.owner_role
def can_delete(self, obj):
@ -596,14 +623,12 @@ class TeamAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by', 'organization').all()
@check_superuser
def can_add(self, data):
if self.user.is_superuser:
org_pk = get_pk_from_dict(data, 'organization')
org = get_object_or_400(Organization, pk=org_pk)
if self.user in org.admin_role:
return True
else:
org_pk = get_pk_from_dict(data, 'organization')
org = get_object_or_400(Organization, pk=org_pk)
if self.user in org.admin_role:
return True
return False
def can_change(self, obj, data):
@ -611,6 +636,8 @@ class TeamAccess(BaseAccess):
org_pk = get_pk_from_dict(data, 'organization')
if obj and org_pk and obj.organization.pk != org_pk:
raise PermissionDenied('Unable to change organization on a team')
if self.user.is_superuser:
return True
return self.user in obj.admin_role
def can_delete(self, obj):
@ -640,15 +667,13 @@ class ProjectAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all()
@check_superuser
def can_add(self, data):
if self.user.is_superuser:
return True
qs = Organization.accessible_objects(self.user, 'admin_role')
return qs.exists()
@check_superuser
def can_change(self, obj, data):
if self.user.is_superuser:
return True
return self.user in obj.admin_role
def can_delete(self, obj):
@ -674,9 +699,11 @@ class ProjectUpdateAccess(BaseAccess):
project_ids = set(self.user.get_queryset(Project).values_list('id', flat=True))
return qs.filter(project_id__in=project_ids)
@check_superuser
def can_cancel(self, obj):
return self.can_change(obj, {}) and obj.can_cancel
@check_superuser
def can_delete(self, obj):
return obj and self.user in obj.project.admin_role
@ -704,8 +731,7 @@ class JobTemplateAccess(BaseAccess):
'credential', 'cloud_credential', 'next_schedule').all()
def can_read(self, obj):
# you can only see the job templates that you have permission to launch.
return self.can_start(obj, validate_license=False)
return self.user in obj.read_role
def can_add(self, data):
'''
@ -847,10 +873,8 @@ class JobAccess(BaseAccess):
def can_change(self, obj, data):
return obj.status == 'new' and self.can_read(obj) and self.can_add(data)
@check_superuser
def can_delete(self, obj):
# Allow org admins and superusers to delete jobs
if self.user.is_superuser:
return True
return self.user in obj.inventory.admin_role
def can_start(self, obj):
@ -866,11 +890,12 @@ class JobAccess(BaseAccess):
return self.user in obj.job_template.execute_role
inventory_access = self.user in obj.inventory.use_role
credential_access = self.user in obj.credential.use_role
org_access = self.user in obj.inventory.organization.admin_role
project_access = obj.project is None or self.user in obj.project.admin_role
return inventory_access and (org_access or project_access)
return inventory_access and credential_access and (org_access or project_access)
def can_cancel(self, obj):
return self.can_read(obj) and obj.can_cancel
@ -1146,18 +1171,16 @@ class ScheduleAccess(BaseAccess):
UnifiedJobTemplate.objects.filter(Q(inventorysource__in=inventory_source_qs))
return qs.filter(unified_job_template__in=unified_qs)
@check_superuser
def can_read(self, obj):
if self.user.is_superuser:
return True
if obj and obj.unified_job_template:
job_class = obj.unified_job_template
return self.user.can_access(type(job_class), 'read', obj.unified_job_template)
else:
return False
@check_superuser
def can_add(self, data):
if self.user.is_superuser:
return True
pk = get_pk_from_dict(data, 'unified_job_template')
obj = get_object_or_400(UnifiedJobTemplate, pk=pk)
if obj:
@ -1165,18 +1188,16 @@ class ScheduleAccess(BaseAccess):
else:
return False
@check_superuser
def can_change(self, obj, data):
if self.user.is_superuser:
return True
if obj and obj.unified_job_template:
job_class = obj.unified_job_template
return self.user.can_access(type(job_class), 'change', job_class, None)
else:
return False
@check_superuser
def can_delete(self, obj):
if self.user.is_superuser:
return True
if obj and obj.unified_job_template:
job_class = obj.unified_job_template
return self.user.can_access(type(job_class), 'change', job_class, None)
@ -1195,25 +1216,22 @@ class NotifierAccess(BaseAccess):
return qs
return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all())
@check_superuser
def can_read(self, obj):
if self.user.is_superuser:
return True
if obj.organization is not None:
return self.user in obj.organization.admin_role
return False
@check_superuser
def can_add(self, data):
if self.user.is_superuser:
return True
if not data:
return Organization.accessible_objects(self.user, 'admin_role').exists()
org_pk = get_pk_from_dict(data, 'organization')
org = get_object_or_400(Organization, pk=org_pk)
return self.user in org.admin_role
@check_superuser
def can_change(self, obj, data):
if self.user.is_superuser:
return True
org_pk = get_pk_from_dict(data, 'organization')
if obj and org_pk and obj.organization.pk != org_pk:
org = get_object_or_400(Organization, pk=org_pk)
@ -1260,15 +1278,12 @@ class LabelAccess(BaseAccess):
organization__in=Organization.accessible_objects(self.user, 'read_role')
)
@check_superuser
def can_read(self, obj):
if self.user.is_superuser:
return True
return self.user in obj.organization.read_role
@check_superuser
def can_add(self, data):
if self.user.is_superuser:
return True
if not data or '_method' in data: # So the browseable API will work?
return True
@ -1276,10 +1291,8 @@ class LabelAccess(BaseAccess):
org = get_object_or_400(Organization, pk=org_pk)
return self.user in org.read_role
@check_superuser
def can_change(self, obj, data):
if self.user.is_superuser:
return True
if self.can_add(data) is False:
return False
@ -1376,26 +1389,10 @@ class CustomInventoryScriptAccess(BaseAccess):
return self.model.objects.distinct().all()
return self.model.accessible_objects(self.user, 'read_role').all()
@check_superuser
def can_read(self, obj):
if self.user.is_superuser:
return True
return self.user in obj.read_role
def can_add(self, data):
if self.user.is_superuser:
return True
return False
def can_change(self, obj, data):
if self.user.is_superuser:
return True
return False
def can_delete(self, obj):
if self.user.is_superuser:
return True
return False
class TowerSettingsAccess(BaseAccess):
'''
@ -1409,17 +1406,6 @@ class TowerSettingsAccess(BaseAccess):
model = TowerSettings
def get_queryset(self):
if self.user.is_superuser:
return self.model.objects.all()
return self.model.objects.none()
def can_change(self, obj, data):
return self.user.is_superuser
def can_delete(self, obj):
return self.user.is_superuser
class RoleAccess(BaseAccess):
'''
@ -1432,14 +1418,6 @@ class RoleAccess(BaseAccess):
model = Role
def get_queryset(self):
if self.user.is_superuser:
return self.model.objects.all()
return Role.objects.none()
def can_change(self, obj, data):
return self.user.is_superuser
def can_read(self, obj):
if not obj:
return False
@ -1463,9 +1441,8 @@ class RoleAccess(BaseAccess):
skip_sub_obj_read_check=False):
return self.can_unattach(obj, sub_obj, relationship)
@check_superuser
def can_unattach(self, obj, sub_obj, relationship):
if self.user.is_superuser:
return True
if obj.object_id and \
isinstance(obj.content_object, ResourceMixin) and \
self.user in obj.content_object.admin_role:

View File

@ -18,12 +18,12 @@ from django.db.models.fields.related import (
ReverseManyRelatedObjectsDescriptor,
)
from django.utils.encoding import smart_text
from django.utils.timezone import now
# AWX
from awx.main.models.rbac import batch_role_ancestor_rebuilding
from awx.main.models.rbac import batch_role_ancestor_rebuilding, Role
from awx.main.utils import get_current_apps
__all__ = ['AutoOneToOneField', 'ImplicitRoleField']
@ -92,9 +92,7 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
class ImplicitRoleField(models.ForeignKey):
"""Implicitly creates a role entry for a resource"""
def __init__(self, role_name=None, role_description=None, parent_role=None, *args, **kwargs):
self.role_name = role_name
self.role_description = role_description if role_description else ""
def __init__(self, parent_role=None, *args, **kwargs):
self.parent_role = parent_role
kwargs.setdefault('to', 'Role')
@ -104,8 +102,6 @@ class ImplicitRoleField(models.ForeignKey):
def deconstruct(self):
name, path, args, kwargs = super(ImplicitRoleField, self).deconstruct()
kwargs['role_name'] = self.role_name
kwargs['role_description'] = self.role_description
kwargs['parent_role'] = self.parent_role
return name, path, args, kwargs
@ -190,11 +186,7 @@ class ImplicitRoleField(models.ForeignKey):
if cur_role is None:
missing_roles.append(
Role_(
created=now(),
modified=now(),
role_field=implicit_role_field.name,
name=implicit_role_field.role_name,
description=implicit_role_field.role_description,
content_type_id=ct_id,
object_id=instance.id
)
@ -208,7 +200,7 @@ class ImplicitRoleField(models.ForeignKey):
updates[role.role_field] = role.id
role_ids.append(role.id)
type(instance).objects.filter(pk=instance.pk).update(**updates)
Role_._simultaneous_ancestry_rebuild(role_ids)
Role.rebuild_role_ancestor_list(role_ids, [])
# Update parentage if necessary
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
@ -247,12 +239,7 @@ class ImplicitRoleField(models.ForeignKey):
if qs.count() >= 1:
role = qs[0]
else:
role = Role_.objects.create(created=now(),
modified=now(),
role_field=path,
singleton_name=singleton_name,
name=singleton_name,
description=singleton_name)
role = Role_.objects.create(singleton_name=singleton_name, role_field=singleton_name)
parents = [role.id]
else:
parents = resolve_role_field(instance, path)
@ -269,4 +256,4 @@ class ImplicitRoleField(models.ForeignKey):
Role_ = get_current_apps().get_model('main', 'Role')
child_ids = [x for x in Role_.parents.through.objects.filter(to_role_id__in=role_ids).distinct().values_list('from_role_id', flat=True)]
Role_.objects.filter(id__in=role_ids).delete()
Role_._simultaneous_ancestry_rebuild(child_ids)
Role.rebuild_role_ancestor_list([], child_ids)

View File

@ -468,9 +468,7 @@ def load_inventory_source(source, all_group=None, group_filter_re=None,
'''
# Sanity check: We sanitize these module names for our API but Ansible proper doesn't follow
# good naming conventions
if source == 'azure':
source = 'windows_azure'
source = source.replace('azure.py', 'windows_azure.py')
logger.debug('Analyzing type of source: %s', source)
original_all_group = all_group
if not os.path.exists(source):

View File

@ -2,21 +2,21 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
import taggit.managers
import awx.main.fields
class Migration(migrations.Migration):
dependencies = [
('taggit', '0002_auto_20150616_2121'),
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('main', '0007_v300_active_flag_removal'),
]
operations = [
#
# Patch up existing
#
migrations.RenameField(
'Organization',
'admins',
@ -47,300 +47,6 @@ class Migration(migrations.Migration):
name='deprecated_projects',
field=models.ManyToManyField(related_name='deprecated_teams', to='main.Project', blank=True),
),
migrations.CreateModel(
name='RoleAncestorEntry',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('role_field', models.TextField()),
('content_type_id', models.PositiveIntegerField(null=False)),
('object_id', models.PositiveIntegerField(null=False)),
],
options={
'db_table': 'main_rbac_role_ancestors',
'verbose_name_plural': 'role_ancestors',
},
),
migrations.CreateModel(
name='Role',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('name', models.CharField(max_length=512)),
('singleton_name', models.TextField(default=None, unique=True, null=True, db_index=True)),
('object_id', models.PositiveIntegerField(default=None, null=True)),
('ancestors', models.ManyToManyField(related_name='descendents', through='main.RoleAncestorEntry', to='main.Role')),
('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)),
('created_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)),
('modified_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('parents', models.ManyToManyField(related_name='children', to='main.Role')),
('implicit_parents', models.TextField(null=False, default=b'[]')),
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
],
options={
'db_table': 'main_rbac_roles',
'verbose_name_plural': 'roles',
},
),
migrations.AddField(
model_name='roleancestorentry',
name='ancestor',
field=models.ForeignKey(related_name='+', to='main.Role'),
),
migrations.AddField(
model_name='roleancestorentry',
name='descendent',
field=models.ForeignKey(related_name='+', to='main.Role'),
),
migrations.AlterIndexTogether(
name='roleancestorentry',
index_together=set([('ancestor', 'content_type_id', 'object_id'), ('ancestor', 'content_type_id', 'role_field')]),
),
migrations.AddField(
model_name='credential',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Auditor of the credential', parent_role=[b'singleton:System Auditor'], to='main.Role', role_name=b'Credential Auditor', null=b'True'),
),
migrations.AddField(
model_name='credential',
name='owner_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Owner of the credential', parent_role=[b'singleton:System Administrator'], to='main.Role', role_name=b'Credential Owner', null=b'True'),
),
migrations.AddField(
model_name='credential',
name='use_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Credential User', null=b'True'),
),
migrations.AddField(
model_name='custominventoryscript',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'CustomInventory Administrator', null=b'True'),
),
migrations.AddField(
model_name='custominventoryscript',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'CustomInventory Auditor', null=b'True'),
),
migrations.AddField(
model_name='custominventoryscript',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.member_role', to='main.Role', role_name=b'CustomInventory Member', null=b'True'),
),
migrations.AddField(
model_name='group',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', role_name=b'Inventory Group Administrator', null=b'True'),
),
migrations.AddField(
model_name='group',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', role_name=b'Inventory Group Auditor', null=b'True'),
),
migrations.AddField(
model_name='group',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'),
),
migrations.AddField(
model_name='group',
name='update_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Inventory Administrator', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Inventory Auditor', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=None, to='main.Role', role_name=b'Inventory Executor', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='update_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=None, to='main.Role', role_name=b'Inventory Updater', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='use_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True'),
),
migrations.AddField(
model_name='jobtemplate',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=[(b'project.admin_role', b'inventory.admin_role')], to='main.Role', role_name=b'Job Template Administrator', null=b'True'),
),
migrations.AddField(
model_name='jobtemplate',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=[(b'project.auditor_role', b'inventory.auditor_role')], to='main.Role', role_name=b'Job Template Auditor', null=b'True'),
),
migrations.AddField(
model_name='jobtemplate',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=None, to='main.Role', role_name=b'Job Template Runner', null=b'True'),
),
migrations.AddField(
model_name='organization',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage all aspects of this organization', parent_role=b'singleton:System Administrator', to='main.Role', role_name=b'Organization Administrator', null=b'True'),
),
migrations.AddField(
model_name='organization',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this organization', parent_role=b'singleton:System Auditor', to='main.Role', role_name=b'Organization Auditor', null=b'True'),
),
migrations.AddField(
model_name='organization',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this organization', parent_role=b'admin_role', to='main.Role', role_name=b'Organization Member', null=b'True'),
),
migrations.AddField(
model_name='project',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this project', parent_role=[b'organization.admin_role', b'singleton:System Administrator'], to='main.Role', role_name=b'Project Administrator', null=b'True'),
),
migrations.AddField(
model_name='project',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this project', parent_role=[b'organization.auditor_role', b'singleton:System Auditor'], to='main.Role', role_name=b'Project Auditor', null=b'True'),
),
migrations.AddField(
model_name='project',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=None, to='main.Role', role_name=b'Project Member', null=b'True'),
),
migrations.AddField(
model_name='project',
name='scm_update_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update this project from the source control management system', parent_role=b'admin_role', to='main.Role', role_name=b'Project Updater', null=b'True'),
),
migrations.AddField(
model_name='team',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this team', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Team Administrator', null=b'True'),
),
migrations.AddField(
model_name='team',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this team', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Team Auditor', null=b'True'),
),
migrations.AddField(
model_name='team',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', to='main.Role', role_name=b'Team Member', null=b'True'),
),
migrations.AddField(
model_name='credential',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read this credential', parent_role=[b'use_role', b'auditor_role', b'owner_role'], to='main.Role', role_name=b'Credential REad', null=b'True'),
),
migrations.AddField(
model_name='custominventoryscript',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=[b'auditor_role', b'member_role', b'admin_role'], to='main.Role', role_name=b'CustomInventory Read', null=b'True'),
),
migrations.AddField(
model_name='group',
name='adhoc_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute ad hoc commands against this inventory', parent_role=[b'inventory.adhoc_role', b'parents.adhoc_role', b'admin_role'], to='main.Role', role_name=b'Inventory Ad Hoc', null=b'True'),
),
migrations.AddField(
model_name='group',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'execute_role', b'update_role', b'auditor_role', b'admin_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='adhoc_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute ad hoc commands against this inventory', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory Ad Hoc', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view this inventory', parent_role=[b'auditor_role', b'execute_role', b'update_role', b'use_role', b'admin_role'], to='main.Role', role_name=b'Read', null=b'True'),
),
migrations.AddField(
model_name='jobtemplate',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=[b'execute_role', b'auditor_role', b'admin_role'], to='main.Role', role_name=b'Job Template Runner', null=b'True'),
),
migrations.AddField(
model_name='organization',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read an organization', parent_role=[b'member_role', b'auditor_role'], to='main.Role', role_name=b'Organization Read Access', null=b'True'),
),
migrations.AddField(
model_name='project',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read access to this project', parent_role=[b'member_role', b'auditor_role', b'scm_update_role'], to='main.Role', role_name=b'Project Read Access', null=b'True'),
),
migrations.AddField(
model_name='role',
name='role_field',
field=models.TextField(default=b''),
),
migrations.AddField(
model_name='team',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Can view this team', parent_role=[b'admin_role', b'auditor_role', b'member_role'], to='main.Role', role_name=b'Read', null=b'True'),
),
migrations.AlterField(
model_name='credential',
name='use_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=[b'owner_role'], to='main.Role', role_name=b'Credential User', null=b'True'),
),
migrations.AlterField(
model_name='group',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.execute_role', b'adhoc_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'),
),
migrations.AlterField(
model_name='group',
name='update_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.update_role', b'admin_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True'),
),
migrations.AlterField(
model_name='inventory',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=b'adhoc_role', to='main.Role', role_name=b'Inventory Executor', null=b'True'),
),
migrations.AlterField(
model_name='inventory',
name='update_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory Updater', null=b'True'),
),
migrations.AlterField(
model_name='inventory',
name='use_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory User', null=b'True'),
),
migrations.AlterField(
model_name='jobtemplate',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=[b'admin_role'], to='main.Role', role_name=b'Job Template Runner', null=b'True'),
),
migrations.AlterField(
model_name='project',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=b'admin_role', to='main.Role', role_name=b'Project Member', null=b'True'),
),
migrations.RenameField(
model_name='organization',
old_name='projects',
@ -380,4 +86,245 @@ class Migration(migrations.Migration):
name='credential',
unique_together=set([]),
),
#
# New RBAC models and fields
#
migrations.CreateModel(
name='Role',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('role_field', models.TextField()),
('singleton_name', models.TextField(default=None, unique=True, null=True, db_index=True)),
('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)),
('parents', models.ManyToManyField(related_name='children', to='main.Role')),
('implicit_parents', models.TextField(default=b'[]')),
('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)),
('object_id', models.PositiveIntegerField(default=None, null=True)),
],
options={
'db_table': 'main_rbac_roles',
'verbose_name_plural': 'roles',
},
),
migrations.CreateModel(
name='RoleAncestorEntry',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('role_field', models.TextField()),
('content_type_id', models.PositiveIntegerField()),
('object_id', models.PositiveIntegerField()),
('ancestor', models.ForeignKey(related_name='+', to='main.Role')),
('descendent', models.ForeignKey(related_name='+', to='main.Role')),
],
options={
'db_table': 'main_rbac_role_ancestors',
'verbose_name_plural': 'role_ancestors',
},
),
migrations.AddField(
model_name='role',
name='ancestors',
field=models.ManyToManyField(related_name='descendents', through='main.RoleAncestorEntry', to='main.Role'),
),
migrations.AlterIndexTogether(
name='role',
index_together=set([('content_type', 'object_id')]),
),
migrations.AlterIndexTogether(
name='roleancestorentry',
index_together=set([('ancestor', 'content_type_id', 'object_id'), ('ancestor', 'content_type_id', 'role_field'), ('ancestor', 'descendent')]),
),
migrations.AddField(
model_name='credential',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_auditor'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='credential',
name='owner_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_administrator'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='credential',
name='use_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'owner_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='credential',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'use_role', b'auditor_role', b'owner_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='custominventoryscript',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='custominventoryscript',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='custominventoryscript',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.member_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='custominventoryscript',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'auditor_role', b'member_role', b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='group',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='group',
name='adhoc_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.adhoc_role', b'parents.adhoc_role', b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='group',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='group',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.execute_role', b'parents.execute_role', b'adhoc_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='group',
name='update_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.update_role', b'parents.update_role', b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='group',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'execute_role', b'update_role', b'auditor_role', b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='adhoc_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'adhoc_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='update_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='use_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='inventory',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'auditor_role', b'execute_role', b'update_role', b'use_role', b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='jobtemplate',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[(b'project.admin_role', b'inventory.admin_role')], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='jobtemplate',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[(b'project.auditor_role', b'inventory.auditor_role')], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='jobtemplate',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='jobtemplate',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'execute_role', b'auditor_role', b'admin_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='organization',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'singleton:system_administrator', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='organization',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'singleton:system_auditor', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='organization',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='organization',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'member_role', b'auditor_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='project',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'organization.admin_role', b'singleton:system_administrator'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='project',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'organization.auditor_role', b'singleton:system_auditor'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='project',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='project',
name='scm_update_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='project',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'member_role', b'auditor_role', b'scm_update_role'], to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='team',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='team',
name='auditor_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='team',
name='member_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=None, to='main.Role', null=b'True'),
),
migrations.AddField(
model_name='team',
name='read_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role', b'auditor_role', b'member_role'], to='main.Role', null=b'True'),
),
]

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals
from awx.main.migrations import _rbac as rbac
from awx.main.migrations import _migration_utils as migration_utils
from django.db import migrations
@ -12,11 +13,14 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(rbac.init_rbac_migration),
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
migrations.RunPython(rbac.migrate_users),
migrations.RunPython(rbac.create_roles),
migrations.RunPython(rbac.migrate_organization),
migrations.RunPython(rbac.migrate_team),
migrations.RunPython(rbac.migrate_inventory),
migrations.RunPython(rbac.migrate_projects),
migrations.RunPython(rbac.migrate_credential),
migrations.RunPython(rbac.migrate_job_templates),
migrations.RunPython(rbac.rebuild_role_hierarchy),
]

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals
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
@ -12,5 +13,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
migrations.RunPython(ask_for_variables.migrate_credential),
]

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0019_v300_new_azure_credential'),
]
operations = [
migrations.RemoveField(
model_name='job',
name='labels',
),
migrations.RemoveField(
model_name='jobtemplate',
name='labels',
),
migrations.AddField(
model_name='unifiedjob',
name='labels',
field=models.ManyToManyField(related_name='unifiedjob_labels', to='main.Label', blank=True),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='labels',
field=models.ManyToManyField(related_name='unifiedjobtemplate_labels', to='main.Label', blank=True),
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0020_v300_labels_changes'),
]
operations = [
migrations.AddField(
model_name='activitystream',
name='role',
field=models.ManyToManyField(to='main.Role', blank=True),
),
]

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0021_v300_activity_stream'),
]
operations = [
migrations.AddField(
model_name='adhoccommand',
name='extra_vars',
field=models.TextField(default=b'', blank=True),
),
migrations.AlterField(
model_name='credential',
name='kind',
field=models.CharField(default=b'ssh', max_length=32, choices=[(b'ssh', 'Machine'), (b'net', 'Network'), (b'scm', 'Source Control'), (b'aws', 'Amazon Web Services'), (b'rax', 'Rackspace'), (b'vmware', 'VMware vCenter'), (b'foreman', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure Classic (deprecated)'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'openstack', 'OpenStack')]),
),
migrations.AlterField(
model_name='inventorysource',
name='source',
field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'Local File, Directory or Script'), (b'rax', 'Rackspace Cloud Servers'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure Classic (deprecated)'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'foreman', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'custom', 'Custom Script')]),
),
migrations.AlterField(
model_name='inventoryupdate',
name='source',
field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'Local File, Directory or Script'), (b'rax', 'Rackspace Cloud Servers'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure Classic (deprecated)'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'foreman', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'custom', 'Custom Script')]),
),
]

View File

@ -0,0 +1,11 @@
from awx.main.utils import set_current_apps
def set_current_apps_for_migrations(apps, schema_editor):
'''
This is necessary for migrations which do explicit saves on any model that
has an ImplicitRoleFIeld (which generally means anything that has
some RBAC bindings associated with it). This sets the current 'apps' that
the ImplicitRoleFIeld should be using when creating new roles.
'''
set_current_apps(apps)

View File

@ -206,9 +206,9 @@ class UserAccess(BaseAccess):
return qs
return qs.filter(
Q(pk=self.user.pk) |
Q(organizations__in=self.user.deprecated_admin_of_organizations) |
Q(organizations__in=self.user.deprecated_organizations) |
Q(deprecated_teams__in=self.user.deprecated_teams)
Q(deprecated_organizations__in=self.user.deprecated_admin_of_organizations.all()) |
Q(deprecated_organizations__in=self.user.deprecated_organizations.all()) |
Q(deprecated_teams__in=self.user.deprecated_teams.all())
).distinct()
def can_add(self, data):
@ -563,18 +563,18 @@ class CredentialAccess(BaseAccess):
# If the user is a superuser, and therefore can see everything, this
# is also sufficient, and we are done.
qs = self.model.objects.distinct()
qs = qs.select_related('created_by', 'modified_by', 'user', 'team')
qs = qs.select_related('created_by', 'modified_by')
if self.user.is_superuser:
return qs
# Get the list of organizations for which the user is an admin
orgs_as_admin_ids = set(self.user.deprecated_admin_of_organizations.values_list('id', flat=True))
return qs.filter(
Q(user=self.user) |
Q(user__deprecated_organizations__id__in=orgs_as_admin_ids) |
Q(user__deprecated_admin_of_organizations__id__in=orgs_as_admin_ids) |
Q(team__organization__id__in=orgs_as_admin_ids) |
Q(team__deprecated_users__in=[self.user])
Q(deprecated_user=self.user) |
Q(deprecated_user__deprecated_organizations__id__in=orgs_as_admin_ids) |
Q(deprecated_user__deprecated_admin_of_organizations__id__in=orgs_as_admin_ids) |
Q(deprecated_team__organization__id__in=orgs_as_admin_ids) |
Q(deprecated_team__deprecated_users__in=[self.user])
)
def can_add(self, data):
@ -597,22 +597,22 @@ class CredentialAccess(BaseAccess):
return False
if self.user == obj.created_by:
return True
if obj.user:
if self.user == obj.user:
if obj.deprecated_user:
if self.user == obj.deprecated_user:
return True
if obj.user.deprecated_organizations.filter(deprecated_admins__in=[self.user]).exists():
if obj.deprecated_user.deprecated_organizations.filter(deprecated_admins__in=[self.user]).exists():
return True
if obj.user.deprecated_admin_of_organizations.filter(deprecated_admins__in=[self.user]).exists():
if obj.deprecated_user.deprecated_admin_of_organizations.filter(deprecated_admins__in=[self.user]).exists():
return True
if obj.team:
if self.user in obj.team.organization.deprecated_admins.all():
if obj.deprecated_team:
if self.user in obj.deprecated_team.organization.deprecated_admins.all():
return True
return False
def can_delete(self, obj):
# Unassociated credentials may be marked deleted by anyone, though we
# shouldn't ever end up with those.
if obj.user is None and obj.team is None:
if obj.deprecated_user is None and obj.deprecated_team is None:
return True
return self.can_change(obj, None)

View File

@ -1,11 +1,12 @@
import logging
from time import time
from django.utils.encoding import smart_text
from django.db.models import Q
from django.utils.timezone import now
from collections import defaultdict
from awx.main.utils import getattrd, set_current_apps
from awx.main.utils import getattrd
from awx.main.models.rbac import Role, batch_role_ancestor_rebuilding
import _old_access as old_access
logger = logging.getLogger(__name__)
@ -27,8 +28,35 @@ def log_migration(wrapped):
return wrapper
@log_migration
def init_rbac_migration(apps, schema_editor):
set_current_apps(apps)
def create_roles(apps, schema_editor):
'''
Implicit role creation happens in our post_save hook for all of our
resources. Here we iterate through all of our resource types and call
.save() to ensure all that happens for every object in the system before we
get busy with the actual migration work.
This gets run after migrate_users, which does role creation for users a
little differently.
'''
models = [
apps.get_model('main', m) for m in [
'Organization',
'Team',
'Inventory',
'Group',
'Project',
'Credential',
'CustomInventoryScript',
'JobTemplate',
]
]
with batch_role_ancestor_rebuilding():
for model in models:
for obj in model.objects.iterator():
obj.save()
@log_migration
def migrate_users(apps, schema_editor):
@ -44,9 +72,7 @@ def migrate_users(apps, schema_editor):
logger.info(smart_text(u"found existing role for user: {}".format(user.username)))
except Role.DoesNotExist:
role = Role.objects.create(
created=now(),
modified=now(),
singleton_name = smart_text(u'{}-admin_role'.format(user.username)),
role_field='admin_role',
content_type = user_content_type,
object_id = user.id
)
@ -54,14 +80,12 @@ def migrate_users(apps, schema_editor):
logger.info(smart_text(u"migrating to new role for user: {}".format(user.username)))
if user.is_superuser:
if Role.objects.filter(singleton_name='System Administrator').exists():
sa_role = Role.objects.get(singleton_name='System Administrator')
if Role.objects.filter(singleton_name='system_administrator').exists():
sa_role = Role.objects.get(singleton_name='system_administrator')
else:
sa_role = Role.objects.create(
created=now(),
modified=now(),
singleton_name='System Administrator',
name='System Administrator'
singleton_name='system_administrator',
role_field='system_administrator'
)
sa_role.members.add(user)
@ -71,19 +95,17 @@ def migrate_users(apps, schema_editor):
def migrate_organization(apps, schema_editor):
Organization = apps.get_model('main', "Organization")
for org in Organization.objects.iterator():
org.save() # force creates missing roles
for admin in org.deprecated_admins.all():
org.admin_role.members.add(admin)
logger.info(smart_text(u"added admin: {}, {}".format(org.name, admin.username)))
for user in org.deprecated_users.all():
org.auditor_role.members.add(user)
logger.info(smart_text(u"added auditor: {}, {}".format(org.name, user.username)))
org.member_role.members.add(user)
logger.info(smart_text(u"added member: {}, {}".format(org.name, user.username)))
@log_migration
def migrate_team(apps, schema_editor):
Team = apps.get_model('main', 'Team')
for t in Team.objects.iterator():
t.save()
for user in t.deprecated_users.all():
t.member_role.members.add(user)
logger.info(smart_text(u"team: {}, added user: {}".format(t.name, user.username)))
@ -103,8 +125,6 @@ def attrfunc(attr_path):
def _update_credential_parents(org, cred):
org.admin_role.children.add(cred.owner_role)
org.member_role.children.add(cred.use_role)
cred.deprecated_user, cred.deprecated_team = None, None
cred.save()
def _discover_credentials(instances, cred, orgfunc):
@ -122,7 +142,12 @@ def _discover_credentials(instances, cred, orgfunc):
'''
orgs = defaultdict(list)
for inst in instances:
orgs[orgfunc(inst)].append(inst)
try:
orgs[orgfunc(inst)].append(inst)
except AttributeError:
# JobTemplate.inventory can be NULL sometimes, eg when an inventory
# has been deleted. This protects against that.
pass
if len(orgs) == 1:
_update_credential_parents(orgfunc(instances[0]), cred)
@ -136,7 +161,6 @@ def _discover_credentials(instances, cred, orgfunc):
cred.save()
# Unlink the old information from the new credential
cred.deprecated_user, cred.deprecated_team = None, None
cred.owner_role, cred.use_role = None, None
cred.save()
@ -150,43 +174,32 @@ def migrate_credential(apps, schema_editor):
Credential = apps.get_model('main', "Credential")
JobTemplate = apps.get_model('main', 'JobTemplate')
Project = apps.get_model('main', 'Project')
Role = apps.get_model('main', 'Role')
User = apps.get_model('auth', 'User')
InventorySource = apps.get_model('main', 'InventorySource')
ContentType = apps.get_model('contenttypes', "ContentType")
user_content_type = ContentType.objects.get_for_model(User)
for cred in Credential.objects.iterator():
cred.save()
results = (JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all() or
InventorySource.objects.filter(credential=cred).all())
if results:
results = [x for x in JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all()] + \
[x for x in InventorySource.objects.filter(credential=cred).all()]
if cred.deprecated_team is not None and results:
if len(results) == 1:
_update_credential_parents(results[0].inventory.organization, cred)
else:
_discover_credentials(results, cred, attrfunc('inventory.organization'))
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
continue
projs = Project.objects.filter(credential=cred).all()
if projs:
if cred.deprecated_team is not None and projs:
if len(projs) == 1:
_update_credential_parents(projs[0].organization, cred)
else:
_discover_credentials(projs, cred, attrfunc('organization'))
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
continue
if cred.deprecated_team is not None:
cred.deprecated_team.admin_role.children.add(cred.owner_role)
cred.deprecated_team.member_role.children.add(cred.use_role)
cred.deprecated_user, cred.deprecated_team = None, None
cred.deprecated_team.member_role.children.add(cred.owner_role)
cred.save()
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host)))
elif cred.deprecated_user is not None:
user_admin_role = Role.objects.get(content_type=user_content_type, object_id=cred.deprecated_user.id)
user_admin_role.children.add(cred.owner_role)
cred.deprecated_user, cred.deprecated_team = None, None
cred.owner_role.members.add(cred.deprecated_user)
cred.save()
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host, )))
else:
@ -205,14 +218,13 @@ def migrate_inventory(apps, schema_editor):
return inventory.auditor_role
elif perm.permission_type == 'write':
return inventory.update_role
elif perm.permission_type == 'check' or perm.permission_type == 'run':
elif perm.permission_type == 'check' or perm.permission_type == 'run' or perm.permission_type == 'create':
# These permission types are handled differntly in RBAC now, nothing to migrate.
return False
else:
return None
for inventory in Inventory.objects.iterator():
inventory.save()
for perm in Permission.objects.filter(inventory=inventory):
role = None
execrole = None
@ -260,7 +272,6 @@ def migrate_projects(apps, schema_editor):
# Migrate projects to single organizations, duplicating as necessary
for project in Project.objects.iterator():
project.save()
original_project_name = project.name
project_orgs = project.deprecated_organizations.distinct().all()
@ -373,7 +384,6 @@ def migrate_job_templates(apps, schema_editor):
Permission = apps.get_model('main', 'Permission')
for jt in JobTemplate.objects.iterator():
jt.save()
permission = Permission.objects.filter(
inventory=jt.inventory,
project=jt.project,
@ -390,7 +400,7 @@ def migrate_job_templates(apps, schema_editor):
jt.execute_role.members.add(user)
logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name)))
if user in jt.execute_role:
if jt.execute_role.ancestors.filter(members=user).exists(): # aka "user in jt.execute_role"
# If the job template is already accessible by the user, because they
# are a sytem, organization, or project admin, then don't add an explicit
# role entry for them
@ -399,3 +409,22 @@ def migrate_job_templates(apps, schema_editor):
if old_access.check_user_access(user, jt.__class__, 'start', jt, False):
jt.execute_role.members.add(user)
logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name)))
@log_migration
def rebuild_role_hierarchy(apps, schema_editor):
logger.info('Computing role roots..')
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))
start = time()
Role.rebuild_role_ancestor_list(roots, [])
stop = time()
logger.info('Rebuild completed in %f seconds' % (stop - start))
logger.info('Done.')

View File

@ -56,6 +56,7 @@ class ActivityStream(models.Model):
notifier = models.ManyToManyField("Notifier", blank=True)
notification = models.ManyToManyField("Notification", blank=True)
label = models.ManyToManyField("Label", blank=True)
role = models.ManyToManyField("Role", blank=True)
def get_absolute_url(self):
return reverse('api:activity_stream_detail', args=(self.pk,))

View File

@ -84,11 +84,17 @@ class AdHocCommand(UnifiedJob):
editable=False,
through='AdHocCommandEvent',
)
extra_vars = models.TextField(
blank=True,
default='',
)
extra_vars_dict = VarsDictProperty('extra_vars', True)
def clean_inventory(self):
inv = self.inventory
if not inv:
raise ValidationError('Inventory is no longer available.')
raise ValidationError('No valid inventory.')
return inv
def clean_credential(self):

View File

@ -204,27 +204,19 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
help_text=_('Tenant identifier for this credential'),
)
owner_role = ImplicitRoleField(
role_name='Credential Owner',
role_description='Owner of the credential',
parent_role=[
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
],
)
auditor_role = ImplicitRoleField(
role_name='Credential Auditor',
role_description='Auditor of the credential',
parent_role=[
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
],
)
use_role = ImplicitRoleField(
role_name='Credential User',
role_description='May use this credential, but not read sensitive portions or modify it',
parent_role=['owner_role']
)
read_role = ImplicitRoleField(
role_name='Credential REad',
role_description='May read this credential',
parent_role=[
'use_role', 'auditor_role', 'owner_role'
],

View File

@ -97,39 +97,25 @@ class Inventory(CommonModel, ResourceMixin):
help_text=_('Number of external inventory sources in this inventory with failures.'),
)
admin_role = ImplicitRoleField(
role_name='Inventory Administrator',
role_description='May manage this inventory',
parent_role='organization.admin_role',
)
auditor_role = ImplicitRoleField(
role_name='Inventory Auditor',
role_description='May view but not modify this inventory',
parent_role='organization.auditor_role',
)
update_role = ImplicitRoleField(
role_name='Inventory Updater',
role_description='May update the inventory',
parent_role=['admin_role'],
)
use_role = ImplicitRoleField(
role_name='Inventory User',
role_description='May use this inventory, but not read sensitive portions or modify it',
parent_role=['admin_role'],
)
adhoc_role = ImplicitRoleField(
role_name='Inventory Ad Hoc',
role_description='May execute ad hoc commands against this inventory',
parent_role=['admin_role'],
)
execute_role = ImplicitRoleField(
role_name='Inventory Executor',
role_description='May execute jobs against this inventory',
parent_role='adhoc_role',
)
read_role = ImplicitRoleField(
role_name='Read',
parent_role=['auditor_role', 'execute_role', 'update_role', 'use_role', 'admin_role'],
role_description='May view this inventory',
)
def get_absolute_url(self):
@ -335,7 +321,7 @@ class Inventory(CommonModel, ResourceMixin):
return self.groups.exclude(parents__pk__in=group_pks).distinct()
class Host(CommonModelNameNotUnique, ResourceMixin):
class Host(CommonModelNameNotUnique):
'''
A managed node
'''
@ -531,28 +517,21 @@ class Group(CommonModelNameNotUnique, ResourceMixin):
help_text=_('Inventory source(s) that created or modified this group.'),
)
admin_role = ImplicitRoleField(
role_name='Inventory Group Administrator',
parent_role=['inventory.admin_role', 'parents.admin_role'],
)
auditor_role = ImplicitRoleField(
role_name='Inventory Group Auditor',
parent_role=['inventory.auditor_role', 'parents.auditor_role'],
)
update_role = ImplicitRoleField(
role_name='Inventory Group Updater',
parent_role=['inventory.update_role', 'parents.update_role', 'admin_role'],
)
adhoc_role = ImplicitRoleField(
role_name='Inventory Ad Hoc',
parent_role=['inventory.adhoc_role', 'parents.adhoc_role', 'admin_role'],
role_description='May execute ad hoc commands against this inventory',
)
execute_role = ImplicitRoleField(
role_name='Inventory Group Executor',
parent_role=['inventory.execute_role', 'parents.execute_role', 'adhoc_role'],
)
read_role = ImplicitRoleField(
role_name='Inventory Group Executor',
parent_role=['execute_role', 'update_role', 'auditor_role', 'admin_role'],
)
@ -1321,25 +1300,15 @@ class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
)
admin_role = ImplicitRoleField(
role_name='CustomInventory Administrator',
role_description='May manage this inventory',
parent_role='organization.admin_role',
)
member_role = ImplicitRoleField(
role_name='CustomInventory Member',
role_description='May view but not modify this inventory',
parent_role='organization.member_role',
)
auditor_role = ImplicitRoleField(
role_name='CustomInventory Auditor',
role_description='May view but not modify this inventory',
parent_role='organization.auditor_role',
)
read_role = ImplicitRoleField(
role_name='CustomInventory Read',
role_description='May view but not modify this inventory',
parent_role=['auditor_role', 'member_role', 'admin_role'],
)

View File

@ -135,11 +135,6 @@ class JobOptions(BaseModel):
become_enabled = models.BooleanField(
default=False,
)
labels = models.ManyToManyField(
"Label",
blank=True,
related_name='%(class)s_labels'
)
extra_vars_dict = VarsDictProperty('extra_vars', True)
@ -226,23 +221,15 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
default={},
)
admin_role = ImplicitRoleField(
role_name='Job Template Administrator',
role_description='Full access to all settings',
parent_role=[('project.admin_role', 'inventory.admin_role')]
)
auditor_role = ImplicitRoleField(
role_name='Job Template Auditor',
role_description='Read-only access to all settings',
parent_role=[('project.auditor_role', 'inventory.auditor_role')]
)
execute_role = ImplicitRoleField(
role_name='Job Template Runner',
role_description='May run the job template',
parent_role=['admin_role'],
)
read_role = ImplicitRoleField(
role_name='Job Template Runner',
role_description='May run the job template',
parent_role=['execute_role', 'auditor_role', 'admin_role'],
)
@ -523,6 +510,36 @@ class Job(UnifiedJob, JobOptions):
return self.job_template.ask_variables_on_launch
return False
@property
def ask_limit_on_launch(self):
if self.job_template is not None:
return self.job_template.ask_limit_on_launch
return False
@property
def ask_tags_on_launch(self):
if self.job_template is not None:
return self.job_template.ask_tags_on_launch
return False
@property
def ask_job_type_on_launch(self):
if self.job_template is not None:
return self.job_template.ask_job_type_on_launch
return False
@property
def ask_inventory_on_launch(self):
if self.job_template is not None:
return self.job_template.ask_inventory_on_launch
return False
@property
def ask_credential_on_launch(self):
if self.job_template is not None:
return self.job_template.ask_credential_on_launch
return False
def get_passwords_needed_to_start(self):
return self.passwords_needed_to_start

View File

@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
# AWX
from awx.main.models.base import CommonModelNameNotUnique
from awx.main.models.unified_jobs import UnifiedJobTemplate, UnifiedJob
__all__ = ('Label', )
@ -39,3 +40,19 @@ class Label(CommonModelNameNotUnique):
jobtemplate_labels__isnull=True
)
def is_detached(self):
return bool(
Label.objects.filter(
id=self.id,
unifiedjob_labels__isnull=True,
unifiedjobtemplate_labels__isnull=True
).count())
def is_candidate_for_detach(self):
c1 = UnifiedJob.objects.filter(labels__in=[self.id]).count()
c2 = UnifiedJobTemplate.objects.filter(labels__in=[self.id]).count()
if (c1 + c2 - 1) == 0:
return True
else:
return False

View File

@ -53,23 +53,15 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin):
related_name='deprecated_organizations',
)
admin_role = ImplicitRoleField(
role_name='Organization Administrator',
role_description='May manage all aspects of this organization',
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
)
auditor_role = ImplicitRoleField(
role_name='Organization Auditor',
role_description='May read all settings associated with this organization',
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
)
member_role = ImplicitRoleField(
role_name='Organization Member',
role_description='A member of this organization',
parent_role='admin_role',
)
read_role = ImplicitRoleField(
role_name='Organization Read Access',
role_description='Read an organization',
parent_role=['member_role', 'auditor_role'],
)
@ -110,22 +102,13 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
related_name='deprecated_teams',
)
admin_role = ImplicitRoleField(
role_name='Team Administrator',
role_description='May manage this team',
parent_role='organization.admin_role',
)
auditor_role = ImplicitRoleField(
role_name='Team Auditor',
role_description='May read all settings associated with this team',
parent_role='organization.auditor_role',
)
member_role = ImplicitRoleField(
role_name='Team Member',
role_description='A member of this team',
)
member_role = ImplicitRoleField()
read_role = ImplicitRoleField(
role_name='Read',
role_description='Can view this team',
parent_role=['admin_role', 'auditor_role', 'member_role'],
)

View File

@ -221,34 +221,24 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
blank=True,
)
admin_role = ImplicitRoleField(
role_name='Project Administrator',
role_description='May manage this project',
parent_role=[
'organization.admin_role',
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
],
)
auditor_role = ImplicitRoleField(
role_name='Project Auditor',
role_description='May read all settings associated with this project',
parent_role=[
'organization.auditor_role',
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
],
)
member_role = ImplicitRoleField(
role_name='Project Member',
role_description='Implies membership within this project',
parent_role='admin_role',
)
scm_update_role = ImplicitRoleField(
role_name='Project Updater',
role_description='May update this project from the source control management system',
parent_role='admin_role',
)
read_role = ImplicitRoleField(
role_name='Project Read Access',
role_description='Read access to this project',
parent_role=['member_role', 'auditor_role', 'scm_update_role'],
)

View File

@ -8,7 +8,6 @@ import contextlib
# Django
from django.db import models, transaction, connection
from django.db.models import Q
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType
@ -29,8 +28,39 @@ __all__ = [
logger = logging.getLogger('awx.main.models.rbac')
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator'
ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor'
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='system_administrator'
ROLE_SINGLETON_SYSTEM_AUDITOR='system_auditor'
role_names = {
'system_administrator' : 'System Administrator',
'system_auditor' : 'System Auditor',
'adhoc_role' : 'Ad Hoc',
'admin_role' : 'Admin',
'auditor_role' : 'Auditor',
'execute_role' : 'Execute',
'member_role' : 'Member',
'owner_role' : 'Owner',
'read_role' : 'Read',
'scm_update_role' : 'SCM Update',
'update_role' : 'Update',
'use_role' : 'Use',
}
role_descriptions = {
'system_administrator' : '[TODO] System Administrator',
'system_auditor' : '[TODO] System Auditor',
'adhoc_role' : '[TODO] Ad Hoc',
'admin_role' : '[TODO] Admin',
'auditor_role' : '[TODO] Auditor',
'execute_role' : '[TODO] Execute',
'member_role' : '[TODO] Member',
'owner_role' : '[TODO] Owner',
'read_role' : '[TODO] Read',
'scm_update_role' : '[TODO] SCM Update',
'update_role' : '[TODO] Update',
'use_role' : '[TODO] Use',
}
tls = threading.local() # thread local storage
@ -51,23 +81,22 @@ def batch_role_ancestor_rebuilding(allow_nesting=False):
try:
setattr(tls, 'batch_role_rebuilding', True)
if not batch_role_rebuilding:
setattr(tls, 'roles_needing_rebuilding', set())
setattr(tls, 'additions', set())
setattr(tls, 'removals', set())
yield
finally:
setattr(tls, 'batch_role_rebuilding', batch_role_rebuilding)
if not batch_role_rebuilding:
rebuild_set = getattr(tls, 'roles_needing_rebuilding')
additions = getattr(tls, 'additions')
removals = getattr(tls, 'removals')
with transaction.atomic():
Role._simultaneous_ancestry_rebuild(list(rebuild_set))
#for role in Role.objects.filter(id__in=list(rebuild_set)).all():
# # TODO: We can reduce this to one rebuild call with our new upcoming rebuild method.. do this
# role.rebuild_role_ancestor_list()
delattr(tls, 'roles_needing_rebuilding')
Role.rebuild_role_ancestor_list(list(additions), list(removals))
delattr(tls, 'additions')
delattr(tls, 'removals')
class Role(CommonModelNameNotUnique):
class Role(models.Model):
'''
Role model
'''
@ -76,9 +105,12 @@ class Role(CommonModelNameNotUnique):
app_label = 'main'
verbose_name_plural = _('roles')
db_table = 'main_rbac_roles'
index_together = [
("content_type", "object_id")
]
role_field = models.TextField(null=False)
singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True)
role_field = models.TextField(null=False, default='')
parents = models.ManyToManyField('Role', related_name='children')
implicit_parents = models.TextField(null=False, default='[]')
ancestors = models.ManyToManyField(
@ -94,7 +126,7 @@ class Role(CommonModelNameNotUnique):
def save(self, *args, **kwargs):
super(Role, self).save(*args, **kwargs)
self.rebuild_role_ancestor_list()
self.rebuild_role_ancestor_list([self.id], [])
def get_absolute_url(self):
return reverse('api:role_detail', args=(self.pk,))
@ -112,20 +144,36 @@ class Role(CommonModelNameNotUnique):
object_id=accessor.id)
return self.ancestors.filter(pk__in=roles).exists()
def rebuild_role_ancestor_list(self):
@property
def name(self):
global role_names
return role_names[self.role_field]
@property
def description(self):
global role_descriptions
return role_descriptions[self.role_field]
@staticmethod
def rebuild_role_ancestor_list(additions, removals):
'''
Updates our `ancestors` map to accurately reflect all of the ancestors for a role
You should never need to call this. Signal handlers should be calling
this method when the role hierachy changes automatically.
Note that this method relies on any parents' ancestor list being correct.
'''
Role._simultaneous_ancestry_rebuild([self.id])
@staticmethod
def _simultaneous_ancestry_rebuild(role_ids_to_rebuild):
# The ancestry table
# =================================================
#
# The role ancestors table denormalizes the parental relations
# between all roles in the system. If you have role A which is a
# parent of B which is a parent of C, then the ancestors table will
# contain a row noting that B is a descendent of A, and two rows for
# denoting that C is a descendent of both A and B. In addition to
# storing entries for each descendent relationship, we also store an
# entry that states that C is a 'descendent' of itself, C. This makes
# usage of this table simple in our queries as it enables us to do
# straight joins where we would have to do unions otherwise.
#
# The simple version of what this function is doing
# =================================================
@ -163,37 +211,18 @@ class Role(CommonModelNameNotUnique):
#
# SQL Breakdown
# =============
# The Role ancestors has three columns, (id, from_role_id, to_role_id)
#
# id: Unqiue row ID
# from_role_id: Descendent role ID
# to_role_id: Ancestor role ID
#
# *NOTE* In addition to mapping roles to parents, there also
# always exists must exist an entry where
#
# from_role_id == role_id == to_role_id
#
# this makes our joins simple when we go to derive permissions or
# accessible objects.
#
#
# We operate under the assumption that our parent's ancestor list is
# correct, thus we can always compute what our ancestor list should
# be by taking the union of our parent's ancestor lists and adding
# our self reference entry from_role_id == role_id == to_role_id
# our self reference entry where ancestor_id = descendent_id
#
# The inner query for the two SQL statements compute this union,
# the union of the parent's ancestors and the self referncing entry,
# for all roles in the current set of roles to rebuild.
# The DELETE query deletes all entries in the ancestor table that
# should no longer be there (as determined by the NOT EXISTS query,
# which checks to see if the ancestor is still an ancestor of one
# or more of our parents)
#
# The DELETE query uses this to select all entries on disk for the
# roles we're dealing with, and removes the entries that are not in
# this list.
#
# The INSERT query uses this to select all entries in the list that
# are not in the database yet, and inserts all of the missing
# records.
# The INSERT query computes the list of what our ancestor maps should
# be, and inserts any missing entries.
#
# Once complete, we select all of the children for the roles we are
# working with, this list becomes the new role list we are working
@ -205,18 +234,17 @@ class Role(CommonModelNameNotUnique):
#
#
if len(role_ids_to_rebuild) == 0:
if len(additions) == 0 and len(removals) == 0:
return
global tls
batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False)
if batch_role_rebuilding:
roles_needing_rebuilding = getattr(tls, 'roles_needing_rebuilding')
roles_needing_rebuilding.update(set(role_ids_to_rebuild))
getattr(tls, 'additions').update(set(additions))
getattr(tls, 'removals').update(set(removals))
return
cursor = connection.cursor()
loop_ct = 0
@ -226,94 +254,143 @@ class Role(CommonModelNameNotUnique):
'roles_table': Role._meta.db_table,
}
# SQLlite has a 1M sql statement limit.. since the django sqllite
# driver isn't letting us pass in the ids through the preferred
# parameter binding system, this function exists to obey this.
# est max 12 bytes per number, used up to 2 times in a query,
# minus 4k of padding for the other parts of the query, leads us
# to the magic number of 41496, or 40000 for a nice round number
def split_ids_for_sqlite(role_ids):
for i in xrange(0, len(role_ids), 999):
yield role_ids[i:i + 999]
while role_ids_to_rebuild:
if loop_ct > 1000:
raise Exception('Ancestry role rebuilding error: infinite loop detected')
loop_ct += 1
delete_ct = 0
for ids in split_ids_for_sqlite(role_ids_to_rebuild):
sql_params['ids'] = ','.join(str(x) for x in ids)
cursor.execute('''
DELETE FROM %(ancestors_table)s
WHERE descendent_id IN (%(ids)s)
AND
id NOT IN (
SELECT %(ancestors_table)s.id FROM (
SELECT parents.from_role_id from_id, ancestors.ancestor_id to_id
FROM %(parents_table)s as parents
LEFT JOIN %(ancestors_table)s as ancestors
ON (parents.to_role_id = ancestors.descendent_id)
WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL
UNION
SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s)
) new_ancestry_list
LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id
AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id)
WHERE %(ancestors_table)s.id IS NOT NULL
)
''' % sql_params)
delete_ct += cursor.rowcount
insert_ct = 0
for ids in split_ids_for_sqlite(role_ids_to_rebuild):
sql_params['ids'] = ','.join(str(x) for x in ids)
cursor.execute('''
INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id)
SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM (
SELECT parents.from_role_id from_id,
ancestors.ancestor_id to_id,
roles.role_field,
COALESCE(roles.content_type_id, 0) content_type_id,
COALESCE(roles.object_id, 0) object_id
FROM %(parents_table)s as parents
INNER JOIN %(roles_table)s as roles ON (parents.from_role_id = roles.id)
LEFT OUTER JOIN %(ancestors_table)s as ancestors
ON (parents.to_role_id = ancestors.descendent_id)
WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL
UNION
SELECT id from_id,
id to_id,
role_field,
COALESCE(content_type_id, 0) content_type_id,
COALESCE(object_id, 0) object_id
from %(roles_table)s WHERE id IN (%(ids)s)
) new_ancestry_list
LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id
AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id)
WHERE %(ancestors_table)s.id IS NULL
''' % sql_params)
insert_ct += cursor.rowcount
if insert_ct == 0 and delete_ct == 0:
break
new_role_ids_to_rebuild = set()
for ids in split_ids_for_sqlite(role_ids_to_rebuild):
sql_params['ids'] = ','.join(str(x) for x in ids)
new_role_ids_to_rebuild.update(set(Role.objects.distinct()
.filter(id__in=ids, children__id__isnull=False)
.values_list('children__id', flat=True)))
role_ids_to_rebuild = list(new_role_ids_to_rebuild)
for i in xrange(0, len(role_ids), 40000):
yield role_ids[i:i + 40000]
with transaction.atomic():
while len(additions) > 0 or len(removals) > 0:
if loop_ct > 100:
raise Exception('Role ancestry rebuilding error: infinite loop detected')
loop_ct += 1
delete_ct = 0
if len(removals) > 0:
for ids in split_ids_for_sqlite(removals):
sql_params['ids'] = ','.join(str(x) for x in ids)
cursor.execute('''
DELETE FROM %(ancestors_table)s
WHERE descendent_id IN (%(ids)s)
AND descendent_id != ancestor_id
AND NOT EXISTS (
SELECT 1
FROM %(parents_table)s as parents
INNER JOIN %(ancestors_table)s as inner_ancestors
ON (parents.to_role_id = inner_ancestors.descendent_id)
WHERE parents.from_role_id = %(ancestors_table)s.descendent_id
AND %(ancestors_table)s.ancestor_id = inner_ancestors.ancestor_id
)
''' % sql_params)
delete_ct += cursor.rowcount
insert_ct = 0
if len(additions) > 0:
for ids in split_ids_for_sqlite(additions):
sql_params['ids'] = ','.join(str(x) for x in ids)
cursor.execute('''
INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id)
SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM (
SELECT roles.id from_id,
ancestors.ancestor_id to_id,
roles.role_field,
COALESCE(roles.content_type_id, 0) content_type_id,
COALESCE(roles.object_id, 0) object_id
FROM %(roles_table)s as roles
INNER JOIN %(parents_table)s as parents
ON (parents.from_role_id = roles.id)
INNER JOIN %(ancestors_table)s as ancestors
ON (parents.to_role_id = ancestors.descendent_id)
WHERE roles.id IN (%(ids)s)
UNION
SELECT id from_id,
id to_id,
role_field,
COALESCE(content_type_id, 0) content_type_id,
COALESCE(object_id, 0) object_id
from %(roles_table)s WHERE id IN (%(ids)s)
) new_ancestry_list
WHERE NOT EXISTS (
SELECT 1 FROM %(ancestors_table)s
WHERE %(ancestors_table)s.descendent_id = new_ancestry_list.from_id
AND %(ancestors_table)s.ancestor_id = new_ancestry_list.to_id
)
''' % sql_params)
insert_ct += cursor.rowcount
if insert_ct == 0 and delete_ct == 0:
break
new_additions = set()
for ids in split_ids_for_sqlite(additions):
sql_params['ids'] = ','.join(str(x) for x in ids)
# get all children for the roles we're operating on
cursor.execute('SELECT DISTINCT from_role_id FROM %(parents_table)s WHERE to_role_id IN (%(ids)s)' % sql_params)
new_additions.update([row[0] for row in cursor.fetchall()])
additions = list(new_additions)
new_removals = set()
for ids in split_ids_for_sqlite(removals):
sql_params['ids'] = ','.join(str(x) for x in ids)
# get all children for the roles we're operating on
cursor.execute('SELECT DISTINCT from_role_id FROM %(parents_table)s WHERE to_role_id IN (%(ids)s)' % sql_params)
new_removals.update([row[0] for row in cursor.fetchall()])
removals = list(new_removals)
@staticmethod
def visible_roles(user):
return Role.objects.filter(Q(descendents__in=user.roles.filter()) | Q(ancestors__in=user.roles.filter()))
sql_params = {
'ancestors_table': Role.ancestors.through._meta.db_table,
'parents_table': Role.parents.through._meta.db_table,
'roles_table': Role._meta.db_table,
'ids': ','.join(str(x) for x in user.roles.values_list('id', flat=True))
}
qs = Role.objects.extra(
where = ['''
%(roles_table)s.id IN (
SELECT descendent_id FROM %(ancestors_table)s WHERE ancestor_id IN (%(ids)s)
UNION
SELECT ancestor_id FROM %(ancestors_table)s WHERE descendent_id IN (%(ids)s)
)
''' % sql_params]
)
return qs
@staticmethod
def filter_visible_roles(user, roles_qs):
sql_params = {
'ancestors_table': Role.ancestors.through._meta.db_table,
'parents_table': Role.parents.through._meta.db_table,
'roles_table': Role._meta.db_table,
'ids': ','.join(str(x) for x in user.roles.all().values_list('id', flat=True))
}
qs = roles_qs.extra(
where = ['''
EXISTS (
SELECT 1 FROM
%(ancestors_table)s
WHERE (descendent_id = %(roles_table)s.id AND ancestor_id IN (%(ids)s))
OR (ancestor_id = %(roles_table)s.id AND descendent_id IN (%(ids)s))
) ''' % sql_params]
)
return qs
@staticmethod
def singleton(name):
role, _ = Role.objects.get_or_create(singleton_name=name, name=name)
role, _ = Role.objects.get_or_create(singleton_name=name, role_field=name)
return role
def is_ancestor_of(self, role):
@ -328,6 +405,7 @@ class RoleAncestorEntry(models.Model):
index_together = [
("ancestor", "content_type_id", "object_id"), # used by get_roles_on_resource
("ancestor", "content_type_id", "role_field"), # used by accessible_objects
("ancestor", "descendent"), # used by rebuild_role_ancestor_list in the NOT EXISTS clauses.
]
descendent = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+')
@ -359,5 +437,5 @@ def get_roles_on_resource(resource, accessor):
ancestor__in=roles,
content_type_id=ContentType.objects.get_for_model(resource).id,
object_id=resource.id
).values_list('role_field', flat=True)
).values_list('role_field', flat=True).distinct()
]

View File

@ -141,6 +141,11 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
default='ok',
editable=False,
)
labels = models.ManyToManyField(
"Label",
blank=True,
related_name='%(class)s_labels'
)
def get_absolute_url(self):
real_instance = self.get_real_instance()
@ -476,6 +481,12 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
default='',
editable=False,
)
labels = models.ManyToManyField(
"Label",
blank=True,
related_name='%(class)s_labels'
)
def get_absolute_url(self):
real_instance = self.get_real_instance()

View File

@ -43,6 +43,8 @@ class SlackBackend(TowerBaseEmailBackend):
for m in messages:
try:
for r in m.recipients():
if r.startswith('#'):
r = r[1:]
self.connection.rtm_send_message(r, m.subject)
sent_messages += 1
except Exception as e:

View File

@ -3,7 +3,7 @@
import logging
from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
logger = logging.getLogger('awx.main.registrar')
@ -17,12 +17,12 @@ class ActivityStreamRegistrar(object):
if not getattr(tower_settings, 'ACTIVITY_STREAM_ENABLED', True):
return
from awx.main.signals import activity_stream_create, activity_stream_update, activity_stream_delete, activity_stream_associate
if model not in self.models:
self.models.append(model)
post_save.connect(activity_stream_create, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_create")
pre_save.connect(activity_stream_update, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_update")
post_delete.connect(activity_stream_delete, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_delete")
pre_delete.connect(activity_stream_delete, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_delete")
for m2mfield in model._meta.many_to_many:
try:
@ -36,7 +36,7 @@ class ActivityStreamRegistrar(object):
if model in self.models:
post_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_create")
pre_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_update")
post_delete.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_delete")
pre_delete.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_delete")
self.models.pop(model)

View File

@ -108,12 +108,17 @@ def emit_update_inventory_on_created_or_deleted(sender, **kwargs):
def rebuild_role_ancestor_list(reverse, model, instance, pk_set, action, **kwargs):
'When a role parent is added or removed, update our role hierarchy list'
if action in ['post_add', 'post_remove', 'post_clear']:
if action == 'post_add':
if reverse:
for id in pk_set:
model.objects.get(id=id).rebuild_role_ancestor_list()
model.rebuild_role_ancestor_list(list(pk_set), [])
else:
instance.rebuild_role_ancestor_list()
model.rebuild_role_ancestor_list([instance.id], [])
if action in ['post_remove', 'post_clear']:
if reverse:
model.rebuild_role_ancestor_list([], list(pk_set))
else:
model.rebuild_role_ancestor_list([], [instance.id])
def sync_superuser_status_to_rbac(instance, **kwargs):
'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role'
@ -127,11 +132,10 @@ def create_user_role(instance, **kwargs):
Role.objects.get(
content_type=ContentType.objects.get_for_model(instance),
object_id=instance.id,
name = 'User Admin'
role_field='admin_role'
)
except Role.DoesNotExist:
role = Role.objects.create(
name = 'User Admin',
role_field='admin_role',
content_object = instance,
)
@ -152,6 +156,24 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs):
if action == 'pre_remove':
instance.content_object.admin_role.children.remove(user.admin_role)
def rbac_activity_stream(instance, sender, **kwargs):
user_type = ContentType.objects.get_for_model(User)
# Only if we are associating/disassociating
if kwargs['action'] in ['pre_add', 'pre_remove']:
# Only if this isn't for the User.admin_role
if hasattr(instance, 'content_type'):
if instance.content_type in [None, user_type]:
return
role = instance
instance = instance.content_object
else:
role = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']).first()
activity_stream_associate(sender, instance, role=role, **kwargs)
def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs):
for l in instance.labels.all():
if l.is_candidate_for_detach():
l.delete()
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
@ -169,9 +191,11 @@ post_save.connect(emit_job_event_detail, sender=JobEvent)
post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent)
m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through)
m2m_changed.connect(org_admin_edit_members, Role.members.through)
m2m_changed.connect(rbac_activity_stream, Role.members.through)
post_save.connect(sync_superuser_status_to_rbac, sender=User)
post_save.connect(create_user_role, sender=User)
pre_delete.connect(cleanup_detached_labels_on_deleted_parent, sender=UnifiedJob)
pre_delete.connect(cleanup_detached_labels_on_deleted_parent, sender=UnifiedJobTemplate)
# Migrate hosts, groups to parent group(s) whenever a group is deleted
@ -331,14 +355,10 @@ def activity_stream_update(sender, instance, **kwargs):
def activity_stream_delete(sender, instance, **kwargs):
if not activity_stream_enabled:
return
try:
old = sender.objects.get(id=instance.id)
except sender.DoesNotExist:
return
# Skip recording any inventory source directly associated with a group.
if isinstance(instance, InventorySource) and instance.group:
return
changes = model_instance_diff(old, instance)
changes = model_to_dict(instance)
object1 = camelcase_to_underscore(instance.__class__.__name__)
activity_entry = ActivityStream(
operation='delete',
@ -349,7 +369,7 @@ def activity_stream_delete(sender, instance, **kwargs):
def activity_stream_associate(sender, instance, **kwargs):
if not activity_stream_enabled:
return
if 'pre_add' in kwargs['action'] or 'pre_remove' in kwargs['action']:
if kwargs['action'] in ['pre_add', 'pre_remove']:
if kwargs['action'] == 'pre_add':
action = 'associate'
elif kwargs['action'] == 'pre_remove':
@ -378,6 +398,23 @@ def activity_stream_associate(sender, instance, **kwargs):
getattr(activity_entry, object1).add(obj1)
getattr(activity_entry, object2).add(obj2_actual)
# Record the role for RBAC changes
if 'role' in kwargs:
role = kwargs['role']
if role.content_object is not None:
obj_rel = '.'.join([role.content_object.__module__,
role.content_object.__class__.__name__,
role.role_field])
# 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)
activity_entry.role.add(role)
activity_entry.object_relationship_type = obj_rel
activity_entry.save()
@receiver(current_user_getter)
def get_current_user_from_drf_request(sender, **kwargs):

View File

@ -208,7 +208,7 @@ def handle_work_success(self, result, task_actual):
notification_body = instance.notification_data()
notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name,
task_actual['id'],
instance_name,
smart_text(instance_name),
notification_body['url'])
notification_body['friendly_name'] = friendly_name
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
@ -246,8 +246,8 @@ def handle_work_error(self, task_id, subtasks=None):
instance_name = instance.module_name
notifiers = []
friendly_name = "AdHoc Command"
elif task_actual['type'] == 'system_job':
instance = SystemJob.objects.get(id=task_actual['id'])
elif each_task['type'] == 'system_job':
instance = SystemJob.objects.get(id=each_task['id'])
instance_name = instance.system_job_template.name
notifiers = instance.system_job_template.notifiers
friendly_name = "System Job"
@ -270,7 +270,7 @@ def handle_work_error(self, task_id, subtasks=None):
notification_body = first_task.notification_data()
notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name,
first_task_id,
first_task_name,
smart_text(first_task_name),
notification_body['url'])
notification_body['friendly_name'] = first_task_friendly_name
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
@ -558,7 +558,7 @@ class BaseTask(Task):
instance = self.update_model(instance.pk)
if instance.cancel_flag:
try:
if tower_settings.AWX_PROOT_ENABLED:
if tower_settings.AWX_PROOT_ENABLED and self.should_use_proot(instance):
# NOTE: Refactor this once we get a newer psutil across the board
if not psutil:
os.kill(child.pid, signal.SIGKILL)
@ -1615,6 +1615,9 @@ class RunAdHocCommand(BaseTask):
if ad_hoc_command.verbosity:
args.append('-%s' % ('v' * min(5, ad_hoc_command.verbosity)))
if ad_hoc_command.extra_vars_dict:
args.extend(['-e', json.dumps(ad_hoc_command.extra_vars_dict)])
args.extend(['-m', ad_hoc_command.module_name])
args.extend(['-a', ad_hoc_command.module_args])

View File

@ -59,3 +59,29 @@ def test_middleware_actor_added(monkeypatch, post, get, user):
assert response.status_code == 200
assert response.data['summary_fields']['actor']['username'] == 'admin-poster'
@pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled")
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_rbac_stream_resource_roles(mocker, organization, user):
member = user('test', False)
organization.admin_role.members.add(member)
activity_stream = ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first()
assert activity_stream.user.first() == member
assert activity_stream.organization.first() == organization
assert activity_stream.role.first() == organization.admin_role
assert activity_stream.object_relationship_type == 'awx.main.models.organization.Organization.admin_role'
@pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled")
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_rbac_stream_user_roles(mocker, organization, user):
member = user('test', False)
member.roles.add(organization.admin_role)
activity_stream = ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first()
assert activity_stream.user.first() == member
assert activity_stream.organization.first() == organization
assert activity_stream.role.first() == organization.admin_role
assert activity_stream.object_relationship_type == 'awx.main.models.organization.Organization.admin_role'

View File

@ -24,6 +24,7 @@ def test_create_user_credential_via_credentials_list(post, get, alice):
@pytest.mark.django_db
def test_create_user_credential_via_user_credentials_list(post, get, alice):
response = post(reverse('api:user_credentials_list', args=(alice.pk,)), {
'user': alice.pk,
'name': 'Some name',
'username': 'someusername',
}, alice)
@ -45,6 +46,7 @@ def test_create_user_credential_via_credentials_list_xfail(post, alice, bob):
@pytest.mark.django_db
def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob):
response = post(reverse('api:user_credentials_list', args=(bob.pk,)), {
'user': bob.pk,
'name': 'Some name',
'username': 'someusername'
}, alice)
@ -71,6 +73,7 @@ def test_create_team_credential(post, get, team, org_admin, team_member):
@pytest.mark.django_db
def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member):
response = post(reverse('api:team_credentials_list', args=(team.pk,)), {
'team': team.pk,
'name': 'Some name',
'username': 'someusername',
}, org_admin)

View File

@ -22,6 +22,10 @@ def runtime_data(organization):
credential=cred_obj.pk,
)
@pytest.fixture
def job_with_links(machine_credential, inventory):
return Job.objects.create(name='existing-job', credential=machine_credential, inventory=inventory)
@pytest.fixture
def job_template_prompts(project, inventory, machine_credential):
def rf(on_off):
@ -155,7 +159,7 @@ def test_job_reject_invalid_prompted_extra_vars(runtime_data, job_template_promp
dict(extra_vars='{"unbalanced brackets":'), user('admin', True))
assert response.status_code == 400
assert response.data['extra_vars'] == ['Must be valid JSON or YAML']
assert response.data['extra_vars'] == ['Must be a valid JSON or YAML dictionary']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
@ -167,51 +171,72 @@ 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
def test_job_launch_fails_without_inventory_access(job_template_prompts, runtime_data, machine_credential, post, user, mocker):
def test_job_launch_fails_without_inventory_access(job_template_prompts, runtime_data, post, user):
job_template = job_template_prompts(True)
common_user = user('test-user', False)
job_template.execute_role.members.add(common_user)
# Assure that the base job template can be launched to begin with
mock_job = mocker.MagicMock(spec=Job, id=968, **runtime_data)
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.create_unified_job', return_value=mock_job):
with mocker.patch('awx.api.serializers.JobSerializer.to_representation'):
response = post(reverse('api:job_template_launch',
args=[job_template.pk]), {}, common_user)
assert response.status_code == 201
# Assure that giving an inventory without access to the inventory blocks the launch
new_inv = job_template.project.organization.inventories.create(name="user-can-not-use")
runtime_inventory = Inventory.objects.get(pk=runtime_data['inventory'])
response = post(reverse('api:job_template_launch', args=[job_template.pk]),
dict(inventory=new_inv.pk), common_user)
dict(inventory=runtime_inventory.pk), common_user)
assert response.status_code == 403
assert response.data['detail'] == u'You do not have permission to perform this action.'
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_relaunch_copy_vars(runtime_data, job_template_prompts, project, post, mocker):
def test_job_launch_fails_without_credential_access(job_template_prompts, runtime_data, post, user):
job_template = job_template_prompts(True)
common_user = user('test-user', False)
job_template.execute_role.members.add(common_user)
# Create a job with the given data that will be relaunched
job_create_kwargs = runtime_data
inv_obj = Inventory.objects.get(pk=job_create_kwargs.pop('inventory'))
cred_obj = Credential.objects.get(pk=job_create_kwargs.pop('credential'))
original_job = Job.objects.create(inventory=inv_obj, credential=cred_obj, job_template=job_template, **job_create_kwargs)
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate._get_unified_job_field_names', return_value=runtime_data.keys()):
second_job = original_job.copy()
# Assure that giving a credential without access blocks the launch
runtime_credential = Credential.objects.get(pk=runtime_data['credential'])
response = post(reverse('api:job_template_launch', args=[job_template.pk]),
dict(credential=runtime_credential.pk), common_user)
assert response.status_code == 403
assert response.data['detail'] == u'You do not have permission to perform this action.'
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_relaunch_copy_vars(job_with_links, machine_credential, inventory,
deploy_jobtemplate, post, mocker):
job_with_links.job_template = deploy_jobtemplate
job_with_links.limit = "my_server"
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate._get_unified_job_field_names',
return_value=['inventory', 'credential', 'limit']):
second_job = job_with_links.copy()
# Check that job data matches the original variables
assert 'job_launch_var' in yaml.load(second_job.extra_vars)
assert original_job.limit == second_job.limit
assert original_job.job_type == second_job.job_type
assert original_job.inventory.pk == second_job.inventory.pk
assert original_job.job_tags == second_job.job_tags
assert second_job.credential == job_with_links.credential
assert second_job.inventory == job_with_links.inventory
assert second_job.limit == 'my_server'
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_relaunch_resource_access(job_with_links, user):
inventory_user = user('user1', False)
credential_user = user('user2', False)
both_user = user('user3', False)
# Confirm that a user with inventory & credential access can launch
job_with_links.credential.use_role.members.add(both_user)
job_with_links.inventory.use_role.members.add(both_user)
assert both_user.can_access(Job, 'start', job_with_links)
# Confirm that a user with credential access alone can not launch
job_with_links.credential.use_role.members.add(credential_user)
assert not credential_user.can_access(Job, 'start', job_with_links)
# Confirm that a user with inventory access alone can not launch
job_with_links.inventory.use_role.members.add(inventory_user)
assert not inventory_user.can_access(Job, 'start', job_with_links)
@pytest.mark.django_db
def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):

View File

@ -150,6 +150,26 @@ def test_two_organizations(resourced_organization, organizations, user, get):
assert counts[org_id_full] == COUNTS_PRIMES
assert counts[org_id_zero] == COUNTS_ZEROS
@pytest.mark.django_db
def test_scan_JT_counted(resourced_organization, user, get):
admin_user = user('admin', True)
# Add a scan job template to the org
resourced_organization.projects.all()[0].jobtemplates.create(
job_type='scan', inventory=resourced_organization.inventories.all()[0],
name='scan-job-template')
counts_dict = COUNTS_PRIMES
counts_dict['job_templates'] += 1
# Test list view
list_response = get(reverse('api:organization_list', args=[]), admin_user)
assert list_response.status_code == 200
assert list_response.data['results'][0]['summary_fields']['related_field_counts'] == counts_dict
# Test detail view
detail_response = get(reverse('api:organization_detail', args=[resourced_organization.pk]), admin_user)
assert detail_response.status_code == 200
assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict
@pytest.mark.django_db
def test_JT_associated_with_project(organizations, project, user, get):
# Check that adding a project to an organization gets the project's JT

View File

@ -1,6 +1,7 @@
import pytest
from django.core.urlresolvers import reverse
from awx.main.models import Role
@pytest.mark.django_db
def test_indirect_access_list(get, organization, project, team_factory, user, admin):
@ -53,5 +54,5 @@ def test_indirect_access_list(get, organization, project, team_factory, user, ad
assert org_admin_team_member_entry['team_name'] == org_admin_team.name
admin_entry = admin_res['summary_fields']['indirect_access'][0]['role']
assert admin_entry['name'] == 'System Administrator'
assert admin_entry['name'] == Role.singleton('system_administrator').name

View File

@ -36,7 +36,6 @@ from awx.main.models.organization import (
Team,
)
from awx.main.models.rbac import Role
from awx.main.models.notifications import Notifier
'''
@ -193,11 +192,6 @@ def notifier(organization):
notification_type="webhook",
notification_configuration=dict(url="http://localhost",
headers={"Test": "Header"}))
@pytest.fixture
def role():
return Role.objects.create(name='role')
@pytest.fixture
def admin(user):
return user('admin', True)

View File

@ -10,6 +10,10 @@ def mock_feature_enabled(feature, bypass_database=None):
#@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.fixture
def role():
return Role.objects.create()
#
# /roles
@ -85,13 +89,14 @@ def test_get_user_roles_list(get, admin):
response = get(url, admin)
assert response.status_code == 200
roles = response.data
assert roles['count'] > 0 # 'System Administrator' role if nothing else
assert roles['count'] > 0 # 'system_administrator' role if nothing else
@pytest.mark.django_db
def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob):
'Users can see roles for other users, but only the roles that that user has access to see as well'
organization.member_role.members.add(alice)
organization.admin_role.members.add(bob)
organization.member_role.members.add(bob)
custom_role = Role.objects.create(name='custom_role-test_user_view_admin_roles_list')
organization.member_role.children.add(custom_role)
team.member_role.members.add(bob)

View File

@ -11,8 +11,8 @@ from awx.main.models import (
@pytest.mark.django_db
def test_auto_inheritance_by_children(organization, alice):
A = Role.objects.create(name='A', role_field='')
B = Role.objects.create(name='B', role_field='')
A = Role.objects.create()
B = Role.objects.create()
A.members.add(alice)
assert alice not in organization.admin_role
@ -38,8 +38,8 @@ def test_auto_inheritance_by_children(organization, alice):
@pytest.mark.django_db
def test_auto_inheritance_by_parents(organization, alice):
A = Role.objects.create(name='A')
B = Role.objects.create(name='B')
A = Role.objects.create()
B = Role.objects.create()
A.members.add(alice)
assert alice not in organization.admin_role
@ -58,9 +58,9 @@ def test_auto_inheritance_by_parents(organization, alice):
@pytest.mark.django_db
def test_accessible_objects(organization, alice, bob):
A = Role.objects.create(name='A')
A = Role.objects.create()
A.members.add(alice)
B = Role.objects.create(name='B')
B = Role.objects.create()
B.members.add(alice)
B.members.add(bob)
@ -118,7 +118,7 @@ def test_auto_field_adjustments(organization, inventory, team, alice):
def test_implicit_deletes(alice):
'Ensures implicit resources and roles delete themselves'
delorg = Organization.objects.create(name='test-org')
child = Role.objects.create(name='child-role')
child = Role.objects.create()
child.parents.add(delorg.admin_role)
delorg.admin_role.members.add(alice)
@ -129,14 +129,14 @@ def test_implicit_deletes(alice):
assert Role.objects.filter(id=admin_role_id).count() == 1
assert Role.objects.filter(id=auditor_role_id).count() == 1
n_alice_roles = alice.roles.count()
n_system_admin_children = Role.singleton('System Administrator').children.count()
n_system_admin_children = Role.singleton('system_administrator').children.count()
delorg.delete()
assert Role.objects.filter(id=admin_role_id).count() == 0
assert Role.objects.filter(id=auditor_role_id).count() == 0
assert alice.roles.count() == (n_alice_roles - 1)
assert Role.singleton('System Administrator').children.count() == (n_system_admin_children - 1)
assert Role.singleton('system_administrator').children.count() == (n_system_admin_children - 1)
assert child.ancestors.count() == 1
assert child.ancestors.all()[0] == child
@ -152,11 +152,11 @@ def test_content_object(user):
def test_hierarchy_rebuilding_multi_path():
'Tests a subdtle cases around role hierarchy rebuilding when you have multiple paths to the same role of different length'
X = Role.objects.create(name='X')
A = Role.objects.create(name='A')
B = Role.objects.create(name='B')
C = Role.objects.create(name='C')
D = Role.objects.create(name='D')
X = Role.objects.create()
A = Role.objects.create()
B = Role.objects.create()
C = Role.objects.create()
D = Role.objects.create()
A.children.add(B)
A.children.add(D)

View File

@ -27,7 +27,7 @@ def test_credential_use_role(credential, user, permissions):
@pytest.mark.django_db
def test_credential_migration_team_member(credential, team, user, permissions):
u = user('user', False)
team.admin_role.members.add(u)
team.member_role.members.add(u)
credential.deprecated_team = team
credential.save()
@ -91,7 +91,8 @@ def test_credential_access_admin(user, team, credential):
assert access.can_change(credential, {'user': u.pk})
@pytest.mark.django_db
def test_cred_job_template(user, deploy_jobtemplate):
def test_cred_job_template_xfail(user, deploy_jobtemplate):
' Personal credential migration '
a = user('admin', False)
org = deploy_jobtemplate.project.organization
org.admin_role.members.add(a)
@ -102,19 +103,17 @@ def test_cred_job_template(user, deploy_jobtemplate):
access = CredentialAccess(a)
rbac.migrate_credential(apps, None)
assert access.can_change(cred, {'organization': org.pk})
org.admin_role.members.remove(a)
assert not access.can_change(cred, {'organization': org.pk})
@pytest.mark.django_db
def test_cred_multi_job_template_single_org(user, deploy_jobtemplate):
def test_cred_job_template(user, team, deploy_jobtemplate):
' Team credential migration => org credential '
a = user('admin', False)
org = deploy_jobtemplate.project.organization
org.admin_role.members.add(a)
cred = deploy_jobtemplate.credential
cred.deprecated_user = user('john', False)
cred.deprecated_team = team
cred.save()
access = CredentialAccess(a)
@ -125,8 +124,42 @@ def test_cred_multi_job_template_single_org(user, deploy_jobtemplate):
assert not access.can_change(cred, {'organization': org.pk})
@pytest.mark.django_db
def test_single_cred_multi_job_template_multi_org(user, organizations, credential):
def test_cred_multi_job_template_single_org_xfail(user, deploy_jobtemplate):
a = user('admin', False)
org = deploy_jobtemplate.project.organization
org.admin_role.members.add(a)
cred = deploy_jobtemplate.credential
cred.deprecated_user = user('john', False)
cred.save()
access = CredentialAccess(a)
rbac.migrate_credential(apps, None)
assert not access.can_change(cred, {'organization': org.pk})
@pytest.mark.django_db
def test_cred_multi_job_template_single_org(user, team, deploy_jobtemplate):
a = user('admin', False)
org = deploy_jobtemplate.project.organization
org.admin_role.members.add(a)
cred = deploy_jobtemplate.credential
cred.deprecated_team = team
cred.save()
access = CredentialAccess(a)
rbac.migrate_credential(apps, None)
assert access.can_change(cred, {'organization': org.pk})
org.admin_role.members.remove(a)
assert not access.can_change(cred, {'organization': org.pk})
@pytest.mark.django_db
def test_single_cred_multi_job_template_multi_org(user, organizations, credential, team):
orgs = organizations(2)
credential.deprecated_team = team
credential.save()
jts = []
for org in orgs:
inv = org.inventories.create(name="inv-%d" % org.pk)
@ -169,7 +202,7 @@ def test_cred_inventory_source(user, inventory, credential):
assert u not in credential.use_role
rbac.migrate_credential(apps, None)
assert u in credential.use_role
assert u not in credential.use_role
@pytest.mark.django_db
def test_cred_project(user, credential, project):
@ -181,7 +214,7 @@ def test_cred_project(user, credential, project):
assert u not in credential.use_role
rbac.migrate_credential(apps, None)
assert u in credential.use_role
assert u not in credential.use_role
@pytest.mark.django_db
def test_cred_no_org(user, credential):

View File

@ -87,7 +87,7 @@ def test_job_template_team_migration_check(deploy_jobtemplate, check_jobtemplate
rbac.migrate_projects(apps, None)
rbac.migrate_inventory(apps, None)
assert joe in check_jobtemplate.read_role
assert joe not in check_jobtemplate.read_role
assert admin in check_jobtemplate.execute_role
assert joe not in check_jobtemplate.execute_role
@ -120,12 +120,13 @@ def test_job_template_team_deploy_migration(deploy_jobtemplate, check_jobtemplat
rbac.migrate_projects(apps, None)
rbac.migrate_inventory(apps, None)
assert joe in deploy_jobtemplate.read_role
assert joe not in deploy_jobtemplate.read_role
assert admin in deploy_jobtemplate.execute_role
assert joe not in deploy_jobtemplate.execute_role
rbac.migrate_job_templates(apps, None)
assert joe in deploy_jobtemplate.read_role
assert admin in deploy_jobtemplate.execute_role
assert joe in deploy_jobtemplate.execute_role

View File

@ -148,7 +148,7 @@ def test_project_user_project(user_project, project, user):
def test_project_accessible_by_sa(user, project):
u = user('systemadmin', is_superuser=True)
# This gets setup by a signal, but we want to test the migration which will set this up too, so remove it
Role.singleton('System Administrator').members.remove(u)
Role.singleton('system_administrator').members.remove(u)
assert u not in project.read_role
rbac.migrate_organization(apps, None)

View File

@ -3,6 +3,25 @@ import pytest
from awx.main.access import TeamAccess
from awx.main.models import Project
@pytest.mark.django_db
def test_team_attach_unattach(team, user):
u = user('member', False)
access = TeamAccess(u)
team.member_role.members.add(u)
assert not access.can_attach(team, u.admin_role, 'member_role.children', None)
assert not access.can_unattach(team, u.admin_role, 'member_role.children')
team.admin_role.members.add(u)
assert access.can_attach(team, u.admin_role, 'member_role.children', None)
assert access.can_unattach(team, u.admin_role, 'member_role.children')
u2 = user('non-member', False)
access = TeamAccess(u2)
assert not access.can_attach(team, u2.admin_role, 'member_role.children', None)
assert not access.can_unattach(team, u2.admin_role, 'member_role.chidlren')
@pytest.mark.django_db
def test_team_access_superuser(team, user):
team.member_role.members.add(user('member', False))

View File

@ -13,7 +13,7 @@ def test_user_admin(user_project, project, user):
joe = user(username, is_superuser = False)
admin = user('admin', is_superuser = True)
sa = Role.singleton('System Administrator')
sa = Role.singleton('system_administrator')
# this should happen automatically with our signal
assert sa.members.filter(id=admin.id).exists() is True

View File

@ -0,0 +1,17 @@
import pytest
from awx.api.filters import FieldLookupBackend
from awx.main.models import JobTemplate
@pytest.mark.parametrize(u"empty_value", [u'', ''])
def test_empty_in(empty_value):
field_lookup = FieldLookupBackend()
with pytest.raises(ValueError) as excinfo:
field_lookup.value_to_python(JobTemplate, 'project__in', empty_value)
assert 'empty value for __in' in str(excinfo.value)
@pytest.mark.parametrize(u"valid_value", [u'foo', u'foo,'])
def test_valid_in(valid_value):
field_lookup = FieldLookupBackend()
value, new_lookup = field_lookup.value_to_python(JobTemplate, 'project__in', valid_value)
assert 'foo' in value

View File

@ -1,13 +1,14 @@
# Python
import pytest
import mock
# DRF
from rest_framework import status
from rest_framework.response import Response
# AWX
from awx.api.generics import ParentMixin, SubListCreateAttachDetachAPIView
from awx.api.generics import ParentMixin, SubListCreateAttachDetachAPIView, DeleteLastUnattachLabelMixin
@pytest.fixture
def get_object_or_404(mocker):
@ -37,7 +38,7 @@ def parent_relationship_factory(mocker):
return (serializer, mock_parent_relationship)
return rf
# TODO: Test create and associate failure (i.e. id doesn't exist or record already exists)
# TODO: Test create and associate failure (i.e. id doesn't exist, record already exists, permission denied)
# TODO: Mock and check return (Response)
class TestSubListCreateAttachDetachAPIView:
def test_attach_create_and_associate(self, mocker, get_object_or_400, parent_relationship_factory, mock_response_new):
@ -65,6 +66,97 @@ class TestSubListCreateAttachDetachAPIView:
mock_parent_relationship.wife.add.assert_called_with(get_object_or_400.return_value)
mock_response_new.assert_called_with(Response, status=status.HTTP_204_NO_CONTENT)
def test_unattach_validate_ok(self, mocker):
mock_request = mocker.MagicMock(data=dict(id=1))
serializer = SubListCreateAttachDetachAPIView()
(sub_id, res) = serializer.unattach_validate(mock_request)
assert sub_id == 1
assert res is None
def test_unattach_validate_missing_id(self, mocker):
mock_request = mocker.MagicMock(data=dict())
serializer = SubListCreateAttachDetachAPIView()
(sub_id, res) = serializer.unattach_validate(mock_request)
assert sub_id is None
assert type(res) is Response
def test_unattach_by_id_ok(self, mocker, parent_relationship_factory, get_object_or_400):
(serializer, mock_parent_relationship) = parent_relationship_factory(SubListCreateAttachDetachAPIView, 'wife')
mock_request = mocker.MagicMock()
mock_sub = mocker.MagicMock(name="object to unattach")
get_object_or_400.return_value = mock_sub
res = serializer.unattach_by_id(mock_request, 1)
assert type(res) is Response
assert res.status_code == status.HTTP_204_NO_CONTENT
mock_parent_relationship.wife.remove.assert_called_with(mock_sub)
def test_unattach_ok(self, mocker):
mock_request = mocker.MagicMock()
mock_sub_id = mocker.MagicMock()
view = SubListCreateAttachDetachAPIView()
view.unattach_validate = mocker.MagicMock()
view.unattach_by_id = mocker.MagicMock()
view.unattach_validate.return_value = (mock_sub_id, None)
view.unattach(mock_request)
view.unattach_validate.assert_called_with(mock_request)
view.unattach_by_id.assert_called_with(mock_request, mock_sub_id)
def test_unattach_invalid(self, mocker):
mock_request = mocker.MagicMock()
mock_res = mocker.MagicMock()
view = SubListCreateAttachDetachAPIView()
view.unattach_validate = mocker.MagicMock()
view.unattach_by_id = mocker.MagicMock()
view.unattach_validate.return_value = (None, mock_res)
view.unattach(mock_request)
view.unattach_validate.assert_called_with(mock_request)
view.unattach_by_id.assert_not_called()
class TestDeleteLastUnattachLabelMixin:
@mock.patch('awx.api.generics.super')
def test_unattach_ok(self, super, mocker):
mock_request = mocker.MagicMock()
mock_sub_id = mocker.MagicMock()
super.return_value = super
super.unattach_validate = mocker.MagicMock(return_value=(mock_sub_id, None))
super.unattach_by_id = mocker.MagicMock()
mock_label = mocker.patch('awx.api.generics.Label')
mock_label.objects.get.return_value = mock_label
mock_label.is_detached.return_value = True
view = DeleteLastUnattachLabelMixin()
view.unattach(mock_request, None, None)
super.unattach_validate.assert_called_with(mock_request, None, None)
super.unattach_by_id.assert_called_with(mock_request, mock_sub_id)
mock_label.is_detached.assert_called_with()
mock_label.objects.get.assert_called_with(id=mock_sub_id)
mock_label.delete.assert_called_with()
@mock.patch('awx.api.generics.super')
def test_unattach_fail(self, super, mocker):
mock_request = mocker.MagicMock()
mock_response = mocker.MagicMock()
super.return_value = super
super.unattach_validate = mocker.MagicMock(return_value=(None, mock_response))
view = DeleteLastUnattachLabelMixin()
res = view.unattach(mock_request, None, None)
super.unattach_validate.assert_called_with(mock_request, None, None)
assert mock_response == res
class TestParentMixin:
def test_get_parent_object(self, mocker, get_object_or_404):
parent_mixin = ParentMixin()

View File

@ -1,8 +1,9 @@
# Python
import pytest
# AWX
from awx.api.views import ApiV1RootView
from awx.api.views import (
ApiV1RootView,
)
@pytest.fixture
def mock_response_new(mocker):
@ -10,6 +11,7 @@ def mock_response_new(mocker):
m.return_value = m
return m
class TestApiV1RootView:
def test_get_endpoints(self, mocker, mock_response_new):
endpoints = [

View File

@ -1,4 +1,8 @@
import pytest
from awx.main.models.label import Label
from awx.main.models.unified_jobs import UnifiedJobTemplate, UnifiedJob
def test_get_orphaned_labels(mocker):
mock_query_set = mocker.MagicMock()
@ -8,3 +12,54 @@ def test_get_orphaned_labels(mocker):
assert mock_query_set == ret
Label.objects.filter.assert_called_with(organization=None, jobtemplate_labels__isnull=True)
def test_is_detached(mocker):
mock_query_set = mocker.MagicMock()
Label.objects.filter = mocker.MagicMock(return_value=mock_query_set)
mock_query_set.count.return_value = 1
label = Label(id=37)
ret = label.is_detached()
assert ret is True
Label.objects.filter.assert_called_with(id=37, unifiedjob_labels__isnull=True, unifiedjobtemplate_labels__isnull=True)
mock_query_set.count.assert_called_with()
def test_is_detached_not(mocker):
mock_query_set = mocker.MagicMock()
Label.objects.filter = mocker.MagicMock(return_value=mock_query_set)
mock_query_set.count.return_value = 0
label = Label(id=37)
ret = label.is_detached()
assert ret is False
Label.objects.filter.assert_called_with(id=37, unifiedjob_labels__isnull=True, unifiedjobtemplate_labels__isnull=True)
mock_query_set.count.assert_called_with()
@pytest.mark.parametrize("jt_count,j_count,expected", [
(1, 0, True),
(0, 1, True),
(1, 1, False),
])
def test_is_candidate_for_detach(mocker, jt_count, j_count, expected):
mock_job_qs = mocker.MagicMock()
mock_job_qs.count = mocker.MagicMock(return_value=j_count)
UnifiedJob.objects = mocker.MagicMock()
UnifiedJob.objects.filter = mocker.MagicMock(return_value=mock_job_qs)
mock_jt_qs = mocker.MagicMock()
mock_jt_qs.count = mocker.MagicMock(return_value=jt_count)
UnifiedJobTemplate.objects = mocker.MagicMock()
UnifiedJobTemplate.objects.filter = mocker.MagicMock(return_value=mock_jt_qs)
label = Label(id=37)
ret = label.is_candidate_for_detach()
UnifiedJob.objects.filter.assert_called_with(labels__in=[label.id])
UnifiedJobTemplate.objects.filter.assert_called_with(labels__in=[label.id])
mock_job_qs.count.assert_called_with()
mock_jt_qs.count.assert_called_with()
assert ret is expected

View File

@ -0,0 +1,21 @@
from django.contrib.auth.models import User
from awx.main.access import (
BaseAccess,
check_superuser,
)
def test_superuser(mocker):
user = mocker.MagicMock(spec=User, id=1, is_superuser=True)
access = BaseAccess(user)
can_add = check_superuser(BaseAccess.can_add)
assert can_add(access, None) is True
def test_not_superuser(mocker):
user = mocker.MagicMock(spec=User, id=1, is_superuser=False)
access = BaseAccess(user)
can_add = check_superuser(BaseAccess.can_add)
assert can_add(access, None) is False

View File

@ -0,0 +1,17 @@
from awx.main import signals
class TestCleanupDetachedLabels:
def test_cleanup_detached_labels_on_deleted_parent(self, mocker):
mock_labels = [mocker.MagicMock(), mocker.MagicMock()]
mock_instance = mocker.MagicMock()
mock_instance.labels.all = mocker.MagicMock()
mock_instance.labels.all.return_value = mock_labels
mock_labels[0].is_candidate_for_detach.return_value = True
mock_labels[1].is_candidate_for_detach.return_value = False
signals.cleanup_detached_labels_on_deleted_parent(None, mock_instance)
mock_labels[0].is_candidate_for_detach.assert_called_with()
mock_labels[1].is_candidate_for_detach.assert_called_with()
mock_labels[0].delete.assert_called_with()
mock_labels[1].delete.assert_not_called()

View File

@ -70,7 +70,7 @@ import psutil
# pass
# statsd = NoStatsClient()
CENSOR_FIELD_WHITELIST=[
CENSOR_FIELD_WHITELIST = [
'msg',
'failed',
'changed',
@ -80,7 +80,6 @@ CENSOR_FIELD_WHITELIST=[
'delta',
'cmd',
'_ansible_no_log',
'cmd',
'rc',
'failed_when_result',
'skipped',
@ -114,6 +113,7 @@ def censor(obj, no_log=False):
obj['results'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
return obj
class TokenAuth(requests.auth.AuthBase):
def __init__(self, token):
@ -194,31 +194,7 @@ class BaseCallbackModule(object):
self._init_connection()
if self.context is None:
self._start_connection()
if 'res' in event_data and hasattr(event_data['res'], 'get') \
and event_data['res'].get('_ansible_no_log', False):
res = event_data['res']
if 'stdout' in res and res['stdout']:
res['stdout'] = '<censored>'
if 'stdout_lines' in res and res['stdout_lines']:
res['stdout_lines'] = ['<censored>']
if 'stderr' in res and res['stderr']:
res['stderr'] = '<censored>'
if 'stderr_lines' in res and res['stderr_lines']:
res['stderr_lines'] = ['<censored>']
if res.get('cmd', None) and re.search(r'\s', res['cmd']):
res['cmd'] = re.sub(r'^(([^\s\\]|\\\s)+).*$',
r'\1 <censored>',
res['cmd'])
if 'invocation' in res \
and 'module_args' in res['invocation'] \
and '_raw_params' in res['invocation']['module_args'] \
and re.search(r'\s',
res['invocation']['module_args']['_raw_params']):
res['invocation']['module_args']['_raw_params'] = \
re.sub(r'^(([^\s\\]|\\\s)+).*$',
r'\1 <censored>',
res['invocation']['module_args']['_raw_params'])
msg['event_data']['res'] = res
self.socket.send_json(msg)
self.socket.recv()
return

View File

@ -32,7 +32,6 @@
import sys
import os
import time
from copy import deepcopy
from ansible import constants as C
try:
from ansible.cache.base import BaseCacheModule
@ -50,7 +49,7 @@ class CacheModule(BaseCacheModule):
def __init__(self, *args, **kwargs):
# Basic in-memory caching for typical runs
self._cache = {}
self._cache_prev = {}
self._all_keys = {}
# This is the local tower zmq connection
self._tower_connection = C.CACHE_PLUGIN_CONNECTION
@ -72,12 +71,10 @@ class CacheModule(BaseCacheModule):
def identify_new_module(self, key, value):
# Return the first key found that doesn't exist in the
# previous set of facts
if key in self._cache_prev:
value_old = self._cache_prev[key]
for k,v in value.iteritems():
if k not in value_old:
if not k.startswith('ansible_'):
return k
if key in self._all_keys:
for k in value.iterkeys():
if k not in self._all_keys[key] and not k.startswith('ansible_'):
return k
# First time we have seen facts from this host
# it's either ansible facts or a module facts (including module_setup)
elif len(value) == 1:
@ -110,7 +107,7 @@ class CacheModule(BaseCacheModule):
# Assume ansible fact triggered the set if no new module found
facts = self.filter_ansible_facts(value) if not module else dict({ module : value[module]})
self._cache[key] = value
self._cache_prev = deepcopy(self._cache)
self._all_keys[key] = value.keys()
packet = {
'host': key,
'inventory_id': os.environ['INVENTORY_ID'],

View File

@ -59,8 +59,8 @@ class ServiceScanService(BaseService):
initctl_path = self.module.get_bin_path("initctl")
chkconfig_path = self.module.get_bin_path("chkconfig")
# Upstart and sysvinit
if initctl_path is not None and chkconfig_path is None:
# sysvinit
if service_path is not None and chkconfig_path is None:
rc, stdout, stderr = self.module.run_command("%s --status-all 2>&1 | grep -E \"\\[ (\\+|\\-) \\]\"" % service_path, use_unsafe_shell=True)
for line in stdout.split("\n"):
line_data = line.split()
@ -72,6 +72,9 @@ class ServiceScanService(BaseService):
else:
service_state = "stopped"
services.append({"name": service_name, "state": service_state, "source": "sysv"})
# Upstart
if initctl_path is not None and chkconfig_path is None:
p = re.compile('^\s?(?P<name>.*)\s(?P<goal>\w+)\/(?P<state>\w+)(\,\sprocess\s(?P<pid>[0-9]+))?\s*$')
rc, stdout, stderr = self.module.run_command("%s list" % initctl_path)
real_stdout = stdout.replace("\r","")

View File

@ -47,8 +47,9 @@ body .navbar .navbar-brand:hover {
}
body .navbar .navbar-brand img {
display: inline-block;
max-width: 150px;
max-height: 50px;
width: 93px;
height: 30px;
margin-right:14px;
}
body .navbar .navbar-brand > span {
display: inline-block;

View File

@ -24,7 +24,7 @@
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{% url 'api:api_root_view' %}">
<img class="logo" src="{% static 'assets/main_menu_logo.png' %}">
<img class="logo" src="{% static 'assets/tower-logo-header.svg' %}">
<span>{% trans 'REST API' %}</span>
</a>
<a class="navbar-title" href="{{ request.get_full_path }}">
@ -49,10 +49,7 @@
<div id="footer">
<div class="container">
<div class="row">
<div class="col-sm-6 footer-logo">
<a href="http://www.ansible.com" target="_blank">
<img alt="Red Hat, Inc. | Ansible, Inc." src="{% static 'assets/footer-logo.png' %}" />
</a>
<div class="col-sm-6">
</div>
<div class="col-sm-6 footer-copyright">
Copyright &copy; 2016 <a href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc. All Rights Reserved.

View File

@ -0,0 +1 @@
<svg id="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 92.42 30"><defs><style>.cls-1{fill:#707070;}.cls-2{fill:#fff;}</style></defs><title>tower-logo-header2</title><path class="cls-1" d="M40.6,9.8V20.9H39.2V9.8H35.6V8.5h8.6V9.7l-3.6.1h0Z"/><path class="cls-1" d="M50.8,21.1c-3.1,0-5.1-2.6-5.1-6.4s2.1-6.4,5.2-6.4,5.2,2.6,5.2,6.4S53.9,21.1,50.8,21.1Zm0-11.5c-2.1,0-3.7,2-3.7,5.1s1.6,5.2,3.8,5.2,3.7-2,3.7-5.1S53,9.6,50.8,9.6h0Z"/><path class="cls-1" d="M68.4,20.9H66.8L64.9,13c-0.2-.8-0.5-2.1-0.6-2.8-0.1.8-.4,1.9-0.6,2.8l-1.9,7.9H60.3L57.7,8.5h1.4l1.4,7.9c0.1,0.8.4,2.2,0.5,2.8,0.1-.6.4-1.9,0.6-2.7l2-8H65l2,8c0.2,0.8.5,2.1,0.6,2.7,0.1-.6.3-1.9,0.5-2.8l1.4-7.9h1.3Z"/><path class="cls-1" d="M73.4,20.9V8.5h7.5V9.7H74.8v3.9h3.5v1.3H74.8v4.7h6.4v1.2C81.2,20.9,73.4,20.9,73.4,20.9Z"/><path class="cls-1" d="M89.6,15.5l2.7,5.4H90.7L88,15.6H85.1v5.3H83.7V8.5h4.9c2.2,0,3.8,1.1,3.8,3.5A3.09,3.09,0,0,1,89.6,15.5Zm-1-5.7H85.2v4.6h3.3c1.8,0,2.7-.8,2.7-2.3s-0.9-2.3-2.6-2.3h0Z"/><circle class="cls-1" cx="15" cy="15" r="15"/><path class="cls-2" d="M22.1,21l-6-14.4a1,1,0,0,0-1.8,0L7.7,22.3H10l2.6-6.5,7.7,6.3a2,2,0,0,0,.8.4,1,1,0,0,0,1.1-1V21.4A0.6,0.6,0,0,0,22.1,21ZM15.2,9.2l3.9,9.6-5.9-4.6Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -896,10 +896,6 @@ select.field-mini-height {
font-size: 10.5px;
}
.ask-checkbox {
margin-left: 10px;
}
.no-padding {
padding: 0;
margin: 0;

View File

@ -15,12 +15,25 @@
flex-direction: row;
}
.Form-textArea{
width: 100% !important;
}
.Form-header--fields{
flex: 1 1 auto;
}
.Form-header-field{
margin-left: 10px;
flex: 1 1 auto;
}
.Form-header{
display: flex;
}
.Form-title{
flex: 1 0 auto;
flex: 0 1 auto;
text-transform: uppercase;
color: @list-header-txt;
font-size: 14px;
@ -144,7 +157,7 @@
flex: 1 0 auto;
margin-bottom: 20px;
width: 33%;
padding-right: 50px;
padding-right: 30px;
}
.Form-subForm {
@ -455,6 +468,12 @@ input[type='radio']:checked:before {
padding-right: 0px;
}
.Form-subCheckbox {
margin-top: 5px;
font-size: small;
color: @default-interface-txt;
}
@media only screen and (max-width: 650px) {
.Form-formGroup {
flex: 1 0 auto;

View File

@ -166,6 +166,10 @@ table, tbody {
border-radius: 5px;
}
.List-titleBadge--selected {
background-color: @default-link;
}
.List-titleText {
color: @list-title-txt;
font-size: 14px;

View File

@ -2,7 +2,7 @@
@import "awx/ui/client/src/shared/branding/colors.default.less";
.About-cowsay--container{
width: 340px;
width: 340px;
margin: 0 auto;
}
.About-cowsay--code{
@ -23,10 +23,8 @@
padding-top: 0px;
}
.About-brand--redhat{
max-width: 420px;
margin: 0 auto;
margin-top: -50px;
margin-bottom: -30px;
float: left;
width: 112px;
}
.About-brand--ansible{
max-width: 120px;
@ -36,7 +34,14 @@
position: absolute;
top: 15px;
right: 15px;
z-index: 10;
}
.About p{
color: @default-interface-txt;
}
margin: 0;
font-size: 12px;
padding-top: 10px;
}
.About-modal--footer {
clear: both;
}

View File

@ -3,7 +3,7 @@ export default
var processVersion = function(version){
// prettify version & calculate padding
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
var split = version.split('-')[0]
var split = version.split('-')[0];
var spaces = Math.floor((16-split.length)/2),
paddedStr = "";
for(var i=0; i<=spaces; i++){
@ -13,7 +13,7 @@ export default
for(var j = paddedStr.length; j<16; j++){
paddedStr = paddedStr + " ";
}
return paddedStr
return paddedStr;
};
var init = function(){
CheckLicense.get()
@ -28,4 +28,4 @@ export default
});
init();
}
];
];

View File

@ -2,7 +2,6 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<img class="About-brand--ansible img-responsive" src="/static/assets/ansible_tower_logo_minimalc.png" />
<button data-dismiss="modal" type="button" class="close About-close">
<span class="fa fa-times-circle"></span>
</button>
@ -22,11 +21,11 @@
|| ||
</pre>
</div>
<img class="About-brand--redhat img-responsive" src="/static/assets/redhat_ansible_lockup.png" />
<p class="text-center">Copyright 2016. All rights reserved.<br>
Ansible and Ansible Tower are registered trademarks of <a href="http://www.redhat.com/" target="_blank">Red Hat, Inc</a>.<br>
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
{{subscription}}</p>
<div class="About-modal--footer">
<img class="About-brand--redhat img-responsive" src="/static/assets/tower-logo-login.svg" />
<p class="text-right">Copyright &copy; 2016 Red Hat, Inc. <br>
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
</div>
</div>
</div>
</div>

View File

@ -36,7 +36,7 @@ export default ['templateUrl', function(templateUrl) {
$scope.changeStreamTarget = function(){
if($scope.streamTarget && $scope.streamTarget == 'dashboard') {
if($scope.streamTarget && $scope.streamTarget === 'dashboard') {
// Just navigate to the base activity stream
$state.go('activityStream', {}, {inherit: false});
}
@ -45,7 +45,7 @@ export default ['templateUrl', function(templateUrl) {
$state.go('activityStream', {target: $scope.streamTarget}, {inherit: false});
}
}
};
}],
};
}];

View File

@ -22,6 +22,10 @@ function adhocController($q, $scope, $rootScope, $location, $stateParams,
var privateFn = {};
this.privateFn = privateFn;
var id = $stateParams.inventory_id,
urls = privateFn.setAvailableUrls(),
hostPattern = $rootScope.hostPatterns || "all";
// note: put any urls that the controller will use in here!!!!
privateFn.setAvailableUrls = function() {
return {
@ -31,10 +35,6 @@ function adhocController($q, $scope, $rootScope, $location, $stateParams,
};
};
var id = $stateParams.inventory_id,
urls = privateFn.setAvailableUrls(),
hostPattern = $rootScope.hostPatterns || "all";
// set the default options for the selects of the adhoc form
privateFn.setFieldDefaults = function(verbosity_options, forks_default) {
var verbosity;
@ -164,7 +164,7 @@ function adhocController($q, $scope, $rootScope, $location, $stateParams,
$scope.formCancel = function(){
$state.go('inventoryManage');
}
};
// remove all data input into the form and reset the form back to defaults
$scope.formReset = function () {

View File

@ -10,10 +10,5 @@ export default {
route: '/adhoc',
name: 'inventoryManage.adhoc',
templateUrl: templateUrl('adhoc/adhoc'),
controller: 'adhocController',
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: 'adhocController'
};

View File

@ -5,12 +5,13 @@
*************************************************/
var urlPrefix;
var $basePath;
if ($basePath) {
urlPrefix = $basePath;
} else {
// required to make tests work
var $basePath = '/static/';
$basePath = '/static/';
urlPrefix = $basePath;
}
@ -20,7 +21,7 @@ import './lists';
import './widgets';
import './help';
import './filters';
import {Home, HomeGroups, HomeHosts} from './controllers/Home';
import {Home, HomeGroups} from './controllers/Home';
import {SocketsController} from './controllers/Sockets';
import {CredentialsAdd, CredentialsEdit, CredentialsList} from './controllers/Credentials';
import {JobsListController} from './controllers/Jobs';
@ -49,20 +50,17 @@ import adhoc from './adhoc/main';
import login from './login/main';
import activityStream from './activity-stream/main';
import standardOut from './standard-out/main';
import lookUpHelper from './lookup/main';
import JobTemplates from './job-templates/main';
import search from './search/main';
import {ScheduleEditController} from './controllers/Schedules';
import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
import OrganizationsList from './organizations/list/organizations-list.controller';
import OrganizationsAdd from './organizations/add/organizations-add.controller';
import OrganizationsEdit from './organizations/edit/organizations-edit.controller';
import {InventoriesAdd, InventoriesEdit, InventoriesList, InventoriesManage} from './inventories/main';
import {AdminsList} from './controllers/Admins';
import {UsersList, UsersAdd, UsersEdit} from './controllers/Users';
import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams';
import RestServices from './rest/main';
import './lookup/main';
import './shared/api-loader';
import './shared/form-generator';
import './shared/Modal';
@ -175,7 +173,6 @@ var tower = angular.module('Tower', [
'CredentialsHelper',
'StreamListDefinition',
'HomeGroupListDefinition',
'HomeHostListDefinition',
'ActivityDetailDefinition',
'VariablesHelper',
'SchedulesListDefinition',
@ -198,7 +195,7 @@ var tower = angular.module('Tower', [
'pendolytics',
'ui.router',
'ncy-angular-breadcrumb',
'scheduler',
scheduler.name,
'ApiModelHelper',
'ActivityStreamHelper',
'dndLists'
@ -261,30 +258,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: {
parent: 'dashboard',
label: "GROUPS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('dashboardHosts', {
url: '/home/hosts?has_active_failures&name&id',
templateUrl: urlPrefix + 'partials/subhome.html',
controller: HomeHosts,
data: {
activityStream: true,
activityStreamTarget: 'host'
},
ncyBreadcrumb: {
parent: 'dashboard',
label: "HOSTS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -294,11 +267,6 @@ var tower = angular.module('Tower', [
controller: JobsListController,
ncyBreadcrumb: {
label: "JOBS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -312,11 +280,6 @@ var tower = angular.module('Tower', [
},
ncyBreadcrumb: {
label: "PROJECTS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -327,11 +290,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: {
parent: "projects",
label: "CREATE PROJECT"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -341,79 +299,19 @@ var tower = angular.module('Tower', [
controller: ProjectsEdit,
data: {
activityStreamId: 'id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('projectOrganizations', {
url: '/projects/:project_id/organizations',
templateUrl: urlPrefix + 'partials/projects.html',
controller: OrganizationsList,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: OrganizationsList
}).
state('projectOrganizationAdd', {
url: '/projects/:project_id/organizations/add',
templateUrl: urlPrefix + 'partials/projects.html',
controller: OrganizationsAdd,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: OrganizationsAdd
}).
state('organizationAdmins', {
url: '/organizations/:organization_id/admins',
templateUrl: urlPrefix + 'partials/organizations.html',
controller: AdminsList,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('organizationUsers', {
url:'/organizations/:organization_id/users',
templateUrl: urlPrefix + 'partials/users.html',
controller: UsersList,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('organizationUserAdd', {
url: '/organizations/:organization_id/users/add',
templateUrl: urlPrefix + 'partials/users.html',
controller: UsersAdd,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('organizationUserEdit', {
url: '/organizations/:organization_id/users/:user_id',
templateUrl: urlPrefix + 'partials/users.html',
controller: UsersEdit,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('teams', {
url: '/teams',
templateUrl: urlPrefix + 'partials/teams.html',
@ -425,11 +323,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: {
parent: 'setup',
label: 'TEAMS'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -440,11 +333,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: {
parent: "teams",
label: "CREATE TEAM"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -454,100 +342,55 @@ var tower = angular.module('Tower', [
controller: TeamsEdit,
data: {
activityStreamId: 'team_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('teamUsers', {
url: '/teams/:team_id/users',
templateUrl: urlPrefix + 'partials/teams.html',
controller: UsersList,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: UsersList
}).
state('teamUserEdit', {
url: '/teams/:team_id/users/:user_id',
templateUrl: urlPrefix + 'partials/teams.html',
controller: UsersEdit,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: UsersEdit
}).
state('teamProjects', {
url: '/teams/:team_id/projects',
templateUrl: urlPrefix + 'partials/teams.html',
controller: ProjectsList,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: ProjectsList
}).
state('teamProjectAdd', {
url: '/teams/:team_id/projects/add',
templateUrl: urlPrefix + 'partials/teams.html',
controller: ProjectsAdd,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: ProjectsAdd
}).
state('teamProjectEdit', {
url: '/teams/:team_id/projects/:project_id',
templateUrl: urlPrefix + 'partials/teams.html',
controller: ProjectsEdit,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: ProjectsEdit
}).
state('teamCredentials', {
url: '/teams/:team_id/credentials',
templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsList,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: CredentialsList
}).
state('teamCredentialAdd', {
url: '/teams/:team_id/credentials/add',
templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsAdd,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: CredentialsAdd
}).
state('teamCredentialEdit', {
url: '/teams/:team_id/credentials/:credential_id',
templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsEdit,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: CredentialsEdit
}).
state('credentials', {
@ -561,11 +404,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: {
parent: 'setup',
label: 'CREDENTIALS'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -576,11 +414,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: {
parent: "credentials",
label: "CREATE CREDENTIAL"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -590,11 +423,6 @@ var tower = angular.module('Tower', [
controller: CredentialsEdit,
data: {
activityStreamId: 'credential_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -609,11 +437,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: {
parent: 'setup',
label: 'USERS'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -624,11 +447,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: {
parent: "users",
label: "CREATE USER"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
@ -638,45 +456,25 @@ var tower = angular.module('Tower', [
controller: UsersEdit,
data: {
activityStreamId: 'user_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('userCredentials', {
url: '/users/:user_id/credentials',
templateUrl: urlPrefix + 'partials/users.html',
controller: CredentialsList,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: CredentialsList
}).
state('userCredentialAdd', {
url: '/users/:user_id/credentials/add',
templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsAdd,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: CredentialsAdd
}).
state('teamUserCredentialEdit', {
url: '/teams/:user_id/credentials/:credential_id',
templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsEdit,
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
controller: CredentialsEdit
}).
state('sockets', {
@ -710,7 +508,7 @@ var tower = angular.module('Tower', [
var sock;
$rootScope.addPermission = function (scope) {
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
}
};
$rootScope.deletePermission = function (user, role, userName,
roleName, resourceName) {
@ -738,6 +536,66 @@ var tower = angular.module('Tower', [
});
};
$rootScope.deletePermissionFromUser = function (userId, userName, roleName, roleType, url) {
var action = function () {
$('#prompt-modal').modal('hide');
Wait('start');
Rest.setUrl(url);
Rest.post({"disassociate": true, "id": userId})
.success(function () {
Wait('stop');
$rootScope.$broadcast("refreshList", "permission");
})
.error(function (data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Could not disassociate user from role. Call to ' + url + ' failed. DELETE returned status: ' + status });
});
};
Prompt({
hdr: `Remove role`,
body: `
<div class="Prompt-bodyQuery">
Confirm the removal of the ${roleType}
<span class="Prompt-emphasis"> ${roleName} </span>
role associated with ${userName}.
</div>
`,
action: action,
actionText: 'REMOVE'
});
};
$rootScope.deletePermissionFromTeam = function (teamId, teamName, roleName, roleType, url) {
var action = function () {
$('#prompt-modal').modal('hide');
Wait('start');
Rest.setUrl(url);
Rest.post({"disassociate": true, "id": teamId})
.success(function () {
Wait('stop');
$rootScope.$broadcast("refreshList", "role");
})
.error(function (data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Could not disassociate team from role. Call to ' + url + ' failed. DELETE returned status: ' + status });
});
};
Prompt({
hdr: `Remove role`,
body: `
<div class="Prompt-bodyQuery">
Confirm the removal of the ${roleType}
<span class="Prompt-emphasis"> ${roleName} </span>
role associated with the ${teamName} team.
</div>
`,
action: action,
actionText: 'REMOVE'
});
};
function activateTab() {
// Make the correct tab active
var base = $location.path().replace(/^\//, '').split('/')[0];
@ -774,16 +632,17 @@ var tower = angular.module('Tower', [
$rootScope.removeConfigReady();
}
$rootScope.removeConfigReady = $rootScope.$on('ConfigReady', function() {
var list, id;
// initially set row edit indicator for crud pages
if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") {
var list = $location.$$path.split("/")[3];
var id = $location.$$path.split("/")[4];
list = $location.$$path.split("/")[3];
id = $location.$$path.split("/")[4];
$rootScope.listBeingEdited = list;
$rootScope.rowBeingEdited = id;
$rootScope.initialIndicatorLoad = true;
} else if ($location.$$path.split("/")[2]) {
var list = $location.$$path.split("/")[1];
var id = $location.$$path.split("/")[2];
list = $location.$$path.split("/")[1];
id = $location.$$path.split("/")[2];
$rootScope.listBeingEdited = list;
$rootScope.rowBeingEdited = id;
}
@ -871,6 +730,9 @@ var tower = angular.module('Tower', [
$rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) {
if (next.name !== 'signOut'){
CheckLicense.notify();
}
$rootScope.$broadcast("closePermissionsModal");
// this line removes the query params attached to a route
if(prev && prev.$$route &&
@ -915,15 +777,16 @@ var tower = angular.module('Tower', [
activateTab();
});
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState) {
// catch license expiration notifications immediately after user logs in, redirect
if (fromState.name == 'signIn'){
if (fromState.name === 'signIn'){
CheckLicense.notify();
}
var list, id;
// broadcast event change if editing crud object
if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") {
var list = $location.$$path.split("/")[3];
var id = $location.$$path.split("/")[4];
list = $location.$$path.split("/")[3];
id = $location.$$path.split("/")[4];
if (!$rootScope.initialIndicatorLoad) {
delete $rootScope.listBeingEdited;
@ -934,8 +797,8 @@ var tower = angular.module('Tower', [
$rootScope.$broadcast("EditIndicatorChange", list, id);
} else if ($location.$$path.split("/")[2]) {
var list = $location.$$path.split("/")[1];
var id = $location.$$path.split("/")[2];
list = $location.$$path.split("/")[1];
id = $location.$$path.split("/")[2];
delete $rootScope.listBeingEdited;
delete $rootScope.rowBeingEdited;
@ -962,6 +825,7 @@ var tower = angular.module('Tower', [
$rootScope.sessionTimer = timer;
$rootScope.$emit('OpenSocket');
pendoService.issuePendoIdentity();
CheckLicense.notify();
});
}
}

View File

@ -3,7 +3,7 @@ export default
return {
restrict: 'E',
templateUrl: templateUrl('bread-crumb/bread-crumb'),
link: function(scope, element, attrs) {
link: function(scope) {
var streamConfig = {};
@ -15,15 +15,15 @@ export default
if(streamConfig && streamConfig.activityStream) {
if(streamConfig.activityStreamTarget) {
stateGoParams['target'] = streamConfig.activityStreamTarget;
stateGoParams.target = streamConfig.activityStreamTarget;
}
if(streamConfig.activityStreamId) {
stateGoParams['id'] = $state.params[streamConfig.activityStreamId];
stateGoParams.id = $state.params[streamConfig.activityStreamId];
}
}
$state.go('activityStream', stateGoParams);
}
};
scope.$on("$stateChangeSuccess", function updateActivityStreamButton(event, toState) {
@ -38,7 +38,7 @@ export default
// attached to the $rootScope.
FeaturesService.get()
.then(function(features) {
.then(function() {
if(FeaturesService.featureEnabled('activity_streams')) {
scope.showActivityStreamButton = true;
}

View File

@ -1,5 +1,5 @@
<div id="bread_crumb" class="BreadCrumb" ng-class="{'is-loggedOut' : !$root.current_user.username}">
<div ncy-breadcrumb></div>
<div ng-if="!licenseMissing" ncy-breadcrumb></div>
<div class="BreadCrumb-menuLink"
id="bread_crumb_activity_stream"
aw-tool-tip="View Activity Stream"
@ -8,6 +8,7 @@
data-container="body"
ng-class="{'BreadCrumb-menuLinkActive' : activityStreamActive}"
ng-if="showActivityStreamButton"
ng-hide= "licenseMissing"
ng-click="openActivityStream()">
<i class="BreadCrumb-menuLinkImage icon-activity-stream"
alt="Activity Stream">
@ -20,6 +21,7 @@
data-placement="left"
data-trigger="hover"
data-container="body"
ng-hide="licenseMissing"
ng-if="!showActivityStreamButton">
<i class="BreadCrumb-menuLinkImage fa fa-tachometer"
alt="Dashboard">

View File

@ -519,8 +519,8 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
$scope.project = data.project;
break;
case 'azure':
$scope.subscription_id = data.username;
$scope.subscription = data.username;
break;
}
$scope.credential_obj = data;

View File

@ -1,5 +1,5 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
@ -155,12 +155,10 @@ export function HomeGroups($rootScope, $log, $scope, $filter, $compile, $locatio
ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
//scope.
var generator = GenerateList,
list = HomeGroupList,
defaultUrl = GetBasePath('groups'),
scope = $scope,
modal_scope = $scope.$new(),
opt, PreviousSearchParams;
generator.inject(list, { mode: 'edit', scope: scope });
@ -517,112 +515,3 @@ HomeGroups.$inject = ['$rootScope', '$log', '$scope', '$filter', '$compile', '$l
* @description This loads the page for 'home/hosts'
*
*/
export function HomeHosts($scope, $location, $stateParams, HomeHostList, GenerateList, ProcessErrors, ReturnToCaller, ClearScope,
GetBasePath, SearchInit, PaginateInit, FormatDate, SetStatus, ToggleHostEnabled, HostsEdit, Find, ShowJobSummary) {
ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
//scope.
var generator = GenerateList,
list = HomeHostList,
defaultUrl = GetBasePath('hosts');
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function () {
for (var i = 0; i < $scope.hosts.length; i++) {
$scope.hosts[i].inventory_name = $scope.hosts[i].summary_fields.inventory.name;
//SetHostStatus($scope['hosts'][i]);
SetStatus({
$scope: $scope,
host: $scope.hosts[i]
});
}
generator.inject(list, { mode: 'edit', scope: $scope });
});
SearchInit({
scope: $scope,
set: 'hosts',
list: list,
url: defaultUrl
});
PaginateInit({
scope: $scope,
list: list,
url: defaultUrl
});
// Process search params
if ($stateParams.name) {
$scope[HomeHostList.iterator + 'InputDisable'] = false;
$scope[HomeHostList.iterator + 'SearchValue'] = $stateParams.name;
$scope[HomeHostList.iterator + 'SearchField'] = 'name';
$scope[HomeHostList.iterator + 'SearchFieldLabel'] = list.fields.name.label;
}
if ($stateParams.id) {
$scope[HomeHostList.iterator + 'InputDisable'] = false;
$scope[HomeHostList.iterator + 'SearchValue'] = $stateParams.id;
$scope[HomeHostList.iterator + 'SearchField'] = 'id';
$scope[HomeHostList.iterator + 'SearchFieldLabel'] = list.fields.id.label;
$scope[HomeHostList.iterator + 'SearchSelectValue'] = null;
}
if ($stateParams.has_active_failures) {
$scope[HomeHostList.iterator + 'InputDisable'] = true;
$scope[HomeHostList.iterator + 'SearchValue'] = $stateParams.has_active_failures;
$scope[HomeHostList.iterator + 'SearchField'] = 'has_active_failures';
$scope[HomeHostList.iterator + 'SearchFieldLabel'] = HomeHostList.fields.has_active_failures.label;
$scope[HomeHostList.iterator + 'SearchSelectValue'] = ($stateParams.has_active_failures === 'true') ? { value: 1 } : { value: 0 };
}
$scope.search(list.iterator);
$scope.refreshHosts = function() {
$scope.search(list.iterator);
};
$scope.toggleHostEnabled = function (id, sources) {
ToggleHostEnabled({
host_id: id,
external_source: sources,
host_scope: $scope
});
};
$scope.editHost = function (host_id) {
var host = Find({
list: $scope.hosts,
key: 'id',
val: host_id
});
if (host) {
HostsEdit({
host_scope: $scope,
host_id: host_id,
inventory_id: host.inventory,
group_id: null,
hostsReload: false,
mode: 'edit'
});
}
};
$scope.showJobSummary = function (job_id) {
ShowJobSummary({
job_id: job_id
});
};
}
HomeHosts.$inject = ['$scope', '$location', '$stateParams', 'HomeHostList', 'generateList', 'ProcessErrors', 'ReturnToCaller',
'ClearScope', 'GetBasePath', 'SearchInit', 'PaginateInit', 'FormatDate', 'SetStatus', 'ToggleHostEnabled', 'HostsEdit',
'Find', 'ShowJobSummary'
];

View File

@ -28,7 +28,6 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
mode = (base === 'projects') ? 'edit' : 'select',
url = (base === 'teams') ? GetBasePath('teams') + $stateParams.team_id + '/projects/' : defaultUrl,
choiceCount = 0;
view.inject(list, { mode: mode, scope: $scope });
$rootScope.flashMessage = null;

View File

@ -16,7 +16,7 @@ GetBasePath, Wait, Find, LoadDialogPartial, LoadSchedulesScope, GetChoices) {
ClearScope();
var base, e, id, url, parentObject;
var base, id, url, parentObject;
base = $location.path().replace(/^\//, '').split('/')[0];

View File

@ -188,7 +188,7 @@ TeamsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log',
];
export function TeamsEdit($scope, $rootScope, $location,
export function TeamsEdit($scope, $rootScope, $location,
$stateParams, TeamForm, GenerateForm, Rest, ProcessErrors,
RelatedSearchInit, RelatedPaginateInit, ClearScope,
LookUpInit, GetBasePath, OrganizationList, Wait, $state) {
@ -198,16 +198,15 @@ export function TeamsEdit($scope, $rootScope, $location,
var defaultUrl = GetBasePath('teams'),
generator = GenerateForm,
form = TeamForm,
base = $location.path().replace(/^\//, '').split('/')[0],
master = {},
id = $stateParams.team_id,
relatedSets = {};
relatedSets = {},
set;
$scope.team_id = id;
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset()
generator.reset();
var setScopeFields = function(data){
_(data)
@ -218,7 +217,7 @@ export function TeamsEdit($scope, $rootScope, $location,
$scope[key] = value;
})
.value();
return
return;
};
var setScopeRelated = function(data, related){
_(related)
@ -242,7 +241,7 @@ export function TeamsEdit($scope, $rootScope, $location,
data[key] = $scope[key];
}
});
return data
return data;
};
var init = function(){
@ -251,7 +250,7 @@ export function TeamsEdit($scope, $rootScope, $location,
Wait('start');
Rest.get(url).success(function(data){
setScopeFields(data);
setScopeRelated(data, form.related)
setScopeRelated(data, form.related);
$scope.organization_name = data.summary_fields.organization.name;
RelatedSearchInit({
@ -265,6 +264,12 @@ export function TeamsEdit($scope, $rootScope, $location,
relatedSets: relatedSets
});
for (set in relatedSets) {
$scope.search(relatedSets[set].iterator);
}
$scope.team_obj = data;
LookUpInit({
url: GetBasePath('organizations'),
scope: $scope,
@ -275,11 +280,11 @@ export function TeamsEdit($scope, $rootScope, $location,
input_type: 'radio'
});
});
}
};
$scope.formCancel = function(){
$state.go('teams', null, {reload: true});
}
};
$scope.formSave = function(){
generator.clearApiErrors();
@ -288,23 +293,31 @@ export function TeamsEdit($scope, $rootScope, $location,
if ($scope[form.name + '_form'].$valid){
Rest.setUrl(defaultUrl + id + '/');
var data = processNewData(form.fields);
Rest.put(data).success(function(res){
Rest.put(data).success(function(){
$state.go('teams', null, {reload: true});
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user: ' +
$stateParams.id + '. GET status: ' + status });
});
});
}
};
init();
$scope.convertApiUrl = function(str) {
if (str) {
return str.replace("api/v1", "#");
} else {
return null;
}
};
/* Related Set implementation TDB */
}
TeamsEdit.$inject = ['$scope', '$rootScope', '$location',
'$stateParams', 'TeamForm', 'GenerateForm', 'Rest',
'$stateParams', 'TeamForm', 'GenerateForm', 'Rest',
'ProcessErrors', 'RelatedSearchInit', 'RelatedPaginateInit',
'ClearScope', 'LookUpInit', 'GetBasePath',
'OrganizationList', 'Wait', '$state'

View File

@ -229,10 +229,10 @@ export function UsersEdit($scope, $rootScope, $location,
var defaultUrl = GetBasePath('users'),
generator = GenerateForm,
form = UserForm,
base = $location.path().replace(/^\//, '').split('/')[0],
master = {},
id = $stateParams.user_id,
relatedSets = {};
relatedSets = {},
set;
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
@ -246,20 +246,28 @@ export function UsersEdit($scope, $rootScope, $location,
$scope[key] = value;
})
.value();
return
return;
};
$scope.convertApiUrl = function(str) {
if (str) {
return str.replace("api/v1", "#");
} else {
return null;
}
};
var setScopeRelated = function(data, related){
_(related)
.pick(function(value, key){
return data.related.hasOwnProperty(key) === true;
})
.forEach(function(value, key){
relatedSets[key] = {
url: data.related[key],
iterator: value.iterator
};
})
.pick(function(value, key){
return data.related.hasOwnProperty(key) === true;
})
.forEach(function(value, key){
relatedSets[key] = {
url: data.related[key],
iterator: value.iterator
};
})
.value();
};
// prepares a data payload for a PUT request to the API
@ -270,7 +278,7 @@ export function UsersEdit($scope, $rootScope, $location,
data[key] = $scope[key];
}
});
return data
return data;
};
var init = function(){
@ -296,6 +304,11 @@ export function UsersEdit($scope, $rootScope, $location,
scope: $scope,
relatedSets: relatedSets
});
for (set in relatedSets) {
$scope.search(relatedSets[set].iterator);
}
Wait('stop');
})
.error(function (data, status) {
@ -315,13 +328,13 @@ export function UsersEdit($scope, $rootScope, $location,
if ($scope[form.name + '_form'].$valid){
Rest.setUrl(defaultUrl + id + '/');
var data = processNewData(form.fields);
Rest.put(data).success(function(res){
$state.go('users', null, {reload: true})
Rest.put(data).success(function(){
$state.go('users', null, {reload: true});
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user: ' +
$stateParams.id + '. GET status: ' + status });
});
});
}
};
@ -338,7 +351,7 @@ export function UsersEdit($scope, $rootScope, $location,
}
UsersEdit.$inject = ['$scope', '$rootScope', '$location',
'$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'ProcessErrors',
'$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'ProcessErrors',
'RelatedSearchInit', 'RelatedPaginateInit', 'ClearScope', 'GetBasePath',
'ResetForm', 'Wait', '$state'
];

View File

@ -38,7 +38,7 @@ export default
label: "Hosts"
},
{
url: "/#/home/hosts?has_active_failures=true",
url: "/#/home/hosts?active-failures=true",
number: scope.data.hosts.failed,
label: "Failed Hosts",
isFailureCount: true

View File

@ -0,0 +1,50 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['$scope', '$state', '$stateParams', 'DashboardHostsForm', 'GenerateForm', 'ParseTypeChange', 'DashboardHostService', 'host',
function($scope, $state, $stateParams, DashboardHostsForm, GenerateForm, ParseTypeChange, DashboardHostService, host){
var generator = GenerateForm,
form = DashboardHostsForm;
$scope.parseType = 'yaml';
$scope.formCancel = function(){
$state.go('^', null, {reload: true});
};
$scope.toggleHostEnabled = function(){
$scope.host.enabled = !$scope.host.enabled;
};
$scope.toggleEnabled = function(){
$scope.host.enabled = !$scope.host.enabled;
};
$scope.formSave = function(){
var host = {
id: $scope.host.id,
variables: $scope.extraVars === '---' || $scope.extraVars === '{}' ? null : $scope.extraVars,
name: $scope.name,
description: $scope.description,
enabled: $scope.host.enabled
};
DashboardHostService.putHost(host).then(function(){
$state.go('^', null, {reload: true});
});
};
var init = function(){
$scope.host = host;
$scope.extraVars = host.variables === '' ? '---' : host.variables;
generator.inject(form, {mode: 'edit', related: false, scope: $scope});
$scope.extraVars = $scope.host.variables === '' ? '---' : $scope.host.variables;
$scope.name = host.name;
$scope.description = host.description;
ParseTypeChange({
scope: $scope,
field_id: 'host_variables',
variable: 'extraVars',
});
};
init();
}];

View File

@ -0,0 +1,4 @@
<div class="tab-pane" id="organizations">
<div ui-view></div>
<div ng-cloak id="htmlTemplate" class="Panel"></div>
</div>

View File

@ -0,0 +1,64 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['$scope', '$state', '$stateParams', 'PageRangeSetup', 'GetBasePath', 'DashboardHostsList',
'generateList', 'PaginateInit', 'SetStatus', 'DashboardHostService', 'hosts',
function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts){
var setJobStatus = function(){
_.forEach($scope.hosts, function(value){
SetStatus({
scope: $scope,
host: value
});
});
};
var generator = GenerateList,
list = DashboardHostsList,
defaultUrl = GetBasePath('hosts');
$scope.hostPageSize = 10;
$scope.editHost = function(id){
$state.go('dashboardHosts.edit', {id: id});
};
$scope.toggleHostEnabled = function(host){
DashboardHostService.setHostStatus(host, !host.enabled)
.then(function(res){
var index = _.findIndex($scope.hosts, function(o) {return o.id === res.data.id;});
$scope.hosts[index].enabled = res.data.enabled;
});
};
$scope.$on('PostRefresh', function(){
$scope.hosts = _.map($scope.hosts, function(value){
value.inventory_name = value.summary_fields.inventory.name;
value.inventory_id = value.summary_fields.inventory.id;
return value;
});
setJobStatus();
});
var init = function(){
$scope.list = list;
$scope.host_active_search = false;
$scope.host_total_rows = hosts.results.length;
$scope.hosts = hosts.results;
setJobStatus();
generator.inject(list, {mode: 'edit', scope: $scope});
PaginateInit({
scope: $scope,
list: list,
url: defaultUrl,
pageSize: 10
});
PageRangeSetup({
scope: $scope,
count: hosts.count,
next: hosts.next,
previous: hosts.previous,
iterator: list.iterator
});
$scope.hostLoading = false;
};
init();
}];

View File

@ -0,0 +1,4 @@
<div class="tab-pane" id="HomeHosts">
<div ui-view></div>
<div ng-cloak id="htmlTemplate" class="Panel"></div>
</div>

View File

@ -0,0 +1,77 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default function(){
return {
editTitle: '{{host.name}}',
name: 'host',
well: true,
formLabelSize: 'col-lg-3',
formFieldSize: 'col-lg-9',
iterator: 'host',
headerFields:{
enabled: {
//flag: 'host.enabled',
class: 'Form-header-field',
ngClick: 'toggleHostEnabled()',
type: 'toggle',
editRequired: false,
awToolTip: "<p>Indicates if a host is available and should be included in running jobs.</p><p>For hosts that " +
"are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.</p>",
dataTitle: 'Host Enabled'
}
},
fields: {
name: {
label: 'Host Name',
type: 'text',
editRequired: true,
value: '{{name}}',
awPopOver: "<p>Provide a host name, ip address, or ip address:port. Examples include:</p>" +
"<blockquote>myserver.domain.com<br/>" +
"127.0.0.1<br />" +
"10.1.0.140:25<br />" +
"server.example.com:25" +
"</blockquote>",
dataTitle: 'Host Name',
dataPlacement: 'right',
dataContainer: 'body'
},
description: {
label: 'Description',
type: 'text',
editRequired: false
},
variables: {
label: 'Variables',
type: 'textarea',
editRequired: false,
rows: 6,
class: 'modal-input-xlarge Form-textArea',
dataTitle: 'Host Variables',
dataPlacement: 'right',
dataContainer: 'body',
default: '---',
awPopOver: "<p>Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.</p>" +
"JSON:<br />\n" +
"<blockquote>{<br />\"somevar\": \"somevalue\",<br />\"password\": \"magic\"<br /> }</blockquote>\n" +
"YAML:<br />\n" +
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n" +
'<p>View JSON examples at <a href="http://www.json.org" target="_blank">www.json.org</a></p>' +
'<p>View YAML examples at <a href="http://docs.ansible.com/YAMLSyntax.html" target="_blank">docs.ansible.com</a></p>',
}
},
buttons: {
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: "host_form.$invalid"//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons
},
cancel: {
ngClick: 'formCancel()'
}
}
};
}

View File

@ -1,14 +1,12 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
angular.module('HomeHostListDefinition', [])
.value('HomeHostList', {
export default function(){
return {
name: 'hosts',
iterator: 'host',
selectTitle: 'Add Existing Hosts',
@ -17,41 +15,48 @@ export default
index: false,
hover: true,
well: true,
emptyListText: 'NO ACTIVE FAILURES FOUND',
fields: {
status: {
label: "",
basePath: 'unified_jobs',
label: '',
iconOnly: true,
icon: "{{ 'icon-job-' + host.active_failures }}",
awToolTip: "{{ host.badgeToolTip }}",
awTipPlacement: "right",
dataPlacement: "right",
awPopOver: "{{ host.job_status_html }}",
ngClick:"bob",
columnClass: "List-staticColumn--smallStatus",
searchable: false,
nosort: true
searchable: true,
searchType: 'select',
nosort: true,
searchOptions: [],
searchLabel: 'Job Status',
icon: 'icon-job-{{ host.active_failures }}',
awToolTip: '{{ host.badgeToolTip }}',
awTipPlacement: 'right',
dataPlacement: 'right',
awPopOver: '{{ host.job_status_html }}',
ngClick:'viewHost(host.id)',
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus'
},
name: {
key: true,
label: 'Name',
columnClass: 'col-lg-5 col-md-5 col-sm-5 col-xs-8 ellipsis List-staticColumnAdjacent',
ngClass: "{ 'host-disabled-label': !host.enabled }",
ngClick: "editHost(host.id)"
ngClick: 'editHost(host.id)'
},
inventory_name: {
label: 'Inventory',
sourceModel: 'inventory',
sourceField: 'name',
columnClass: 'col-lg-5 col-md-4 col-sm-4 hidden-xs elllipsis',
linkTo: "{{ '/#/inventories/' + host.inventory }}"
linkTo: "{{ '/#/inventories/' + host.inventory_id }}",
searchable: false
},
enabled: {
label: 'Disabled?',
searchSingleValue: true,
searchType: 'boolean',
searchValue: 'false',
searchOnly: true
label: 'Status',
columnClass: 'List-staticColumn--toggle',
type: 'toggle',
ngClick: 'toggleHostEnabled(host)',
searchable: false,
nosort: true,
awToolTip: "<p>Indicates if a host is available and should be included in running jobs.</p><p>For hosts that are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.</p>",
dataTitle: 'Host Enabled',
},
has_active_failures: {
label: 'Has failed jobs?',
@ -66,30 +71,15 @@ export default
searchType: 'boolean',
searchValue: 'true',
searchOnly: true
},
id: {
label: 'ID',
searchOnly: true
}
},
fieldActions: {
columnClass: 'col-lg-2 col-md-3 col-sm-3 col-xs-4',
/*active_failures: {
//label: 'Job Status',
//ngHref: "\{\{'/#/hosts/' + host.id + '/job_host_summaries/?inventory=' + inventory_id \}\}",
awPopOver: "{{ host.job_status_html }}",
dataTitle: "{{ host.job_status_title }}",
awToolTip: "{{ host.badgeToolTip }}",
awTipPlacement: 'top',
dataPlacement: 'left',
iconClass: "{{ 'fa icon-failures-' + host.has_active_failures }}"
}*/
edit: {
label: 'Edit',
ngClick: "editHost(host.id)",
ngClick: 'editHost(host.id)',
icon: 'icon-edit',
awToolTip: 'Edit host',
dataPlacement: 'top'
@ -99,5 +89,5 @@ export default
actions: {
}
});
};
}

View File

@ -0,0 +1,64 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import listController from './dashboard-hosts-list.controller';
import editController from './dashboard-hosts-edit.controller';
var dashboardHostsList = {
name: 'dashboardHosts',
url: '/home/hosts?:active-failures',
controller: listController,
templateUrl: templateUrl('dashboard/hosts/dashboard-hosts-list'),
data: {
activityStream: true,
activityStreamTarget: 'host'
},
ncyBreadcrumb: {
parent: 'dashboard',
label: "HOSTS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
hosts: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams){
var defaultUrl = GetBasePath('hosts') + '?page_size=10' + ($stateParams['active-failures'] ? '&has_active_failures=true' : '' );
Rest.setUrl(defaultUrl);
return Rest.get().then(function(res){
var results = _.map(res.data.results, function(value){
value.inventory_name = value.summary_fields.inventory.name;
value.inventory_id = value.summary_fields.inventory.id;
return value;
});
res.data.results = results;
return res.data;
});
}]
}
};
var dashboardHostsEdit = {
name: 'dashboardHosts.edit',
url: '/:id',
controller: editController,
templateUrl: templateUrl('dashboard/hosts/dashboard-hosts-edit'),
ncyBreadcrumb: {
parent: 'dashboardHosts',
label: "{{host.name}}"
},
resolve: {
host: ['$stateParams', 'Rest', 'GetBasePath', function($stateParams, Rest, GetBasePath){
var defaultUrl = GetBasePath('hosts') + '?id=' + $stateParams.id;
Rest.setUrl(defaultUrl);
return Rest.get().then(function(res){
return res.data.results[0];
});
}]
}
};
export {dashboardHostsList, dashboardHostsEdit};

View File

@ -0,0 +1,30 @@
export default
['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($rootScope, Rest, GetBasePath, ProcessErrors){
return {
setHostStatus: function(host, enabled){
var url = GetBasePath('hosts') + host.id;
Rest.setUrl(url);
return Rest.put({enabled: enabled, name: host.name})
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
putHost: function(host){
var url = GetBasePath('hosts') + host.id;
Rest.setUrl(url);
return Rest.put(host)
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
}
};
}];

View File

@ -0,0 +1,20 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {dashboardHostsList, dashboardHostsEdit} from './dashboard-hosts.route';
import list from './dashboard-hosts.list';
import form from './dashboard-hosts.form';
import service from './dashboard-hosts.service';
export default
angular.module('dashboardHosts', [])
.service('DashboardHostService', service)
.factory('DashboardHostsList', list)
.factory('DashboardHostsForm', form)
.run(['$stateExtender', function($stateExtender){
$stateExtender.addState(dashboardHostsList);
$stateExtender.addState(dashboardHostsEdit);
}]);

View File

@ -2,7 +2,8 @@ import dashboardCounts from './counts/main';
import dashboardGraphs from './graphs/main';
import dashboardLists from './lists/main';
import dashboardDirective from './dashboard.directive';
import dashboardHosts from './hosts/main';
export default
angular.module('dashboard', [dashboardCounts.name, dashboardGraphs.name, dashboardLists.name])
angular.module('dashboard', [dashboardHosts.name, dashboardCounts.name, dashboardGraphs.name, dashboardLists.name])
.directive('dashboard', dashboardDirective);

View File

@ -1,8 +1,3 @@
<footer class='Footer'>
<a href="http://www.ansible.com" target="_blank">
<div class="Footer-logo" ng-class="{'is-loggedOut' : !$root.current_user.username}">
<img id="footer-logo" alt="Red Hat, Inc. | Ansible, Inc." class="Footer-logoImage" src="/static/assets/footer-logo.png">
</div>
</a>
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !$root.current_user.username}">Copyright &copy 2016 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
</footer>

View File

@ -114,12 +114,17 @@ export default
label: 'Secret Key',
type: 'sensitive',
ngShow: "kind.value == 'aws'",
ngDisabled: "secret_key_ask",
awRequiredWhen: {
reqExpression: "aws_required",
init: false
},
autocomplete: false,
ask: false,
subCheckbox: {
variable: 'secret_key_ask',
text: 'Ask at runtime?',
ngChange: 'ask(\'secret_key\', \'undefined\')'
},
clear: false,
hasShowInputButton: true,
apiField: 'password',
@ -141,7 +146,7 @@ export default
"host": {
labelBind: 'hostLabel',
type: 'text',
ngShow: "kind.value == 'vmware' || kind.value == 'openstack'",
ngShow: "kind.value == 'vmware' || kind.value == 'openstack' || kind.value === 'foreman' || kind.value === 'cloudforms'",
awPopOverWatch: "hostPopOver",
awPopOver: "set in helpers/credentials",
dataTitle: 'Host',
@ -154,6 +159,23 @@ export default
},
subForm: 'credentialSubForm'
},
"subscription": {
label: "Subscription ID",
type: 'text',
ngShow: "kind.value == 'azure' || kind.value == 'azure_rm'",
awRequiredWhen: {
reqExpression: 'subscription_required',
init: false
},
addRequired: false,
editRequired: false,
autocomplete: false,
awPopOver: '<p>Subscription ID is an Azure construct, which is mapped to a username.</p>',
dataTitle: 'Subscription ID',
dataPlacement: 'right',
dataContainer: "body",
subForm: 'credentialSubForm'
},
"username": {
labelBind: 'usernameLabel',
type: 'text',
@ -181,23 +203,6 @@ export default
dataContainer: "body",
subForm: 'credentialSubForm'
},
"subscription_id": {
labelBind: "usernameLabel",
type: 'text',
ngShow: "kind.value == 'azure'",
awRequiredWhen: {
reqExpression: 'subscription_required',
init: false
},
addRequired: false,
editRequired: false,
autocomplete: false,
awPopOver: '<p>Subscription ID is an Azure construct, which is mapped to a username.</p>',
dataTitle: 'Subscription ID',
dataPlacement: 'right',
dataContainer: "body",
subForm: 'credentialSubForm'
},
"api_key": {
label: 'API Key',
type: 'sensitive',
@ -207,7 +212,6 @@ export default
init: false
},
autocomplete: false,
ask: false,
hasShowInputButton: true,
clear: false,
subForm: 'credentialSubForm'
@ -215,10 +219,7 @@ export default
"password": {
labelBind: 'passwordLabel',
type: 'sensitive',
ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack'",
addRequired: false,
editRequired: false,
ask: false,
ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack'|| kind.value == 'foreman'|| kind.value == 'cloudforms'|| kind.value == 'net' || kind.value == 'azure_rm'",
clear: false,
autocomplete: false,
hasShowInputButton: true,
@ -229,12 +230,17 @@ export default
subForm: "credentialSubForm"
},
"ssh_password": {
label: 'Password', // formally 'SSH Password'
label: 'Password',
type: 'sensitive',
ngShow: "kind.value == 'ssh'",
ngDisabled: "ssh_password_ask",
addRequired: false,
editRequired: false,
ask: true,
subCheckbox: {
variable: 'ssh_password_ask',
text: 'Ask at runtime?',
ngChange: 'ask(\'ssh_password\', \'undefined\')'
},
hasShowInputButton: true,
autocomplete: false,
subForm: 'credentialSubForm'
@ -243,7 +249,7 @@ export default
labelBind: 'sshKeyDataLabel',
type: 'textarea',
ngShow: "kind.value == 'ssh' || kind.value == 'scm' || " +
"kind.value == 'gce' || kind.value == 'azure'",
"kind.value == 'gce' || kind.value == 'azure' || kind.value == 'net'",
awRequiredWhen: {
reqExpression: 'key_required',
init: true
@ -267,10 +273,15 @@ export default
ngShow: "kind.value == 'ssh' || kind.value == 'scm'",
addRequired: false,
editRequired: false,
ngDisabled: "keyEntered === false",
ask: true,
ngDisabled: "keyEntered === false || ssh_key_unlock_ask",
subCheckbox: {
variable: 'ssh_key_unlock_ask',
ngShow: "kind.value == 'ssh'",
text: 'Ask at runtime?',
ngChange: 'ask(\'ssh_key_unlock\', \'undefined\')',
ngDisabled: "keyEntered === false"
},
hasShowInputButton: true,
askShow: "kind.value == 'ssh'", // Only allow ask for machine credentials
subForm: 'credentialSubForm'
},
"become_method": {
@ -288,25 +299,77 @@ export default
subForm: 'credentialSubForm'
},
"become_username": {
label: 'Privilege Escalation Username',
labelBind: 'becomeUsernameLabel',
type: 'text',
ngShow: "kind.value == 'ssh' && (become_method && become_method.value)",
ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ",
addRequired: false,
editRequired: false,
autocomplete: false,
subForm: 'credentialSubForm'
},
"become_password": {
label: 'Privilege Escalation Password',
labelBind: 'becomePasswordLabel',
type: 'sensitive',
ngShow: "kind.value == 'ssh' && (become_method && become_method.value)",
ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ",
ngDisabled: "become_password_ask",
addRequired: false,
editRequired: false,
ask: true,
subCheckbox: {
variable: 'become_password_ask',
text: 'Ask at runtime?',
ngChange: 'ask(\'become_password\', \'undefined\')'
},
hasShowInputButton: true,
autocomplete: false,
subForm: 'credentialSubForm'
},
client:{
type: 'text',
label: 'Client ID',
awRequiredWhen: {
reqExpression: "azure_rm_required",
init: false
},
subForm: 'credentialSubForm',
ngShow: "kind.value === 'azure_rm'"
},
secret:{
type: 'sensitive',
hasShowInputButton: true,
autocomplete: false,
label: 'Client Secret',
awRequiredWhen: {
reqExpression: "azure_rm_required",
init: false
},
subForm: 'credentialSubForm',
ngShow: "kind.value === 'azure_rm'"
},
tenant: {
type: 'text',
label: 'Tenent ID',
awRequiredWhen: {
reqExpression: "azure_rm_required",
init: false
},
subForm: 'credentialSubForm',
ngShow: "kind.value === 'azure_rm'"
},
authorize: {
label: 'Authorize',
type: 'checkbox',
ngChange: "toggleCallback('host_config_key')",
subForm: 'credentialSubForm',
ngShow: "kind.value === 'net'"
},
authorize_password: {
label: 'Authorize Password',
type: 'sensitive',
hasShowInputButton: true,
autocomplete: false,
subForm: 'credentialSubForm',
ngShow: "authorize && authorize !== 'false'",
},
"project": {
labelBind: 'projectLabel',
type: 'text',
@ -344,9 +407,14 @@ export default
label: "Vault Password",
type: 'sensitive',
ngShow: "kind.value == 'ssh'",
ngDisabled: "vault_password_ask",
addRequired: false,
editRequired: false,
ask: true,
subCheckbox: {
variable: 'vault_password_ask',
text: 'Ask at runtime?',
ngChange: 'ask(\'vault_password\', \'undefined\')'
},
hasShowInputButton: true,
autocomplete: false,
subForm: 'credentialSubForm'

View File

@ -91,7 +91,7 @@ export default
type: 'select',
ngOptions: 'source.label for source in source_region_choices track by source.value',
multiSelect: true,
ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure')",
ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure' || source.value == 'azure_rm')",
addRequired: false,
editRequired: false,
dataTitle: 'Source Regions',

View File

@ -15,12 +15,23 @@ export default
.value('HostForm', {
addTitle: 'Create Host',
editTitle: '{{ name }}',
editTitle: '{{ host.name }}',
name: 'host',
well: false,
formLabelSize: 'col-lg-3',
formFieldSize: 'col-lg-9',
iterator: 'host',
headerFields:{
enabled: {
class: 'Form-header-field',
ngClick: 'toggleHostEnabled(host)',
type: 'toggle',
editRequired: false,
awToolTip: "<p>Indicates if a host is available and should be included in running jobs.</p><p>For hosts that " +
"are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.</p>",
dataTitle: 'Host Enabled',
}
},
fields: {
name: {
label: 'Host Name',
@ -43,19 +54,6 @@ export default
addRequired: false,
editRequired: false
},
enabled: {
label: 'Enabled?',
type: 'checkbox',
addRequired: false,
editRequired: false,
"default": true,
awPopOver: "<p>Indicates if a host is available and should be included in running jobs.</p><p>For hosts that " +
"are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.</p>",
dataTitle: 'Host Enabled',
dataPlacement: 'right',
dataContainer: 'body',
ngDisabled: 'has_inventory_sources == true'
},
variables: {
label: 'Variables',
type: 'textarea',
@ -82,17 +80,15 @@ export default
}
},
buttons: { //for now always generates <button> tags
/*
buttons: {
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: true //Disable when $pristine or $invalid, optional
ngClick: 'formSave()',
ngDisabled: true
},
reset: {
ngClick: 'formReset()',
ngDisabled: true //Disabled when $pristine
cancel: {
ngClick: 'formCancel()',
ngDisabled: true
}
*/
},
related: {}

View File

@ -50,7 +50,12 @@ export default
" syntax, test environment setup and report problems.</p>",
dataTitle: 'Job Type',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
subCheckbox: {
variable: 'ask_job_type_on_launch',
ngShow: "!job_type.value || job_type.value !== 'scan'",
text: 'Prompt on launch'
}
},
inventory: {
label: 'Inventory',
@ -59,14 +64,20 @@ export default
sourceField: 'name',
ngClick: 'lookUpInventory()',
awRequiredWhen: {
reqExpression: "inventoryrequired",
init: "true"
reqExpression: '!ask_inventory_on_launch',
alwaysShowAsterisk: true
},
requiredErrorMsg: "Please select an Inventory or check the Prompt on launch option.",
column: 1,
awPopOver: "<p>Select the inventory containing the hosts you want this job to manage.</p>",
dataTitle: 'Inventory',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
subCheckbox: {
variable: 'ask_inventory_on_launch',
ngShow: "!job_type.value || job_type.value !== 'scan'",
text: 'Prompt on launch'
}
},
project: {
label: 'Project',
@ -90,7 +101,7 @@ export default
ngOptions: 'book for book in playbook_options track by book',
id: 'playbook-select',
awRequiredWhen: {
reqExpression: "playbookrequired",
reqExpression: "playbookrequired",
init: "true"
},
column: 1,
@ -111,14 +122,21 @@ export default
sourceModel: 'credential',
sourceField: 'name',
ngClick: 'lookUpCredential()',
addRequired: false,
editRequired: false,
awRequiredWhen: {
reqExpression: '!ask_credential_on_launch',
alwaysShowAsterisk: true
},
requiredErrorMsg: "Please select a Machine Credential or check the Prompt on launch option.",
column: 1,
awPopOver: "<p>Select the credential you want the job to use when accessing the remote hosts. Choose the credential containing " +
" the username and SSH key or password that Ansible will need to log into the remote hosts.</p>",
dataTitle: 'Credential',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
subCheckbox: {
variable: 'ask_credential_on_launch',
text: 'Prompt on launch'
}
},
cloud_credential: {
label: 'Cloud Credential',
@ -165,7 +183,11 @@ export default
"<a href=\"http://docs.ansible.com/intro_patterns.html\" target=\"_blank\">the Patterns topic at docs.ansible.com</a>.</p>",
dataTitle: 'Limit',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
subCheckbox: {
variable: 'ask_limit_on_launch',
text: 'Prompt on launch'
}
},
verbosity: {
label: 'Verbosity',
@ -196,7 +218,11 @@ export default
"in the Job Tags field:</p>\n<blockquote>configuration,packages</blockquote>\n",
dataTitle: "Job Tags",
dataPlacement: "right",
dataContainer: "body"
dataContainer: "body",
subCheckbox: {
variable: 'ask_tags_on_launch',
text: 'Prompt on launch'
}
},
labels: {
label: 'Labels',
@ -227,20 +253,11 @@ export default
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n",
dataTitle: 'Extra Variables',
dataPlacement: 'right',
dataContainer: "body"
},
ask_variables_on_launch: {
label: 'Prompt for Extra Variables',
type: 'checkbox',
addRequired: false,
editRequird: false,
trueValue: 'true',
falseValue: 'false',
column: 2,
awPopOver: "<p>If checked, user will be prompted at job launch with a dialog allowing override of the extra variables setting.</p>",
dataPlacement: 'right',
dataTitle: 'Prompt for Extra Variables',
dataContainer: "body"
dataContainer: "body",
subCheckbox: {
variable: 'ask_variables_on_launch',
text: 'Prompt on launch'
}
},
become_enabled: {
label: 'Enable Privilege Escalation',

View File

@ -46,100 +46,6 @@ export default
},
related: {
users: {
type: 'collection',
title: 'Users',
iterator: 'user',
index: false,
open: false,
actions: {
add: {
ngClick: "add('users')",
label: 'Add',
awToolTip: 'Add a new user',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
username: {
key: true,
label: 'Username'
},
first_name: {
label: 'First Name'
},
last_name: {
label: 'Last Name'
}
},
fieldActions: {
edit: {
label: 'Edit',
ngClick: "edit('users', user.id, user.username)",
icon: 'icon-edit',
'class': 'btn-default',
awToolTip: 'Edit user'
},
"delete": {
label: 'Delete',
ngClick: "delete('users', user.id, user.username, 'user')",
icon: 'icon-trash',
"class": 'btn-danger',
awToolTip: 'Remove user'
}
}
},
admins: { // Assumes a plural name (e.g. things)
type: 'collection',
title: 'Administrators',
iterator: 'admin', // Singular form of name (e.g. thing)
index: false,
open: false, // Open accordion on load?
base: '/users',
actions: { // Actions displayed top right of list
add: {
ngClick: "add('admins')",
label: 'Add',
awToolTip: 'Add new administrator',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
username: {
key: true,
label: 'Username'
},
first_name: {
label: 'First Name'
},
last_name: {
label: 'Last Name'
}
},
fieldActions: { // Actions available on each row
edit: {
label: 'Edit',
ngClick: "edit('users', admin.id, admin.username)",
icon: 'icon-edit',
awToolTip: 'Edit administrator',
'class': 'btn-default'
},
"delete": {
label: 'Delete',
ngClick: "delete('admins', admin.id, admin.username, 'administrator')",
icon: 'icon-trash',
"class": 'btn-danger',
awToolTip: 'Remove administrator'
}
}
},
permissions: {
type: 'collection',
title: 'Permissions',

View File

@ -59,11 +59,10 @@ export default
},
related: {
/*
permissions: {
access_list: {
basePath: 'teams/:id/access_list/',
type: 'collection',
title: 'Permissions',
title: 'Users',
iterator: 'permission',
index: false,
open: false,
@ -76,148 +75,59 @@ export default
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
}
},
*/
credentials: {
type: 'collection',
title: 'Credentials',
iterator: 'credential',
open: false,
index: false,
actions: {
add: {
ngClick: "add('credentials')",
label: 'Add',
add: 'Add a new credential',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
name: {
key: true,
label: 'Name'
},
description: {
label: 'Description'
}
},
fieldActions: {
edit: {
label: 'Edit',
ngClick: "edit('credentials', credential.id, credential.name)",
icon: 'icon-edit',
awToolTip: 'Modify the credential',
'class': 'btn btn-default'
},
"delete": {
label: 'Delete',
ngClick: "delete('credentials', credential.id, credential.name, 'credential')",
icon: 'icon-trash',
"class": 'btn-danger',
awToolTip: 'Remove the credential'
}
}
},
projects: {
type: 'collection',
title: 'Projects',
iterator: 'project',
open: false,
index: false,
actions: {
add: {
ngClick: "add('projects')",
label: 'Add',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
name: {
key: true,
label: 'Name'
},
description: {
label: 'Description'
}
},
fieldActions: {
edit: {
label: 'Edit',
ngClick: "edit('projects', project.id, project.name)",
icon: 'icon-edit',
awToolTip: 'Modify the project',
'class': 'btn btn-default'
},
"delete": {
label: 'Delete',
ngClick: "delete('projects', project.id, project.name, 'project')",
icon: 'icon-trash',
"class": 'btn-danger',
awToolTip: 'Remove the project'
}
}
},
users: {
type: 'collection',
title: 'Users',
iterator: 'user',
open: false,
index: false,
actions: {
add: {
ngClick: "add('users')",
label: 'Add',
awToolTip: 'Add a user',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
username: {
key: true,
label: 'Username'
label: 'User',
linkBase: 'users',
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
},
first_name: {
label: 'First Name'
},
last_name: {
label: 'Last Name'
}
},
fieldActions: {
edit: {
label: 'Edit',
ngClick: "edit('users', user.id, user.username)",
icon: 'icon-edit',
awToolTip: 'Edit user',
'class': 'btn btn-default'
},
"delete": {
label: 'Delete',
ngClick: "delete('users', user.id, user.username, 'user')",
icon: 'icon-terash',
"class": 'btn-danger',
awToolTip: 'Remove user'
role: {
label: 'Role',
type: 'role',
noSort: true,
class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8'
}
}
},
roles: {
type: 'collection',
title: 'Permissions',
iterator: 'role',
open: false,
index: false,
actions: {},
fields: {
name: {
label: 'Name',
ngBind: 'role.summary_fields.resource_name',
linkTo: '{{convertApiUrl(role.related[role.summary_fields.resource_type])}}',
noSort: true
},
type: {
label: 'Type',
ngBind: 'role.summary_fields.resource_type_display_name',
noSort: true
},
role: {
label: 'Role',
ngBind: 'role.name',
noSort: true
}
},
fieldActions: {
"delete": {
label: 'Remove',
ngClick: 'deletePermissionFromTeam(team_id, team_obj.name, role.name, role.summary_fields.resource_name, role.related.teams)',
class: "List-actionButton--delete",
iconClass: 'fa fa-times',
awToolTip: 'Dissasociate permission from team'
}
},
hideOnSuperuser: true
}
}
},
}); //InventoryForm

View File

@ -115,71 +115,6 @@ export default
},
related: {
/*
permissions: {
basePath: 'teams/:id/access_list/',
type: 'collection',
title: 'Permissions',
iterator: 'permission',
index: false,
open: false,
searchType: 'select',
actions: {
add: {
ngClick: "addPermission",
label: 'Add',
awToolTip: 'Add a permission',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
}
},
*/
credentials: {
type: 'collection',
title: 'Credentials',
iterator: 'credential',
open: false,
index: false,
actions: {
add: {
ngClick: "add('credentials')",
label: 'Add',
awToolTip: 'Add a credential for this user',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
name: {
key: true,
label: 'Name'
},
description: {
label: 'Description'
}
},
fieldActions: {
edit: {
label: 'Edit',
ngClick: "edit('credentials', credential.id, credential.name)",
icon: 'icon-edit',
awToolTip: 'Edit the credential',
'class': 'btn btn-default'
},
"delete": {
label: 'Delete',
ngClick: "delete('credentials', credential.id, credential.name, 'credential')",
icon: 'icon-trash',
"class": 'btn-danger',
awToolTip: 'Delete the credential'
}
}
},
organizations: {
type: 'collection',
title: 'Organizations',
@ -197,9 +132,9 @@ export default
description: {
label: 'Description'
}
}
},
hideOnSuperuser: true
},
teams: {
type: 'collection',
title: 'Teams',
@ -217,8 +152,44 @@ export default
description: {
label: 'Description'
}
}
},
hideOnSuperuser: true
},
roles: {
hideSearchAndActions: true,
type: 'collection',
title: 'Permissions',
iterator: 'permission',
open: false,
index: false,
fields: {
name: {
label: 'Name',
ngBind: 'permission.summary_fields.resource_name',
linkTo: '{{convertApiUrl(permission.related[permission.summary_fields.resource_type])}}',
noSort: true
},
type: {
label: 'Type',
ngBind: 'permission.summary_fields.resource_type_display_name',
noSort: true
},
role: {
label: 'Role',
ngBind: 'permission.name',
noSort: true
},
},
fieldActions: {
"delete": {
label: 'Remove',
ngClick: 'deletePermissionFromUser(user_id, username, permission.name, permission.summary_fields.resource_name, permission.related.users)',
iconClass: 'fa fa-times',
awToolTip: 'Dissasociate permission from user'
}
},
hideOnSuperuser: true
}
}
}); //UserForm
});

View File

@ -88,6 +88,8 @@ angular.module('CredentialsHelper', ['Utilities'])
break;
case 'ssh':
scope.usernameLabel = 'Username'; //formally 'SSH Username'
scope.becomeUsernameLabel = 'Privilege Escalation Username';
scope.becomePasswordLabel = 'Privilege Escalation Password';
break;
case 'scm':
scope.sshKeyDataLabel = 'SCM Private Key';
@ -109,13 +111,20 @@ angular.module('CredentialsHelper', ['Utilities'])
"as: </p><p>adjective-noun-000</p>";
break;
case 'azure':
scope.usernameLabel = "Subscription ID";
scope.sshKeyDataLabel = 'Management Certificate';
scope.subscription_required = true;
scope.key_required = true;
scope.key_description = "Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console.";
scope.key_hint= "drag and drop a management certificate file on the field below";
break;
case 'azure_rm':
scope.usernameLabel = "Username";
scope.subscription_required = true;
scope.username_required = true;
scope.password_required = true;
scope.passwordLabel = 'Password';
scope.azure_rm_required = true;
break;
case 'vmware':
scope.username_required = true;
scope.host_required = true;
@ -137,6 +146,26 @@ angular.module('CredentialsHelper', ['Utilities'])
scope.hostPopOver = "<p>The host to authenticate with." +
"<br />For example, https://openstack.business.com/v2.0/";
break;
case 'foreman':
scope.username_required = true;
scope.password_required = true;
scope.passwordLabel = 'Password';
scope.host_required = true;
scope.hostLabel = "Satellite 6 Host";
break;
case 'cloudforms':
scope.username_required = true;
scope.password_required = true;
scope.passwordLabel = 'Password';
scope.host_required = true;
scope.hostLabel = "CloudForms Host";
break;
case 'net':
scope.username_required = true;
scope.password_required = true;
scope.passwordLabel = 'Password';
scope.sshKeyDataLabel = 'SSH Key';
break;
}
}
@ -205,7 +234,7 @@ angular.module('CredentialsHelper', ['Utilities'])
if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' &&
fld !== 'ssh_password') {
if (fld === "organization" && !scope[fld]) {
data["user"] = $rootScope.current_user.id;
data.user = $rootScope.current_user.id;
} else if (scope[fld] === null) {
data[fld] = "";
} else {
@ -238,7 +267,7 @@ angular.module('CredentialsHelper', ['Utilities'])
data.project = scope.project;
break;
case 'azure':
data.username = scope.subscription_id;
data.username = scope.subscription;
}
Wait('start');

View File

@ -281,7 +281,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
CreateSelect2({
element: '#source_source_regions'
});
} else if (scope.source.value === 'azure') {
} else if (scope.source.value === 'azure' || scope.source.value === 'azure_rm') {
scope.source_region_choices = scope.azure_regions;
$('#source_form').addClass('squeeze');
CreateSelect2({
@ -312,8 +312,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
}
if (scope.source.value === 'rax' ||
scope.source.value === 'ec2' ||
scope.source.value==='gce' ||
scope.source.value ==='gce' ||
scope.source.value === 'foreman' ||
scope.source.value ==='cloudforms' ||
scope.source.value === 'azure' ||
scope.source.value === 'azure_rm' ||
scope.source.value === 'vmware' ||
scope.source.value === 'openstack') {
if (scope.source.value === 'ec2') {
@ -1008,7 +1011,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
}
else if(fld === "inventory_script"){
// the API stores it as 'source_script', we call it inventory_script
data.summary_fields['inventory_script'] = data.summary_fields.source_script;
data.summary_fields.inventory_script = data.summary_fields.source_script;
sources_scope.inventory_script = data.source_script;
master.inventory_script = sources_scope.inventory_script;
} else if (fld === "source_regions") {

View File

@ -236,47 +236,6 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name,
};
}])
.factory('ToggleHostEnabled', [ 'GetBasePath', 'Rest', 'Wait', 'ProcessErrors', 'Alert', 'Find', 'SetEnabledMsg',
function(GetBasePath, Rest, Wait, ProcessErrors, Alert, Find, SetEnabledMsg) {
return function(params) {
var id = params.host_id,
external_source = params.external_source,
parent_scope = params.parent_scope,
host_scope = params.host_scope,
host;
function setMsg(host) {
host.enabled = (host.enabled) ? false : true;
host.enabled_flag = host.enabled;
SetEnabledMsg(host);
}
if (!external_source) {
// Host is not managed by an external source
Wait('start');
host = Find({ list: host_scope.hosts, key: 'id', val: id });
setMsg(host);
Rest.setUrl(GetBasePath('hosts') + id + '/');
Rest.put(host)
.success( function() {
Wait('stop');
})
.error( function(data, status) {
// Flip the enabled flag back
setMsg(host);
ProcessErrors(parent_scope, data, status, null,
{ hdr: 'Error!', msg: 'Failed to update host. PUT returned status: ' + status });
});
}
else {
Alert('Action Not Allowed', 'This host is managed by an external cloud source. Disable it at the external source, ' +
'then run an inventory sync to update Tower with the new status.', 'alert-info');
}
};
}])
.factory('HostsList', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'HostList', 'generateList',
'Prompt', 'SearchInit', 'PaginateInit', 'ProcessErrors', 'GetBasePath', 'HostsAdd', 'HostsReload', 'SelectionInit',
function($rootScope, $location, $log, $stateParams, Rest, Alert, HostList, GenerateList, Prompt, SearchInit,

View File

@ -351,29 +351,30 @@ export default
};
}])
.factory('UpdateJobStatus', ['GetElapsed', 'Empty', 'JobIsFinished', function(GetElapsed, Empty, JobIsFinished) {
.factory('UpdateJobStatus', ['GetElapsed', 'Empty', 'JobIsFinished', 'longDateFilter', function(GetElapsed, Empty, JobIsFinished, longDateFilter) {
return function(params) {
var scope = params.scope,
failed = params.failed,
modified = params.modified,
started = params.started;
started = params.started,
finished = params.finished;
if (failed && scope.job_status.status !== 'failed' && scope.job_status.status !== 'error' &&
scope.job_status.status !== 'canceled') {
scope.job_status.status = 'failed';
}
if (JobIsFinished(scope) && !Empty(modified)) {
scope.job_status.finished = longDateFilter(modified)
scope.job_status.finished = longDateFilter(modified);
}
if (!Empty(started) && Empty(scope.job_status.started)) {
scope.job_status.started = longDateFilter(modified)
scope.job_status.started = longDateFilter(modified);
}
if (!Empty(scope.job_status.finished) && !Empty(scope.job_status.started)) {
scope.job_status.elapsed = GetElapsed({
start: started,
end: finished
});
}
}
};
}])
@ -900,8 +901,7 @@ export default
.factory('SelectTask', ['JobDetailService', function(JobDetailService) {
return function(params) {
var scope = params.scope,
id = params.id,
callback = params.callback;
id = params.id;
scope.selectedTask = id;
scope.tasks.forEach(function(task, idx) {
@ -912,11 +912,11 @@ export default
scope.tasks[idx].taskActiveClass = '';
}
});
var params = {
params = {
parent: scope.selectedTask,
event__startswith: 'runner',
page_size: scope.hostResultsMaxRows,
order: 'host_name,counter',
order: 'host_name,counter',
};
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
scope.hostResults = JobDetailService.processHostEvents(res.results);

View File

@ -804,7 +804,7 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) ||
(base === 'home')){
// use $state.go with reload: true option to re-instantiate sockets in
$state.go('jobDetail', {id: job}, {reload: true})
$state.go('jobDetail', {id: job}, {reload: true});
}
});

Some files were not shown because too many files have changed in this diff Show More