diff --git a/awx/api/serializers.py b/awx/api/serializers.py
index 08722afc20..fb86e6a1de 100644
--- a/awx/api/serializers.py
+++ b/awx/api/serializers.py
@@ -679,12 +679,13 @@ class UserSerializer(BaseSerializer):
password = serializers.CharField(required=False, default='', write_only=True,
help_text='Write-only field used to change the password.')
ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
+ is_system_auditor = serializers.BooleanField(default=False)
class Meta:
model = User
fields = ('*', '-name', '-description', '-modified',
'-summary_fields', 'username', 'first_name', 'last_name',
- 'email', 'is_superuser', 'password', 'ldap_dn')
+ 'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn')
def to_representation(self, obj):
ret = super(UserSerializer, self).to_representation(obj)
diff --git a/awx/api/views.py b/awx/api/views.py
index 84fd3347e7..6f3f83ac21 100644
--- a/awx/api/views.py
+++ b/awx/api/views.py
@@ -720,14 +720,31 @@ class OrganizationInventoriesList(SubListAPIView):
parent_model = Organization
relationship = 'inventories'
-class OrganizationUsersList(SubListCreateAttachDetachAPIView):
+
+class BaseUsersList(SubListCreateAttachDetachAPIView):
+ def post(self, request, *args, **kwargs):
+ ret = super(BaseUsersList, self).post( request, *args, **kwargs)
+ try:
+ if request.data.get('is_system_auditor', False):
+ # This is a faux-field that just maps to checking the system
+ # auditor role member list.. unfortunately this means we can't
+ # set it on creation, and thus needs to be set here.
+ user = User.objects.get(id=ret.data['id'])
+ user.is_system_auditor = request.data['is_system_auditor']
+ ret.data['is_system_auditor'] = request.data['is_system_auditor']
+ except AttributeError as exc:
+ print(exc)
+ pass
+ return ret
+
+class OrganizationUsersList(BaseUsersList):
model = User
serializer_class = UserSerializer
parent_model = Organization
relationship = 'member_role.members'
-class OrganizationAdminsList(SubListCreateAttachDetachAPIView):
+class OrganizationAdminsList(BaseUsersList):
model = User
serializer_class = UserSerializer
@@ -830,7 +847,7 @@ class TeamDetail(RetrieveUpdateDestroyAPIView):
model = Team
serializer_class = TeamSerializer
-class TeamUsersList(SubListCreateAttachDetachAPIView):
+class TeamUsersList(BaseUsersList):
model = User
serializer_class = UserSerializer
@@ -1097,6 +1114,21 @@ class UserList(ListCreateAPIView):
model = User
serializer_class = UserSerializer
+ def post(self, request, *args, **kwargs):
+ ret = super(UserList, self).post( request, *args, **kwargs)
+ try:
+ if request.data.get('is_system_auditor', False):
+ # This is a faux-field that just maps to checking the system
+ # auditor role member list.. unfortunately this means we can't
+ # set it on creation, and thus needs to be set here.
+ user = User.objects.get(id=ret.data['id'])
+ user.is_system_auditor = request.data['is_system_auditor']
+ ret.data['is_system_auditor'] = request.data['is_system_auditor']
+ except AttributeError as exc:
+ print(exc)
+ pass
+ return ret
+
class UserMeList(ListAPIView):
model = User
@@ -1228,17 +1260,25 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
obj = self.get_object()
can_change = request.user.can_access(User, 'change', obj, request.data)
can_admin = request.user.can_access(User, 'admin', obj, request.data)
+
+ su_only_edit_fields = ('is_superuser', 'is_system_auditor')
+ admin_only_edit_fields = ('last_name', 'first_name', 'username', 'is_active')
+
+ fields_to_check = ()
+ if not request.user.is_superuser:
+ fields_to_check += su_only_edit_fields
+
if can_change and not can_admin:
- admin_only_edit_fields = ('last_name', 'first_name', 'username',
- 'is_active', 'is_superuser')
- changed = {}
- for field in admin_only_edit_fields:
- left = getattr(obj, field, None)
- right = request.data.get(field, None)
- if left is not None and right is not None and left != right:
- changed[field] = (left, right)
- if changed:
- raise PermissionDenied('Cannot change %s.' % ', '.join(changed.keys()))
+ fields_to_check += admin_only_edit_fields
+
+ bad_changes = {}
+ for field in fields_to_check:
+ left = getattr(obj, field, None)
+ right = request.data.get(field, None)
+ if left is not None and right is not None and left != right:
+ bad_changes[field] = (left, right)
+ if bad_changes:
+ raise PermissionDenied('Cannot change %s.' % ', '.join(bad_changes.keys()))
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py
index ed13b4a30c..5528776bdd 100644
--- a/awx/main/models/__init__.py
+++ b/awx/main/models/__init__.py
@@ -55,6 +55,20 @@ def user_get_admin_of_organizations(user):
User.add_to_class('organizations', user_get_organizations)
User.add_to_class('admin_of_organizations', user_get_admin_of_organizations)
+@property
+def user_is_system_auditor(user):
+ return Role.singleton('system_auditor').members.filter(id=user.id).exists()
+
+@user_is_system_auditor.setter
+def user_is_system_auditor(user, tf):
+ if user.id:
+ if tf:
+ Role.singleton('system_auditor').members.add(user)
+ else:
+ Role.singleton('system_auditor').members.remove(user)
+
+User.add_to_class('is_system_auditor', user_is_system_auditor)
+
# Import signal handlers only after models have been defined.
import awx.main.signals # noqa
diff --git a/awx/ui/client/legacy-styles/forms.less b/awx/ui/client/legacy-styles/forms.less
index e34359ee18..eb68a821a4 100644
--- a/awx/ui/client/legacy-styles/forms.less
+++ b/awx/ui/client/legacy-styles/forms.less
@@ -49,7 +49,7 @@
min-height: 40px;
}
-.Form-title--is_superuser{
+.Form-title--is_superuser, .Form-title--is_system_auditor, .Form-title--is_ldap_user{
height:15px;
color: @default-interface-txt;
background-color: @default-list-header-bg;
@@ -61,7 +61,7 @@
margin-left: 10px;
text-transform: uppercase;
font-weight: 100;
- position: absolute;
+ //position: absolute;
margin-top: 2.25px;
height: 16px;
}
diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js
index f6ac43d219..09bde96e87 100644
--- a/awx/ui/client/src/controllers/Users.js
+++ b/awx/ui/client/src/controllers/Users.js
@@ -10,6 +10,26 @@
* @description This controller's the Users page
*/
+const user_type_options = [
+ {type: 'normal' , label: 'Normal User' },
+ {type: 'system_auditor' , label: 'System Auditor' },
+ {type: 'system_administrator', label: 'System Administrator' },
+];
+
+function user_type_sync($scope) {
+ return (type_option) => {
+ $scope.is_superuser = false;
+ $scope.is_system_auditor = false;
+ switch (type_option.type) {
+ case 'system_administrator':
+ $scope.is_superuser = true;
+ break;
+ case 'system_auditor':
+ $scope.is_system_auditor = true;
+ break;
+ }
+ };
+}
export function UsersList($scope, $rootScope, $location, $log, $stateParams,
Rest, Alert, UserList, GenerateList, Prompt, SearchInit, PaginateInit,
@@ -116,10 +136,13 @@ UsersList.$inject = ['$scope', '$rootScope', '$location', '$log',
];
+
+
+
export function UsersAdd($scope, $rootScope, $compile, $location, $log,
$stateParams, UserForm, GenerateForm, Rest, Alert, ProcessErrors,
ReturnToCaller, ClearScope, GetBasePath, LookUpInit, OrganizationList,
- ResetForm, Wait, $state) {
+ ResetForm, Wait, CreateSelect2, $state) {
ClearScope();
@@ -138,6 +161,15 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
generator.reset();
+ $scope.user_type_options = user_type_options;
+ $scope.user_type = user_type_options[0]
+ $scope.$watch('user_type', user_type_sync($scope));
+
+ CreateSelect2({
+ element: '#user_user_type',
+ multiple: false
+ });
+
// Configure the lookup dialog. If we're adding a user through the Organizations tab,
// default the Organization value.
LookUpInit({
@@ -177,7 +209,8 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
data[fld] = $scope[fld];
}
}
- data.is_superuser = data.is_superuser || false;
+ data.is_superuser = $scope.is_superuser;
+ data.is_system_auditor = $scope.is_system_auditor;
Wait('start');
Rest.post(data)
.success(function (data) {
@@ -215,14 +248,14 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
UsersAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log',
'$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath',
- 'LookUpInit', 'OrganizationList', 'ResetForm', 'Wait', '$state'
+ 'LookUpInit', 'OrganizationList', 'ResetForm', 'Wait', 'CreateSelect2', '$state'
];
export function UsersEdit($scope, $rootScope, $location,
$stateParams, UserForm, GenerateForm, Rest, ProcessErrors,
RelatedSearchInit, RelatedPaginateInit, ClearScope,
- GetBasePath, ResetForm, Wait, $state) {
+ GetBasePath, ResetForm, Wait, CreateSelect2 ,$state) {
ClearScope();
@@ -237,6 +270,10 @@ export function UsersEdit($scope, $rootScope, $location,
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
+ $scope.user_type_options = user_type_options;
+ $scope.user_type = user_type_options[0]
+ $scope.$watch('user_type', user_type_sync($scope));
+
var setScopeFields = function(data){
_(data)
.pick(function(value, key){
@@ -278,6 +315,8 @@ export function UsersEdit($scope, $rootScope, $location,
data[key] = $scope[key];
}
});
+ data.is_superuser = $scope.is_superuser;
+ data.is_system_auditor = $scope.is_system_auditor;
return data;
};
@@ -292,6 +331,24 @@ export function UsersEdit($scope, $rootScope, $location,
master.ldap_user = $scope.ldap_user;
$scope.socialAuthUser = (data.auth.length > 0) ? true : false;
+ $scope.user_type = $scope.user_type_options[0];
+ $scope.is_system_auditor = false;
+ $scope.is_superuser = false;
+ if (data.is_system_auditor) {
+ $scope.user_type = $scope.user_type_options[1];
+ $scope.is_system_auditor = true;
+ }
+ if (data.is_superuser) {
+ $scope.user_type = $scope.user_type_options[2];
+ $scope.is_superuser = true;
+ }
+
+ CreateSelect2({
+ element: '#user_user_type',
+ multiple: false
+ });
+
+
setScopeFields(data);
setScopeRelated(data, form.related);
@@ -353,5 +410,5 @@ export function UsersEdit($scope, $rootScope, $location,
UsersEdit.$inject = ['$scope', '$rootScope', '$location',
'$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'ProcessErrors',
'RelatedSearchInit', 'RelatedPaginateInit', 'ClearScope', 'GetBasePath',
- 'ResetForm', 'Wait', '$state'
+ 'ResetForm', 'Wait', 'CreateSelect2', '$state'
];
diff --git a/awx/ui/client/src/forms/Users.js b/awx/ui/client/src/forms/Users.js
index 3db17ad5fb..9ae8cde69f 100644
--- a/awx/ui/client/src/forms/Users.js
+++ b/awx/ui/client/src/forms/Users.js
@@ -87,21 +87,14 @@ export default
associated: 'password',
autocomplete: false
},
- is_superuser: {
- label: 'Superuser (User has full system administration privileges)',
- type: 'checkbox',
- trueValue: 'true',
- falseValue: 'false',
- "default": 'false',
- ngShow: "current_user['is_superuser'] == true",
- ngModel: 'is_superuser'
+ user_type: {
+ label: 'User Type',
+ type: 'select',
+ ngOptions: 'item as item.label for item in user_type_options track by item.type',
+ disableChooseOption: true,
+ ngModel: 'user_type',
+ ngShow: 'current_user["is_superuser"]',
},
- ldap_user: {
- label: 'Created by LDAP',
- type: 'checkbox',
- readonly: true,
- awFeature: 'ldap'
- }
},
buttons: {
diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js
index b05c9bd4ec..60e6f1e7a9 100644
--- a/awx/ui/client/src/shared/form-generator.js
+++ b/awx/ui/client/src/shared/form-generator.js
@@ -918,6 +918,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "