mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 19:30:39 -03:30
Merge branch 'devel' of github.com:ansible/ansible-tower into devel
This commit is contained in:
commit
1f12a89b00
25
Makefile
25
Makefile
@ -249,7 +249,19 @@ rebase:
|
||||
push:
|
||||
git push origin master
|
||||
|
||||
virtualenv:
|
||||
virtualenv: virtualenv_ansible virtualenv_tower
|
||||
|
||||
virtualenv_ansible:
|
||||
if [ "$(VENV_BASE)" ]; then \
|
||||
if [ ! -d "$(VENV_BASE)" ]; then \
|
||||
mkdir $(VENV_BASE); \
|
||||
fi; \
|
||||
if [ ! -d "$(VENV_BASE)/ansible" ]; then \
|
||||
virtualenv --system-site-packages $(VENV_BASE)/ansible; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
virtualenv_tower:
|
||||
if [ "$(VENV_BASE)" ]; then \
|
||||
if [ ! -d "$(VENV_BASE)" ]; then \
|
||||
mkdir $(VENV_BASE); \
|
||||
@ -257,12 +269,9 @@ virtualenv:
|
||||
if [ ! -d "$(VENV_BASE)/tower" ]; then \
|
||||
virtualenv --system-site-packages $(VENV_BASE)/tower; \
|
||||
fi; \
|
||||
if [ ! -d "$(VENV_BASE)/ansible" ]; then \
|
||||
virtualenv --system-site-packages $(VENV_BASE)/ansible; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
requirements_ansible:
|
||||
requirements_ansible: virtualenv_ansible
|
||||
if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/ansible/bin/activate; \
|
||||
$(VENV_BASE)/ansible/bin/pip install -U pip==8.1.1; \
|
||||
@ -273,7 +282,7 @@ requirements_ansible:
|
||||
fi
|
||||
|
||||
# Install third-party requirements needed for Tower's environment.
|
||||
requirements_tower:
|
||||
requirements_tower: virtualenv_tower
|
||||
if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/tower/bin/activate; \
|
||||
$(VENV_BASE)/tower/bin/pip install -U pip==8.1.1; \
|
||||
@ -299,7 +308,7 @@ requirements_jenkins:
|
||||
fi && \
|
||||
$(NPM_BIN) install csslint jshint
|
||||
|
||||
requirements: virtualenv requirements_ansible requirements_tower
|
||||
requirements: requirements_ansible requirements_tower
|
||||
|
||||
requirements_dev: requirements requirements_tower_dev
|
||||
|
||||
@ -640,7 +649,7 @@ tar-build/$(SETUP_TAR_FILE):
|
||||
@cp -a setup tar-build/$(SETUP_TAR_NAME)
|
||||
@rsync -az docs/licenses tar-build/$(SETUP_TAR_NAME)/
|
||||
@cd tar-build/$(SETUP_TAR_NAME) && sed -e 's#%NAME%#$(NAME)#;s#%VERSION%#$(VERSION)#;s#%RELEASE%#$(RELEASE)#;' group_vars/all.in > group_vars/all
|
||||
@cd tar-build && tar -czf $(SETUP_TAR_FILE) --exclude "*/all.in" $(SETUP_TAR_NAME)/
|
||||
@cd tar-build && tar -czf $(SETUP_TAR_FILE) --exclude "*/all.in" --exclude "**/test/*" $(SETUP_TAR_NAME)/
|
||||
@ln -sf $(SETUP_TAR_FILE) tar-build/$(SETUP_TAR_LINK)
|
||||
|
||||
tar-build/$(SETUP_TAR_CHECKSUM):
|
||||
|
||||
@ -131,6 +131,8 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
value = to_python_boolean(value)
|
||||
elif new_lookup.endswith('__in'):
|
||||
items = []
|
||||
if not value:
|
||||
raise ValueError('cannot provide empty value for __in')
|
||||
for item in value.split(','):
|
||||
items.append(self.value_to_python_for_field(field, item))
|
||||
value = items
|
||||
@ -218,7 +220,7 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
q = Q(**{k:v})
|
||||
queryset = queryset.filter(q)
|
||||
queryset = queryset.filter(*args)
|
||||
return queryset.distinct()
|
||||
return queryset
|
||||
except (FieldError, FieldDoesNotExist, ValueError), e:
|
||||
raise ParseError(e.args[0])
|
||||
except ValidationError, e:
|
||||
|
||||
@ -25,6 +25,7 @@ from rest_framework import views
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.models import Label
|
||||
from awx.main.utils import * # noqa
|
||||
from awx.api.serializers import ResourceAccessListElementSerializer
|
||||
|
||||
@ -35,7 +36,8 @@ __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView',
|
||||
'SubDetailAPIView',
|
||||
'ResourceAccessList',
|
||||
'ParentMixin',]
|
||||
'ParentMixin',
|
||||
'DeleteLastUnattachLabelMixin',]
|
||||
|
||||
logger = logging.getLogger('awx.api.generics')
|
||||
|
||||
@ -399,12 +401,15 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
else:
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def unattach(self, request, *args, **kwargs):
|
||||
def unattach_validate(self, request):
|
||||
sub_id = request.data.get('id', None)
|
||||
res = None
|
||||
if not sub_id:
|
||||
data = dict(msg='"id" is required to disassociate')
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
res = Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return (sub_id, res)
|
||||
|
||||
def unattach_by_id(self, request, sub_id):
|
||||
parent = self.get_parent_object()
|
||||
parent_key = getattr(self, 'parent_key', None)
|
||||
relationship = getattrd(parent, self.relationship)
|
||||
@ -421,6 +426,12 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def unattach(self, request, *args, **kwargs):
|
||||
(sub_id, res) = self.unattach_validate(request)
|
||||
if res:
|
||||
return res
|
||||
return self.unattach_by_id(request, sub_id)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not isinstance(request.data, dict):
|
||||
return Response('invalid type for post data',
|
||||
@ -430,6 +441,21 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
else:
|
||||
return self.attach(request, *args, **kwargs)
|
||||
|
||||
class DeleteLastUnattachLabelMixin(object):
|
||||
def unattach(self, request, *args, **kwargs):
|
||||
(sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request, *args, **kwargs)
|
||||
if res:
|
||||
return res
|
||||
|
||||
res = super(DeleteLastUnattachLabelMixin, self).unattach_by_id(request, sub_id)
|
||||
|
||||
label = Label.objects.get(id=sub_id)
|
||||
|
||||
if label.is_detached():
|
||||
label.delete()
|
||||
|
||||
return res
|
||||
|
||||
class SubDetailAPIView(generics.RetrieveAPIView, GenericAPIView, ParentMixin):
|
||||
pass
|
||||
|
||||
@ -474,7 +500,7 @@ class ResourceAccessList(ListAPIView):
|
||||
def get_queryset(self):
|
||||
self.object_id = self.kwargs['pk']
|
||||
resource_model = getattr(self, 'resource_model')
|
||||
obj = resource_model.objects.get(pk=self.object_id)
|
||||
obj = get_object_or_404(resource_model, pk=self.object_id)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id))
|
||||
@ -483,4 +509,3 @@ class ResourceAccessList(ListAPIView):
|
||||
for r in roles:
|
||||
ancestors.update(set(r.ancestors.all()))
|
||||
return User.objects.filter(roles__in=list(ancestors))
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ from polymorphic import PolymorphicModel
|
||||
from awx.main.constants import SCHEDULEABLE_PROVIDERS
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat, camelcase_to_underscore
|
||||
from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat, camelcase_to_underscore, getattrd
|
||||
from awx.main.redact import REPLACE_STR
|
||||
from awx.main.conf import tower_settings
|
||||
|
||||
@ -78,7 +78,6 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
|
||||
'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
|
||||
'network_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'net'),
|
||||
'permission': DEFAULT_SUMMARY_FIELDS,
|
||||
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
|
||||
'job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',),
|
||||
@ -90,6 +89,7 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
|
||||
'inventory_source': ('source', 'last_updated', 'status'),
|
||||
'source_script': ('name', 'description'),
|
||||
'role': ('id', 'role_field')
|
||||
}
|
||||
|
||||
|
||||
@ -340,16 +340,18 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
return None
|
||||
elif isinstance(obj, User):
|
||||
return obj.date_joined
|
||||
else:
|
||||
elif hasattr(obj, 'created'):
|
||||
return obj.created
|
||||
return None
|
||||
|
||||
def get_modified(self, obj):
|
||||
if obj is None:
|
||||
return None
|
||||
elif isinstance(obj, User):
|
||||
return obj.last_login # Not actually exposed for User.
|
||||
else:
|
||||
elif hasattr(obj, 'modified'):
|
||||
return obj.modified
|
||||
return None
|
||||
|
||||
def build_standard_field(self, field_name, model_field):
|
||||
# DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits
|
||||
@ -699,7 +701,7 @@ class UserSerializer(BaseSerializer):
|
||||
|
||||
def validate_password(self, value):
|
||||
if not self.instance and value in (None, ''):
|
||||
raise serializers.ValidationError('Password required for new User')
|
||||
raise serializers.ValidationError('Password required for new User.')
|
||||
return value
|
||||
|
||||
def _update_password(self, obj, new_password):
|
||||
@ -763,7 +765,7 @@ class UserSerializer(BaseSerializer):
|
||||
ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
|
||||
if field_name in ldap_managed_fields:
|
||||
if value != getattr(self.instance, field_name):
|
||||
raise serializers.ValidationError('Unable to change %s on user managed by LDAP' % field_name)
|
||||
raise serializers.ValidationError('Unable to change %s on user managed by LDAP.' % field_name)
|
||||
return value
|
||||
|
||||
def validate_username(self, value):
|
||||
@ -796,6 +798,7 @@ class OrganizationSerializer(BaseSerializer):
|
||||
users = reverse('api:organization_users_list', args=(obj.pk,)),
|
||||
admins = reverse('api:organization_admins_list', args=(obj.pk,)),
|
||||
teams = reverse('api:organization_teams_list', args=(obj.pk,)),
|
||||
credentials = reverse('api:organization_credential_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,)),
|
||||
notifiers = reverse('api:organization_notifiers_list', args=(obj.pk,)),
|
||||
notifiers_any = reverse('api:organization_notifiers_any_list', args=(obj.pk,)),
|
||||
@ -961,7 +964,7 @@ class BaseSerializerWithVariables(BaseSerializer):
|
||||
try:
|
||||
yaml.safe_load(value)
|
||||
except yaml.YAMLError:
|
||||
raise serializers.ValidationError('Must be valid JSON or YAML')
|
||||
raise serializers.ValidationError('Must be valid JSON or YAML.')
|
||||
return value
|
||||
|
||||
|
||||
@ -1113,7 +1116,7 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
vars_dict['ansible_ssh_port'] = port
|
||||
attrs['variables'] = yaml.dump(vars_dict)
|
||||
except (yaml.YAMLError, TypeError):
|
||||
raise serializers.ValidationError('Must be valid JSON or YAML')
|
||||
raise serializers.ValidationError('Must be valid JSON or YAML.')
|
||||
|
||||
return super(HostSerializer, self).validate(attrs)
|
||||
|
||||
@ -1170,7 +1173,7 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
|
||||
def validate_name(self, value):
|
||||
if value in ('all', '_meta'):
|
||||
raise serializers.ValidationError('Invalid group name')
|
||||
raise serializers.ValidationError('Invalid group name.')
|
||||
return value
|
||||
|
||||
def to_representation(self, obj):
|
||||
@ -1303,10 +1306,10 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
||||
else:
|
||||
try:
|
||||
if source_script.organization != self.instance.inventory.organization:
|
||||
errors['source_script'] = 'source_script does not belong to the same organization as the inventory'
|
||||
errors['source_script'] = 'source_script does not belong to the same organization as the inventory.'
|
||||
except Exception:
|
||||
# TODO: Log
|
||||
errors['source_script'] = 'source_script doesn\'t exist'
|
||||
errors['source_script'] = 'source_script doesn\'t exist.'
|
||||
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
@ -1441,8 +1444,26 @@ class RoleSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ('*', 'description', 'name')
|
||||
read_only_fields = ('description', 'name')
|
||||
read_only_fields = ('id', 'role_field', 'description', 'name')
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(RoleSerializer, self).to_representation(obj)
|
||||
|
||||
def spacify_type_name(cls):
|
||||
return re.sub(r'([a-z])([A-Z])', '\g<1> \g<2>', cls.__name__)
|
||||
|
||||
if obj.object_id:
|
||||
content_object = obj.content_object
|
||||
if hasattr(content_object, 'username'):
|
||||
ret['summary_fields']['resource_name'] = obj.content_object.username
|
||||
if hasattr(content_object, 'name'):
|
||||
ret['summary_fields']['resource_name'] = obj.content_object.name
|
||||
ret['summary_fields']['resource_type'] = obj.content_type.name
|
||||
ret['summary_fields']['resource_type_display_name'] = spacify_type_name(obj.content_type.model_class())
|
||||
|
||||
ret.pop('created')
|
||||
ret.pop('modified')
|
||||
return ret
|
||||
|
||||
def get_related(self, obj):
|
||||
ret = super(RoleSerializer, self).get_related(obj)
|
||||
@ -1524,6 +1545,15 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
||||
.filter(content_type=team_content_type,
|
||||
members=user,
|
||||
children__in=direct_permissive_role_ids)
|
||||
if content_type == team_content_type:
|
||||
# When looking at the access list for a team, exclude the entries
|
||||
# for that team. This exists primarily so we don't list the read role
|
||||
# as a direct role when a user is a member or admin of a team
|
||||
direct_team_roles = direct_team_roles.exclude(
|
||||
children__content_type=team_content_type,
|
||||
children__object_id=obj.id
|
||||
)
|
||||
|
||||
|
||||
indirect_team_roles = Role.objects \
|
||||
.filter(content_type=team_content_type,
|
||||
@ -1553,6 +1583,18 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
||||
class CredentialSerializer(BaseSerializer):
|
||||
|
||||
# FIXME: may want to make some fields filtered based on user accessing
|
||||
user = serializers.PrimaryKeyRelatedField(
|
||||
queryset=User.objects.all(), required=False, default=None, write_only=True,
|
||||
help_text='Write-only field used to add user to owner role. If provided, '
|
||||
'do not give either team or organization. Only valid for creation.')
|
||||
team = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Team.objects.all(), required=False, default=None, write_only=True,
|
||||
help_text='Write-only field used to add team to owner role. If provided, '
|
||||
'do not give either user or organization. Only valid for creation.')
|
||||
organization = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Organization.objects.all(), required=False, default=None, write_only=True,
|
||||
help_text='Write-only field used to add organization to owner role. If provided, '
|
||||
'do not give either team or team. Only valid for creation.')
|
||||
|
||||
class Meta:
|
||||
model = Credential
|
||||
@ -1561,7 +1603,14 @@ class CredentialSerializer(BaseSerializer):
|
||||
'ssh_key_data', 'ssh_key_unlock',
|
||||
'become_method', 'become_username', 'become_password',
|
||||
'vault_password', 'subscription', 'tenant', 'secret', 'client',
|
||||
'authorize', 'authorize_password')
|
||||
'authorize', 'authorize_password',
|
||||
'user', 'team', 'organization')
|
||||
|
||||
def create(self, validated_data):
|
||||
# Remove the user, team, and organization processed in view
|
||||
for field in ['user', 'team', 'organization']:
|
||||
validated_data.pop(field, None)
|
||||
return super(CredentialSerializer, self).create(validated_data)
|
||||
|
||||
def build_standard_field(self, field_name, model_field):
|
||||
field_class, field_kwargs = super(CredentialSerializer, self).build_standard_field(field_name, model_field)
|
||||
@ -1657,9 +1706,9 @@ class JobOptionsSerializer(BaseSerializer):
|
||||
if not project and job_type != PERM_INVENTORY_SCAN:
|
||||
raise serializers.ValidationError({'project': 'This field is required.'})
|
||||
if project and playbook and force_text(playbook) not in project.playbooks:
|
||||
raise serializers.ValidationError({'playbook': 'Playbook not found for project'})
|
||||
raise serializers.ValidationError({'playbook': 'Playbook not found for project.'})
|
||||
if project and not playbook:
|
||||
raise serializers.ValidationError({'playbook': 'Must select playbook for project'})
|
||||
raise serializers.ValidationError({'playbook': 'Must select playbook for project.'})
|
||||
|
||||
return super(JobOptionsSerializer, self).validate(attrs)
|
||||
|
||||
@ -1720,7 +1769,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
survey_enabled = attrs.get('survey_enabled', self.instance and self.instance.survey_enabled or False)
|
||||
job_type = attrs.get('job_type', self.instance and self.instance.job_type or None)
|
||||
if survey_enabled and job_type == PERM_INVENTORY_SCAN:
|
||||
raise serializers.ValidationError({'survey_enabled': 'Survey Enabled can not be used with scan jobs'})
|
||||
raise serializers.ValidationError({'survey_enabled': 'Survey Enabled can not be used with scan jobs.'})
|
||||
|
||||
return super(JobTemplateSerializer, self).validate(attrs)
|
||||
|
||||
@ -1733,12 +1782,13 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
ask_tags_on_launch = serializers.ReadOnlyField()
|
||||
ask_job_type_on_launch = serializers.ReadOnlyField()
|
||||
ask_inventory_on_launch = serializers.ReadOnlyField()
|
||||
ask_credential_on_launch = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = ('*', 'job_template', 'passwords_needed_to_start', 'ask_variables_on_launch',
|
||||
'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch',
|
||||
'ask_inventory_on_launch')
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(JobSerializer, self).get_related(obj)
|
||||
@ -1865,9 +1915,9 @@ class JobRelaunchSerializer(JobSerializer):
|
||||
if not obj.credential:
|
||||
raise serializers.ValidationError(dict(credential=["Credential not found or deleted."]))
|
||||
if obj.job_type != PERM_INVENTORY_SCAN and obj.project is None:
|
||||
raise serializers.ValidationError(dict(errors=["Job Template Project is missing or undefined"]))
|
||||
raise serializers.ValidationError(dict(errors=["Job Template Project is missing or undefined."]))
|
||||
if obj.inventory is None:
|
||||
raise serializers.ValidationError(dict(errors=["Job Template Inventory is missing or undefined"]))
|
||||
raise serializers.ValidationError(dict(errors=["Job Template Inventory is missing or undefined."]))
|
||||
attrs = super(JobRelaunchSerializer, self).validate(attrs)
|
||||
return attrs
|
||||
|
||||
@ -1876,7 +1926,7 @@ class AdHocCommandSerializer(UnifiedJobSerializer):
|
||||
class Meta:
|
||||
model = AdHocCommand
|
||||
fields = ('*', 'job_type', 'inventory', 'limit', 'credential',
|
||||
'module_name', 'module_args', 'forks', 'verbosity',
|
||||
'module_name', 'module_args', 'forks', 'verbosity', 'extra_vars',
|
||||
'become_enabled', '-unified_job_template', '-description')
|
||||
extra_kwargs = {
|
||||
'name': {
|
||||
@ -2104,6 +2154,8 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
inventory_needed_to_start = serializers.SerializerMethodField()
|
||||
survey_enabled = serializers.SerializerMethodField()
|
||||
extra_vars = VerbatimField(required=False, write_only=True)
|
||||
job_template_data = serializers.SerializerMethodField()
|
||||
defaults = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = JobTemplate
|
||||
@ -2113,7 +2165,8 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
'ask_job_type_on_launch', 'ask_limit_on_launch',
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch',
|
||||
'survey_enabled', 'variables_needed_to_start',
|
||||
'credential_needed_to_start', 'inventory_needed_to_start',)
|
||||
'credential_needed_to_start', 'inventory_needed_to_start',
|
||||
'job_template_data', 'defaults')
|
||||
read_only_fields = ('ask_variables_on_launch', 'ask_limit_on_launch',
|
||||
'ask_tags_on_launch', 'ask_job_type_on_launch',
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch')
|
||||
@ -2137,6 +2190,21 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
return obj.survey_enabled and 'spec' in obj.survey_spec
|
||||
return False
|
||||
|
||||
def get_defaults(self, obj):
|
||||
ask_for_vars_dict = obj._ask_for_vars_dict()
|
||||
defaults_dict = {}
|
||||
for field in ask_for_vars_dict:
|
||||
if field in ('inventory', 'credential'):
|
||||
defaults_dict[field] = dict(
|
||||
name=getattrd(obj, '%s.name' % field, None),
|
||||
id=getattrd(obj, '%s.pk' % field, None))
|
||||
else:
|
||||
defaults_dict[field] = getattr(obj, field)
|
||||
return defaults_dict
|
||||
|
||||
def get_job_template_data(self, obj):
|
||||
return dict(name=obj.name, id=obj.id, description=obj.description)
|
||||
|
||||
def validate(self, attrs):
|
||||
errors = {}
|
||||
obj = self.context.get('obj')
|
||||
@ -2166,8 +2234,9 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
except (ValueError, TypeError):
|
||||
try:
|
||||
extra_vars = yaml.safe_load(extra_vars)
|
||||
except (yaml.YAMLError, TypeError, AttributeError):
|
||||
errors['extra_vars'] = 'Must be valid JSON or YAML'
|
||||
assert isinstance(extra_vars, dict)
|
||||
except (yaml.YAMLError, TypeError, AttributeError, AssertionError):
|
||||
errors['extra_vars'] = 'Must be a valid JSON or YAML dictionary'
|
||||
|
||||
if not isinstance(extra_vars, dict):
|
||||
extra_vars = {}
|
||||
@ -2178,9 +2247,9 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
errors['variables_needed_to_start'] = validation_errors
|
||||
|
||||
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None):
|
||||
errors['project'] = 'Job Template Project is missing or undefined'
|
||||
errors['project'] = 'Job Template Project is missing or undefined.'
|
||||
if (obj.inventory is None) and not attrs.get('inventory', None):
|
||||
errors['inventory'] = 'Job Template Inventory is missing or undefined'
|
||||
errors['inventory'] = 'Job Template Inventory is missing or undefined.'
|
||||
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
@ -2318,7 +2387,7 @@ class ScheduleSerializer(BaseSerializer):
|
||||
|
||||
def validate_unified_job_template(self, value):
|
||||
if type(value) == InventorySource and value.source not in SCHEDULEABLE_PROVIDERS:
|
||||
raise serializers.ValidationError('Inventory Source must be a cloud resource')
|
||||
raise serializers.ValidationError('Inventory Source must be a cloud resource.')
|
||||
return value
|
||||
|
||||
# We reject rrules if:
|
||||
@ -2510,7 +2579,7 @@ class TowerSettingsSerializer(BaseSerializer):
|
||||
def validate(self, attrs):
|
||||
manifest = settings.TOWER_SETTINGS_MANIFEST
|
||||
if attrs['key'] not in manifest:
|
||||
raise serializers.ValidationError(dict(key=["Key {0} is not a valid settings key".format(attrs['key'])]))
|
||||
raise serializers.ValidationError(dict(key=["Key {0} is not a valid settings key.".format(attrs['key'])]))
|
||||
|
||||
if attrs['value_type'] == 'json':
|
||||
attrs['value'] = json.dumps(attrs['value'])
|
||||
@ -2544,7 +2613,7 @@ class AuthTokenSerializer(serializers.Serializer):
|
||||
else:
|
||||
raise serializers.ValidationError('Unable to login with provided credentials.')
|
||||
else:
|
||||
raise serializers.ValidationError('Must include "username" and "password"')
|
||||
raise serializers.ValidationError('Must include "username" and "password".')
|
||||
|
||||
|
||||
class FactVersionSerializer(BaseFactSerializer):
|
||||
|
||||
@ -1,20 +1,3 @@
|
||||
TOWER SOFTWARE END USER LICENSE AGREEMENT
|
||||
|
||||
Unless otherwise agreed to, and executed in a definitive agreement, between
|
||||
Ansible, Inc. (“Ansible”) and the individual or entity (“Customer”) signing or
|
||||
electronically accepting these terms of use for the Tower Software (“EULA”),
|
||||
all Tower Software, including any and all versions released or made available
|
||||
by Ansible, shall be subject to the Ansible Software Subscription and Services
|
||||
Agreement found at www.ansible.com/subscription-agreement (“Agreement”).
|
||||
Ansible is not responsible for any additional obligations, conditions or
|
||||
warranties agreed to between Customer and an authorized distributor, or
|
||||
reseller, of the Tower Software. BY DOWNLOADING AND USING THE TOWER SOFTWARE,
|
||||
OR BY CLICKING ON THE “YES” BUTTON OR OTHER BUTTON OR MECHANISM DESIGNED TO
|
||||
ACKNOWLEDGE CONSENT TO THE TERMS OF AN ELECTRONIC COPY OF THIS EULA, THE
|
||||
CUSTOMER HEREBY ACKNOWLEDGES THAT CUSTOMER HAS READ, UNDERSTOOD, AND AGREES TO
|
||||
BE BOUND BY THE TERMS OF THIS EULA AND AGREEMENT, INCLUDING ALL TERMS
|
||||
INCORPORATED HEREIN BY REFERENCE, AND THAT THIS EULA AND AGREEMENT IS
|
||||
EQUIVALENT TO ANY WRITTEN NEGOTIATED AGREEMENT BETWEEN CUSTOMER AND ANSIBLE.
|
||||
THIS EULA AND AGREEMENT IS ENFORCEABLE AGAINST ANY PERSON OR ENTITY THAT USES
|
||||
OR AVAILS ITSELF OF THE TOWER SOFTWARE OR ANY PERSON OR ENTITY THAT USES THE OR
|
||||
AVAILS ITSELF OF THE TOWER SOFTWARE ON ANOTHER PERSON’S OR ENTITY’S BEHALF.
|
||||
Unless otherwise agreed to, and executed in a definitive agreement, between Ansible, Inc. (“Ansible”) and the individual or entity (“Customer”) signing or electronically accepting these terms of use for the Tower Software (“EULA”), all Tower Software, including any and all versions released or made available by Ansible, shall be subject to the Ansible Software Subscription and Services Agreement found at www.ansible.com/subscription-agreement (“Agreement”). Ansible is not responsible for any additional obligations, conditions or warranties agreed to between Customer and an authorized distributor, or reseller, of the Tower Software. BY DOWNLOADING AND USING THE TOWER SOFTWARE, OR BY CLICKING ON THE “YES” BUTTON OR OTHER BUTTON OR MECHANISM DESIGNED TO ACKNOWLEDGE CONSENT TO THE TERMS OF AN ELECTRONIC COPY OF THIS EULA, THE CUSTOMER HEREBY ACKNOWLEDGES THAT CUSTOMER HAS READ, UNDERSTOOD, AND AGREES TO BE BOUND BY THE TERMS OF THIS EULA AND AGREEMENT, INCLUDING ALL TERMS INCORPORATED HEREIN BY REFERENCE, AND THAT THIS EULA AND AGREEMENT IS EQUIVALENT TO ANY WRITTEN NEGOTIATED AGREEMENT BETWEEN CUSTOMER AND ANSIBLE. THIS EULA AND AGREEMENT IS ENFORCEABLE AGAINST ANY PERSON OR ENTITY THAT USES OR AVAILS ITSELF OF THE TOWER SOFTWARE OR ANY PERSON OR ENTITY THAT USES THE OR AVAILS ITSELF OF THE TOWER SOFTWARE ON ANOTHER PERSON’S OR ENTITY’S BEHALF.
|
||||
|
||||
@ -30,6 +30,8 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.servers.basehttp import FileWrapper
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import PermissionDenied, ParseError
|
||||
@ -616,9 +618,14 @@ class OrganizationList(ListCreateAPIView):
|
||||
|
||||
JT_reference = 'project__organization'
|
||||
db_results['job_templates'] = JobTemplate.accessible_objects(
|
||||
self.request.user, 'read_role').values(JT_reference).annotate(
|
||||
self.request.user, 'read_role').exclude(job_type='scan').values(JT_reference).annotate(
|
||||
Count(JT_reference)).order_by(JT_reference)
|
||||
|
||||
JT_scan_reference = 'inventory__organization'
|
||||
db_results['job_templates_scan'] = JobTemplate.accessible_objects(
|
||||
self.request.user, 'read_role').filter(job_type='scan').values(JT_scan_reference).annotate(
|
||||
Count(JT_scan_reference)).order_by(JT_scan_reference)
|
||||
|
||||
db_results['projects'] = project_qs\
|
||||
.values('organization').annotate(Count('organization')).order_by('organization')
|
||||
|
||||
@ -638,6 +645,8 @@ class OrganizationList(ListCreateAPIView):
|
||||
for res in db_results:
|
||||
if res == 'job_templates':
|
||||
org_reference = JT_reference
|
||||
elif res == 'job_templates_scan':
|
||||
org_reference = JT_scan_reference
|
||||
elif res == 'users':
|
||||
org_reference = 'id'
|
||||
else:
|
||||
@ -651,6 +660,12 @@ class OrganizationList(ListCreateAPIView):
|
||||
continue
|
||||
count_context[org_id][res] = entry['%s__count' % org_reference]
|
||||
|
||||
# Combine the counts for job templates with scan job templates
|
||||
for org in org_id_list:
|
||||
org_id = org['id']
|
||||
if 'job_templates_scan' in count_context[org_id]:
|
||||
count_context[org_id]['job_templates'] += count_context[org_id].pop('job_templates_scan')
|
||||
|
||||
full_context['related_field_counts'] = count_context
|
||||
|
||||
return full_context
|
||||
@ -684,8 +699,10 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView):
|
||||
organization__id=org_id).count()
|
||||
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(
|
||||
organization__id=org_id).count()
|
||||
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(
|
||||
project__organization__id=org_id).count()
|
||||
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).exclude(
|
||||
job_type='scan').filter(project__organization__id=org_id).count()
|
||||
org_counts['job_templates'] += JobTemplate.accessible_objects(**access_kwargs).filter(
|
||||
job_type='scan').filter(inventory__organization__id=org_id).count()
|
||||
|
||||
full_context['related_field_counts'] = {}
|
||||
full_context['related_field_counts'][org_id] = org_counts
|
||||
@ -814,10 +831,11 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
|
||||
relationship='member_role.children'
|
||||
|
||||
def get_queryset(self):
|
||||
team = Team.objects.get(pk=self.kwargs['pk'])
|
||||
return team.member_role.children.filter(id__in=Role.visible_roles(self.request.user))
|
||||
team = get_object_or_404(Team, pk=self.kwargs['pk'])
|
||||
if not self.request.user.can_access(Team, 'read', team):
|
||||
raise PermissionDenied()
|
||||
return Role.filter_visible_roles(self.request.user, team.member_role.children.all())
|
||||
|
||||
# XXX: Need to enforce permissions
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Forbid implicit role creation here
|
||||
sub_id = request.data.get('id', None)
|
||||
@ -1081,8 +1099,12 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get_queryset(self):
|
||||
#u = User.objects.get(pk=self.kwargs['pk'])
|
||||
return Role.visible_roles(self.request.user).filter(members__in=[int(self.kwargs['pk']), ])
|
||||
u = get_object_or_404(User, pk=self.kwargs['pk'])
|
||||
if not self.request.user.can_access(User, 'read', u):
|
||||
raise PermissionDenied()
|
||||
content_type = ContentType.objects.get_for_model(User)
|
||||
return Role.filter_visible_roles(self.request.user, u.roles.all()) \
|
||||
.exclude(content_type=content_type, object_id=u.id)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Forbid implicit role creation here
|
||||
@ -1090,6 +1112,10 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
|
||||
if not sub_id:
|
||||
data = dict(msg='Role "id" field is missing')
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if sub_id == self.request.user.admin_role.pk:
|
||||
raise PermissionDenied('You may not remove your own admin_role')
|
||||
|
||||
return super(UserRolesList, self).post(request, *args, **kwargs)
|
||||
|
||||
def check_parent_access(self, parent=None):
|
||||
@ -1205,6 +1231,10 @@ class CredentialList(ListCreateAPIView):
|
||||
serializer_class = CredentialSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
for field in [x for x in ['user', 'team', 'organization'] if x in request.data and request.data[x] in ('', None)]:
|
||||
request.data.pop(field)
|
||||
kwargs.pop(field, None)
|
||||
|
||||
if not any([x in request.data for x in ['user', 'team', 'organization']]):
|
||||
return Response({'detail': 'Missing user, team, or organization'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -1213,15 +1243,15 @@ class CredentialList(ListCreateAPIView):
|
||||
|
||||
if 'user' in request.data:
|
||||
user = User.objects.get(pk=request.data['user'])
|
||||
obj = user
|
||||
can_add_params = {'user': user.id}
|
||||
if 'team' in request.data:
|
||||
team = Team.objects.get(pk=request.data['team'])
|
||||
obj = team
|
||||
can_add_params = {'team': team.id}
|
||||
if 'organization' in request.data:
|
||||
organization = Organization.objects.get(pk=request.data['organization'])
|
||||
obj = organization
|
||||
can_add_params = {'organization': organization.id}
|
||||
|
||||
if self.request.user not in obj.admin_role:
|
||||
if not self.request.user.can_access(Credential, 'add', can_add_params):
|
||||
raise PermissionDenied()
|
||||
|
||||
ret = super(CredentialList, self).post(request, *args, **kwargs)
|
||||
@ -1251,8 +1281,7 @@ class UserCredentialsList(CredentialList):
|
||||
return user_creds & visible_creds
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
user = User.objects.get(pk=self.kwargs['pk'])
|
||||
request.data['user'] = user.id
|
||||
request.data['user'] = self.kwargs['pk']
|
||||
# The following post takes care of ensuring the current user can add a cred to this user
|
||||
return super(UserCredentialsList, self).post(request, args, kwargs)
|
||||
|
||||
@ -1271,8 +1300,7 @@ class TeamCredentialsList(CredentialList):
|
||||
return team_creds & visible_creds
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
team = Team.objects.get(pk=self.kwargs['pk'])
|
||||
request.data['team'] = team.id
|
||||
request.data['team'] = self.kwargs['pk']
|
||||
# The following post takes care of ensuring the current user can add a cred to this user
|
||||
return super(TeamCredentialsList, self).post(request, args, kwargs)
|
||||
|
||||
@ -1479,7 +1507,7 @@ class HostAllGroupsList(SubListAPIView):
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
qs = self.request.user.get_queryset(self.model).distinct()
|
||||
sublist_qs = parent.all_groups.distinct()
|
||||
return qs & sublist_qs
|
||||
|
||||
@ -2263,7 +2291,7 @@ class JobTemplateNotifiersSuccessList(SubListCreateAttachDetachAPIView):
|
||||
parent_model = JobTemplate
|
||||
relationship = 'notifiers_success'
|
||||
|
||||
class JobTemplateLabelList(SubListCreateAttachDetachAPIView):
|
||||
class JobTemplateLabelList(SubListCreateAttachDetachAPIView, DeleteLastUnattachLabelMixin):
|
||||
|
||||
model = Label
|
||||
serializer_class = LabelSerializer
|
||||
@ -2454,7 +2482,7 @@ class SystemJobTemplateList(ListAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.user.is_superuser:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
raise PermissionDenied("Superuser privileges needed")
|
||||
return super(SystemJobTemplateList, self).get(request, *args, **kwargs)
|
||||
|
||||
class SystemJobTemplateDetail(RetrieveAPIView):
|
||||
@ -3184,7 +3212,7 @@ class SystemJobList(ListCreateAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.user.is_superuser:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
raise PermissionDenied("Superuser privileges needed")
|
||||
return super(SystemJobList, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
@ -3573,7 +3601,7 @@ class RoleChildrenList(SubListAPIView):
|
||||
# XXX: This should be the intersection between the roles of the user
|
||||
# and the roles that the requesting user has access to see
|
||||
role = Role.objects.get(pk=self.kwargs['pk'])
|
||||
return role.children
|
||||
return role.children.all()
|
||||
|
||||
|
||||
|
||||
|
||||
@ -110,6 +110,18 @@ def check_user_access(user, model_class, action, *args, **kwargs):
|
||||
return result
|
||||
return False
|
||||
|
||||
def check_superuser(func):
|
||||
'''
|
||||
check_superuser is a decorator that provides a simple short circuit
|
||||
for access checks. If the User object is a superuser, return True, otherwise
|
||||
execute the logic of the can_access method.
|
||||
'''
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
class BaseAccess(object):
|
||||
'''
|
||||
Base class for checking user access to a given model. Subclasses should
|
||||
@ -242,10 +254,8 @@ class UserAccess(BaseAccess):
|
||||
# that a user should be able to edit for themselves.
|
||||
return bool(self.user == obj or self.can_admin(obj, data))
|
||||
|
||||
@check_superuser
|
||||
def can_admin(self, obj, data):
|
||||
# Admin implies changing all user fields.
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return Organization.objects.filter(member_role__members=obj, admin_role__members=self.user).exists()
|
||||
|
||||
def can_delete(self, obj):
|
||||
@ -277,9 +287,8 @@ class OrganizationAccess(BaseAccess):
|
||||
qs = self.model.accessible_objects(self.user, 'read_role')
|
||||
return qs.select_related('created_by', 'modified_by').all()
|
||||
|
||||
@check_superuser
|
||||
def can_change(self, obj, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return self.user in obj.admin_role
|
||||
|
||||
def can_delete(self, obj):
|
||||
@ -312,27 +321,25 @@ class InventoryAccess(BaseAccess):
|
||||
qs = self.model.accessible_objects(self.user, 'read_role')
|
||||
return qs.select_related('created_by', 'modified_by', 'organization').all()
|
||||
|
||||
@check_superuser
|
||||
def can_read(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return self.user in obj.read_role
|
||||
|
||||
@check_superuser
|
||||
def can_use(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return self.user in obj.use_role
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
# If no data is specified, just checking for generic add permission?
|
||||
if not data:
|
||||
return Organization.accessible_objects(self.user, 'admin_role').exists()
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
org = get_object_or_400(Organization, pk=org_pk)
|
||||
return self.user in org.admin_role
|
||||
|
||||
@check_superuser
|
||||
def can_change(self, obj, data):
|
||||
# Verify that the user has access to the new organization if moving an
|
||||
# inventory to a new organization.
|
||||
@ -342,8 +349,9 @@ class InventoryAccess(BaseAccess):
|
||||
if self.user not in org.admin_role:
|
||||
return False
|
||||
# Otherwise, just check for write permission.
|
||||
return self.user in obj.admin_role
|
||||
return self.user in obj.update_role
|
||||
|
||||
@check_superuser
|
||||
def can_admin(self, obj, data):
|
||||
# Verify that the user has access to the new organization if moving an
|
||||
# inventory to a new organization.
|
||||
@ -371,12 +379,16 @@ class HostAccess(BaseAccess):
|
||||
|
||||
def get_queryset(self):
|
||||
inv_qs = Inventory.accessible_objects(self.user, 'read_role')
|
||||
group_qs = Group.accessible_objects(self.user, 'read_role')
|
||||
qs = (self.model.objects.filter(inventory=inv_qs) | self.model.objects.filter(groups=group_qs)).distinct()
|
||||
#qs = qs.select_related('created_by', 'modified_by', 'inventory',
|
||||
# 'last_job__job_template',
|
||||
# 'last_job_host_summary__job')
|
||||
#return qs.prefetch_related('groups').all()
|
||||
group_qs = Group.accessible_objects(self.user, 'read_role').exclude(inventory__in=inv_qs)
|
||||
if group_qs.count():
|
||||
qs = self.model.objects.filter(Q(inventory__in=inv_qs) | Q(groups__in=group_qs))
|
||||
else:
|
||||
qs = self.model.objects.filter(inventory__in=inv_qs)
|
||||
|
||||
qs = qs.select_related('created_by', 'modified_by', 'inventory',
|
||||
'last_job__job_template',
|
||||
'last_job_host_summary__job')
|
||||
qs =qs.prefetch_related('groups').all()
|
||||
return qs
|
||||
|
||||
def can_read(self, obj):
|
||||
@ -389,7 +401,7 @@ class HostAccess(BaseAccess):
|
||||
# Checks for admin or change permission on inventory.
|
||||
inventory_pk = get_pk_from_dict(data, 'inventory')
|
||||
inventory = get_object_or_400(Inventory, pk=inventory_pk)
|
||||
if self.user not in inventory.admin_role:
|
||||
if self.user not in inventory.update_role:
|
||||
return False
|
||||
|
||||
# Check to see if we have enough licenses
|
||||
@ -403,7 +415,7 @@ class HostAccess(BaseAccess):
|
||||
raise PermissionDenied('Unable to change inventory on a host')
|
||||
# Checks for admin or change permission on inventory, controls whether
|
||||
# the user can edit variable data.
|
||||
return obj and self.user in obj.inventory.admin_role
|
||||
return obj and self.user in obj.inventory.update_role
|
||||
|
||||
def can_attach(self, obj, sub_obj, relationship, data,
|
||||
skip_sub_obj_read_check=False):
|
||||
@ -440,7 +452,7 @@ class GroupAccess(BaseAccess):
|
||||
# Checks for admin or change permission on inventory.
|
||||
inventory_pk = get_pk_from_dict(data, 'inventory')
|
||||
inventory = get_object_or_400(Inventory, pk=inventory_pk)
|
||||
return self.user in inventory.admin_role
|
||||
return self.user in inventory.update_role
|
||||
|
||||
def can_change(self, obj, data):
|
||||
# Prevent moving a group to a different inventory.
|
||||
@ -449,7 +461,7 @@ class GroupAccess(BaseAccess):
|
||||
raise PermissionDenied('Unable to change inventory on a group')
|
||||
# Checks for admin or change permission on inventory, controls whether
|
||||
# the user can attach subgroups or edit variable data.
|
||||
return obj and self.user in obj.inventory.admin_role
|
||||
return obj and self.user in obj.inventory.update_role
|
||||
|
||||
def can_attach(self, obj, sub_obj, relationship, data,
|
||||
skip_sub_obj_read_check=False):
|
||||
@ -483,7 +495,7 @@ class InventorySourceAccess(BaseAccess):
|
||||
def get_queryset(self):
|
||||
qs = self.model.objects.all()
|
||||
qs = qs.select_related('created_by', 'modified_by', 'group', 'inventory')
|
||||
inventory_ids = set(self.user.get_queryset(Inventory).values_list('id', flat=True))
|
||||
inventory_ids = self.user.get_queryset(Inventory)
|
||||
return qs.filter(Q(inventory_id__in=inventory_ids) |
|
||||
Q(group__inventory_id__in=inventory_ids))
|
||||
|
||||
@ -502,7 +514,7 @@ class InventorySourceAccess(BaseAccess):
|
||||
def can_change(self, obj, data):
|
||||
# Checks for admin or change permission on group.
|
||||
if obj and obj.group:
|
||||
return self.user in obj.group.admin_role
|
||||
return self.user in obj.group.update_role
|
||||
# Can't change inventory sources attached to only the inventory, since
|
||||
# these are created automatically from the management command.
|
||||
else:
|
||||
@ -555,21 +567,36 @@ class CredentialAccess(BaseAccess):
|
||||
qs = self.model.accessible_objects(self.user, 'read_role')
|
||||
return qs.select_related('created_by', 'modified_by').all()
|
||||
|
||||
@check_superuser
|
||||
def can_read(self, obj):
|
||||
return self.user in obj.read_role
|
||||
|
||||
def can_add(self, data):
|
||||
# Access enforced in our view where we have context enough to make a decision
|
||||
return True
|
||||
|
||||
def can_use(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
user_pk = get_pk_from_dict(data, 'user')
|
||||
if user_pk:
|
||||
user_obj = get_object_or_400(User, pk=user_pk)
|
||||
return check_user_access(self.user, User, 'change', user_obj, None)
|
||||
team_pk = get_pk_from_dict(data, 'team')
|
||||
if team_pk:
|
||||
team_obj = get_object_or_400(Team, pk=team_pk)
|
||||
return check_user_access(self.user, Team, 'change', team_obj, None)
|
||||
organization_pk = get_pk_from_dict(data, 'organization')
|
||||
if organization_pk:
|
||||
organization_obj = get_object_or_400(Organization, pk=organization_pk)
|
||||
return check_user_access(self.user, Organization, 'change', organization_obj, None)
|
||||
return False
|
||||
|
||||
|
||||
@check_superuser
|
||||
def can_use(self, obj):
|
||||
return self.user in obj.use_role
|
||||
|
||||
@check_superuser
|
||||
def can_change(self, obj, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if not self.can_add(data):
|
||||
return False
|
||||
return self.user in obj.owner_role
|
||||
|
||||
def can_delete(self, obj):
|
||||
@ -596,14 +623,12 @@ class TeamAccess(BaseAccess):
|
||||
qs = self.model.accessible_objects(self.user, 'read_role')
|
||||
return qs.select_related('created_by', 'modified_by', 'organization').all()
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
org = get_object_or_400(Organization, pk=org_pk)
|
||||
if self.user in org.admin_role:
|
||||
return True
|
||||
else:
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
org = get_object_or_400(Organization, pk=org_pk)
|
||||
if self.user in org.admin_role:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_change(self, obj, data):
|
||||
@ -611,6 +636,8 @@ class TeamAccess(BaseAccess):
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
if obj and org_pk and obj.organization.pk != org_pk:
|
||||
raise PermissionDenied('Unable to change organization on a team')
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return self.user in obj.admin_role
|
||||
|
||||
def can_delete(self, obj):
|
||||
@ -640,15 +667,13 @@ class ProjectAccess(BaseAccess):
|
||||
qs = self.model.accessible_objects(self.user, 'read_role')
|
||||
return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all()
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
qs = Organization.accessible_objects(self.user, 'admin_role')
|
||||
return qs.exists()
|
||||
|
||||
@check_superuser
|
||||
def can_change(self, obj, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return self.user in obj.admin_role
|
||||
|
||||
def can_delete(self, obj):
|
||||
@ -674,9 +699,11 @@ class ProjectUpdateAccess(BaseAccess):
|
||||
project_ids = set(self.user.get_queryset(Project).values_list('id', flat=True))
|
||||
return qs.filter(project_id__in=project_ids)
|
||||
|
||||
@check_superuser
|
||||
def can_cancel(self, obj):
|
||||
return self.can_change(obj, {}) and obj.can_cancel
|
||||
|
||||
@check_superuser
|
||||
def can_delete(self, obj):
|
||||
return obj and self.user in obj.project.admin_role
|
||||
|
||||
@ -704,8 +731,7 @@ class JobTemplateAccess(BaseAccess):
|
||||
'credential', 'cloud_credential', 'next_schedule').all()
|
||||
|
||||
def can_read(self, obj):
|
||||
# you can only see the job templates that you have permission to launch.
|
||||
return self.can_start(obj, validate_license=False)
|
||||
return self.user in obj.read_role
|
||||
|
||||
def can_add(self, data):
|
||||
'''
|
||||
@ -847,10 +873,8 @@ class JobAccess(BaseAccess):
|
||||
def can_change(self, obj, data):
|
||||
return obj.status == 'new' and self.can_read(obj) and self.can_add(data)
|
||||
|
||||
@check_superuser
|
||||
def can_delete(self, obj):
|
||||
# Allow org admins and superusers to delete jobs
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return self.user in obj.inventory.admin_role
|
||||
|
||||
def can_start(self, obj):
|
||||
@ -866,11 +890,12 @@ class JobAccess(BaseAccess):
|
||||
return self.user in obj.job_template.execute_role
|
||||
|
||||
inventory_access = self.user in obj.inventory.use_role
|
||||
credential_access = self.user in obj.credential.use_role
|
||||
|
||||
org_access = self.user in obj.inventory.organization.admin_role
|
||||
project_access = obj.project is None or self.user in obj.project.admin_role
|
||||
|
||||
return inventory_access and (org_access or project_access)
|
||||
return inventory_access and credential_access and (org_access or project_access)
|
||||
|
||||
def can_cancel(self, obj):
|
||||
return self.can_read(obj) and obj.can_cancel
|
||||
@ -1146,18 +1171,16 @@ class ScheduleAccess(BaseAccess):
|
||||
UnifiedJobTemplate.objects.filter(Q(inventorysource__in=inventory_source_qs))
|
||||
return qs.filter(unified_job_template__in=unified_qs)
|
||||
|
||||
@check_superuser
|
||||
def can_read(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if obj and obj.unified_job_template:
|
||||
job_class = obj.unified_job_template
|
||||
return self.user.can_access(type(job_class), 'read', obj.unified_job_template)
|
||||
else:
|
||||
return False
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
pk = get_pk_from_dict(data, 'unified_job_template')
|
||||
obj = get_object_or_400(UnifiedJobTemplate, pk=pk)
|
||||
if obj:
|
||||
@ -1165,18 +1188,16 @@ class ScheduleAccess(BaseAccess):
|
||||
else:
|
||||
return False
|
||||
|
||||
@check_superuser
|
||||
def can_change(self, obj, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if obj and obj.unified_job_template:
|
||||
job_class = obj.unified_job_template
|
||||
return self.user.can_access(type(job_class), 'change', job_class, None)
|
||||
else:
|
||||
return False
|
||||
|
||||
@check_superuser
|
||||
def can_delete(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if obj and obj.unified_job_template:
|
||||
job_class = obj.unified_job_template
|
||||
return self.user.can_access(type(job_class), 'change', job_class, None)
|
||||
@ -1195,25 +1216,22 @@ class NotifierAccess(BaseAccess):
|
||||
return qs
|
||||
return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all())
|
||||
|
||||
@check_superuser
|
||||
def can_read(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if obj.organization is not None:
|
||||
return self.user in obj.organization.admin_role
|
||||
return False
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if not data:
|
||||
return Organization.accessible_objects(self.user, 'admin_role').exists()
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
org = get_object_or_400(Organization, pk=org_pk)
|
||||
return self.user in org.admin_role
|
||||
|
||||
@check_superuser
|
||||
def can_change(self, obj, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
if obj and org_pk and obj.organization.pk != org_pk:
|
||||
org = get_object_or_400(Organization, pk=org_pk)
|
||||
@ -1260,15 +1278,12 @@ class LabelAccess(BaseAccess):
|
||||
organization__in=Organization.accessible_objects(self.user, 'read_role')
|
||||
)
|
||||
|
||||
@check_superuser
|
||||
def can_read(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return self.user in obj.organization.read_role
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
|
||||
if not data or '_method' in data: # So the browseable API will work?
|
||||
return True
|
||||
|
||||
@ -1276,10 +1291,8 @@ class LabelAccess(BaseAccess):
|
||||
org = get_object_or_400(Organization, pk=org_pk)
|
||||
return self.user in org.read_role
|
||||
|
||||
@check_superuser
|
||||
def can_change(self, obj, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
|
||||
if self.can_add(data) is False:
|
||||
return False
|
||||
|
||||
@ -1376,26 +1389,10 @@ class CustomInventoryScriptAccess(BaseAccess):
|
||||
return self.model.objects.distinct().all()
|
||||
return self.model.accessible_objects(self.user, 'read_role').all()
|
||||
|
||||
@check_superuser
|
||||
def can_read(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return self.user in obj.read_role
|
||||
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_change(self, obj, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_delete(self, obj):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class TowerSettingsAccess(BaseAccess):
|
||||
'''
|
||||
@ -1409,17 +1406,6 @@ class TowerSettingsAccess(BaseAccess):
|
||||
|
||||
model = TowerSettings
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
return self.model.objects.all()
|
||||
return self.model.objects.none()
|
||||
|
||||
def can_change(self, obj, data):
|
||||
return self.user.is_superuser
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.user.is_superuser
|
||||
|
||||
|
||||
class RoleAccess(BaseAccess):
|
||||
'''
|
||||
@ -1432,14 +1418,6 @@ class RoleAccess(BaseAccess):
|
||||
|
||||
model = Role
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
return self.model.objects.all()
|
||||
return Role.objects.none()
|
||||
|
||||
def can_change(self, obj, data):
|
||||
return self.user.is_superuser
|
||||
|
||||
def can_read(self, obj):
|
||||
if not obj:
|
||||
return False
|
||||
@ -1463,9 +1441,8 @@ class RoleAccess(BaseAccess):
|
||||
skip_sub_obj_read_check=False):
|
||||
return self.can_unattach(obj, sub_obj, relationship)
|
||||
|
||||
@check_superuser
|
||||
def can_unattach(self, obj, sub_obj, relationship):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if obj.object_id and \
|
||||
isinstance(obj.content_object, ResourceMixin) and \
|
||||
self.user in obj.content_object.admin_role:
|
||||
|
||||
@ -18,12 +18,12 @@ from django.db.models.fields.related import (
|
||||
ReverseManyRelatedObjectsDescriptor,
|
||||
)
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.timezone import now
|
||||
|
||||
# AWX
|
||||
from awx.main.models.rbac import batch_role_ancestor_rebuilding
|
||||
from awx.main.models.rbac import batch_role_ancestor_rebuilding, Role
|
||||
from awx.main.utils import get_current_apps
|
||||
|
||||
|
||||
__all__ = ['AutoOneToOneField', 'ImplicitRoleField']
|
||||
|
||||
|
||||
@ -92,9 +92,7 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
class ImplicitRoleField(models.ForeignKey):
|
||||
"""Implicitly creates a role entry for a resource"""
|
||||
|
||||
def __init__(self, role_name=None, role_description=None, parent_role=None, *args, **kwargs):
|
||||
self.role_name = role_name
|
||||
self.role_description = role_description if role_description else ""
|
||||
def __init__(self, parent_role=None, *args, **kwargs):
|
||||
self.parent_role = parent_role
|
||||
|
||||
kwargs.setdefault('to', 'Role')
|
||||
@ -104,8 +102,6 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(ImplicitRoleField, self).deconstruct()
|
||||
kwargs['role_name'] = self.role_name
|
||||
kwargs['role_description'] = self.role_description
|
||||
kwargs['parent_role'] = self.parent_role
|
||||
return name, path, args, kwargs
|
||||
|
||||
@ -190,11 +186,7 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
if cur_role is None:
|
||||
missing_roles.append(
|
||||
Role_(
|
||||
created=now(),
|
||||
modified=now(),
|
||||
role_field=implicit_role_field.name,
|
||||
name=implicit_role_field.role_name,
|
||||
description=implicit_role_field.role_description,
|
||||
content_type_id=ct_id,
|
||||
object_id=instance.id
|
||||
)
|
||||
@ -208,7 +200,7 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
updates[role.role_field] = role.id
|
||||
role_ids.append(role.id)
|
||||
type(instance).objects.filter(pk=instance.pk).update(**updates)
|
||||
Role_._simultaneous_ancestry_rebuild(role_ids)
|
||||
Role.rebuild_role_ancestor_list(role_ids, [])
|
||||
|
||||
# Update parentage if necessary
|
||||
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
|
||||
@ -247,12 +239,7 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
if qs.count() >= 1:
|
||||
role = qs[0]
|
||||
else:
|
||||
role = Role_.objects.create(created=now(),
|
||||
modified=now(),
|
||||
role_field=path,
|
||||
singleton_name=singleton_name,
|
||||
name=singleton_name,
|
||||
description=singleton_name)
|
||||
role = Role_.objects.create(singleton_name=singleton_name, role_field=singleton_name)
|
||||
parents = [role.id]
|
||||
else:
|
||||
parents = resolve_role_field(instance, path)
|
||||
@ -269,4 +256,4 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
Role_ = get_current_apps().get_model('main', 'Role')
|
||||
child_ids = [x for x in Role_.parents.through.objects.filter(to_role_id__in=role_ids).distinct().values_list('from_role_id', flat=True)]
|
||||
Role_.objects.filter(id__in=role_ids).delete()
|
||||
Role_._simultaneous_ancestry_rebuild(child_ids)
|
||||
Role.rebuild_role_ancestor_list([], child_ids)
|
||||
|
||||
@ -468,9 +468,7 @@ def load_inventory_source(source, all_group=None, group_filter_re=None,
|
||||
'''
|
||||
# Sanity check: We sanitize these module names for our API but Ansible proper doesn't follow
|
||||
# good naming conventions
|
||||
if source == 'azure':
|
||||
source = 'windows_azure'
|
||||
|
||||
source = source.replace('azure.py', 'windows_azure.py')
|
||||
logger.debug('Analyzing type of source: %s', source)
|
||||
original_all_group = all_group
|
||||
if not os.path.exists(source):
|
||||
|
||||
@ -2,21 +2,21 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
import taggit.managers
|
||||
import awx.main.fields
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('main', '0007_v300_active_flag_removal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
#
|
||||
# Patch up existing
|
||||
#
|
||||
migrations.RenameField(
|
||||
'Organization',
|
||||
'admins',
|
||||
@ -47,300 +47,6 @@ class Migration(migrations.Migration):
|
||||
name='deprecated_projects',
|
||||
field=models.ManyToManyField(related_name='deprecated_teams', to='main.Project', blank=True),
|
||||
),
|
||||
|
||||
migrations.CreateModel(
|
||||
name='RoleAncestorEntry',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('role_field', models.TextField()),
|
||||
('content_type_id', models.PositiveIntegerField(null=False)),
|
||||
('object_id', models.PositiveIntegerField(null=False)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'main_rbac_role_ancestors',
|
||||
'verbose_name_plural': 'role_ancestors',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('description', models.TextField(default=b'', blank=True)),
|
||||
('name', models.CharField(max_length=512)),
|
||||
('singleton_name', models.TextField(default=None, unique=True, null=True, db_index=True)),
|
||||
('object_id', models.PositiveIntegerField(default=None, null=True)),
|
||||
('ancestors', models.ManyToManyField(related_name='descendents', through='main.RoleAncestorEntry', to='main.Role')),
|
||||
('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('parents', models.ManyToManyField(related_name='children', to='main.Role')),
|
||||
('implicit_parents', models.TextField(null=False, default=b'[]')),
|
||||
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'main_rbac_roles',
|
||||
'verbose_name_plural': 'roles',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roleancestorentry',
|
||||
name='ancestor',
|
||||
field=models.ForeignKey(related_name='+', to='main.Role'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roleancestorentry',
|
||||
name='descendent',
|
||||
field=models.ForeignKey(related_name='+', to='main.Role'),
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='roleancestorentry',
|
||||
index_together=set([('ancestor', 'content_type_id', 'object_id'), ('ancestor', 'content_type_id', 'role_field')]),
|
||||
),
|
||||
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Auditor of the credential', parent_role=[b'singleton:System Auditor'], to='main.Role', role_name=b'Credential Auditor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='owner_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Owner of the credential', parent_role=[b'singleton:System Administrator'], to='main.Role', role_name=b'Credential Owner', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='use_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Credential User', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custominventoryscript',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'CustomInventory Administrator', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custominventoryscript',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'CustomInventory Auditor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custominventoryscript',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.member_role', to='main.Role', role_name=b'CustomInventory Member', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', role_name=b'Inventory Group Administrator', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', role_name=b'Inventory Group Auditor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Inventory Administrator', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Inventory Auditor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=None, to='main.Role', role_name=b'Inventory Executor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=None, to='main.Role', role_name=b'Inventory Updater', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='use_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=[(b'project.admin_role', b'inventory.admin_role')], to='main.Role', role_name=b'Job Template Administrator', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=[(b'project.auditor_role', b'inventory.auditor_role')], to='main.Role', role_name=b'Job Template Auditor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=None, to='main.Role', role_name=b'Job Template Runner', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage all aspects of this organization', parent_role=b'singleton:System Administrator', to='main.Role', role_name=b'Organization Administrator', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this organization', parent_role=b'singleton:System Auditor', to='main.Role', role_name=b'Organization Auditor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this organization', parent_role=b'admin_role', to='main.Role', role_name=b'Organization Member', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this project', parent_role=[b'organization.admin_role', b'singleton:System Administrator'], to='main.Role', role_name=b'Project Administrator', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this project', parent_role=[b'organization.auditor_role', b'singleton:System Auditor'], to='main.Role', role_name=b'Project Auditor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=None, to='main.Role', role_name=b'Project Member', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='scm_update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update this project from the source control management system', parent_role=b'admin_role', to='main.Role', role_name=b'Project Updater', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this team', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Team Administrator', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this team', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Team Auditor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', to='main.Role', role_name=b'Team Member', null=b'True'),
|
||||
),
|
||||
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read this credential', parent_role=[b'use_role', b'auditor_role', b'owner_role'], to='main.Role', role_name=b'Credential REad', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custominventoryscript',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=[b'auditor_role', b'member_role', b'admin_role'], to='main.Role', role_name=b'CustomInventory Read', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='adhoc_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute ad hoc commands against this inventory', parent_role=[b'inventory.adhoc_role', b'parents.adhoc_role', b'admin_role'], to='main.Role', role_name=b'Inventory Ad Hoc', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'execute_role', b'update_role', b'auditor_role', b'admin_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='adhoc_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute ad hoc commands against this inventory', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory Ad Hoc', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view this inventory', parent_role=[b'auditor_role', b'execute_role', b'update_role', b'use_role', b'admin_role'], to='main.Role', role_name=b'Read', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=[b'execute_role', b'auditor_role', b'admin_role'], to='main.Role', role_name=b'Job Template Runner', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read an organization', parent_role=[b'member_role', b'auditor_role'], to='main.Role', role_name=b'Organization Read Access', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read access to this project', parent_role=[b'member_role', b'auditor_role', b'scm_update_role'], to='main.Role', role_name=b'Project Read Access', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='role_field',
|
||||
field=models.TextField(default=b''),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Can view this team', parent_role=[b'admin_role', b'auditor_role', b'member_role'], to='main.Role', role_name=b'Read', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='use_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=[b'owner_role'], to='main.Role', role_name=b'Credential User', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='group',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.execute_role', b'adhoc_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='group',
|
||||
name='update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.update_role', b'admin_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventory',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=b'adhoc_role', to='main.Role', role_name=b'Inventory Executor', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventory',
|
||||
name='update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory Updater', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventory',
|
||||
name='use_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory User', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=[b'admin_role'], to='main.Role', role_name=b'Job Template Runner', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=b'admin_role', to='main.Role', role_name=b'Project Member', null=b'True'),
|
||||
),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
migrations.RenameField(
|
||||
model_name='organization',
|
||||
old_name='projects',
|
||||
@ -380,4 +86,245 @@ class Migration(migrations.Migration):
|
||||
name='credential',
|
||||
unique_together=set([]),
|
||||
),
|
||||
|
||||
|
||||
#
|
||||
# New RBAC models and fields
|
||||
#
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('role_field', models.TextField()),
|
||||
('singleton_name', models.TextField(default=None, unique=True, null=True, db_index=True)),
|
||||
('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)),
|
||||
('parents', models.ManyToManyField(related_name='children', to='main.Role')),
|
||||
('implicit_parents', models.TextField(default=b'[]')),
|
||||
('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)),
|
||||
('object_id', models.PositiveIntegerField(default=None, null=True)),
|
||||
|
||||
],
|
||||
options={
|
||||
'db_table': 'main_rbac_roles',
|
||||
'verbose_name_plural': 'roles',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RoleAncestorEntry',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('role_field', models.TextField()),
|
||||
('content_type_id', models.PositiveIntegerField()),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('ancestor', models.ForeignKey(related_name='+', to='main.Role')),
|
||||
('descendent', models.ForeignKey(related_name='+', to='main.Role')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'main_rbac_role_ancestors',
|
||||
'verbose_name_plural': 'role_ancestors',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='ancestors',
|
||||
field=models.ManyToManyField(related_name='descendents', through='main.RoleAncestorEntry', to='main.Role'),
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='role',
|
||||
index_together=set([('content_type', 'object_id')]),
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='roleancestorentry',
|
||||
index_together=set([('ancestor', 'content_type_id', 'object_id'), ('ancestor', 'content_type_id', 'role_field'), ('ancestor', 'descendent')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_auditor'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='owner_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_administrator'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='use_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'owner_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'use_role', b'auditor_role', b'owner_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custominventoryscript',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custominventoryscript',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custominventoryscript',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.member_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custominventoryscript',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'auditor_role', b'member_role', b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='adhoc_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.adhoc_role', b'parents.adhoc_role', b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.execute_role', b'parents.execute_role', b'adhoc_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.update_role', b'parents.update_role', b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'execute_role', b'update_role', b'auditor_role', b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='adhoc_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'adhoc_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='use_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'auditor_role', b'execute_role', b'update_role', b'use_role', b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[(b'project.admin_role', b'inventory.admin_role')], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[(b'project.auditor_role', b'inventory.auditor_role')], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='execute_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'execute_role', b'auditor_role', b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'singleton:system_administrator', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'singleton:system_auditor', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'member_role', b'auditor_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'organization.admin_role', b'singleton:system_administrator'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'organization.auditor_role', b'singleton:system_auditor'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='scm_update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'member_role', b'auditor_role', b'scm_update_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=None, to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role', b'auditor_role', b'member_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
]
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.migrations import _migration_utils as migration_utils
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@ -12,11 +13,14 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(rbac.init_rbac_migration),
|
||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||
migrations.RunPython(rbac.migrate_users),
|
||||
migrations.RunPython(rbac.create_roles),
|
||||
migrations.RunPython(rbac.migrate_organization),
|
||||
migrations.RunPython(rbac.migrate_team),
|
||||
migrations.RunPython(rbac.migrate_inventory),
|
||||
migrations.RunPython(rbac.migrate_projects),
|
||||
migrations.RunPython(rbac.migrate_credential),
|
||||
migrations.RunPython(rbac.migrate_job_templates),
|
||||
migrations.RunPython(rbac.rebuild_role_hierarchy),
|
||||
]
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from awx.main.migrations import _ask_for_variables as ask_for_variables
|
||||
from awx.main.migrations import _migration_utils as migration_utils
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@ -12,5 +13,6 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||
migrations.RunPython(ask_for_variables.migrate_credential),
|
||||
]
|
||||
|
||||
32
awx/main/migrations/0020_v300_labels_changes.py
Normal file
32
awx/main/migrations/0020_v300_labels_changes.py
Normal 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),
|
||||
),
|
||||
]
|
||||
19
awx/main/migrations/0021_v300_activity_stream.py
Normal file
19
awx/main/migrations/0021_v300_activity_stream.py
Normal 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),
|
||||
),
|
||||
]
|
||||
34
awx/main/migrations/0022_v300_adhoc_extravars.py
Normal file
34
awx/main/migrations/0022_v300_adhoc_extravars.py
Normal 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')]),
|
||||
),
|
||||
]
|
||||
11
awx/main/migrations/_migration_utils.py
Normal file
11
awx/main/migrations/_migration_utils.py
Normal 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)
|
||||
@ -206,9 +206,9 @@ class UserAccess(BaseAccess):
|
||||
return qs
|
||||
return qs.filter(
|
||||
Q(pk=self.user.pk) |
|
||||
Q(organizations__in=self.user.deprecated_admin_of_organizations) |
|
||||
Q(organizations__in=self.user.deprecated_organizations) |
|
||||
Q(deprecated_teams__in=self.user.deprecated_teams)
|
||||
Q(deprecated_organizations__in=self.user.deprecated_admin_of_organizations.all()) |
|
||||
Q(deprecated_organizations__in=self.user.deprecated_organizations.all()) |
|
||||
Q(deprecated_teams__in=self.user.deprecated_teams.all())
|
||||
).distinct()
|
||||
|
||||
def can_add(self, data):
|
||||
@ -563,18 +563,18 @@ class CredentialAccess(BaseAccess):
|
||||
# If the user is a superuser, and therefore can see everything, this
|
||||
# is also sufficient, and we are done.
|
||||
qs = self.model.objects.distinct()
|
||||
qs = qs.select_related('created_by', 'modified_by', 'user', 'team')
|
||||
qs = qs.select_related('created_by', 'modified_by')
|
||||
if self.user.is_superuser:
|
||||
return qs
|
||||
|
||||
# Get the list of organizations for which the user is an admin
|
||||
orgs_as_admin_ids = set(self.user.deprecated_admin_of_organizations.values_list('id', flat=True))
|
||||
return qs.filter(
|
||||
Q(user=self.user) |
|
||||
Q(user__deprecated_organizations__id__in=orgs_as_admin_ids) |
|
||||
Q(user__deprecated_admin_of_organizations__id__in=orgs_as_admin_ids) |
|
||||
Q(team__organization__id__in=orgs_as_admin_ids) |
|
||||
Q(team__deprecated_users__in=[self.user])
|
||||
Q(deprecated_user=self.user) |
|
||||
Q(deprecated_user__deprecated_organizations__id__in=orgs_as_admin_ids) |
|
||||
Q(deprecated_user__deprecated_admin_of_organizations__id__in=orgs_as_admin_ids) |
|
||||
Q(deprecated_team__organization__id__in=orgs_as_admin_ids) |
|
||||
Q(deprecated_team__deprecated_users__in=[self.user])
|
||||
)
|
||||
|
||||
def can_add(self, data):
|
||||
@ -597,22 +597,22 @@ class CredentialAccess(BaseAccess):
|
||||
return False
|
||||
if self.user == obj.created_by:
|
||||
return True
|
||||
if obj.user:
|
||||
if self.user == obj.user:
|
||||
if obj.deprecated_user:
|
||||
if self.user == obj.deprecated_user:
|
||||
return True
|
||||
if obj.user.deprecated_organizations.filter(deprecated_admins__in=[self.user]).exists():
|
||||
if obj.deprecated_user.deprecated_organizations.filter(deprecated_admins__in=[self.user]).exists():
|
||||
return True
|
||||
if obj.user.deprecated_admin_of_organizations.filter(deprecated_admins__in=[self.user]).exists():
|
||||
if obj.deprecated_user.deprecated_admin_of_organizations.filter(deprecated_admins__in=[self.user]).exists():
|
||||
return True
|
||||
if obj.team:
|
||||
if self.user in obj.team.organization.deprecated_admins.all():
|
||||
if obj.deprecated_team:
|
||||
if self.user in obj.deprecated_team.organization.deprecated_admins.all():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_delete(self, obj):
|
||||
# Unassociated credentials may be marked deleted by anyone, though we
|
||||
# shouldn't ever end up with those.
|
||||
if obj.user is None and obj.team is None:
|
||||
if obj.deprecated_user is None and obj.deprecated_team is None:
|
||||
return True
|
||||
return self.can_change(obj, None)
|
||||
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import logging
|
||||
from time import time
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
|
||||
from collections import defaultdict
|
||||
from awx.main.utils import getattrd, set_current_apps
|
||||
from awx.main.utils import getattrd
|
||||
from awx.main.models.rbac import Role, batch_role_ancestor_rebuilding
|
||||
|
||||
import _old_access as old_access
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -27,8 +28,35 @@ def log_migration(wrapped):
|
||||
return wrapper
|
||||
|
||||
@log_migration
|
||||
def init_rbac_migration(apps, schema_editor):
|
||||
set_current_apps(apps)
|
||||
def create_roles(apps, schema_editor):
|
||||
'''
|
||||
Implicit role creation happens in our post_save hook for all of our
|
||||
resources. Here we iterate through all of our resource types and call
|
||||
.save() to ensure all that happens for every object in the system before we
|
||||
get busy with the actual migration work.
|
||||
|
||||
This gets run after migrate_users, which does role creation for users a
|
||||
little differently.
|
||||
'''
|
||||
|
||||
models = [
|
||||
apps.get_model('main', m) for m in [
|
||||
'Organization',
|
||||
'Team',
|
||||
'Inventory',
|
||||
'Group',
|
||||
'Project',
|
||||
'Credential',
|
||||
'CustomInventoryScript',
|
||||
'JobTemplate',
|
||||
]
|
||||
]
|
||||
|
||||
with batch_role_ancestor_rebuilding():
|
||||
for model in models:
|
||||
for obj in model.objects.iterator():
|
||||
obj.save()
|
||||
|
||||
|
||||
@log_migration
|
||||
def migrate_users(apps, schema_editor):
|
||||
@ -44,9 +72,7 @@ def migrate_users(apps, schema_editor):
|
||||
logger.info(smart_text(u"found existing role for user: {}".format(user.username)))
|
||||
except Role.DoesNotExist:
|
||||
role = Role.objects.create(
|
||||
created=now(),
|
||||
modified=now(),
|
||||
singleton_name = smart_text(u'{}-admin_role'.format(user.username)),
|
||||
role_field='admin_role',
|
||||
content_type = user_content_type,
|
||||
object_id = user.id
|
||||
)
|
||||
@ -54,14 +80,12 @@ def migrate_users(apps, schema_editor):
|
||||
logger.info(smart_text(u"migrating to new role for user: {}".format(user.username)))
|
||||
|
||||
if user.is_superuser:
|
||||
if Role.objects.filter(singleton_name='System Administrator').exists():
|
||||
sa_role = Role.objects.get(singleton_name='System Administrator')
|
||||
if Role.objects.filter(singleton_name='system_administrator').exists():
|
||||
sa_role = Role.objects.get(singleton_name='system_administrator')
|
||||
else:
|
||||
sa_role = Role.objects.create(
|
||||
created=now(),
|
||||
modified=now(),
|
||||
singleton_name='System Administrator',
|
||||
name='System Administrator'
|
||||
singleton_name='system_administrator',
|
||||
role_field='system_administrator'
|
||||
)
|
||||
|
||||
sa_role.members.add(user)
|
||||
@ -71,19 +95,17 @@ def migrate_users(apps, schema_editor):
|
||||
def migrate_organization(apps, schema_editor):
|
||||
Organization = apps.get_model('main', "Organization")
|
||||
for org in Organization.objects.iterator():
|
||||
org.save() # force creates missing roles
|
||||
for admin in org.deprecated_admins.all():
|
||||
org.admin_role.members.add(admin)
|
||||
logger.info(smart_text(u"added admin: {}, {}".format(org.name, admin.username)))
|
||||
for user in org.deprecated_users.all():
|
||||
org.auditor_role.members.add(user)
|
||||
logger.info(smart_text(u"added auditor: {}, {}".format(org.name, user.username)))
|
||||
org.member_role.members.add(user)
|
||||
logger.info(smart_text(u"added member: {}, {}".format(org.name, user.username)))
|
||||
|
||||
@log_migration
|
||||
def migrate_team(apps, schema_editor):
|
||||
Team = apps.get_model('main', 'Team')
|
||||
for t in Team.objects.iterator():
|
||||
t.save()
|
||||
for user in t.deprecated_users.all():
|
||||
t.member_role.members.add(user)
|
||||
logger.info(smart_text(u"team: {}, added user: {}".format(t.name, user.username)))
|
||||
@ -103,8 +125,6 @@ def attrfunc(attr_path):
|
||||
|
||||
def _update_credential_parents(org, cred):
|
||||
org.admin_role.children.add(cred.owner_role)
|
||||
org.member_role.children.add(cred.use_role)
|
||||
cred.deprecated_user, cred.deprecated_team = None, None
|
||||
cred.save()
|
||||
|
||||
def _discover_credentials(instances, cred, orgfunc):
|
||||
@ -122,7 +142,12 @@ def _discover_credentials(instances, cred, orgfunc):
|
||||
'''
|
||||
orgs = defaultdict(list)
|
||||
for inst in instances:
|
||||
orgs[orgfunc(inst)].append(inst)
|
||||
try:
|
||||
orgs[orgfunc(inst)].append(inst)
|
||||
except AttributeError:
|
||||
# JobTemplate.inventory can be NULL sometimes, eg when an inventory
|
||||
# has been deleted. This protects against that.
|
||||
pass
|
||||
|
||||
if len(orgs) == 1:
|
||||
_update_credential_parents(orgfunc(instances[0]), cred)
|
||||
@ -136,7 +161,6 @@ def _discover_credentials(instances, cred, orgfunc):
|
||||
cred.save()
|
||||
|
||||
# Unlink the old information from the new credential
|
||||
cred.deprecated_user, cred.deprecated_team = None, None
|
||||
cred.owner_role, cred.use_role = None, None
|
||||
cred.save()
|
||||
|
||||
@ -150,43 +174,32 @@ def migrate_credential(apps, schema_editor):
|
||||
Credential = apps.get_model('main', "Credential")
|
||||
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||
Project = apps.get_model('main', 'Project')
|
||||
Role = apps.get_model('main', 'Role')
|
||||
User = apps.get_model('auth', 'User')
|
||||
InventorySource = apps.get_model('main', 'InventorySource')
|
||||
ContentType = apps.get_model('contenttypes', "ContentType")
|
||||
user_content_type = ContentType.objects.get_for_model(User)
|
||||
|
||||
for cred in Credential.objects.iterator():
|
||||
cred.save()
|
||||
results = (JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all() or
|
||||
InventorySource.objects.filter(credential=cred).all())
|
||||
if results:
|
||||
results = [x for x in JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all()] + \
|
||||
[x for x in InventorySource.objects.filter(credential=cred).all()]
|
||||
if cred.deprecated_team is not None and results:
|
||||
if len(results) == 1:
|
||||
_update_credential_parents(results[0].inventory.organization, cred)
|
||||
else:
|
||||
_discover_credentials(results, cred, attrfunc('inventory.organization'))
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
|
||||
continue
|
||||
|
||||
projs = Project.objects.filter(credential=cred).all()
|
||||
if projs:
|
||||
if cred.deprecated_team is not None and projs:
|
||||
if len(projs) == 1:
|
||||
_update_credential_parents(projs[0].organization, cred)
|
||||
else:
|
||||
_discover_credentials(projs, cred, attrfunc('organization'))
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
|
||||
continue
|
||||
|
||||
if cred.deprecated_team is not None:
|
||||
cred.deprecated_team.admin_role.children.add(cred.owner_role)
|
||||
cred.deprecated_team.member_role.children.add(cred.use_role)
|
||||
cred.deprecated_user, cred.deprecated_team = None, None
|
||||
cred.deprecated_team.member_role.children.add(cred.owner_role)
|
||||
cred.save()
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host)))
|
||||
elif cred.deprecated_user is not None:
|
||||
user_admin_role = Role.objects.get(content_type=user_content_type, object_id=cred.deprecated_user.id)
|
||||
user_admin_role.children.add(cred.owner_role)
|
||||
cred.deprecated_user, cred.deprecated_team = None, None
|
||||
cred.owner_role.members.add(cred.deprecated_user)
|
||||
cred.save()
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host, )))
|
||||
else:
|
||||
@ -205,14 +218,13 @@ def migrate_inventory(apps, schema_editor):
|
||||
return inventory.auditor_role
|
||||
elif perm.permission_type == 'write':
|
||||
return inventory.update_role
|
||||
elif perm.permission_type == 'check' or perm.permission_type == 'run':
|
||||
elif perm.permission_type == 'check' or perm.permission_type == 'run' or perm.permission_type == 'create':
|
||||
# These permission types are handled differntly in RBAC now, nothing to migrate.
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
|
||||
for inventory in Inventory.objects.iterator():
|
||||
inventory.save()
|
||||
for perm in Permission.objects.filter(inventory=inventory):
|
||||
role = None
|
||||
execrole = None
|
||||
@ -260,7 +272,6 @@ def migrate_projects(apps, schema_editor):
|
||||
|
||||
# Migrate projects to single organizations, duplicating as necessary
|
||||
for project in Project.objects.iterator():
|
||||
project.save()
|
||||
original_project_name = project.name
|
||||
project_orgs = project.deprecated_organizations.distinct().all()
|
||||
|
||||
@ -373,7 +384,6 @@ def migrate_job_templates(apps, schema_editor):
|
||||
Permission = apps.get_model('main', 'Permission')
|
||||
|
||||
for jt in JobTemplate.objects.iterator():
|
||||
jt.save()
|
||||
permission = Permission.objects.filter(
|
||||
inventory=jt.inventory,
|
||||
project=jt.project,
|
||||
@ -390,7 +400,7 @@ def migrate_job_templates(apps, schema_editor):
|
||||
jt.execute_role.members.add(user)
|
||||
logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name)))
|
||||
|
||||
if user in jt.execute_role:
|
||||
if jt.execute_role.ancestors.filter(members=user).exists(): # aka "user in jt.execute_role"
|
||||
# If the job template is already accessible by the user, because they
|
||||
# are a sytem, organization, or project admin, then don't add an explicit
|
||||
# role entry for them
|
||||
@ -399,3 +409,22 @@ def migrate_job_templates(apps, schema_editor):
|
||||
if old_access.check_user_access(user, jt.__class__, 'start', jt, False):
|
||||
jt.execute_role.members.add(user)
|
||||
logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name)))
|
||||
|
||||
@log_migration
|
||||
def rebuild_role_hierarchy(apps, schema_editor):
|
||||
logger.info('Computing role roots..')
|
||||
start = time()
|
||||
roots = Role.objects \
|
||||
.all() \
|
||||
.exclude(pk__in=Role.parents.through.objects.all()
|
||||
.values_list('from_role_id', flat=True).distinct()) \
|
||||
.values_list('id', flat=True)
|
||||
stop = time()
|
||||
logger.info('Found %d roots in %f seconds, rebuilding ancestry map' % (len(roots), stop - start))
|
||||
start = time()
|
||||
Role.rebuild_role_ancestor_list(roots, [])
|
||||
stop = time()
|
||||
logger.info('Rebuild completed in %f seconds' % (stop - start))
|
||||
logger.info('Done.')
|
||||
|
||||
|
||||
|
||||
@ -56,6 +56,7 @@ class ActivityStream(models.Model):
|
||||
notifier = models.ManyToManyField("Notifier", blank=True)
|
||||
notification = models.ManyToManyField("Notification", blank=True)
|
||||
label = models.ManyToManyField("Label", blank=True)
|
||||
role = models.ManyToManyField("Role", blank=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:activity_stream_detail', args=(self.pk,))
|
||||
|
||||
@ -84,11 +84,17 @@ class AdHocCommand(UnifiedJob):
|
||||
editable=False,
|
||||
through='AdHocCommandEvent',
|
||||
)
|
||||
extra_vars = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
)
|
||||
|
||||
extra_vars_dict = VarsDictProperty('extra_vars', True)
|
||||
|
||||
def clean_inventory(self):
|
||||
inv = self.inventory
|
||||
if not inv:
|
||||
raise ValidationError('Inventory is no longer available.')
|
||||
raise ValidationError('No valid inventory.')
|
||||
return inv
|
||||
|
||||
def clean_credential(self):
|
||||
|
||||
@ -204,27 +204,19 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
help_text=_('Tenant identifier for this credential'),
|
||||
)
|
||||
owner_role = ImplicitRoleField(
|
||||
role_name='Credential Owner',
|
||||
role_description='Owner of the credential',
|
||||
parent_role=[
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
],
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Credential Auditor',
|
||||
role_description='Auditor of the credential',
|
||||
parent_role=[
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
],
|
||||
)
|
||||
use_role = ImplicitRoleField(
|
||||
role_name='Credential User',
|
||||
role_description='May use this credential, but not read sensitive portions or modify it',
|
||||
parent_role=['owner_role']
|
||||
)
|
||||
read_role = ImplicitRoleField(
|
||||
role_name='Credential REad',
|
||||
role_description='May read this credential',
|
||||
parent_role=[
|
||||
'use_role', 'auditor_role', 'owner_role'
|
||||
],
|
||||
|
||||
@ -97,39 +97,25 @@ class Inventory(CommonModel, ResourceMixin):
|
||||
help_text=_('Number of external inventory sources in this inventory with failures.'),
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Inventory Administrator',
|
||||
role_description='May manage this inventory',
|
||||
parent_role='organization.admin_role',
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Inventory Auditor',
|
||||
role_description='May view but not modify this inventory',
|
||||
parent_role='organization.auditor_role',
|
||||
)
|
||||
update_role = ImplicitRoleField(
|
||||
role_name='Inventory Updater',
|
||||
role_description='May update the inventory',
|
||||
parent_role=['admin_role'],
|
||||
)
|
||||
use_role = ImplicitRoleField(
|
||||
role_name='Inventory User',
|
||||
role_description='May use this inventory, but not read sensitive portions or modify it',
|
||||
parent_role=['admin_role'],
|
||||
)
|
||||
adhoc_role = ImplicitRoleField(
|
||||
role_name='Inventory Ad Hoc',
|
||||
role_description='May execute ad hoc commands against this inventory',
|
||||
parent_role=['admin_role'],
|
||||
)
|
||||
execute_role = ImplicitRoleField(
|
||||
role_name='Inventory Executor',
|
||||
role_description='May execute jobs against this inventory',
|
||||
parent_role='adhoc_role',
|
||||
)
|
||||
read_role = ImplicitRoleField(
|
||||
role_name='Read',
|
||||
parent_role=['auditor_role', 'execute_role', 'update_role', 'use_role', 'admin_role'],
|
||||
role_description='May view this inventory',
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
@ -335,7 +321,7 @@ class Inventory(CommonModel, ResourceMixin):
|
||||
return self.groups.exclude(parents__pk__in=group_pks).distinct()
|
||||
|
||||
|
||||
class Host(CommonModelNameNotUnique, ResourceMixin):
|
||||
class Host(CommonModelNameNotUnique):
|
||||
'''
|
||||
A managed node
|
||||
'''
|
||||
@ -531,28 +517,21 @@ class Group(CommonModelNameNotUnique, ResourceMixin):
|
||||
help_text=_('Inventory source(s) that created or modified this group.'),
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Administrator',
|
||||
parent_role=['inventory.admin_role', 'parents.admin_role'],
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Auditor',
|
||||
parent_role=['inventory.auditor_role', 'parents.auditor_role'],
|
||||
)
|
||||
update_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Updater',
|
||||
parent_role=['inventory.update_role', 'parents.update_role', 'admin_role'],
|
||||
)
|
||||
adhoc_role = ImplicitRoleField(
|
||||
role_name='Inventory Ad Hoc',
|
||||
parent_role=['inventory.adhoc_role', 'parents.adhoc_role', 'admin_role'],
|
||||
role_description='May execute ad hoc commands against this inventory',
|
||||
)
|
||||
execute_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Executor',
|
||||
parent_role=['inventory.execute_role', 'parents.execute_role', 'adhoc_role'],
|
||||
)
|
||||
read_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Executor',
|
||||
parent_role=['execute_role', 'update_role', 'auditor_role', 'admin_role'],
|
||||
)
|
||||
|
||||
@ -1321,25 +1300,15 @@ class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
|
||||
)
|
||||
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='CustomInventory Administrator',
|
||||
role_description='May manage this inventory',
|
||||
parent_role='organization.admin_role',
|
||||
)
|
||||
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='CustomInventory Member',
|
||||
role_description='May view but not modify this inventory',
|
||||
parent_role='organization.member_role',
|
||||
)
|
||||
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='CustomInventory Auditor',
|
||||
role_description='May view but not modify this inventory',
|
||||
parent_role='organization.auditor_role',
|
||||
)
|
||||
read_role = ImplicitRoleField(
|
||||
role_name='CustomInventory Read',
|
||||
role_description='May view but not modify this inventory',
|
||||
parent_role=['auditor_role', 'member_role', 'admin_role'],
|
||||
)
|
||||
|
||||
|
||||
@ -135,11 +135,6 @@ class JobOptions(BaseModel):
|
||||
become_enabled = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
labels = models.ManyToManyField(
|
||||
"Label",
|
||||
blank=True,
|
||||
related_name='%(class)s_labels'
|
||||
)
|
||||
|
||||
extra_vars_dict = VarsDictProperty('extra_vars', True)
|
||||
|
||||
@ -226,23 +221,15 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
default={},
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Job Template Administrator',
|
||||
role_description='Full access to all settings',
|
||||
parent_role=[('project.admin_role', 'inventory.admin_role')]
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Job Template Auditor',
|
||||
role_description='Read-only access to all settings',
|
||||
parent_role=[('project.auditor_role', 'inventory.auditor_role')]
|
||||
)
|
||||
execute_role = ImplicitRoleField(
|
||||
role_name='Job Template Runner',
|
||||
role_description='May run the job template',
|
||||
parent_role=['admin_role'],
|
||||
)
|
||||
read_role = ImplicitRoleField(
|
||||
role_name='Job Template Runner',
|
||||
role_description='May run the job template',
|
||||
parent_role=['execute_role', 'auditor_role', 'admin_role'],
|
||||
)
|
||||
|
||||
@ -523,6 +510,36 @@ class Job(UnifiedJob, JobOptions):
|
||||
return self.job_template.ask_variables_on_launch
|
||||
return False
|
||||
|
||||
@property
|
||||
def ask_limit_on_launch(self):
|
||||
if self.job_template is not None:
|
||||
return self.job_template.ask_limit_on_launch
|
||||
return False
|
||||
|
||||
@property
|
||||
def ask_tags_on_launch(self):
|
||||
if self.job_template is not None:
|
||||
return self.job_template.ask_tags_on_launch
|
||||
return False
|
||||
|
||||
@property
|
||||
def ask_job_type_on_launch(self):
|
||||
if self.job_template is not None:
|
||||
return self.job_template.ask_job_type_on_launch
|
||||
return False
|
||||
|
||||
@property
|
||||
def ask_inventory_on_launch(self):
|
||||
if self.job_template is not None:
|
||||
return self.job_template.ask_inventory_on_launch
|
||||
return False
|
||||
|
||||
@property
|
||||
def ask_credential_on_launch(self):
|
||||
if self.job_template is not None:
|
||||
return self.job_template.ask_credential_on_launch
|
||||
return False
|
||||
|
||||
def get_passwords_needed_to_start(self):
|
||||
return self.passwords_needed_to_start
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# AWX
|
||||
from awx.main.models.base import CommonModelNameNotUnique
|
||||
from awx.main.models.unified_jobs import UnifiedJobTemplate, UnifiedJob
|
||||
|
||||
__all__ = ('Label', )
|
||||
|
||||
@ -39,3 +40,19 @@ class Label(CommonModelNameNotUnique):
|
||||
jobtemplate_labels__isnull=True
|
||||
)
|
||||
|
||||
def is_detached(self):
|
||||
return bool(
|
||||
Label.objects.filter(
|
||||
id=self.id,
|
||||
unifiedjob_labels__isnull=True,
|
||||
unifiedjobtemplate_labels__isnull=True
|
||||
).count())
|
||||
|
||||
def is_candidate_for_detach(self):
|
||||
c1 = UnifiedJob.objects.filter(labels__in=[self.id]).count()
|
||||
c2 = UnifiedJobTemplate.objects.filter(labels__in=[self.id]).count()
|
||||
if (c1 + c2 - 1) == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@ -53,23 +53,15 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin):
|
||||
related_name='deprecated_organizations',
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Organization Administrator',
|
||||
role_description='May manage all aspects of this organization',
|
||||
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Organization Auditor',
|
||||
role_description='May read all settings associated with this organization',
|
||||
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Organization Member',
|
||||
role_description='A member of this organization',
|
||||
parent_role='admin_role',
|
||||
)
|
||||
read_role = ImplicitRoleField(
|
||||
role_name='Organization Read Access',
|
||||
role_description='Read an organization',
|
||||
parent_role=['member_role', 'auditor_role'],
|
||||
)
|
||||
|
||||
@ -110,22 +102,13 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
|
||||
related_name='deprecated_teams',
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Team Administrator',
|
||||
role_description='May manage this team',
|
||||
parent_role='organization.admin_role',
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Team Auditor',
|
||||
role_description='May read all settings associated with this team',
|
||||
parent_role='organization.auditor_role',
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Team Member',
|
||||
role_description='A member of this team',
|
||||
)
|
||||
member_role = ImplicitRoleField()
|
||||
read_role = ImplicitRoleField(
|
||||
role_name='Read',
|
||||
role_description='Can view this team',
|
||||
parent_role=['admin_role', 'auditor_role', 'member_role'],
|
||||
)
|
||||
|
||||
|
||||
@ -221,34 +221,24 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
|
||||
blank=True,
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Project Administrator',
|
||||
role_description='May manage this project',
|
||||
parent_role=[
|
||||
'organization.admin_role',
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
],
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Project Auditor',
|
||||
role_description='May read all settings associated with this project',
|
||||
parent_role=[
|
||||
'organization.auditor_role',
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
],
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Project Member',
|
||||
role_description='Implies membership within this project',
|
||||
parent_role='admin_role',
|
||||
)
|
||||
scm_update_role = ImplicitRoleField(
|
||||
role_name='Project Updater',
|
||||
role_description='May update this project from the source control management system',
|
||||
parent_role='admin_role',
|
||||
)
|
||||
read_role = ImplicitRoleField(
|
||||
role_name='Project Read Access',
|
||||
role_description='Read access to this project',
|
||||
parent_role=['member_role', 'auditor_role', 'scm_update_role'],
|
||||
)
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import contextlib
|
||||
|
||||
# Django
|
||||
from django.db import models, transaction, connection
|
||||
from django.db.models import Q
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -29,8 +28,39 @@ __all__ = [
|
||||
|
||||
logger = logging.getLogger('awx.main.models.rbac')
|
||||
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator'
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor'
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='system_administrator'
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR='system_auditor'
|
||||
|
||||
role_names = {
|
||||
'system_administrator' : 'System Administrator',
|
||||
'system_auditor' : 'System Auditor',
|
||||
'adhoc_role' : 'Ad Hoc',
|
||||
'admin_role' : 'Admin',
|
||||
'auditor_role' : 'Auditor',
|
||||
'execute_role' : 'Execute',
|
||||
'member_role' : 'Member',
|
||||
'owner_role' : 'Owner',
|
||||
'read_role' : 'Read',
|
||||
'scm_update_role' : 'SCM Update',
|
||||
'update_role' : 'Update',
|
||||
'use_role' : 'Use',
|
||||
}
|
||||
|
||||
role_descriptions = {
|
||||
'system_administrator' : '[TODO] System Administrator',
|
||||
'system_auditor' : '[TODO] System Auditor',
|
||||
'adhoc_role' : '[TODO] Ad Hoc',
|
||||
'admin_role' : '[TODO] Admin',
|
||||
'auditor_role' : '[TODO] Auditor',
|
||||
'execute_role' : '[TODO] Execute',
|
||||
'member_role' : '[TODO] Member',
|
||||
'owner_role' : '[TODO] Owner',
|
||||
'read_role' : '[TODO] Read',
|
||||
'scm_update_role' : '[TODO] SCM Update',
|
||||
'update_role' : '[TODO] Update',
|
||||
'use_role' : '[TODO] Use',
|
||||
}
|
||||
|
||||
|
||||
tls = threading.local() # thread local storage
|
||||
|
||||
@ -51,23 +81,22 @@ def batch_role_ancestor_rebuilding(allow_nesting=False):
|
||||
try:
|
||||
setattr(tls, 'batch_role_rebuilding', True)
|
||||
if not batch_role_rebuilding:
|
||||
setattr(tls, 'roles_needing_rebuilding', set())
|
||||
setattr(tls, 'additions', set())
|
||||
setattr(tls, 'removals', set())
|
||||
yield
|
||||
|
||||
finally:
|
||||
setattr(tls, 'batch_role_rebuilding', batch_role_rebuilding)
|
||||
if not batch_role_rebuilding:
|
||||
rebuild_set = getattr(tls, 'roles_needing_rebuilding')
|
||||
additions = getattr(tls, 'additions')
|
||||
removals = getattr(tls, 'removals')
|
||||
with transaction.atomic():
|
||||
Role._simultaneous_ancestry_rebuild(list(rebuild_set))
|
||||
|
||||
#for role in Role.objects.filter(id__in=list(rebuild_set)).all():
|
||||
# # TODO: We can reduce this to one rebuild call with our new upcoming rebuild method.. do this
|
||||
# role.rebuild_role_ancestor_list()
|
||||
delattr(tls, 'roles_needing_rebuilding')
|
||||
Role.rebuild_role_ancestor_list(list(additions), list(removals))
|
||||
delattr(tls, 'additions')
|
||||
delattr(tls, 'removals')
|
||||
|
||||
|
||||
class Role(CommonModelNameNotUnique):
|
||||
class Role(models.Model):
|
||||
'''
|
||||
Role model
|
||||
'''
|
||||
@ -76,9 +105,12 @@ class Role(CommonModelNameNotUnique):
|
||||
app_label = 'main'
|
||||
verbose_name_plural = _('roles')
|
||||
db_table = 'main_rbac_roles'
|
||||
index_together = [
|
||||
("content_type", "object_id")
|
||||
]
|
||||
|
||||
role_field = models.TextField(null=False)
|
||||
singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True)
|
||||
role_field = models.TextField(null=False, default='')
|
||||
parents = models.ManyToManyField('Role', related_name='children')
|
||||
implicit_parents = models.TextField(null=False, default='[]')
|
||||
ancestors = models.ManyToManyField(
|
||||
@ -94,7 +126,7 @@ class Role(CommonModelNameNotUnique):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(Role, self).save(*args, **kwargs)
|
||||
self.rebuild_role_ancestor_list()
|
||||
self.rebuild_role_ancestor_list([self.id], [])
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:role_detail', args=(self.pk,))
|
||||
@ -112,20 +144,36 @@ class Role(CommonModelNameNotUnique):
|
||||
object_id=accessor.id)
|
||||
return self.ancestors.filter(pk__in=roles).exists()
|
||||
|
||||
def rebuild_role_ancestor_list(self):
|
||||
@property
|
||||
def name(self):
|
||||
global role_names
|
||||
return role_names[self.role_field]
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
global role_descriptions
|
||||
return role_descriptions[self.role_field]
|
||||
|
||||
@staticmethod
|
||||
def rebuild_role_ancestor_list(additions, removals):
|
||||
'''
|
||||
Updates our `ancestors` map to accurately reflect all of the ancestors for a role
|
||||
|
||||
You should never need to call this. Signal handlers should be calling
|
||||
this method when the role hierachy changes automatically.
|
||||
|
||||
Note that this method relies on any parents' ancestor list being correct.
|
||||
'''
|
||||
Role._simultaneous_ancestry_rebuild([self.id])
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _simultaneous_ancestry_rebuild(role_ids_to_rebuild):
|
||||
# The ancestry table
|
||||
# =================================================
|
||||
#
|
||||
# The role ancestors table denormalizes the parental relations
|
||||
# between all roles in the system. If you have role A which is a
|
||||
# parent of B which is a parent of C, then the ancestors table will
|
||||
# contain a row noting that B is a descendent of A, and two rows for
|
||||
# denoting that C is a descendent of both A and B. In addition to
|
||||
# storing entries for each descendent relationship, we also store an
|
||||
# entry that states that C is a 'descendent' of itself, C. This makes
|
||||
# usage of this table simple in our queries as it enables us to do
|
||||
# straight joins where we would have to do unions otherwise.
|
||||
#
|
||||
# The simple version of what this function is doing
|
||||
# =================================================
|
||||
@ -163,37 +211,18 @@ class Role(CommonModelNameNotUnique):
|
||||
#
|
||||
# SQL Breakdown
|
||||
# =============
|
||||
# The Role ancestors has three columns, (id, from_role_id, to_role_id)
|
||||
#
|
||||
# id: Unqiue row ID
|
||||
# from_role_id: Descendent role ID
|
||||
# to_role_id: Ancestor role ID
|
||||
#
|
||||
# *NOTE* In addition to mapping roles to parents, there also
|
||||
# always exists must exist an entry where
|
||||
#
|
||||
# from_role_id == role_id == to_role_id
|
||||
#
|
||||
# this makes our joins simple when we go to derive permissions or
|
||||
# accessible objects.
|
||||
#
|
||||
#
|
||||
# We operate under the assumption that our parent's ancestor list is
|
||||
# correct, thus we can always compute what our ancestor list should
|
||||
# be by taking the union of our parent's ancestor lists and adding
|
||||
# our self reference entry from_role_id == role_id == to_role_id
|
||||
# our self reference entry where ancestor_id = descendent_id
|
||||
#
|
||||
# The inner query for the two SQL statements compute this union,
|
||||
# the union of the parent's ancestors and the self referncing entry,
|
||||
# for all roles in the current set of roles to rebuild.
|
||||
# The DELETE query deletes all entries in the ancestor table that
|
||||
# should no longer be there (as determined by the NOT EXISTS query,
|
||||
# which checks to see if the ancestor is still an ancestor of one
|
||||
# or more of our parents)
|
||||
#
|
||||
# The DELETE query uses this to select all entries on disk for the
|
||||
# roles we're dealing with, and removes the entries that are not in
|
||||
# this list.
|
||||
#
|
||||
# The INSERT query uses this to select all entries in the list that
|
||||
# are not in the database yet, and inserts all of the missing
|
||||
# records.
|
||||
# The INSERT query computes the list of what our ancestor maps should
|
||||
# be, and inserts any missing entries.
|
||||
#
|
||||
# Once complete, we select all of the children for the roles we are
|
||||
# working with, this list becomes the new role list we are working
|
||||
@ -205,18 +234,17 @@ class Role(CommonModelNameNotUnique):
|
||||
#
|
||||
#
|
||||
|
||||
if len(role_ids_to_rebuild) == 0:
|
||||
if len(additions) == 0 and len(removals) == 0:
|
||||
return
|
||||
|
||||
global tls
|
||||
batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False)
|
||||
|
||||
if batch_role_rebuilding:
|
||||
roles_needing_rebuilding = getattr(tls, 'roles_needing_rebuilding')
|
||||
roles_needing_rebuilding.update(set(role_ids_to_rebuild))
|
||||
getattr(tls, 'additions').update(set(additions))
|
||||
getattr(tls, 'removals').update(set(removals))
|
||||
return
|
||||
|
||||
|
||||
cursor = connection.cursor()
|
||||
loop_ct = 0
|
||||
|
||||
@ -226,94 +254,143 @@ class Role(CommonModelNameNotUnique):
|
||||
'roles_table': Role._meta.db_table,
|
||||
}
|
||||
|
||||
# SQLlite has a 1M sql statement limit.. since the django sqllite
|
||||
# driver isn't letting us pass in the ids through the preferred
|
||||
# parameter binding system, this function exists to obey this.
|
||||
# est max 12 bytes per number, used up to 2 times in a query,
|
||||
# minus 4k of padding for the other parts of the query, leads us
|
||||
# to the magic number of 41496, or 40000 for a nice round number
|
||||
def split_ids_for_sqlite(role_ids):
|
||||
for i in xrange(0, len(role_ids), 999):
|
||||
yield role_ids[i:i + 999]
|
||||
|
||||
while role_ids_to_rebuild:
|
||||
if loop_ct > 1000:
|
||||
raise Exception('Ancestry role rebuilding error: infinite loop detected')
|
||||
loop_ct += 1
|
||||
|
||||
delete_ct = 0
|
||||
for ids in split_ids_for_sqlite(role_ids_to_rebuild):
|
||||
sql_params['ids'] = ','.join(str(x) for x in ids)
|
||||
cursor.execute('''
|
||||
DELETE FROM %(ancestors_table)s
|
||||
WHERE descendent_id IN (%(ids)s)
|
||||
AND
|
||||
id NOT IN (
|
||||
SELECT %(ancestors_table)s.id FROM (
|
||||
SELECT parents.from_role_id from_id, ancestors.ancestor_id to_id
|
||||
FROM %(parents_table)s as parents
|
||||
LEFT JOIN %(ancestors_table)s as ancestors
|
||||
ON (parents.to_role_id = ancestors.descendent_id)
|
||||
WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL
|
||||
|
||||
UNION
|
||||
|
||||
SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s)
|
||||
) new_ancestry_list
|
||||
LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id
|
||||
AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id)
|
||||
WHERE %(ancestors_table)s.id IS NOT NULL
|
||||
)
|
||||
''' % sql_params)
|
||||
delete_ct += cursor.rowcount
|
||||
|
||||
insert_ct = 0
|
||||
for ids in split_ids_for_sqlite(role_ids_to_rebuild):
|
||||
sql_params['ids'] = ','.join(str(x) for x in ids)
|
||||
cursor.execute('''
|
||||
INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id)
|
||||
SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM (
|
||||
SELECT parents.from_role_id from_id,
|
||||
ancestors.ancestor_id to_id,
|
||||
roles.role_field,
|
||||
COALESCE(roles.content_type_id, 0) content_type_id,
|
||||
COALESCE(roles.object_id, 0) object_id
|
||||
FROM %(parents_table)s as parents
|
||||
INNER JOIN %(roles_table)s as roles ON (parents.from_role_id = roles.id)
|
||||
LEFT OUTER JOIN %(ancestors_table)s as ancestors
|
||||
ON (parents.to_role_id = ancestors.descendent_id)
|
||||
WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL
|
||||
|
||||
UNION
|
||||
|
||||
SELECT id from_id,
|
||||
id to_id,
|
||||
role_field,
|
||||
COALESCE(content_type_id, 0) content_type_id,
|
||||
COALESCE(object_id, 0) object_id
|
||||
from %(roles_table)s WHERE id IN (%(ids)s)
|
||||
) new_ancestry_list
|
||||
LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id
|
||||
AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id)
|
||||
WHERE %(ancestors_table)s.id IS NULL
|
||||
''' % sql_params)
|
||||
insert_ct += cursor.rowcount
|
||||
|
||||
if insert_ct == 0 and delete_ct == 0:
|
||||
break
|
||||
|
||||
new_role_ids_to_rebuild = set()
|
||||
for ids in split_ids_for_sqlite(role_ids_to_rebuild):
|
||||
sql_params['ids'] = ','.join(str(x) for x in ids)
|
||||
new_role_ids_to_rebuild.update(set(Role.objects.distinct()
|
||||
.filter(id__in=ids, children__id__isnull=False)
|
||||
.values_list('children__id', flat=True)))
|
||||
role_ids_to_rebuild = list(new_role_ids_to_rebuild)
|
||||
for i in xrange(0, len(role_ids), 40000):
|
||||
yield role_ids[i:i + 40000]
|
||||
|
||||
|
||||
with transaction.atomic():
|
||||
while len(additions) > 0 or len(removals) > 0:
|
||||
if loop_ct > 100:
|
||||
raise Exception('Role ancestry rebuilding error: infinite loop detected')
|
||||
loop_ct += 1
|
||||
|
||||
delete_ct = 0
|
||||
if len(removals) > 0:
|
||||
for ids in split_ids_for_sqlite(removals):
|
||||
sql_params['ids'] = ','.join(str(x) for x in ids)
|
||||
cursor.execute('''
|
||||
DELETE FROM %(ancestors_table)s
|
||||
WHERE descendent_id IN (%(ids)s)
|
||||
AND descendent_id != ancestor_id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM %(parents_table)s as parents
|
||||
INNER JOIN %(ancestors_table)s as inner_ancestors
|
||||
ON (parents.to_role_id = inner_ancestors.descendent_id)
|
||||
WHERE parents.from_role_id = %(ancestors_table)s.descendent_id
|
||||
AND %(ancestors_table)s.ancestor_id = inner_ancestors.ancestor_id
|
||||
)
|
||||
''' % sql_params)
|
||||
|
||||
delete_ct += cursor.rowcount
|
||||
|
||||
insert_ct = 0
|
||||
if len(additions) > 0:
|
||||
for ids in split_ids_for_sqlite(additions):
|
||||
sql_params['ids'] = ','.join(str(x) for x in ids)
|
||||
cursor.execute('''
|
||||
INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id)
|
||||
SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM (
|
||||
SELECT roles.id from_id,
|
||||
ancestors.ancestor_id to_id,
|
||||
roles.role_field,
|
||||
COALESCE(roles.content_type_id, 0) content_type_id,
|
||||
COALESCE(roles.object_id, 0) object_id
|
||||
FROM %(roles_table)s as roles
|
||||
INNER JOIN %(parents_table)s as parents
|
||||
ON (parents.from_role_id = roles.id)
|
||||
INNER JOIN %(ancestors_table)s as ancestors
|
||||
ON (parents.to_role_id = ancestors.descendent_id)
|
||||
WHERE roles.id IN (%(ids)s)
|
||||
|
||||
UNION
|
||||
|
||||
SELECT id from_id,
|
||||
id to_id,
|
||||
role_field,
|
||||
COALESCE(content_type_id, 0) content_type_id,
|
||||
COALESCE(object_id, 0) object_id
|
||||
from %(roles_table)s WHERE id IN (%(ids)s)
|
||||
) new_ancestry_list
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM %(ancestors_table)s
|
||||
WHERE %(ancestors_table)s.descendent_id = new_ancestry_list.from_id
|
||||
AND %(ancestors_table)s.ancestor_id = new_ancestry_list.to_id
|
||||
)
|
||||
|
||||
''' % sql_params)
|
||||
insert_ct += cursor.rowcount
|
||||
|
||||
if insert_ct == 0 and delete_ct == 0:
|
||||
break
|
||||
|
||||
new_additions = set()
|
||||
for ids in split_ids_for_sqlite(additions):
|
||||
sql_params['ids'] = ','.join(str(x) for x in ids)
|
||||
# get all children for the roles we're operating on
|
||||
cursor.execute('SELECT DISTINCT from_role_id FROM %(parents_table)s WHERE to_role_id IN (%(ids)s)' % sql_params)
|
||||
new_additions.update([row[0] for row in cursor.fetchall()])
|
||||
additions = list(new_additions)
|
||||
|
||||
new_removals = set()
|
||||
for ids in split_ids_for_sqlite(removals):
|
||||
sql_params['ids'] = ','.join(str(x) for x in ids)
|
||||
# get all children for the roles we're operating on
|
||||
cursor.execute('SELECT DISTINCT from_role_id FROM %(parents_table)s WHERE to_role_id IN (%(ids)s)' % sql_params)
|
||||
new_removals.update([row[0] for row in cursor.fetchall()])
|
||||
removals = list(new_removals)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def visible_roles(user):
|
||||
return Role.objects.filter(Q(descendents__in=user.roles.filter()) | Q(ancestors__in=user.roles.filter()))
|
||||
sql_params = {
|
||||
'ancestors_table': Role.ancestors.through._meta.db_table,
|
||||
'parents_table': Role.parents.through._meta.db_table,
|
||||
'roles_table': Role._meta.db_table,
|
||||
'ids': ','.join(str(x) for x in user.roles.values_list('id', flat=True))
|
||||
}
|
||||
|
||||
qs = Role.objects.extra(
|
||||
where = ['''
|
||||
%(roles_table)s.id IN (
|
||||
SELECT descendent_id FROM %(ancestors_table)s WHERE ancestor_id IN (%(ids)s)
|
||||
UNION
|
||||
SELECT ancestor_id FROM %(ancestors_table)s WHERE descendent_id IN (%(ids)s)
|
||||
)
|
||||
''' % sql_params]
|
||||
)
|
||||
return qs
|
||||
|
||||
@staticmethod
|
||||
def filter_visible_roles(user, roles_qs):
|
||||
sql_params = {
|
||||
'ancestors_table': Role.ancestors.through._meta.db_table,
|
||||
'parents_table': Role.parents.through._meta.db_table,
|
||||
'roles_table': Role._meta.db_table,
|
||||
'ids': ','.join(str(x) for x in user.roles.all().values_list('id', flat=True))
|
||||
}
|
||||
|
||||
qs = roles_qs.extra(
|
||||
where = ['''
|
||||
EXISTS (
|
||||
SELECT 1 FROM
|
||||
%(ancestors_table)s
|
||||
WHERE (descendent_id = %(roles_table)s.id AND ancestor_id IN (%(ids)s))
|
||||
OR (ancestor_id = %(roles_table)s.id AND descendent_id IN (%(ids)s))
|
||||
) ''' % sql_params]
|
||||
)
|
||||
return qs
|
||||
|
||||
@staticmethod
|
||||
def singleton(name):
|
||||
role, _ = Role.objects.get_or_create(singleton_name=name, name=name)
|
||||
role, _ = Role.objects.get_or_create(singleton_name=name, role_field=name)
|
||||
return role
|
||||
|
||||
def is_ancestor_of(self, role):
|
||||
@ -328,6 +405,7 @@ class RoleAncestorEntry(models.Model):
|
||||
index_together = [
|
||||
("ancestor", "content_type_id", "object_id"), # used by get_roles_on_resource
|
||||
("ancestor", "content_type_id", "role_field"), # used by accessible_objects
|
||||
("ancestor", "descendent"), # used by rebuild_role_ancestor_list in the NOT EXISTS clauses.
|
||||
]
|
||||
|
||||
descendent = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+')
|
||||
@ -359,5 +437,5 @@ def get_roles_on_resource(resource, accessor):
|
||||
ancestor__in=roles,
|
||||
content_type_id=ContentType.objects.get_for_model(resource).id,
|
||||
object_id=resource.id
|
||||
).values_list('role_field', flat=True)
|
||||
).values_list('role_field', flat=True).distinct()
|
||||
]
|
||||
|
||||
@ -141,6 +141,11 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
default='ok',
|
||||
editable=False,
|
||||
)
|
||||
labels = models.ManyToManyField(
|
||||
"Label",
|
||||
blank=True,
|
||||
related_name='%(class)s_labels'
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
real_instance = self.get_real_instance()
|
||||
@ -476,6 +481,12 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
labels = models.ManyToManyField(
|
||||
"Label",
|
||||
blank=True,
|
||||
related_name='%(class)s_labels'
|
||||
)
|
||||
|
||||
|
||||
def get_absolute_url(self):
|
||||
real_instance = self.get_real_instance()
|
||||
|
||||
@ -43,6 +43,8 @@ class SlackBackend(TowerBaseEmailBackend):
|
||||
for m in messages:
|
||||
try:
|
||||
for r in m.recipients():
|
||||
if r.startswith('#'):
|
||||
r = r[1:]
|
||||
self.connection.rtm_send_message(r, m.subject)
|
||||
sent_messages += 1
|
||||
except Exception as e:
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed
|
||||
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
|
||||
|
||||
logger = logging.getLogger('awx.main.registrar')
|
||||
|
||||
@ -17,12 +17,12 @@ class ActivityStreamRegistrar(object):
|
||||
if not getattr(tower_settings, 'ACTIVITY_STREAM_ENABLED', True):
|
||||
return
|
||||
from awx.main.signals import activity_stream_create, activity_stream_update, activity_stream_delete, activity_stream_associate
|
||||
|
||||
|
||||
if model not in self.models:
|
||||
self.models.append(model)
|
||||
post_save.connect(activity_stream_create, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_create")
|
||||
pre_save.connect(activity_stream_update, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_update")
|
||||
post_delete.connect(activity_stream_delete, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_delete")
|
||||
pre_delete.connect(activity_stream_delete, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_delete")
|
||||
|
||||
for m2mfield in model._meta.many_to_many:
|
||||
try:
|
||||
@ -36,7 +36,7 @@ class ActivityStreamRegistrar(object):
|
||||
if model in self.models:
|
||||
post_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_create")
|
||||
pre_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_update")
|
||||
post_delete.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_delete")
|
||||
pre_delete.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_delete")
|
||||
self.models.pop(model)
|
||||
|
||||
|
||||
|
||||
@ -108,12 +108,17 @@ def emit_update_inventory_on_created_or_deleted(sender, **kwargs):
|
||||
|
||||
def rebuild_role_ancestor_list(reverse, model, instance, pk_set, action, **kwargs):
|
||||
'When a role parent is added or removed, update our role hierarchy list'
|
||||
if action in ['post_add', 'post_remove', 'post_clear']:
|
||||
if action == 'post_add':
|
||||
if reverse:
|
||||
for id in pk_set:
|
||||
model.objects.get(id=id).rebuild_role_ancestor_list()
|
||||
model.rebuild_role_ancestor_list(list(pk_set), [])
|
||||
else:
|
||||
instance.rebuild_role_ancestor_list()
|
||||
model.rebuild_role_ancestor_list([instance.id], [])
|
||||
|
||||
if action in ['post_remove', 'post_clear']:
|
||||
if reverse:
|
||||
model.rebuild_role_ancestor_list([], list(pk_set))
|
||||
else:
|
||||
model.rebuild_role_ancestor_list([], [instance.id])
|
||||
|
||||
def sync_superuser_status_to_rbac(instance, **kwargs):
|
||||
'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role'
|
||||
@ -127,11 +132,10 @@ def create_user_role(instance, **kwargs):
|
||||
Role.objects.get(
|
||||
content_type=ContentType.objects.get_for_model(instance),
|
||||
object_id=instance.id,
|
||||
name = 'User Admin'
|
||||
role_field='admin_role'
|
||||
)
|
||||
except Role.DoesNotExist:
|
||||
role = Role.objects.create(
|
||||
name = 'User Admin',
|
||||
role_field='admin_role',
|
||||
content_object = instance,
|
||||
)
|
||||
@ -152,6 +156,24 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs):
|
||||
if action == 'pre_remove':
|
||||
instance.content_object.admin_role.children.remove(user.admin_role)
|
||||
|
||||
def rbac_activity_stream(instance, sender, **kwargs):
|
||||
user_type = ContentType.objects.get_for_model(User)
|
||||
# Only if we are associating/disassociating
|
||||
if kwargs['action'] in ['pre_add', 'pre_remove']:
|
||||
# Only if this isn't for the User.admin_role
|
||||
if hasattr(instance, 'content_type'):
|
||||
if instance.content_type in [None, user_type]:
|
||||
return
|
||||
role = instance
|
||||
instance = instance.content_object
|
||||
else:
|
||||
role = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']).first()
|
||||
activity_stream_associate(sender, instance, role=role, **kwargs)
|
||||
|
||||
def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs):
|
||||
for l in instance.labels.all():
|
||||
if l.is_candidate_for_detach():
|
||||
l.delete()
|
||||
|
||||
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
|
||||
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
|
||||
@ -169,9 +191,11 @@ post_save.connect(emit_job_event_detail, sender=JobEvent)
|
||||
post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent)
|
||||
m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through)
|
||||
m2m_changed.connect(org_admin_edit_members, Role.members.through)
|
||||
m2m_changed.connect(rbac_activity_stream, Role.members.through)
|
||||
post_save.connect(sync_superuser_status_to_rbac, sender=User)
|
||||
post_save.connect(create_user_role, sender=User)
|
||||
|
||||
pre_delete.connect(cleanup_detached_labels_on_deleted_parent, sender=UnifiedJob)
|
||||
pre_delete.connect(cleanup_detached_labels_on_deleted_parent, sender=UnifiedJobTemplate)
|
||||
|
||||
# Migrate hosts, groups to parent group(s) whenever a group is deleted
|
||||
|
||||
@ -331,14 +355,10 @@ def activity_stream_update(sender, instance, **kwargs):
|
||||
def activity_stream_delete(sender, instance, **kwargs):
|
||||
if not activity_stream_enabled:
|
||||
return
|
||||
try:
|
||||
old = sender.objects.get(id=instance.id)
|
||||
except sender.DoesNotExist:
|
||||
return
|
||||
# Skip recording any inventory source directly associated with a group.
|
||||
if isinstance(instance, InventorySource) and instance.group:
|
||||
return
|
||||
changes = model_instance_diff(old, instance)
|
||||
changes = model_to_dict(instance)
|
||||
object1 = camelcase_to_underscore(instance.__class__.__name__)
|
||||
activity_entry = ActivityStream(
|
||||
operation='delete',
|
||||
@ -349,7 +369,7 @@ def activity_stream_delete(sender, instance, **kwargs):
|
||||
def activity_stream_associate(sender, instance, **kwargs):
|
||||
if not activity_stream_enabled:
|
||||
return
|
||||
if 'pre_add' in kwargs['action'] or 'pre_remove' in kwargs['action']:
|
||||
if kwargs['action'] in ['pre_add', 'pre_remove']:
|
||||
if kwargs['action'] == 'pre_add':
|
||||
action = 'associate'
|
||||
elif kwargs['action'] == 'pre_remove':
|
||||
@ -378,6 +398,23 @@ def activity_stream_associate(sender, instance, **kwargs):
|
||||
getattr(activity_entry, object1).add(obj1)
|
||||
getattr(activity_entry, object2).add(obj2_actual)
|
||||
|
||||
# Record the role for RBAC changes
|
||||
if 'role' in kwargs:
|
||||
role = kwargs['role']
|
||||
if role.content_object is not None:
|
||||
obj_rel = '.'.join([role.content_object.__module__,
|
||||
role.content_object.__class__.__name__,
|
||||
role.role_field])
|
||||
|
||||
# If the m2m is from the User side we need to
|
||||
# set the content_object of the Role for our entry.
|
||||
if type(instance) == User and role.content_object is not None:
|
||||
getattr(activity_entry, role.content_type.name).add(role.content_object)
|
||||
|
||||
activity_entry.role.add(role)
|
||||
activity_entry.object_relationship_type = obj_rel
|
||||
activity_entry.save()
|
||||
|
||||
|
||||
@receiver(current_user_getter)
|
||||
def get_current_user_from_drf_request(sender, **kwargs):
|
||||
|
||||
@ -208,7 +208,7 @@ def handle_work_success(self, result, task_actual):
|
||||
notification_body = instance.notification_data()
|
||||
notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name,
|
||||
task_actual['id'],
|
||||
instance_name,
|
||||
smart_text(instance_name),
|
||||
notification_body['url'])
|
||||
notification_body['friendly_name'] = friendly_name
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
@ -246,8 +246,8 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
instance_name = instance.module_name
|
||||
notifiers = []
|
||||
friendly_name = "AdHoc Command"
|
||||
elif task_actual['type'] == 'system_job':
|
||||
instance = SystemJob.objects.get(id=task_actual['id'])
|
||||
elif each_task['type'] == 'system_job':
|
||||
instance = SystemJob.objects.get(id=each_task['id'])
|
||||
instance_name = instance.system_job_template.name
|
||||
notifiers = instance.system_job_template.notifiers
|
||||
friendly_name = "System Job"
|
||||
@ -270,7 +270,7 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
notification_body = first_task.notification_data()
|
||||
notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name,
|
||||
first_task_id,
|
||||
first_task_name,
|
||||
smart_text(first_task_name),
|
||||
notification_body['url'])
|
||||
notification_body['friendly_name'] = first_task_friendly_name
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
@ -558,7 +558,7 @@ class BaseTask(Task):
|
||||
instance = self.update_model(instance.pk)
|
||||
if instance.cancel_flag:
|
||||
try:
|
||||
if tower_settings.AWX_PROOT_ENABLED:
|
||||
if tower_settings.AWX_PROOT_ENABLED and self.should_use_proot(instance):
|
||||
# NOTE: Refactor this once we get a newer psutil across the board
|
||||
if not psutil:
|
||||
os.kill(child.pid, signal.SIGKILL)
|
||||
@ -1615,6 +1615,9 @@ class RunAdHocCommand(BaseTask):
|
||||
if ad_hoc_command.verbosity:
|
||||
args.append('-%s' % ('v' * min(5, ad_hoc_command.verbosity)))
|
||||
|
||||
if ad_hoc_command.extra_vars_dict:
|
||||
args.extend(['-e', json.dumps(ad_hoc_command.extra_vars_dict)])
|
||||
|
||||
args.extend(['-m', ad_hoc_command.module_name])
|
||||
args.extend(['-a', ad_hoc_command.module_args])
|
||||
|
||||
|
||||
@ -59,3 +59,29 @@ def test_middleware_actor_added(monkeypatch, post, get, user):
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data['summary_fields']['actor']['username'] == 'admin-poster'
|
||||
|
||||
@pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled")
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_rbac_stream_resource_roles(mocker, organization, user):
|
||||
member = user('test', False)
|
||||
organization.admin_role.members.add(member)
|
||||
|
||||
activity_stream = ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first()
|
||||
assert activity_stream.user.first() == member
|
||||
assert activity_stream.organization.first() == organization
|
||||
assert activity_stream.role.first() == organization.admin_role
|
||||
assert activity_stream.object_relationship_type == 'awx.main.models.organization.Organization.admin_role'
|
||||
|
||||
@pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled")
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_rbac_stream_user_roles(mocker, organization, user):
|
||||
member = user('test', False)
|
||||
member.roles.add(organization.admin_role)
|
||||
|
||||
activity_stream = ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first()
|
||||
assert activity_stream.user.first() == member
|
||||
assert activity_stream.organization.first() == organization
|
||||
assert activity_stream.role.first() == organization.admin_role
|
||||
assert activity_stream.object_relationship_type == 'awx.main.models.organization.Organization.admin_role'
|
||||
|
||||
@ -24,6 +24,7 @@ def test_create_user_credential_via_credentials_list(post, get, alice):
|
||||
@pytest.mark.django_db
|
||||
def test_create_user_credential_via_user_credentials_list(post, get, alice):
|
||||
response = post(reverse('api:user_credentials_list', args=(alice.pk,)), {
|
||||
'user': alice.pk,
|
||||
'name': 'Some name',
|
||||
'username': 'someusername',
|
||||
}, alice)
|
||||
@ -45,6 +46,7 @@ def test_create_user_credential_via_credentials_list_xfail(post, alice, bob):
|
||||
@pytest.mark.django_db
|
||||
def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob):
|
||||
response = post(reverse('api:user_credentials_list', args=(bob.pk,)), {
|
||||
'user': bob.pk,
|
||||
'name': 'Some name',
|
||||
'username': 'someusername'
|
||||
}, alice)
|
||||
@ -71,6 +73,7 @@ def test_create_team_credential(post, get, team, org_admin, team_member):
|
||||
@pytest.mark.django_db
|
||||
def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member):
|
||||
response = post(reverse('api:team_credentials_list', args=(team.pk,)), {
|
||||
'team': team.pk,
|
||||
'name': 'Some name',
|
||||
'username': 'someusername',
|
||||
}, org_admin)
|
||||
|
||||
@ -22,6 +22,10 @@ def runtime_data(organization):
|
||||
credential=cred_obj.pk,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def job_with_links(machine_credential, inventory):
|
||||
return Job.objects.create(name='existing-job', credential=machine_credential, inventory=inventory)
|
||||
|
||||
@pytest.fixture
|
||||
def job_template_prompts(project, inventory, machine_credential):
|
||||
def rf(on_off):
|
||||
@ -155,7 +159,7 @@ def test_job_reject_invalid_prompted_extra_vars(runtime_data, job_template_promp
|
||||
dict(extra_vars='{"unbalanced brackets":'), user('admin', True))
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.data['extra_vars'] == ['Must be valid JSON or YAML']
|
||||
assert response.data['extra_vars'] == ['Must be a valid JSON or YAML dictionary']
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.job_runtime_vars
|
||||
@ -167,51 +171,72 @@ def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, user):
|
||||
args=[deploy_jobtemplate.pk]), {}, user('admin', True))
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.data['inventory'] == ['Job Template Inventory is missing or undefined']
|
||||
assert response.data['inventory'] == ['Job Template Inventory is missing or undefined.']
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.job_runtime_vars
|
||||
def test_job_launch_fails_without_inventory_access(job_template_prompts, runtime_data, machine_credential, post, user, mocker):
|
||||
def test_job_launch_fails_without_inventory_access(job_template_prompts, runtime_data, post, user):
|
||||
job_template = job_template_prompts(True)
|
||||
common_user = user('test-user', False)
|
||||
job_template.execute_role.members.add(common_user)
|
||||
|
||||
# Assure that the base job template can be launched to begin with
|
||||
mock_job = mocker.MagicMock(spec=Job, id=968, **runtime_data)
|
||||
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.create_unified_job', return_value=mock_job):
|
||||
with mocker.patch('awx.api.serializers.JobSerializer.to_representation'):
|
||||
response = post(reverse('api:job_template_launch',
|
||||
args=[job_template.pk]), {}, common_user)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
# Assure that giving an inventory without access to the inventory blocks the launch
|
||||
new_inv = job_template.project.organization.inventories.create(name="user-can-not-use")
|
||||
runtime_inventory = Inventory.objects.get(pk=runtime_data['inventory'])
|
||||
response = post(reverse('api:job_template_launch', args=[job_template.pk]),
|
||||
dict(inventory=new_inv.pk), common_user)
|
||||
dict(inventory=runtime_inventory.pk), common_user)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.data['detail'] == u'You do not have permission to perform this action.'
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.job_runtime_vars
|
||||
def test_job_relaunch_copy_vars(runtime_data, job_template_prompts, project, post, mocker):
|
||||
def test_job_launch_fails_without_credential_access(job_template_prompts, runtime_data, post, user):
|
||||
job_template = job_template_prompts(True)
|
||||
common_user = user('test-user', False)
|
||||
job_template.execute_role.members.add(common_user)
|
||||
|
||||
# Create a job with the given data that will be relaunched
|
||||
job_create_kwargs = runtime_data
|
||||
inv_obj = Inventory.objects.get(pk=job_create_kwargs.pop('inventory'))
|
||||
cred_obj = Credential.objects.get(pk=job_create_kwargs.pop('credential'))
|
||||
original_job = Job.objects.create(inventory=inv_obj, credential=cred_obj, job_template=job_template, **job_create_kwargs)
|
||||
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate._get_unified_job_field_names', return_value=runtime_data.keys()):
|
||||
second_job = original_job.copy()
|
||||
# Assure that giving a credential without access blocks the launch
|
||||
runtime_credential = Credential.objects.get(pk=runtime_data['credential'])
|
||||
response = post(reverse('api:job_template_launch', args=[job_template.pk]),
|
||||
dict(credential=runtime_credential.pk), common_user)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.data['detail'] == u'You do not have permission to perform this action.'
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.job_runtime_vars
|
||||
def test_job_relaunch_copy_vars(job_with_links, machine_credential, inventory,
|
||||
deploy_jobtemplate, post, mocker):
|
||||
job_with_links.job_template = deploy_jobtemplate
|
||||
job_with_links.limit = "my_server"
|
||||
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate._get_unified_job_field_names',
|
||||
return_value=['inventory', 'credential', 'limit']):
|
||||
second_job = job_with_links.copy()
|
||||
|
||||
# Check that job data matches the original variables
|
||||
assert 'job_launch_var' in yaml.load(second_job.extra_vars)
|
||||
assert original_job.limit == second_job.limit
|
||||
assert original_job.job_type == second_job.job_type
|
||||
assert original_job.inventory.pk == second_job.inventory.pk
|
||||
assert original_job.job_tags == second_job.job_tags
|
||||
assert second_job.credential == job_with_links.credential
|
||||
assert second_job.inventory == job_with_links.inventory
|
||||
assert second_job.limit == 'my_server'
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.job_runtime_vars
|
||||
def test_job_relaunch_resource_access(job_with_links, user):
|
||||
inventory_user = user('user1', False)
|
||||
credential_user = user('user2', False)
|
||||
both_user = user('user3', False)
|
||||
|
||||
# Confirm that a user with inventory & credential access can launch
|
||||
job_with_links.credential.use_role.members.add(both_user)
|
||||
job_with_links.inventory.use_role.members.add(both_user)
|
||||
assert both_user.can_access(Job, 'start', job_with_links)
|
||||
|
||||
# Confirm that a user with credential access alone can not launch
|
||||
job_with_links.credential.use_role.members.add(credential_user)
|
||||
assert not credential_user.can_access(Job, 'start', job_with_links)
|
||||
|
||||
# Confirm that a user with inventory access alone can not launch
|
||||
job_with_links.inventory.use_role.members.add(inventory_user)
|
||||
assert not inventory_user.can_access(Job, 'start', job_with_links)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
|
||||
|
||||
@ -150,6 +150,26 @@ def test_two_organizations(resourced_organization, organizations, user, get):
|
||||
assert counts[org_id_full] == COUNTS_PRIMES
|
||||
assert counts[org_id_zero] == COUNTS_ZEROS
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scan_JT_counted(resourced_organization, user, get):
|
||||
admin_user = user('admin', True)
|
||||
# Add a scan job template to the org
|
||||
resourced_organization.projects.all()[0].jobtemplates.create(
|
||||
job_type='scan', inventory=resourced_organization.inventories.all()[0],
|
||||
name='scan-job-template')
|
||||
counts_dict = COUNTS_PRIMES
|
||||
counts_dict['job_templates'] += 1
|
||||
|
||||
# Test list view
|
||||
list_response = get(reverse('api:organization_list', args=[]), admin_user)
|
||||
assert list_response.status_code == 200
|
||||
assert list_response.data['results'][0]['summary_fields']['related_field_counts'] == counts_dict
|
||||
|
||||
# Test detail view
|
||||
detail_response = get(reverse('api:organization_detail', args=[resourced_organization.pk]), admin_user)
|
||||
assert detail_response.status_code == 200
|
||||
assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_JT_associated_with_project(organizations, project, user, get):
|
||||
# Check that adding a project to an organization gets the project's JT
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from awx.main.models import Role
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_indirect_access_list(get, organization, project, team_factory, user, admin):
|
||||
@ -53,5 +54,5 @@ def test_indirect_access_list(get, organization, project, team_factory, user, ad
|
||||
assert org_admin_team_member_entry['team_name'] == org_admin_team.name
|
||||
|
||||
admin_entry = admin_res['summary_fields']['indirect_access'][0]['role']
|
||||
assert admin_entry['name'] == 'System Administrator'
|
||||
assert admin_entry['name'] == Role.singleton('system_administrator').name
|
||||
|
||||
|
||||
@ -36,7 +36,6 @@ from awx.main.models.organization import (
|
||||
Team,
|
||||
)
|
||||
|
||||
from awx.main.models.rbac import Role
|
||||
from awx.main.models.notifications import Notifier
|
||||
|
||||
'''
|
||||
@ -193,11 +192,6 @@ def notifier(organization):
|
||||
notification_type="webhook",
|
||||
notification_configuration=dict(url="http://localhost",
|
||||
headers={"Test": "Header"}))
|
||||
|
||||
@pytest.fixture
|
||||
def role():
|
||||
return Role.objects.create(name='role')
|
||||
|
||||
@pytest.fixture
|
||||
def admin(user):
|
||||
return user('admin', True)
|
||||
|
||||
@ -10,6 +10,10 @@ def mock_feature_enabled(feature, bypass_database=None):
|
||||
|
||||
#@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
|
||||
@pytest.fixture
|
||||
def role():
|
||||
return Role.objects.create()
|
||||
|
||||
|
||||
#
|
||||
# /roles
|
||||
@ -85,13 +89,14 @@ def test_get_user_roles_list(get, admin):
|
||||
response = get(url, admin)
|
||||
assert response.status_code == 200
|
||||
roles = response.data
|
||||
assert roles['count'] > 0 # 'System Administrator' role if nothing else
|
||||
assert roles['count'] > 0 # 'system_administrator' role if nothing else
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob):
|
||||
'Users can see roles for other users, but only the roles that that user has access to see as well'
|
||||
organization.member_role.members.add(alice)
|
||||
organization.admin_role.members.add(bob)
|
||||
organization.member_role.members.add(bob)
|
||||
custom_role = Role.objects.create(name='custom_role-test_user_view_admin_roles_list')
|
||||
organization.member_role.children.add(custom_role)
|
||||
team.member_role.members.add(bob)
|
||||
|
||||
@ -11,8 +11,8 @@ from awx.main.models import (
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_inheritance_by_children(organization, alice):
|
||||
A = Role.objects.create(name='A', role_field='')
|
||||
B = Role.objects.create(name='B', role_field='')
|
||||
A = Role.objects.create()
|
||||
B = Role.objects.create()
|
||||
A.members.add(alice)
|
||||
|
||||
assert alice not in organization.admin_role
|
||||
@ -38,8 +38,8 @@ def test_auto_inheritance_by_children(organization, alice):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_inheritance_by_parents(organization, alice):
|
||||
A = Role.objects.create(name='A')
|
||||
B = Role.objects.create(name='B')
|
||||
A = Role.objects.create()
|
||||
B = Role.objects.create()
|
||||
A.members.add(alice)
|
||||
|
||||
assert alice not in organization.admin_role
|
||||
@ -58,9 +58,9 @@ def test_auto_inheritance_by_parents(organization, alice):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_accessible_objects(organization, alice, bob):
|
||||
A = Role.objects.create(name='A')
|
||||
A = Role.objects.create()
|
||||
A.members.add(alice)
|
||||
B = Role.objects.create(name='B')
|
||||
B = Role.objects.create()
|
||||
B.members.add(alice)
|
||||
B.members.add(bob)
|
||||
|
||||
@ -118,7 +118,7 @@ def test_auto_field_adjustments(organization, inventory, team, alice):
|
||||
def test_implicit_deletes(alice):
|
||||
'Ensures implicit resources and roles delete themselves'
|
||||
delorg = Organization.objects.create(name='test-org')
|
||||
child = Role.objects.create(name='child-role')
|
||||
child = Role.objects.create()
|
||||
child.parents.add(delorg.admin_role)
|
||||
delorg.admin_role.members.add(alice)
|
||||
|
||||
@ -129,14 +129,14 @@ def test_implicit_deletes(alice):
|
||||
assert Role.objects.filter(id=admin_role_id).count() == 1
|
||||
assert Role.objects.filter(id=auditor_role_id).count() == 1
|
||||
n_alice_roles = alice.roles.count()
|
||||
n_system_admin_children = Role.singleton('System Administrator').children.count()
|
||||
n_system_admin_children = Role.singleton('system_administrator').children.count()
|
||||
|
||||
delorg.delete()
|
||||
|
||||
assert Role.objects.filter(id=admin_role_id).count() == 0
|
||||
assert Role.objects.filter(id=auditor_role_id).count() == 0
|
||||
assert alice.roles.count() == (n_alice_roles - 1)
|
||||
assert Role.singleton('System Administrator').children.count() == (n_system_admin_children - 1)
|
||||
assert Role.singleton('system_administrator').children.count() == (n_system_admin_children - 1)
|
||||
assert child.ancestors.count() == 1
|
||||
assert child.ancestors.all()[0] == child
|
||||
|
||||
@ -152,11 +152,11 @@ def test_content_object(user):
|
||||
def test_hierarchy_rebuilding_multi_path():
|
||||
'Tests a subdtle cases around role hierarchy rebuilding when you have multiple paths to the same role of different length'
|
||||
|
||||
X = Role.objects.create(name='X')
|
||||
A = Role.objects.create(name='A')
|
||||
B = Role.objects.create(name='B')
|
||||
C = Role.objects.create(name='C')
|
||||
D = Role.objects.create(name='D')
|
||||
X = Role.objects.create()
|
||||
A = Role.objects.create()
|
||||
B = Role.objects.create()
|
||||
C = Role.objects.create()
|
||||
D = Role.objects.create()
|
||||
|
||||
A.children.add(B)
|
||||
A.children.add(D)
|
||||
|
||||
@ -27,7 +27,7 @@ def test_credential_use_role(credential, user, permissions):
|
||||
@pytest.mark.django_db
|
||||
def test_credential_migration_team_member(credential, team, user, permissions):
|
||||
u = user('user', False)
|
||||
team.admin_role.members.add(u)
|
||||
team.member_role.members.add(u)
|
||||
credential.deprecated_team = team
|
||||
credential.save()
|
||||
|
||||
@ -91,7 +91,8 @@ def test_credential_access_admin(user, team, credential):
|
||||
assert access.can_change(credential, {'user': u.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_job_template(user, deploy_jobtemplate):
|
||||
def test_cred_job_template_xfail(user, deploy_jobtemplate):
|
||||
' Personal credential migration '
|
||||
a = user('admin', False)
|
||||
org = deploy_jobtemplate.project.organization
|
||||
org.admin_role.members.add(a)
|
||||
@ -102,19 +103,17 @@ def test_cred_job_template(user, deploy_jobtemplate):
|
||||
|
||||
access = CredentialAccess(a)
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
org.admin_role.members.remove(a)
|
||||
assert not access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_multi_job_template_single_org(user, deploy_jobtemplate):
|
||||
def test_cred_job_template(user, team, deploy_jobtemplate):
|
||||
' Team credential migration => org credential '
|
||||
a = user('admin', False)
|
||||
org = deploy_jobtemplate.project.organization
|
||||
org.admin_role.members.add(a)
|
||||
|
||||
cred = deploy_jobtemplate.credential
|
||||
cred.deprecated_user = user('john', False)
|
||||
cred.deprecated_team = team
|
||||
cred.save()
|
||||
|
||||
access = CredentialAccess(a)
|
||||
@ -125,8 +124,42 @@ def test_cred_multi_job_template_single_org(user, deploy_jobtemplate):
|
||||
assert not access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_single_cred_multi_job_template_multi_org(user, organizations, credential):
|
||||
def test_cred_multi_job_template_single_org_xfail(user, deploy_jobtemplate):
|
||||
a = user('admin', False)
|
||||
org = deploy_jobtemplate.project.organization
|
||||
org.admin_role.members.add(a)
|
||||
|
||||
cred = deploy_jobtemplate.credential
|
||||
cred.deprecated_user = user('john', False)
|
||||
cred.save()
|
||||
|
||||
access = CredentialAccess(a)
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert not access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_multi_job_template_single_org(user, team, deploy_jobtemplate):
|
||||
a = user('admin', False)
|
||||
org = deploy_jobtemplate.project.organization
|
||||
org.admin_role.members.add(a)
|
||||
|
||||
cred = deploy_jobtemplate.credential
|
||||
cred.deprecated_team = team
|
||||
cred.save()
|
||||
|
||||
access = CredentialAccess(a)
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
org.admin_role.members.remove(a)
|
||||
assert not access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_single_cred_multi_job_template_multi_org(user, organizations, credential, team):
|
||||
orgs = organizations(2)
|
||||
credential.deprecated_team = team
|
||||
credential.save()
|
||||
|
||||
jts = []
|
||||
for org in orgs:
|
||||
inv = org.inventories.create(name="inv-%d" % org.pk)
|
||||
@ -169,7 +202,7 @@ def test_cred_inventory_source(user, inventory, credential):
|
||||
assert u not in credential.use_role
|
||||
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert u in credential.use_role
|
||||
assert u not in credential.use_role
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_project(user, credential, project):
|
||||
@ -181,7 +214,7 @@ def test_cred_project(user, credential, project):
|
||||
assert u not in credential.use_role
|
||||
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert u in credential.use_role
|
||||
assert u not in credential.use_role
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_no_org(user, credential):
|
||||
|
||||
@ -87,7 +87,7 @@ def test_job_template_team_migration_check(deploy_jobtemplate, check_jobtemplate
|
||||
rbac.migrate_projects(apps, None)
|
||||
rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert joe in check_jobtemplate.read_role
|
||||
assert joe not in check_jobtemplate.read_role
|
||||
assert admin in check_jobtemplate.execute_role
|
||||
assert joe not in check_jobtemplate.execute_role
|
||||
|
||||
@ -120,12 +120,13 @@ def test_job_template_team_deploy_migration(deploy_jobtemplate, check_jobtemplat
|
||||
rbac.migrate_projects(apps, None)
|
||||
rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert joe in deploy_jobtemplate.read_role
|
||||
assert joe not in deploy_jobtemplate.read_role
|
||||
assert admin in deploy_jobtemplate.execute_role
|
||||
assert joe not in deploy_jobtemplate.execute_role
|
||||
|
||||
rbac.migrate_job_templates(apps, None)
|
||||
|
||||
assert joe in deploy_jobtemplate.read_role
|
||||
assert admin in deploy_jobtemplate.execute_role
|
||||
assert joe in deploy_jobtemplate.execute_role
|
||||
|
||||
|
||||
@ -148,7 +148,7 @@ def test_project_user_project(user_project, project, user):
|
||||
def test_project_accessible_by_sa(user, project):
|
||||
u = user('systemadmin', is_superuser=True)
|
||||
# This gets setup by a signal, but we want to test the migration which will set this up too, so remove it
|
||||
Role.singleton('System Administrator').members.remove(u)
|
||||
Role.singleton('system_administrator').members.remove(u)
|
||||
|
||||
assert u not in project.read_role
|
||||
rbac.migrate_organization(apps, None)
|
||||
|
||||
@ -3,6 +3,25 @@ import pytest
|
||||
from awx.main.access import TeamAccess
|
||||
from awx.main.models import Project
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_team_attach_unattach(team, user):
|
||||
u = user('member', False)
|
||||
access = TeamAccess(u)
|
||||
|
||||
team.member_role.members.add(u)
|
||||
assert not access.can_attach(team, u.admin_role, 'member_role.children', None)
|
||||
assert not access.can_unattach(team, u.admin_role, 'member_role.children')
|
||||
|
||||
team.admin_role.members.add(u)
|
||||
assert access.can_attach(team, u.admin_role, 'member_role.children', None)
|
||||
assert access.can_unattach(team, u.admin_role, 'member_role.children')
|
||||
|
||||
u2 = user('non-member', False)
|
||||
access = TeamAccess(u2)
|
||||
assert not access.can_attach(team, u2.admin_role, 'member_role.children', None)
|
||||
assert not access.can_unattach(team, u2.admin_role, 'member_role.chidlren')
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_team_access_superuser(team, user):
|
||||
team.member_role.members.add(user('member', False))
|
||||
|
||||
@ -13,7 +13,7 @@ def test_user_admin(user_project, project, user):
|
||||
|
||||
joe = user(username, is_superuser = False)
|
||||
admin = user('admin', is_superuser = True)
|
||||
sa = Role.singleton('System Administrator')
|
||||
sa = Role.singleton('system_administrator')
|
||||
|
||||
# this should happen automatically with our signal
|
||||
assert sa.members.filter(id=admin.id).exists() is True
|
||||
|
||||
17
awx/main/tests/unit/api/test_filters.py
Normal file
17
awx/main/tests/unit/api/test_filters.py
Normal 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
|
||||
@ -1,13 +1,14 @@
|
||||
|
||||
# Python
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
# DRF
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# AWX
|
||||
from awx.api.generics import ParentMixin, SubListCreateAttachDetachAPIView
|
||||
from awx.api.generics import ParentMixin, SubListCreateAttachDetachAPIView, DeleteLastUnattachLabelMixin
|
||||
|
||||
@pytest.fixture
|
||||
def get_object_or_404(mocker):
|
||||
@ -37,7 +38,7 @@ def parent_relationship_factory(mocker):
|
||||
return (serializer, mock_parent_relationship)
|
||||
return rf
|
||||
|
||||
# TODO: Test create and associate failure (i.e. id doesn't exist or record already exists)
|
||||
# TODO: Test create and associate failure (i.e. id doesn't exist, record already exists, permission denied)
|
||||
# TODO: Mock and check return (Response)
|
||||
class TestSubListCreateAttachDetachAPIView:
|
||||
def test_attach_create_and_associate(self, mocker, get_object_or_400, parent_relationship_factory, mock_response_new):
|
||||
@ -65,6 +66,97 @@ class TestSubListCreateAttachDetachAPIView:
|
||||
mock_parent_relationship.wife.add.assert_called_with(get_object_or_400.return_value)
|
||||
mock_response_new.assert_called_with(Response, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def test_unattach_validate_ok(self, mocker):
|
||||
mock_request = mocker.MagicMock(data=dict(id=1))
|
||||
serializer = SubListCreateAttachDetachAPIView()
|
||||
|
||||
(sub_id, res) = serializer.unattach_validate(mock_request)
|
||||
|
||||
assert sub_id == 1
|
||||
assert res is None
|
||||
|
||||
def test_unattach_validate_missing_id(self, mocker):
|
||||
mock_request = mocker.MagicMock(data=dict())
|
||||
serializer = SubListCreateAttachDetachAPIView()
|
||||
|
||||
(sub_id, res) = serializer.unattach_validate(mock_request)
|
||||
|
||||
assert sub_id is None
|
||||
assert type(res) is Response
|
||||
|
||||
def test_unattach_by_id_ok(self, mocker, parent_relationship_factory, get_object_or_400):
|
||||
(serializer, mock_parent_relationship) = parent_relationship_factory(SubListCreateAttachDetachAPIView, 'wife')
|
||||
mock_request = mocker.MagicMock()
|
||||
mock_sub = mocker.MagicMock(name="object to unattach")
|
||||
get_object_or_400.return_value = mock_sub
|
||||
|
||||
res = serializer.unattach_by_id(mock_request, 1)
|
||||
|
||||
assert type(res) is Response
|
||||
assert res.status_code == status.HTTP_204_NO_CONTENT
|
||||
mock_parent_relationship.wife.remove.assert_called_with(mock_sub)
|
||||
|
||||
def test_unattach_ok(self, mocker):
|
||||
mock_request = mocker.MagicMock()
|
||||
mock_sub_id = mocker.MagicMock()
|
||||
view = SubListCreateAttachDetachAPIView()
|
||||
view.unattach_validate = mocker.MagicMock()
|
||||
view.unattach_by_id = mocker.MagicMock()
|
||||
view.unattach_validate.return_value = (mock_sub_id, None)
|
||||
|
||||
view.unattach(mock_request)
|
||||
|
||||
view.unattach_validate.assert_called_with(mock_request)
|
||||
view.unattach_by_id.assert_called_with(mock_request, mock_sub_id)
|
||||
|
||||
def test_unattach_invalid(self, mocker):
|
||||
mock_request = mocker.MagicMock()
|
||||
mock_res = mocker.MagicMock()
|
||||
view = SubListCreateAttachDetachAPIView()
|
||||
view.unattach_validate = mocker.MagicMock()
|
||||
view.unattach_by_id = mocker.MagicMock()
|
||||
view.unattach_validate.return_value = (None, mock_res)
|
||||
|
||||
view.unattach(mock_request)
|
||||
|
||||
view.unattach_validate.assert_called_with(mock_request)
|
||||
view.unattach_by_id.assert_not_called()
|
||||
|
||||
class TestDeleteLastUnattachLabelMixin:
|
||||
@mock.patch('awx.api.generics.super')
|
||||
def test_unattach_ok(self, super, mocker):
|
||||
mock_request = mocker.MagicMock()
|
||||
mock_sub_id = mocker.MagicMock()
|
||||
super.return_value = super
|
||||
super.unattach_validate = mocker.MagicMock(return_value=(mock_sub_id, None))
|
||||
super.unattach_by_id = mocker.MagicMock()
|
||||
mock_label = mocker.patch('awx.api.generics.Label')
|
||||
mock_label.objects.get.return_value = mock_label
|
||||
mock_label.is_detached.return_value = True
|
||||
|
||||
view = DeleteLastUnattachLabelMixin()
|
||||
|
||||
view.unattach(mock_request, None, None)
|
||||
|
||||
super.unattach_validate.assert_called_with(mock_request, None, None)
|
||||
super.unattach_by_id.assert_called_with(mock_request, mock_sub_id)
|
||||
mock_label.is_detached.assert_called_with()
|
||||
mock_label.objects.get.assert_called_with(id=mock_sub_id)
|
||||
mock_label.delete.assert_called_with()
|
||||
|
||||
@mock.patch('awx.api.generics.super')
|
||||
def test_unattach_fail(self, super, mocker):
|
||||
mock_request = mocker.MagicMock()
|
||||
mock_response = mocker.MagicMock()
|
||||
super.return_value = super
|
||||
super.unattach_validate = mocker.MagicMock(return_value=(None, mock_response))
|
||||
view = DeleteLastUnattachLabelMixin()
|
||||
|
||||
res = view.unattach(mock_request, None, None)
|
||||
|
||||
super.unattach_validate.assert_called_with(mock_request, None, None)
|
||||
assert mock_response == res
|
||||
|
||||
class TestParentMixin:
|
||||
def test_get_parent_object(self, mocker, get_object_or_404):
|
||||
parent_mixin = ParentMixin()
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
# Python
|
||||
import pytest
|
||||
|
||||
# AWX
|
||||
from awx.api.views import ApiV1RootView
|
||||
from awx.api.views import (
|
||||
ApiV1RootView,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_response_new(mocker):
|
||||
@ -10,6 +11,7 @@ def mock_response_new(mocker):
|
||||
m.return_value = m
|
||||
return m
|
||||
|
||||
|
||||
class TestApiV1RootView:
|
||||
def test_get_endpoints(self, mocker, mock_response_new):
|
||||
endpoints = [
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.models.label import Label
|
||||
from awx.main.models.unified_jobs import UnifiedJobTemplate, UnifiedJob
|
||||
|
||||
|
||||
def test_get_orphaned_labels(mocker):
|
||||
mock_query_set = mocker.MagicMock()
|
||||
@ -8,3 +12,54 @@ def test_get_orphaned_labels(mocker):
|
||||
|
||||
assert mock_query_set == ret
|
||||
Label.objects.filter.assert_called_with(organization=None, jobtemplate_labels__isnull=True)
|
||||
|
||||
def test_is_detached(mocker):
|
||||
mock_query_set = mocker.MagicMock()
|
||||
Label.objects.filter = mocker.MagicMock(return_value=mock_query_set)
|
||||
mock_query_set.count.return_value = 1
|
||||
|
||||
label = Label(id=37)
|
||||
ret = label.is_detached()
|
||||
|
||||
assert ret is True
|
||||
Label.objects.filter.assert_called_with(id=37, unifiedjob_labels__isnull=True, unifiedjobtemplate_labels__isnull=True)
|
||||
mock_query_set.count.assert_called_with()
|
||||
|
||||
def test_is_detached_not(mocker):
|
||||
mock_query_set = mocker.MagicMock()
|
||||
Label.objects.filter = mocker.MagicMock(return_value=mock_query_set)
|
||||
mock_query_set.count.return_value = 0
|
||||
|
||||
label = Label(id=37)
|
||||
ret = label.is_detached()
|
||||
|
||||
assert ret is False
|
||||
Label.objects.filter.assert_called_with(id=37, unifiedjob_labels__isnull=True, unifiedjobtemplate_labels__isnull=True)
|
||||
mock_query_set.count.assert_called_with()
|
||||
|
||||
@pytest.mark.parametrize("jt_count,j_count,expected", [
|
||||
(1, 0, True),
|
||||
(0, 1, True),
|
||||
(1, 1, False),
|
||||
])
|
||||
def test_is_candidate_for_detach(mocker, jt_count, j_count, expected):
|
||||
mock_job_qs = mocker.MagicMock()
|
||||
mock_job_qs.count = mocker.MagicMock(return_value=j_count)
|
||||
UnifiedJob.objects = mocker.MagicMock()
|
||||
UnifiedJob.objects.filter = mocker.MagicMock(return_value=mock_job_qs)
|
||||
|
||||
mock_jt_qs = mocker.MagicMock()
|
||||
mock_jt_qs.count = mocker.MagicMock(return_value=jt_count)
|
||||
UnifiedJobTemplate.objects = mocker.MagicMock()
|
||||
UnifiedJobTemplate.objects.filter = mocker.MagicMock(return_value=mock_jt_qs)
|
||||
|
||||
label = Label(id=37)
|
||||
ret = label.is_candidate_for_detach()
|
||||
|
||||
UnifiedJob.objects.filter.assert_called_with(labels__in=[label.id])
|
||||
UnifiedJobTemplate.objects.filter.assert_called_with(labels__in=[label.id])
|
||||
mock_job_qs.count.assert_called_with()
|
||||
mock_jt_qs.count.assert_called_with()
|
||||
|
||||
assert ret is expected
|
||||
|
||||
|
||||
21
awx/main/tests/unit/test_access.py
Normal file
21
awx/main/tests/unit/test_access.py
Normal 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
|
||||
|
||||
17
awx/main/tests/unit/test_signals.py
Normal file
17
awx/main/tests/unit/test_signals.py
Normal 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()
|
||||
@ -70,7 +70,7 @@ import psutil
|
||||
# pass
|
||||
# statsd = NoStatsClient()
|
||||
|
||||
CENSOR_FIELD_WHITELIST=[
|
||||
CENSOR_FIELD_WHITELIST = [
|
||||
'msg',
|
||||
'failed',
|
||||
'changed',
|
||||
@ -80,7 +80,6 @@ CENSOR_FIELD_WHITELIST=[
|
||||
'delta',
|
||||
'cmd',
|
||||
'_ansible_no_log',
|
||||
'cmd',
|
||||
'rc',
|
||||
'failed_when_result',
|
||||
'skipped',
|
||||
@ -114,6 +113,7 @@ def censor(obj, no_log=False):
|
||||
obj['results'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
|
||||
return obj
|
||||
|
||||
|
||||
class TokenAuth(requests.auth.AuthBase):
|
||||
|
||||
def __init__(self, token):
|
||||
@ -194,31 +194,7 @@ class BaseCallbackModule(object):
|
||||
self._init_connection()
|
||||
if self.context is None:
|
||||
self._start_connection()
|
||||
if 'res' in event_data and hasattr(event_data['res'], 'get') \
|
||||
and event_data['res'].get('_ansible_no_log', False):
|
||||
res = event_data['res']
|
||||
if 'stdout' in res and res['stdout']:
|
||||
res['stdout'] = '<censored>'
|
||||
if 'stdout_lines' in res and res['stdout_lines']:
|
||||
res['stdout_lines'] = ['<censored>']
|
||||
if 'stderr' in res and res['stderr']:
|
||||
res['stderr'] = '<censored>'
|
||||
if 'stderr_lines' in res and res['stderr_lines']:
|
||||
res['stderr_lines'] = ['<censored>']
|
||||
if res.get('cmd', None) and re.search(r'\s', res['cmd']):
|
||||
res['cmd'] = re.sub(r'^(([^\s\\]|\\\s)+).*$',
|
||||
r'\1 <censored>',
|
||||
res['cmd'])
|
||||
if 'invocation' in res \
|
||||
and 'module_args' in res['invocation'] \
|
||||
and '_raw_params' in res['invocation']['module_args'] \
|
||||
and re.search(r'\s',
|
||||
res['invocation']['module_args']['_raw_params']):
|
||||
res['invocation']['module_args']['_raw_params'] = \
|
||||
re.sub(r'^(([^\s\\]|\\\s)+).*$',
|
||||
r'\1 <censored>',
|
||||
res['invocation']['module_args']['_raw_params'])
|
||||
msg['event_data']['res'] = res
|
||||
|
||||
self.socket.send_json(msg)
|
||||
self.socket.recv()
|
||||
return
|
||||
|
||||
@ -32,7 +32,6 @@
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from ansible import constants as C
|
||||
try:
|
||||
from ansible.cache.base import BaseCacheModule
|
||||
@ -50,7 +49,7 @@ class CacheModule(BaseCacheModule):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Basic in-memory caching for typical runs
|
||||
self._cache = {}
|
||||
self._cache_prev = {}
|
||||
self._all_keys = {}
|
||||
|
||||
# This is the local tower zmq connection
|
||||
self._tower_connection = C.CACHE_PLUGIN_CONNECTION
|
||||
@ -72,12 +71,10 @@ class CacheModule(BaseCacheModule):
|
||||
def identify_new_module(self, key, value):
|
||||
# Return the first key found that doesn't exist in the
|
||||
# previous set of facts
|
||||
if key in self._cache_prev:
|
||||
value_old = self._cache_prev[key]
|
||||
for k,v in value.iteritems():
|
||||
if k not in value_old:
|
||||
if not k.startswith('ansible_'):
|
||||
return k
|
||||
if key in self._all_keys:
|
||||
for k in value.iterkeys():
|
||||
if k not in self._all_keys[key] and not k.startswith('ansible_'):
|
||||
return k
|
||||
# First time we have seen facts from this host
|
||||
# it's either ansible facts or a module facts (including module_setup)
|
||||
elif len(value) == 1:
|
||||
@ -110,7 +107,7 @@ class CacheModule(BaseCacheModule):
|
||||
# Assume ansible fact triggered the set if no new module found
|
||||
facts = self.filter_ansible_facts(value) if not module else dict({ module : value[module]})
|
||||
self._cache[key] = value
|
||||
self._cache_prev = deepcopy(self._cache)
|
||||
self._all_keys[key] = value.keys()
|
||||
packet = {
|
||||
'host': key,
|
||||
'inventory_id': os.environ['INVENTORY_ID'],
|
||||
|
||||
@ -59,8 +59,8 @@ class ServiceScanService(BaseService):
|
||||
initctl_path = self.module.get_bin_path("initctl")
|
||||
chkconfig_path = self.module.get_bin_path("chkconfig")
|
||||
|
||||
# Upstart and sysvinit
|
||||
if initctl_path is not None and chkconfig_path is None:
|
||||
# sysvinit
|
||||
if service_path is not None and chkconfig_path is None:
|
||||
rc, stdout, stderr = self.module.run_command("%s --status-all 2>&1 | grep -E \"\\[ (\\+|\\-) \\]\"" % service_path, use_unsafe_shell=True)
|
||||
for line in stdout.split("\n"):
|
||||
line_data = line.split()
|
||||
@ -72,6 +72,9 @@ class ServiceScanService(BaseService):
|
||||
else:
|
||||
service_state = "stopped"
|
||||
services.append({"name": service_name, "state": service_state, "source": "sysv"})
|
||||
|
||||
# Upstart
|
||||
if initctl_path is not None and chkconfig_path is None:
|
||||
p = re.compile('^\s?(?P<name>.*)\s(?P<goal>\w+)\/(?P<state>\w+)(\,\sprocess\s(?P<pid>[0-9]+))?\s*$')
|
||||
rc, stdout, stderr = self.module.run_command("%s list" % initctl_path)
|
||||
real_stdout = stdout.replace("\r","")
|
||||
|
||||
@ -47,8 +47,9 @@ body .navbar .navbar-brand:hover {
|
||||
}
|
||||
body .navbar .navbar-brand img {
|
||||
display: inline-block;
|
||||
max-width: 150px;
|
||||
max-height: 50px;
|
||||
width: 93px;
|
||||
height: 30px;
|
||||
margin-right:14px;
|
||||
}
|
||||
body .navbar .navbar-brand > span {
|
||||
display: inline-block;
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{% url 'api:api_root_view' %}">
|
||||
<img class="logo" src="{% static 'assets/main_menu_logo.png' %}">
|
||||
<img class="logo" src="{% static 'assets/tower-logo-header.svg' %}">
|
||||
<span>{% trans 'REST API' %}</span>
|
||||
</a>
|
||||
<a class="navbar-title" href="{{ request.get_full_path }}">
|
||||
@ -49,10 +49,7 @@
|
||||
<div id="footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 footer-logo">
|
||||
<a href="http://www.ansible.com" target="_blank">
|
||||
<img alt="Red Hat, Inc. | Ansible, Inc." src="{% static 'assets/footer-logo.png' %}" />
|
||||
</a>
|
||||
<div class="col-sm-6">
|
||||
</div>
|
||||
<div class="col-sm-6 footer-copyright">
|
||||
Copyright © 2016 <a href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc. All Rights Reserved.
|
||||
|
||||
1
awx/ui/client/assets/tower-logo-header.svg
Normal file
1
awx/ui/client/assets/tower-logo-header.svg
Normal 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 |
1
awx/ui/client/assets/tower-logo-login.svg
Normal file
1
awx/ui/client/assets/tower-logo-login.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.6 KiB |
@ -896,10 +896,6 @@ select.field-mini-height {
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
.ask-checkbox {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
@ -15,12 +15,25 @@
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.Form-textArea{
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.Form-header--fields{
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.Form-header-field{
|
||||
margin-left: 10px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.Form-header{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Form-title{
|
||||
flex: 1 0 auto;
|
||||
flex: 0 1 auto;
|
||||
text-transform: uppercase;
|
||||
color: @list-header-txt;
|
||||
font-size: 14px;
|
||||
@ -144,7 +157,7 @@
|
||||
flex: 1 0 auto;
|
||||
margin-bottom: 20px;
|
||||
width: 33%;
|
||||
padding-right: 50px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.Form-subForm {
|
||||
@ -455,6 +468,12 @@ input[type='radio']:checked:before {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.Form-subCheckbox {
|
||||
margin-top: 5px;
|
||||
font-size: small;
|
||||
color: @default-interface-txt;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 650px) {
|
||||
.Form-formGroup {
|
||||
flex: 1 0 auto;
|
||||
|
||||
@ -166,6 +166,10 @@ table, tbody {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.List-titleBadge--selected {
|
||||
background-color: @default-link;
|
||||
}
|
||||
|
||||
.List-titleText {
|
||||
color: @list-title-txt;
|
||||
font-size: 14px;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
@import "awx/ui/client/src/shared/branding/colors.default.less";
|
||||
|
||||
.About-cowsay--container{
|
||||
width: 340px;
|
||||
width: 340px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.About-cowsay--code{
|
||||
@ -23,10 +23,8 @@
|
||||
padding-top: 0px;
|
||||
}
|
||||
.About-brand--redhat{
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
margin-top: -50px;
|
||||
margin-bottom: -30px;
|
||||
float: left;
|
||||
width: 112px;
|
||||
}
|
||||
.About-brand--ansible{
|
||||
max-width: 120px;
|
||||
@ -36,7 +34,14 @@
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
.About p{
|
||||
color: @default-interface-txt;
|
||||
}
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.About-modal--footer {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ export default
|
||||
var processVersion = function(version){
|
||||
// prettify version & calculate padding
|
||||
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
|
||||
var split = version.split('-')[0]
|
||||
var split = version.split('-')[0];
|
||||
var spaces = Math.floor((16-split.length)/2),
|
||||
paddedStr = "";
|
||||
for(var i=0; i<=spaces; i++){
|
||||
@ -13,7 +13,7 @@ export default
|
||||
for(var j = paddedStr.length; j<16; j++){
|
||||
paddedStr = paddedStr + " ";
|
||||
}
|
||||
return paddedStr
|
||||
return paddedStr;
|
||||
};
|
||||
var init = function(){
|
||||
CheckLicense.get()
|
||||
@ -28,4 +28,4 @@ export default
|
||||
});
|
||||
init();
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img class="About-brand--ansible img-responsive" src="/static/assets/ansible_tower_logo_minimalc.png" />
|
||||
<button data-dismiss="modal" type="button" class="close About-close">
|
||||
<span class="fa fa-times-circle"></span>
|
||||
</button>
|
||||
@ -22,11 +21,11 @@
|
||||
|| ||
|
||||
</pre>
|
||||
</div>
|
||||
<img class="About-brand--redhat img-responsive" src="/static/assets/redhat_ansible_lockup.png" />
|
||||
<p class="text-center">Copyright 2016. All rights reserved.<br>
|
||||
Ansible and Ansible Tower are registered trademarks of <a href="http://www.redhat.com/" target="_blank">Red Hat, Inc</a>.<br>
|
||||
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
|
||||
{{subscription}}</p>
|
||||
<div class="About-modal--footer">
|
||||
<img class="About-brand--redhat img-responsive" src="/static/assets/tower-logo-login.svg" />
|
||||
<p class="text-right">Copyright © 2016 Red Hat, Inc. <br>
|
||||
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -36,7 +36,7 @@ export default ['templateUrl', function(templateUrl) {
|
||||
|
||||
$scope.changeStreamTarget = function(){
|
||||
|
||||
if($scope.streamTarget && $scope.streamTarget == 'dashboard') {
|
||||
if($scope.streamTarget && $scope.streamTarget === 'dashboard') {
|
||||
// Just navigate to the base activity stream
|
||||
$state.go('activityStream', {}, {inherit: false});
|
||||
}
|
||||
@ -45,7 +45,7 @@ export default ['templateUrl', function(templateUrl) {
|
||||
$state.go('activityStream', {target: $scope.streamTarget}, {inherit: false});
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}],
|
||||
};
|
||||
}];
|
||||
|
||||
@ -22,6 +22,10 @@ function adhocController($q, $scope, $rootScope, $location, $stateParams,
|
||||
var privateFn = {};
|
||||
this.privateFn = privateFn;
|
||||
|
||||
var id = $stateParams.inventory_id,
|
||||
urls = privateFn.setAvailableUrls(),
|
||||
hostPattern = $rootScope.hostPatterns || "all";
|
||||
|
||||
// note: put any urls that the controller will use in here!!!!
|
||||
privateFn.setAvailableUrls = function() {
|
||||
return {
|
||||
@ -31,10 +35,6 @@ function adhocController($q, $scope, $rootScope, $location, $stateParams,
|
||||
};
|
||||
};
|
||||
|
||||
var id = $stateParams.inventory_id,
|
||||
urls = privateFn.setAvailableUrls(),
|
||||
hostPattern = $rootScope.hostPatterns || "all";
|
||||
|
||||
// set the default options for the selects of the adhoc form
|
||||
privateFn.setFieldDefaults = function(verbosity_options, forks_default) {
|
||||
var verbosity;
|
||||
@ -164,7 +164,7 @@ function adhocController($q, $scope, $rootScope, $location, $stateParams,
|
||||
|
||||
$scope.formCancel = function(){
|
||||
$state.go('inventoryManage');
|
||||
}
|
||||
};
|
||||
|
||||
// remove all data input into the form and reset the form back to defaults
|
||||
$scope.formReset = function () {
|
||||
|
||||
@ -10,10 +10,5 @@ export default {
|
||||
route: '/adhoc',
|
||||
name: 'inventoryManage.adhoc',
|
||||
templateUrl: templateUrl('adhoc/adhoc'),
|
||||
controller: 'adhocController',
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: 'adhocController'
|
||||
};
|
||||
|
||||
@ -5,12 +5,13 @@
|
||||
*************************************************/
|
||||
|
||||
var urlPrefix;
|
||||
var $basePath;
|
||||
|
||||
if ($basePath) {
|
||||
urlPrefix = $basePath;
|
||||
} else {
|
||||
// required to make tests work
|
||||
var $basePath = '/static/';
|
||||
$basePath = '/static/';
|
||||
urlPrefix = $basePath;
|
||||
}
|
||||
|
||||
@ -20,7 +21,7 @@ import './lists';
|
||||
import './widgets';
|
||||
import './help';
|
||||
import './filters';
|
||||
import {Home, HomeGroups, HomeHosts} from './controllers/Home';
|
||||
import {Home, HomeGroups} from './controllers/Home';
|
||||
import {SocketsController} from './controllers/Sockets';
|
||||
import {CredentialsAdd, CredentialsEdit, CredentialsList} from './controllers/Credentials';
|
||||
import {JobsListController} from './controllers/Jobs';
|
||||
@ -49,20 +50,17 @@ import adhoc from './adhoc/main';
|
||||
import login from './login/main';
|
||||
import activityStream from './activity-stream/main';
|
||||
import standardOut from './standard-out/main';
|
||||
import lookUpHelper from './lookup/main';
|
||||
import JobTemplates from './job-templates/main';
|
||||
import search from './search/main';
|
||||
import {ScheduleEditController} from './controllers/Schedules';
|
||||
import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
|
||||
import OrganizationsList from './organizations/list/organizations-list.controller';
|
||||
import OrganizationsAdd from './organizations/add/organizations-add.controller';
|
||||
import OrganizationsEdit from './organizations/edit/organizations-edit.controller';
|
||||
import {InventoriesAdd, InventoriesEdit, InventoriesList, InventoriesManage} from './inventories/main';
|
||||
import {AdminsList} from './controllers/Admins';
|
||||
import {UsersList, UsersAdd, UsersEdit} from './controllers/Users';
|
||||
import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams';
|
||||
|
||||
import RestServices from './rest/main';
|
||||
import './lookup/main';
|
||||
import './shared/api-loader';
|
||||
import './shared/form-generator';
|
||||
import './shared/Modal';
|
||||
@ -175,7 +173,6 @@ var tower = angular.module('Tower', [
|
||||
'CredentialsHelper',
|
||||
'StreamListDefinition',
|
||||
'HomeGroupListDefinition',
|
||||
'HomeHostListDefinition',
|
||||
'ActivityDetailDefinition',
|
||||
'VariablesHelper',
|
||||
'SchedulesListDefinition',
|
||||
@ -198,7 +195,7 @@ var tower = angular.module('Tower', [
|
||||
'pendolytics',
|
||||
'ui.router',
|
||||
'ncy-angular-breadcrumb',
|
||||
'scheduler',
|
||||
scheduler.name,
|
||||
'ApiModelHelper',
|
||||
'ActivityStreamHelper',
|
||||
'dndLists'
|
||||
@ -261,30 +258,6 @@ var tower = angular.module('Tower', [
|
||||
ncyBreadcrumb: {
|
||||
parent: 'dashboard',
|
||||
label: "GROUPS"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
state('dashboardHosts', {
|
||||
url: '/home/hosts?has_active_failures&name&id',
|
||||
templateUrl: urlPrefix + 'partials/subhome.html',
|
||||
controller: HomeHosts,
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'host'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'dashboard',
|
||||
label: "HOSTS"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -294,11 +267,6 @@ var tower = angular.module('Tower', [
|
||||
controller: JobsListController,
|
||||
ncyBreadcrumb: {
|
||||
label: "JOBS"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -312,11 +280,6 @@ var tower = angular.module('Tower', [
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: "PROJECTS"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -327,11 +290,6 @@ var tower = angular.module('Tower', [
|
||||
ncyBreadcrumb: {
|
||||
parent: "projects",
|
||||
label: "CREATE PROJECT"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -341,79 +299,19 @@ var tower = angular.module('Tower', [
|
||||
controller: ProjectsEdit,
|
||||
data: {
|
||||
activityStreamId: 'id'
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
state('projectOrganizations', {
|
||||
url: '/projects/:project_id/organizations',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: OrganizationsList,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: OrganizationsList
|
||||
}).
|
||||
|
||||
state('projectOrganizationAdd', {
|
||||
url: '/projects/:project_id/organizations/add',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: OrganizationsAdd,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: OrganizationsAdd
|
||||
}).
|
||||
|
||||
state('organizationAdmins', {
|
||||
url: '/organizations/:organization_id/admins',
|
||||
templateUrl: urlPrefix + 'partials/organizations.html',
|
||||
controller: AdminsList,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
state('organizationUsers', {
|
||||
url:'/organizations/:organization_id/users',
|
||||
templateUrl: urlPrefix + 'partials/users.html',
|
||||
controller: UsersList,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
state('organizationUserAdd', {
|
||||
url: '/organizations/:organization_id/users/add',
|
||||
templateUrl: urlPrefix + 'partials/users.html',
|
||||
controller: UsersAdd,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
state('organizationUserEdit', {
|
||||
url: '/organizations/:organization_id/users/:user_id',
|
||||
templateUrl: urlPrefix + 'partials/users.html',
|
||||
controller: UsersEdit,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
state('teams', {
|
||||
url: '/teams',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
@ -425,11 +323,6 @@ var tower = angular.module('Tower', [
|
||||
ncyBreadcrumb: {
|
||||
parent: 'setup',
|
||||
label: 'TEAMS'
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -440,11 +333,6 @@ var tower = angular.module('Tower', [
|
||||
ncyBreadcrumb: {
|
||||
parent: "teams",
|
||||
label: "CREATE TEAM"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -454,100 +342,55 @@ var tower = angular.module('Tower', [
|
||||
controller: TeamsEdit,
|
||||
data: {
|
||||
activityStreamId: 'team_id'
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
state('teamUsers', {
|
||||
url: '/teams/:team_id/users',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: UsersList,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: UsersList
|
||||
}).
|
||||
|
||||
state('teamUserEdit', {
|
||||
url: '/teams/:team_id/users/:user_id',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: UsersEdit,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: UsersEdit
|
||||
}).
|
||||
|
||||
state('teamProjects', {
|
||||
url: '/teams/:team_id/projects',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: ProjectsList,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: ProjectsList
|
||||
}).
|
||||
|
||||
state('teamProjectAdd', {
|
||||
url: '/teams/:team_id/projects/add',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: ProjectsAdd,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: ProjectsAdd
|
||||
}).
|
||||
|
||||
state('teamProjectEdit', {
|
||||
url: '/teams/:team_id/projects/:project_id',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: ProjectsEdit,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: ProjectsEdit
|
||||
}).
|
||||
|
||||
state('teamCredentials', {
|
||||
url: '/teams/:team_id/credentials',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: CredentialsList,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: CredentialsList
|
||||
}).
|
||||
|
||||
state('teamCredentialAdd', {
|
||||
url: '/teams/:team_id/credentials/add',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: CredentialsAdd,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: CredentialsAdd
|
||||
}).
|
||||
|
||||
state('teamCredentialEdit', {
|
||||
url: '/teams/:team_id/credentials/:credential_id',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: CredentialsEdit,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: CredentialsEdit
|
||||
}).
|
||||
|
||||
state('credentials', {
|
||||
@ -561,11 +404,6 @@ var tower = angular.module('Tower', [
|
||||
ncyBreadcrumb: {
|
||||
parent: 'setup',
|
||||
label: 'CREDENTIALS'
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -576,11 +414,6 @@ var tower = angular.module('Tower', [
|
||||
ncyBreadcrumb: {
|
||||
parent: "credentials",
|
||||
label: "CREATE CREDENTIAL"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -590,11 +423,6 @@ var tower = angular.module('Tower', [
|
||||
controller: CredentialsEdit,
|
||||
data: {
|
||||
activityStreamId: 'credential_id'
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -609,11 +437,6 @@ var tower = angular.module('Tower', [
|
||||
ncyBreadcrumb: {
|
||||
parent: 'setup',
|
||||
label: 'USERS'
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -624,11 +447,6 @@ var tower = angular.module('Tower', [
|
||||
ncyBreadcrumb: {
|
||||
parent: "users",
|
||||
label: "CREATE USER"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
@ -638,45 +456,25 @@ var tower = angular.module('Tower', [
|
||||
controller: UsersEdit,
|
||||
data: {
|
||||
activityStreamId: 'user_id'
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
state('userCredentials', {
|
||||
url: '/users/:user_id/credentials',
|
||||
templateUrl: urlPrefix + 'partials/users.html',
|
||||
controller: CredentialsList,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: CredentialsList
|
||||
}).
|
||||
|
||||
state('userCredentialAdd', {
|
||||
url: '/users/:user_id/credentials/add',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: CredentialsAdd,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: CredentialsAdd
|
||||
}).
|
||||
|
||||
state('teamUserCredentialEdit', {
|
||||
url: '/teams/:user_id/credentials/:credential_id',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
controller: CredentialsEdit,
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
controller: CredentialsEdit
|
||||
}).
|
||||
|
||||
state('sockets', {
|
||||
@ -710,7 +508,7 @@ var tower = angular.module('Tower', [
|
||||
var sock;
|
||||
$rootScope.addPermission = function (scope) {
|
||||
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
|
||||
}
|
||||
};
|
||||
|
||||
$rootScope.deletePermission = function (user, role, userName,
|
||||
roleName, resourceName) {
|
||||
@ -738,6 +536,66 @@ var tower = angular.module('Tower', [
|
||||
});
|
||||
};
|
||||
|
||||
$rootScope.deletePermissionFromUser = function (userId, userName, roleName, roleType, url) {
|
||||
var action = function () {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
Rest.setUrl(url);
|
||||
Rest.post({"disassociate": true, "id": userId})
|
||||
.success(function () {
|
||||
Wait('stop');
|
||||
$rootScope.$broadcast("refreshList", "permission");
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Could not disassociate user from role. Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
Prompt({
|
||||
hdr: `Remove role`,
|
||||
body: `
|
||||
<div class="Prompt-bodyQuery">
|
||||
Confirm the removal of the ${roleType}
|
||||
<span class="Prompt-emphasis"> ${roleName} </span>
|
||||
role associated with ${userName}.
|
||||
</div>
|
||||
`,
|
||||
action: action,
|
||||
actionText: 'REMOVE'
|
||||
});
|
||||
};
|
||||
|
||||
$rootScope.deletePermissionFromTeam = function (teamId, teamName, roleName, roleType, url) {
|
||||
var action = function () {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
Rest.setUrl(url);
|
||||
Rest.post({"disassociate": true, "id": teamId})
|
||||
.success(function () {
|
||||
Wait('stop');
|
||||
$rootScope.$broadcast("refreshList", "role");
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Could not disassociate team from role. Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
Prompt({
|
||||
hdr: `Remove role`,
|
||||
body: `
|
||||
<div class="Prompt-bodyQuery">
|
||||
Confirm the removal of the ${roleType}
|
||||
<span class="Prompt-emphasis"> ${roleName} </span>
|
||||
role associated with the ${teamName} team.
|
||||
</div>
|
||||
`,
|
||||
action: action,
|
||||
actionText: 'REMOVE'
|
||||
});
|
||||
};
|
||||
|
||||
function activateTab() {
|
||||
// Make the correct tab active
|
||||
var base = $location.path().replace(/^\//, '').split('/')[0];
|
||||
@ -774,16 +632,17 @@ var tower = angular.module('Tower', [
|
||||
$rootScope.removeConfigReady();
|
||||
}
|
||||
$rootScope.removeConfigReady = $rootScope.$on('ConfigReady', function() {
|
||||
var list, id;
|
||||
// initially set row edit indicator for crud pages
|
||||
if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") {
|
||||
var list = $location.$$path.split("/")[3];
|
||||
var id = $location.$$path.split("/")[4];
|
||||
list = $location.$$path.split("/")[3];
|
||||
id = $location.$$path.split("/")[4];
|
||||
$rootScope.listBeingEdited = list;
|
||||
$rootScope.rowBeingEdited = id;
|
||||
$rootScope.initialIndicatorLoad = true;
|
||||
} else if ($location.$$path.split("/")[2]) {
|
||||
var list = $location.$$path.split("/")[1];
|
||||
var id = $location.$$path.split("/")[2];
|
||||
list = $location.$$path.split("/")[1];
|
||||
id = $location.$$path.split("/")[2];
|
||||
$rootScope.listBeingEdited = list;
|
||||
$rootScope.rowBeingEdited = id;
|
||||
}
|
||||
@ -871,6 +730,9 @@ var tower = angular.module('Tower', [
|
||||
|
||||
|
||||
$rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) {
|
||||
if (next.name !== 'signOut'){
|
||||
CheckLicense.notify();
|
||||
}
|
||||
$rootScope.$broadcast("closePermissionsModal");
|
||||
// this line removes the query params attached to a route
|
||||
if(prev && prev.$$route &&
|
||||
@ -915,15 +777,16 @@ var tower = angular.module('Tower', [
|
||||
activateTab();
|
||||
});
|
||||
|
||||
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
|
||||
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState) {
|
||||
// catch license expiration notifications immediately after user logs in, redirect
|
||||
if (fromState.name == 'signIn'){
|
||||
if (fromState.name === 'signIn'){
|
||||
CheckLicense.notify();
|
||||
}
|
||||
var list, id;
|
||||
// broadcast event change if editing crud object
|
||||
if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") {
|
||||
var list = $location.$$path.split("/")[3];
|
||||
var id = $location.$$path.split("/")[4];
|
||||
list = $location.$$path.split("/")[3];
|
||||
id = $location.$$path.split("/")[4];
|
||||
|
||||
if (!$rootScope.initialIndicatorLoad) {
|
||||
delete $rootScope.listBeingEdited;
|
||||
@ -934,8 +797,8 @@ var tower = angular.module('Tower', [
|
||||
|
||||
$rootScope.$broadcast("EditIndicatorChange", list, id);
|
||||
} else if ($location.$$path.split("/")[2]) {
|
||||
var list = $location.$$path.split("/")[1];
|
||||
var id = $location.$$path.split("/")[2];
|
||||
list = $location.$$path.split("/")[1];
|
||||
id = $location.$$path.split("/")[2];
|
||||
|
||||
delete $rootScope.listBeingEdited;
|
||||
delete $rootScope.rowBeingEdited;
|
||||
@ -962,6 +825,7 @@ var tower = angular.module('Tower', [
|
||||
$rootScope.sessionTimer = timer;
|
||||
$rootScope.$emit('OpenSocket');
|
||||
pendoService.issuePendoIdentity();
|
||||
CheckLicense.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ export default
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: templateUrl('bread-crumb/bread-crumb'),
|
||||
link: function(scope, element, attrs) {
|
||||
link: function(scope) {
|
||||
|
||||
var streamConfig = {};
|
||||
|
||||
@ -15,15 +15,15 @@ export default
|
||||
|
||||
if(streamConfig && streamConfig.activityStream) {
|
||||
if(streamConfig.activityStreamTarget) {
|
||||
stateGoParams['target'] = streamConfig.activityStreamTarget;
|
||||
stateGoParams.target = streamConfig.activityStreamTarget;
|
||||
}
|
||||
if(streamConfig.activityStreamId) {
|
||||
stateGoParams['id'] = $state.params[streamConfig.activityStreamId];
|
||||
stateGoParams.id = $state.params[streamConfig.activityStreamId];
|
||||
}
|
||||
}
|
||||
|
||||
$state.go('activityStream', stateGoParams);
|
||||
}
|
||||
};
|
||||
|
||||
scope.$on("$stateChangeSuccess", function updateActivityStreamButton(event, toState) {
|
||||
|
||||
@ -38,7 +38,7 @@ export default
|
||||
// attached to the $rootScope.
|
||||
|
||||
FeaturesService.get()
|
||||
.then(function(features) {
|
||||
.then(function() {
|
||||
if(FeaturesService.featureEnabled('activity_streams')) {
|
||||
scope.showActivityStreamButton = true;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<div id="bread_crumb" class="BreadCrumb" ng-class="{'is-loggedOut' : !$root.current_user.username}">
|
||||
<div ncy-breadcrumb></div>
|
||||
<div ng-if="!licenseMissing" ncy-breadcrumb></div>
|
||||
<div class="BreadCrumb-menuLink"
|
||||
id="bread_crumb_activity_stream"
|
||||
aw-tool-tip="View Activity Stream"
|
||||
@ -8,6 +8,7 @@
|
||||
data-container="body"
|
||||
ng-class="{'BreadCrumb-menuLinkActive' : activityStreamActive}"
|
||||
ng-if="showActivityStreamButton"
|
||||
ng-hide= "licenseMissing"
|
||||
ng-click="openActivityStream()">
|
||||
<i class="BreadCrumb-menuLinkImage icon-activity-stream"
|
||||
alt="Activity Stream">
|
||||
@ -20,6 +21,7 @@
|
||||
data-placement="left"
|
||||
data-trigger="hover"
|
||||
data-container="body"
|
||||
ng-hide="licenseMissing"
|
||||
ng-if="!showActivityStreamButton">
|
||||
<i class="BreadCrumb-menuLinkImage fa fa-tachometer"
|
||||
alt="Dashboard">
|
||||
|
||||
@ -519,8 +519,8 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
|
||||
$scope.project = data.project;
|
||||
break;
|
||||
case 'azure':
|
||||
$scope.subscription_id = data.username;
|
||||
|
||||
$scope.subscription = data.username;
|
||||
break;
|
||||
}
|
||||
$scope.credential_obj = data;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
@ -155,12 +155,10 @@ export function HomeGroups($rootScope, $log, $scope, $filter, $compile, $locatio
|
||||
|
||||
ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
|
||||
//scope.
|
||||
|
||||
var generator = GenerateList,
|
||||
list = HomeGroupList,
|
||||
defaultUrl = GetBasePath('groups'),
|
||||
scope = $scope,
|
||||
modal_scope = $scope.$new(),
|
||||
opt, PreviousSearchParams;
|
||||
|
||||
generator.inject(list, { mode: 'edit', scope: scope });
|
||||
@ -517,112 +515,3 @@ HomeGroups.$inject = ['$rootScope', '$log', '$scope', '$filter', '$compile', '$l
|
||||
* @description This loads the page for 'home/hosts'
|
||||
*
|
||||
*/
|
||||
|
||||
export function HomeHosts($scope, $location, $stateParams, HomeHostList, GenerateList, ProcessErrors, ReturnToCaller, ClearScope,
|
||||
GetBasePath, SearchInit, PaginateInit, FormatDate, SetStatus, ToggleHostEnabled, HostsEdit, Find, ShowJobSummary) {
|
||||
|
||||
ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
|
||||
//scope.
|
||||
|
||||
var generator = GenerateList,
|
||||
list = HomeHostList,
|
||||
defaultUrl = GetBasePath('hosts');
|
||||
|
||||
if ($scope.removePostRefresh) {
|
||||
$scope.removePostRefresh();
|
||||
}
|
||||
$scope.removePostRefresh = $scope.$on('PostRefresh', function () {
|
||||
for (var i = 0; i < $scope.hosts.length; i++) {
|
||||
$scope.hosts[i].inventory_name = $scope.hosts[i].summary_fields.inventory.name;
|
||||
//SetHostStatus($scope['hosts'][i]);
|
||||
SetStatus({
|
||||
$scope: $scope,
|
||||
host: $scope.hosts[i]
|
||||
});
|
||||
}
|
||||
|
||||
generator.inject(list, { mode: 'edit', scope: $scope });
|
||||
|
||||
});
|
||||
|
||||
SearchInit({
|
||||
scope: $scope,
|
||||
set: 'hosts',
|
||||
list: list,
|
||||
url: defaultUrl
|
||||
});
|
||||
|
||||
PaginateInit({
|
||||
scope: $scope,
|
||||
list: list,
|
||||
url: defaultUrl
|
||||
});
|
||||
|
||||
// Process search params
|
||||
if ($stateParams.name) {
|
||||
$scope[HomeHostList.iterator + 'InputDisable'] = false;
|
||||
$scope[HomeHostList.iterator + 'SearchValue'] = $stateParams.name;
|
||||
$scope[HomeHostList.iterator + 'SearchField'] = 'name';
|
||||
$scope[HomeHostList.iterator + 'SearchFieldLabel'] = list.fields.name.label;
|
||||
}
|
||||
|
||||
if ($stateParams.id) {
|
||||
$scope[HomeHostList.iterator + 'InputDisable'] = false;
|
||||
$scope[HomeHostList.iterator + 'SearchValue'] = $stateParams.id;
|
||||
$scope[HomeHostList.iterator + 'SearchField'] = 'id';
|
||||
$scope[HomeHostList.iterator + 'SearchFieldLabel'] = list.fields.id.label;
|
||||
$scope[HomeHostList.iterator + 'SearchSelectValue'] = null;
|
||||
}
|
||||
|
||||
if ($stateParams.has_active_failures) {
|
||||
$scope[HomeHostList.iterator + 'InputDisable'] = true;
|
||||
$scope[HomeHostList.iterator + 'SearchValue'] = $stateParams.has_active_failures;
|
||||
$scope[HomeHostList.iterator + 'SearchField'] = 'has_active_failures';
|
||||
$scope[HomeHostList.iterator + 'SearchFieldLabel'] = HomeHostList.fields.has_active_failures.label;
|
||||
$scope[HomeHostList.iterator + 'SearchSelectValue'] = ($stateParams.has_active_failures === 'true') ? { value: 1 } : { value: 0 };
|
||||
}
|
||||
|
||||
$scope.search(list.iterator);
|
||||
|
||||
$scope.refreshHosts = function() {
|
||||
$scope.search(list.iterator);
|
||||
};
|
||||
|
||||
$scope.toggleHostEnabled = function (id, sources) {
|
||||
ToggleHostEnabled({
|
||||
host_id: id,
|
||||
external_source: sources,
|
||||
host_scope: $scope
|
||||
});
|
||||
};
|
||||
|
||||
$scope.editHost = function (host_id) {
|
||||
var host = Find({
|
||||
list: $scope.hosts,
|
||||
key: 'id',
|
||||
val: host_id
|
||||
});
|
||||
if (host) {
|
||||
HostsEdit({
|
||||
host_scope: $scope,
|
||||
host_id: host_id,
|
||||
inventory_id: host.inventory,
|
||||
group_id: null,
|
||||
hostsReload: false,
|
||||
mode: 'edit'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showJobSummary = function (job_id) {
|
||||
ShowJobSummary({
|
||||
job_id: job_id
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
HomeHosts.$inject = ['$scope', '$location', '$stateParams', 'HomeHostList', 'generateList', 'ProcessErrors', 'ReturnToCaller',
|
||||
'ClearScope', 'GetBasePath', 'SearchInit', 'PaginateInit', 'FormatDate', 'SetStatus', 'ToggleHostEnabled', 'HostsEdit',
|
||||
'Find', 'ShowJobSummary'
|
||||
];
|
||||
|
||||
@ -28,7 +28,6 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
||||
mode = (base === 'projects') ? 'edit' : 'select',
|
||||
url = (base === 'teams') ? GetBasePath('teams') + $stateParams.team_id + '/projects/' : defaultUrl,
|
||||
choiceCount = 0;
|
||||
|
||||
view.inject(list, { mode: mode, scope: $scope });
|
||||
|
||||
$rootScope.flashMessage = null;
|
||||
|
||||
@ -16,7 +16,7 @@ GetBasePath, Wait, Find, LoadDialogPartial, LoadSchedulesScope, GetChoices) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
var base, e, id, url, parentObject;
|
||||
var base, id, url, parentObject;
|
||||
|
||||
base = $location.path().replace(/^\//, '').split('/')[0];
|
||||
|
||||
|
||||
@ -188,7 +188,7 @@ TeamsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log',
|
||||
];
|
||||
|
||||
|
||||
export function TeamsEdit($scope, $rootScope, $location,
|
||||
export function TeamsEdit($scope, $rootScope, $location,
|
||||
$stateParams, TeamForm, GenerateForm, Rest, ProcessErrors,
|
||||
RelatedSearchInit, RelatedPaginateInit, ClearScope,
|
||||
LookUpInit, GetBasePath, OrganizationList, Wait, $state) {
|
||||
@ -198,16 +198,15 @@ export function TeamsEdit($scope, $rootScope, $location,
|
||||
var defaultUrl = GetBasePath('teams'),
|
||||
generator = GenerateForm,
|
||||
form = TeamForm,
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
master = {},
|
||||
id = $stateParams.team_id,
|
||||
relatedSets = {};
|
||||
relatedSets = {},
|
||||
set;
|
||||
|
||||
$scope.team_id = id;
|
||||
|
||||
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset()
|
||||
generator.reset();
|
||||
|
||||
var setScopeFields = function(data){
|
||||
_(data)
|
||||
@ -218,7 +217,7 @@ export function TeamsEdit($scope, $rootScope, $location,
|
||||
$scope[key] = value;
|
||||
})
|
||||
.value();
|
||||
return
|
||||
return;
|
||||
};
|
||||
var setScopeRelated = function(data, related){
|
||||
_(related)
|
||||
@ -242,7 +241,7 @@ export function TeamsEdit($scope, $rootScope, $location,
|
||||
data[key] = $scope[key];
|
||||
}
|
||||
});
|
||||
return data
|
||||
return data;
|
||||
};
|
||||
|
||||
var init = function(){
|
||||
@ -251,7 +250,7 @@ export function TeamsEdit($scope, $rootScope, $location,
|
||||
Wait('start');
|
||||
Rest.get(url).success(function(data){
|
||||
setScopeFields(data);
|
||||
setScopeRelated(data, form.related)
|
||||
setScopeRelated(data, form.related);
|
||||
$scope.organization_name = data.summary_fields.organization.name;
|
||||
|
||||
RelatedSearchInit({
|
||||
@ -265,6 +264,12 @@ export function TeamsEdit($scope, $rootScope, $location,
|
||||
relatedSets: relatedSets
|
||||
});
|
||||
|
||||
for (set in relatedSets) {
|
||||
$scope.search(relatedSets[set].iterator);
|
||||
}
|
||||
|
||||
$scope.team_obj = data;
|
||||
|
||||
LookUpInit({
|
||||
url: GetBasePath('organizations'),
|
||||
scope: $scope,
|
||||
@ -275,11 +280,11 @@ export function TeamsEdit($scope, $rootScope, $location,
|
||||
input_type: 'radio'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.formCancel = function(){
|
||||
$state.go('teams', null, {reload: true});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.formSave = function(){
|
||||
generator.clearApiErrors();
|
||||
@ -288,23 +293,31 @@ export function TeamsEdit($scope, $rootScope, $location,
|
||||
if ($scope[form.name + '_form'].$valid){
|
||||
Rest.setUrl(defaultUrl + id + '/');
|
||||
var data = processNewData(form.fields);
|
||||
Rest.put(data).success(function(res){
|
||||
Rest.put(data).success(function(){
|
||||
$state.go('teams', null, {reload: true});
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user: ' +
|
||||
$stateParams.id + '. GET status: ' + status });
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
$scope.convertApiUrl = function(str) {
|
||||
if (str) {
|
||||
return str.replace("api/v1", "#");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/* Related Set implementation TDB */
|
||||
}
|
||||
|
||||
TeamsEdit.$inject = ['$scope', '$rootScope', '$location',
|
||||
'$stateParams', 'TeamForm', 'GenerateForm', 'Rest',
|
||||
'$stateParams', 'TeamForm', 'GenerateForm', 'Rest',
|
||||
'ProcessErrors', 'RelatedSearchInit', 'RelatedPaginateInit',
|
||||
'ClearScope', 'LookUpInit', 'GetBasePath',
|
||||
'OrganizationList', 'Wait', '$state'
|
||||
|
||||
@ -229,10 +229,10 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
var defaultUrl = GetBasePath('users'),
|
||||
generator = GenerateForm,
|
||||
form = UserForm,
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
master = {},
|
||||
id = $stateParams.user_id,
|
||||
relatedSets = {};
|
||||
relatedSets = {},
|
||||
set;
|
||||
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset();
|
||||
@ -246,20 +246,28 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
$scope[key] = value;
|
||||
})
|
||||
.value();
|
||||
return
|
||||
return;
|
||||
};
|
||||
|
||||
$scope.convertApiUrl = function(str) {
|
||||
if (str) {
|
||||
return str.replace("api/v1", "#");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
var setScopeRelated = function(data, related){
|
||||
_(related)
|
||||
.pick(function(value, key){
|
||||
return data.related.hasOwnProperty(key) === true;
|
||||
})
|
||||
.forEach(function(value, key){
|
||||
relatedSets[key] = {
|
||||
url: data.related[key],
|
||||
iterator: value.iterator
|
||||
};
|
||||
})
|
||||
.pick(function(value, key){
|
||||
return data.related.hasOwnProperty(key) === true;
|
||||
})
|
||||
.forEach(function(value, key){
|
||||
relatedSets[key] = {
|
||||
url: data.related[key],
|
||||
iterator: value.iterator
|
||||
};
|
||||
})
|
||||
.value();
|
||||
};
|
||||
// prepares a data payload for a PUT request to the API
|
||||
@ -270,7 +278,7 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
data[key] = $scope[key];
|
||||
}
|
||||
});
|
||||
return data
|
||||
return data;
|
||||
};
|
||||
|
||||
var init = function(){
|
||||
@ -296,6 +304,11 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
scope: $scope,
|
||||
relatedSets: relatedSets
|
||||
});
|
||||
|
||||
for (set in relatedSets) {
|
||||
$scope.search(relatedSets[set].iterator);
|
||||
}
|
||||
|
||||
Wait('stop');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
@ -315,13 +328,13 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
if ($scope[form.name + '_form'].$valid){
|
||||
Rest.setUrl(defaultUrl + id + '/');
|
||||
var data = processNewData(form.fields);
|
||||
Rest.put(data).success(function(res){
|
||||
$state.go('users', null, {reload: true})
|
||||
Rest.put(data).success(function(){
|
||||
$state.go('users', null, {reload: true});
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user: ' +
|
||||
$stateParams.id + '. GET status: ' + status });
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -338,7 +351,7 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
}
|
||||
|
||||
UsersEdit.$inject = ['$scope', '$rootScope', '$location',
|
||||
'$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'ProcessErrors',
|
||||
'$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'ProcessErrors',
|
||||
'RelatedSearchInit', 'RelatedPaginateInit', 'ClearScope', 'GetBasePath',
|
||||
'ResetForm', 'Wait', '$state'
|
||||
];
|
||||
|
||||
@ -38,7 +38,7 @@ export default
|
||||
label: "Hosts"
|
||||
},
|
||||
{
|
||||
url: "/#/home/hosts?has_active_failures=true",
|
||||
url: "/#/home/hosts?active-failures=true",
|
||||
number: scope.data.hosts.failed,
|
||||
label: "Failed Hosts",
|
||||
isFailureCount: true
|
||||
|
||||
@ -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();
|
||||
}];
|
||||
@ -0,0 +1,4 @@
|
||||
<div class="tab-pane" id="organizations">
|
||||
<div ui-view></div>
|
||||
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||
</div>
|
||||
@ -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();
|
||||
}];
|
||||
@ -0,0 +1,4 @@
|
||||
<div class="tab-pane" id="HomeHosts">
|
||||
<div ui-view></div>
|
||||
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||
</div>
|
||||
77
awx/ui/client/src/dashboard/hosts/dashboard-hosts.form.js
Normal file
77
awx/ui/client/src/dashboard/hosts/dashboard-hosts.form.js
Normal 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()'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,14 +1,12 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
export default
|
||||
angular.module('HomeHostListDefinition', [])
|
||||
.value('HomeHostList', {
|
||||
|
||||
export default function(){
|
||||
return {
|
||||
name: 'hosts',
|
||||
iterator: 'host',
|
||||
selectTitle: 'Add Existing Hosts',
|
||||
@ -17,41 +15,48 @@ export default
|
||||
index: false,
|
||||
hover: true,
|
||||
well: true,
|
||||
|
||||
emptyListText: 'NO ACTIVE FAILURES FOUND',
|
||||
fields: {
|
||||
status: {
|
||||
label: "",
|
||||
basePath: 'unified_jobs',
|
||||
label: '',
|
||||
iconOnly: true,
|
||||
icon: "{{ 'icon-job-' + host.active_failures }}",
|
||||
awToolTip: "{{ host.badgeToolTip }}",
|
||||
awTipPlacement: "right",
|
||||
dataPlacement: "right",
|
||||
awPopOver: "{{ host.job_status_html }}",
|
||||
ngClick:"bob",
|
||||
columnClass: "List-staticColumn--smallStatus",
|
||||
searchable: false,
|
||||
nosort: true
|
||||
searchable: true,
|
||||
searchType: 'select',
|
||||
nosort: true,
|
||||
searchOptions: [],
|
||||
searchLabel: 'Job Status',
|
||||
icon: 'icon-job-{{ host.active_failures }}',
|
||||
awToolTip: '{{ host.badgeToolTip }}',
|
||||
awTipPlacement: 'right',
|
||||
dataPlacement: 'right',
|
||||
awPopOver: '{{ host.job_status_html }}',
|
||||
ngClick:'viewHost(host.id)',
|
||||
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus'
|
||||
},
|
||||
name: {
|
||||
key: true,
|
||||
label: 'Name',
|
||||
columnClass: 'col-lg-5 col-md-5 col-sm-5 col-xs-8 ellipsis List-staticColumnAdjacent',
|
||||
ngClass: "{ 'host-disabled-label': !host.enabled }",
|
||||
ngClick: "editHost(host.id)"
|
||||
ngClick: 'editHost(host.id)'
|
||||
},
|
||||
inventory_name: {
|
||||
label: 'Inventory',
|
||||
sourceModel: 'inventory',
|
||||
sourceField: 'name',
|
||||
columnClass: 'col-lg-5 col-md-4 col-sm-4 hidden-xs elllipsis',
|
||||
linkTo: "{{ '/#/inventories/' + host.inventory }}"
|
||||
linkTo: "{{ '/#/inventories/' + host.inventory_id }}",
|
||||
searchable: false
|
||||
},
|
||||
enabled: {
|
||||
label: 'Disabled?',
|
||||
searchSingleValue: true,
|
||||
searchType: 'boolean',
|
||||
searchValue: 'false',
|
||||
searchOnly: true
|
||||
label: 'Status',
|
||||
columnClass: 'List-staticColumn--toggle',
|
||||
type: 'toggle',
|
||||
ngClick: 'toggleHostEnabled(host)',
|
||||
searchable: false,
|
||||
nosort: true,
|
||||
awToolTip: "<p>Indicates if a host is available and should be included in running jobs.</p><p>For hosts that are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.</p>",
|
||||
dataTitle: 'Host Enabled',
|
||||
},
|
||||
has_active_failures: {
|
||||
label: 'Has failed jobs?',
|
||||
@ -66,30 +71,15 @@ export default
|
||||
searchType: 'boolean',
|
||||
searchValue: 'true',
|
||||
searchOnly: true
|
||||
},
|
||||
id: {
|
||||
label: 'ID',
|
||||
searchOnly: true
|
||||
}
|
||||
},
|
||||
|
||||
fieldActions: {
|
||||
|
||||
columnClass: 'col-lg-2 col-md-3 col-sm-3 col-xs-4',
|
||||
|
||||
/*active_failures: {
|
||||
//label: 'Job Status',
|
||||
//ngHref: "\{\{'/#/hosts/' + host.id + '/job_host_summaries/?inventory=' + inventory_id \}\}",
|
||||
awPopOver: "{{ host.job_status_html }}",
|
||||
dataTitle: "{{ host.job_status_title }}",
|
||||
awToolTip: "{{ host.badgeToolTip }}",
|
||||
awTipPlacement: 'top',
|
||||
dataPlacement: 'left',
|
||||
iconClass: "{{ 'fa icon-failures-' + host.has_active_failures }}"
|
||||
}*/
|
||||
edit: {
|
||||
label: 'Edit',
|
||||
ngClick: "editHost(host.id)",
|
||||
ngClick: 'editHost(host.id)',
|
||||
icon: 'icon-edit',
|
||||
awToolTip: 'Edit host',
|
||||
dataPlacement: 'top'
|
||||
@ -99,5 +89,5 @@ export default
|
||||
actions: {
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
}
|
||||
64
awx/ui/client/src/dashboard/hosts/dashboard-hosts.route.js
Normal file
64
awx/ui/client/src/dashboard/hosts/dashboard-hosts.route.js
Normal 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};
|
||||
30
awx/ui/client/src/dashboard/hosts/dashboard-hosts.service.js
Normal file
30
awx/ui/client/src/dashboard/hosts/dashboard-hosts.service.js
Normal 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 });
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
20
awx/ui/client/src/dashboard/hosts/main.js
Normal file
20
awx/ui/client/src/dashboard/hosts/main.js
Normal 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);
|
||||
}]);
|
||||
@ -2,7 +2,8 @@ import dashboardCounts from './counts/main';
|
||||
import dashboardGraphs from './graphs/main';
|
||||
import dashboardLists from './lists/main';
|
||||
import dashboardDirective from './dashboard.directive';
|
||||
import dashboardHosts from './hosts/main';
|
||||
|
||||
export default
|
||||
angular.module('dashboard', [dashboardCounts.name, dashboardGraphs.name, dashboardLists.name])
|
||||
angular.module('dashboard', [dashboardHosts.name, dashboardCounts.name, dashboardGraphs.name, dashboardLists.name])
|
||||
.directive('dashboard', dashboardDirective);
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
<footer class='Footer'>
|
||||
<a href="http://www.ansible.com" target="_blank">
|
||||
<div class="Footer-logo" ng-class="{'is-loggedOut' : !$root.current_user.username}">
|
||||
<img id="footer-logo" alt="Red Hat, Inc. | Ansible, Inc." class="Footer-logoImage" src="/static/assets/footer-logo.png">
|
||||
</div>
|
||||
</a>
|
||||
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !$root.current_user.username}">Copyright © 2016 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
|
||||
</footer>
|
||||
|
||||
@ -114,12 +114,17 @@ export default
|
||||
label: 'Secret Key',
|
||||
type: 'sensitive',
|
||||
ngShow: "kind.value == 'aws'",
|
||||
ngDisabled: "secret_key_ask",
|
||||
awRequiredWhen: {
|
||||
reqExpression: "aws_required",
|
||||
init: false
|
||||
},
|
||||
autocomplete: false,
|
||||
ask: false,
|
||||
subCheckbox: {
|
||||
variable: 'secret_key_ask',
|
||||
text: 'Ask at runtime?',
|
||||
ngChange: 'ask(\'secret_key\', \'undefined\')'
|
||||
},
|
||||
clear: false,
|
||||
hasShowInputButton: true,
|
||||
apiField: 'password',
|
||||
@ -141,7 +146,7 @@ export default
|
||||
"host": {
|
||||
labelBind: 'hostLabel',
|
||||
type: 'text',
|
||||
ngShow: "kind.value == 'vmware' || kind.value == 'openstack'",
|
||||
ngShow: "kind.value == 'vmware' || kind.value == 'openstack' || kind.value === 'foreman' || kind.value === 'cloudforms'",
|
||||
awPopOverWatch: "hostPopOver",
|
||||
awPopOver: "set in helpers/credentials",
|
||||
dataTitle: 'Host',
|
||||
@ -154,6 +159,23 @@ export default
|
||||
},
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"subscription": {
|
||||
label: "Subscription ID",
|
||||
type: 'text',
|
||||
ngShow: "kind.value == 'azure' || kind.value == 'azure_rm'",
|
||||
awRequiredWhen: {
|
||||
reqExpression: 'subscription_required',
|
||||
init: false
|
||||
},
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
autocomplete: false,
|
||||
awPopOver: '<p>Subscription ID is an Azure construct, which is mapped to a username.</p>',
|
||||
dataTitle: 'Subscription ID',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"username": {
|
||||
labelBind: 'usernameLabel',
|
||||
type: 'text',
|
||||
@ -181,23 +203,6 @@ export default
|
||||
dataContainer: "body",
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"subscription_id": {
|
||||
labelBind: "usernameLabel",
|
||||
type: 'text',
|
||||
ngShow: "kind.value == 'azure'",
|
||||
awRequiredWhen: {
|
||||
reqExpression: 'subscription_required',
|
||||
init: false
|
||||
},
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
autocomplete: false,
|
||||
awPopOver: '<p>Subscription ID is an Azure construct, which is mapped to a username.</p>',
|
||||
dataTitle: 'Subscription ID',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"api_key": {
|
||||
label: 'API Key',
|
||||
type: 'sensitive',
|
||||
@ -207,7 +212,6 @@ export default
|
||||
init: false
|
||||
},
|
||||
autocomplete: false,
|
||||
ask: false,
|
||||
hasShowInputButton: true,
|
||||
clear: false,
|
||||
subForm: 'credentialSubForm'
|
||||
@ -215,10 +219,7 @@ export default
|
||||
"password": {
|
||||
labelBind: 'passwordLabel',
|
||||
type: 'sensitive',
|
||||
ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack'",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
ask: false,
|
||||
ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack'|| kind.value == 'foreman'|| kind.value == 'cloudforms'|| kind.value == 'net' || kind.value == 'azure_rm'",
|
||||
clear: false,
|
||||
autocomplete: false,
|
||||
hasShowInputButton: true,
|
||||
@ -229,12 +230,17 @@ export default
|
||||
subForm: "credentialSubForm"
|
||||
},
|
||||
"ssh_password": {
|
||||
label: 'Password', // formally 'SSH Password'
|
||||
label: 'Password',
|
||||
type: 'sensitive',
|
||||
ngShow: "kind.value == 'ssh'",
|
||||
ngDisabled: "ssh_password_ask",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
ask: true,
|
||||
subCheckbox: {
|
||||
variable: 'ssh_password_ask',
|
||||
text: 'Ask at runtime?',
|
||||
ngChange: 'ask(\'ssh_password\', \'undefined\')'
|
||||
},
|
||||
hasShowInputButton: true,
|
||||
autocomplete: false,
|
||||
subForm: 'credentialSubForm'
|
||||
@ -243,7 +249,7 @@ export default
|
||||
labelBind: 'sshKeyDataLabel',
|
||||
type: 'textarea',
|
||||
ngShow: "kind.value == 'ssh' || kind.value == 'scm' || " +
|
||||
"kind.value == 'gce' || kind.value == 'azure'",
|
||||
"kind.value == 'gce' || kind.value == 'azure' || kind.value == 'net'",
|
||||
awRequiredWhen: {
|
||||
reqExpression: 'key_required',
|
||||
init: true
|
||||
@ -267,10 +273,15 @@ export default
|
||||
ngShow: "kind.value == 'ssh' || kind.value == 'scm'",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
ngDisabled: "keyEntered === false",
|
||||
ask: true,
|
||||
ngDisabled: "keyEntered === false || ssh_key_unlock_ask",
|
||||
subCheckbox: {
|
||||
variable: 'ssh_key_unlock_ask',
|
||||
ngShow: "kind.value == 'ssh'",
|
||||
text: 'Ask at runtime?',
|
||||
ngChange: 'ask(\'ssh_key_unlock\', \'undefined\')',
|
||||
ngDisabled: "keyEntered === false"
|
||||
},
|
||||
hasShowInputButton: true,
|
||||
askShow: "kind.value == 'ssh'", // Only allow ask for machine credentials
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"become_method": {
|
||||
@ -288,25 +299,77 @@ export default
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"become_username": {
|
||||
label: 'Privilege Escalation Username',
|
||||
labelBind: 'becomeUsernameLabel',
|
||||
type: 'text',
|
||||
ngShow: "kind.value == 'ssh' && (become_method && become_method.value)",
|
||||
ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
autocomplete: false,
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"become_password": {
|
||||
label: 'Privilege Escalation Password',
|
||||
labelBind: 'becomePasswordLabel',
|
||||
type: 'sensitive',
|
||||
ngShow: "kind.value == 'ssh' && (become_method && become_method.value)",
|
||||
ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ",
|
||||
ngDisabled: "become_password_ask",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
ask: true,
|
||||
subCheckbox: {
|
||||
variable: 'become_password_ask',
|
||||
text: 'Ask at runtime?',
|
||||
ngChange: 'ask(\'become_password\', \'undefined\')'
|
||||
},
|
||||
hasShowInputButton: true,
|
||||
autocomplete: false,
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
client:{
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "azure_rm_required",
|
||||
init: false
|
||||
},
|
||||
subForm: 'credentialSubForm',
|
||||
ngShow: "kind.value === 'azure_rm'"
|
||||
},
|
||||
secret:{
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
autocomplete: false,
|
||||
label: 'Client Secret',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "azure_rm_required",
|
||||
init: false
|
||||
},
|
||||
subForm: 'credentialSubForm',
|
||||
ngShow: "kind.value === 'azure_rm'"
|
||||
},
|
||||
tenant: {
|
||||
type: 'text',
|
||||
label: 'Tenent ID',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "azure_rm_required",
|
||||
init: false
|
||||
},
|
||||
subForm: 'credentialSubForm',
|
||||
ngShow: "kind.value === 'azure_rm'"
|
||||
},
|
||||
authorize: {
|
||||
label: 'Authorize',
|
||||
type: 'checkbox',
|
||||
ngChange: "toggleCallback('host_config_key')",
|
||||
subForm: 'credentialSubForm',
|
||||
ngShow: "kind.value === 'net'"
|
||||
},
|
||||
authorize_password: {
|
||||
label: 'Authorize Password',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
autocomplete: false,
|
||||
subForm: 'credentialSubForm',
|
||||
ngShow: "authorize && authorize !== 'false'",
|
||||
},
|
||||
"project": {
|
||||
labelBind: 'projectLabel',
|
||||
type: 'text',
|
||||
@ -344,9 +407,14 @@ export default
|
||||
label: "Vault Password",
|
||||
type: 'sensitive',
|
||||
ngShow: "kind.value == 'ssh'",
|
||||
ngDisabled: "vault_password_ask",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
ask: true,
|
||||
subCheckbox: {
|
||||
variable: 'vault_password_ask',
|
||||
text: 'Ask at runtime?',
|
||||
ngChange: 'ask(\'vault_password\', \'undefined\')'
|
||||
},
|
||||
hasShowInputButton: true,
|
||||
autocomplete: false,
|
||||
subForm: 'credentialSubForm'
|
||||
|
||||
@ -91,7 +91,7 @@ export default
|
||||
type: 'select',
|
||||
ngOptions: 'source.label for source in source_region_choices track by source.value',
|
||||
multiSelect: true,
|
||||
ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure')",
|
||||
ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure' || source.value == 'azure_rm')",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
dataTitle: 'Source Regions',
|
||||
|
||||
@ -15,12 +15,23 @@ export default
|
||||
.value('HostForm', {
|
||||
|
||||
addTitle: 'Create Host',
|
||||
editTitle: '{{ name }}',
|
||||
editTitle: '{{ host.name }}',
|
||||
name: 'host',
|
||||
well: false,
|
||||
formLabelSize: 'col-lg-3',
|
||||
formFieldSize: 'col-lg-9',
|
||||
|
||||
iterator: 'host',
|
||||
headerFields:{
|
||||
enabled: {
|
||||
class: 'Form-header-field',
|
||||
ngClick: 'toggleHostEnabled(host)',
|
||||
type: 'toggle',
|
||||
editRequired: false,
|
||||
awToolTip: "<p>Indicates if a host is available and should be included in running jobs.</p><p>For hosts that " +
|
||||
"are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.</p>",
|
||||
dataTitle: 'Host Enabled',
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
name: {
|
||||
label: 'Host Name',
|
||||
@ -43,19 +54,6 @@ export default
|
||||
addRequired: false,
|
||||
editRequired: false
|
||||
},
|
||||
enabled: {
|
||||
label: 'Enabled?',
|
||||
type: 'checkbox',
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
"default": true,
|
||||
awPopOver: "<p>Indicates if a host is available and should be included in running jobs.</p><p>For hosts that " +
|
||||
"are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.</p>",
|
||||
dataTitle: 'Host Enabled',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: 'body',
|
||||
ngDisabled: 'has_inventory_sources == true'
|
||||
},
|
||||
variables: {
|
||||
label: 'Variables',
|
||||
type: 'textarea',
|
||||
@ -82,17 +80,15 @@ export default
|
||||
}
|
||||
},
|
||||
|
||||
buttons: { //for now always generates <button> tags
|
||||
/*
|
||||
buttons: {
|
||||
save: {
|
||||
ngClick: 'formSave()', //$scope.function to call on click, optional
|
||||
ngDisabled: true //Disable when $pristine or $invalid, optional
|
||||
ngClick: 'formSave()',
|
||||
ngDisabled: true
|
||||
},
|
||||
reset: {
|
||||
ngClick: 'formReset()',
|
||||
ngDisabled: true //Disabled when $pristine
|
||||
cancel: {
|
||||
ngClick: 'formCancel()',
|
||||
ngDisabled: true
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
related: {}
|
||||
|
||||
@ -50,7 +50,12 @@ export default
|
||||
" syntax, test environment setup and report problems.</p>",
|
||||
dataTitle: 'Job Type',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body"
|
||||
dataContainer: "body",
|
||||
subCheckbox: {
|
||||
variable: 'ask_job_type_on_launch',
|
||||
ngShow: "!job_type.value || job_type.value !== 'scan'",
|
||||
text: 'Prompt on launch'
|
||||
}
|
||||
},
|
||||
inventory: {
|
||||
label: 'Inventory',
|
||||
@ -59,14 +64,20 @@ export default
|
||||
sourceField: 'name',
|
||||
ngClick: 'lookUpInventory()',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "inventoryrequired",
|
||||
init: "true"
|
||||
reqExpression: '!ask_inventory_on_launch',
|
||||
alwaysShowAsterisk: true
|
||||
},
|
||||
requiredErrorMsg: "Please select an Inventory or check the Prompt on launch option.",
|
||||
column: 1,
|
||||
awPopOver: "<p>Select the inventory containing the hosts you want this job to manage.</p>",
|
||||
dataTitle: 'Inventory',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body"
|
||||
dataContainer: "body",
|
||||
subCheckbox: {
|
||||
variable: 'ask_inventory_on_launch',
|
||||
ngShow: "!job_type.value || job_type.value !== 'scan'",
|
||||
text: 'Prompt on launch'
|
||||
}
|
||||
},
|
||||
project: {
|
||||
label: 'Project',
|
||||
@ -90,7 +101,7 @@ export default
|
||||
ngOptions: 'book for book in playbook_options track by book',
|
||||
id: 'playbook-select',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "playbookrequired",
|
||||
reqExpression: "playbookrequired",
|
||||
init: "true"
|
||||
},
|
||||
column: 1,
|
||||
@ -111,14 +122,21 @@ export default
|
||||
sourceModel: 'credential',
|
||||
sourceField: 'name',
|
||||
ngClick: 'lookUpCredential()',
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
awRequiredWhen: {
|
||||
reqExpression: '!ask_credential_on_launch',
|
||||
alwaysShowAsterisk: true
|
||||
},
|
||||
requiredErrorMsg: "Please select a Machine Credential or check the Prompt on launch option.",
|
||||
column: 1,
|
||||
awPopOver: "<p>Select the credential you want the job to use when accessing the remote hosts. Choose the credential containing " +
|
||||
" the username and SSH key or password that Ansible will need to log into the remote hosts.</p>",
|
||||
dataTitle: 'Credential',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body"
|
||||
dataContainer: "body",
|
||||
subCheckbox: {
|
||||
variable: 'ask_credential_on_launch',
|
||||
text: 'Prompt on launch'
|
||||
}
|
||||
},
|
||||
cloud_credential: {
|
||||
label: 'Cloud Credential',
|
||||
@ -165,7 +183,11 @@ export default
|
||||
"<a href=\"http://docs.ansible.com/intro_patterns.html\" target=\"_blank\">the Patterns topic at docs.ansible.com</a>.</p>",
|
||||
dataTitle: 'Limit',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body"
|
||||
dataContainer: "body",
|
||||
subCheckbox: {
|
||||
variable: 'ask_limit_on_launch',
|
||||
text: 'Prompt on launch'
|
||||
}
|
||||
},
|
||||
verbosity: {
|
||||
label: 'Verbosity',
|
||||
@ -196,7 +218,11 @@ export default
|
||||
"in the Job Tags field:</p>\n<blockquote>configuration,packages</blockquote>\n",
|
||||
dataTitle: "Job Tags",
|
||||
dataPlacement: "right",
|
||||
dataContainer: "body"
|
||||
dataContainer: "body",
|
||||
subCheckbox: {
|
||||
variable: 'ask_tags_on_launch',
|
||||
text: 'Prompt on launch'
|
||||
}
|
||||
},
|
||||
labels: {
|
||||
label: 'Labels',
|
||||
@ -227,20 +253,11 @@ export default
|
||||
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n",
|
||||
dataTitle: 'Extra Variables',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body"
|
||||
},
|
||||
ask_variables_on_launch: {
|
||||
label: 'Prompt for Extra Variables',
|
||||
type: 'checkbox',
|
||||
addRequired: false,
|
||||
editRequird: false,
|
||||
trueValue: 'true',
|
||||
falseValue: 'false',
|
||||
column: 2,
|
||||
awPopOver: "<p>If checked, user will be prompted at job launch with a dialog allowing override of the extra variables setting.</p>",
|
||||
dataPlacement: 'right',
|
||||
dataTitle: 'Prompt for Extra Variables',
|
||||
dataContainer: "body"
|
||||
dataContainer: "body",
|
||||
subCheckbox: {
|
||||
variable: 'ask_variables_on_launch',
|
||||
text: 'Prompt on launch'
|
||||
}
|
||||
},
|
||||
become_enabled: {
|
||||
label: 'Enable Privilege Escalation',
|
||||
|
||||
@ -46,100 +46,6 @@ export default
|
||||
},
|
||||
|
||||
related: {
|
||||
|
||||
users: {
|
||||
type: 'collection',
|
||||
title: 'Users',
|
||||
iterator: 'user',
|
||||
index: false,
|
||||
open: false,
|
||||
|
||||
actions: {
|
||||
add: {
|
||||
ngClick: "add('users')",
|
||||
label: 'Add',
|
||||
awToolTip: 'Add a new user',
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ 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: '+ ADD'
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
username: {
|
||||
key: true,
|
||||
label: 'Username'
|
||||
},
|
||||
first_name: {
|
||||
label: 'First Name'
|
||||
},
|
||||
last_name: {
|
||||
label: 'Last Name'
|
||||
}
|
||||
},
|
||||
fieldActions: { // Actions available on each row
|
||||
edit: {
|
||||
label: 'Edit',
|
||||
ngClick: "edit('users', admin.id, admin.username)",
|
||||
icon: 'icon-edit',
|
||||
awToolTip: 'Edit administrator',
|
||||
'class': 'btn-default'
|
||||
},
|
||||
"delete": {
|
||||
label: 'Delete',
|
||||
ngClick: "delete('admins', admin.id, admin.username, 'administrator')",
|
||||
icon: 'icon-trash',
|
||||
"class": 'btn-danger',
|
||||
awToolTip: 'Remove administrator'
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: {
|
||||
type: 'collection',
|
||||
title: 'Permissions',
|
||||
|
||||
@ -59,11 +59,10 @@ export default
|
||||
},
|
||||
|
||||
related: {
|
||||
/*
|
||||
permissions: {
|
||||
access_list: {
|
||||
basePath: 'teams/:id/access_list/',
|
||||
type: 'collection',
|
||||
title: 'Permissions',
|
||||
title: 'Users',
|
||||
iterator: 'permission',
|
||||
index: false,
|
||||
open: false,
|
||||
@ -76,148 +75,59 @@ export default
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ 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: '+ 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: '+ 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: '+ ADD'
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
username: {
|
||||
key: true,
|
||||
label: 'Username'
|
||||
label: 'User',
|
||||
linkBase: 'users',
|
||||
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
|
||||
},
|
||||
first_name: {
|
||||
label: 'First Name'
|
||||
},
|
||||
last_name: {
|
||||
label: 'Last Name'
|
||||
}
|
||||
},
|
||||
|
||||
fieldActions: {
|
||||
edit: {
|
||||
label: 'Edit',
|
||||
ngClick: "edit('users', user.id, user.username)",
|
||||
icon: 'icon-edit',
|
||||
awToolTip: 'Edit user',
|
||||
'class': 'btn btn-default'
|
||||
},
|
||||
"delete": {
|
||||
label: 'Delete',
|
||||
ngClick: "delete('users', user.id, user.username, 'user')",
|
||||
icon: 'icon-terash',
|
||||
"class": 'btn-danger',
|
||||
awToolTip: 'Remove user'
|
||||
role: {
|
||||
label: 'Role',
|
||||
type: 'role',
|
||||
noSort: true,
|
||||
class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8'
|
||||
}
|
||||
}
|
||||
},
|
||||
roles: {
|
||||
type: 'collection',
|
||||
title: 'Permissions',
|
||||
iterator: 'role',
|
||||
open: false,
|
||||
index: false,
|
||||
actions: {},
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
label: 'Name',
|
||||
ngBind: 'role.summary_fields.resource_name',
|
||||
linkTo: '{{convertApiUrl(role.related[role.summary_fields.resource_type])}}',
|
||||
noSort: true
|
||||
},
|
||||
type: {
|
||||
label: 'Type',
|
||||
ngBind: 'role.summary_fields.resource_type_display_name',
|
||||
noSort: true
|
||||
},
|
||||
role: {
|
||||
label: 'Role',
|
||||
ngBind: 'role.name',
|
||||
noSort: true
|
||||
}
|
||||
},
|
||||
fieldActions: {
|
||||
"delete": {
|
||||
label: 'Remove',
|
||||
ngClick: 'deletePermissionFromTeam(team_id, team_obj.name, role.name, role.summary_fields.resource_name, role.related.teams)',
|
||||
class: "List-actionButton--delete",
|
||||
iconClass: 'fa fa-times',
|
||||
awToolTip: 'Dissasociate permission from team'
|
||||
}
|
||||
},
|
||||
hideOnSuperuser: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
}); //InventoryForm
|
||||
|
||||
@ -115,71 +115,6 @@ export default
|
||||
},
|
||||
|
||||
related: {
|
||||
/*
|
||||
permissions: {
|
||||
basePath: 'teams/:id/access_list/',
|
||||
type: 'collection',
|
||||
title: 'Permissions',
|
||||
iterator: 'permission',
|
||||
index: false,
|
||||
open: false,
|
||||
searchType: 'select',
|
||||
actions: {
|
||||
add: {
|
||||
ngClick: "addPermission",
|
||||
label: 'Add',
|
||||
awToolTip: 'Add a permission',
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ 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: '+ ADD'
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'Name'
|
||||
},
|
||||
description: {
|
||||
label: 'Description'
|
||||
}
|
||||
},
|
||||
|
||||
fieldActions: {
|
||||
edit: {
|
||||
label: 'Edit',
|
||||
ngClick: "edit('credentials', credential.id, credential.name)",
|
||||
icon: 'icon-edit',
|
||||
awToolTip: 'Edit the credential',
|
||||
'class': 'btn btn-default'
|
||||
},
|
||||
"delete": {
|
||||
label: 'Delete',
|
||||
ngClick: "delete('credentials', credential.id, credential.name, 'credential')",
|
||||
icon: 'icon-trash',
|
||||
"class": 'btn-danger',
|
||||
awToolTip: 'Delete the credential'
|
||||
}
|
||||
}
|
||||
},
|
||||
organizations: {
|
||||
type: 'collection',
|
||||
title: 'Organizations',
|
||||
@ -197,9 +132,9 @@ export default
|
||||
description: {
|
||||
label: 'Description'
|
||||
}
|
||||
}
|
||||
},
|
||||
hideOnSuperuser: true
|
||||
},
|
||||
|
||||
teams: {
|
||||
type: 'collection',
|
||||
title: 'Teams',
|
||||
@ -217,8 +152,44 @@ export default
|
||||
description: {
|
||||
label: 'Description'
|
||||
}
|
||||
}
|
||||
},
|
||||
hideOnSuperuser: true
|
||||
},
|
||||
roles: {
|
||||
hideSearchAndActions: true,
|
||||
type: 'collection',
|
||||
title: 'Permissions',
|
||||
iterator: 'permission',
|
||||
open: false,
|
||||
index: false,
|
||||
fields: {
|
||||
name: {
|
||||
label: 'Name',
|
||||
ngBind: 'permission.summary_fields.resource_name',
|
||||
linkTo: '{{convertApiUrl(permission.related[permission.summary_fields.resource_type])}}',
|
||||
noSort: true
|
||||
},
|
||||
type: {
|
||||
label: 'Type',
|
||||
ngBind: 'permission.summary_fields.resource_type_display_name',
|
||||
noSort: true
|
||||
},
|
||||
role: {
|
||||
label: 'Role',
|
||||
ngBind: 'permission.name',
|
||||
noSort: true
|
||||
},
|
||||
},
|
||||
fieldActions: {
|
||||
"delete": {
|
||||
label: 'Remove',
|
||||
ngClick: 'deletePermissionFromUser(user_id, username, permission.name, permission.summary_fields.resource_name, permission.related.users)',
|
||||
iconClass: 'fa fa-times',
|
||||
awToolTip: 'Dissasociate permission from user'
|
||||
}
|
||||
},
|
||||
hideOnSuperuser: true
|
||||
}
|
||||
}
|
||||
|
||||
}); //UserForm
|
||||
});
|
||||
|
||||
@ -88,6 +88,8 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
break;
|
||||
case 'ssh':
|
||||
scope.usernameLabel = 'Username'; //formally 'SSH Username'
|
||||
scope.becomeUsernameLabel = 'Privilege Escalation Username';
|
||||
scope.becomePasswordLabel = 'Privilege Escalation Password';
|
||||
break;
|
||||
case 'scm':
|
||||
scope.sshKeyDataLabel = 'SCM Private Key';
|
||||
@ -109,13 +111,20 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
"as: </p><p>adjective-noun-000</p>";
|
||||
break;
|
||||
case 'azure':
|
||||
scope.usernameLabel = "Subscription ID";
|
||||
scope.sshKeyDataLabel = 'Management Certificate';
|
||||
scope.subscription_required = true;
|
||||
scope.key_required = true;
|
||||
scope.key_description = "Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console.";
|
||||
scope.key_hint= "drag and drop a management certificate file on the field below";
|
||||
break;
|
||||
case 'azure_rm':
|
||||
scope.usernameLabel = "Username";
|
||||
scope.subscription_required = true;
|
||||
scope.username_required = true;
|
||||
scope.password_required = true;
|
||||
scope.passwordLabel = 'Password';
|
||||
scope.azure_rm_required = true;
|
||||
break;
|
||||
case 'vmware':
|
||||
scope.username_required = true;
|
||||
scope.host_required = true;
|
||||
@ -137,6 +146,26 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
scope.hostPopOver = "<p>The host to authenticate with." +
|
||||
"<br />For example, https://openstack.business.com/v2.0/";
|
||||
break;
|
||||
case 'foreman':
|
||||
scope.username_required = true;
|
||||
scope.password_required = true;
|
||||
scope.passwordLabel = 'Password';
|
||||
scope.host_required = true;
|
||||
scope.hostLabel = "Satellite 6 Host";
|
||||
break;
|
||||
case 'cloudforms':
|
||||
scope.username_required = true;
|
||||
scope.password_required = true;
|
||||
scope.passwordLabel = 'Password';
|
||||
scope.host_required = true;
|
||||
scope.hostLabel = "CloudForms Host";
|
||||
break;
|
||||
case 'net':
|
||||
scope.username_required = true;
|
||||
scope.password_required = true;
|
||||
scope.passwordLabel = 'Password';
|
||||
scope.sshKeyDataLabel = 'SSH Key';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,7 +234,7 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' &&
|
||||
fld !== 'ssh_password') {
|
||||
if (fld === "organization" && !scope[fld]) {
|
||||
data["user"] = $rootScope.current_user.id;
|
||||
data.user = $rootScope.current_user.id;
|
||||
} else if (scope[fld] === null) {
|
||||
data[fld] = "";
|
||||
} else {
|
||||
@ -238,7 +267,7 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
data.project = scope.project;
|
||||
break;
|
||||
case 'azure':
|
||||
data.username = scope.subscription_id;
|
||||
data.username = scope.subscription;
|
||||
}
|
||||
|
||||
Wait('start');
|
||||
|
||||
@ -281,7 +281,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
CreateSelect2({
|
||||
element: '#source_source_regions'
|
||||
});
|
||||
} else if (scope.source.value === 'azure') {
|
||||
} else if (scope.source.value === 'azure' || scope.source.value === 'azure_rm') {
|
||||
scope.source_region_choices = scope.azure_regions;
|
||||
$('#source_form').addClass('squeeze');
|
||||
CreateSelect2({
|
||||
@ -312,8 +312,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
}
|
||||
if (scope.source.value === 'rax' ||
|
||||
scope.source.value === 'ec2' ||
|
||||
scope.source.value==='gce' ||
|
||||
scope.source.value ==='gce' ||
|
||||
scope.source.value === 'foreman' ||
|
||||
scope.source.value ==='cloudforms' ||
|
||||
scope.source.value === 'azure' ||
|
||||
scope.source.value === 'azure_rm' ||
|
||||
scope.source.value === 'vmware' ||
|
||||
scope.source.value === 'openstack') {
|
||||
if (scope.source.value === 'ec2') {
|
||||
@ -1008,7 +1011,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
}
|
||||
else if(fld === "inventory_script"){
|
||||
// the API stores it as 'source_script', we call it inventory_script
|
||||
data.summary_fields['inventory_script'] = data.summary_fields.source_script;
|
||||
data.summary_fields.inventory_script = data.summary_fields.source_script;
|
||||
sources_scope.inventory_script = data.source_script;
|
||||
master.inventory_script = sources_scope.inventory_script;
|
||||
} else if (fld === "source_regions") {
|
||||
|
||||
@ -236,47 +236,6 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name,
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('ToggleHostEnabled', [ 'GetBasePath', 'Rest', 'Wait', 'ProcessErrors', 'Alert', 'Find', 'SetEnabledMsg',
|
||||
function(GetBasePath, Rest, Wait, ProcessErrors, Alert, Find, SetEnabledMsg) {
|
||||
return function(params) {
|
||||
|
||||
var id = params.host_id,
|
||||
external_source = params.external_source,
|
||||
parent_scope = params.parent_scope,
|
||||
host_scope = params.host_scope,
|
||||
host;
|
||||
|
||||
function setMsg(host) {
|
||||
host.enabled = (host.enabled) ? false : true;
|
||||
host.enabled_flag = host.enabled;
|
||||
SetEnabledMsg(host);
|
||||
}
|
||||
|
||||
if (!external_source) {
|
||||
// Host is not managed by an external source
|
||||
Wait('start');
|
||||
host = Find({ list: host_scope.hosts, key: 'id', val: id });
|
||||
setMsg(host);
|
||||
|
||||
Rest.setUrl(GetBasePath('hosts') + id + '/');
|
||||
Rest.put(host)
|
||||
.success( function() {
|
||||
Wait('stop');
|
||||
})
|
||||
.error( function(data, status) {
|
||||
// Flip the enabled flag back
|
||||
setMsg(host);
|
||||
ProcessErrors(parent_scope, data, status, null,
|
||||
{ hdr: 'Error!', msg: 'Failed to update host. PUT returned status: ' + status });
|
||||
});
|
||||
}
|
||||
else {
|
||||
Alert('Action Not Allowed', 'This host is managed by an external cloud source. Disable it at the external source, ' +
|
||||
'then run an inventory sync to update Tower with the new status.', 'alert-info');
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('HostsList', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'HostList', 'generateList',
|
||||
'Prompt', 'SearchInit', 'PaginateInit', 'ProcessErrors', 'GetBasePath', 'HostsAdd', 'HostsReload', 'SelectionInit',
|
||||
function($rootScope, $location, $log, $stateParams, Rest, Alert, HostList, GenerateList, Prompt, SearchInit,
|
||||
|
||||
@ -351,29 +351,30 @@ export default
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('UpdateJobStatus', ['GetElapsed', 'Empty', 'JobIsFinished', function(GetElapsed, Empty, JobIsFinished) {
|
||||
.factory('UpdateJobStatus', ['GetElapsed', 'Empty', 'JobIsFinished', 'longDateFilter', function(GetElapsed, Empty, JobIsFinished, longDateFilter) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
failed = params.failed,
|
||||
modified = params.modified,
|
||||
started = params.started;
|
||||
started = params.started,
|
||||
finished = params.finished;
|
||||
|
||||
if (failed && scope.job_status.status !== 'failed' && scope.job_status.status !== 'error' &&
|
||||
scope.job_status.status !== 'canceled') {
|
||||
scope.job_status.status = 'failed';
|
||||
}
|
||||
if (JobIsFinished(scope) && !Empty(modified)) {
|
||||
scope.job_status.finished = longDateFilter(modified)
|
||||
scope.job_status.finished = longDateFilter(modified);
|
||||
}
|
||||
if (!Empty(started) && Empty(scope.job_status.started)) {
|
||||
scope.job_status.started = longDateFilter(modified)
|
||||
scope.job_status.started = longDateFilter(modified);
|
||||
}
|
||||
if (!Empty(scope.job_status.finished) && !Empty(scope.job_status.started)) {
|
||||
scope.job_status.elapsed = GetElapsed({
|
||||
start: started,
|
||||
end: finished
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
@ -900,8 +901,7 @@ export default
|
||||
.factory('SelectTask', ['JobDetailService', function(JobDetailService) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
id = params.id,
|
||||
callback = params.callback;
|
||||
id = params.id;
|
||||
|
||||
scope.selectedTask = id;
|
||||
scope.tasks.forEach(function(task, idx) {
|
||||
@ -912,11 +912,11 @@ export default
|
||||
scope.tasks[idx].taskActiveClass = '';
|
||||
}
|
||||
});
|
||||
var params = {
|
||||
params = {
|
||||
parent: scope.selectedTask,
|
||||
event__startswith: 'runner',
|
||||
page_size: scope.hostResultsMaxRows,
|
||||
order: 'host_name,counter',
|
||||
order: 'host_name,counter',
|
||||
};
|
||||
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
|
||||
scope.hostResults = JobDetailService.processHostEvents(res.results);
|
||||
|
||||
@ -804,7 +804,7 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
|
||||
if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) ||
|
||||
(base === 'home')){
|
||||
// use $state.go with reload: true option to re-instantiate sockets in
|
||||
$state.go('jobDetail', {id: job}, {reload: true})
|
||||
$state.go('jobDetail', {id: job}, {reload: true});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user