From 33ac8a96685c77a8ca84cfff0d71b4def51776e1 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 16 Mar 2018 10:40:08 -0400 Subject: [PATCH 1/6] System wide toggle for org admin user/team abilities --- awx/api/conf.py | 2 +- awx/main/access.py | 23 +++++++++++++++++------ awx/main/conf.py | 26 +++++++++++++++++++++++--- awx/settings/defaults.py | 4 +++- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/awx/api/conf.py b/awx/api/conf.py index 3fcfb13584..b91cdae254 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -35,7 +35,7 @@ register( register( 'OAUTH2_PROVIDER', field_class=OAuth2ProviderField, - default={'ACCESS_TOKEN_EXPIRE_SECONDS': 315360000000, + default={'ACCESS_TOKEN_EXPIRE_SECONDS': 315360000000, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600}, label=_('OAuth 2 Timeout Settings'), help_text=_('Dictionary for customizing OAuth 2 timeouts, available items are ' diff --git a/awx/main/access.py b/awx/main/access.py index 28991af3e0..c32462c78d 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -505,7 +505,9 @@ class UserAccess(BaseAccess): return False if self.user.is_superuser: return True - return Organization.accessible_objects(self.user, 'admin_role').exists() + if settings.ORGS_CAN_CREATE_USERS: + return Organization.accessible_objects(self.user, 'admin_role').exists() + return False def can_change(self, obj, data): if data is not None and ('is_superuser' in data or 'is_system_auditor' in data): @@ -1078,7 +1080,9 @@ class TeamAccess(BaseAccess): def can_add(self, data): if not data: # So the browseable API will work return Organization.accessible_objects(self.user, 'admin_role').exists() - return self.check_related('organization', Organization, data) + if settings.ORGS_CAN_ASSIGN_USERS_TEAM: + return self.check_related('organization', Organization, data) + return False def can_change(self, obj, data): # Prevent moving a team to a different organization. @@ -1105,8 +1109,13 @@ class TeamAccess(BaseAccess): role_access = RoleAccess(self.user) return role_access.can_attach(sub_obj, obj, 'member_role.parents', *args, **kwargs) - return super(TeamAccess, self).can_attach(obj, sub_obj, relationship, - *args, **kwargs) + if self.user.is_superuser: + return True + + if settings.ORGS_CAN_ASSIGN_USERS_TEAM: + return super(TeamAccess, self).can_attach(obj, sub_obj, relationship, + *args, **kwargs) + return False def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): if isinstance(sub_obj, Role): @@ -1114,8 +1123,10 @@ class TeamAccess(BaseAccess): role_access = RoleAccess(self.user) return role_access.can_unattach(sub_obj, obj, 'member_role.parents', *args, **kwargs) - return super(TeamAccess, self).can_unattach(obj, sub_obj, relationship, - *args, **kwargs) + if settings.ORGS_CAN_ASSIGN_USERS_TEAM: + return super(TeamAccess, self).can_unattach(obj, sub_obj, relationship, + *args, **kwargs) + return False class ProjectAccess(BaseAccess): diff --git a/awx/main/conf.py b/awx/main/conf.py index f68ee6688c..79aeb5397f 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -43,6 +43,29 @@ register( category_slug='system', ) +register( + 'ORGS_CAN_CREATE_USERS', + field_class=fields.BooleanField, + label=_('Organization Admins can create users.'), + help_text=_('Enable Organizations to create users. You may want to ' + 'disable this if you populate your users from some external source ' + 'like LDAP or SAML.'), + category=_('System'), + category_slug='system', +) + +register( + 'ORGS_CAN_ASSIGN_USERS_TEAM', + field_class=fields.BooleanField, + label=_('Organization Admins can assign users to teams.'), + help_text=_('Enable Organizations to assign users to teams. You may want to ' + 'disable this if you populate your users from some external source ' + 'like LDAP or SAML. This will prevent team assignments for ' + 'Organization and Team admins.'), + category=_('System'), + category_slug='system', +) + register( 'TOWER_ADMIN_ALERTS', field_class=fields.BooleanField, @@ -90,7 +113,6 @@ register( category_slug='system', ) - def _load_default_license_from_file(): try: license_file = os.environ.get('AWX_LICENSE_FILE', '/etc/tower/license') @@ -102,7 +124,6 @@ def _load_default_license_from_file(): logger.warning('Could not read license from "%s".', license_file, exc_info=True) return {} - register( 'LICENSE', field_class=fields.DictField, @@ -484,7 +505,6 @@ register( category_slug='logging', ) - def logging_validate(serializer, attrs): if not serializer.instance or \ not hasattr(serializer.instance, 'LOG_AGGREGATOR_HOST') or \ diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 3449c934c5..77af490dbe 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -340,7 +340,7 @@ AUTHENTICATION_BACKENDS = ( OAUTH2_PROVIDER_APPLICATION_MODEL = 'main.OAuth2Application' OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'main.OAuth2AccessToken' -OAUTH2_PROVIDER = {'ACCESS_TOKEN_EXPIRE_SECONDS': 31536000000, +OAUTH2_PROVIDER = {'ACCESS_TOKEN_EXPIRE_SECONDS': 31536000000, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600} # LDAP server (default to None to skip using LDAP authentication). @@ -945,6 +945,8 @@ FACT_CACHE_PORT = 6564 # Note: This setting may be overridden by database settings. ORG_ADMINS_CAN_SEE_ALL_USERS = True +ORGS_CAN_CREATE_USERS = True +ORGS_CAN_ASSIGN_USERS_TEAM = True # Note: This setting may be overridden by database settings. TOWER_ADMIN_ALERTS = True From eb3b518507f0c7a6de4ff38df2fa2d2c445ea06d Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 19 Mar 2018 11:25:14 -0400 Subject: [PATCH 2/6] Add Organization User/Team toggle to UI --- .../configuration/system-form/sub-forms/system-misc.form.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js index 1e185a5f1a..48f3076a5c 100644 --- a/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js @@ -21,6 +21,12 @@ export default ['i18n', function(i18n) { ORG_ADMINS_CAN_SEE_ALL_USERS: { type: 'toggleSwitch', }, + ORGS_CAN_CREATE_USERS: { + type: 'toggleSwitch', + }, + ORGS_CAN_ASSIGN_USERS_TEAM: { + type: 'toggleSwitch', + }, SESSION_COOKIE_AGE: { type: 'number', integer: true, From 771108e29802dbbd300904e29fbbb94175c67521 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 19 Mar 2018 12:10:13 -0400 Subject: [PATCH 3/6] Protect team assignment for the roles access point --- awx/main/access.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/main/access.py b/awx/main/access.py index c32462c78d..58a0c42bae 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2483,6 +2483,10 @@ class RoleAccess(BaseAccess): @check_superuser def can_unattach(self, obj, sub_obj, relationship, data=None, skip_sub_obj_read_check=False): + if isinstance(obj.content_object, Team): + if not settings.ORGS_CAN_ASSIGN_USERS_TEAM: + return False + 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': From a9da494904d166e7de36f28934e22d92cce4e016 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 19 Mar 2018 14:41:27 -0400 Subject: [PATCH 4/6] switch to single toggle and change name --- awx/main/access.py | 49 ++++++++++++------- awx/main/conf.py | 24 +++------ awx/settings/defaults.py | 3 +- .../system-form/sub-forms/system-misc.form.js | 5 +- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 58a0c42bae..adf7e083bd 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -352,6 +352,9 @@ class BaseAccess(object): # Connot copy manual project without errors user_capabilities[display_method] = False continue + elif display_method == 'copy' and (isinstance(obj, Team) or isinstance(obj, User)): + user_capabilities[display_method] = False + continue elif display_method in ['start', 'schedule'] and isinstance(obj, Group): # TODO: remove in 3.3 try: if obj.deprecated_inventory_source and not obj.deprecated_inventory_source._can_update(): @@ -505,9 +508,9 @@ class UserAccess(BaseAccess): return False if self.user.is_superuser: return True - if settings.ORGS_CAN_CREATE_USERS: - return Organization.accessible_objects(self.user, 'admin_role').exists() - return False + if not settings.MANAGE_ORGANIZATION_AUTH: + return False + return Organization.accessible_objects(self.user, 'admin_role').exists() def can_change(self, obj, data): if data is not None and ('is_superuser' in data or 'is_system_auditor' in data): @@ -519,10 +522,14 @@ class UserAccess(BaseAccess): # A user can be changed if they are themselves, or by org admins or # superusers. Change permission implies changing only certain fields # that a user should be able to edit for themselves. + if not settings.MANAGE_ORGANIZATION_AUTH: + return False return bool(self.user == obj or self.can_admin(obj, data)) @check_superuser def can_admin(self, obj, data): + if not settings.MANAGE_ORGANIZTION_AUTH: + return False return Organization.objects.filter(Q(member_role__members=obj) | Q(admin_role__members=obj), Q(admin_role__members=self.user)).exists() @@ -539,13 +546,19 @@ class UserAccess(BaseAccess): return False def can_attach(self, obj, sub_obj, relationship, *args, **kwargs): - "Reverse obj and sub_obj, defer to RoleAccess if this is a role assignment." + if not settings.MANAGE_ORGANIZTION_AUTH: + return False + + # Reverse obj and sub_obj, defer to RoleAccess if this is a role assignment. if relationship == 'roles': role_access = RoleAccess(self.user) return role_access.can_attach(sub_obj, obj, 'members', *args, **kwargs) return super(UserAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs) def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): + if not settings.MANAGE_ORGANIZTION_AUTH: + return False + if relationship == 'roles': role_access = RoleAccess(self.user) return role_access.can_unattach(sub_obj, obj, 'members', *args, **kwargs) @@ -1080,9 +1093,9 @@ class TeamAccess(BaseAccess): def can_add(self, data): if not data: # So the browseable API will work return Organization.accessible_objects(self.user, 'admin_role').exists() - if settings.ORGS_CAN_ASSIGN_USERS_TEAM: - return self.check_related('organization', Organization, data) - return False + if not settings.MANAGE_ORGANIZATION_AUTH: + return False + return self.check_related('organization', Organization, data) def can_change(self, obj, data): # Prevent moving a team to a different organization. @@ -1091,6 +1104,8 @@ class TeamAccess(BaseAccess): raise PermissionDenied(_('Unable to change organization on a team.')) if self.user.is_superuser: return True + if not settings.MANAGE_ORGANIZATION_AUTH: + return False return self.user in obj.admin_role def can_delete(self, obj): @@ -1099,6 +1114,8 @@ class TeamAccess(BaseAccess): def can_attach(self, obj, sub_obj, relationship, *args, **kwargs): """Reverse obj and sub_obj, defer to RoleAccess if this is an assignment of a resource role to the team.""" + if not settings.MANAGE_ORGANIZATION_AUTH: + return False if isinstance(sub_obj, Role): if sub_obj.content_object is None: raise PermissionDenied(_("The {} role cannot be assigned to a team").format(sub_obj.name)) @@ -1111,22 +1128,20 @@ class TeamAccess(BaseAccess): *args, **kwargs) if self.user.is_superuser: return True - - if settings.ORGS_CAN_ASSIGN_USERS_TEAM: - return super(TeamAccess, self).can_attach(obj, sub_obj, relationship, - *args, **kwargs) - return False + return super(TeamAccess, self).can_attach(obj, sub_obj, relationship, + *args, **kwargs) def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): + if not settings.MANAGE_ORGANIZATION_AUTH: + return False + if isinstance(sub_obj, Role): if isinstance(sub_obj.content_object, ResourceMixin): role_access = RoleAccess(self.user) return role_access.can_unattach(sub_obj, obj, 'member_role.parents', *args, **kwargs) - if settings.ORGS_CAN_ASSIGN_USERS_TEAM: - return super(TeamAccess, self).can_unattach(obj, sub_obj, relationship, - *args, **kwargs) - return False + return super(TeamAccess, self).can_unattach(obj, sub_obj, relationship, + *args, **kwargs) class ProjectAccess(BaseAccess): @@ -2484,7 +2499,7 @@ class RoleAccess(BaseAccess): @check_superuser def can_unattach(self, obj, sub_obj, relationship, data=None, skip_sub_obj_read_check=False): if isinstance(obj.content_object, Team): - if not settings.ORGS_CAN_ASSIGN_USERS_TEAM: + if not settings.MANAGE_ORGANIZATION_AUTH: return False if not skip_sub_obj_read_check and relationship in ['members', 'member_role.parents', 'parents']: diff --git a/awx/main/conf.py b/awx/main/conf.py index 79aeb5397f..154de417e4 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -44,24 +44,11 @@ register( ) register( - 'ORGS_CAN_CREATE_USERS', + 'MANAGE_ORGANIZATION_AUTH', field_class=fields.BooleanField, - label=_('Organization Admins can create users.'), - help_text=_('Enable Organizations to create users. You may want to ' - 'disable this if you populate your users from some external source ' - 'like LDAP or SAML.'), - category=_('System'), - category_slug='system', -) - -register( - 'ORGS_CAN_ASSIGN_USERS_TEAM', - field_class=fields.BooleanField, - label=_('Organization Admins can assign users to teams.'), - help_text=_('Enable Organizations to assign users to teams. You may want to ' - 'disable this if you populate your users from some external source ' - 'like LDAP or SAML. This will prevent team assignments for ' - 'Organization and Team admins.'), + label=_('Organizations Can Manage Users and Teams'), + help_text=_('Controls whether Orgainzations have the privileges to create and manage users and teams. ' + 'You may want to disable this ability if you are using an LDAP or SAML integration.'), category=_('System'), category_slug='system', ) @@ -113,6 +100,7 @@ register( category_slug='system', ) + def _load_default_license_from_file(): try: license_file = os.environ.get('AWX_LICENSE_FILE', '/etc/tower/license') @@ -124,6 +112,7 @@ def _load_default_license_from_file(): logger.warning('Could not read license from "%s".', license_file, exc_info=True) return {} + register( 'LICENSE', field_class=fields.DictField, @@ -505,6 +494,7 @@ register( category_slug='logging', ) + def logging_validate(serializer, attrs): if not serializer.instance or \ not hasattr(serializer.instance, 'LOG_AGGREGATOR_HOST') or \ diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 77af490dbe..4fdc097c33 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -945,8 +945,7 @@ FACT_CACHE_PORT = 6564 # Note: This setting may be overridden by database settings. ORG_ADMINS_CAN_SEE_ALL_USERS = True -ORGS_CAN_CREATE_USERS = True -ORGS_CAN_ASSIGN_USERS_TEAM = True +MANAGE_ORGANIZATION_AUTH = True # Note: This setting may be overridden by database settings. TOWER_ADMIN_ALERTS = True diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js index 48f3076a5c..02b71edaec 100644 --- a/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js @@ -21,10 +21,7 @@ export default ['i18n', function(i18n) { ORG_ADMINS_CAN_SEE_ALL_USERS: { type: 'toggleSwitch', }, - ORGS_CAN_CREATE_USERS: { - type: 'toggleSwitch', - }, - ORGS_CAN_ASSIGN_USERS_TEAM: { + MANAGE_ORGANIZATION_AUTH: { type: 'toggleSwitch', }, SESSION_COOKIE_AGE: { From d5564e8d81003678a110ccba6f51a004aa22d581 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 19 Mar 2018 15:14:08 -0400 Subject: [PATCH 5/6] Fix user capabilities when MANAGE_ORGANIZATION_AUTH is disabled --- awx/main/access.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index adf7e083bd..691e202a93 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -335,6 +335,10 @@ class BaseAccess(object): if display_method not in method_list: continue + if not settings.MANAGE_ORGANIZATION_AUTH and isinstance(obj, (Team, User)): + user_capabilities[display_method] = self.user.is_superuser + continue + # Actions not possible for reason unrelated to RBAC # Cannot copy with validation errors, or update a manual group/project if display_method == 'copy' and isinstance(obj, JobTemplate): @@ -352,9 +356,6 @@ class BaseAccess(object): # Connot copy manual project without errors user_capabilities[display_method] = False continue - elif display_method == 'copy' and (isinstance(obj, Team) or isinstance(obj, User)): - user_capabilities[display_method] = False - continue elif display_method in ['start', 'schedule'] and isinstance(obj, Group): # TODO: remove in 3.3 try: if obj.deprecated_inventory_source and not obj.deprecated_inventory_source._can_update(): @@ -528,7 +529,7 @@ class UserAccess(BaseAccess): @check_superuser def can_admin(self, obj, data): - if not settings.MANAGE_ORGANIZTION_AUTH: + if not settings.MANAGE_ORGANIZATION_AUTH: return False return Organization.objects.filter(Q(member_role__members=obj) | Q(admin_role__members=obj), Q(admin_role__members=self.user)).exists() @@ -546,7 +547,7 @@ class UserAccess(BaseAccess): return False def can_attach(self, obj, sub_obj, relationship, *args, **kwargs): - if not settings.MANAGE_ORGANIZTION_AUTH: + if not settings.MANAGE_ORGANIZTAION_AUTH: return False # Reverse obj and sub_obj, defer to RoleAccess if this is a role assignment. @@ -556,7 +557,7 @@ class UserAccess(BaseAccess): return super(UserAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs) def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): - if not settings.MANAGE_ORGANIZTION_AUTH: + if not settings.MANAGE_ORGANIZATION_AUTH: return False if relationship == 'roles': From d7f26f417d185790945d593944140200209b5f3f Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 19 Mar 2018 15:20:48 -0400 Subject: [PATCH 6/6] Reword help text for manage org auth --- awx/main/access.py | 2 +- awx/main/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 691e202a93..b3ee282260 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -547,7 +547,7 @@ class UserAccess(BaseAccess): return False def can_attach(self, obj, sub_obj, relationship, *args, **kwargs): - if not settings.MANAGE_ORGANIZTAION_AUTH: + if not settings.MANAGE_ORGANIZATION_AUTH: return False # Reverse obj and sub_obj, defer to RoleAccess if this is a role assignment. diff --git a/awx/main/conf.py b/awx/main/conf.py index 154de417e4..593607c078 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -46,8 +46,8 @@ register( register( 'MANAGE_ORGANIZATION_AUTH', field_class=fields.BooleanField, - label=_('Organizations Can Manage Users and Teams'), - help_text=_('Controls whether Orgainzations have the privileges to create and manage users and teams. ' + label=_('Organization Admins Can Manage Users and Teams'), + help_text=_('Controls whether any Organization Admin has the privileges to create and manage users and teams. ' 'You may want to disable this ability if you are using an LDAP or SAML integration.'), category=_('System'), category_slug='system',