def credentialtype_external(): external_type_inputs = { 'fields': [{ 'id': 'url', 'label': 'Server URL', 'type': 'string', 'help_text': 'The server url.' }, { 'id': 'token', 'label': 'Token', 'type': 'string', 'secret': True, 'help_text': 'An access token for the server.' }], 'metadata': [{ 'id': 'key', 'label': 'Key', 'type': 'string' }, { 'id': 'version', 'label': 'Version', 'type': 'string' }], 'required': ['url', 'token', 'key'], } external_type = CredentialType(kind='external', managed_by_tower=True, name='External Service', inputs=external_type_inputs) external_type.save() return external_type
def test_update_credential_type_success(get, patch, delete, admin): _type = CredentialType(kind='cloud') _type.save() url = reverse('api:credential_type_detail', kwargs={'pk': _type.pk}) response = patch(url, {'name': 'Some Other Name'}, admin) assert response.status_code == 200 assert get(url, admin).data.get('name') == 'Some Other Name' assert delete(url, admin).status_code == 204
def test_update_credential_type_in_use_xfail(patch, delete, admin): _type = CredentialType(kind='cloud', inputs={'fields': []}) _type.save() Credential(credential_type=_type, name='My Custom Cred').save() url = reverse('api:credential_type_detail', kwargs={'pk': _type.pk}) response = patch(url, {'name': 'Some Other Name'}, admin) assert response.status_code == 200 url = reverse('api:credential_type_detail', kwargs={'pk': _type.pk}) response = patch(url, {'inputs': {}}, admin) assert response.status_code == 403 assert delete(url, admin).status_code == 403
def test_update_credential_type_in_use_xfail(patch, delete, admin): _type = CredentialType(kind='cloud', inputs={'fields': []}) _type.save() Credential(credential_type=_type, name='My Custom Cred').save() url = reverse('api:credential_type_detail', kwargs={'pk': _type.pk}) patch(url, {'name': 'Some Other Name'}, admin, expect=200) url = reverse('api:credential_type_detail', kwargs={'pk': _type.pk}) response = patch(url, {'inputs': {}}, admin, expect=403) assert response.data['detail'] == 'Modifications to inputs are not allowed for credential types that are in use' response = delete(url, admin, expect=403) assert response.data['detail'] == 'Credential types that are in use cannot be deleted'
def credentialtype_external(): external_type_inputs = { 'fields': [ {'id': 'url', 'label': 'Server URL', 'type': 'string', 'help_text': 'The server url.'}, {'id': 'token', 'label': 'Token', 'type': 'string', 'secret': True, 'help_text': 'An access token for the server.'}, ], 'metadata': [{'id': 'key', 'label': 'Key', 'type': 'string'}, {'id': 'version', 'label': 'Version', 'type': 'string'}], 'required': ['url', 'token', 'key'], } class MockPlugin(object): def backend(self, **kwargs): return 'secret' with mock.patch('awx.main.models.credential.CredentialType.plugin', new_callable=PropertyMock) as mock_plugin: mock_plugin.return_value = MockPlugin() external_type = CredentialType(kind='external', managed_by_tower=True, name='External Service', inputs=external_type_inputs) external_type.save() yield external_type
def forwards(apps, schema_editor): InventoryUpdate = apps.get_model('main', 'InventoryUpdate') InventorySource = apps.get_model('main', 'InventorySource') r = InventoryUpdate.objects.filter(source='tower').update( source='controller') if r: logger.warning(f'Renamed {r} tower inventory updates to controller') InventorySource.objects.filter(source='tower').update(source='controller') if r: logger.warning(f'Renamed {r} tower inventory sources to controller') CredentialType = apps.get_model('main', 'CredentialType') tower_type = CredentialType.objects.filter(managed_by_tower=True, namespace='tower').first() if tower_type is not None: controller_type = CredentialType.objects.filter(managed_by_tower=True, namespace='controller', kind='cloud').first() if controller_type: # this gets created by prior migrations in upgrade scenarios controller_type.delete() registry_type = ManagedCredentialType.registry.get('controller') if not registry_type: raise RuntimeError( 'Excpected to find controller credential, this may need to be edited in the future!' ) logger.warning( 'Renaming the Ansible Tower credential type for existing install') tower_type.name = registry_type.name # sensitive to translations tower_type.namespace = 'controller' # if not done, will error setup_tower_managed_defaults tower_type.save(update_fields=['name', 'namespace']) ModernCredentialType.setup_tower_managed_defaults(apps)
def filter_queryset(self, request, queryset, view): try: # Apply filters specified via query_params. Each entry in the lists # below is (negate, field, value). and_filters = [] or_filters = [] chain_filters = [] role_filters = [] search_filters = {} # Can only have two values: 'AND', 'OR' # If 'AND' is used, an iterm must satisfy all condition to show up in the results. # If 'OR' is used, an item just need to satisfy one condition to appear in results. search_filter_relation = 'OR' for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue # HACK: Make job event filtering by host name mostly work even # when not capturing job event hosts M2M. if queryset.model._meta.object_name == 'JobEvent' and key.startswith( 'hosts__name'): key = key.replace('hosts__name', 'or__host__name') or_filters.append((False, 'host__name__isnull', True)) # Custom __int filter suffix (internal use only). q_int = False if key.endswith('__int'): key = key[:-5] q_int = True # RBAC filtering if key == 'role_level': role_filters.append(values[0]) continue # Search across related objects. if key.endswith('__search'): if values and ',' in values[0]: search_filter_relation = 'AND' values = reduce(lambda list1, list2: list1 + list2, [i.split(',') for i in values]) for value in values: search_value, new_keys = self.value_to_python( queryset.model, key, force_text(value)) assert isinstance(new_keys, list) search_filters[search_value] = new_keys continue # Custom chain__ and or__ filters, mutually exclusive (both can # precede not__). q_chain = False q_or = False if key.startswith('chain__'): key = key[7:] q_chain = True elif key.startswith('or__'): key = key[4:] q_or = True # Custom not__ filter prefix. q_not = False if key.startswith('not__'): key = key[5:] q_not = True # Make legacy v1 Job/Template fields work for backwards compatability # TODO: remove after API v1 deprecation period if queryset.model._meta.object_name in ( 'JobTemplate', 'Job') and key in ( 'credential', 'vault_credential', 'cloud_credential', 'network_credential' ) or queryset.model._meta.object_name in ( 'InventorySource', 'InventoryUpdate') and key == 'credential': key = 'credentials' # Make legacy v1 Credential fields work for backwards compatability # TODO: remove after API v1 deprecation period # # convert v1 `Credential.kind` queries to `Credential.credential_type__pk` if queryset.model._meta.object_name == 'Credential' and key == 'kind': key = key.replace('kind', 'credential_type') if 'ssh' in values: # In 3.2, SSH and Vault became separate credential types, but in the v1 API, # they're both still "kind=ssh" # under the hood, convert `/api/v1/credentials/?kind=ssh` to # `/api/v1/credentials/?or__credential_type=<ssh_pk>&or__credential_type=<vault_pk>` values = set(values) values.add('vault') values = list(values) q_or = True for i, kind in enumerate(values): if kind == 'vault': type_ = CredentialType.objects.get(kind=kind) else: type_ = CredentialType.from_v1_kind(kind) if type_ is None: raise ParseError( _('cannot filter on kind %s') % kind) values[i] = type_.pk # Convert value(s) to python and add to the appropriate list. for value in values: if q_int: value = int(value) value, new_key = self.value_to_python( queryset.model, key, value) if q_chain: chain_filters.append((q_not, new_key, value)) elif q_or: or_filters.append((q_not, new_key, value)) else: and_filters.append((q_not, new_key, value)) # Now build Q objects for database query filter. if and_filters or or_filters or chain_filters or role_filters or search_filters: args = [] for n, k, v in and_filters: if n: args.append(~Q(**{k: v})) else: args.append(Q(**{k: v})) for role_name in role_filters: if not hasattr(queryset.model, 'accessible_pk_qs'): raise ParseError( _('Cannot apply role_level filter to this list because its model ' 'does not use roles for access control.')) args.append( Q(pk__in=queryset.model.accessible_pk_qs( request.user, role_name))) if or_filters: q = Q() for n, k, v in or_filters: if n: q |= ~Q(**{k: v}) else: q |= Q(**{k: v}) args.append(q) if search_filters and search_filter_relation == 'OR': q = Q() for term, constrains in search_filters.items(): for constrain in constrains: q |= Q(**{constrain: term}) args.append(q) elif search_filters and search_filter_relation == 'AND': for term, constrains in search_filters.items(): q_chain = Q() for constrain in constrains: q_chain |= Q(**{constrain: term}) queryset = queryset.filter(q_chain) for n, k, v in chain_filters: if n: q = ~Q(**{k: v}) else: q = Q(**{k: v}) queryset = queryset.filter(q) queryset = queryset.filter(*args).distinct() return queryset except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e: raise ParseError(e.args[0]) except ValidationError as e: raise ParseError(json.dumps(e.messages, ensure_ascii=False))
def filter_queryset(self, request, queryset, view): try: # Apply filters specified via query_params. Each entry in the lists # below is (negate, field, value). and_filters = [] or_filters = [] chain_filters = [] role_filters = [] search_filters = [] for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue # HACK: Make job event filtering by host name mostly work even # when not capturing job event hosts M2M. if queryset.model._meta.object_name == 'JobEvent' and key.startswith( 'hosts__name'): key = key.replace('hosts__name', 'or__host__name') or_filters.append((False, 'host__name__isnull', True)) # Custom __int filter suffix (internal use only). q_int = False if key.endswith('__int'): key = key[:-5] q_int = True # RBAC filtering if key == 'role_level': role_filters.append(values[0]) continue # Search across related objects. if key.endswith('__search'): for value in values: for search_term in force_text(value).replace( ',', ' ').split(): search_value, new_keys = self.value_to_python( queryset.model, key, search_term) assert isinstance(new_keys, list) for new_key in new_keys: search_filters.append((new_key, search_value)) continue # Custom chain__ and or__ filters, mutually exclusive (both can # precede not__). q_chain = False q_or = False if key.startswith('chain__'): key = key[7:] q_chain = True elif key.startswith('or__'): key = key[4:] q_or = True # Custom not__ filter prefix. q_not = False if key.startswith('not__'): key = key[5:] q_not = True # Make legacy v1 Job/Template fields work for backwards compatability # TODO: remove after API v1 deprecation period if queryset.model._meta.object_name in ( 'JobTemplate', 'Job') and key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'): key = 'credentials' # Make legacy v1 Credential fields work for backwards compatability # TODO: remove after API v1 deprecation period # # convert v1 `Credential.kind` queries to `Credential.credential_type__pk` if queryset.model._meta.object_name == 'Credential' and key == 'kind': key = key.replace('kind', 'credential_type') if 'ssh' in values: # In 3.2, SSH and Vault became separate credential types, but in the v1 API, # they're both still "kind=ssh" # under the hood, convert `/api/v1/credentials/?kind=ssh` to # `/api/v1/credentials/?or__credential_type=<ssh_pk>&or__credential_type=<vault_pk>` values = set(values) values.add('vault') values = list(values) q_or = True for i, kind in enumerate(values): if kind == 'vault': type_ = CredentialType.objects.get(kind=kind) else: type_ = CredentialType.from_v1_kind(kind) if type_ is None: raise ParseError( _('cannot filter on kind %s') % kind) values[i] = type_.pk # Convert value(s) to python and add to the appropriate list. for value in values: if q_int: value = int(value) value, new_key = self.value_to_python( queryset.model, key, value) if q_chain: chain_filters.append((q_not, new_key, value)) elif q_or: or_filters.append((q_not, new_key, value)) else: and_filters.append((q_not, new_key, value)) # Now build Q objects for database query filter. if and_filters or or_filters or chain_filters or role_filters or search_filters: args = [] for n, k, v in and_filters: if n: args.append(~Q(**{k: v})) else: args.append(Q(**{k: v})) for role_name in role_filters: args.append( Q(pk__in=RoleAncestorEntry.objects.filter( ancestor__in=request.user.roles.all(), content_type_id=ContentType.objects.get_for_model( queryset.model).id, role_field=role_name).values_list( 'object_id').distinct())) if or_filters: q = Q() for n, k, v in or_filters: if n: q |= ~Q(**{k: v}) else: q |= Q(**{k: v}) args.append(q) if search_filters: q = Q() for k, v in search_filters: q |= Q(**{k: v}) args.append(q) for n, k, v in chain_filters: if n: q = ~Q(**{k: v}) else: q = Q(**{k: v}) queryset = queryset.filter(q) queryset = queryset.filter(*args).distinct() return queryset except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e: raise ParseError(e.args[0]) except ValidationError as e: raise ParseError(json.dumps(e.messages, ensure_ascii=False))