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

This commit is contained in:
AlanCoding 2016-09-26 13:19:12 -04:00
commit e6273ce46e
92 changed files with 2051 additions and 922 deletions

View File

@ -298,12 +298,10 @@ requirements_tower_dev:
# Install third-party requirements needed for running unittests in jenkins
requirements_jenkins:
if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
$(VENV_BASE)/tower/bin/pip install -Ir requirements/requirements_jenkins.txt; \
. $(VENV_BASE)/tower/bin/activate && pip install -Ir requirements/requirements_jenkins.txt; \
else \
pip install -Ir requirements/requirements_jenkins.txt; \
fi && \
$(NPM_BIN) install csslint
fi
requirements: requirements_ansible requirements_tower
@ -317,8 +315,8 @@ develop:
pip uninstall -y awx; \
$(PYTHON) setup.py develop; \
else \
sudo pip uninstall -y awx; \
sudo $(PYTHON) setup.py develop; \
pip uninstall -y awx; \
$(PYTHON) setup.py develop; \
fi
version_file:
@ -448,7 +446,7 @@ pylint: reports
check: flake8 pep8 # pyflakes pylint
TEST_DIRS=awx/main/tests
TEST_DIRS ?= awx/main/tests
# Run all API unit tests.
test:
@if [ "$(VENV_BASE)" ]; then \

View File

@ -1,199 +0,0 @@
import '../support/node';
import adhocModule from 'inventories/manage/adhoc/main';
import RestStub from '../support/rest-stub';
describe("adhoc.controller", function() {
var $scope, $rootScope, $location, $stateParams, $stateExtender,
CheckPasswords, PromptForPasswords, CreateLaunchDialog, AdhocForm,
GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices,
KindChange, LookUpInit, CredentialList, Empty, Wait;
var $controller, ctrl, generateFormCallback, waitCallback, locationCallback,
getBasePath, processErrorsCallback, restCallback, stateExtenderCallback;
beforeEach("instantiate the adhoc module", function() {
angular.mock.module(adhocModule.name);
});
before("create spies", function() {
getBasePath = function(path) {
return '/' + path + '/';
};
generateFormCallback = {
inject: angular.noop
};
waitCallback = sinon.spy();
locationCallback = {
path: sinon.spy()
};
processErrorsCallback = sinon.spy();
restCallback = new RestStub();
stateExtenderCallback = {
addState: angular.noop
}
});
beforeEach("mock dependencies", angular.mock.module(['$provide', function(_provide_) {
var $provide = _provide_;
$provide.value('$location', locationCallback);
$provide.value('CheckPasswords', angular.noop);
$provide.value('PromptForPasswords', angular.noop);
$provide.value('CreateLaunchDialog', angular.noop);
$provide.value('AdhocForm', angular.noop);
$provide.value('GenerateForm', generateFormCallback);
$provide.value('Rest', restCallback);
$provide.value('ProcessErrors', processErrorsCallback);
$provide.value('ClearScope', angular.noop);
$provide.value('GetBasePath', getBasePath);
$provide.value('GetChoices', angular.noop);
$provide.value('KindChange', angular.noop);
$provide.value('LookUpInit', angular.noop);
$provide.value('CredentialList', angular.noop);
$provide.value('Empty', angular.noop);
$provide.value('Wait', waitCallback);
$provide.value('$stateExtender', stateExtenderCallback);
$provide.value('$stateParams', angular.noop);
$provide.value('$state', angular.noop);
}]));
beforeEach("put the controller in scope", inject(function($rootScope, $controller) {
var scope = $rootScope.$new();
ctrl = $controller('adhocController', {$scope: scope});
}));
beforeEach("put $q in scope", window.inject(['$q', function($q) {
restCallback.$q = $q;
}]));
/*
describe("setAvailableUrls", function() {
it('should only have the specified urls ' +
'available for adhoc commands', function() {
var urls = ctrl.privateFn.setAvailableUrls();
expect(urls).to.have.keys('adhocUrl', 'inventoryUrl',
'machineCredentialUrl');
var count = 0;
var i;
for (i in urls) {
if (urls.hasOwnProperty(i)) {
count++;
}
}
expect(count).to.equal(3);
});
});
describe("setFieldDefaults", function() {
it('should set the select form field defaults' +
'based on user settings', function() {
var verbosity_options = [
{label: "0 (Foo)", value: 0, name: "0 (Foo)",
isDefault: false},
{label: "1 (Bar)", value: 1, name: "1 (Bar)",
isDefault: true},
],
forks_field = {};
forks_field.default = 3;
$scope.$apply(function() {
ctrl.privateFn.setFieldDefaults(verbosity_options,
forks_field.default);
});
expect($scope.forks).to.equal(forks_field.default);
expect($scope.verbosity.value).to.equal(1);
});
});
describe("setLoadingStartStop", function() {
it('should start the controller working state when the form is ' +
'loading', function() {
waitCallback.reset();
ctrl.privateFn.setLoadingStartStop();
expect(waitCallback).to.have.been.calledWith("start");
});
it('should stop the indicator after all REST calls in the form load have ' +
'completed', function() {
var forks_field = {},
adhoc_verbosity_options = {};
forks_field.default = "1";
$scope.$apply(function() {
$scope.forks_field = forks_field;
$scope.adhoc_verbosity_options = adhoc_verbosity_options;
});
waitCallback.reset();
$scope.$emit('adhocFormReady');
$scope.$emit('adhocFormReady');
expect(waitCallback).to.have.been.calledWith("stop");
});
});
describe("instantiateArgumentHelp", function() {
it("should initially provide a canned argument help response", function() {
expect($scope.argsPopOver).to.equal('<p>These arguments are used ' +
'with the specified module.</p>');
});
it("should change the help response when the module changes", function() {
$scope.$apply(function () {
$scope.module_name = {value: 'foo'};
});
expect($scope.argsPopOver).to.equal('<p>These arguments are used ' +
'with the specified module. You can find information about ' +
'the foo module <a ' +
'id=\"adhoc_module_arguments_docs_link_for_module_foo\" ' +
'href=\"http://docs.ansible.com/foo_module.html\" ' +
'target=\"_blank\">here</a>.</p>');
});
it("should change the help response when the module changes again", function() {
$scope.$apply(function () {
$scope.module_name = {value: 'bar'};
});
expect($scope.argsPopOver).to.equal('<p>These arguments are used ' +
'with the specified module. You can find information about ' +
'the bar module <a ' +
'id=\"adhoc_module_arguments_docs_link_for_module_bar\" ' +
'href=\"http://docs.ansible.com/bar_module.html\" ' +
'target=\"_blank\">here</a>.</p>');
});
it("should change the help response back to the canned response " +
"when no module is selected", function() {
$scope.$apply(function () {
$scope.module_name = null;
});
expect($scope.argsPopOver).to.equal('<p>These arguments are used ' +
'with the specified module.</p>');
});
});
describe("instantiateHostPatterns", function() {
it("should initialize the limit object based on the provided host " +
"pattern", function() {
ctrl.privateFn.instantiateHostPatterns("foo:bar");
expect($scope.limit).to.equal("foo:bar");
});
it("should set the providedHostPatterns variable to the provided host " +
"pattern so it is accesible on form reset", function() {
ctrl.privateFn.instantiateHostPatterns("foo:bar");
expect($scope.providedHostPatterns).to.equal("foo:bar");
});
it("should remove the hostPattern from rootScope after it has been " +
"utilized", function() {
$rootScope.hostPatterns = "foo";
expect($rootScope.hostPatterns).to.exist;
ctrl.privateFn.instantiateHostPatterns("foo");
expect($rootScope.hostPatterns).to.not.exist;
});
});
*/
});

View File

@ -235,6 +235,13 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
def get_queryset(self):
return self.request.user.get_queryset(self.model)
def paginate_queryset(self, queryset):
page = super(ListAPIView, self).paginate_queryset(queryset)
# Queries RBAC info & stores into list objects
if hasattr(self, 'capabilities_prefetch') and page is not None:
cache_list_capabilities(page, self.capabilities_prefetch, self.model, self.request.user)
return page
def get_description_context(self):
opts = self.model._meta
if 'username' in opts.get_all_field_names():

View File

@ -49,6 +49,9 @@ class ModelAccessPermission(permissions.BasePermission):
if not check_user_access(request.user, view.parent_model, 'read',
parent_obj):
return False
if hasattr(view, 'parent_key'):
if not check_user_access(request.user, view.model, 'add', {view.parent_key: parent_obj.pk}):
return False
return True
elif getattr(view, 'is_job_start', False):
if not obj:
@ -206,6 +209,8 @@ class ProjectUpdatePermission(ModelAccessPermission):
class UserPermission(ModelAccessPermission):
def check_post_permissions(self, request, view, obj=None):
if request.user.is_superuser:
if not request.data:
return request.user.admin_of_organizations.exists()
elif request.user.is_superuser:
return True
raise PermissionDenied()

View File

@ -37,6 +37,7 @@ from polymorphic import PolymorphicModel
# AWX
from awx.main.constants import SCHEDULEABLE_PROVIDERS
from awx.main.models import * # noqa
from awx.main.access import get_user_capabilities
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, getattrd
from awx.main.conf import tower_settings
@ -345,6 +346,19 @@ class BaseSerializer(serializers.ModelSerializer):
}
if len(roles) > 0:
summary_fields['object_roles'] = roles
# Advance display of RBAC capabilities
if hasattr(self, 'show_capabilities'):
view = self.context.get('view', None)
parent_obj = None
if view and hasattr(view, 'parent_model'):
parent_obj = view.get_parent_object()
if view and view.request and view.request.user:
user_capabilities = get_user_capabilities(
view.request.user, obj, method_list=self.show_capabilities, parent_obj=parent_obj)
if user_capabilities:
summary_fields['user_capabilities'] = user_capabilities
return summary_fields
def get_created(self, obj):
@ -553,6 +567,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
class UnifiedJobSerializer(BaseSerializer):
show_capabilities = ['start', 'delete']
result_stdout = serializers.SerializerMethodField()
@ -697,11 +712,12 @@ class UserSerializer(BaseSerializer):
ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
external_account = serializers.SerializerMethodField(help_text='Set if the account is managed by an external service')
is_system_auditor = serializers.BooleanField(default=False)
show_capabilities = ['edit', 'delete']
class Meta:
model = User
fields = ('*', '-name', '-description', '-modified',
'-summary_fields', 'username', 'first_name', 'last_name',
'username', 'first_name', 'last_name',
'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn', 'external_account')
def to_representation(self, obj):
@ -822,6 +838,7 @@ class UserSerializer(BaseSerializer):
class OrganizationSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete']
class Meta:
model = Organization
@ -906,6 +923,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
status = serializers.ChoiceField(choices=Project.PROJECT_STATUS_CHOICES, read_only=True)
last_update_failed = serializers.BooleanField(read_only=True)
last_updated = serializers.DateTimeField(read_only=True)
show_capabilities = ['start', 'schedule', 'edit', 'delete']
class Meta:
model = Project
@ -1014,6 +1032,7 @@ class BaseSerializerWithVariables(BaseSerializer):
class InventorySerializer(BaseSerializerWithVariables):
show_capabilities = ['edit', 'delete', 'adhoc']
class Meta:
model = Inventory
@ -1064,12 +1083,14 @@ class InventoryDetailSerializer(InventorySerializer):
class InventoryScriptSerializer(InventorySerializer):
show_capabilities = ['copy', 'edit', 'delete']
class Meta:
fields = ()
class HostSerializer(BaseSerializerWithVariables):
show_capabilities = ['edit', 'delete']
class Meta:
model = Host
@ -1180,6 +1201,7 @@ class HostSerializer(BaseSerializerWithVariables):
class GroupSerializer(BaseSerializerWithVariables):
show_capabilities = ['start', 'copy', 'schedule', 'edit', 'delete']
class Meta:
model = Group
@ -1284,6 +1306,7 @@ class GroupVariableDataSerializer(BaseVariableDataSerializer):
class CustomInventoryScriptSerializer(BaseSerializer):
script = serializers.CharField(trim_whitespace=False)
show_capabilities = ['edit', 'delete']
class Meta:
model = CustomInventoryScript
@ -1454,6 +1477,7 @@ class InventoryUpdateCancelSerializer(InventoryUpdateSerializer):
class TeamSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete']
class Meta:
model = Team
@ -1522,8 +1546,12 @@ class RoleSerializer(BaseSerializer):
return ret
class RoleSerializerWithParentAccess(RoleSerializer):
show_capabilities = ['unattach']
class ResourceAccessListElementSerializer(UserSerializer):
show_capabilities = [] # Clear fields from UserSerializer parent class
def to_representation(self, user):
'''
@ -1539,18 +1567,25 @@ class ResourceAccessListElementSerializer(UserSerializer):
ret = super(ResourceAccessListElementSerializer, self).to_representation(user)
object_id = self.context['view'].object_id
obj = self.context['view'].resource_model.objects.get(pk=object_id)
if self.context['view'].request is not None:
requesting_user = self.context['view'].request.user
else:
requesting_user = None
if 'summary_fields' not in ret:
ret['summary_fields'] = {}
def format_role_perm(role):
role_dict = { 'id': role.id, 'name': role.name, 'description': role.description}
try:
if role.content_type is not None:
role_dict['resource_name'] = role.content_object.name
role_dict['resource_type'] = role.content_type.name
role_dict['related'] = reverse_gfk(role.content_object)
except:
pass
role_dict['user_capabilities'] = {'unattach': requesting_user.can_access(
Role, 'unattach', role, user, 'members', data={}, skip_sub_obj_read_check=False)}
else:
# Singleton roles should not be managed from this view, as per copy/edit rework spec
role_dict['user_capabilities'] = {'unattach': False}
return { 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, role)}
def format_team_role_perm(team_role, permissive_role_ids):
@ -1563,20 +1598,21 @@ class ResourceAccessListElementSerializer(UserSerializer):
'team_id': team_role.object_id,
'team_name': team_role.content_object.name
}
try:
if role.content_type is not None:
role_dict['resource_name'] = role.content_object.name
role_dict['resource_type'] = role.content_type.name
role_dict['related'] = reverse_gfk(role.content_object)
except:
pass
role_dict['user_capabilities'] = {'unattach': requesting_user.can_access(
Role, 'unattach', role, team_role, 'parents', data={}, skip_sub_obj_read_check=False)}
else:
# Singleton roles should not be managed from this view, as per copy/edit rework spec
role_dict['user_capabilities'] = {'unattach': False}
ret.append({ 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, team_role)})
return ret
team_content_type = ContentType.objects.get_for_model(Team)
content_type = ContentType.objects.get_for_model(obj)
content_type = ContentType.objects.get_for_model(obj)
direct_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('id', flat=True)
all_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('ancestors__id', flat=True)
@ -1621,6 +1657,7 @@ class ResourceAccessListElementSerializer(UserSerializer):
class CredentialSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete']
class Meta:
model = Credential
@ -1825,6 +1862,7 @@ class JobOptionsSerializer(BaseSerializer):
class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete']
status = serializers.ChoiceField(choices=JobTemplate.JOB_TEMPLATE_STATUS_CHOICES, read_only=True, required=False)
@ -1860,21 +1898,6 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
d = super(JobTemplateSerializer, self).get_summary_fields(obj)
if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
request = self.context.get('request', None)
# Check for conditions that would create a validation error if coppied
validation_errors, resources_needed_to_start = obj.resource_validation_data()
if request is None or request.user is None:
d['can_copy'] = False
d['can_edit'] = False
elif request.user.is_superuser:
d['can_copy'] = not validation_errors
d['can_edit'] = True
else:
d['can_copy'] = (not validation_errors) and request.user.can_access(JobTemplate, 'add', {"reference_obj": obj})
d['can_edit'] = request.user.can_access(JobTemplate, 'change', obj, {})
d['recent_jobs'] = self._recent_jobs(obj)
return d
@ -2561,6 +2584,7 @@ class JobLaunchSerializer(BaseSerializer):
return attrs
class NotificationTemplateSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete']
class Meta:
model = NotificationTemplate
@ -2674,6 +2698,7 @@ class LabelSerializer(BaseSerializer):
return res
class ScheduleSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete']
class Meta:
model = Schedule

View File

@ -854,7 +854,7 @@ class TeamUsersList(BaseUsersList):
class TeamRolesList(SubListCreateAttachDetachAPIView):
model = Role
serializer_class = RoleSerializer
serializer_class = RoleSerializerWithParentAccess
metadata_class = RoleMetadata
parent_model = Team
relationship='member_role.children'
@ -953,6 +953,7 @@ class ProjectList(ListCreateAPIView):
model = Project
serializer_class = ProjectSerializer
capabilities_prefetch = ['admin', 'update']
def get_queryset(self):
projects_qs = Project.accessible_objects(self.request.user, 'read_role')
@ -1151,6 +1152,7 @@ class UserList(ListCreateAPIView):
model = User
serializer_class = UserSerializer
capabilities_prefetch = ['admin']
permission_classes = (UserPermission,)
def post(self, request, *args, **kwargs):
@ -1191,7 +1193,7 @@ class UserTeamsList(ListAPIView):
class UserRolesList(SubListCreateAttachDetachAPIView):
model = Role
serializer_class = RoleSerializer
serializer_class = RoleSerializerWithParentAccess
metadata_class = RoleMetadata
parent_model = User
relationship='roles'
@ -1511,6 +1513,7 @@ class InventoryList(ListCreateAPIView):
model = Inventory
serializer_class = InventorySerializer
capabilities_prefetch = ['admin', 'adhoc']
def get_queryset(self):
qs = Inventory.accessible_objects(self.request.user, 'read_role')
@ -1742,6 +1745,7 @@ class GroupList(ListCreateAPIView):
model = Group
serializer_class = GroupSerializer
capabilities_prefetch = ['inventory.admin', 'inventory.adhoc', 'inventory.update']
'''
Useful when you have a self-refering ManyToManyRelationship.
@ -2210,6 +2214,10 @@ class JobTemplateList(ListCreateAPIView):
model = JobTemplate
serializer_class = JobTemplateSerializer
always_allow_superuser = False
capabilities_prefetch = [
'admin', 'execute',
{'copy': ['project.use', 'inventory.use', 'credential.use', 'cloud_credential.use', 'network_credential.use']}
]
def post(self, request, *args, **kwargs):
ret = super(JobTemplateList, self).post(request, *args, **kwargs)
@ -3680,6 +3688,7 @@ class NotificationTemplateTest(GenericAPIView):
model = NotificationTemplate
serializer_class = EmptySerializer
new_in_300 = True
is_job_start = True
def post(self, request, *args, **kwargs):
obj = self.get_object()

View File

@ -116,6 +116,18 @@ def check_user_access(user, model_class, action, *args, **kwargs):
return result
return False
def get_user_capabilities(user, instance, **kwargs):
'''
Returns a dictionary of capabilities the user has on the particular
instance. *NOTE* This is not a direct mapping of can_* methods into this
dictionary, it is intended to munge some queries in a way that is
convenient for the user interface to consume and hide or show various
actions in the interface.
'''
for access_class in access_registry.get(type(instance), []):
return access_class(user).get_user_capabilities(instance, **kwargs)
return None
def check_superuser(func):
'''
check_superuser is a decorator that provides a simple short circuit
@ -207,6 +219,78 @@ class BaseAccess(object):
elif "features" not in validation_info:
raise LicenseForbids("Features not found in active license.")
def get_user_capabilities(self, obj, method_list=[], parent_obj=None):
if obj is None:
return {}
user_capabilities = {}
# Custom ordering to loop through methods so we can reuse earlier calcs
for display_method in ['edit', 'delete', 'start', 'schedule', 'copy', 'adhoc', 'unattach']:
if display_method not in method_list:
continue
# Validation consistency checks
if display_method == 'copy' and isinstance(obj, JobTemplate):
validation_errors, resources_needed_to_start = obj.resource_validation_data()
if validation_errors:
user_capabilities[display_method] = False
continue
elif display_method == 'start' and isinstance(obj, Group):
if obj.inventory_source and not obj.inventory_source._can_update():
user_capabilities[display_method] = False
continue
# Grab the answer from the cache, if available
if hasattr(obj, 'capabilities_cache') and display_method in obj.capabilities_cache:
user_capabilities[display_method] = obj.capabilities_cache[display_method]
continue
# Aliases for going form UI language to API language
if display_method == 'edit':
method = 'change'
elif display_method == 'copy':
method = 'add'
elif display_method == 'adhoc':
method = 'run_ad_hoc_commands'
else:
method = display_method
# Shortcuts in certain cases by deferring to earlier property
if display_method == 'schedule':
user_capabilities['schedule'] = user_capabilities['edit']
continue
elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob)):
user_capabilities['delete'] = user_capabilities['edit']
continue
elif display_method == 'copy' and isinstance(obj, (Group, Host)):
user_capabilities['copy'] = user_capabilities['edit']
continue
# Preprocessing before the access method is called
data = {}
if method == 'add':
if isinstance(obj, JobTemplate):
data['reference_obj'] = obj
# Compute permission
access_method = getattr(self, "can_%s" % method)
if method in ['change']: # 3 args
user_capabilities[display_method] = access_method(obj, data)
elif method in ['delete', 'start', 'run_ad_hoc_commands']: # 2 args
user_capabilities[display_method] = access_method(obj)
elif method in ['add']: # 2 args with data
user_capabilities[display_method] = access_method(data)
elif method in ['attach', 'unattach']: # parent/sub-object call
if type(parent_obj) == Team:
relationship = 'parents'
parent_obj = parent_obj.member_role
else:
relationship = 'members'
user_capabilities[display_method] = access_method(
obj, parent_obj, relationship, skip_sub_obj_read_check=True, data=data)
return user_capabilities
class UserAccess(BaseAccess):
'''
@ -526,6 +610,12 @@ class GroupAccess(BaseAccess):
"active_jobs": active_jobs})
return True
def can_start(self, obj):
# Used as another alias to inventory_source start access for user_capabilities
if obj and obj.inventory_source:
return self.user.can_access(InventorySource, 'start', obj.inventory_source)
return False
class InventorySourceAccess(BaseAccess):
'''
I can see inventory sources whenever I can see their group or inventory.
@ -594,6 +684,13 @@ class InventoryUpdateAccess(BaseAccess):
# Inventory cascade deletes to inventory update, descends from org admin
return self.user in obj.inventory_source.inventory.admin_role
def can_start(self, obj):
# For relaunching
if obj and obj.inventory_source:
access = InventorySourceAccess(self.user)
return access.can_start(obj.inventory_source)
return False
@check_superuser
def can_delete(self, obj):
return self.user in obj.inventory_source.inventory.admin_role
@ -815,6 +912,12 @@ class ProjectUpdateAccess(BaseAccess):
# Project updates cascade delete with project, admin role descends from org admin
return self.user in obj.project.admin_role
def can_start(self, obj):
# for relaunching
if obj and obj.project:
return self.user in obj.project.update_role
return False
@check_superuser
def can_delete(self, obj):
return obj and self.user in obj.project.admin_role
@ -855,7 +958,9 @@ class JobTemplateAccess(BaseAccess):
Users who are able to create deploy jobs can also run normal and check (dry run) jobs.
'''
if not data: # So the browseable API will work
return True
return (
Project.accessible_objects(self.user, 'use_role').exists() or
Inventory.accessible_objects(self.user, 'use_role').exists())
# if reference_obj is provided, determine if it can be coppied
reference_obj = data.pop('reference_obj', None)
@ -1132,6 +1237,10 @@ class SystemJobAccess(BaseAccess):
'''
model = SystemJob
def can_start(self, obj):
return False # no relaunching of system jobs
# TODO:
class WorkflowJobTemplateNodeAccess(BaseAccess):
'''
I can see/use a WorkflowJobTemplateNode if I have read permission
@ -1762,6 +1871,12 @@ class NotificationTemplateAccess(BaseAccess):
def can_delete(self, obj):
return self.can_change(obj, None)
@check_superuser
def can_start(self, obj):
if obj.organization is None:
return False
return self.user in obj.organization.admin_role
class NotificationAccess(BaseAccess):
'''
I can see/use a notification if I have permission to
@ -1970,8 +2085,13 @@ class RoleAccess(BaseAccess):
@check_superuser
def can_unattach(self, obj, sub_obj, relationship, data=None, skip_sub_obj_read_check=False):
if not skip_sub_obj_read_check and relationship in ['members', 'member_role.parents']:
if not check_user_access(self.user, sub_obj.__class__, 'read', sub_obj):
if not skip_sub_obj_read_check and relationship in ['members', 'member_role.parents', 'parents']:
# If we are unattaching a team Role, check the Team read access
if relationship == 'parents':
sub_obj_resource = sub_obj.content_object
else:
sub_obj_resource = sub_obj
if not check_user_access(self.user, sub_obj_resource.__class__, 'read', sub_obj_resource):
return False
if isinstance(obj.content_object, ResourceMixin) and \

View File

@ -47,3 +47,4 @@ class Command(BaseCommand):
inventory=i,
credential=c)
print('Default organization added.')
print('Demo Credential, Inventory, and Job Template added.')

View File

@ -340,6 +340,10 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
errors.append("'%s' value missing" % survey_element['variable'])
elif survey_element['type'] in ["textarea", "text", "password"]:
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (str, unicode):
errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']):
errors.append("'%s' value %s is too small (length is %s must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min']))
@ -348,6 +352,10 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'integer':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != int:
errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] < int(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
@ -356,20 +364,18 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
data[survey_element['variable']] > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
if type(data[survey_element['variable']]) != int:
errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']],
survey_element['variable']))
elif survey_element['type'] == 'float':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (float, int):
errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
if type(data[survey_element['variable']]) not in (float, int):
errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']],
survey_element['variable']))
elif survey_element['type'] == 'multiselect':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != list:

View File

@ -177,7 +177,6 @@ class WorkflowJobNode(WorkflowNodeBase):
# TODO: merge artifacts, add ancestor_artifacts to kwargs
if extra_vars:
data['extra_vars'] = extra_vars
print ' job KV data: ' + str(data)
return data
class WorkflowJobOptions(BaseModel):

View File

@ -654,7 +654,8 @@ class BaseTask(Task):
if status == 'canceled':
raise Exception("Task %s(pk:%s) was canceled (rc=%s)" % (str(self.model.__class__), str(pk), str(rc)))
else:
raise Exception("Task %s(pk:%s) encountered an error (rc=%s)" % (str(self.model.__class__), str(pk), str(rc)))
raise Exception("Task %s(pk:%s) encountered an error (rc=%s), please see task stdout for details." %
(str(self.model.__class__), str(pk), str(rc)))
if not hasattr(settings, 'CELERY_UNIT_TEST'):
self.signal_finished(pk)

View File

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

View File

@ -0,0 +1,323 @@
import pytest
from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
from awx.main.models.jobs import JobTemplate
from awx.main.models import Role, Group
from awx.main.access import (
access_registry,
get_user_capabilities
)
from awx.main.utils import cache_list_capabilities
from awx.api.serializers import JobTemplateSerializer
# This file covers special-cases of displays of user_capabilities
# general functionality should be covered fully by unit tests, see:
# awx/main/tests/unit/api/test_serializers.py ::
# TestJobTemplateSerializerGetSummaryFields.test_copy_edit_standard
# awx/main/tests/unit/test_access.py ::
# test_user_capabilities_method
@pytest.mark.django_db
class TestOptionsRBAC:
"""
Several endpoints are relied-upon by the UI to list POST as an
allowed action or not depending on whether the user has permission
to create a resource.
"""
def test_inventory_group_host_can_add(self, inventory, alice, options):
inventory.admin_role.members.add(alice)
response = options(reverse('api:inventory_hosts_list', args=[inventory.pk]), alice)
assert 'POST' in response.data['actions']
response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), alice)
assert 'POST' in response.data['actions']
def test_inventory_group_host_can_not_add(self, inventory, bob, options):
inventory.read_role.members.add(bob)
response = options(reverse('api:inventory_hosts_list', args=[inventory.pk]), bob)
assert 'POST' not in response.data['actions']
response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), bob)
assert 'POST' not in response.data['actions']
def test_user_list_can_add(self, org_member, org_admin, options):
response = options(reverse('api:user_list'), org_admin)
assert 'POST' in response.data['actions']
def test_user_list_can_not_add(self, org_member, org_admin, options):
response = options(reverse('api:user_list'), org_member)
assert 'POST' not in response.data['actions']
@pytest.mark.django_db
class TestJobTemplateCopyEdit:
"""
Tests contain scenarios that were raised as issues in the past,
which resulted from failed copy/edit actions even though the buttons
to do these actions were displayed.
"""
@pytest.fixture
def jt_copy_edit(self, job_template_factory, project):
objects = job_template_factory(
'copy-edit-job-template',
project=project)
return objects.job_template
def fake_context(self, user):
request = RequestFactory().get('/api/v1/resource/42/')
request.user = user
class FakeView(object):
pass
fake_view = FakeView()
fake_view.request = request
context = {}
context['view'] = fake_view
context['request'] = request
return context
def test_validation_bad_data_copy_edit(self, admin_user, project):
"""
If a required resource (inventory here) was deleted, copying not allowed
because doing so would caues a validation error
"""
jt_res = JobTemplate.objects.create(
job_type='run',
project=project,
inventory=None, ask_inventory_on_launch=False, # not allowed
credential=None, ask_credential_on_launch=True,
name='deploy-job-template'
)
serializer = JobTemplateSerializer(jt_res)
serializer.context = self.fake_context(admin_user)
response = serializer.to_representation(jt_res)
assert not response['summary_fields']['user_capabilities']['copy']
assert response['summary_fields']['user_capabilities']['edit']
def test_sys_admin_copy_edit(self, jt_copy_edit, admin_user):
"Absent a validation error, system admins can do everything"
serializer = JobTemplateSerializer(jt_copy_edit)
serializer.context = self.fake_context(admin_user)
response = serializer.to_representation(jt_copy_edit)
assert response['summary_fields']['user_capabilities']['copy']
assert response['summary_fields']['user_capabilities']['edit']
def test_org_admin_copy_edit(self, jt_copy_edit, org_admin):
"Organization admins SHOULD be able to copy a JT firmly in their org"
serializer = JobTemplateSerializer(jt_copy_edit)
serializer.context = self.fake_context(org_admin)
response = serializer.to_representation(jt_copy_edit)
assert response['summary_fields']['user_capabilities']['copy']
assert response['summary_fields']['user_capabilities']['edit']
def test_org_admin_foreign_cred_no_copy_edit(self, jt_copy_edit, org_admin, machine_credential):
"""
Organization admins without access to the 3 related resources:
SHOULD NOT be able to copy JT
SHOULD be able to edit that job template, for nonsensitive changes
"""
# Attach credential to JT that org admin can not use
jt_copy_edit.credential = machine_credential
jt_copy_edit.save()
serializer = JobTemplateSerializer(jt_copy_edit)
serializer.context = self.fake_context(org_admin)
response = serializer.to_representation(jt_copy_edit)
assert not response['summary_fields']['user_capabilities']['copy']
assert response['summary_fields']['user_capabilities']['edit']
def test_jt_admin_copy_edit(self, jt_copy_edit, rando):
"""
JT admins wihout access to associated resources SHOULD NOT be able to copy
SHOULD be able to make nonsensitive changes"""
# random user given JT admin access only
jt_copy_edit.admin_role.members.add(rando)
jt_copy_edit.save()
serializer = JobTemplateSerializer(jt_copy_edit)
serializer.context = self.fake_context(rando)
response = serializer.to_representation(jt_copy_edit)
assert not response['summary_fields']['user_capabilities']['copy']
assert response['summary_fields']['user_capabilities']['edit']
def test_proj_jt_admin_copy_edit(self, jt_copy_edit, rando):
"JT admins with access to associated resources SHOULD be able to copy"
# random user given JT and project admin abilities
jt_copy_edit.admin_role.members.add(rando)
jt_copy_edit.save()
jt_copy_edit.project.admin_role.members.add(rando)
jt_copy_edit.project.save()
serializer = JobTemplateSerializer(jt_copy_edit)
serializer.context = self.fake_context(rando)
response = serializer.to_representation(jt_copy_edit)
assert response['summary_fields']['user_capabilities']['copy']
assert response['summary_fields']['user_capabilities']['edit']
@pytest.fixture
def mock_access_method(mocker):
mock_method = mocker.MagicMock()
mock_method.return_value = 'foobar'
mock_method.__name__ = 'bars' # Required for a logging statement
return mock_method
@pytest.mark.django_db
class TestAccessListCapabilities:
"""
Test that the access_list serializer shows the exact output of the RoleAccess.can_attach
- looks at /api/v1/inventories/N/access_list/
- test for types: direct, indirect, and team access
"""
extra_kwargs = dict(skip_sub_obj_read_check=False, data={})
def _assert_one_in_list(self, data, sublist='direct_access'):
"Establish that exactly 1 type of access exists so we know the entry is the right one"
assert len(data['results']) == 1
assert len(data['results'][0]['summary_fields'][sublist]) == 1
def test_access_list_direct_access_capability(
self, inventory, rando, get, mocker, mock_access_method):
inventory.admin_role.members.add(rando)
with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method):
response = get(reverse('api:inventory_access_list', args=(inventory.id,)), rando)
mock_access_method.assert_called_once_with(inventory.admin_role, rando, 'members', **self.extra_kwargs)
self._assert_one_in_list(response.data)
direct_access_list = response.data['results'][0]['summary_fields']['direct_access']
assert direct_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar'
def test_access_list_indirect_access_capability(
self, inventory, organization, org_admin, get, mocker, mock_access_method):
with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method):
response = get(reverse('api:inventory_access_list', args=(inventory.id,)), org_admin)
mock_access_method.assert_called_once_with(organization.admin_role, org_admin, 'members', **self.extra_kwargs)
self._assert_one_in_list(response.data, sublist='indirect_access')
indirect_access_list = response.data['results'][0]['summary_fields']['indirect_access']
assert indirect_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar'
def test_access_list_team_direct_access_capability(
self, inventory, team, team_member, get, mocker, mock_access_method):
team.member_role.children.add(inventory.admin_role)
with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method):
response = get(reverse('api:inventory_access_list', args=(inventory.id,)), team_member)
mock_access_method.assert_called_once_with(inventory.admin_role, team.member_role, 'parents', **self.extra_kwargs)
self._assert_one_in_list(response.data)
direct_access_list = response.data['results'][0]['summary_fields']['direct_access']
assert direct_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar'
@pytest.mark.django_db
def test_team_roles_unattach(mocker, team, team_member, inventory, mock_access_method, get):
team.member_role.children.add(inventory.admin_role)
with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method):
response = get(reverse('api:team_roles_list', args=(team.id,)), team_member)
# Did we assess whether team_member can remove team's permission to the inventory?
mock_access_method.assert_called_once_with(
inventory.admin_role, team.member_role, 'parents', skip_sub_obj_read_check=True, data={})
assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] == 'foobar'
@pytest.mark.django_db
def test_user_roles_unattach(mocker, organization, alice, bob, mock_access_method, get):
# Add to same organization so that alice and bob can see each other
organization.member_role.members.add(alice)
organization.member_role.members.add(bob)
with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method):
response = get(reverse('api:user_roles_list', args=(alice.id,)), bob)
# Did we assess whether bob can remove alice's permission to the inventory?
mock_access_method.assert_called_once_with(
organization.member_role, alice, 'members', skip_sub_obj_read_check=True, data={})
assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] == 'foobar'
@pytest.mark.django_db
def test_team_roles_unattach_functional(team, team_member, inventory, get):
team.member_role.children.add(inventory.admin_role)
response = get(reverse('api:team_roles_list', args=(team.id,)), team_member)
# Team member should be able to remove access to inventory, becauase
# the inventory admin_role grants that ability
assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach']
@pytest.mark.django_db
def test_user_roles_unattach_functional(organization, alice, bob, get):
organization.member_role.members.add(alice)
organization.member_role.members.add(bob)
response = get(reverse('api:user_roles_list', args=(alice.id,)), bob)
# Org members can not revoke the membership of other members
assert not response.data['results'][0]['summary_fields']['user_capabilities']['unattach']
@pytest.mark.django_db
def test_prefetch_jt_capabilities(job_template, rando):
job_template.execute_role.members.add(rando)
qs = JobTemplate.objects.all()
cache_list_capabilities(qs, ['admin', 'execute'], JobTemplate, rando)
assert qs[0].capabilities_cache == {'edit': False, 'start': True}
@pytest.mark.django_db
def test_prefetch_group_capabilities(group, rando):
group.inventory.adhoc_role.members.add(rando)
qs = Group.objects.all()
cache_list_capabilities(qs, ['inventory.admin', 'inventory.adhoc'], Group, rando)
assert qs[0].capabilities_cache == {'edit': False, 'adhoc': True}
@pytest.mark.django_db
def test_prefetch_jt_copy_capability(job_template, project, inventory, machine_credential, rando):
job_template.project = project
job_template.inventory = inventory
job_template.credential = machine_credential
job_template.save()
qs = JobTemplate.objects.all()
cache_list_capabilities(qs, [{'copy': [
'project.use', 'inventory.use', 'credential.use',
'cloud_credential.use', 'network_credential.use'
]}], JobTemplate, rando)
assert qs[0].capabilities_cache == {'copy': False}
project.use_role.members.add(rando)
inventory.use_role.members.add(rando)
machine_credential.use_role.members.add(rando)
cache_list_capabilities(qs, [{'copy': [
'project.use', 'inventory.use', 'credential.use',
'cloud_credential.use', 'network_credential.use'
]}], JobTemplate, rando)
assert qs[0].capabilities_cache == {'copy': True}
@pytest.mark.django_db
def test_group_update_capabilities_possible(group, inventory_source, admin_user):
group.inventory_source = inventory_source
group.save()
capabilities = get_user_capabilities(admin_user, group, method_list=['start'])
assert capabilities['start']
@pytest.mark.django_db
def test_group_update_capabilities_impossible(group, inventory_source, admin_user):
inventory_source.source = ""
inventory_source.save()
group.inventory_source = inventory_source
group.save()
capabilities = get_user_capabilities(admin_user, group, method_list=['start'])
assert not capabilities['start']

View File

@ -6,9 +6,13 @@ import mock
from awx.api.serializers import (
JobTemplateSerializer,
)
from awx.api.views import JobTemplateDetail
from awx.main.models import (
Role,
User,
Job,
)
from rest_framework.test import APIRequestFactory
#DRF
from rest_framework import serializers
@ -77,21 +81,33 @@ class TestJobTemplateSerializerGetSummaryFields():
summary = get_summary_fields_mock_and_run(JobTemplateSerializer, job_template)
assert 'survey' not in summary
@pytest.mark.skip(reason="RBAC needs to land")
def test_can_copy_true(self, mocker, job_template):
pass
def test_copy_edit_standard(self, mocker, job_template_factory):
"""Verify that the exact output of the access.py methods
are put into the serializer user_capabilities"""
@pytest.mark.skip(reason="RBAC needs to land")
def test_can_copy_false(self, mocker, job_template):
pass
jt_obj = job_template_factory('testJT', project='proj1', persisted=False).job_template
jt_obj.id = 5
jt_obj.admin_role = Role(id=9, role_field='admin_role')
jt_obj.execute_role = Role(id=8, role_field='execute_role')
jt_obj.read_role = Role(id=7, role_field='execute_role')
user = User(username="auser")
serializer = JobTemplateSerializer(job_template)
serializer.show_capabilities = ['copy', 'edit']
serializer._summary_field_labels = lambda self: []
serializer._recent_jobs = lambda self: []
request = APIRequestFactory().get('/api/v1/job_templates/42/')
request.user = user
view = JobTemplateDetail()
view.request = request
serializer.context['view'] = view
@pytest.mark.skip(reason="RBAC needs to land")
def test_can_edit_true(self, mocker, job_template):
pass
with mocker.patch("awx.main.models.rbac.Role.get_description", return_value='Can eat pie'):
with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'):
with mocker.patch("awx.main.access.JobTemplateAccess.can_add", return_value='foo'):
response = serializer.get_summary_fields(jt_obj)
@pytest.mark.skip(reason="RBAC needs to land")
def test_can_edit_false(self, mocker, job_template):
pass
assert response['user_capabilities']['copy'] == 'foo'
assert response['user_capabilities']['edit'] == 'foobar'
class TestJobTemplateSerializerValidation(object):

View File

@ -50,3 +50,32 @@ def test_job_template_survey_password_redaction(job_template_with_survey_passwor
"""Tests the JobTemplate model's funciton to redact passwords from
extra_vars - used when creating a new job"""
assert job_template_with_survey_passwords_unit.survey_password_variables() == ['secret_key', 'SSN']
def test_job_template_survey_variable_validation(job_template_factory):
objects = job_template_factory(
'survey_variable_validation',
organization='org1',
inventory='inventory1',
credential='cred1',
persisted=False,
)
obj = objects.job_template
obj.survey_spec = {
"description": "",
"spec": [
{
"required": True,
"min": 0,
"default": "5",
"max": 1024,
"question_description": "",
"choices": "",
"variable": "a",
"question_name": "Whosyourdaddy",
"type": "text"
}
],
"name": ""
}
obj.survey_enabled = True
assert obj.survey_variable_validation({"a": 5}) == ["Value 5 for 'a' expected to be a string."]

View File

@ -134,3 +134,28 @@ class TestWorkflowAccessMethods:
with mock.patch('awx.main.access.get_object_or_400', mock_get_object):
assert access.can_add({'organization': 1})
@pytest.mark.django_db
def test_user_capabilities_method():
"""Unit test to verify that the user_capabilities method will defer
to the appropriate sub-class methods of the access classes.
Note that normal output is True/False, but a string is returned
in these tests to establish uniqueness.
"""
class FooAccess(BaseAccess):
def can_change(self, obj, data):
return 'bar'
def can_add(self, data):
return 'foobar'
user = User(username='auser')
foo_access = FooAccess(user)
foo = object()
foo_capabilities = foo_access.get_user_capabilities(foo, ['edit', 'copy'])
assert foo_capabilities == {
'edit': 'bar',
'copy': 'foobar'
}

View File

@ -74,6 +74,7 @@ def test_net_cred_ssh_agent(mocker, get_ssh_version):
mocker.patch.object(run_job, 'should_use_proot', return_value=False)
mocker.patch.object(run_job, 'run_pexpect', return_value=('successful', 0))
mocker.patch.object(run_job, 'open_fifo_write', return_value=None)
mocker.patch.object(run_job, 'post_run_hook', return_value=None)
run_job.run(mock_job.id)
assert run_job.update_model.call_count == 3

View File

@ -33,7 +33,7 @@ logger = logging.getLogger('awx.main.utils')
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', 'memoize',
'get_ansible_version', 'get_ssh_version', 'get_awx_version', 'update_scm_url',
'get_type_for_model', 'get_model_for_type', 'to_python_boolean',
'get_type_for_model', 'get_model_for_type', 'cache_list_capabilities', 'to_python_boolean',
'ignore_inventory_computed_fields', 'ignore_inventory_group_removal',
'_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided',
'get_current_apps', 'set_current_apps']
@ -409,6 +409,72 @@ def get_model_for_type(type):
return ct_model
def cache_list_capabilities(page, prefetch_list, model, user):
'''
Given a `page` list of objects, the specified roles for the specified user
are save on each object in the list, using 1 query for each role type
Examples:
capabilities_prefetch = ['admin', 'execute']
--> prefetch the admin (edit) and execute (start) permissions for
items in list for current user
capabilities_prefetch = ['inventory.admin']
--> prefetch the related inventory FK permissions for current user,
and put it into the object's cache
capabilities_prefetch = [{'copy': ['inventory.admin', 'project.admin']}]
--> prefetch logical combination of admin permission to inventory AND
project, put into cache dictionary as "copy"
'''
from django.db.models import Q
page_ids = [obj.id for obj in page]
for obj in page:
obj.capabilities_cache = {}
for prefetch_entry in prefetch_list:
display_method = None
if type(prefetch_entry) is dict:
display_method = prefetch_entry.keys()[0]
paths = prefetch_entry[display_method]
else:
paths = prefetch_entry
if type(paths) is not list:
paths = [paths]
# Build the query for accessible_objects according the user & role(s)
qs_obj = None
for role_path in paths:
if '.' in role_path:
res_path = '__'.join(role_path.split('.')[:-1])
role_type = role_path.split('.')[-1]
if qs_obj is None:
qs_obj = model.objects
parent_model = model._meta.get_field(res_path).related_model
kwargs = {'%s__in' % res_path: parent_model.accessible_objects(user, '%s_role' % role_type)}
qs_obj = qs_obj.filter(Q(**kwargs) | Q(**{'%s__isnull' % res_path: True}))
else:
role_type = role_path
qs_obj = model.accessible_objects(user, '%s_role' % role_type)
if display_method is None:
# Role name translation to UI names for methods
display_method = role_type
if role_type == 'admin':
display_method = 'edit'
elif role_type in ['execute', 'update']:
display_method = 'start'
# Union that query with the list of items on page
ids_with_role = set(qs_obj.filter(pk__in=page_ids).values_list('pk', flat=True))
# Save data item-by-item
for obj in page:
obj.capabilities_cache[display_method] = False
if obj.pk in ids_with_role:
obj.capabilities_cache[display_method] = True
def get_system_task_capacity():
'''
Measure system memory and use it as a baseline for determining the system's capacity

1
awx/ui/.npmrc Normal file
View File

@ -0,0 +1 @@
progress=false

View File

@ -1,13 +1,13 @@
<div class="RoleList-tagContainer"
ng-repeat="entry in access_list">
<div class="RoleList-deleteContainer"
ng-if="entry.explicit"
ng-if="entry.explicit && entry.user_capabilities.unattach"
ng-click="deletePermission(permission, entry)">
<i ng-if="entry.explicit"
<i ng-if="entry.explicit && entry.user_capabilities.unattach"
class="fa fa-times RoleList-tagDelete"></i>
</div>
<div class="RoleList-tag"
ng-class="{'RoleList-tag--deletable': entry.explicit,
ng-class="{'RoleList-tag--deletable': entry.explicit && entry.user_capabilities.unattach,
'RoleList-tag--team': entry.team_id}"
aw-tool-tip='{{entry.team_name | sanitize}}' aw-tip-placement='bottom'>
<span class="RoleList-name">{{ entry.name }}</span>

View File

@ -839,6 +839,7 @@ var tower = angular.module('Tower', [
// If browser refresh, set the user_is_superuser value
$rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser');
$rootScope.user_is_system_auditor = Authorization.getUserInfo('is_system_auditor');
// state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket)
if (!_.contains($location.$$url, '/login')) {
ConfigService.getConfig().then(function() {

View File

@ -14,9 +14,14 @@
export function CredentialsList($scope, $rootScope, $location, $log,
$stateParams, Rest, Alert, CredentialList, GenerateList, Prompt, SearchInit,
PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath,
SelectionInit, GetChoices, Wait, $state, $filter) {
SelectionInit, GetChoices, Wait, $state, $filter, rbacUiControlService) {
ClearScope();
rbacUiControlService.canAdd('credentials')
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
Wait('start');
var list = CredentialList,
@ -129,7 +134,7 @@ CredentialsList.$inject = ['$scope', '$rootScope', '$location', '$log',
'$stateParams', 'Rest', 'Alert', 'CredentialList', 'generateList', 'Prompt',
'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
'ProcessErrors', 'GetBasePath', 'SelectionInit', 'GetChoices', 'Wait',
'$state', '$filter'
'$state', '$filter', 'rbacUiControlService'
];
@ -138,7 +143,6 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log,
ReturnToCaller, ClearScope, GenerateList, SearchInit, PaginateInit,
LookUpInit, OrganizationList, GetBasePath, GetChoices, Empty, KindChange,
OwnerChange, FormSave, $state, CreateSelect2) {
ClearScope();
// Inject dynamic view
@ -337,6 +341,7 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
}
ClearScope();
var defaultUrl = GetBasePath('credentials'),
generator = GenerateForm,
form = CredentialForm,
@ -344,10 +349,17 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
master = {},
id = $stateParams.credential_id,
relatedSets = {};
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
$scope.id = id;
$scope.$watch('credential_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
$scope.canShareCredential = false;
if ($rootScope.current_user.is_superuser) {

View File

@ -13,7 +13,7 @@
export function JobsListController ($rootScope, $log, $scope, $compile, $stateParams,
ClearScope, LoadSchedulesScope,
LoadJobsScope, AllJobsList, ScheduledJobsList, GetChoices, GetBasePath, Wait) {
LoadJobsScope, AllJobsList, ScheduledJobsList, GetChoices, GetBasePath, Wait, $state) {
ClearScope();
@ -61,6 +61,11 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
}
}
jobs_scope = $scope.$new(true);
jobs_scope.viewJob = function (id) {
$state.transitionTo('jobDetail', {id: id});
};
jobs_scope.showJobType = true;
LoadJobsScope({
parent_scope: $scope,
@ -153,4 +158,4 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
JobsListController.$inject = ['$rootScope', '$log', '$scope', '$compile', '$stateParams',
'ClearScope', 'LoadSchedulesScope', 'LoadJobsScope',
'AllJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait'];
'AllJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait', '$state'];

View File

@ -15,10 +15,16 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
Rest, Alert, ProjectList, GenerateList, Prompt, SearchInit,
PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath,
SelectionInit, ProjectUpdate, Refresh, Wait, GetChoices, Empty,
Find, GetProjectIcon, GetProjectToolTip, $filter, $state) {
Find, GetProjectIcon, GetProjectToolTip, $filter, $state, rbacUiControlService) {
ClearScope();
$scope.canAdd = false;
rbacUiControlService.canAdd('projects')
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
Wait('start');
var list = ProjectList,
@ -369,7 +375,7 @@ ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log',
'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
'ProcessErrors', 'GetBasePath', 'SelectionInit', 'ProjectUpdate',
'Refresh', 'Wait', 'GetChoices', 'Empty', 'Find',
'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state'
'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state', 'rbacUiControlService'
];
@ -379,6 +385,15 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l
OrganizationList, CredentialList, GetChoices, DebugForm, Wait, $state,
CreateSelect2) {
Rest.setUrl(GetBasePath('projects'));
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add a project.', 'alert-info');
}
});
ClearScope();
// Inject dynamic view
@ -559,6 +574,12 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
ClearScope('htmlTemplate');
$scope.$watch('project_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
// Inject dynamic view
var form = ProjectsForm(),
generator = GenerateForm,

View File

@ -14,10 +14,16 @@
export function TeamsList($scope, $rootScope, $location, $log, $stateParams,
Rest, Alert, TeamList, GenerateList, Prompt, SearchInit, PaginateInit,
ReturnToCaller, ClearScope, ProcessErrors, SetTeamListeners, GetBasePath,
SelectionInit, Wait, $state, Refresh, $filter) {
SelectionInit, Wait, $state, Refresh, $filter, rbacUiControlService) {
ClearScope();
$scope.canAdd = false;
rbacUiControlService.canAdd('teams')
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
var list = TeamList,
defaultUrl = GetBasePath('teams'),
generator = GenerateList,
@ -126,7 +132,7 @@ TeamsList.$inject = ['$scope', '$rootScope', '$location', '$log',
'$stateParams', 'Rest', 'Alert', 'TeamList', 'generateList', 'Prompt',
'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
'ProcessErrors', 'SetTeamListeners', 'GetBasePath', 'SelectionInit', 'Wait',
'$state', 'Refresh', '$filter'
'$state', 'Refresh', '$filter', 'rbacUiControlService'
];
@ -137,6 +143,15 @@ export function TeamsAdd($scope, $rootScope, $compile, $location, $log,
ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
//$scope.
Rest.setUrl(GetBasePath('teams'));
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add a team.', 'alert-info');
}
});
// Inject dynamic view
var defaultUrl = GetBasePath('teams'),
form = TeamForm,
@ -208,6 +223,12 @@ export function TeamsEdit($scope, $rootScope, $location,
$scope.team_id = id;
$scope.$watch('team_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();

View File

@ -34,10 +34,16 @@ function user_type_sync($scope) {
export function UsersList($scope, $rootScope, $location, $log, $stateParams,
Rest, Alert, UserList, GenerateList, Prompt, SearchInit, PaginateInit,
ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit,
Wait, $state, Refresh, $filter) {
Wait, $state, Refresh, $filter, rbacUiControlService) {
ClearScope();
$scope.canAdd = false;
rbacUiControlService.canAdd('users')
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
var list = UserList,
defaultUrl = GetBasePath('users'),
generator = GenerateList,
@ -136,7 +142,7 @@ UsersList.$inject = ['$scope', '$rootScope', '$location', '$log',
'$stateParams', 'Rest', 'Alert', 'UserList', 'generateList', 'Prompt',
'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
'ProcessErrors', 'GetBasePath', 'SelectionInit', 'Wait', '$state',
'Refresh', '$filter'
'Refresh', '$filter', 'rbacUiControlService'
];
@ -148,6 +154,15 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
ReturnToCaller, ClearScope, GetBasePath, LookUpInit, OrganizationList,
ResetForm, Wait, CreateSelect2, $state) {
Rest.setUrl(GetBasePath('users'));
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add a user.', 'alert-info');
}
});
ClearScope();
// Inject dynamic view
@ -279,6 +294,12 @@ export function UsersEdit($scope, $rootScope, $location,
$scope.user_type = user_type_options[0];
$scope.$watch('user_type', user_type_sync($scope));
$scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
var setScopeFields = function(data){
_(data)
.pick(function(value, key){

View File

@ -32,13 +32,15 @@ export default
type: 'text',
addRequired: true,
editRequired: true,
autocomplete: false
autocomplete: false,
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
editRequired: false,
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
organization: {
addRequired: false,
@ -52,7 +54,8 @@ export default
awPopOver: "<p>If no organization is given, the credential can only be used by the user that creates the credential. Organization admins and system administrators can assign an organization so that roles for the credential can be assigned to users and teams in that organization.</p>",
dataTitle: 'Organization ',
dataPlacement: 'bottom',
dataContainer: "body"
dataContainer: "body",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
kind: {
label: 'Type',
@ -83,7 +86,8 @@ export default
dataTitle: 'Type',
dataPlacement: 'right',
dataContainer: "body",
hasSubForm: true
hasSubForm: true,
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
access_key: {
label: 'Access Key',
@ -96,12 +100,13 @@ export default
autocomplete: false,
apiField: 'username',
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
secret_key: {
label: 'Secret Key',
type: 'sensitive',
ngShow: "kind.value == 'aws'",
ngDisabled: "secret_key_ask",
ngDisabled: "secret_key_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)",
awRequiredWhen: {
reqExpression: "aws_required",
init: false
@ -123,7 +128,8 @@ export default
dataTitle: 'STS Token',
dataPlacement: 'right',
dataContainer: "body",
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"host": {
labelBind: 'hostLabel',
@ -139,7 +145,8 @@ export default
reqExpression: 'host_required',
init: false
},
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"subscription": {
label: "Subscription ID",
@ -156,7 +163,8 @@ export default
dataTitle: 'Subscription ID',
dataPlacement: 'right',
dataContainer: "body",
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"username": {
labelBind: 'usernameLabel',
@ -168,7 +176,8 @@ export default
init: false
},
autocomplete: false,
subForm: "credentialSubForm"
subForm: "credentialSubForm",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"email_address": {
labelBind: 'usernameLabel',
@ -183,7 +192,8 @@ export default
dataTitle: 'Email',
dataPlacement: 'right',
dataContainer: "body",
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"api_key": {
label: 'API Key',
@ -196,7 +206,8 @@ export default
autocomplete: false,
hasShowInputButton: true,
clear: false,
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"password": {
labelBind: 'passwordLabel',
@ -209,13 +220,14 @@ export default
reqExpression: "password_required",
init: false
},
subForm: "credentialSubForm"
subForm: "credentialSubForm",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"ssh_password": {
label: 'Password',
type: 'sensitive',
ngShow: "kind.value == 'ssh'",
ngDisabled: "ssh_password_ask",
ngDisabled: "ssh_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)",
addRequired: false,
editRequired: false,
subCheckbox: {
@ -247,7 +259,8 @@ export default
dataTitle: 'Private Key',
dataPlacement: 'right',
dataContainer: "body",
subForm: "credentialSubForm"
subForm: "credentialSubForm",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"ssh_key_unlock": {
label: 'Private Key Passphrase',
@ -255,7 +268,7 @@ export default
ngShow: "kind.value == 'ssh' || kind.value == 'scm'",
addRequired: false,
editRequired: false,
ngDisabled: "keyEntered === false || ssh_key_unlock_ask",
ngDisabled: "keyEntered === false || ssh_key_unlock_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)",
subCheckbox: {
variable: 'ssh_key_unlock_ask',
ngShow: "kind.value == 'ssh'",
@ -278,7 +291,8 @@ export default
"<code>sudo | su | pbrun | pfexec | runas</code> <br>(defaults to <code>sudo</code>)</p>",
dataPlacement: 'right',
dataContainer: "body",
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"become_username": {
labelBind: 'becomeUsernameLabel',
@ -287,13 +301,14 @@ export default
addRequired: false,
editRequired: false,
autocomplete: false,
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"become_password": {
labelBind: 'becomePasswordLabel',
type: 'sensitive',
ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ",
ngDisabled: "become_password_ask",
ngDisabled: "become_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)",
addRequired: false,
editRequired: false,
subCheckbox: {
@ -309,7 +324,8 @@ export default
type: 'text',
label: 'Client ID',
subForm: 'credentialSubForm',
ngShow: "kind.value === 'azure_rm'"
ngShow: "kind.value === 'azure_rm'",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
secret:{
type: 'sensitive',
@ -317,20 +333,23 @@ export default
autocomplete: false,
label: 'Client Secret',
subForm: 'credentialSubForm',
ngShow: "kind.value === 'azure_rm'"
ngShow: "kind.value === 'azure_rm'",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
tenant: {
type: 'text',
label: 'Tenant ID',
subForm: 'credentialSubForm',
ngShow: "kind.value === 'azure_rm'"
ngShow: "kind.value === 'azure_rm'",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
authorize: {
label: 'Authorize',
type: 'checkbox',
ngChange: "toggleCallback('host_config_key')",
subForm: 'credentialSubForm',
ngShow: "kind.value === 'net'"
ngShow: "kind.value === 'net'",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
authorize_password: {
label: 'Authorize Password',
@ -339,6 +358,7 @@ export default
autocomplete: false,
subForm: 'credentialSubForm',
ngShow: "authorize && authorize !== 'false'",
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"project": {
labelBind: 'projectLabel',
@ -355,7 +375,8 @@ export default
reqExpression: 'project_required',
init: false
},
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"domain": {
labelBind: 'domainLabel',
@ -371,13 +392,14 @@ export default
dataContainer: "body",
addRequired: false,
editRequired: false,
subForm: 'credentialSubForm'
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
"vault_password": {
label: "Vault Password",
type: 'sensitive',
ngShow: "kind.value == 'ssh'",
ngDisabled: "vault_password_ask",
ngDisabled: "vault_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)",
addRequired: false,
editRequired: false,
subCheckbox: {
@ -394,11 +416,17 @@ export default
buttons: {
cancel: {
ngClick: 'formCancel()',
ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
label: 'Save',
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: true //Disable when $pristine or $invalid, optional
ngDisabled: true,
ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)' //Disable when $pristine or $invalid, optional
}
},
@ -421,7 +449,8 @@ export default
label: 'Add',
awToolTip: 'Add a permission',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},

View File

@ -26,14 +26,16 @@ export default
type: 'text',
addRequired: true,
editRequired: true,
tab: 'properties'
tab: 'properties',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false,
tab: 'properties'
tab: 'properties',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
variables: {
label: 'Variables',
@ -65,7 +67,8 @@ export default
ngChange: 'sourceChange(source)',
addRequired: false,
editRequired: false,
ngModel: 'source'
ngModel: 'source',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
credential: {
label: 'Cloud Credential',
@ -77,7 +80,8 @@ export default
awRequiredWhen: {
reqExpression: "cloudCredentialRequired",
init: "false"
}
},
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
source_regions: {
label: 'Regions',
@ -92,7 +96,8 @@ export default
awPopOver: "<p>Click on the regions field to see a list of regions for your cloud provider. You can select multiple regions, " +
"or choose <em>All</em> to include all regions. Tower will only be updated with Hosts associated with the selected regions." +
"</p>",
dataContainer: 'body'
dataContainer: 'body',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
instance_filters: {
label: 'Instance Filters',
@ -112,7 +117,8 @@ export default
"<blockquote>tag:Name=test*</blockquote>\n" +
"<p>View the <a href=\"http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInstances.html\" target=\"_blank\">Describe Instances documentation</a> " +
"for a complete list of supported filters.</p>",
dataContainer: 'body'
dataContainer: 'body',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
group_by: {
label: 'Only Group By',
@ -137,7 +143,8 @@ export default
"<li>VPC ID: <strong>vpcs &raquo; vpc-5ca1ab1e</strong></li>" +
"<li>Tag None: <strong>tags &raquo; tag_none</strong></li>" +
"</ul><p>If blank, all groups above are created except <em>Instance ID</em>.</p>",
dataContainer: 'body'
dataContainer: 'body',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
inventory_script: {
label : "Custom Inventory Script",
@ -149,6 +156,7 @@ export default
addRequired: true,
editRequired: true,
ngRequired: "source && source.value === 'custom'",
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)',
},
custom_variables: {
id: 'custom_variables',
@ -269,7 +277,8 @@ export default
dataTitle: 'Overwrite',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options'
labelClass: 'checkbox-options',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
}, {
name: 'overwrite_vars',
label: 'Overwrite Variables',
@ -283,7 +292,8 @@ export default
dataTitle: 'Overwrite Variables',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options'
labelClass: 'checkbox-options',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
}, {
name: 'update_on_launch',
label: 'Update on Launch',
@ -296,7 +306,8 @@ export default
dataTitle: 'Update on Launch',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options'
labelClass: 'checkbox-options',
ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
}]
},
update_cache_timeout: {
@ -321,11 +332,17 @@ export default
buttons: {
cancel: {
ngClick: 'formCancel()'
ngClick: 'formCancel()',
ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()',
ngDisabled: true
ngDisabled: true,
ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},

View File

@ -46,13 +46,15 @@ export default
"</blockquote>",
dataTitle: 'Host Name',
dataPlacement: 'right',
dataContainer: 'body'
dataContainer: 'body',
ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
editRequired: false,
ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)'
},
variables: {
label: 'Variables',
@ -83,10 +85,16 @@ export default
buttons: {
cancel: {
ngClick: 'formCancel()',
ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(host.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()',
ngDisabled: true
ngDisabled: true,
ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)'
}
},

View File

@ -26,14 +26,16 @@ export default
type: 'text',
addRequired: true,
editRequired: true,
capitalize: false
capitalize: false,
ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
},
inventory_description: {
realName: 'description',
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
editRequired: false,
ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
},
organization: {
label: 'Organization',
@ -44,7 +46,8 @@ export default
awRequiredWhen: {
reqExpression: "organizationrequired",
init: "true"
}
},
ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
},
variables: {
label: 'Variables',
@ -63,17 +66,24 @@ export default
'<p>View YAML examples at <a href="http://docs.ansible.com/YAMLSyntax.html" target="_blank">docs.ansible.com</a></p>',
dataTitle: 'Inventory Variables',
dataPlacement: 'right',
dataContainer: 'body'
dataContainer: 'body',
ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working
}
},
buttons: {
cancel: {
ngClick: 'formCancel()'
ngClick: 'formCancel()',
ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngHide: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()',
ngDisabled: true
ngDisabled: true,
ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
@ -94,7 +104,8 @@ export default
label: 'Add',
awToolTip: 'Add a permission',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},

View File

@ -27,14 +27,16 @@ export default
type: 'text',
addRequired: true,
editRequired: true,
column: 1
column: 1,
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false,
column: 1
column: 1,
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
job_type: {
label: 'Job Type',
@ -56,7 +58,8 @@ export default
variable: 'ask_job_type_on_launch',
ngShow: "!job_type.value || job_type.value !== 'scan'",
text: 'Prompt on launch'
}
},
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
inventory: {
label: 'Inventory',
@ -78,7 +81,8 @@ export default
variable: 'ask_inventory_on_launch',
ngShow: "!job_type.value || job_type.value !== 'scan'",
text: 'Prompt on launch'
}
},
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
project: {
label: 'Project',
@ -100,12 +104,13 @@ export default
dataTitle: 'Project',
dataPlacement: 'right',
dataContainer: "body",
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
playbook: {
label: 'Playbook',
type:'select',
ngOptions: 'book for book in playbook_options track by book',
ngDisabled: "job_type.value === 'scan' && project_name === 'Default'",
ngDisabled: "(job_type.value === 'scan' && project_name === 'Default') || !(job_template_obj.summary_fields.user_capabilities.edit || canAdd)",
id: 'playbook-select',
awRequiredWhen: {
reqExpression: "playbookrequired",
@ -138,7 +143,8 @@ export default
subCheckbox: {
variable: 'ask_credential_on_launch',
text: 'Prompt on launch'
}
},
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
cloud_credential: {
label: 'Cloud Credential',
@ -153,7 +159,8 @@ export default
"running playbook, allowing provisioning into the cloud without manually passing parameters to the included modules.</p>",
dataTitle: 'Cloud Credential',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
network_credential: {
label: 'Network Credential',
@ -167,7 +174,8 @@ export default
awPopOver: "<p>Network credentials are used by Ansible networking modules to connect to and manage networking devices.</p>",
dataTitle: 'Network Credential',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
forks: {
label: 'Forks',
@ -186,7 +194,8 @@ export default
' target=\"_blank\">ansible configuration file</a>.</p>',
dataTitle: 'Forks',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working
},
limit: {
label: 'Limit',
@ -203,7 +212,8 @@ export default
subCheckbox: {
variable: 'ask_limit_on_launch',
text: 'Prompt on launch'
}
},
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
verbosity: {
label: 'Verbosity',
@ -216,7 +226,8 @@ export default
awPopOver: "<p>Control the level of output ansible will produce as the playbook executes.</p>",
dataTitle: 'Verbosity',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
job_tags: {
label: 'Job Tags',
@ -235,7 +246,8 @@ export default
subCheckbox: {
variable: 'ask_tags_on_launch',
text: 'Prompt on launch'
}
},
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
skip_tags: {
label: 'Skip Tags',
@ -254,7 +266,8 @@ export default
subCheckbox: {
variable: 'ask_skip_tags_on_launch',
text: 'Prompt on launch'
}
},
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
checkbox_group: {
label: 'Options',
@ -270,7 +283,8 @@ export default
dataPlacement: 'right',
dataTitle: 'Become Privilege Escalation',
dataContainer: "body",
labelClass: 'stack-inline'
labelClass: 'stack-inline',
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
}, {
name: 'allow_callbacks',
label: 'Allow Provisioning Callbacks',
@ -284,7 +298,8 @@ export default
dataPlacement: 'right',
dataTitle: 'Allow Provisioning Callbacks',
dataContainer: "body",
labelClass: 'stack-inline'
labelClass: 'stack-inline',
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
}]
},
callback_url: {
@ -299,7 +314,8 @@ export default
awPopOverWatch: "callback_help",
dataPlacement: 'top',
dataTitle: 'Provisioning Callback URL',
dataContainer: "body"
dataContainer: "body",
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
host_config_key: {
label: 'Host Config Key',
@ -312,7 +328,8 @@ export default
awPopOverWatch: "callback_help",
dataPlacement: 'right',
dataTitle: "Host Config Key",
dataContainer: "body"
dataContainer: "body",
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
labels: {
label: 'Labels',
@ -325,7 +342,8 @@ export default
dataTitle: 'Labels',
dataPlacement: 'right',
awPopOver: "<p>Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display.</p>",
dataContainer: 'body'
dataContainer: 'body',
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
variables: {
label: 'Extra Variables',
@ -348,14 +366,15 @@ export default
subCheckbox: {
variable: 'ask_variables_on_launch',
text: 'Prompt on launch'
}
},
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working
}
},
buttons: { //for now always generates <button> tags
add_survey: {
ngClick: 'addSurvey()',
ngShow: 'job_type.value !== "scan" && !survey_exists',
ngShow: 'job_type.value !== "scan" && !survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAdd)',
awFeature: 'surveys',
awToolTip: 'Surveys allow users to be prompted at job launch with a series of questions related to the job. This allows for variables to be defined that affect the playbook run at time of launch.',
dataPlacement: 'top'
@ -363,14 +382,25 @@ export default
edit_survey: {
ngClick: 'editSurvey()',
awFeature: 'surveys',
ngShow: 'job_type.value !== "scan" && survey_exists'
ngShow: 'job_type.value !== "scan" && survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
view_survey: {
ngClick: 'editSurvey()',
awFeature: 'surveys',
ngShow: 'job_type.value !== "scan" && survey_exists && !(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
cancel: {
ngClick: 'formCancel()'
ngClick: 'formCancel()',
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: "job_templates_form.$invalid || can_edit!==true"//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons
ngDisabled: "job_templates_form.$invalid",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
@ -394,7 +424,8 @@ export default
label: 'Add',
awToolTip: 'Add a permission',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},

View File

@ -25,23 +25,31 @@ export default
type: 'text',
addRequired: true,
editRequired: true,
capitalize: false
capitalize: false,
ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
editRequired: false,
ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
buttons: { //for now always generates <button> tags
cancel: {
ngClick: 'formCancel()'
ngClick: 'formCancel()',
ngShow: '(organization_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: true //Disable when $pristine or $invalid, optional
ngDisabled: true,
ngShow: '(organization_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
@ -62,7 +70,8 @@ export default
label: 'Add',
awToolTip: 'Add a permission',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: '(organization_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},

View File

@ -30,13 +30,15 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
type: 'text',
addRequired: true,
editRequired: true,
capitalize: false
capitalize: false,
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
editRequired: false,
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
organization: {
label: 'Organization',
@ -50,7 +52,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
},
dataTitle: 'Organization',
dataContainer: 'body',
dataPlacement: 'right'
dataPlacement: 'right',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
scm_type: {
label: 'SCM Type',
@ -61,6 +64,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
addRequired: true,
editRequired: true,
hasSubForm: true,
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
missing_path_alert: {
type: 'alertblock',
@ -82,7 +86,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
'<p>Use PROJECTS_ROOT in your environment settings file to determine the base path value.</p>',
dataTitle: 'Project Base Path',
dataContainer: 'body',
dataPlacement: 'right'
dataPlacement: 'right',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
local_path: {
label: 'Playbook Directory',
@ -99,7 +104,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
'<p>Use PROJECTS_ROOT in your environment settings file to determine the base path value.</p>',
dataTitle: 'Project Path',
dataContainer: 'body',
dataPlacement: 'right'
dataPlacement: 'right',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
scm_url: {
label: 'SCM URL',
@ -115,7 +121,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
awPopOver: "set in controllers/projects",
dataTitle: 'SCM URL',
dataContainer: 'body',
dataPlacement: 'right'
dataPlacement: 'right',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
scm_branch: {
labelBind: "scmBranchLabel",
@ -123,7 +130,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
ngShow: "scm_type && scm_type.value !== 'manual'",
addRequired: false,
editRequired: false,
subForm: 'sourceSubForm'
subForm: 'sourceSubForm',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
credential: {
label: 'SCM Credential',
@ -134,7 +142,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
ngClick: 'lookUpCredential()',
addRequired: false,
editRequired: false,
subForm: 'sourceSubForm'
subForm: 'sourceSubForm',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
checkbox_group: {
label: 'SCM Update Options',
@ -151,7 +160,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
dataTitle: 'SCM Clean',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options stack-inline'
labelClass: 'checkbox-options stack-inline',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
}, {
name: 'scm_delete_on_update',
label: 'Delete on Update',
@ -163,7 +173,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
dataTitle: 'SCM Delete',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options stack-inline'
labelClass: 'checkbox-options stack-inline',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
}, {
name: 'scm_update_on_launch',
label: 'Update on Launch',
@ -174,7 +185,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
dataTitle: 'SCM Update',
dataContainer: 'body',
dataPlacement: 'right',
labelClass: 'checkbox-options stack-inline'
labelClass: 'checkbox-options stack-inline',
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
}]
},
scm_update_cache_timeout: {
@ -193,17 +205,24 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
'and a new project update will be performed.</p>',
dataTitle: 'Cache Timeout',
dataPlacement: 'right',
dataContainer: "body"
dataContainer: "body",
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working
}
},
buttons: {
cancel: {
ngClick: 'formCancel()'
ngClick: 'formCancel()',
ngShow: '(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()',
ngDisabled: true
ngDisabled: true,
ngShow: '(project_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
@ -224,7 +243,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
label: 'Add',
awToolTip: 'Add a permission',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: '(project_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},

View File

@ -25,13 +25,15 @@ export default
type: 'text',
addRequired: true,
editRequired: true,
capitalize: false
capitalize: false,
ngDisabled: '!(team_obj.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
editRequired: false,
ngDisabled: '!(team_obj.summary_fields.user_capabilities.edit || canAdd)'
},
organization: {
label: 'Organization',
@ -44,17 +46,24 @@ export default
awRequiredWhen: {
reqExpression: "orgrequired",
init: true
}
},
ngDisabled: '!(team_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
buttons: {
cancel: {
ngClick: 'formCancel()'
ngClick: 'formCancel()',
ngShow: '(team_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(team_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()',
ngDisabled: true
ngDisabled: true,
ngShow: '(team_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
@ -75,7 +84,8 @@ export default
label: 'Add',
awToolTip: 'Add user to team',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: '(team_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
@ -132,7 +142,8 @@ export default
'class': "List-actionButton--delete",
iconClass: 'fa fa-times',
awToolTip: 'Dissasociate permission from team',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'permission.summary_fields.user_capabilities.unattach'
}
},
hideOnSuperuser: true

View File

@ -26,21 +26,24 @@ export default
type: 'text',
addRequired: true,
editRequired: true,
capitalize: true
capitalize: true,
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
last_name: {
label: 'Last Name',
type: 'text',
addRequired: true,
editRequired: true,
capitalize: true
capitalize: true,
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
email: {
label: 'Email',
type: 'email',
addRequired: true,
editRequired: true,
autocomplete: false
autocomplete: false,
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
username: {
label: 'Username',
@ -49,7 +52,8 @@ export default
reqExpression: "not_ldap_user && external_account === null",
init: true
},
autocomplete: false
autocomplete: false,
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
organization: {
label: 'Organization',
@ -63,7 +67,8 @@ export default
awRequiredWhen: {
reqExpression: "orgrequired",
init: true
}
},
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
password: {
label: 'Password',
@ -74,7 +79,8 @@ export default
editRequired: false,
ngChange: "clearPWConfirm('password_confirm')",
autocomplete: false,
chkPass: true
chkPass: true,
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
password_confirm: {
label: 'Confirm Password',
@ -85,7 +91,8 @@ export default
editRequired: false,
awPassMatch: true,
associated: 'password',
autocomplete: false
autocomplete: false,
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
user_type: {
label: 'User Type',
@ -94,16 +101,23 @@ export default
disableChooseOption: true,
ngModel: 'user_type',
ngShow: 'current_user["is_superuser"]',
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
},
buttons: {
cancel: {
ngClick: 'formCancel()'
ngClick: 'formCancel()',
ngShow: '(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()',
ngDisabled: true
ngDisabled: true,
ngShow: '(user_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
@ -186,7 +200,8 @@ export default
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'
awToolTip: 'Dissasociate permission from user',
ngShow: 'permission.summary_fields.user_capabilities.unattach'
}
},
hideOnSuperuser: true

View File

@ -46,7 +46,7 @@ export default
function createField(onChange, onReady, fld) {
//hide the textarea and show a fresh CodeMirror with the current mode (json or yaml)
scope[fld + 'codeMirror'] = AngularCodeMirror();
scope[fld + 'codeMirror'] = AngularCodeMirror(readOnly);
scope[fld + 'codeMirror'].addModes(global.$AnsibleConfig.variable_edit_modes);
scope[fld + 'codeMirror'].showTextArea({
scope: scope,

View File

@ -3,7 +3,7 @@
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name helpers.function:ProjectPath
@ -47,11 +47,13 @@ export default
Rest.get()
.success(function (data) {
var opts = [], i;
for (i = 0; i < data.project_local_paths.length; i++) {
opts.push({
label: data.project_local_paths[i],
value: data.project_local_paths[i]
});
if (data.project_local_paths) {
for (i = 0; i < data.project_local_paths.length; i++) {
opts.push({
label: data.project_local_paths[i],
value: data.project_local_paths[i]
});
}
}
if (scope.local_path) {
// List only includes paths not assigned to projects, so add the

View File

@ -16,6 +16,15 @@ function InventoriesAdd($scope, $rootScope, $compile, $location, $log,
PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON,
$state) {
Rest.setUrl(GetBasePath('inventory'));
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add an inventory.', 'alert-info');
}
});
ClearScope();
// Inject dynamic view

View File

@ -17,7 +17,6 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
ParseVariableString, RelatedSearchInit, RelatedPaginateInit,
Prompt, InitiatePlaybookRun, CreateDialog, deleteJobTemplate, $state,
$filter) {
ClearScope();
// Inject dynamic view
@ -32,6 +31,13 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
form.formLabelSize = null;
form.formFieldSize = null;
$scope.inventory_id = inventory_id;
$scope.$watch('invnentory_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();

View File

@ -14,7 +14,14 @@ function InventoriesList($scope, $rootScope, $location, $log,
$stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList,
generateList, Prompt, SearchInit, PaginateInit, ReturnToCaller,
ClearScope, ProcessErrors, GetBasePath, Wait,
Find, Empty, $state) {
Find, Empty, $state, rbacUiControlService) {
$scope.canAdd = false;
rbacUiControlService.canAdd('inventory')
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
var list = InventoryList,
defaultUrl = GetBasePath('inventory') + ($stateParams.status === 'sync-failed' ? '?not__inventory_sources_with_failures=0' : ''),
@ -376,4 +383,4 @@ function InventoriesList($scope, $rootScope, $location, $log,
export default ['$scope', '$rootScope', '$location', '$log',
'$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList',
'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller',
'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', InventoriesList];
'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', 'rbacUiControlService', InventoriesList];

View File

@ -6,9 +6,9 @@
export default
['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList', 'inventoryScriptsListObject', 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'LookUpInit',
'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions',
'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'rbacUiControlService',
function($state, $stateParams, $scope, GroupForm, CredentialList, InventoryScriptsList, ParseTypeChange, GenerateForm, inventoryData, LookUpInit,
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions){
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService){
var generator = GenerateForm,
form = GroupForm();
@ -16,6 +16,11 @@
CredentialList = _.cloneDeep(CredentialList);
CredentialList.fields.kind.noSearch = true;
rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/groups")
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
$scope.formCancel = function(){
$state.go('^');
};

View File

@ -18,6 +18,12 @@
CredentialList = _.cloneDeep(CredentialList);
CredentialList.fields.kind.noSearch = true;
$scope.$watch('group_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
$scope.formCancel = function(){
$state.go('^');
};
@ -256,6 +262,7 @@
$scope.credential_name = inventorySourceData.summary_fields.credential.name;
}
$scope = angular.extend($scope, groupData);
$scope.group_obj = groupData;
// instantiate lookup fields
if (inventorySourceData.source !== 'custom'){

View File

@ -5,13 +5,22 @@
*************************************************/
export default
['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroups', 'generateList', 'InventoryUpdate', 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus',
'InventoryManageService', 'groupsUrl', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg',
'InventoryManageService', 'groupsUrl', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Rest', 'GetBasePath', 'rbacUiControlService',
function($scope, $rootScope, $state, $stateParams, InventoryGroups, generateList, InventoryUpdate, GroupManageService, GroupsCancelUpdate, ViewUpdateStatus,
InventoryManageService, groupsUrl, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg){
InventoryManageService, groupsUrl, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg, Rest, GetBasePath, rbacUiControlService){
var list = InventoryGroups,
view = generateList,
pageSize = 20;
$scope.inventory_id = $stateParams.inventory_id;
$scope.canAdd = false;
rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups")
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
// The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope.
// In this case, we don't want to incidentally bind to this scope when editing a host or a group. See:
// https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the

View File

@ -5,10 +5,18 @@
*************************************************/
export default
['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService',
function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService){
['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService', 'rbacUiControlService', 'GetBasePath',
function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService, rbacUiControlService, GetBasePath){
var generator = GenerateForm,
form = HostForm;
$scope.canAdd = false;
rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/hosts")
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
$scope.parseType = 'yaml';
$scope.formCancel = function(){
$state.go('^');

View File

@ -9,6 +9,13 @@
function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService, host){
var generator = GenerateForm,
form = HostForm;
$scope.$watch('host.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
$scope.parseType = 'yaml';
$scope.formCancel = function(){
$state.go('^');

View File

@ -5,12 +5,23 @@
*************************************************/
export default
['$scope', '$rootScope', '$state', '$stateParams', 'InventoryHosts', 'generateList', 'InventoryManageService', 'HostManageService',
'hostsUrl', 'SearchInit', 'PaginateInit', 'SetStatus', 'Prompt', 'Wait', 'inventoryData', '$filter',
'hostsUrl', 'SearchInit', 'PaginateInit', 'SetStatus', 'Prompt', 'Wait', 'inventoryData', '$filter', 'Rest', 'GetBasePath', 'rbacUiControlService',
function($scope, $rootScope, $state, $stateParams, InventoryHosts, generateList, InventoryManageService, HostManageService,
hostsUrl, SearchInit, PaginateInit, SetStatus, Prompt, Wait, inventoryData, $filter){
hostsUrl, SearchInit, PaginateInit, SetStatus, Prompt, Wait, inventoryData, $filter, Rest, GetBasePath, rbacUiControlService){
var list = InventoryHosts,
view = generateList,
pageSize = 20;
$scope.inventory_id = $stateParams.inventory_id;
$scope.canAdd = false;
rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/hosts")
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
// The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope.
// In this case, we don't want to incidentally bind to this scope when editing a host or a group. See:
// https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the

View File

@ -3,21 +3,26 @@
*
* All Rights Reserved
*************************************************/
export default
['$scope', '$state', function($scope, $state){
$scope.groupsSelected = false;
$scope.hostsSelected = false;
$scope.hostsSelectedItems = [];
$scope.groupsSelectedItems = [];
$scope.setAdhocPattern = function(){
var pattern = _($scope.groupsSelectedItems)
.concat($scope.hostsSelectedItems)
.map(function(item){
return item.name;
}).value().join(':');
$state.go('inventoryManage.adhoc', {pattern: pattern});
};
export default
['$scope', '$state', 'inventoryData', function($scope, $state, inventoryData){
$scope.groupsSelected = false;
$scope.hostsSelected = false;
$scope.hostsSelectedItems = [];
$scope.groupsSelectedItems = [];
$scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc;
$scope.setAdhocPattern = function(){
var pattern = _($scope.groupsSelectedItems)
.concat($scope.hostsSelectedItems)
.map(function(item){
return item.name;
}).value().join(':');
$state.go('inventoryManage.adhoc', {pattern: pattern});
};
$scope.$watchGroup(['groupsSelected', 'hostsSelected'], function(newVals) {
$scope.adhocCommandTooltip = (newVals[0] || newVals[1]) ? "Run a command on the selected inventory" : "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.";
});
}];
}];

View File

@ -8,20 +8,27 @@ export default
[ '$rootScope', 'pagination', '$compile','SchedulerInit', 'Rest', 'Wait',
'inventoryScriptsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty',
'GenerateForm', 'SearchInit' , 'PaginateInit',
'LookUpInit', 'OrganizationList', '$scope', '$state',
'LookUpInit', 'OrganizationList', '$scope', '$state', 'Alert',
function(
$rootScope, pagination, $compile, SchedulerInit, Rest, Wait,
inventoryScriptsFormObject, ProcessErrors, GetBasePath, Empty,
GenerateForm, SearchInit, PaginateInit,
LookUpInit, OrganizationList, $scope, $state
LookUpInit, OrganizationList, $scope, $state, Alert
) {
Rest.setUrl(GetBasePath('inventory_scripts'));
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add an inventory script.', 'alert-info');
}
});
var scope = $scope,
generator = GenerateForm,
form = inventoryScriptsFormObject,
url = GetBasePath('inventory_scripts');
$scope.canEdit = true;
generator.inject(form, {
mode: 'add' ,
scope:scope,

View File

@ -17,6 +17,7 @@ export default
LookUpInit, OrganizationList, inventory_script,
$scope, $state
) {
var generator = GenerateForm,
id = inventory_script.id,
form = inventoryScriptsFormObject,
@ -24,6 +25,13 @@ export default
url = GetBasePath('inventory_scripts');
$scope.inventory_script = inventory_script;
$scope.$watch('inventory_script_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
generator.inject(form, {
mode: 'edit' ,
scope:$scope,

View File

@ -24,13 +24,15 @@ export default function() {
type: 'text',
addRequired: true,
editRequired: true,
capitalize: false
capitalize: false,
ngDisabled: '!(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
editRequired: false,
ngDisabled: '!(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)'
},
organization: {
label: 'Organization',
@ -41,7 +43,8 @@ export default function() {
},
sourceModel: 'organization',
sourceField: 'name',
ngClick: 'lookUpOrganization()'
ngClick: 'lookUpOrganization()',
ngDisabled: '!(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)'
},
script: {
label: 'Custom Script',
@ -51,7 +54,7 @@ export default function() {
addRequired: true,
editRequired: true,
awDropFile: true,
ngDisabled: '!canEdit',
ngDisabled: '!(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)',
rows: 10,
awPopOver: "<p>Drag and drop your custom inventory script file here or create one in the field to import your custom inventory. " +
"<br><br> Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python</p>",
@ -64,10 +67,16 @@ export default function() {
buttons: { //for now always generates <button> tags
cancel: {
ngClick: 'formCancel()',
ngShow: '(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: 'custom_inventory_form.$pristine || custom_inventory_form.$invalid || !canEdit' //Disable when $pristine or $invalid, optional
ngDisabled: 'custom_inventory_form.$pristine || custom_inventory_form.$invalid', //Disable when $pristine or $invalid, optional
ngShow: '(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)'
}
}
};

View File

@ -42,7 +42,8 @@ export default function(){
ngClick: 'addCustomInv()',
awToolTip: 'Create a new custom inventory',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: 'canAdd'
}
},
@ -56,7 +57,16 @@ export default function(){
label: 'Edit',
"class": 'btn-sm',
awToolTip: 'Edit inventory script',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'inventory_script.summary_fields.user_capabilities.edit'
},
view: {
ngClick: "editCustomInv(inventory_script.id)",
label: 'View',
"class": 'btn-sm',
awToolTip: 'View inventory script',
dataPlacement: 'top',
ngShow: '!inventory_script.summary_fields.user_capabilities.edit'
},
"delete": {
ngClick: "deleteCustomInv(inventory_script.id, inventory_script.name)",
@ -64,7 +74,8 @@ export default function(){
label: 'Delete',
"class": 'btn-sm',
awToolTip: 'Delete inventory script',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'inventory_script.summary_fields.user_capabilities.delete'
}
}
};

View File

@ -7,17 +7,24 @@
export default
[ '$rootScope','Wait', 'generateList', 'inventoryScriptsListObject',
'GetBasePath' , 'SearchInit' , 'PaginateInit', 'Rest' , 'ProcessErrors',
'Prompt', '$state', '$filter',
'Prompt', '$state', '$filter', 'rbacUiControlService',
function(
$rootScope,Wait, GenerateList, inventoryScriptsListObject,
GetBasePath, SearchInit, PaginateInit,
Rest, ProcessErrors, Prompt, $state, $filter
Rest, ProcessErrors, Prompt, $state, $filter, rbacUiControlService
) {
var scope = $rootScope.$new(),
defaultUrl = GetBasePath('inventory_scripts'),
list = inventoryScriptsListObject,
view = GenerateList;
scope.canAdd = false;
rbacUiControlService.canAdd("inventory_scripts")
.then(function(canAdd) {
scope.canAdd = canAdd;
});
view.inject( list, {
mode: 'edit',
scope: scope

View File

@ -21,6 +21,15 @@
$state, CreateSelect2, $q
) {
Rest.setUrl(GetBasePath('job_templates'));
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add a job template.', 'alert-info');
}
});
ClearScope();
// Inject dynamic view
var defaultUrl = GetBasePath('job_templates'),

View File

@ -36,6 +36,12 @@ export default
ClearScope();
$scope.$watch('job_template_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
var defaultUrl = GetBasePath('job_templates'),
generator = GenerateForm,
form = JobTemplateForm(),

View File

@ -10,18 +10,24 @@ export default
'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
'ProcessErrors', 'GetBasePath', 'JobTemplateForm', 'CredentialList',
'LookUpInit', 'InitiatePlaybookRun', 'Wait', '$compile',
'$state', '$filter',
'$state', '$filter', 'rbacUiControlService',
function(
$scope, $rootScope, $location, $log,
$stateParams, Rest, Alert, JobTemplateList, GenerateList, Prompt,
SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors,
GetBasePath, JobTemplateForm, CredentialList, LookUpInit, InitiatePlaybookRun,
Wait, $compile, $state, $filter
Wait, $compile, $state, $filter, rbacUiControlService
) {
ClearScope();
$scope.canAdd = false;
rbacUiControlService.canAdd("job_templates")
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
var list = JobTemplateList,
defaultUrl = GetBasePath('job_templates'),
view = GenerateList,

View File

@ -70,6 +70,12 @@
flex: 0 0 637px;
max-width: 637px;
}
.SurveyMaker-previewPanel--viewOnly {
flex: 0 0 1155px;
max-width: 1155px;
}
.SurveyMaker-separatorPanel {
display: flex;
flex: 0 0 51px;

View File

@ -94,28 +94,33 @@ export default
fieldActions: {
columnClass: 'col-lg-2 col-md-2 col-sm-3 col-xs-4',
"view": {
mode: "all",
ngClick: "viewJob(all_job.id)",
awToolTip: "View the job",
dataPlacement: "top"
},
submit: {
icon: 'icon-rocket',
mode: 'all',
ngClick: 'relaunchJob($event, all_job.id)',
awToolTip: 'Relaunch using the same parameters',
dataPlacement: 'top',
ngHide: "all_job.type == 'system_job' "
ngShow: "!(all_job.type == 'system_job') && all_job.summary_fields.user_capabilities.start"
},
cancel: {
mode: 'all',
ngClick: 'deleteJob(all_job.id)',
awToolTip: 'Cancel the job',
dataPlacement: 'top',
ngShow: "all_job.status === 'running'|| all_job.status === 'waiting' || all_job.status === 'pending'"
ngShow: "(all_job.status === 'running'|| all_job.status === 'waiting' || all_job.status === 'pending') && all_job.summary_fields.user_capabilities.start"
},
"delete": {
mode: 'all',
ngClick: 'deleteJob(all_job.id)',
awToolTip: 'Delete the job',
dataPlacement: 'top',
ngShow: "all_job.status !== 'running' && all_job.status !== 'waiting' && all_job.status !== 'pending'"
ngShow: "(all_job.status !== 'running' && all_job.status !== 'waiting' && all_job.status !== 'pending') && all_job.summary_fields.user_capabilities.delete"
}
}
});

View File

@ -100,13 +100,14 @@ export default
ngClick: 'relaunchJob($event, completed_job.id)',
awToolTip: 'Relaunch using the same parameters',
dataPlacement: 'top',
ngHide: "completed_job.type == 'system_job' "
ngShow: "!completed_job.type == 'system_job' || completed_job.summary_fields.user_capabilities.start"
},
"delete": {
mode: 'all',
ngClick: 'deleteJob(completed_job.id)',
awToolTip: 'Delete the job',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'completed_job.summary_fields.user_capabilities.delete'
}
}
});

View File

@ -72,7 +72,17 @@ export default
label: 'Edit',
"class": 'btn-sm',
awToolTip: 'Edit credential',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'credential.summary_fields.user_capabilities.edit'
},
view: {
ngClick: "editCredential(credential.id)",
label: 'View',
"class": 'btn-sm',
awToolTip: 'View credential',
dataPlacement: 'top',
ngShow: '!credential.summary_fields.user_capabilities.edit'
},
"delete": {
@ -81,7 +91,8 @@ export default
label: 'Delete',
"class": 'btn-sm',
awToolTip: 'Delete credential',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'credential.summary_fields.user_capabilities.delete'
}
}
});

View File

@ -91,7 +91,8 @@ export default
ngClick: 'addInventory()',
awToolTip: 'Create a new inventory',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: 'canAdd'
}
},
@ -103,13 +104,22 @@ export default
label: 'Edit',
ngClick: 'editInventory(inventory.id)',
awToolTip: 'Edit inventory',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'inventory.summary_fields.user_capabilities.edit'
},
view: {
label: 'View',
ngClick: 'editInventory(inventory.id)',
awToolTip: 'View inventory',
dataPlacement: 'top',
ngShow: '!inventory.summary_fields.user_capabilities.edit'
},
"delete": {
label: 'Delete',
ngClick: "deleteInventory(inventory.id, inventory.name)",
awToolTip: 'Delete inventory',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'inventory.summary_fields.user_capabilities.delete'
}
}
});

View File

@ -143,7 +143,8 @@ export default
actionClass: 'btn List-buttonDefault',
buttonContent: 'RUN COMMANDS',
showTipWhenDisabled: true,
tooltipInnerClass: "Tooltip-wide"
tooltipInnerClass: "Tooltip-wide",
ngShow: 'canAdhoc'
// TODO: set up a tip watcher and change text based on when
// things are selected/not selected. This is started and
// commented out in the inventory controller within the watchers.
@ -155,7 +156,8 @@ export default
ngClick: "createGroup()",
awToolTip: "Create a new group",
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD GROUP'
buttonContent: '&#43; ADD GROUP',
ngShow: 'canAdd'
}
},
@ -169,8 +171,8 @@ export default
ngClick: 'updateGroup(group)',
awToolTip: "{{ group.launch_tooltip }}",
dataTipWatch: "group.launch_tooltip",
ngShow: "group.status !== 'running' && group.status " +
"!== 'pending' && group.status !== 'updating'",
ngShow: "(group.status !== 'running' && group.status " +
"!== 'pending' && group.status !== 'updating') && group.summary_fields.user_capabilities.start",
ngClass: "group.launch_class",
dataPlacement: "top",
},
@ -180,8 +182,8 @@ export default
ngClick: "cancelUpdate(group.id)",
awToolTip: "Cancel sync process",
'class': 'red-txt',
ngShow: "group.status == 'running' || group.status == 'pending' " +
"|| group.status == 'updating'",
ngShow: "(group.status == 'running' || group.status == 'pending' " +
"|| group.status == 'updating') && group.summary_fields.user_capabilities.start",
dataPlacement: "top",
iconClass: "fa fa-minus-circle"
},
@ -189,7 +191,7 @@ export default
mode: 'all',
ngClick: "copyMoveGroup(group.id)",
awToolTip: 'Copy or move group',
ngShow: "group.id > 0",
ngShow: "group.id > 0 && group.summary_fields.user_capabilities.copy",
dataPlacement: "top"
},
schedule: {
@ -198,21 +200,31 @@ export default
awToolTip: "{{ group.group_schedule_tooltip }}",
ngClass: "group.scm_type_class",
dataPlacement: 'top',
ngHide: "group.summary_fields.inventory_source.source === ''"
ngShow: "!(group.summary_fields.inventory_source.source === '') && group.summary_fields.user_capabilities.schedule"
},
edit: {
//label: 'Edit',
mode: 'all',
ngClick: "editGroup(group.id)",
awToolTip: 'Edit group',
dataPlacement: "top"
dataPlacement: "top",
ngShow: "group.summary_fields.user_capabilities.edit"
},
view: {
//label: 'Edit',
mode: 'all',
ngClick: "editGroup(group.id)",
awToolTip: 'View group',
dataPlacement: "top",
ngShow: "!group.summary_fields.user_capabilities.edit"
},
"delete": {
//label: 'Delete',
mode: 'all',
ngClick: "deleteGroup(group)",
awToolTip: 'Delete group',
dataPlacement: "top"
dataPlacement: "top",
ngShow: "group.summary_fields.user_capabilities.delete"
}
}
});

View File

@ -78,21 +78,31 @@ export default
mode: 'all',
ngClick: "copyMoveHost(host.id)",
awToolTip: 'Copy or move host to another group',
dataPlacement: "top"
dataPlacement: "top",
ngShow: 'host.summary_fields.user_capabilities.edit'
},
edit: {
//label: 'Edit',
ngClick: "editHost(host.id)",
icon: 'icon-edit',
awToolTip: 'Edit host',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'host.summary_fields.user_capabilities.edit'
},
view: {
//label: 'Edit',
ngClick: "editHost(host.id)",
awToolTip: 'View host',
dataPlacement: 'top',
ngShow: '!host.summary_fields.user_capabilities.edit'
},
"delete": {
//label: 'Delete',
ngClick: "deleteHost(host.id, host.name)",
icon: 'icon-trash',
awToolTip: 'Delete host',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'host.summary_fields.user_capabilities.delete'
}
},
@ -122,7 +132,8 @@ export default
ngClick: "createHost()",
awToolTip: "Create a new host",
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD HOST'
buttonContent: '&#43; ADD HOST',
ngShow: 'canAdd'
}
}

View File

@ -55,7 +55,8 @@ export default
basePaths: ['job_templates'],
awToolTip: 'Create a new template',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: 'canAdd'
}
},
@ -68,7 +69,8 @@ export default
mode: 'all',
ngClick: 'submitJob(job_template.id)',
awToolTip: 'Start a job using this template',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'job_template.summary_fields.user_capabilities.start'
},
schedule: {
label: 'Schedule',
@ -76,6 +78,7 @@ export default
ngClick: 'scheduleJob(job_template.id)',
awToolTip: 'Schedule future job template runs',
dataPlacement: 'top',
ngShow: 'job_template.summary_fields.user_capabilities.schedule'
},
copy: {
label: 'Copy',
@ -83,7 +86,7 @@ export default
"class": 'btn-danger btn-xs',
awToolTip: 'Copy template',
dataPlacement: 'top',
ngHide: 'job_template.summary_fields.can_copy===false'
ngShow: 'job_template.summary_fields.user_capabilities.copy'
},
edit: {
label: 'Edit',
@ -91,6 +94,15 @@ export default
awToolTip: 'Edit template',
"class": 'btn-default btn-xs',
dataPlacement: 'top',
ngShow: 'job_template.summary_fields.user_capabilities.edit'
},
view: {
label: 'View',
ngClick: "editJobTemplate(job_template.id)",
awToolTip: 'View template',
"class": 'btn-default btn-xs',
dataPlacement: 'top',
ngShow: '!job_template.summary_fields.user_capabilities.edit'
},
"delete": {
label: 'Delete',
@ -98,6 +110,7 @@ export default
"class": 'btn-danger btn-xs',
awToolTip: 'Delete template',
dataPlacement: 'top',
ngShow: 'job_template.summary_fields.user_capabilities.delete'
}
}
});

View File

@ -65,7 +65,8 @@ export default
ngClick: 'addProject()',
awToolTip: 'Create a new project',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: "canAdd"
},
refresh: {
mode: 'all',
@ -86,30 +87,40 @@ export default
awToolTip: "{{ project.scm_update_tooltip }}",
dataTipWatch: "project.scm_update_tooltip",
ngClass: "project.scm_type_class",
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: "project.summary_fields.user_capabilities.start"
},
schedule: {
mode: 'all',
ngClick: "editSchedules(project.id)",
awToolTip: "{{ project.scm_schedule_tooltip }}",
ngClass: "project.scm_type_class",
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: "project.summary_fields.user_capabilities.schedule"
},
edit: {
ngClick: "editProject(project.id)",
awToolTip: 'Edit the project',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: "project.summary_fields.user_capabilities.edit"
},
view: {
ngClick: "editProject(project.id)",
awToolTip: 'View the project',
dataPlacement: 'top',
ngShow: "!project.summary_fields.user_capabilities.edit",
icon: 'fa-eye',
},
"delete": {
ngClick: "deleteProject(project.id, project.name)",
awToolTip: 'Delete the project',
ngShow: "project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending'",
ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending') && project.summary_fields.user_capabilities.delete",
dataPlacement: 'top'
},
cancel: {
ngClick: "cancelUpdate(project.id, project.name)",
awToolTip: 'Cancel the SCM update',
ngShow: "project.status == 'updating' || project.status == 'running' || project.status == 'pending'",
ngShow: "(project.status == 'updating' || project.status == 'running' || project.status == 'pending') && project.summary_fields.user_capabilities.start",
dataPlacement: 'top'
}
}

View File

@ -78,13 +78,22 @@ export default
mode: "all",
ngClick: "editSchedule(schedule)",
awToolTip: "Edit the schedule",
dataPlacement: "top"
dataPlacement: "top",
ngShow: 'schedule.summary_fields.user_capabilities.edit'
},
"view": {
mode: "all",
ngClick: "editSchedule(schedule)",
awToolTip: "View the schedule",
dataPlacement: "top",
ngShow: '!schedule.summary_fields.user_capabilities.edit'
},
"delete": {
mode: 'all',
ngClick: 'deleteSchedule(schedule.id)',
awToolTip: 'Delete the schedule',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'schedule.summary_fields.user_capabilities.delete'
}
}
});

View File

@ -69,7 +69,8 @@ export default
ngClick: 'addSchedule()',
awToolTip: 'Add a new schedule',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: 'canAdd'
}
},
@ -79,14 +80,23 @@ export default
ngClick: "editSchedule(schedule.id)",
icon: 'icon-edit',
awToolTip: 'Edit schedule',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'schedule.summary_fields.user_capabilities.edit'
},
view: {
label: 'View',
ngClick: "editSchedule(schedule.id)",
awToolTip: 'View schedule',
dataPlacement: 'top',
ngShow: '!schedule.summary_fields.user_capabilities.edit'
},
"delete": {
label: 'Delete',
ngClick: "deleteSchedule(schedule.id)",
icon: 'icon-trash',
awToolTip: 'Delete schedule',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'schedule.summary_fields.user_capabilities.delete'
}
}
});

View File

@ -47,7 +47,8 @@ export default
ngClick: 'addTeam()',
awToolTip: 'Create a new team',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: 'canAdd'
}
},
@ -61,16 +62,25 @@ export default
icon: 'icon-edit',
"class": 'btn-xs btn-default',
awToolTip: 'Edit team',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'team.summary_fields.user_capabilities.edit'
},
view: {
label: 'View',
ngClick: "editTeam(team.id)",
"class": 'btn-xs btn-default',
awToolTip: 'View team',
dataPlacement: 'top',
ngShow: '!team.summary_fields.user_capabilities.edit'
},
"delete": {
label: 'Delete',
ngClick: "deleteTeam(team.id, team.name)",
icon: 'icon-trash',
"class": 'btn-xs btn-danger',
awToolTip: 'Delete team',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'team.summary_fields.user_capabilities.delete'
}
}
});

View File

@ -44,7 +44,8 @@ export default
basePaths: ['organizations', 'users'], // base path must be in list, or action not available
awToolTip: 'Create a new user',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: 'canAdd'
}
},
@ -58,7 +59,17 @@ export default
icon: 'icon-edit',
"class": 'btn-xs btn-default',
awToolTip: 'Edit user',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'user.summary_fields.user_capabilities.edit'
},
view: {
label: 'View',
ngClick: "editUser(user.id)",
"class": 'btn-xs btn-default',
awToolTip: 'View user',
dataPlacement: 'top',
ngShow: '!user.summary_fields.user_capabilities.edit'
},
"delete": {
@ -67,7 +78,8 @@ export default
icon: 'icon-trash',
"class": 'btn-xs btn-danger',
awToolTip: 'Delete user',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'user.summary_fields.user_capabilities.delete'
}
}
});

View File

@ -17,6 +17,7 @@
<div class="MgmtCards-actionItems">
<button class="MgmtCards-actionItem List-actionButton"
ng-click='chooseRunJob(card.id, card.name)'
ng-show='current_user.is_superuser'
data-placement="top" aw-tool-tip="Launch Management Job" data-original-title="" title="">
<i class="MgmtCards-actionItemIcon icon-launch"></i>
</button>
@ -27,6 +28,7 @@
</button>
<button class="MgmtCards-actionItem List-actionButton"
ng-click='goToNotifications(card, card.id)'
ng-show='current_user.is_superuser'
data-placement="top" aw-tool-tip="Configure Notifications" data-original-title="" title="" ng-class="{'List-editButton--selected': activeCard === card.id && cardAction === 'notifications'}">
<i class="MgmtCards-actionItemIcon fa fa-bell-o"></i>
</button>

View File

@ -9,14 +9,23 @@ export default
'NotificationsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty',
'GenerateForm', 'SearchInit' , 'PaginateInit', 'LookUpInit',
'OrganizationList', '$scope', '$state', 'CreateSelect2', 'GetChoices',
'NotificationsTypeChange', 'ParseTypeChange',
'NotificationsTypeChange', 'ParseTypeChange', 'Alert',
function(
$rootScope, pagination, $compile, SchedulerInit, Rest, Wait,
NotificationsFormObject, ProcessErrors, GetBasePath, Empty,
GenerateForm, SearchInit, PaginateInit, LookUpInit,
OrganizationList, $scope, $state, CreateSelect2, GetChoices,
NotificationsTypeChange, ParseTypeChange
NotificationsTypeChange, ParseTypeChange, Alert
) {
Rest.setUrl(GetBasePath('projects'));
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add a notification template.', 'alert-info');
}
});
var generator = GenerateForm,
form = NotificationsFormObject,
url = GetBasePath('notification_templates');

View File

@ -26,6 +26,13 @@ export default
url = GetBasePath('notification_templates');
$scope.notification_template = notification_template;
$scope.$watch('notification_template.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
generator.inject(form, {
mode: 'edit' ,
scope:$scope,

View File

@ -8,12 +8,12 @@ export default
[ '$rootScope','Wait', 'generateList', 'NotificationTemplatesList',
'GetBasePath' , 'SearchInit' , 'PaginateInit', 'Rest' ,
'ProcessErrors', 'Prompt', '$state', 'GetChoices', 'Empty', 'Find',
'ngToast', '$compile', '$filter',
'ngToast', '$compile', '$filter', 'rbacUiControlService',
function(
$rootScope,Wait, GenerateList, NotificationTemplatesList,
GetBasePath, SearchInit, PaginateInit, Rest,
ProcessErrors, Prompt, $state, GetChoices, Empty, Find, ngToast,
$compile, $filter) {
$compile, $filter, rbacUiControlService) {
var scope = $rootScope.$new(),
defaultUrl = GetBasePath('notification_templates'),
list = NotificationTemplatesList,
@ -24,6 +24,13 @@ export default
scope: scope
});
scope.canAdd = false;
rbacUiControlService.canAdd("notification_templates")
.then(function(canAdd) {
scope.canAdd = canAdd;
});
scope.removePostRefresh = scope.$on('PostRefresh', function () {
Wait('stop');
if (scope.notification_templates) {

View File

@ -27,13 +27,15 @@ export default function() {
type: 'text',
addRequired: true,
editRequired: true,
capitalize: false
capitalize: false,
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
editRequired: false,
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
organization: {
label: 'Organization',
@ -44,7 +46,8 @@ export default function() {
awRequiredWhen: {
reqExpression: "organizationrequired",
init: "true"
}
},
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
notification_type: {
label: 'Type',
@ -54,13 +57,15 @@ export default function() {
class: 'NotificationsForm-typeSelect',
ngOptions: 'type.label for type in notification_type_options track by type.value',
ngChange: 'typeChange()',
hasSubForm: true
hasSubForm: true,
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
username: {
label: 'Username',
type: 'text',
ngShow: "notification_type.value == 'email' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
host: {
@ -71,7 +76,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'email' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
sender: {
label: 'Sender Email',
@ -81,7 +87,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'email' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
recipients: {
label: 'Recipient List',
@ -97,7 +104,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'email' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
password: {
labelBind: 'passwordLabel',
@ -108,7 +116,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
port: {
labelBind: 'portLabel',
@ -122,7 +131,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'email' || notification_type.value == 'irc'",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
channels: {
label: 'Destination Channels',
@ -138,7 +148,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'slack'",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
rooms: {
label: 'Destination Channels',
@ -154,7 +165,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'hipchat'",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
token: {
labelBind: 'tokenLabel',
@ -165,7 +177,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'slack' || notification_type.value == 'pagerduty' || notification_type.value == 'hipchat'",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
account_token: {
label: 'Account Token',
@ -176,7 +189,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'twilio' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
from_number: {
label: 'Source Phone Number',
@ -188,7 +202,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'twilio' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
to_numbers: {
label: 'Destination SMS Number',
@ -204,7 +219,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'twilio' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
account_sid: {
label: 'Account SID',
@ -214,7 +230,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'twilio' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
subdomain: {
label: 'Pagerduty subdomain',
@ -224,7 +241,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'pagerduty' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
service_key: {
label: 'API Service/Integration Key',
@ -234,7 +252,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'pagerduty' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
client_name: {
label: 'Client Identifier',
@ -244,7 +263,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'pagerduty' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
message_from: {
label: 'Label to be shown with notification',
@ -254,7 +274,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'hipchat' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
api_url: {
label: 'API URL',
@ -265,7 +286,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'hipchat' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
color: {
label: 'Notification Color',
@ -277,13 +299,15 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'hipchat' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
notify: {
label: 'Notify Channel',
type: 'checkbox',
ngShow: "notification_type.value == 'hipchat' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
url: {
label: 'Target URL',
@ -293,7 +317,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'webhook' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
headers: {
label: 'HTTP Headers',
@ -313,7 +338,8 @@ export default function() {
'</pre></p>',
dataPlacement: 'right',
ngShow: "notification_type.value == 'webhook' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
server: {
label: 'IRC Server Address',
@ -323,7 +349,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'irc' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
nickname: {
label: 'IRC Nick',
@ -333,7 +360,8 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'irc' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
targets: {
label: 'Destination Channels or Users',
@ -349,13 +377,15 @@ export default function() {
init: "false"
},
ngShow: "notification_type.value == 'irc' ",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
use_ssl: {
label: 'SSL Connection',
type: 'checkbox',
ngShow: "notification_type.value == 'irc'",
subForm: 'typeSubForm'
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
checkbox_group: {
label: 'Options',
@ -367,13 +397,15 @@ export default function() {
label: 'Use TLS',
type: 'checkbox',
ngShow: "notification_type.value == 'email' ",
labelClass: 'checkbox-options stack-inline'
labelClass: 'checkbox-options stack-inline',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
}, {
name: 'use_ssl',
label: 'Use SSL',
type: 'checkbox',
ngShow: "notification_type.value == 'email'",
labelClass: 'checkbox-options stack-inline'
labelClass: 'checkbox-options stack-inline',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
}]
}
},
@ -381,9 +413,15 @@ export default function() {
buttons: { //for now always generates <button> tags
cancel: {
ngClick: 'formCancel()',
ngShow: '(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngClick: 'formSave()',
ngShow: '(notification_template.summary_fields.user_capabilities.edit || canAdd)', //$scope.function to call on click, optional
ngDisabled: true //Disable when $pristine or $invalid, optional
}
}

View File

@ -49,7 +49,8 @@ export default function(){
ngClick: 'addNotification()',
awToolTip: 'Create a new custom inventory',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
buttonContent: '&#43; ADD',
ngShow: 'canAdd'
}
},
@ -62,7 +63,8 @@ export default function(){
label: 'Edit',
"class": 'btn-sm',
awToolTip: 'Test notification',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'notification_template.summary_fields.user_capabilities.edit'
},
edit: {
ngClick: "editNotification(notification_template.id)",
@ -70,7 +72,16 @@ export default function(){
label: 'Edit',
"class": 'btn-sm',
awToolTip: 'Edit notification',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'notification_template.summary_fields.user_capabilities.edit'
},
view: {
ngClick: "editNotification(notification_template.id)",
label: 'View',
"class": 'btn-sm',
awToolTip: 'View notification',
dataPlacement: 'top',
ngShow: '!notification_template.summary_fields.user_capabilities.edit'
},
"delete": {
ngClick: "deleteNotification(notification_template.id, notification_template.name)",
@ -78,7 +89,8 @@ export default function(){
label: 'Delete',
"class": 'btn-sm',
awToolTip: 'Delete notification',
dataPlacement: 'top'
dataPlacement: 'top',
ngShow: 'notification_template.summary_fields.user_capabilities.delete'
}
}
};

View File

@ -65,7 +65,8 @@ export default function(){
ngClick: 'addNotificationTemplate()',
awToolTip: 'Create a new notification template',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD NOTIFICATION TEMPLATE'
buttonContent: '&#43; ADD NOTIFICATION TEMPLATE',
ngShow: 'current_user.is_superuser || (current_user_admin_orgs && current_user_admin_orgs.length > 0)'
}
}

View File

@ -15,13 +15,21 @@
*/
export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', 'GetChoices',
'$state',
function(Wait, GetBasePath, ProcessErrors, Rest, GetChoices, $state) {
'$state', '$rootScope',
function(Wait, GetBasePath, ProcessErrors, Rest, GetChoices, $state, $rootScope) {
return function(params) {
var scope = params.scope,
url = params.url,
id = params.id;
scope.current_user_admin_orgs = [];
Rest.setUrl($rootScope.current_user.related.admin_of_organizations);
Rest.get()
.success(function(data) {
scope.current_user_admin_orgs = data.results.map(i => i.name);
});
scope.addNotificationTemplate = function(){
$state.go('notifications.add');
};

View File

@ -12,6 +12,15 @@ export default ['$scope', '$rootScope', '$compile', '$location',
$stateParams, OrganizationForm, GenerateForm, Rest, Alert, ProcessErrors,
ClearScope, GetBasePath, ReturnToCaller, Wait, $state) {
Rest.setUrl(GetBasePath('organizations'));
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add an organization.', 'alert-info');
}
});
ClearScope();
// Inject dynamic view

View File

@ -25,6 +25,12 @@ export default ['$scope', '$rootScope', '$compile', '$location',
id = $stateParams.organization_id,
relatedSets = {};
$scope.$watch('organization_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
$scope.$parent.activeMode = 'edit';
$scope.$parent.activeCard = parseInt(id);

View File

@ -8,15 +8,22 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
'$log', '$compile', 'Rest', 'PaginateInit',
'SearchInit', 'OrganizationList', 'Alert', 'Prompt', 'ClearScope',
'ProcessErrors', 'GetBasePath', 'Wait',
'$state', 'generateList', 'Refresh', '$filter',
'$state', 'generateList', 'Refresh', '$filter', 'rbacUiControlService',
function($stateParams, $scope, $rootScope, $location,
$log, $compile, Rest, PaginateInit,
SearchInit, OrganizationList, Alert, Prompt, ClearScope,
ProcessErrors, GetBasePath, Wait,
$state, generateList, Refresh, $filter) {
$state, generateList, Refresh, $filter, rbacUiControlService) {
ClearScope();
$scope.canAdd = false;
rbacUiControlService.canAdd("organizations")
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
var defaultUrl = GetBasePath('organizations'),
list = OrganizationList,
pageSize = 24,
@ -25,6 +32,7 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
var parseCardData = function(cards) {
return cards.map(function(card) {
var val = {}, url = '/#/organizations/' + card.id + '/';
val.user_capabilities = card.summary_fields.user_capabilities;
val.name = card.name;
val.id = card.id;
val.description = card.description || undefined;

View File

@ -13,6 +13,7 @@
<div class="List-actions">
<button class="btn List-buttonSubmit"
aw-tool-tip="Create a new organization"
ng-show="canAdd"
ng-click="addOrganization()">
+ ADD
</button>
@ -31,13 +32,23 @@
<div class="OrgCards-actionItems">
<button class="OrgCards-actionItem
List-actionButton"
ng-show="card.user_capabilities.edit"
ng-class="{'List-editButton--selected': (activeCard === card.id || card.isActiveCard) && activeMode === 'edit' }"
ng-click="editOrganization(card.id)">
<i class="OrgCards-actionItemIcon fa fa-pencil">
</i>
</button>
<button class="OrgCards-actionItem
List-actionButton"
ng-show="!card.user_capabilities.edit"
ng-class="{'List-editButton--selected': (activeCard === card.id || card.isActiveCard) && activeMode === 'edit' }"
ng-click="editOrganization(card.id)">
<i class="OrgCards-actionItemIcon fa fa-search-plus">
</i>
</button>
<button class="OrgCards-actionItem List-actionButton
List-actionButton--delete"
ng-show="card.user_capabilities.delete"
ng-click="deleteOrganization(card.id, card.name)">
<i class="OrgCards-actionItemIcon
fa fa-trash-o">

View File

@ -32,13 +32,13 @@
</div>
</div>
<div class="SurveyMaker-content">
<div class="SurveyMaker-questionPanel">
<div class="SurveyMaker-questionPanel" ng-show="(job_template_obj.summary_fields.user_capabilities.edit || canAdd)">
<div id="survey_maker_question_form"></div>
</div>
<div class="SurveyMaker-separatorPanel">
<div class="SurveyMaker-separatorPanel" ng-show="(job_template_obj.summary_fields.user_capabilities.edit || canAdd)">
<div class="SurveyMaker-contentSeparator"></div>
</div>
<div class="SurveyMaker-previewPanel">
<div class="SurveyMaker-previewPanel" ng-class="{'SurveyMaker-previewPanel--viewOnly': !(job_template_obj.summary_fields.user_capabilities.edit || canAdd)}">
<div style="display: flex; flex-direction: column; width: 100%;">
<div class="SurveyMaker-panelHeader">PREVIEW</div>
<div class="SurveyMaker-panelBody">
@ -56,13 +56,13 @@
<i>{{question.question_description}}</i>
</div>
<div class="SurveyMaker-previewInputRow">
<span dnd-handle class="SurveyMaker-reorderButton" data-placement="top" aw-tool-tip="Drag to reorder question" data-original-title="" title="">
<span dnd-handle class="SurveyMaker-reorderButton" data-placement="top" aw-tool-tip="Drag to reorder question" data-original-title="" title="" ng-show="(job_template_obj.summary_fields.user_capabilities.edit || canAdd)">
<i class="fa fa-ellipsis-v"></i>
<span>&nbsp;</span>
<i class="fa fa-ellipsis-v"></i>
</span>
<survey-question class="SurveyMaker-previewInput" preview="true" question="question" ng-required="question.required" ng-disabled=true></survey-question>
<div class="SurveyMaker-previewActions">
<div class="SurveyMaker-previewActions" ng-show="(job_template_obj.summary_fields.user_capabilities.edit || canAdd)">
<button class="List-actionButton" data-placement="top" ng-class="{'SurveyMaker-previewActions--selected' : editQuestionIndex == $index}" ng-click="editQuestion($index)" aw-tool-tip="Edit question" data-original-title="" title="">
<i class="fa fa-pencil"></i>
</button>
@ -80,9 +80,10 @@
</div>
<div class="SurveyMaker-panelFooter">
<div class="Form-buttons">
<button id="survey-delete-button" class="btn btn-sm SurveyMaker-deleteButton" ng-show="survey_exists" ng-click="showDeleteOverlay('survey')">DELETE SURVEY</button>
<button id="survey-close-button" class="btn btn-sm Form-buttonDefault" ng-click="closeSurvey('survey-modal-dialog')">CANCEL</button>
<button id="survey-save-button" class="btn btn-sm Form-saveButton" ng-click="saveSurvey()" ng-disabled="survey_questions.length < 1 || !can_edit || editQuestionIndex !== null">SAVE</button>
<button id="survey-delete-button" class="btn btn-sm SurveyMaker-deleteButton" ng-show="survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAdd)" ng-click="showDeleteOverlay('survey')">DELETE SURVEY</button>
<button id="survey-close-button" class="btn btn-sm Form-buttonDefault" ng-click="closeSurvey('survey-modal-dialog')" ng-show="(job_template_obj.summary_fields.user_capabilities.edit || canAdd)">CANCEL</button>
<button id="survey-close-button" class="btn btn-sm Form-buttonDefault" ng-click="closeSurvey('survey-modal-dialog')" ng-show="!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)">CLOSE</button>
<button id="survey-save-button" class="btn btn-sm Form-saveButton" ng-click="saveSurvey()" ng-disabled="survey_questions.length < 1 || !can_edit || editQuestionIndex !== null" ng-show="(job_template_obj.summary_fields.user_capabilities.edit || canAdd)">SAVE</button>
</div>
</div>
</div>

View File

@ -14,11 +14,11 @@
export default [
'$scope', '$compile', '$location', '$stateParams', 'SchedulesList', 'Rest',
'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath', 'Wait',
'Find', 'LoadSchedulesScope', 'GetChoices', '$q', '$state',
'Find', 'LoadSchedulesScope', 'GetChoices', '$q', '$state', 'rbacUiControlService',
function ($scope, $compile, $location, $stateParams,
SchedulesList, Rest, ProcessErrors, ReturnToCaller, ClearScope,
GetBasePath, Wait, Find, LoadSchedulesScope, GetChoices,
$q, $state) {
$q, $state, rbacUiControlService) {
var schedList = _.cloneDeep(SchedulesList);
ClearScope();
@ -48,6 +48,14 @@ export default [
}
$scope.removeParentLoaded = $scope.$on('ParentLoaded', function() {
url += "schedules/";
$scope.canAdd = false;
rbacUiControlService.canAdd(url)
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
schedList.well = true;
// include name of item in listTitle

View File

@ -9,7 +9,6 @@ export default ['$compile', '$filter', '$state', '$stateParams', 'AddSchedule',
'Rest', 'ParamPass',
function($compile, $filter, $state, $stateParams, AddSchedule, Wait, $scope,
$rootScope, CreateSelect2, ParseTypeChange, GetBasePath, Rest, ParamPass) {
$scope.processSchedulerEndDt = function(){
// set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight
var dt = new Date($scope.schedulerUTCTime);

View File

@ -865,6 +865,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += ">";
html += "<input type=\"checkbox\" ng-model=\"" + field.subCheckbox.variable + "\" ";
html += (field.subCheckbox.ngChange) ? "ng-change=\"" + field.subCheckbox.ngChange + "\" " : "";
html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : "";
html += "id=\"" + this.form.name + "_" + fld + "_ask_chbox\" ";
html += ">";
html += field.subCheckbox.text ? field.subCheckbox.text : "";
@ -987,6 +988,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if (field.subCheckbox.ngDisabled) {
html += "ng-disabled='" + field.subCheckbox.ngDisabled + "'";
}
html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : "";
html += ">";
html += field.subCheckbox.text ? field.subCheckbox.text : "";
html += "</label>";
@ -1084,6 +1086,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if (field.subCheckbox.ngDisabled) {
html += "ng-disabled='" + field.subCheckbox.ngDisabled + "'";
}
html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : "";
html += ">";
html += field.subCheckbox.text ? field.subCheckbox.text : "";
html += "</label>";
@ -1151,6 +1154,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if (field.subCheckbox.ngDisabled) {
html += "ng-disabled='" + field.subCheckbox.ngDisabled + "'";
}
html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : "";
html += ">";
html += field.subCheckbox.text ? field.subCheckbox.text : "";
html += "</label>";
@ -1199,6 +1203,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += (field.min || field.min === 0) ? this.attr(field, 'min') : "";
html += (field.max) ? this.attr(field, 'max') : "";
html += (field.ngChange) ? this.attr(field, 'ngChange') : "";
html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : "";
html += (field.slider) ? "id=\"" + fld + "-number\"" : (field.id) ? this.attr(field, 'id') : "";
html += (options.mode === 'edit' && field.editRequired) ? "required " : "";
html += (options.mode === 'add' && field.addRequired) ? "required " : "";
@ -1219,6 +1224,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "<input type=\"checkbox\" ng-model=\"" +
field.subCheckbox.variable + "\" ";
html += (field.subCheckbox.ngChange) ? "ng-change=\"" + field.subCheckbox.ngChange + "\" " : "";
html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : "";
html += "id=\"" + this.form.name + "_" + fld + "_ask_chbox\" ";
html += ">";
html += field.subCheckbox.text ? field.subCheckbox.text : "";
@ -1422,6 +1428,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "<input type=\"checkbox\" ng-model=\"" +
field.subCheckbox.variable + "\" ";
html += (field.subCheckbox.ngChange) ? "ng-change=\"" + field.subCheckbox.ngChange + "\" " : "";
html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : "";
html += "id=\"" + this.form.name + "_" + fld + "_ask_chbox\" ";
html += ">";
html += field.subCheckbox.text ? field.subCheckbox.text : "";
@ -1693,6 +1700,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
button.label = 'Cancel';
button['class'] = 'Form-cancelButton';
}
if (btn === 'close') {
button.label = 'Close';
button['class'] = 'Form-cancelButton';
}
if (btn === 'launch') {
button.label = 'Launch';
button['class'] = 'Form-launchButton';
@ -1705,6 +1716,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
button.label = 'Edit Survey';
button['class'] = 'Form-surveyButton';
}
if (btn === 'view_survey') {
button.label = 'View Survey';
button['class'] = 'Form-surveyButton';
}
// Build button HTML
html += "<button type=\"button\" ";

View File

@ -11,12 +11,14 @@ import lodashAsPromised from './lodash-as-promised';
import stringFilters from './string-filters/main';
import truncatedText from './truncated-text.directive';
import stateExtender from './stateExtender.provider';
import rbacUiControl from './rbacUiControl';
export default
angular.module('shared', [listGenerator.name,
pagination.name,
stringFilters.name,
'ui.router'
'ui.router',
rbacUiControl.name
])
.factory('lodashAsPromised', lodashAsPromised)
.directive('truncatedText', truncatedText)

View File

@ -0,0 +1,32 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
angular.module('rbacUiControl', [])
.service('rbacUiControlService', ['$q', 'GetBasePath', 'Rest', 'Wait', function($q, GetBasePath, Rest, Wait){
this.canAdd = function(apiPath) {
var canAddVal = $q.defer();
if (apiPath.indexOf("api/v1") > -1) {
Rest.setUrl(apiPath);
} else {
Rest.setUrl(GetBasePath(apiPath));
}
Wait("start");
Rest.options()
.success(function(data) {
if (data.actions.POST) {
canAddVal.resolve(true);
} else {
canAddVal.reject(false);
}
Wait("stop");
});
return canAddVal.promise;
};
}]);

View File

@ -9,13 +9,13 @@ module.exports = function(config) {
browsers: ['Chrome', 'Firefox'],
coverageReporter: {
reporters: [
{ type: 'html', subdir: 'html' }
{ type: 'html', subdir: 'html' },
]
},
frameworks: [
'jasmine',
],
reporters: ['progress', 'coverage'],
reporters: ['progress', 'coverage', 'junit'],
files: [
'./client/src/app.js',
'./node_modules/angular-mocks/angular-mocks.js',
@ -86,7 +86,9 @@ module.exports = function(config) {
}
},
junitReporter: {
outputFile: 'coverage/test-results.xml'
outputDir: 'coverage',
outputFile: 'ui-unit-test-results.xml',
useBrowserName: false
}
});
};

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@
"expose-loader": "^0.7.1",
"grunt": "^1.0.1",
"grunt-browser-sync": "^2.2.0",
"grunt-cli": "^1.2.0",
"grunt-concurrent": "^2.3.0",
"grunt-contrib-clean": "^1.0.0",
"grunt-contrib-concat": "^1.0.1",
@ -56,12 +57,15 @@
"karma-firefox-launcher": "^1.0.0",
"karma-html2js-preprocessor": "^1.0.0",
"karma-jasmine": "^1.0.2",
"karma-junit-reporter": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.2",
"karma-sauce-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.8.0",
"less-plugin-autoprefix": "^1.4.2",
"load-grunt-configs": "^1.0.0",
"load-grunt-tasks": "^3.5.0",
"phantomjs-prebuilt": "^2.1.12",
"time-grunt": "^1.4.0",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"

View File

@ -29,12 +29,34 @@ RUN yum install -y \
# Remove the 2 lines below and uncomment the 3 lines above to build
# RPMs with the old JS build system.
RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash -
RUN yum install -y nodejs
RUN npm set progress=false
RUN yum install -y nodejs
WORKDIR "/ansible-tower"
ENV VENV_BASE="/venv"
# Copy requirements files
COPY requirements/*.txt requirements/
# Copy __init__.py so the Makefile can retrieve `awx.__version__`
COPY awx/__init__.py awx/
# Copy Makefile
COPY Makefile .
# Install tower runtime virtualenvs
ENV SWIG_FEATURES="-cpperraswarn -includeall -I/usr/include/openssl"
RUN make requirements
# Install tower test requirements
ENV VENV_BASE=""
RUN make requirements_jenkins
# Build front-end deps
COPY awx/ui/package.json awx/ui/
RUN npm set progress=false
RUN make ui-deps-built
ENTRYPOINT ["/bin/bash", "-c"]
CMD ["bash"]

View File

@ -6,6 +6,13 @@ $ docker-compose -f tools/docker-compose/unit-tests/docker-compose.yml run unit-
This will start the container, install the dependencies, and run the unit tests.
To rebuild:
```shell
$ docker-compose -f tools/docker-compose/unit-tests/docker-compose.yml build
```
If you just want to pop into a shell and poke around, run:
```shell

View File

@ -7,7 +7,7 @@ services:
dockerfile: tools/docker-compose/unit-tests/Dockerfile
environment:
SWIG_FEATURES: "-cpperraswarn -includeall -I/usr/include/openssl"
command: ["make requirements_test test"]
TEST_DIRS: "awx/main/tests/unit"
command: ["make test"]
volumes:
- ../../../:/ansible-tower