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 += "