mirror of
https://github.com/ansible/awx.git
synced 2026-03-17 08:57:33 -02:30
add api for managing credential input sources
This commit is contained in:
@@ -46,7 +46,7 @@ from awx.main.constants import (
|
||||
CENSOR_VALUE,
|
||||
)
|
||||
from awx.main.models import (
|
||||
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential,
|
||||
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialInputSource,
|
||||
CredentialType, CustomInventoryScript, Fact, Group, Host, Instance,
|
||||
InstanceGroup, Inventory, InventorySource, InventoryUpdate,
|
||||
InventoryUpdateEvent, Job, JobEvent, JobHostSummary, JobLaunchConfig,
|
||||
@@ -2629,6 +2629,7 @@ class CredentialSerializer(BaseSerializer):
|
||||
))
|
||||
if self.version > 1:
|
||||
res['copy'] = self.reverse('api:credential_copy', kwargs={'pk': obj.pk})
|
||||
res['input_sources'] = self.reverse('api:credential_input_source_sublist', kwargs={'pk': obj.pk})
|
||||
|
||||
# TODO: remove when API v1 is removed
|
||||
if self.version > 1:
|
||||
@@ -2815,6 +2816,43 @@ class CredentialSerializerCreate(CredentialSerializer):
|
||||
return credential
|
||||
|
||||
|
||||
class CredentialInputSourceSerializer(BaseSerializer):
|
||||
source_credential_name = serializers.SerializerMethodField(
|
||||
read_only=True,
|
||||
help_text=_('The name of the source credential.')
|
||||
)
|
||||
source_credential_type = serializers.SerializerMethodField(
|
||||
read_only=True,
|
||||
help_text=_('The credential type of the source credential.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CredentialInputSource
|
||||
fields = (
|
||||
'id',
|
||||
'type',
|
||||
'url',
|
||||
'input_field_name',
|
||||
'target_credential',
|
||||
'source_credential',
|
||||
'source_credential_type',
|
||||
'source_credential_name',
|
||||
'created',
|
||||
'modified',
|
||||
)
|
||||
extra_kwargs = {
|
||||
'input_field_name': {'required': True},
|
||||
'target_credential': {'required': True},
|
||||
'source_credential': {'required': True},
|
||||
}
|
||||
|
||||
def get_source_credential_name(self, obj):
|
||||
return obj.source_credential.name
|
||||
|
||||
def get_source_credential_type(self, obj):
|
||||
return obj.source_credential.credential_type.id
|
||||
|
||||
|
||||
class UserCredentialSerializerCreate(CredentialSerializerCreate):
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -12,6 +12,7 @@ from awx.api.views import (
|
||||
CredentialOwnerUsersList,
|
||||
CredentialOwnerTeamsList,
|
||||
CredentialCopy,
|
||||
CredentialInputSourceSubList,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +25,7 @@ urls = [
|
||||
url(r'^(?P<pk>[0-9]+)/owner_users/$', CredentialOwnerUsersList.as_view(), name='credential_owner_users_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/owner_teams/$', CredentialOwnerTeamsList.as_view(), name='credential_owner_teams_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/copy/$', CredentialCopy.as_view(), name='credential_copy'),
|
||||
url(r'^(?P<pk>[0-9]+)/input_sources/$', CredentialInputSourceSubList.as_view(), name='credential_input_source_sublist'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
|
||||
17
awx/api/urls/credential_input_source.py
Normal file
17
awx/api/urls/credential_input_source.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2019 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from awx.api.views import (
|
||||
CredentialInputSourceDetail,
|
||||
CredentialInputSourceList,
|
||||
)
|
||||
|
||||
|
||||
urls = [
|
||||
url(r'^$', CredentialInputSourceList.as_view(), name='credential_input_source_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', CredentialInputSourceDetail.as_view(), name='credential_input_source_detail'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
@@ -47,6 +47,7 @@ from .inventory_update import urls as inventory_update_urls
|
||||
from .inventory_script import urls as inventory_script_urls
|
||||
from .credential_type import urls as credential_type_urls
|
||||
from .credential import urls as credential_urls
|
||||
from .credential_input_source import urls as credential_input_source_urls
|
||||
from .role import urls as role_urls
|
||||
from .job_template import urls as job_template_urls
|
||||
from .job import urls as job_urls
|
||||
@@ -119,6 +120,7 @@ v1_urls = [
|
||||
v2_urls = [
|
||||
url(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'),
|
||||
url(r'^credential_types/', include(credential_type_urls)),
|
||||
url(r'^credential_input_sources/', include(credential_input_source_urls)),
|
||||
url(r'^hosts/(?P<pk>[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'),
|
||||
url(r'^jobs/(?P<pk>[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'),
|
||||
url(r'^jobs/(?P<pk>[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'),
|
||||
|
||||
@@ -1419,6 +1419,33 @@ class CredentialCopy(CopyAPIView):
|
||||
copy_return_serializer_class = serializers.CredentialSerializer
|
||||
|
||||
|
||||
class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
view_name = _("Credential Input Source Detail")
|
||||
|
||||
model = models.CredentialInputSource
|
||||
serializer_class = serializers.CredentialInputSourceSerializer
|
||||
|
||||
|
||||
class CredentialInputSourceList(ListCreateAPIView):
|
||||
|
||||
view_name = _("Credential Input Sources")
|
||||
|
||||
model = models.CredentialInputSource
|
||||
serializer_class = serializers.CredentialInputSourceSerializer
|
||||
|
||||
|
||||
class CredentialInputSourceSubList(SubListAPIView):
|
||||
|
||||
view_name = _("Credential Input Sources")
|
||||
|
||||
model = models.CredentialInputSource
|
||||
serializer_class = serializers.CredentialInputSourceSerializer
|
||||
parent_model = models.Credential
|
||||
relationship = 'input_source'
|
||||
parent_key = 'target_credential'
|
||||
|
||||
|
||||
class HostRelatedSearchMixin(object):
|
||||
|
||||
@property
|
||||
|
||||
@@ -101,6 +101,7 @@ class ApiVersionRootView(APIView):
|
||||
data['credentials'] = reverse('api:credential_list', request=request)
|
||||
if get_request_version(request) > 1:
|
||||
data['credential_types'] = reverse('api:credential_type_list', request=request)
|
||||
data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request)
|
||||
data['applications'] = reverse('api:o_auth2_application_list', request=request)
|
||||
data['tokens'] = reverse('api:o_auth2_token_list', request=request)
|
||||
data['inventory'] = reverse('api:inventory_list', request=request)
|
||||
|
||||
@@ -30,8 +30,8 @@ from awx.main.utils import (
|
||||
)
|
||||
from awx.main.models import (
|
||||
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialType,
|
||||
CustomInventoryScript, Group, Host, Instance, InstanceGroup, Inventory,
|
||||
InventorySource, InventoryUpdate, InventoryUpdateEvent, Job, JobEvent,
|
||||
CredentialInputSource, CustomInventoryScript, Group, Host, Instance, InstanceGroup,
|
||||
Inventory, InventorySource, InventoryUpdate, InventoryUpdateEvent, Job, JobEvent,
|
||||
JobHostSummary, JobLaunchConfig, JobTemplate, Label, Notification,
|
||||
NotificationTemplate, Organization, Project, ProjectUpdate,
|
||||
ProjectUpdateEvent, Role, Schedule, SystemJob, SystemJobEvent,
|
||||
@@ -1163,6 +1163,19 @@ class CredentialAccess(BaseAccess):
|
||||
return self.can_change(obj, None)
|
||||
|
||||
|
||||
class CredentialInputSourceAccess(BaseAccess):
|
||||
'''
|
||||
I can see credential input sources when:
|
||||
- I'm a superuser (TODO: Update)
|
||||
I can create credential input sources when:
|
||||
- I'm a superuser (TODO: Update)
|
||||
I can delete credential input sources when:
|
||||
- I'm a superuser (TODO: Update)
|
||||
'''
|
||||
|
||||
model = CredentialInputSource
|
||||
|
||||
|
||||
class TeamAccess(BaseAccess):
|
||||
'''
|
||||
I can see a team when:
|
||||
|
||||
@@ -1339,6 +1339,10 @@ class CredentialInputSource(PrimordialModel):
|
||||
backend_kwargs[field_name] = value
|
||||
return backend(**backend_kwargs)
|
||||
|
||||
def get_absolute_url(self, request=None):
|
||||
view_name = 'api:credential_input_source_detail'
|
||||
return reverse(view_name, kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
|
||||
for plugin in credential_plugins:
|
||||
CredentialType.load_plugin(plugin)
|
||||
|
||||
@@ -140,6 +140,7 @@ def pytest_runtest_teardown(item, nextitem):
|
||||
# this is a local test cache, so we want every test to start with empty cache
|
||||
cache.clear()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def mock_external_credential_input_sources():
|
||||
# Credential objects query their related input sources on initialization.
|
||||
|
||||
115
awx/main/tests/functional/api/test_credential_input_sources.py
Normal file
115
awx/main/tests/functional/api/test_credential_input_sources.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import pytest
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_input_source(get, post, admin, vault_credential, external_credential):
|
||||
list_url = reverse(
|
||||
'api:credential_input_source_list',
|
||||
kwargs={'version': 'v2'}
|
||||
)
|
||||
sublist_url = reverse(
|
||||
'api:credential_input_source_sublist',
|
||||
kwargs={'version': 'v2', 'pk': vault_credential.pk}
|
||||
)
|
||||
params = {
|
||||
'source_credential': external_credential.pk,
|
||||
'target_credential': vault_credential.pk,
|
||||
'input_field_name': 'vault_password'
|
||||
}
|
||||
response = post(list_url, params, admin)
|
||||
assert response.status_code == 201
|
||||
|
||||
response = get(response.data['url'], admin)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = get(list_url, admin)
|
||||
assert response.status_code == 200
|
||||
assert response.data['count'] == 1
|
||||
|
||||
response = get(sublist_url, admin)
|
||||
assert response.status_code == 200
|
||||
assert response.data['count'] == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_input_source_using_sublist_returns_405(post, admin, vault_credential, external_credential):
|
||||
sublist_url = reverse(
|
||||
'api:credential_input_source_sublist',
|
||||
kwargs={'version': 'v2', 'pk': vault_credential.pk}
|
||||
)
|
||||
params = {
|
||||
'source_credential': external_credential.pk,
|
||||
'target_credential': vault_credential.pk,
|
||||
'input_field_name': 'vault_password'
|
||||
}
|
||||
response = post(sublist_url, params, admin)
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_input_source_with_external_target_returns_400(post, admin, external_credential, other_external_credential):
|
||||
list_url = reverse(
|
||||
'api:credential_input_source_list',
|
||||
kwargs={'version': 'v2'}
|
||||
)
|
||||
params = {
|
||||
'source_credential': external_credential.pk,
|
||||
'target_credential': other_external_credential.pk,
|
||||
'input_field_name': 'token'
|
||||
}
|
||||
response = post(list_url, params, admin)
|
||||
assert response.status_code == 400
|
||||
assert response.data['target_credential'] == ['Target must be a non-external credential']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_input_source_with_non_external_source_returns_400(post, admin, credential, vault_credential):
|
||||
list_url = reverse(
|
||||
'api:credential_input_source_list',
|
||||
kwargs={'version': 'v2'}
|
||||
)
|
||||
params = {
|
||||
'source_credential': credential.pk,
|
||||
'target_credential': vault_credential.pk,
|
||||
'input_field_name': 'vault_password'
|
||||
}
|
||||
response = post(list_url, params, admin)
|
||||
assert response.status_code == 400
|
||||
assert response.data['source_credential'] == ['Source must be an external credential']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_input_source_with_undefined_input_returns_400(post, admin, vault_credential, external_credential):
|
||||
list_url = reverse(
|
||||
'api:credential_input_source_list',
|
||||
kwargs={'version': 'v2'}
|
||||
)
|
||||
params = {
|
||||
'source_credential': external_credential.pk,
|
||||
'target_credential': vault_credential.pk,
|
||||
'input_field_name': 'not_defined_for_credential_type'
|
||||
}
|
||||
response = post(list_url, params, admin)
|
||||
assert response.status_code == 400
|
||||
assert response.data['input_field_name'] == ['Input field must be defined on target credential.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_input_source_with_already_used_input_returns_400(post, admin, vault_credential, external_credential, other_external_credential):
|
||||
list_url = reverse(
|
||||
'api:credential_input_source_list',
|
||||
kwargs={'version': 'v2'}
|
||||
)
|
||||
all_params = [{
|
||||
'source_credential': external_credential.pk,
|
||||
'target_credential': vault_credential.pk,
|
||||
'input_field_name': 'vault_password'
|
||||
}, {
|
||||
'source_credential': other_external_credential.pk,
|
||||
'target_credential': vault_credential.pk,
|
||||
'input_field_name': 'vault_password'
|
||||
}]
|
||||
all_responses = [post(list_url, params, admin) for params in all_params]
|
||||
assert all_responses.pop().status_code == 400
|
||||
@@ -250,6 +250,33 @@ def credentialtype_insights():
|
||||
return insights_type
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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.'
|
||||
}],
|
||||
'required': ['url', 'token'],
|
||||
}
|
||||
external_type = CredentialType(
|
||||
kind='external',
|
||||
managed_by_tower=True,
|
||||
name='External Service',
|
||||
inputs=external_type_inputs
|
||||
)
|
||||
external_type.save()
|
||||
return external_type
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credential(credentialtype_aws):
|
||||
return Credential.objects.create(credential_type=credentialtype_aws, name='test-cred',
|
||||
@@ -293,6 +320,18 @@ def org_credential(organization, credentialtype_aws):
|
||||
organization=organization)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def external_credential(credentialtype_external):
|
||||
return Credential.objects.create(credential_type=credentialtype_external, name='external-cred',
|
||||
inputs={'url': 'http://testhost.com', 'token': 'secret1'})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_external_credential(credentialtype_external):
|
||||
return Credential.objects.create(credential_type=credentialtype_external, name='other-external-cred',
|
||||
inputs={'url': 'http://testhost.com', 'token': 'secret2'})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inventory(organization):
|
||||
return organization.inventories.create(name="test-inv")
|
||||
|
||||
Reference in New Issue
Block a user