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
252 changed files with 4498 additions and 2890 deletions

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ from rest_framework import views
# AWX # AWX
from awx.main.models import * # noqa from awx.main.models import * # noqa
from awx.main.models import Label
from awx.main.utils import * # noqa from awx.main.utils import * # noqa
from awx.api.serializers import ResourceAccessListElementSerializer from awx.api.serializers import ResourceAccessListElementSerializer
@@ -35,7 +36,8 @@ __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView', 'RetrieveUpdateDestroyAPIView', 'DestroyAPIView',
'SubDetailAPIView', 'SubDetailAPIView',
'ResourceAccessList', 'ResourceAccessList',
'ParentMixin',] 'ParentMixin',
'DeleteLastUnattachLabelMixin',]
logger = logging.getLogger('awx.api.generics') logger = logging.getLogger('awx.api.generics')
@@ -399,12 +401,15 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
else: else:
return Response(status=status.HTTP_204_NO_CONTENT) 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) sub_id = request.data.get('id', None)
res = None
if not sub_id: if not sub_id:
data = dict(msg='"id" is required to disassociate') 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 = self.get_parent_object()
parent_key = getattr(self, 'parent_key', None) parent_key = getattr(self, 'parent_key', None)
relationship = getattrd(parent, self.relationship) relationship = getattrd(parent, self.relationship)
@@ -421,6 +426,12 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
return Response(status=status.HTTP_204_NO_CONTENT) 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): def post(self, request, *args, **kwargs):
if not isinstance(request.data, dict): if not isinstance(request.data, dict):
return Response('invalid type for post data', return Response('invalid type for post data',
@@ -430,6 +441,21 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
else: else:
return self.attach(request, *args, **kwargs) 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): class SubDetailAPIView(generics.RetrieveAPIView, GenericAPIView, ParentMixin):
pass pass
@@ -474,7 +500,7 @@ class ResourceAccessList(ListAPIView):
def get_queryset(self): def get_queryset(self):
self.object_id = self.kwargs['pk'] self.object_id = self.kwargs['pk']
resource_model = getattr(self, 'resource_model') 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) content_type = ContentType.objects.get_for_model(obj)
roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id)) roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id))
@@ -483,4 +509,3 @@ class ResourceAccessList(ListAPIView):
for r in roles: for r in roles:
ancestors.update(set(r.ancestors.all())) ancestors.update(set(r.ancestors.all()))
return User.objects.filter(roles__in=list(ancestors)) 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.constants import SCHEDULEABLE_PROVIDERS
from awx.main.models import * # noqa from awx.main.models import * # noqa
from awx.main.fields import ImplicitRoleField 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.redact import REPLACE_STR
from awx.main.conf import tower_settings from awx.main.conf import tower_settings
@@ -78,7 +78,6 @@ SUMMARIZABLE_FK_FIELDS = {
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
'network_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'net'), 'network_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'net'),
'permission': DEFAULT_SUMMARY_FIELDS,
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
'job_template': DEFAULT_SUMMARY_FIELDS, 'job_template': DEFAULT_SUMMARY_FIELDS,
'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',), 'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',),
@@ -90,6 +89,7 @@ SUMMARIZABLE_FK_FIELDS = {
'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'), 'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
'inventory_source': ('source', 'last_updated', 'status'), 'inventory_source': ('source', 'last_updated', 'status'),
'source_script': ('name', 'description'), 'source_script': ('name', 'description'),
'role': ('id', 'role_field')
} }
@@ -340,16 +340,18 @@ class BaseSerializer(serializers.ModelSerializer):
return None return None
elif isinstance(obj, User): elif isinstance(obj, User):
return obj.date_joined return obj.date_joined
else: elif hasattr(obj, 'created'):
return obj.created return obj.created
return None
def get_modified(self, obj): def get_modified(self, obj):
if obj is None: if obj is None:
return None return None
elif isinstance(obj, User): elif isinstance(obj, User):
return obj.last_login # Not actually exposed for User. return obj.last_login # Not actually exposed for User.
else: elif hasattr(obj, 'modified'):
return obj.modified return obj.modified
return None
def build_standard_field(self, field_name, model_field): 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 # 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): def validate_password(self, value):
if not self.instance and value in (None, ''): 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 return value
def _update_password(self, obj, new_password): 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()) ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
if field_name in ldap_managed_fields: if field_name in ldap_managed_fields:
if value != getattr(self.instance, field_name): 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 return value
def validate_username(self, value): def validate_username(self, value):
@@ -796,6 +798,7 @@ class OrganizationSerializer(BaseSerializer):
users = reverse('api:organization_users_list', args=(obj.pk,)), users = reverse('api:organization_users_list', args=(obj.pk,)),
admins = reverse('api:organization_admins_list', args=(obj.pk,)), admins = reverse('api:organization_admins_list', args=(obj.pk,)),
teams = reverse('api:organization_teams_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,)), activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,)),
notifiers = reverse('api:organization_notifiers_list', args=(obj.pk,)), notifiers = reverse('api:organization_notifiers_list', args=(obj.pk,)),
notifiers_any = reverse('api:organization_notifiers_any_list', args=(obj.pk,)), notifiers_any = reverse('api:organization_notifiers_any_list', args=(obj.pk,)),
@@ -961,7 +964,7 @@ class BaseSerializerWithVariables(BaseSerializer):
try: try:
yaml.safe_load(value) yaml.safe_load(value)
except yaml.YAMLError: except yaml.YAMLError:
raise serializers.ValidationError('Must be valid JSON or YAML') raise serializers.ValidationError('Must be valid JSON or YAML.')
return value return value
@@ -1113,7 +1116,7 @@ class HostSerializer(BaseSerializerWithVariables):
vars_dict['ansible_ssh_port'] = port vars_dict['ansible_ssh_port'] = port
attrs['variables'] = yaml.dump(vars_dict) attrs['variables'] = yaml.dump(vars_dict)
except (yaml.YAMLError, TypeError): 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) return super(HostSerializer, self).validate(attrs)
@@ -1170,7 +1173,7 @@ class GroupSerializer(BaseSerializerWithVariables):
def validate_name(self, value): def validate_name(self, value):
if value in ('all', '_meta'): if value in ('all', '_meta'):
raise serializers.ValidationError('Invalid group name') raise serializers.ValidationError('Invalid group name.')
return value return value
def to_representation(self, obj): def to_representation(self, obj):
@@ -1303,10 +1306,10 @@ class InventorySourceOptionsSerializer(BaseSerializer):
else: else:
try: try:
if source_script.organization != self.instance.inventory.organization: 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: except Exception:
# TODO: Log # TODO: Log
errors['source_script'] = 'source_script doesn\'t exist' errors['source_script'] = 'source_script doesn\'t exist.'
if errors: if errors:
raise serializers.ValidationError(errors) raise serializers.ValidationError(errors)
@@ -1441,8 +1444,26 @@ class RoleSerializer(BaseSerializer):
class Meta: class Meta:
model = Role model = Role
fields = ('*', 'description', 'name') read_only_fields = ('id', 'role_field', 'description', 'name')
read_only_fields = ('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): def get_related(self, obj):
ret = super(RoleSerializer, self).get_related(obj) ret = super(RoleSerializer, self).get_related(obj)
@@ -1524,6 +1545,15 @@ class ResourceAccessListElementSerializer(UserSerializer):
.filter(content_type=team_content_type, .filter(content_type=team_content_type,
members=user, members=user,
children__in=direct_permissive_role_ids) 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 \ indirect_team_roles = Role.objects \
.filter(content_type=team_content_type, .filter(content_type=team_content_type,
@@ -1553,6 +1583,18 @@ class ResourceAccessListElementSerializer(UserSerializer):
class CredentialSerializer(BaseSerializer): class CredentialSerializer(BaseSerializer):
# FIXME: may want to make some fields filtered based on user accessing # 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: class Meta:
model = Credential model = Credential
@@ -1561,7 +1603,14 @@ class CredentialSerializer(BaseSerializer):
'ssh_key_data', 'ssh_key_unlock', 'ssh_key_data', 'ssh_key_unlock',
'become_method', 'become_username', 'become_password', 'become_method', 'become_username', 'become_password',
'vault_password', 'subscription', 'tenant', 'secret', 'client', '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): def build_standard_field(self, field_name, model_field):
field_class, field_kwargs = super(CredentialSerializer, self).build_standard_field(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: if not project and job_type != PERM_INVENTORY_SCAN:
raise serializers.ValidationError({'project': 'This field is required.'}) raise serializers.ValidationError({'project': 'This field is required.'})
if project and playbook and force_text(playbook) not in project.playbooks: 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: 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) 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) 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) job_type = attrs.get('job_type', self.instance and self.instance.job_type or None)
if survey_enabled and job_type == PERM_INVENTORY_SCAN: 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) return super(JobTemplateSerializer, self).validate(attrs)
@@ -1733,12 +1782,13 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
ask_tags_on_launch = serializers.ReadOnlyField() ask_tags_on_launch = serializers.ReadOnlyField()
ask_job_type_on_launch = serializers.ReadOnlyField() ask_job_type_on_launch = serializers.ReadOnlyField()
ask_inventory_on_launch = serializers.ReadOnlyField() ask_inventory_on_launch = serializers.ReadOnlyField()
ask_credential_on_launch = serializers.ReadOnlyField()
class Meta: class Meta:
model = Job model = Job
fields = ('*', 'job_template', 'passwords_needed_to_start', 'ask_variables_on_launch', 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_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): def get_related(self, obj):
res = super(JobSerializer, self).get_related(obj) res = super(JobSerializer, self).get_related(obj)
@@ -1865,9 +1915,9 @@ class JobRelaunchSerializer(JobSerializer):
if not obj.credential: if not obj.credential:
raise serializers.ValidationError(dict(credential=["Credential not found or deleted."])) raise serializers.ValidationError(dict(credential=["Credential not found or deleted."]))
if obj.job_type != PERM_INVENTORY_SCAN and obj.project is None: 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: 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) attrs = super(JobRelaunchSerializer, self).validate(attrs)
return attrs return attrs
@@ -1876,7 +1926,7 @@ class AdHocCommandSerializer(UnifiedJobSerializer):
class Meta: class Meta:
model = AdHocCommand model = AdHocCommand
fields = ('*', 'job_type', 'inventory', 'limit', 'credential', 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') 'become_enabled', '-unified_job_template', '-description')
extra_kwargs = { extra_kwargs = {
'name': { 'name': {
@@ -2104,6 +2154,8 @@ class JobLaunchSerializer(BaseSerializer):
inventory_needed_to_start = serializers.SerializerMethodField() inventory_needed_to_start = serializers.SerializerMethodField()
survey_enabled = serializers.SerializerMethodField() survey_enabled = serializers.SerializerMethodField()
extra_vars = VerbatimField(required=False, write_only=True) extra_vars = VerbatimField(required=False, write_only=True)
job_template_data = serializers.SerializerMethodField()
defaults = serializers.SerializerMethodField()
class Meta: class Meta:
model = JobTemplate model = JobTemplate
@@ -2113,7 +2165,8 @@ class JobLaunchSerializer(BaseSerializer):
'ask_job_type_on_launch', 'ask_limit_on_launch', 'ask_job_type_on_launch', 'ask_limit_on_launch',
'ask_inventory_on_launch', 'ask_credential_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch',
'survey_enabled', 'variables_needed_to_start', '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', read_only_fields = ('ask_variables_on_launch', 'ask_limit_on_launch',
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch',
'ask_inventory_on_launch', 'ask_credential_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 obj.survey_enabled and 'spec' in obj.survey_spec
return False 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): def validate(self, attrs):
errors = {} errors = {}
obj = self.context.get('obj') obj = self.context.get('obj')
@@ -2166,8 +2234,9 @@ class JobLaunchSerializer(BaseSerializer):
except (ValueError, TypeError): except (ValueError, TypeError):
try: try:
extra_vars = yaml.safe_load(extra_vars) extra_vars = yaml.safe_load(extra_vars)
except (yaml.YAMLError, TypeError, AttributeError): assert isinstance(extra_vars, dict)
errors['extra_vars'] = 'Must be valid JSON or YAML' except (yaml.YAMLError, TypeError, AttributeError, AssertionError):
errors['extra_vars'] = 'Must be a valid JSON or YAML dictionary'
if not isinstance(extra_vars, dict): if not isinstance(extra_vars, dict):
extra_vars = {} extra_vars = {}
@@ -2178,9 +2247,9 @@ class JobLaunchSerializer(BaseSerializer):
errors['variables_needed_to_start'] = validation_errors errors['variables_needed_to_start'] = validation_errors
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None): 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): 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: if errors:
raise serializers.ValidationError(errors) raise serializers.ValidationError(errors)
@@ -2318,7 +2387,7 @@ class ScheduleSerializer(BaseSerializer):
def validate_unified_job_template(self, value): def validate_unified_job_template(self, value):
if type(value) == InventorySource and value.source not in SCHEDULEABLE_PROVIDERS: 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 return value
# We reject rrules if: # We reject rrules if:
@@ -2510,7 +2579,7 @@ class TowerSettingsSerializer(BaseSerializer):
def validate(self, attrs): def validate(self, attrs):
manifest = settings.TOWER_SETTINGS_MANIFEST manifest = settings.TOWER_SETTINGS_MANIFEST
if attrs['key'] not in 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': if attrs['value_type'] == 'json':
attrs['value'] = json.dumps(attrs['value']) attrs['value'] = json.dumps(attrs['value'])
@@ -2544,7 +2613,7 @@ class AuthTokenSerializer(serializers.Serializer):
else: else:
raise serializers.ValidationError('Unable to login with provided credentials.') raise serializers.ValidationError('Unable to login with provided credentials.')
else: else:
raise serializers.ValidationError('Must include "username" and "password"') raise serializers.ValidationError('Must include "username" and "password".')
class FactVersionSerializer(BaseFactSerializer): class FactVersionSerializer(BaseFactSerializer):

View File

@@ -1,20 +1,3 @@
TOWER SOFTWARE END USER LICENSE AGREEMENT TOWER SOFTWARE END USER LICENSE AGREEMENT
Unless otherwise agreed to, and executed in a definitive agreement, between 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.
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.template.loader import render_to_string
from django.core.servers.basehttp import FileWrapper from django.core.servers.basehttp import FileWrapper
from django.http import HttpResponse from django.http import HttpResponse
from django.contrib.contenttypes.models import ContentType
# Django REST Framework # Django REST Framework
from rest_framework.exceptions import PermissionDenied, ParseError from rest_framework.exceptions import PermissionDenied, ParseError
@@ -616,9 +618,14 @@ class OrganizationList(ListCreateAPIView):
JT_reference = 'project__organization' JT_reference = 'project__organization'
db_results['job_templates'] = JobTemplate.accessible_objects( 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) 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\ db_results['projects'] = project_qs\
.values('organization').annotate(Count('organization')).order_by('organization') .values('organization').annotate(Count('organization')).order_by('organization')
@@ -638,6 +645,8 @@ class OrganizationList(ListCreateAPIView):
for res in db_results: for res in db_results:
if res == 'job_templates': if res == 'job_templates':
org_reference = JT_reference org_reference = JT_reference
elif res == 'job_templates_scan':
org_reference = JT_scan_reference
elif res == 'users': elif res == 'users':
org_reference = 'id' org_reference = 'id'
else: else:
@@ -651,6 +660,12 @@ class OrganizationList(ListCreateAPIView):
continue continue
count_context[org_id][res] = entry['%s__count' % org_reference] 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 full_context['related_field_counts'] = count_context
return full_context return full_context
@@ -684,8 +699,10 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView):
organization__id=org_id).count() organization__id=org_id).count()
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter( org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count() organization__id=org_id).count()
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter( org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).exclude(
project__organization__id=org_id).count() 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'] = {}
full_context['related_field_counts'][org_id] = org_counts full_context['related_field_counts'][org_id] = org_counts
@@ -814,10 +831,11 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
relationship='member_role.children' relationship='member_role.children'
def get_queryset(self): def get_queryset(self):
team = Team.objects.get(pk=self.kwargs['pk']) team = get_object_or_404(Team, pk=self.kwargs['pk'])
return team.member_role.children.filter(id__in=Role.visible_roles(self.request.user)) 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): def post(self, request, *args, **kwargs):
# Forbid implicit role creation here # Forbid implicit role creation here
sub_id = request.data.get('id', None) sub_id = request.data.get('id', None)
@@ -1081,8 +1099,12 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
def get_queryset(self): def get_queryset(self):
#u = User.objects.get(pk=self.kwargs['pk']) u = get_object_or_404(User, pk=self.kwargs['pk'])
return Role.visible_roles(self.request.user).filter(members__in=[int(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): def post(self, request, *args, **kwargs):
# Forbid implicit role creation here # Forbid implicit role creation here
@@ -1090,6 +1112,10 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
if not sub_id: if not sub_id:
data = dict(msg='Role "id" field is missing') data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST) 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) return super(UserRolesList, self).post(request, *args, **kwargs)
def check_parent_access(self, parent=None): def check_parent_access(self, parent=None):
@@ -1205,6 +1231,10 @@ class CredentialList(ListCreateAPIView):
serializer_class = CredentialSerializer serializer_class = CredentialSerializer
def post(self, request, *args, **kwargs): 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']]): 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) 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: if 'user' in request.data:
user = User.objects.get(pk=request.data['user']) user = User.objects.get(pk=request.data['user'])
obj = user can_add_params = {'user': user.id}
if 'team' in request.data: if 'team' in request.data:
team = Team.objects.get(pk=request.data['team']) team = Team.objects.get(pk=request.data['team'])
obj = team can_add_params = {'team': team.id}
if 'organization' in request.data: if 'organization' in request.data:
organization = Organization.objects.get(pk=request.data['organization']) 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() raise PermissionDenied()
ret = super(CredentialList, self).post(request, *args, **kwargs) ret = super(CredentialList, self).post(request, *args, **kwargs)
@@ -1251,8 +1281,7 @@ class UserCredentialsList(CredentialList):
return user_creds & visible_creds return user_creds & visible_creds
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
user = User.objects.get(pk=self.kwargs['pk']) request.data['user'] = self.kwargs['pk']
request.data['user'] = user.id
# The following post takes care of ensuring the current user can add a cred to this user # 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) return super(UserCredentialsList, self).post(request, args, kwargs)
@@ -1271,8 +1300,7 @@ class TeamCredentialsList(CredentialList):
return team_creds & visible_creds return team_creds & visible_creds
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
team = Team.objects.get(pk=self.kwargs['pk']) request.data['team'] = self.kwargs['pk']
request.data['team'] = team.id
# The following post takes care of ensuring the current user can add a cred to this user # 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) return super(TeamCredentialsList, self).post(request, args, kwargs)
@@ -1479,7 +1507,7 @@ class HostAllGroupsList(SubListAPIView):
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
self.check_parent_access(parent) 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() sublist_qs = parent.all_groups.distinct()
return qs & sublist_qs return qs & sublist_qs
@@ -2263,7 +2291,7 @@ class JobTemplateNotifiersSuccessList(SubListCreateAttachDetachAPIView):
parent_model = JobTemplate parent_model = JobTemplate
relationship = 'notifiers_success' relationship = 'notifiers_success'
class JobTemplateLabelList(SubListCreateAttachDetachAPIView): class JobTemplateLabelList(SubListCreateAttachDetachAPIView, DeleteLastUnattachLabelMixin):
model = Label model = Label
serializer_class = LabelSerializer serializer_class = LabelSerializer
@@ -2454,7 +2482,7 @@ class SystemJobTemplateList(ListAPIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if not request.user.is_superuser: 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) return super(SystemJobTemplateList, self).get(request, *args, **kwargs)
class SystemJobTemplateDetail(RetrieveAPIView): class SystemJobTemplateDetail(RetrieveAPIView):
@@ -3184,7 +3212,7 @@ class SystemJobList(ListCreateAPIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if not request.user.is_superuser: 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) 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 # XXX: This should be the intersection between the roles of the user
# and the roles that the requesting user has access to see # and the roles that the requesting user has access to see
role = Role.objects.get(pk=self.kwargs['pk']) 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 result
return False 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): class BaseAccess(object):
''' '''
Base class for checking user access to a given model. Subclasses should 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. # that a user should be able to edit for themselves.
return bool(self.user == obj or self.can_admin(obj, data)) return bool(self.user == obj or self.can_admin(obj, data))
@check_superuser
def can_admin(self, obj, data): 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() return Organization.objects.filter(member_role__members=obj, admin_role__members=self.user).exists()
def can_delete(self, obj): def can_delete(self, obj):
@@ -277,9 +287,8 @@ class OrganizationAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role') qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by').all() return qs.select_related('created_by', 'modified_by').all()
@check_superuser
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser:
return True
return self.user in obj.admin_role return self.user in obj.admin_role
def can_delete(self, obj): def can_delete(self, obj):
@@ -312,27 +321,25 @@ class InventoryAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role') qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by', 'organization').all() return qs.select_related('created_by', 'modified_by', 'organization').all()
@check_superuser
def can_read(self, obj): def can_read(self, obj):
if self.user.is_superuser:
return True
return self.user in obj.read_role return self.user in obj.read_role
@check_superuser
def can_use(self, obj): def can_use(self, obj):
if self.user.is_superuser:
return True
return self.user in obj.use_role return self.user in obj.use_role
@check_superuser
def can_add(self, data): def can_add(self, data):
# If no data is specified, just checking for generic add permission? # If no data is specified, just checking for generic add permission?
if not data: if not data:
return Organization.accessible_objects(self.user, 'admin_role').exists() 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_pk = get_pk_from_dict(data, 'organization')
org = get_object_or_400(Organization, pk=org_pk) org = get_object_or_400(Organization, pk=org_pk)
return self.user in org.admin_role return self.user in org.admin_role
@check_superuser
def can_change(self, obj, data): def can_change(self, obj, data):
# Verify that the user has access to the new organization if moving an # Verify that the user has access to the new organization if moving an
# inventory to a new organization. # inventory to a new organization.
@@ -342,8 +349,9 @@ class InventoryAccess(BaseAccess):
if self.user not in org.admin_role: if self.user not in org.admin_role:
return False return False
# Otherwise, just check for write permission. # 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): def can_admin(self, obj, data):
# Verify that the user has access to the new organization if moving an # Verify that the user has access to the new organization if moving an
# inventory to a new organization. # inventory to a new organization.
@@ -371,12 +379,16 @@ class HostAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
inv_qs = Inventory.accessible_objects(self.user, 'read_role') inv_qs = Inventory.accessible_objects(self.user, 'read_role')
group_qs = Group.accessible_objects(self.user, 'read_role') group_qs = Group.accessible_objects(self.user, 'read_role').exclude(inventory__in=inv_qs)
qs = (self.model.objects.filter(inventory=inv_qs) | self.model.objects.filter(groups=group_qs)).distinct() if group_qs.count():
#qs = qs.select_related('created_by', 'modified_by', 'inventory', qs = self.model.objects.filter(Q(inventory__in=inv_qs) | Q(groups__in=group_qs))
# 'last_job__job_template', else:
# 'last_job_host_summary__job') qs = self.model.objects.filter(inventory__in=inv_qs)
#return qs.prefetch_related('groups').all()
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 return qs
def can_read(self, obj): def can_read(self, obj):
@@ -389,7 +401,7 @@ class HostAccess(BaseAccess):
# Checks for admin or change permission on inventory. # Checks for admin or change permission on inventory.
inventory_pk = get_pk_from_dict(data, 'inventory') inventory_pk = get_pk_from_dict(data, 'inventory')
inventory = get_object_or_400(Inventory, pk=inventory_pk) 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 return False
# Check to see if we have enough licenses # Check to see if we have enough licenses
@@ -403,7 +415,7 @@ class HostAccess(BaseAccess):
raise PermissionDenied('Unable to change inventory on a host') raise PermissionDenied('Unable to change inventory on a host')
# Checks for admin or change permission on inventory, controls whether # Checks for admin or change permission on inventory, controls whether
# the user can edit variable data. # 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, def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False): skip_sub_obj_read_check=False):
@@ -440,7 +452,7 @@ class GroupAccess(BaseAccess):
# Checks for admin or change permission on inventory. # Checks for admin or change permission on inventory.
inventory_pk = get_pk_from_dict(data, 'inventory') inventory_pk = get_pk_from_dict(data, 'inventory')
inventory = get_object_or_400(Inventory, pk=inventory_pk) 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): def can_change(self, obj, data):
# Prevent moving a group to a different inventory. # Prevent moving a group to a different inventory.
@@ -449,7 +461,7 @@ class GroupAccess(BaseAccess):
raise PermissionDenied('Unable to change inventory on a group') raise PermissionDenied('Unable to change inventory on a group')
# Checks for admin or change permission on inventory, controls whether # Checks for admin or change permission on inventory, controls whether
# the user can attach subgroups or edit variable data. # 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, def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False): skip_sub_obj_read_check=False):
@@ -483,7 +495,7 @@ class InventorySourceAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.objects.all() qs = self.model.objects.all()
qs = qs.select_related('created_by', 'modified_by', 'group', 'inventory') 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) | return qs.filter(Q(inventory_id__in=inventory_ids) |
Q(group__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): def can_change(self, obj, data):
# Checks for admin or change permission on group. # Checks for admin or change permission on group.
if obj and obj.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 # Can't change inventory sources attached to only the inventory, since
# these are created automatically from the management command. # these are created automatically from the management command.
else: else:
@@ -555,21 +567,36 @@ class CredentialAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role') qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by').all() return qs.select_related('created_by', 'modified_by').all()
@check_superuser
def can_read(self, obj): def can_read(self, obj):
return self.user in obj.read_role return self.user in obj.read_role
def can_add(self, data): 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: if self.user.is_superuser:
return True 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 return self.user in obj.use_role
@check_superuser
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser: if not self.can_add(data):
return True return False
return self.user in obj.owner_role return self.user in obj.owner_role
def can_delete(self, obj): def can_delete(self, obj):
@@ -596,14 +623,12 @@ class TeamAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role') qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by', 'organization').all() return qs.select_related('created_by', 'modified_by', 'organization').all()
@check_superuser
def can_add(self, data): 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 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 return False
def can_change(self, obj, data): def can_change(self, obj, data):
@@ -611,6 +636,8 @@ class TeamAccess(BaseAccess):
org_pk = get_pk_from_dict(data, 'organization') org_pk = get_pk_from_dict(data, 'organization')
if obj and org_pk and obj.organization.pk != org_pk: if obj and org_pk and obj.organization.pk != org_pk:
raise PermissionDenied('Unable to change organization on a team') raise PermissionDenied('Unable to change organization on a team')
if self.user.is_superuser:
return True
return self.user in obj.admin_role return self.user in obj.admin_role
def can_delete(self, obj): def can_delete(self, obj):
@@ -640,15 +667,13 @@ class ProjectAccess(BaseAccess):
qs = self.model.accessible_objects(self.user, 'read_role') qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all() return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all()
@check_superuser
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser:
return True
qs = Organization.accessible_objects(self.user, 'admin_role') qs = Organization.accessible_objects(self.user, 'admin_role')
return qs.exists() return qs.exists()
@check_superuser
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser:
return True
return self.user in obj.admin_role return self.user in obj.admin_role
def can_delete(self, obj): 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)) project_ids = set(self.user.get_queryset(Project).values_list('id', flat=True))
return qs.filter(project_id__in=project_ids) return qs.filter(project_id__in=project_ids)
@check_superuser
def can_cancel(self, obj): def can_cancel(self, obj):
return self.can_change(obj, {}) and obj.can_cancel return self.can_change(obj, {}) and obj.can_cancel
@check_superuser
def can_delete(self, obj): def can_delete(self, obj):
return obj and self.user in obj.project.admin_role return obj and self.user in obj.project.admin_role
@@ -704,8 +731,7 @@ class JobTemplateAccess(BaseAccess):
'credential', 'cloud_credential', 'next_schedule').all() 'credential', 'cloud_credential', 'next_schedule').all()
def can_read(self, obj): def can_read(self, obj):
# you can only see the job templates that you have permission to launch. return self.user in obj.read_role
return self.can_start(obj, validate_license=False)
def can_add(self, data): def can_add(self, data):
''' '''
@@ -847,10 +873,8 @@ class JobAccess(BaseAccess):
def can_change(self, obj, data): def can_change(self, obj, data):
return obj.status == 'new' and self.can_read(obj) and self.can_add(data) return obj.status == 'new' and self.can_read(obj) and self.can_add(data)
@check_superuser
def can_delete(self, obj): 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 return self.user in obj.inventory.admin_role
def can_start(self, obj): def can_start(self, obj):
@@ -866,11 +890,12 @@ class JobAccess(BaseAccess):
return self.user in obj.job_template.execute_role return self.user in obj.job_template.execute_role
inventory_access = self.user in obj.inventory.use_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 org_access = self.user in obj.inventory.organization.admin_role
project_access = obj.project is None or self.user in obj.project.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): def can_cancel(self, obj):
return self.can_read(obj) and obj.can_cancel 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)) UnifiedJobTemplate.objects.filter(Q(inventorysource__in=inventory_source_qs))
return qs.filter(unified_job_template__in=unified_qs) return qs.filter(unified_job_template__in=unified_qs)
@check_superuser
def can_read(self, obj): def can_read(self, obj):
if self.user.is_superuser:
return True
if obj and obj.unified_job_template: if obj and obj.unified_job_template:
job_class = obj.unified_job_template job_class = obj.unified_job_template
return self.user.can_access(type(job_class), 'read', obj.unified_job_template) return self.user.can_access(type(job_class), 'read', obj.unified_job_template)
else: else:
return False return False
@check_superuser
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser:
return True
pk = get_pk_from_dict(data, 'unified_job_template') pk = get_pk_from_dict(data, 'unified_job_template')
obj = get_object_or_400(UnifiedJobTemplate, pk=pk) obj = get_object_or_400(UnifiedJobTemplate, pk=pk)
if obj: if obj:
@@ -1165,18 +1188,16 @@ class ScheduleAccess(BaseAccess):
else: else:
return False return False
@check_superuser
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser:
return True
if obj and obj.unified_job_template: if obj and obj.unified_job_template:
job_class = obj.unified_job_template job_class = obj.unified_job_template
return self.user.can_access(type(job_class), 'change', job_class, None) return self.user.can_access(type(job_class), 'change', job_class, None)
else: else:
return False return False
@check_superuser
def can_delete(self, obj): def can_delete(self, obj):
if self.user.is_superuser:
return True
if obj and obj.unified_job_template: if obj and obj.unified_job_template:
job_class = obj.unified_job_template job_class = obj.unified_job_template
return self.user.can_access(type(job_class), 'change', job_class, None) return self.user.can_access(type(job_class), 'change', job_class, None)
@@ -1195,25 +1216,22 @@ class NotifierAccess(BaseAccess):
return qs return qs
return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all()) return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all())
@check_superuser
def can_read(self, obj): def can_read(self, obj):
if self.user.is_superuser:
return True
if obj.organization is not None: if obj.organization is not None:
return self.user in obj.organization.admin_role return self.user in obj.organization.admin_role
return False return False
@check_superuser
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser:
return True
if not data: if not data:
return Organization.accessible_objects(self.user, 'admin_role').exists() return Organization.accessible_objects(self.user, 'admin_role').exists()
org_pk = get_pk_from_dict(data, 'organization') org_pk = get_pk_from_dict(data, 'organization')
org = get_object_or_400(Organization, pk=org_pk) org = get_object_or_400(Organization, pk=org_pk)
return self.user in org.admin_role return self.user in org.admin_role
@check_superuser
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser:
return True
org_pk = get_pk_from_dict(data, 'organization') org_pk = get_pk_from_dict(data, 'organization')
if obj and org_pk and obj.organization.pk != org_pk: if obj and org_pk and obj.organization.pk != org_pk:
org = get_object_or_400(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') organization__in=Organization.accessible_objects(self.user, 'read_role')
) )
@check_superuser
def can_read(self, obj): def can_read(self, obj):
if self.user.is_superuser:
return True
return self.user in obj.organization.read_role return self.user in obj.organization.read_role
@check_superuser
def can_add(self, data): 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? if not data or '_method' in data: # So the browseable API will work?
return True return True
@@ -1276,10 +1291,8 @@ class LabelAccess(BaseAccess):
org = get_object_or_400(Organization, pk=org_pk) org = get_object_or_400(Organization, pk=org_pk)
return self.user in org.read_role return self.user in org.read_role
@check_superuser
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser:
return True
if self.can_add(data) is False: if self.can_add(data) is False:
return False return False
@@ -1376,26 +1389,10 @@ class CustomInventoryScriptAccess(BaseAccess):
return self.model.objects.distinct().all() return self.model.objects.distinct().all()
return self.model.accessible_objects(self.user, 'read_role').all() return self.model.accessible_objects(self.user, 'read_role').all()
@check_superuser
def can_read(self, obj): def can_read(self, obj):
if self.user.is_superuser:
return True
return self.user in obj.read_role 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): class TowerSettingsAccess(BaseAccess):
''' '''
@@ -1409,17 +1406,6 @@ class TowerSettingsAccess(BaseAccess):
model = TowerSettings 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): class RoleAccess(BaseAccess):
''' '''
@@ -1432,14 +1418,6 @@ class RoleAccess(BaseAccess):
model = Role 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): def can_read(self, obj):
if not obj: if not obj:
return False return False
@@ -1463,9 +1441,8 @@ class RoleAccess(BaseAccess):
skip_sub_obj_read_check=False): skip_sub_obj_read_check=False):
return self.can_unattach(obj, sub_obj, relationship) return self.can_unattach(obj, sub_obj, relationship)
@check_superuser
def can_unattach(self, obj, sub_obj, relationship): def can_unattach(self, obj, sub_obj, relationship):
if self.user.is_superuser:
return True
if obj.object_id and \ if obj.object_id and \
isinstance(obj.content_object, ResourceMixin) and \ isinstance(obj.content_object, ResourceMixin) and \
self.user in obj.content_object.admin_role: self.user in obj.content_object.admin_role:

View File

@@ -18,12 +18,12 @@ from django.db.models.fields.related import (
ReverseManyRelatedObjectsDescriptor, ReverseManyRelatedObjectsDescriptor,
) )
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.utils.timezone import now
# AWX # 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 from awx.main.utils import get_current_apps
__all__ = ['AutoOneToOneField', 'ImplicitRoleField'] __all__ = ['AutoOneToOneField', 'ImplicitRoleField']
@@ -92,9 +92,7 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
class ImplicitRoleField(models.ForeignKey): class ImplicitRoleField(models.ForeignKey):
"""Implicitly creates a role entry for a resource""" """Implicitly creates a role entry for a resource"""
def __init__(self, role_name=None, role_description=None, parent_role=None, *args, **kwargs): def __init__(self, parent_role=None, *args, **kwargs):
self.role_name = role_name
self.role_description = role_description if role_description else ""
self.parent_role = parent_role self.parent_role = parent_role
kwargs.setdefault('to', 'Role') kwargs.setdefault('to', 'Role')
@@ -104,8 +102,6 @@ class ImplicitRoleField(models.ForeignKey):
def deconstruct(self): def deconstruct(self):
name, path, args, kwargs = super(ImplicitRoleField, self).deconstruct() 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 kwargs['parent_role'] = self.parent_role
return name, path, args, kwargs return name, path, args, kwargs
@@ -190,11 +186,7 @@ class ImplicitRoleField(models.ForeignKey):
if cur_role is None: if cur_role is None:
missing_roles.append( missing_roles.append(
Role_( Role_(
created=now(),
modified=now(),
role_field=implicit_role_field.name, role_field=implicit_role_field.name,
name=implicit_role_field.role_name,
description=implicit_role_field.role_description,
content_type_id=ct_id, content_type_id=ct_id,
object_id=instance.id object_id=instance.id
) )
@@ -208,7 +200,7 @@ class ImplicitRoleField(models.ForeignKey):
updates[role.role_field] = role.id updates[role.role_field] = role.id
role_ids.append(role.id) role_ids.append(role.id)
type(instance).objects.filter(pk=instance.pk).update(**updates) 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 # Update parentage if necessary
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
@@ -247,12 +239,7 @@ class ImplicitRoleField(models.ForeignKey):
if qs.count() >= 1: if qs.count() >= 1:
role = qs[0] role = qs[0]
else: else:
role = Role_.objects.create(created=now(), role = Role_.objects.create(singleton_name=singleton_name, role_field=singleton_name)
modified=now(),
role_field=path,
singleton_name=singleton_name,
name=singleton_name,
description=singleton_name)
parents = [role.id] parents = [role.id]
else: else:
parents = resolve_role_field(instance, path) parents = resolve_role_field(instance, path)
@@ -269,4 +256,4 @@ class ImplicitRoleField(models.ForeignKey):
Role_ = get_current_apps().get_model('main', 'Role') 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)] 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_.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 # Sanity check: We sanitize these module names for our API but Ansible proper doesn't follow
# good naming conventions # good naming conventions
if source == 'azure': source = source.replace('azure.py', 'windows_azure.py')
source = 'windows_azure'
logger.debug('Analyzing type of source: %s', source) logger.debug('Analyzing type of source: %s', source)
original_all_group = all_group original_all_group = all_group
if not os.path.exists(source): if not os.path.exists(source):

View File

@@ -2,21 +2,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings from django.conf import settings
import taggit.managers
import awx.main.fields import awx.main.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('taggit', '0002_auto_20150616_2121'),
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('main', '0007_v300_active_flag_removal'), ('main', '0007_v300_active_flag_removal'),
] ]
operations = [ operations = [
#
# Patch up existing
#
migrations.RenameField( migrations.RenameField(
'Organization', 'Organization',
'admins', 'admins',
@@ -47,300 +47,6 @@ class Migration(migrations.Migration):
name='deprecated_projects', name='deprecated_projects',
field=models.ManyToManyField(related_name='deprecated_teams', to='main.Project', blank=True), 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( migrations.RenameField(
model_name='organization', model_name='organization',
old_name='projects', old_name='projects',
@@ -380,4 +86,245 @@ class Migration(migrations.Migration):
name='credential', name='credential',
unique_together=set([]), 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 __future__ import unicode_literals
from awx.main.migrations import _rbac as rbac from awx.main.migrations import _rbac as rbac
from awx.main.migrations import _migration_utils as migration_utils
from django.db import migrations from django.db import migrations
@@ -12,11 +13,14 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython(rbac.init_rbac_migration), migrations.RunPython(migration_utils.set_current_apps_for_migrations),
migrations.RunPython(rbac.migrate_users), migrations.RunPython(rbac.migrate_users),
migrations.RunPython(rbac.create_roles),
migrations.RunPython(rbac.migrate_organization), migrations.RunPython(rbac.migrate_organization),
migrations.RunPython(rbac.migrate_team), migrations.RunPython(rbac.migrate_team),
migrations.RunPython(rbac.migrate_inventory), migrations.RunPython(rbac.migrate_inventory),
migrations.RunPython(rbac.migrate_projects), migrations.RunPython(rbac.migrate_projects),
migrations.RunPython(rbac.migrate_credential), 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 __future__ import unicode_literals
from awx.main.migrations import _ask_for_variables as ask_for_variables 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 from django.db import migrations
@@ -12,5 +13,6 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
migrations.RunPython(ask_for_variables.migrate_credential), 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
return qs.filter( return qs.filter(
Q(pk=self.user.pk) | Q(pk=self.user.pk) |
Q(organizations__in=self.user.deprecated_admin_of_organizations) | Q(deprecated_organizations__in=self.user.deprecated_admin_of_organizations.all()) |
Q(organizations__in=self.user.deprecated_organizations) | Q(deprecated_organizations__in=self.user.deprecated_organizations.all()) |
Q(deprecated_teams__in=self.user.deprecated_teams) Q(deprecated_teams__in=self.user.deprecated_teams.all())
).distinct() ).distinct()
def can_add(self, data): def can_add(self, data):
@@ -563,18 +563,18 @@ class CredentialAccess(BaseAccess):
# If the user is a superuser, and therefore can see everything, this # If the user is a superuser, and therefore can see everything, this
# is also sufficient, and we are done. # is also sufficient, and we are done.
qs = self.model.objects.distinct() 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: if self.user.is_superuser:
return qs return qs
# Get the list of organizations for which the user is an admin # 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)) orgs_as_admin_ids = set(self.user.deprecated_admin_of_organizations.values_list('id', flat=True))
return qs.filter( return qs.filter(
Q(user=self.user) | Q(deprecated_user=self.user) |
Q(user__deprecated_organizations__id__in=orgs_as_admin_ids) | Q(deprecated_user__deprecated_organizations__id__in=orgs_as_admin_ids) |
Q(user__deprecated_admin_of_organizations__id__in=orgs_as_admin_ids) | Q(deprecated_user__deprecated_admin_of_organizations__id__in=orgs_as_admin_ids) |
Q(team__organization__id__in=orgs_as_admin_ids) | Q(deprecated_team__organization__id__in=orgs_as_admin_ids) |
Q(team__deprecated_users__in=[self.user]) Q(deprecated_team__deprecated_users__in=[self.user])
) )
def can_add(self, data): def can_add(self, data):
@@ -597,22 +597,22 @@ class CredentialAccess(BaseAccess):
return False return False
if self.user == obj.created_by: if self.user == obj.created_by:
return True return True
if obj.user: if obj.deprecated_user:
if self.user == obj.user: if self.user == obj.deprecated_user:
return True 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 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 return True
if obj.team: if obj.deprecated_team:
if self.user in obj.team.organization.deprecated_admins.all(): if self.user in obj.deprecated_team.organization.deprecated_admins.all():
return True return True
return False return False
def can_delete(self, obj): def can_delete(self, obj):
# Unassociated credentials may be marked deleted by anyone, though we # Unassociated credentials may be marked deleted by anyone, though we
# shouldn't ever end up with those. # 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 True
return self.can_change(obj, None) return self.can_change(obj, None)

View File

@@ -1,11 +1,12 @@
import logging import logging
from time import time
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.db.models import Q from django.db.models import Q
from django.utils.timezone import now
from collections import defaultdict 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 import _old_access as old_access
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -27,8 +28,35 @@ def log_migration(wrapped):
return wrapper return wrapper
@log_migration @log_migration
def init_rbac_migration(apps, schema_editor): def create_roles(apps, schema_editor):
set_current_apps(apps) '''
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 @log_migration
def migrate_users(apps, schema_editor): 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))) logger.info(smart_text(u"found existing role for user: {}".format(user.username)))
except Role.DoesNotExist: except Role.DoesNotExist:
role = Role.objects.create( role = Role.objects.create(
created=now(), role_field='admin_role',
modified=now(),
singleton_name = smart_text(u'{}-admin_role'.format(user.username)),
content_type = user_content_type, content_type = user_content_type,
object_id = user.id 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))) logger.info(smart_text(u"migrating to new role for user: {}".format(user.username)))
if user.is_superuser: if user.is_superuser:
if Role.objects.filter(singleton_name='System Administrator').exists(): if Role.objects.filter(singleton_name='system_administrator').exists():
sa_role = Role.objects.get(singleton_name='System Administrator') sa_role = Role.objects.get(singleton_name='system_administrator')
else: else:
sa_role = Role.objects.create( sa_role = Role.objects.create(
created=now(), singleton_name='system_administrator',
modified=now(), role_field='system_administrator'
singleton_name='System Administrator',
name='System Administrator'
) )
sa_role.members.add(user) sa_role.members.add(user)
@@ -71,19 +95,17 @@ def migrate_users(apps, schema_editor):
def migrate_organization(apps, schema_editor): def migrate_organization(apps, schema_editor):
Organization = apps.get_model('main', "Organization") Organization = apps.get_model('main', "Organization")
for org in Organization.objects.iterator(): for org in Organization.objects.iterator():
org.save() # force creates missing roles
for admin in org.deprecated_admins.all(): for admin in org.deprecated_admins.all():
org.admin_role.members.add(admin) org.admin_role.members.add(admin)
logger.info(smart_text(u"added admin: {}, {}".format(org.name, admin.username))) logger.info(smart_text(u"added admin: {}, {}".format(org.name, admin.username)))
for user in org.deprecated_users.all(): for user in org.deprecated_users.all():
org.auditor_role.members.add(user) org.member_role.members.add(user)
logger.info(smart_text(u"added auditor: {}, {}".format(org.name, user.username))) logger.info(smart_text(u"added member: {}, {}".format(org.name, user.username)))
@log_migration @log_migration
def migrate_team(apps, schema_editor): def migrate_team(apps, schema_editor):
Team = apps.get_model('main', 'Team') Team = apps.get_model('main', 'Team')
for t in Team.objects.iterator(): for t in Team.objects.iterator():
t.save()
for user in t.deprecated_users.all(): for user in t.deprecated_users.all():
t.member_role.members.add(user) t.member_role.members.add(user)
logger.info(smart_text(u"team: {}, added user: {}".format(t.name, user.username))) 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): def _update_credential_parents(org, cred):
org.admin_role.children.add(cred.owner_role) 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() cred.save()
def _discover_credentials(instances, cred, orgfunc): def _discover_credentials(instances, cred, orgfunc):
@@ -122,7 +142,12 @@ def _discover_credentials(instances, cred, orgfunc):
''' '''
orgs = defaultdict(list) orgs = defaultdict(list)
for inst in instances: 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: if len(orgs) == 1:
_update_credential_parents(orgfunc(instances[0]), cred) _update_credential_parents(orgfunc(instances[0]), cred)
@@ -136,7 +161,6 @@ def _discover_credentials(instances, cred, orgfunc):
cred.save() cred.save()
# Unlink the old information from the new credential # 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.owner_role, cred.use_role = None, None
cred.save() cred.save()
@@ -150,43 +174,32 @@ def migrate_credential(apps, schema_editor):
Credential = apps.get_model('main', "Credential") Credential = apps.get_model('main', "Credential")
JobTemplate = apps.get_model('main', 'JobTemplate') JobTemplate = apps.get_model('main', 'JobTemplate')
Project = apps.get_model('main', 'Project') Project = apps.get_model('main', 'Project')
Role = apps.get_model('main', 'Role')
User = apps.get_model('auth', 'User')
InventorySource = apps.get_model('main', 'InventorySource') 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(): for cred in Credential.objects.iterator():
cred.save() results = [x for x in JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all()] + \
results = (JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all() or [x for x in InventorySource.objects.filter(credential=cred).all()]
InventorySource.objects.filter(credential=cred).all()) if cred.deprecated_team is not None and results:
if results:
if len(results) == 1: if len(results) == 1:
_update_credential_parents(results[0].inventory.organization, cred) _update_credential_parents(results[0].inventory.organization, cred)
else: else:
_discover_credentials(results, cred, attrfunc('inventory.organization')) _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))) 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() projs = Project.objects.filter(credential=cred).all()
if projs: if cred.deprecated_team is not None and projs:
if len(projs) == 1: if len(projs) == 1:
_update_credential_parents(projs[0].organization, cred) _update_credential_parents(projs[0].organization, cred)
else: else:
_discover_credentials(projs, cred, attrfunc('organization')) _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))) 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: 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.owner_role)
cred.deprecated_team.member_role.children.add(cred.use_role)
cred.deprecated_user, cred.deprecated_team = None, None
cred.save() cred.save()
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host))) 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: elif cred.deprecated_user is not None:
user_admin_role = Role.objects.get(content_type=user_content_type, object_id=cred.deprecated_user.id) cred.owner_role.members.add(cred.deprecated_user)
user_admin_role.children.add(cred.owner_role)
cred.deprecated_user, cred.deprecated_team = None, None
cred.save() cred.save()
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host, ))) logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host, )))
else: else:
@@ -205,14 +218,13 @@ def migrate_inventory(apps, schema_editor):
return inventory.auditor_role return inventory.auditor_role
elif perm.permission_type == 'write': elif perm.permission_type == 'write':
return inventory.update_role 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. # These permission types are handled differntly in RBAC now, nothing to migrate.
return False return False
else: else:
return None return None
for inventory in Inventory.objects.iterator(): for inventory in Inventory.objects.iterator():
inventory.save()
for perm in Permission.objects.filter(inventory=inventory): for perm in Permission.objects.filter(inventory=inventory):
role = None role = None
execrole = None execrole = None
@@ -260,7 +272,6 @@ def migrate_projects(apps, schema_editor):
# Migrate projects to single organizations, duplicating as necessary # Migrate projects to single organizations, duplicating as necessary
for project in Project.objects.iterator(): for project in Project.objects.iterator():
project.save()
original_project_name = project.name original_project_name = project.name
project_orgs = project.deprecated_organizations.distinct().all() project_orgs = project.deprecated_organizations.distinct().all()
@@ -373,7 +384,6 @@ def migrate_job_templates(apps, schema_editor):
Permission = apps.get_model('main', 'Permission') Permission = apps.get_model('main', 'Permission')
for jt in JobTemplate.objects.iterator(): for jt in JobTemplate.objects.iterator():
jt.save()
permission = Permission.objects.filter( permission = Permission.objects.filter(
inventory=jt.inventory, inventory=jt.inventory,
project=jt.project, project=jt.project,
@@ -390,7 +400,7 @@ def migrate_job_templates(apps, schema_editor):
jt.execute_role.members.add(user) jt.execute_role.members.add(user)
logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name))) 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 # 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 # are a sytem, organization, or project admin, then don't add an explicit
# role entry for them # 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): if old_access.check_user_access(user, jt.__class__, 'start', jt, False):
jt.execute_role.members.add(user) jt.execute_role.members.add(user)
logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name))) 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) notifier = models.ManyToManyField("Notifier", blank=True)
notification = models.ManyToManyField("Notification", blank=True) notification = models.ManyToManyField("Notification", blank=True)
label = models.ManyToManyField("Label", blank=True) label = models.ManyToManyField("Label", blank=True)
role = models.ManyToManyField("Role", blank=True)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('api:activity_stream_detail', args=(self.pk,)) return reverse('api:activity_stream_detail', args=(self.pk,))

View File

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

View File

@@ -204,27 +204,19 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
help_text=_('Tenant identifier for this credential'), help_text=_('Tenant identifier for this credential'),
) )
owner_role = ImplicitRoleField( owner_role = ImplicitRoleField(
role_name='Credential Owner',
role_description='Owner of the credential',
parent_role=[ parent_role=[
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
], ],
) )
auditor_role = ImplicitRoleField( auditor_role = ImplicitRoleField(
role_name='Credential Auditor',
role_description='Auditor of the credential',
parent_role=[ parent_role=[
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
], ],
) )
use_role = ImplicitRoleField( 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'] parent_role=['owner_role']
) )
read_role = ImplicitRoleField( read_role = ImplicitRoleField(
role_name='Credential REad',
role_description='May read this credential',
parent_role=[ parent_role=[
'use_role', 'auditor_role', 'owner_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.'), help_text=_('Number of external inventory sources in this inventory with failures.'),
) )
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='Inventory Administrator',
role_description='May manage this inventory',
parent_role='organization.admin_role', parent_role='organization.admin_role',
) )
auditor_role = ImplicitRoleField( auditor_role = ImplicitRoleField(
role_name='Inventory Auditor',
role_description='May view but not modify this inventory',
parent_role='organization.auditor_role', parent_role='organization.auditor_role',
) )
update_role = ImplicitRoleField( update_role = ImplicitRoleField(
role_name='Inventory Updater',
role_description='May update the inventory',
parent_role=['admin_role'], parent_role=['admin_role'],
) )
use_role = ImplicitRoleField( 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'], parent_role=['admin_role'],
) )
adhoc_role = ImplicitRoleField( adhoc_role = ImplicitRoleField(
role_name='Inventory Ad Hoc',
role_description='May execute ad hoc commands against this inventory',
parent_role=['admin_role'], parent_role=['admin_role'],
) )
execute_role = ImplicitRoleField( execute_role = ImplicitRoleField(
role_name='Inventory Executor',
role_description='May execute jobs against this inventory',
parent_role='adhoc_role', parent_role='adhoc_role',
) )
read_role = ImplicitRoleField( read_role = ImplicitRoleField(
role_name='Read',
parent_role=['auditor_role', 'execute_role', 'update_role', 'use_role', 'admin_role'], parent_role=['auditor_role', 'execute_role', 'update_role', 'use_role', 'admin_role'],
role_description='May view this inventory',
) )
def get_absolute_url(self): def get_absolute_url(self):
@@ -335,7 +321,7 @@ class Inventory(CommonModel, ResourceMixin):
return self.groups.exclude(parents__pk__in=group_pks).distinct() return self.groups.exclude(parents__pk__in=group_pks).distinct()
class Host(CommonModelNameNotUnique, ResourceMixin): class Host(CommonModelNameNotUnique):
''' '''
A managed node A managed node
''' '''
@@ -531,28 +517,21 @@ class Group(CommonModelNameNotUnique, ResourceMixin):
help_text=_('Inventory source(s) that created or modified this group.'), help_text=_('Inventory source(s) that created or modified this group.'),
) )
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='Inventory Group Administrator',
parent_role=['inventory.admin_role', 'parents.admin_role'], parent_role=['inventory.admin_role', 'parents.admin_role'],
) )
auditor_role = ImplicitRoleField( auditor_role = ImplicitRoleField(
role_name='Inventory Group Auditor',
parent_role=['inventory.auditor_role', 'parents.auditor_role'], parent_role=['inventory.auditor_role', 'parents.auditor_role'],
) )
update_role = ImplicitRoleField( update_role = ImplicitRoleField(
role_name='Inventory Group Updater',
parent_role=['inventory.update_role', 'parents.update_role', 'admin_role'], parent_role=['inventory.update_role', 'parents.update_role', 'admin_role'],
) )
adhoc_role = ImplicitRoleField( adhoc_role = ImplicitRoleField(
role_name='Inventory Ad Hoc',
parent_role=['inventory.adhoc_role', 'parents.adhoc_role', 'admin_role'], parent_role=['inventory.adhoc_role', 'parents.adhoc_role', 'admin_role'],
role_description='May execute ad hoc commands against this inventory',
) )
execute_role = ImplicitRoleField( execute_role = ImplicitRoleField(
role_name='Inventory Group Executor',
parent_role=['inventory.execute_role', 'parents.execute_role', 'adhoc_role'], parent_role=['inventory.execute_role', 'parents.execute_role', 'adhoc_role'],
) )
read_role = ImplicitRoleField( read_role = ImplicitRoleField(
role_name='Inventory Group Executor',
parent_role=['execute_role', 'update_role', 'auditor_role', 'admin_role'], parent_role=['execute_role', 'update_role', 'auditor_role', 'admin_role'],
) )
@@ -1321,25 +1300,15 @@ class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
) )
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='CustomInventory Administrator',
role_description='May manage this inventory',
parent_role='organization.admin_role', parent_role='organization.admin_role',
) )
member_role = ImplicitRoleField( member_role = ImplicitRoleField(
role_name='CustomInventory Member',
role_description='May view but not modify this inventory',
parent_role='organization.member_role', parent_role='organization.member_role',
) )
auditor_role = ImplicitRoleField( auditor_role = ImplicitRoleField(
role_name='CustomInventory Auditor',
role_description='May view but not modify this inventory',
parent_role='organization.auditor_role', parent_role='organization.auditor_role',
) )
read_role = ImplicitRoleField( read_role = ImplicitRoleField(
role_name='CustomInventory Read',
role_description='May view but not modify this inventory',
parent_role=['auditor_role', 'member_role', 'admin_role'], parent_role=['auditor_role', 'member_role', 'admin_role'],
) )

View File

@@ -135,11 +135,6 @@ class JobOptions(BaseModel):
become_enabled = models.BooleanField( become_enabled = models.BooleanField(
default=False, default=False,
) )
labels = models.ManyToManyField(
"Label",
blank=True,
related_name='%(class)s_labels'
)
extra_vars_dict = VarsDictProperty('extra_vars', True) extra_vars_dict = VarsDictProperty('extra_vars', True)
@@ -226,23 +221,15 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
default={}, default={},
) )
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='Job Template Administrator',
role_description='Full access to all settings',
parent_role=[('project.admin_role', 'inventory.admin_role')] parent_role=[('project.admin_role', 'inventory.admin_role')]
) )
auditor_role = ImplicitRoleField( auditor_role = ImplicitRoleField(
role_name='Job Template Auditor',
role_description='Read-only access to all settings',
parent_role=[('project.auditor_role', 'inventory.auditor_role')] parent_role=[('project.auditor_role', 'inventory.auditor_role')]
) )
execute_role = ImplicitRoleField( execute_role = ImplicitRoleField(
role_name='Job Template Runner',
role_description='May run the job template',
parent_role=['admin_role'], parent_role=['admin_role'],
) )
read_role = ImplicitRoleField( read_role = ImplicitRoleField(
role_name='Job Template Runner',
role_description='May run the job template',
parent_role=['execute_role', 'auditor_role', 'admin_role'], 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 self.job_template.ask_variables_on_launch
return False 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): def get_passwords_needed_to_start(self):
return self.passwords_needed_to_start return self.passwords_needed_to_start

View File

@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
# AWX # AWX
from awx.main.models.base import CommonModelNameNotUnique from awx.main.models.base import CommonModelNameNotUnique
from awx.main.models.unified_jobs import UnifiedJobTemplate, UnifiedJob
__all__ = ('Label', ) __all__ = ('Label', )
@@ -39,3 +40,19 @@ class Label(CommonModelNameNotUnique):
jobtemplate_labels__isnull=True 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', related_name='deprecated_organizations',
) )
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='Organization Administrator',
role_description='May manage all aspects of this organization',
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
) )
auditor_role = ImplicitRoleField( auditor_role = ImplicitRoleField(
role_name='Organization Auditor',
role_description='May read all settings associated with this organization',
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
) )
member_role = ImplicitRoleField( member_role = ImplicitRoleField(
role_name='Organization Member',
role_description='A member of this organization',
parent_role='admin_role', parent_role='admin_role',
) )
read_role = ImplicitRoleField( read_role = ImplicitRoleField(
role_name='Organization Read Access',
role_description='Read an organization',
parent_role=['member_role', 'auditor_role'], parent_role=['member_role', 'auditor_role'],
) )
@@ -110,22 +102,13 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
related_name='deprecated_teams', related_name='deprecated_teams',
) )
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='Team Administrator',
role_description='May manage this team',
parent_role='organization.admin_role', parent_role='organization.admin_role',
) )
auditor_role = ImplicitRoleField( auditor_role = ImplicitRoleField(
role_name='Team Auditor',
role_description='May read all settings associated with this team',
parent_role='organization.auditor_role', parent_role='organization.auditor_role',
) )
member_role = ImplicitRoleField( member_role = ImplicitRoleField()
role_name='Team Member',
role_description='A member of this team',
)
read_role = ImplicitRoleField( read_role = ImplicitRoleField(
role_name='Read',
role_description='Can view this team',
parent_role=['admin_role', 'auditor_role', 'member_role'], parent_role=['admin_role', 'auditor_role', 'member_role'],
) )

View File

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

View File

@@ -8,7 +8,6 @@ import contextlib
# Django # Django
from django.db import models, transaction, connection from django.db import models, transaction, connection
from django.db.models import Q
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@@ -29,8 +28,39 @@ __all__ = [
logger = logging.getLogger('awx.main.models.rbac') logger = logging.getLogger('awx.main.models.rbac')
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator' ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='system_administrator'
ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor' 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 tls = threading.local() # thread local storage
@@ -51,23 +81,22 @@ def batch_role_ancestor_rebuilding(allow_nesting=False):
try: try:
setattr(tls, 'batch_role_rebuilding', True) setattr(tls, 'batch_role_rebuilding', True)
if not batch_role_rebuilding: if not batch_role_rebuilding:
setattr(tls, 'roles_needing_rebuilding', set()) setattr(tls, 'additions', set())
setattr(tls, 'removals', set())
yield yield
finally: finally:
setattr(tls, 'batch_role_rebuilding', batch_role_rebuilding) setattr(tls, 'batch_role_rebuilding', batch_role_rebuilding)
if not 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(): with transaction.atomic():
Role._simultaneous_ancestry_rebuild(list(rebuild_set)) Role.rebuild_role_ancestor_list(list(additions), list(removals))
delattr(tls, 'additions')
#for role in Role.objects.filter(id__in=list(rebuild_set)).all(): delattr(tls, 'removals')
# # 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')
class Role(CommonModelNameNotUnique): class Role(models.Model):
''' '''
Role model Role model
''' '''
@@ -76,9 +105,12 @@ class Role(CommonModelNameNotUnique):
app_label = 'main' app_label = 'main'
verbose_name_plural = _('roles') verbose_name_plural = _('roles')
db_table = 'main_rbac_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) 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') parents = models.ManyToManyField('Role', related_name='children')
implicit_parents = models.TextField(null=False, default='[]') implicit_parents = models.TextField(null=False, default='[]')
ancestors = models.ManyToManyField( ancestors = models.ManyToManyField(
@@ -94,7 +126,7 @@ class Role(CommonModelNameNotUnique):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(Role, self).save(*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): def get_absolute_url(self):
return reverse('api:role_detail', args=(self.pk,)) return reverse('api:role_detail', args=(self.pk,))
@@ -112,20 +144,36 @@ class Role(CommonModelNameNotUnique):
object_id=accessor.id) object_id=accessor.id)
return self.ancestors.filter(pk__in=roles).exists() 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 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 You should never need to call this. Signal handlers should be calling
this method when the role hierachy changes automatically. 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]) # The ancestry table
# =================================================
#
@staticmethod # The role ancestors table denormalizes the parental relations
def _simultaneous_ancestry_rebuild(role_ids_to_rebuild): # 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 # The simple version of what this function is doing
# ================================================= # =================================================
@@ -163,37 +211,18 @@ class Role(CommonModelNameNotUnique):
# #
# SQL Breakdown # 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 # We operate under the assumption that our parent's ancestor list is
# correct, thus we can always compute what our ancestor list should # correct, thus we can always compute what our ancestor list should
# be by taking the union of our parent's ancestor lists and adding # 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 DELETE query deletes all entries in the ancestor table that
# the union of the parent's ancestors and the self referncing entry, # should no longer be there (as determined by the NOT EXISTS query,
# for all roles in the current set of roles to rebuild. # 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 # The INSERT query computes the list of what our ancestor maps should
# roles we're dealing with, and removes the entries that are not in # be, and inserts any missing entries.
# 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.
# #
# Once complete, we select all of the children for the roles we are # 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 # 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 return
global tls global tls
batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False) batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False)
if batch_role_rebuilding: if batch_role_rebuilding:
roles_needing_rebuilding = getattr(tls, 'roles_needing_rebuilding') getattr(tls, 'additions').update(set(additions))
roles_needing_rebuilding.update(set(role_ids_to_rebuild)) getattr(tls, 'removals').update(set(removals))
return return
cursor = connection.cursor() cursor = connection.cursor()
loop_ct = 0 loop_ct = 0
@@ -226,94 +254,143 @@ class Role(CommonModelNameNotUnique):
'roles_table': Role._meta.db_table, '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): def split_ids_for_sqlite(role_ids):
for i in xrange(0, len(role_ids), 999): for i in xrange(0, len(role_ids), 40000):
yield role_ids[i:i + 999] yield role_ids[i:i + 40000]
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)
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 @staticmethod
def visible_roles(user): 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 @staticmethod
def singleton(name): 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 return role
def is_ancestor_of(self, role): def is_ancestor_of(self, role):
@@ -328,6 +405,7 @@ class RoleAncestorEntry(models.Model):
index_together = [ index_together = [
("ancestor", "content_type_id", "object_id"), # used by get_roles_on_resource ("ancestor", "content_type_id", "object_id"), # used by get_roles_on_resource
("ancestor", "content_type_id", "role_field"), # used by accessible_objects ("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='+') 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, ancestor__in=roles,
content_type_id=ContentType.objects.get_for_model(resource).id, content_type_id=ContentType.objects.get_for_model(resource).id,
object_id=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', default='ok',
editable=False, editable=False,
) )
labels = models.ManyToManyField(
"Label",
blank=True,
related_name='%(class)s_labels'
)
def get_absolute_url(self): def get_absolute_url(self):
real_instance = self.get_real_instance() real_instance = self.get_real_instance()
@@ -476,6 +481,12 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
default='', default='',
editable=False, editable=False,
) )
labels = models.ManyToManyField(
"Label",
blank=True,
related_name='%(class)s_labels'
)
def get_absolute_url(self): def get_absolute_url(self):
real_instance = self.get_real_instance() real_instance = self.get_real_instance()

View File

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

View File

@@ -3,7 +3,7 @@
import logging 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') logger = logging.getLogger('awx.main.registrar')
@@ -17,12 +17,12 @@ class ActivityStreamRegistrar(object):
if not getattr(tower_settings, 'ACTIVITY_STREAM_ENABLED', True): if not getattr(tower_settings, 'ACTIVITY_STREAM_ENABLED', True):
return return
from awx.main.signals import activity_stream_create, activity_stream_update, activity_stream_delete, activity_stream_associate from awx.main.signals import activity_stream_create, activity_stream_update, activity_stream_delete, activity_stream_associate
if model not in self.models: if model not in self.models:
self.models.append(model) self.models.append(model)
post_save.connect(activity_stream_create, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_create") 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") 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: for m2mfield in model._meta.many_to_many:
try: try:
@@ -36,7 +36,7 @@ class ActivityStreamRegistrar(object):
if model in self.models: if model in self.models:
post_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_create") post_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_create")
pre_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_update") 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) 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): 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' '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: if reverse:
for id in pk_set: model.rebuild_role_ancestor_list(list(pk_set), [])
model.objects.get(id=id).rebuild_role_ancestor_list()
else: 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): 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' '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( Role.objects.get(
content_type=ContentType.objects.get_for_model(instance), content_type=ContentType.objects.get_for_model(instance),
object_id=instance.id, object_id=instance.id,
name = 'User Admin' role_field='admin_role'
) )
except Role.DoesNotExist: except Role.DoesNotExist:
role = Role.objects.create( role = Role.objects.create(
name = 'User Admin',
role_field='admin_role', role_field='admin_role',
content_object = instance, content_object = instance,
) )
@@ -152,6 +156,24 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs):
if action == 'pre_remove': if action == 'pre_remove':
instance.content_object.admin_role.children.remove(user.admin_role) 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_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
post_delete.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) post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent)
m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through) m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through)
m2m_changed.connect(org_admin_edit_members, Role.members.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(sync_superuser_status_to_rbac, sender=User)
post_save.connect(create_user_role, 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 # 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): def activity_stream_delete(sender, instance, **kwargs):
if not activity_stream_enabled: if not activity_stream_enabled:
return return
try:
old = sender.objects.get(id=instance.id)
except sender.DoesNotExist:
return
# Skip recording any inventory source directly associated with a group. # Skip recording any inventory source directly associated with a group.
if isinstance(instance, InventorySource) and instance.group: if isinstance(instance, InventorySource) and instance.group:
return return
changes = model_instance_diff(old, instance) changes = model_to_dict(instance)
object1 = camelcase_to_underscore(instance.__class__.__name__) object1 = camelcase_to_underscore(instance.__class__.__name__)
activity_entry = ActivityStream( activity_entry = ActivityStream(
operation='delete', operation='delete',
@@ -349,7 +369,7 @@ def activity_stream_delete(sender, instance, **kwargs):
def activity_stream_associate(sender, instance, **kwargs): def activity_stream_associate(sender, instance, **kwargs):
if not activity_stream_enabled: if not activity_stream_enabled:
return 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': if kwargs['action'] == 'pre_add':
action = 'associate' action = 'associate'
elif kwargs['action'] == 'pre_remove': 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, object1).add(obj1)
getattr(activity_entry, object2).add(obj2_actual) 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) @receiver(current_user_getter)
def get_current_user_from_drf_request(sender, **kwargs): 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_body = instance.notification_data()
notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name, notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name,
task_actual['id'], task_actual['id'],
instance_name, smart_text(instance_name),
notification_body['url']) notification_body['url'])
notification_body['friendly_name'] = friendly_name notification_body['friendly_name'] = friendly_name
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id 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 instance_name = instance.module_name
notifiers = [] notifiers = []
friendly_name = "AdHoc Command" friendly_name = "AdHoc Command"
elif task_actual['type'] == 'system_job': elif each_task['type'] == 'system_job':
instance = SystemJob.objects.get(id=task_actual['id']) instance = SystemJob.objects.get(id=each_task['id'])
instance_name = instance.system_job_template.name instance_name = instance.system_job_template.name
notifiers = instance.system_job_template.notifiers notifiers = instance.system_job_template.notifiers
friendly_name = "System Job" friendly_name = "System Job"
@@ -270,7 +270,7 @@ def handle_work_error(self, task_id, subtasks=None):
notification_body = first_task.notification_data() notification_body = first_task.notification_data()
notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name, notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name,
first_task_id, first_task_id,
first_task_name, smart_text(first_task_name),
notification_body['url']) notification_body['url'])
notification_body['friendly_name'] = first_task_friendly_name notification_body['friendly_name'] = first_task_friendly_name
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
@@ -558,7 +558,7 @@ class BaseTask(Task):
instance = self.update_model(instance.pk) instance = self.update_model(instance.pk)
if instance.cancel_flag: if instance.cancel_flag:
try: 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 # NOTE: Refactor this once we get a newer psutil across the board
if not psutil: if not psutil:
os.kill(child.pid, signal.SIGKILL) os.kill(child.pid, signal.SIGKILL)
@@ -1615,6 +1615,9 @@ class RunAdHocCommand(BaseTask):
if ad_hoc_command.verbosity: if ad_hoc_command.verbosity:
args.append('-%s' % ('v' * min(5, 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(['-m', ad_hoc_command.module_name])
args.extend(['-a', ad_hoc_command.module_args]) 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.status_code == 200
assert response.data['summary_fields']['actor']['username'] == 'admin-poster' 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 @pytest.mark.django_db
def test_create_user_credential_via_user_credentials_list(post, get, alice): def test_create_user_credential_via_user_credentials_list(post, get, alice):
response = post(reverse('api:user_credentials_list', args=(alice.pk,)), { response = post(reverse('api:user_credentials_list', args=(alice.pk,)), {
'user': alice.pk,
'name': 'Some name', 'name': 'Some name',
'username': 'someusername', 'username': 'someusername',
}, alice) }, alice)
@@ -45,6 +46,7 @@ def test_create_user_credential_via_credentials_list_xfail(post, alice, bob):
@pytest.mark.django_db @pytest.mark.django_db
def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob): def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob):
response = post(reverse('api:user_credentials_list', args=(bob.pk,)), { response = post(reverse('api:user_credentials_list', args=(bob.pk,)), {
'user': bob.pk,
'name': 'Some name', 'name': 'Some name',
'username': 'someusername' 'username': 'someusername'
}, alice) }, alice)
@@ -71,6 +73,7 @@ def test_create_team_credential(post, get, team, org_admin, team_member):
@pytest.mark.django_db @pytest.mark.django_db
def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member): 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,)), { response = post(reverse('api:team_credentials_list', args=(team.pk,)), {
'team': team.pk,
'name': 'Some name', 'name': 'Some name',
'username': 'someusername', 'username': 'someusername',
}, org_admin) }, org_admin)

View File

@@ -22,6 +22,10 @@ def runtime_data(organization):
credential=cred_obj.pk, 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 @pytest.fixture
def job_template_prompts(project, inventory, machine_credential): def job_template_prompts(project, inventory, machine_credential):
def rf(on_off): 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)) dict(extra_vars='{"unbalanced brackets":'), user('admin', True))
assert response.status_code == 400 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.django_db
@pytest.mark.job_runtime_vars @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)) args=[deploy_jobtemplate.pk]), {}, user('admin', True))
assert response.status_code == 400 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.django_db
@pytest.mark.job_runtime_vars @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) job_template = job_template_prompts(True)
common_user = user('test-user', False) common_user = user('test-user', False)
job_template.execute_role.members.add(common_user) 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 # 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]), 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.status_code == 403
assert response.data['detail'] == u'You do not have permission to perform this action.' assert response.data['detail'] == u'You do not have permission to perform this action.'
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.job_runtime_vars @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) 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 # Assure that giving a credential without access blocks the launch
job_create_kwargs = runtime_data runtime_credential = Credential.objects.get(pk=runtime_data['credential'])
inv_obj = Inventory.objects.get(pk=job_create_kwargs.pop('inventory')) response = post(reverse('api:job_template_launch', args=[job_template.pk]),
cred_obj = Credential.objects.get(pk=job_create_kwargs.pop('credential')) dict(credential=runtime_credential.pk), common_user)
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()): assert response.status_code == 403
second_job = original_job.copy() 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 # Check that job data matches the original variables
assert 'job_launch_var' in yaml.load(second_job.extra_vars) assert second_job.credential == job_with_links.credential
assert original_job.limit == second_job.limit assert second_job.inventory == job_with_links.inventory
assert original_job.job_type == second_job.job_type assert second_job.limit == 'my_server'
assert original_job.inventory.pk == second_job.inventory.pk
assert original_job.job_tags == second_job.job_tags @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 @pytest.mark.django_db
def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate): 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_full] == COUNTS_PRIMES
assert counts[org_id_zero] == COUNTS_ZEROS 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 @pytest.mark.django_db
def test_JT_associated_with_project(organizations, project, user, get): def test_JT_associated_with_project(organizations, project, user, get):
# Check that adding a project to an organization gets the project's JT # Check that adding a project to an organization gets the project's JT

View File

@@ -1,6 +1,7 @@
import pytest import pytest
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from awx.main.models import Role
@pytest.mark.django_db @pytest.mark.django_db
def test_indirect_access_list(get, organization, project, team_factory, user, admin): 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 assert org_admin_team_member_entry['team_name'] == org_admin_team.name
admin_entry = admin_res['summary_fields']['indirect_access'][0]['role'] 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, Team,
) )
from awx.main.models.rbac import Role
from awx.main.models.notifications import Notifier from awx.main.models.notifications import Notifier
''' '''
@@ -193,11 +192,6 @@ def notifier(organization):
notification_type="webhook", notification_type="webhook",
notification_configuration=dict(url="http://localhost", notification_configuration=dict(url="http://localhost",
headers={"Test": "Header"})) headers={"Test": "Header"}))
@pytest.fixture
def role():
return Role.objects.create(name='role')
@pytest.fixture @pytest.fixture
def admin(user): def admin(user):
return user('admin', True) 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) #@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.fixture
def role():
return Role.objects.create()
# #
# /roles # /roles
@@ -85,13 +89,14 @@ def test_get_user_roles_list(get, admin):
response = get(url, admin) response = get(url, admin)
assert response.status_code == 200 assert response.status_code == 200
roles = response.data 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 @pytest.mark.django_db
def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob): 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' '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.member_role.members.add(alice)
organization.admin_role.members.add(bob) 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') custom_role = Role.objects.create(name='custom_role-test_user_view_admin_roles_list')
organization.member_role.children.add(custom_role) organization.member_role.children.add(custom_role)
team.member_role.members.add(bob) team.member_role.members.add(bob)

View File

@@ -11,8 +11,8 @@ from awx.main.models import (
@pytest.mark.django_db @pytest.mark.django_db
def test_auto_inheritance_by_children(organization, alice): def test_auto_inheritance_by_children(organization, alice):
A = Role.objects.create(name='A', role_field='') A = Role.objects.create()
B = Role.objects.create(name='B', role_field='') B = Role.objects.create()
A.members.add(alice) A.members.add(alice)
assert alice not in organization.admin_role assert alice not in organization.admin_role
@@ -38,8 +38,8 @@ def test_auto_inheritance_by_children(organization, alice):
@pytest.mark.django_db @pytest.mark.django_db
def test_auto_inheritance_by_parents(organization, alice): def test_auto_inheritance_by_parents(organization, alice):
A = Role.objects.create(name='A') A = Role.objects.create()
B = Role.objects.create(name='B') B = Role.objects.create()
A.members.add(alice) A.members.add(alice)
assert alice not in organization.admin_role assert alice not in organization.admin_role
@@ -58,9 +58,9 @@ def test_auto_inheritance_by_parents(organization, alice):
@pytest.mark.django_db @pytest.mark.django_db
def test_accessible_objects(organization, alice, bob): def test_accessible_objects(organization, alice, bob):
A = Role.objects.create(name='A') A = Role.objects.create()
A.members.add(alice) A.members.add(alice)
B = Role.objects.create(name='B') B = Role.objects.create()
B.members.add(alice) B.members.add(alice)
B.members.add(bob) B.members.add(bob)
@@ -118,7 +118,7 @@ def test_auto_field_adjustments(organization, inventory, team, alice):
def test_implicit_deletes(alice): def test_implicit_deletes(alice):
'Ensures implicit resources and roles delete themselves' 'Ensures implicit resources and roles delete themselves'
delorg = Organization.objects.create(name='test-org') delorg = Organization.objects.create(name='test-org')
child = Role.objects.create(name='child-role') child = Role.objects.create()
child.parents.add(delorg.admin_role) child.parents.add(delorg.admin_role)
delorg.admin_role.members.add(alice) 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=admin_role_id).count() == 1
assert Role.objects.filter(id=auditor_role_id).count() == 1 assert Role.objects.filter(id=auditor_role_id).count() == 1
n_alice_roles = alice.roles.count() 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() delorg.delete()
assert Role.objects.filter(id=admin_role_id).count() == 0 assert Role.objects.filter(id=admin_role_id).count() == 0
assert Role.objects.filter(id=auditor_role_id).count() == 0 assert Role.objects.filter(id=auditor_role_id).count() == 0
assert alice.roles.count() == (n_alice_roles - 1) 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.count() == 1
assert child.ancestors.all()[0] == child assert child.ancestors.all()[0] == child
@@ -152,11 +152,11 @@ def test_content_object(user):
def test_hierarchy_rebuilding_multi_path(): 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' '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') X = Role.objects.create()
A = Role.objects.create(name='A') A = Role.objects.create()
B = Role.objects.create(name='B') B = Role.objects.create()
C = Role.objects.create(name='C') C = Role.objects.create()
D = Role.objects.create(name='D') D = Role.objects.create()
A.children.add(B) A.children.add(B)
A.children.add(D) A.children.add(D)

View File

@@ -27,7 +27,7 @@ def test_credential_use_role(credential, user, permissions):
@pytest.mark.django_db @pytest.mark.django_db
def test_credential_migration_team_member(credential, team, user, permissions): def test_credential_migration_team_member(credential, team, user, permissions):
u = user('user', False) u = user('user', False)
team.admin_role.members.add(u) team.member_role.members.add(u)
credential.deprecated_team = team credential.deprecated_team = team
credential.save() credential.save()
@@ -91,7 +91,8 @@ def test_credential_access_admin(user, team, credential):
assert access.can_change(credential, {'user': u.pk}) assert access.can_change(credential, {'user': u.pk})
@pytest.mark.django_db @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) a = user('admin', False)
org = deploy_jobtemplate.project.organization org = deploy_jobtemplate.project.organization
org.admin_role.members.add(a) org.admin_role.members.add(a)
@@ -102,19 +103,17 @@ def test_cred_job_template(user, deploy_jobtemplate):
access = CredentialAccess(a) access = CredentialAccess(a)
rbac.migrate_credential(apps, None) 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}) assert not access.can_change(cred, {'organization': org.pk})
@pytest.mark.django_db @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) a = user('admin', False)
org = deploy_jobtemplate.project.organization org = deploy_jobtemplate.project.organization
org.admin_role.members.add(a) org.admin_role.members.add(a)
cred = deploy_jobtemplate.credential cred = deploy_jobtemplate.credential
cred.deprecated_user = user('john', False) cred.deprecated_team = team
cred.save() cred.save()
access = CredentialAccess(a) 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}) assert not access.can_change(cred, {'organization': org.pk})
@pytest.mark.django_db @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) orgs = organizations(2)
credential.deprecated_team = team
credential.save()
jts = [] jts = []
for org in orgs: for org in orgs:
inv = org.inventories.create(name="inv-%d" % org.pk) 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 assert u not in credential.use_role
rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
assert u in credential.use_role assert u not in credential.use_role
@pytest.mark.django_db @pytest.mark.django_db
def test_cred_project(user, credential, project): 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 assert u not in credential.use_role
rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
assert u in credential.use_role assert u not in credential.use_role
@pytest.mark.django_db @pytest.mark.django_db
def test_cred_no_org(user, credential): 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_projects(apps, None)
rbac.migrate_inventory(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 admin in check_jobtemplate.execute_role
assert joe not 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_projects(apps, None)
rbac.migrate_inventory(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 admin in deploy_jobtemplate.execute_role
assert joe not in deploy_jobtemplate.execute_role assert joe not in deploy_jobtemplate.execute_role
rbac.migrate_job_templates(apps, None) rbac.migrate_job_templates(apps, None)
assert joe in deploy_jobtemplate.read_role
assert admin in deploy_jobtemplate.execute_role assert admin in deploy_jobtemplate.execute_role
assert joe 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): def test_project_accessible_by_sa(user, project):
u = user('systemadmin', is_superuser=True) 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 # 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 assert u not in project.read_role
rbac.migrate_organization(apps, None) rbac.migrate_organization(apps, None)

View File

@@ -3,6 +3,25 @@ import pytest
from awx.main.access import TeamAccess from awx.main.access import TeamAccess
from awx.main.models import Project 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 @pytest.mark.django_db
def test_team_access_superuser(team, user): def test_team_access_superuser(team, user):
team.member_role.members.add(user('member', False)) 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) joe = user(username, is_superuser = False)
admin = user('admin', is_superuser = True) admin = user('admin', is_superuser = True)
sa = Role.singleton('System Administrator') sa = Role.singleton('system_administrator')
# this should happen automatically with our signal # this should happen automatically with our signal
assert sa.members.filter(id=admin.id).exists() is True 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 # Python
import pytest import pytest
import mock
# DRF # DRF
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
# AWX # AWX
from awx.api.generics import ParentMixin, SubListCreateAttachDetachAPIView from awx.api.generics import ParentMixin, SubListCreateAttachDetachAPIView, DeleteLastUnattachLabelMixin
@pytest.fixture @pytest.fixture
def get_object_or_404(mocker): def get_object_or_404(mocker):
@@ -37,7 +38,7 @@ def parent_relationship_factory(mocker):
return (serializer, mock_parent_relationship) return (serializer, mock_parent_relationship)
return rf 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) # TODO: Mock and check return (Response)
class TestSubListCreateAttachDetachAPIView: class TestSubListCreateAttachDetachAPIView:
def test_attach_create_and_associate(self, mocker, get_object_or_400, parent_relationship_factory, mock_response_new): 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_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) 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: class TestParentMixin:
def test_get_parent_object(self, mocker, get_object_or_404): def test_get_parent_object(self, mocker, get_object_or_404):
parent_mixin = ParentMixin() parent_mixin = ParentMixin()

View File

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

View File

@@ -1,4 +1,8 @@
import pytest
from awx.main.models.label import Label from awx.main.models.label import Label
from awx.main.models.unified_jobs import UnifiedJobTemplate, UnifiedJob
def test_get_orphaned_labels(mocker): def test_get_orphaned_labels(mocker):
mock_query_set = mocker.MagicMock() mock_query_set = mocker.MagicMock()
@@ -8,3 +12,54 @@ def test_get_orphaned_labels(mocker):
assert mock_query_set == ret assert mock_query_set == ret
Label.objects.filter.assert_called_with(organization=None, jobtemplate_labels__isnull=True) 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 # pass
# statsd = NoStatsClient() # statsd = NoStatsClient()
CENSOR_FIELD_WHITELIST=[ CENSOR_FIELD_WHITELIST = [
'msg', 'msg',
'failed', 'failed',
'changed', 'changed',
@@ -80,7 +80,6 @@ CENSOR_FIELD_WHITELIST=[
'delta', 'delta',
'cmd', 'cmd',
'_ansible_no_log', '_ansible_no_log',
'cmd',
'rc', 'rc',
'failed_when_result', 'failed_when_result',
'skipped', '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" obj['results'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
return obj return obj
class TokenAuth(requests.auth.AuthBase): class TokenAuth(requests.auth.AuthBase):
def __init__(self, token): def __init__(self, token):
@@ -194,31 +194,7 @@ class BaseCallbackModule(object):
self._init_connection() self._init_connection()
if self.context is None: if self.context is None:
self._start_connection() 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.send_json(msg)
self.socket.recv() self.socket.recv()
return return

View File

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

View File

@@ -59,8 +59,8 @@ class ServiceScanService(BaseService):
initctl_path = self.module.get_bin_path("initctl") initctl_path = self.module.get_bin_path("initctl")
chkconfig_path = self.module.get_bin_path("chkconfig") chkconfig_path = self.module.get_bin_path("chkconfig")
# Upstart and sysvinit # sysvinit
if initctl_path is not None and chkconfig_path is None: 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) 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"): for line in stdout.split("\n"):
line_data = line.split() line_data = line.split()
@@ -72,6 +72,9 @@ class ServiceScanService(BaseService):
else: else:
service_state = "stopped" service_state = "stopped"
services.append({"name": service_name, "state": service_state, "source": "sysv"}) 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*$') 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) rc, stdout, stderr = self.module.run_command("%s list" % initctl_path)
real_stdout = stdout.replace("\r","") real_stdout = stdout.replace("\r","")

View File

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

View File

@@ -24,7 +24,7 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="{% url 'api:api_root_view' %}"> <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> <span>{% trans 'REST API' %}</span>
</a> </a>
<a class="navbar-title" href="{{ request.get_full_path }}"> <a class="navbar-title" href="{{ request.get_full_path }}">
@@ -49,10 +49,7 @@
<div id="footer"> <div id="footer">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-sm-6 footer-logo"> <div class="col-sm-6">
<a href="http://www.ansible.com" target="_blank">
<img alt="Red Hat, Inc. | Ansible, Inc." src="{% static 'assets/footer-logo.png' %}" />
</a>
</div> </div>
<div class="col-sm-6 footer-copyright"> <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. 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; font-size: 10.5px;
} }
.ask-checkbox {
margin-left: 10px;
}
.no-padding { .no-padding {
padding: 0; padding: 0;
margin: 0; margin: 0;

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
@import "awx/ui/client/src/shared/branding/colors.default.less"; @import "awx/ui/client/src/shared/branding/colors.default.less";
.About-cowsay--container{ .About-cowsay--container{
width: 340px; width: 340px;
margin: 0 auto; margin: 0 auto;
} }
.About-cowsay--code{ .About-cowsay--code{
@@ -23,10 +23,8 @@
padding-top: 0px; padding-top: 0px;
} }
.About-brand--redhat{ .About-brand--redhat{
max-width: 420px; float: left;
margin: 0 auto; width: 112px;
margin-top: -50px;
margin-bottom: -30px;
} }
.About-brand--ansible{ .About-brand--ansible{
max-width: 120px; max-width: 120px;
@@ -36,7 +34,14 @@
position: absolute; position: absolute;
top: 15px; top: 15px;
right: 15px; right: 15px;
z-index: 10;
} }
.About p{ .About p{
color: @default-interface-txt; 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){ var processVersion = function(version){
// prettify version & calculate padding // prettify version & calculate padding
// e,g 3.0.0-0.git201602191743/ -> 3.0.0 // 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), var spaces = Math.floor((16-split.length)/2),
paddedStr = ""; paddedStr = "";
for(var i=0; i<=spaces; i++){ for(var i=0; i<=spaces; i++){
@@ -13,7 +13,7 @@ export default
for(var j = paddedStr.length; j<16; j++){ for(var j = paddedStr.length; j<16; j++){
paddedStr = paddedStr + " "; paddedStr = paddedStr + " ";
} }
return paddedStr return paddedStr;
}; };
var init = function(){ var init = function(){
CheckLicense.get() CheckLicense.get()
@@ -28,4 +28,4 @@ export default
}); });
init(); init();
} }
]; ];

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,12 +5,13 @@
*************************************************/ *************************************************/
var urlPrefix; var urlPrefix;
var $basePath;
if ($basePath) { if ($basePath) {
urlPrefix = $basePath; urlPrefix = $basePath;
} else { } else {
// required to make tests work // required to make tests work
var $basePath = '/static/'; $basePath = '/static/';
urlPrefix = $basePath; urlPrefix = $basePath;
} }
@@ -20,7 +21,7 @@ import './lists';
import './widgets'; import './widgets';
import './help'; import './help';
import './filters'; import './filters';
import {Home, HomeGroups, HomeHosts} from './controllers/Home'; import {Home, HomeGroups} from './controllers/Home';
import {SocketsController} from './controllers/Sockets'; import {SocketsController} from './controllers/Sockets';
import {CredentialsAdd, CredentialsEdit, CredentialsList} from './controllers/Credentials'; import {CredentialsAdd, CredentialsEdit, CredentialsList} from './controllers/Credentials';
import {JobsListController} from './controllers/Jobs'; import {JobsListController} from './controllers/Jobs';
@@ -49,20 +50,17 @@ import adhoc from './adhoc/main';
import login from './login/main'; import login from './login/main';
import activityStream from './activity-stream/main'; import activityStream from './activity-stream/main';
import standardOut from './standard-out/main'; import standardOut from './standard-out/main';
import lookUpHelper from './lookup/main';
import JobTemplates from './job-templates/main'; import JobTemplates from './job-templates/main';
import search from './search/main'; import search from './search/main';
import {ScheduleEditController} from './controllers/Schedules';
import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects'; import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
import OrganizationsList from './organizations/list/organizations-list.controller'; import OrganizationsList from './organizations/list/organizations-list.controller';
import OrganizationsAdd from './organizations/add/organizations-add.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 {AdminsList} from './controllers/Admins';
import {UsersList, UsersAdd, UsersEdit} from './controllers/Users'; import {UsersList, UsersAdd, UsersEdit} from './controllers/Users';
import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams'; import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams';
import RestServices from './rest/main'; import RestServices from './rest/main';
import './lookup/main';
import './shared/api-loader'; import './shared/api-loader';
import './shared/form-generator'; import './shared/form-generator';
import './shared/Modal'; import './shared/Modal';
@@ -175,7 +173,6 @@ var tower = angular.module('Tower', [
'CredentialsHelper', 'CredentialsHelper',
'StreamListDefinition', 'StreamListDefinition',
'HomeGroupListDefinition', 'HomeGroupListDefinition',
'HomeHostListDefinition',
'ActivityDetailDefinition', 'ActivityDetailDefinition',
'VariablesHelper', 'VariablesHelper',
'SchedulesListDefinition', 'SchedulesListDefinition',
@@ -198,7 +195,7 @@ var tower = angular.module('Tower', [
'pendolytics', 'pendolytics',
'ui.router', 'ui.router',
'ncy-angular-breadcrumb', 'ncy-angular-breadcrumb',
'scheduler', scheduler.name,
'ApiModelHelper', 'ApiModelHelper',
'ActivityStreamHelper', 'ActivityStreamHelper',
'dndLists' 'dndLists'
@@ -261,30 +258,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: { ncyBreadcrumb: {
parent: 'dashboard', parent: 'dashboard',
label: "GROUPS" 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, controller: JobsListController,
ncyBreadcrumb: { ncyBreadcrumb: {
label: "JOBS" label: "JOBS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -312,11 +280,6 @@ var tower = angular.module('Tower', [
}, },
ncyBreadcrumb: { ncyBreadcrumb: {
label: "PROJECTS" label: "PROJECTS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -327,11 +290,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: { ncyBreadcrumb: {
parent: "projects", parent: "projects",
label: "CREATE PROJECT" label: "CREATE PROJECT"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -341,79 +299,19 @@ var tower = angular.module('Tower', [
controller: ProjectsEdit, controller: ProjectsEdit,
data: { data: {
activityStreamId: 'id' activityStreamId: 'id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
state('projectOrganizations', { state('projectOrganizations', {
url: '/projects/:project_id/organizations', url: '/projects/:project_id/organizations',
templateUrl: urlPrefix + 'partials/projects.html', templateUrl: urlPrefix + 'partials/projects.html',
controller: OrganizationsList, controller: OrganizationsList
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('projectOrganizationAdd', { state('projectOrganizationAdd', {
url: '/projects/:project_id/organizations/add', url: '/projects/:project_id/organizations/add',
templateUrl: urlPrefix + 'partials/projects.html', templateUrl: urlPrefix + 'partials/projects.html',
controller: OrganizationsAdd, controller: OrganizationsAdd
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
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', { state('teams', {
url: '/teams', url: '/teams',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
@@ -425,11 +323,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: { ncyBreadcrumb: {
parent: 'setup', parent: 'setup',
label: 'TEAMS' label: 'TEAMS'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -440,11 +333,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: { ncyBreadcrumb: {
parent: "teams", parent: "teams",
label: "CREATE TEAM" label: "CREATE TEAM"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -454,100 +342,55 @@ var tower = angular.module('Tower', [
controller: TeamsEdit, controller: TeamsEdit,
data: { data: {
activityStreamId: 'team_id' activityStreamId: 'team_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
state('teamUsers', { state('teamUsers', {
url: '/teams/:team_id/users', url: '/teams/:team_id/users',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: UsersList, controller: UsersList
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('teamUserEdit', { state('teamUserEdit', {
url: '/teams/:team_id/users/:user_id', url: '/teams/:team_id/users/:user_id',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: UsersEdit, controller: UsersEdit
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('teamProjects', { state('teamProjects', {
url: '/teams/:team_id/projects', url: '/teams/:team_id/projects',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: ProjectsList, controller: ProjectsList
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('teamProjectAdd', { state('teamProjectAdd', {
url: '/teams/:team_id/projects/add', url: '/teams/:team_id/projects/add',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: ProjectsAdd, controller: ProjectsAdd
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('teamProjectEdit', { state('teamProjectEdit', {
url: '/teams/:team_id/projects/:project_id', url: '/teams/:team_id/projects/:project_id',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: ProjectsEdit, controller: ProjectsEdit
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('teamCredentials', { state('teamCredentials', {
url: '/teams/:team_id/credentials', url: '/teams/:team_id/credentials',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsList, controller: CredentialsList
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('teamCredentialAdd', { state('teamCredentialAdd', {
url: '/teams/:team_id/credentials/add', url: '/teams/:team_id/credentials/add',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsAdd, controller: CredentialsAdd
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('teamCredentialEdit', { state('teamCredentialEdit', {
url: '/teams/:team_id/credentials/:credential_id', url: '/teams/:team_id/credentials/:credential_id',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsEdit, controller: CredentialsEdit
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('credentials', { state('credentials', {
@@ -561,11 +404,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: { ncyBreadcrumb: {
parent: 'setup', parent: 'setup',
label: 'CREDENTIALS' label: 'CREDENTIALS'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -576,11 +414,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: { ncyBreadcrumb: {
parent: "credentials", parent: "credentials",
label: "CREATE CREDENTIAL" label: "CREATE CREDENTIAL"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -590,11 +423,6 @@ var tower = angular.module('Tower', [
controller: CredentialsEdit, controller: CredentialsEdit,
data: { data: {
activityStreamId: 'credential_id' activityStreamId: 'credential_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -609,11 +437,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: { ncyBreadcrumb: {
parent: 'setup', parent: 'setup',
label: 'USERS' label: 'USERS'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -624,11 +447,6 @@ var tower = angular.module('Tower', [
ncyBreadcrumb: { ncyBreadcrumb: {
parent: "users", parent: "users",
label: "CREATE USER" label: "CREATE USER"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
@@ -638,45 +456,25 @@ var tower = angular.module('Tower', [
controller: UsersEdit, controller: UsersEdit,
data: { data: {
activityStreamId: 'user_id' activityStreamId: 'user_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
} }
}). }).
state('userCredentials', { state('userCredentials', {
url: '/users/:user_id/credentials', url: '/users/:user_id/credentials',
templateUrl: urlPrefix + 'partials/users.html', templateUrl: urlPrefix + 'partials/users.html',
controller: CredentialsList, controller: CredentialsList
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('userCredentialAdd', { state('userCredentialAdd', {
url: '/users/:user_id/credentials/add', url: '/users/:user_id/credentials/add',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsAdd, controller: CredentialsAdd
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('teamUserCredentialEdit', { state('teamUserCredentialEdit', {
url: '/teams/:user_id/credentials/:credential_id', url: '/teams/:user_id/credentials/:credential_id',
templateUrl: urlPrefix + 'partials/teams.html', templateUrl: urlPrefix + 'partials/teams.html',
controller: CredentialsEdit, controller: CredentialsEdit
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}). }).
state('sockets', { state('sockets', {
@@ -710,7 +508,7 @@ var tower = angular.module('Tower', [
var sock; var sock;
$rootScope.addPermission = function (scope) { $rootScope.addPermission = function (scope) {
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope); $compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
} };
$rootScope.deletePermission = function (user, role, userName, $rootScope.deletePermission = function (user, role, userName,
roleName, resourceName) { 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() { function activateTab() {
// Make the correct tab active // Make the correct tab active
var base = $location.path().replace(/^\//, '').split('/')[0]; var base = $location.path().replace(/^\//, '').split('/')[0];
@@ -774,16 +632,17 @@ var tower = angular.module('Tower', [
$rootScope.removeConfigReady(); $rootScope.removeConfigReady();
} }
$rootScope.removeConfigReady = $rootScope.$on('ConfigReady', function() { $rootScope.removeConfigReady = $rootScope.$on('ConfigReady', function() {
var list, id;
// initially set row edit indicator for crud pages // initially set row edit indicator for crud pages
if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") { if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") {
var list = $location.$$path.split("/")[3]; list = $location.$$path.split("/")[3];
var id = $location.$$path.split("/")[4]; id = $location.$$path.split("/")[4];
$rootScope.listBeingEdited = list; $rootScope.listBeingEdited = list;
$rootScope.rowBeingEdited = id; $rootScope.rowBeingEdited = id;
$rootScope.initialIndicatorLoad = true; $rootScope.initialIndicatorLoad = true;
} else if ($location.$$path.split("/")[2]) { } else if ($location.$$path.split("/")[2]) {
var list = $location.$$path.split("/")[1]; list = $location.$$path.split("/")[1];
var id = $location.$$path.split("/")[2]; id = $location.$$path.split("/")[2];
$rootScope.listBeingEdited = list; $rootScope.listBeingEdited = list;
$rootScope.rowBeingEdited = id; $rootScope.rowBeingEdited = id;
} }
@@ -871,6 +730,9 @@ var tower = angular.module('Tower', [
$rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) { $rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) {
if (next.name !== 'signOut'){
CheckLicense.notify();
}
$rootScope.$broadcast("closePermissionsModal"); $rootScope.$broadcast("closePermissionsModal");
// this line removes the query params attached to a route // this line removes the query params attached to a route
if(prev && prev.$$route && if(prev && prev.$$route &&
@@ -915,15 +777,16 @@ var tower = angular.module('Tower', [
activateTab(); 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 // catch license expiration notifications immediately after user logs in, redirect
if (fromState.name == 'signIn'){ if (fromState.name === 'signIn'){
CheckLicense.notify(); CheckLicense.notify();
} }
var list, id;
// broadcast event change if editing crud object // broadcast event change if editing crud object
if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") { if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") {
var list = $location.$$path.split("/")[3]; list = $location.$$path.split("/")[3];
var id = $location.$$path.split("/")[4]; id = $location.$$path.split("/")[4];
if (!$rootScope.initialIndicatorLoad) { if (!$rootScope.initialIndicatorLoad) {
delete $rootScope.listBeingEdited; delete $rootScope.listBeingEdited;
@@ -934,8 +797,8 @@ var tower = angular.module('Tower', [
$rootScope.$broadcast("EditIndicatorChange", list, id); $rootScope.$broadcast("EditIndicatorChange", list, id);
} else if ($location.$$path.split("/")[2]) { } else if ($location.$$path.split("/")[2]) {
var list = $location.$$path.split("/")[1]; list = $location.$$path.split("/")[1];
var id = $location.$$path.split("/")[2]; id = $location.$$path.split("/")[2];
delete $rootScope.listBeingEdited; delete $rootScope.listBeingEdited;
delete $rootScope.rowBeingEdited; delete $rootScope.rowBeingEdited;
@@ -962,6 +825,7 @@ var tower = angular.module('Tower', [
$rootScope.sessionTimer = timer; $rootScope.sessionTimer = timer;
$rootScope.$emit('OpenSocket'); $rootScope.$emit('OpenSocket');
pendoService.issuePendoIdentity(); pendoService.issuePendoIdentity();
CheckLicense.notify();
}); });
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/************************************************* /*************************************************
* Copyright (c) 2015 Ansible, Inc. * Copyright (c) 2016 Ansible, Inc.
* *
* All Rights Reserved * 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 ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
//scope. //scope.
var generator = GenerateList, var generator = GenerateList,
list = HomeGroupList, list = HomeGroupList,
defaultUrl = GetBasePath('groups'), defaultUrl = GetBasePath('groups'),
scope = $scope, scope = $scope,
modal_scope = $scope.$new(),
opt, PreviousSearchParams; opt, PreviousSearchParams;
generator.inject(list, { mode: 'edit', scope: scope }); 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' * @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', mode = (base === 'projects') ? 'edit' : 'select',
url = (base === 'teams') ? GetBasePath('teams') + $stateParams.team_id + '/projects/' : defaultUrl, url = (base === 'teams') ? GetBasePath('teams') + $stateParams.team_id + '/projects/' : defaultUrl,
choiceCount = 0; choiceCount = 0;
view.inject(list, { mode: mode, scope: $scope }); view.inject(list, { mode: mode, scope: $scope });
$rootScope.flashMessage = null; $rootScope.flashMessage = null;

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ export default
label: "Hosts" label: "Hosts"
}, },
{ {
url: "/#/home/hosts?has_active_failures=true", url: "/#/home/hosts?active-failures=true",
number: scope.data.hosts.failed, number: scope.data.hosts.failed,
label: "Failed Hosts", label: "Failed Hosts",
isFailureCount: true 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. * Copyright (c) 2015 Ansible, Inc.
* *
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
export default export default function(){
angular.module('HomeHostListDefinition', []) return {
.value('HomeHostList', {
name: 'hosts', name: 'hosts',
iterator: 'host', iterator: 'host',
selectTitle: 'Add Existing Hosts', selectTitle: 'Add Existing Hosts',
@@ -17,41 +15,48 @@ export default
index: false, index: false,
hover: true, hover: true,
well: true, well: true,
emptyListText: 'NO ACTIVE FAILURES FOUND',
fields: { fields: {
status: { status: {
label: "", basePath: 'unified_jobs',
label: '',
iconOnly: true, iconOnly: true,
icon: "{{ 'icon-job-' + host.active_failures }}", searchable: true,
awToolTip: "{{ host.badgeToolTip }}", searchType: 'select',
awTipPlacement: "right", nosort: true,
dataPlacement: "right", searchOptions: [],
awPopOver: "{{ host.job_status_html }}", searchLabel: 'Job Status',
ngClick:"bob", icon: 'icon-job-{{ host.active_failures }}',
columnClass: "List-staticColumn--smallStatus", awToolTip: '{{ host.badgeToolTip }}',
searchable: false, awTipPlacement: 'right',
nosort: true 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: { name: {
key: true, key: true,
label: 'Name', label: 'Name',
columnClass: 'col-lg-5 col-md-5 col-sm-5 col-xs-8 ellipsis List-staticColumnAdjacent', 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: { inventory_name: {
label: 'Inventory', label: 'Inventory',
sourceModel: 'inventory', sourceModel: 'inventory',
sourceField: 'name', sourceField: 'name',
columnClass: 'col-lg-5 col-md-4 col-sm-4 hidden-xs elllipsis', 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: { enabled: {
label: 'Disabled?', label: 'Status',
searchSingleValue: true, columnClass: 'List-staticColumn--toggle',
searchType: 'boolean', type: 'toggle',
searchValue: 'false', ngClick: 'toggleHostEnabled(host)',
searchOnly: true 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: { has_active_failures: {
label: 'Has failed jobs?', label: 'Has failed jobs?',
@@ -66,30 +71,15 @@ export default
searchType: 'boolean', searchType: 'boolean',
searchValue: 'true', searchValue: 'true',
searchOnly: true searchOnly: true
},
id: {
label: 'ID',
searchOnly: true
} }
}, },
fieldActions: { fieldActions: {
columnClass: 'col-lg-2 col-md-3 col-sm-3 col-xs-4', 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: { edit: {
label: 'Edit', label: 'Edit',
ngClick: "editHost(host.id)", ngClick: 'editHost(host.id)',
icon: 'icon-edit', icon: 'icon-edit',
awToolTip: 'Edit host', awToolTip: 'Edit host',
dataPlacement: 'top' dataPlacement: 'top'
@@ -99,5 +89,5 @@ export default
actions: { 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 dashboardGraphs from './graphs/main';
import dashboardLists from './lists/main'; import dashboardLists from './lists/main';
import dashboardDirective from './dashboard.directive'; import dashboardDirective from './dashboard.directive';
import dashboardHosts from './hosts/main';
export default 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); .directive('dashboard', dashboardDirective);

View File

@@ -1,8 +1,3 @@
<footer class='Footer'> <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> <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> </footer>

View File

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

View File

@@ -91,7 +91,7 @@ export default
type: 'select', type: 'select',
ngOptions: 'source.label for source in source_region_choices track by source.value', ngOptions: 'source.label for source in source_region_choices track by source.value',
multiSelect: true, 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, addRequired: false,
editRequired: false, editRequired: false,
dataTitle: 'Source Regions', dataTitle: 'Source Regions',

View File

@@ -15,12 +15,23 @@ export default
.value('HostForm', { .value('HostForm', {
addTitle: 'Create Host', addTitle: 'Create Host',
editTitle: '{{ name }}', editTitle: '{{ host.name }}',
name: 'host', name: 'host',
well: false, well: false,
formLabelSize: 'col-lg-3', formLabelSize: 'col-lg-3',
formFieldSize: 'col-lg-9', 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: { fields: {
name: { name: {
label: 'Host Name', label: 'Host Name',
@@ -43,19 +54,6 @@ export default
addRequired: false, addRequired: false,
editRequired: 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: { variables: {
label: 'Variables', label: 'Variables',
type: 'textarea', type: 'textarea',
@@ -82,17 +80,15 @@ export default
} }
}, },
buttons: { //for now always generates <button> tags buttons: {
/*
save: { save: {
ngClick: 'formSave()', //$scope.function to call on click, optional ngClick: 'formSave()',
ngDisabled: true //Disable when $pristine or $invalid, optional ngDisabled: true
}, },
reset: { cancel: {
ngClick: 'formReset()', ngClick: 'formCancel()',
ngDisabled: true //Disabled when $pristine ngDisabled: true
} }
*/
}, },
related: {} related: {}

View File

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

View File

@@ -46,100 +46,6 @@ export default
}, },
related: { 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: { permissions: {
type: 'collection', type: 'collection',
title: 'Permissions', title: 'Permissions',

View File

@@ -59,11 +59,10 @@ export default
}, },
related: { related: {
/* access_list: {
permissions: {
basePath: 'teams/:id/access_list/', basePath: 'teams/:id/access_list/',
type: 'collection', type: 'collection',
title: 'Permissions', title: 'Users',
iterator: 'permission', iterator: 'permission',
index: false, index: false,
open: false, open: false,
@@ -76,148 +75,59 @@ export default
actionClass: 'btn List-buttonSubmit', actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD' 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: { fields: {
username: { username: {
key: true, key: true,
label: 'Username' label: 'User',
linkBase: 'users',
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
}, },
first_name: { role: {
label: 'First Name' label: 'Role',
}, type: 'role',
last_name: { noSort: true,
label: 'Last Name' class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8'
}
},
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'
} }
} }
},
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 }); //InventoryForm

View File

@@ -115,71 +115,6 @@ export default
}, },
related: { 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: { organizations: {
type: 'collection', type: 'collection',
title: 'Organizations', title: 'Organizations',
@@ -197,9 +132,9 @@ export default
description: { description: {
label: 'Description' label: 'Description'
} }
} },
hideOnSuperuser: true
}, },
teams: { teams: {
type: 'collection', type: 'collection',
title: 'Teams', title: 'Teams',
@@ -217,8 +152,44 @@ export default
description: { description: {
label: '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; break;
case 'ssh': case 'ssh':
scope.usernameLabel = 'Username'; //formally 'SSH Username' scope.usernameLabel = 'Username'; //formally 'SSH Username'
scope.becomeUsernameLabel = 'Privilege Escalation Username';
scope.becomePasswordLabel = 'Privilege Escalation Password';
break; break;
case 'scm': case 'scm':
scope.sshKeyDataLabel = 'SCM Private Key'; scope.sshKeyDataLabel = 'SCM Private Key';
@@ -109,13 +111,20 @@ angular.module('CredentialsHelper', ['Utilities'])
"as: </p><p>adjective-noun-000</p>"; "as: </p><p>adjective-noun-000</p>";
break; break;
case 'azure': case 'azure':
scope.usernameLabel = "Subscription ID";
scope.sshKeyDataLabel = 'Management Certificate'; scope.sshKeyDataLabel = 'Management Certificate';
scope.subscription_required = true; scope.subscription_required = true;
scope.key_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_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"; scope.key_hint= "drag and drop a management certificate file on the field below";
break; 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': case 'vmware':
scope.username_required = true; scope.username_required = true;
scope.host_required = true; scope.host_required = true;
@@ -137,6 +146,26 @@ angular.module('CredentialsHelper', ['Utilities'])
scope.hostPopOver = "<p>The host to authenticate with." + scope.hostPopOver = "<p>The host to authenticate with." +
"<br />For example, https://openstack.business.com/v2.0/"; "<br />For example, https://openstack.business.com/v2.0/";
break; 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' && if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' &&
fld !== 'ssh_password') { fld !== 'ssh_password') {
if (fld === "organization" && !scope[fld]) { if (fld === "organization" && !scope[fld]) {
data["user"] = $rootScope.current_user.id; data.user = $rootScope.current_user.id;
} else if (scope[fld] === null) { } else if (scope[fld] === null) {
data[fld] = ""; data[fld] = "";
} else { } else {
@@ -238,7 +267,7 @@ angular.module('CredentialsHelper', ['Utilities'])
data.project = scope.project; data.project = scope.project;
break; break;
case 'azure': case 'azure':
data.username = scope.subscription_id; data.username = scope.subscription;
} }
Wait('start'); Wait('start');

View File

@@ -281,7 +281,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
CreateSelect2({ CreateSelect2({
element: '#source_source_regions' 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; scope.source_region_choices = scope.azure_regions;
$('#source_form').addClass('squeeze'); $('#source_form').addClass('squeeze');
CreateSelect2({ CreateSelect2({
@@ -312,8 +312,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
} }
if (scope.source.value === 'rax' || if (scope.source.value === 'rax' ||
scope.source.value === 'ec2' || 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' ||
scope.source.value === 'azure_rm' ||
scope.source.value === 'vmware' || scope.source.value === 'vmware' ||
scope.source.value === 'openstack') { scope.source.value === 'openstack') {
if (scope.source.value === 'ec2') { if (scope.source.value === 'ec2') {
@@ -1008,7 +1011,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
} }
else if(fld === "inventory_script"){ else if(fld === "inventory_script"){
// the API stores it as 'source_script', we call it 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; sources_scope.inventory_script = data.source_script;
master.inventory_script = sources_scope.inventory_script; master.inventory_script = sources_scope.inventory_script;
} else if (fld === "source_regions") { } 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', .factory('HostsList', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'HostList', 'generateList',
'Prompt', 'SearchInit', 'PaginateInit', 'ProcessErrors', 'GetBasePath', 'HostsAdd', 'HostsReload', 'SelectionInit', 'Prompt', 'SearchInit', 'PaginateInit', 'ProcessErrors', 'GetBasePath', 'HostsAdd', 'HostsReload', 'SelectionInit',
function($rootScope, $location, $log, $stateParams, Rest, Alert, HostList, GenerateList, Prompt, SearchInit, 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) { return function(params) {
var scope = params.scope, var scope = params.scope,
failed = params.failed, failed = params.failed,
modified = params.modified, modified = params.modified,
started = params.started; started = params.started,
finished = params.finished;
if (failed && scope.job_status.status !== 'failed' && scope.job_status.status !== 'error' && if (failed && scope.job_status.status !== 'failed' && scope.job_status.status !== 'error' &&
scope.job_status.status !== 'canceled') { scope.job_status.status !== 'canceled') {
scope.job_status.status = 'failed'; scope.job_status.status = 'failed';
} }
if (JobIsFinished(scope) && !Empty(modified)) { 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)) { 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)) { if (!Empty(scope.job_status.finished) && !Empty(scope.job_status.started)) {
scope.job_status.elapsed = GetElapsed({ scope.job_status.elapsed = GetElapsed({
start: started, start: started,
end: finished end: finished
}); });
} }
}; };
}]) }])
@@ -900,8 +901,7 @@ export default
.factory('SelectTask', ['JobDetailService', function(JobDetailService) { .factory('SelectTask', ['JobDetailService', function(JobDetailService) {
return function(params) { return function(params) {
var scope = params.scope, var scope = params.scope,
id = params.id, id = params.id;
callback = params.callback;
scope.selectedTask = id; scope.selectedTask = id;
scope.tasks.forEach(function(task, idx) { scope.tasks.forEach(function(task, idx) {
@@ -912,11 +912,11 @@ export default
scope.tasks[idx].taskActiveClass = ''; scope.tasks[idx].taskActiveClass = '';
} }
}); });
var params = { params = {
parent: scope.selectedTask, parent: scope.selectedTask,
event__startswith: 'runner', event__startswith: 'runner',
page_size: scope.hostResultsMaxRows, page_size: scope.hostResultsMaxRows,
order: 'host_name,counter', order: 'host_name,counter',
}; };
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){ JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
scope.hostResults = JobDetailService.processHostEvents(res.results); 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) || if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) ||
(base === 'home')){ (base === 'home')){
// use $state.go with reload: true option to re-instantiate sockets in // 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