mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 07:17:40 -02:30
Introduce a new CredentialTemplate model
Credentials now have a required CredentialType, which defines inputs (i.e., username, password) and injectors (i.e., assign the username to SOME_ENV_VARIABLE at job runtime) This commit only implements the model changes necessary to support the new inputs model, and includes code for the credential serializer that allows backwards-compatible support for /api/v1/credentials/; tasks.py still needs to be updated to actually respect CredentialType injectors. This change *will* break the UI for credentials (because it needs to be updated to use the new v2 endpoint). see: #5877 see: #5876 see: #5805
This commit is contained in:
@@ -824,6 +824,36 @@ class InventoryUpdateAccess(BaseAccess):
|
||||
return self.user in obj.inventory_source.inventory.admin_role
|
||||
|
||||
|
||||
class CredentialTypeAccess(BaseAccess):
|
||||
'''
|
||||
I can see credentials types when:
|
||||
- I'm authenticated
|
||||
I can create when:
|
||||
- I'm a superuser:
|
||||
I can change when:
|
||||
- I'm a superuser and the type is not "managed by Tower"
|
||||
I can change/delete when:
|
||||
- I'm a superuser and the type is not "managed by Tower"
|
||||
'''
|
||||
|
||||
model = CredentialType
|
||||
|
||||
def can_read(self, obj):
|
||||
return True
|
||||
|
||||
def can_use(self, obj):
|
||||
return True
|
||||
|
||||
def can_add(self, data):
|
||||
return self.user.is_superuser
|
||||
|
||||
def can_change(self, obj, data):
|
||||
return self.user.is_superuser and not obj.managed_by_tower
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.user.is_superuser and not obj.managed_by_tower
|
||||
|
||||
|
||||
class CredentialAccess(BaseAccess):
|
||||
'''
|
||||
I can see credentials when:
|
||||
@@ -2282,6 +2312,7 @@ register_access(Group, GroupAccess)
|
||||
register_access(InventorySource, InventorySourceAccess)
|
||||
register_access(InventoryUpdate, InventoryUpdateAccess)
|
||||
register_access(Credential, CredentialAccess)
|
||||
register_access(CredentialType, CredentialTypeAccess)
|
||||
register_access(Team, TeamAccess)
|
||||
register_access(Project, ProjectAccess)
|
||||
register_access(ProjectUpdate, ProjectUpdateAccess)
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import six
|
||||
from pyparsing import infixNotation, opAssoc, Optional, Literal, CharsNotIn
|
||||
|
||||
from jinja2 import Environment, StrictUndefined
|
||||
from jinja2.exceptions import UndefinedError
|
||||
|
||||
# Django
|
||||
from django.core import exceptions as django_exceptions
|
||||
from django.db.models.signals import (
|
||||
post_save,
|
||||
post_delete,
|
||||
@@ -24,6 +29,10 @@ from django.db.models.fields.related import (
|
||||
)
|
||||
from django.utils.encoding import smart_text
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# jsonschema
|
||||
from jsonschema import Draft4Validator
|
||||
|
||||
# Django-JSONField
|
||||
from jsonfield import JSONField as upstream_JSONField
|
||||
@@ -526,3 +535,236 @@ class DynamicFilterField(models.TextField):
|
||||
raise RuntimeError("Parsing the filter_string %s went terribly wrong" % filter_string)
|
||||
|
||||
|
||||
class JSONSchemaField(JSONBField):
|
||||
"""
|
||||
A JSONB field that self-validates against a defined JSON schema
|
||||
(http://json-schema.org). This base class is intended to be overwritten by
|
||||
defining `self.schema`.
|
||||
"""
|
||||
|
||||
# If an empty {} is provided, we still want to perform this schema
|
||||
# validation
|
||||
empty_values=(None, '')
|
||||
|
||||
def get_default(self):
|
||||
return copy.deepcopy(super(JSONBField, self).get_default())
|
||||
|
||||
def schema(self, model_instance):
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
super(JSONSchemaField, self).validate(value, model_instance)
|
||||
errors = []
|
||||
for error in Draft4Validator(self.schema(model_instance)).iter_errors(value):
|
||||
errors.append(error)
|
||||
|
||||
if errors:
|
||||
raise django_exceptions.ValidationError(
|
||||
[e.message for e in errors],
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if connection.vendor == 'sqlite':
|
||||
# sqlite (which we use for tests) does not support jsonb;
|
||||
return json.dumps(value)
|
||||
return super(JSONSchemaField, self).get_db_prep_value(
|
||||
value, connection, prepared
|
||||
)
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
# Work around a bug in django-jsonfield
|
||||
# https://bitbucket.org/schinckel/django-jsonfield/issues/57/cannot-use-in-the-same-project-as-djangos
|
||||
if isinstance(value, six.string_types):
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
class CredentialInputField(JSONSchemaField):
|
||||
"""
|
||||
Used to validate JSON for
|
||||
`awx.main.models.credential:Credential().inputs`.
|
||||
|
||||
Input data for credentials is represented as a dictionary e.g.,
|
||||
{'api_token': 'abc123', 'api_secret': 'SECRET'}
|
||||
|
||||
For the data to be valid, the keys of this dictionary should correspond
|
||||
with the field names (and datatypes) defined in the associated
|
||||
CredentialType e.g.,
|
||||
|
||||
{
|
||||
'fields': [{
|
||||
'id': 'api_token',
|
||||
'label': 'API Token',
|
||||
'type': 'string'
|
||||
}, {
|
||||
'id': 'api_secret',
|
||||
'label': 'API Secret',
|
||||
'type': 'string'
|
||||
}]
|
||||
}
|
||||
"""
|
||||
|
||||
def schema(self, model_instance):
|
||||
# determine the defined fields for the associated credential type
|
||||
properties = {}
|
||||
for field in model_instance.credential_type.inputs.get('fields', []):
|
||||
field = field.copy()
|
||||
properties[field.pop('id')] = field
|
||||
return {
|
||||
'type': 'object',
|
||||
'properties': properties,
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
super(CredentialInputField, self).validate(
|
||||
value, model_instance
|
||||
)
|
||||
|
||||
errors = []
|
||||
inputs = model_instance.credential_type.inputs
|
||||
for field in inputs.get('required', []):
|
||||
if not value.get(field, None):
|
||||
errors.append(
|
||||
_('%s required for %s credential.') % (
|
||||
field, model_instance.credential_type.name
|
||||
)
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise django_exceptions.ValidationError(
|
||||
errors,
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
|
||||
class CredentialTypeInputField(JSONSchemaField):
|
||||
"""
|
||||
Used to validate JSON for
|
||||
`awx.main.models.credential:CredentialType().inputs`.
|
||||
"""
|
||||
|
||||
def schema(self, model_instance):
|
||||
return {
|
||||
'type': 'object',
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'fields': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'type': {'enum': ['string', 'number', 'ssh_private_key']},
|
||||
'choices': {
|
||||
'type': 'array',
|
||||
'minItems': 1,
|
||||
'items': {'type': 'string'},
|
||||
'uniqueItems': True
|
||||
},
|
||||
'id': {'type': 'string'},
|
||||
'label': {'type': 'string'},
|
||||
'help_text': {'type': 'string'},
|
||||
'multiline': {'type': 'boolean'},
|
||||
'secret': {'type': 'boolean'},
|
||||
'ask_at_runtime': {'type': 'boolean'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['id', 'label'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class CredentialTypeInjectorField(JSONSchemaField):
|
||||
"""
|
||||
Used to validate JSON for
|
||||
`awx.main.models.credential:CredentialType().injectors`.
|
||||
"""
|
||||
|
||||
def schema(self, model_instance):
|
||||
return {
|
||||
'type': 'object',
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'file': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'template': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['template'],
|
||||
},
|
||||
'ssh': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'private': {'type': 'string'},
|
||||
'public': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['public', 'private'],
|
||||
},
|
||||
'password': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'key': {'type': 'string'},
|
||||
'value': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['key', 'value'],
|
||||
},
|
||||
'env': {
|
||||
'type': 'object',
|
||||
'patternProperties': {
|
||||
# http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
|
||||
# In the shell command language, a word consisting solely
|
||||
# of underscores, digits, and alphabetics from the portable
|
||||
# character set. The first character of a name is not
|
||||
# a digit.
|
||||
'^[a-zA-Z_]+[a-zA-Z0-9_]*$': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
},
|
||||
'extra_vars': {
|
||||
'type': 'object',
|
||||
'patternProperties': {
|
||||
# http://docs.ansible.com/ansible/playbooks_variables.html#what-makes-a-valid-variable-name
|
||||
'^[a-zA-Z_]+[a-zA-Z0-9_]*$': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
},
|
||||
},
|
||||
'additionalProperties': False
|
||||
}
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
super(CredentialTypeInjectorField, self).validate(
|
||||
value, model_instance
|
||||
)
|
||||
|
||||
# make sure the inputs are clean first
|
||||
CredentialTypeInputField().validate(model_instance.inputs, model_instance)
|
||||
|
||||
# In addition to basic schema validation, search the injector fields
|
||||
# for template variables and make sure they match the fields defined in
|
||||
# the inputs
|
||||
valid_namespace = dict(
|
||||
(field, 'EXAMPLE')
|
||||
for field in model_instance.defined_fields
|
||||
)
|
||||
for type_, injector in value.items():
|
||||
for key, tmpl in injector.items():
|
||||
try:
|
||||
Environment(
|
||||
undefined=StrictUndefined
|
||||
).from_string(tmpl).render(valid_namespace)
|
||||
except UndefinedError as e:
|
||||
raise django_exceptions.ValidationError(
|
||||
_('%s uses an undefined field (%s)') % (key, e),
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from crum import impersonate
|
||||
from awx.main.models import User, Organization, Project, Inventory, Credential, Host, JobTemplate
|
||||
from awx.main.models import User, Organization, Project, Inventory, CredentialType, Credential, Host, JobTemplate
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -30,8 +30,12 @@ class Command(BaseCommand):
|
||||
scm_update_cache_timeout=0,
|
||||
organization=o)
|
||||
p.save(skip_update=True)
|
||||
c = Credential.objects.create(name='Demo Credential',
|
||||
username=superuser.username,
|
||||
ssh_type = CredentialType.from_v1_kind('ssh')
|
||||
c = Credential.objects.create(credential_type=ssh_type,
|
||||
name='Demo Credential',
|
||||
inputs={
|
||||
'username': superuser.username
|
||||
},
|
||||
created_by=superuser)
|
||||
c.admin_role.members.add(superuser)
|
||||
i = Inventory.objects.create(name='Demo Inventory',
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
import taggit.managers
|
||||
import awx.main.fields
|
||||
from awx.main.migrations import _credentialtypes as credentialtypes
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0038_v320_data_migrations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CredentialType',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('description', models.TextField(default=b'', blank=True)),
|
||||
('name', models.CharField(max_length=512)),
|
||||
('kind', models.CharField(max_length=32, choices=[(b'machine', 'Machine'), (b'net', 'Network'), (b'scm', 'Source Control'), (b'cloud', 'Cloud')])),
|
||||
('managed_by_tower', models.BooleanField(default=False, editable=False)),
|
||||
('inputs', awx.main.fields.CredentialTypeInputField(default={}, blank=True)),
|
||||
('injectors', awx.main.fields.CredentialTypeInjectorField(default={}, blank=True)),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'credentialtype', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'credentialtype', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('kind', 'name'),
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='credential',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='inputs',
|
||||
field=awx.main.fields.CredentialInputField(default={}, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='credential_type',
|
||||
field=models.ForeignKey(related_name='credentials', to='main.CredentialType', null=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='credential',
|
||||
unique_together=set([('organization', 'name', 'credential_type')]),
|
||||
),
|
||||
migrations.RunPython(credentialtypes.create_tower_managed_credential_types),
|
||||
# MIGRATION TODO: For each credential, look at the columns below to
|
||||
# determine the appropriate CredentialType (and assign it). Additionally,
|
||||
# set `self.input` to the appropriate JSON blob
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='authorize',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='authorize_password',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='become_method',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='become_password',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='become_username',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='client',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='cloud',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='domain',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='host',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='kind',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='password',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='project',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='secret',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='security_token',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='ssh_key_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='ssh_key_unlock',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='subscription',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='tenant',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='username',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='vault_password',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='credentialtype',
|
||||
unique_together=set([('name', 'kind')]),
|
||||
),
|
||||
# MIGRATION TODO: Once credentials are migrated, alter the credential_type
|
||||
# foreign key to be non-NULLable
|
||||
]
|
||||
5
awx/main/migrations/_credentialtypes.py
Normal file
5
awx/main/migrations/_credentialtypes.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from awx.main.models import CredentialType
|
||||
|
||||
|
||||
def create_tower_managed_credential_types(apps, schema_editor):
|
||||
CredentialType.setup_tower_managed_defaults()
|
||||
@@ -99,7 +99,7 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
|
||||
|
||||
def clean_credential(self):
|
||||
cred = self.credential
|
||||
if cred and cred.kind != 'ssh':
|
||||
if cred and cred.kind != 'machine':
|
||||
raise ValidationError(
|
||||
_('You must provide a machine / SSH credential.'),
|
||||
)
|
||||
|
||||
@@ -229,10 +229,8 @@ class PasswordFieldsModel(BaseModel):
|
||||
setattr(self, field, '')
|
||||
else:
|
||||
ask = self._password_field_allows_ask(field)
|
||||
encrypted = encrypt_field(self, field, ask)
|
||||
setattr(self, field, encrypted)
|
||||
if field not in update_fields:
|
||||
update_fields.append(field)
|
||||
self.encrypt_field(field, ask)
|
||||
self.mark_field_for_save(update_fields, field)
|
||||
super(PasswordFieldsModel, self).save(*args, **kwargs)
|
||||
# After saving a new instance for the first time, set the password
|
||||
# fields and save again.
|
||||
@@ -241,9 +239,17 @@ class PasswordFieldsModel(BaseModel):
|
||||
for field in self.PASSWORD_FIELDS:
|
||||
saved_value = getattr(self, '_saved_%s' % field, '')
|
||||
setattr(self, field, saved_value)
|
||||
update_fields.append(field)
|
||||
self.mark_field_for_save(update_fields, field)
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
def encrypt_field(self, field, ask):
|
||||
encrypted = encrypt_field(self, field, ask)
|
||||
setattr(self, field, encrypted)
|
||||
|
||||
def mark_field_for_save(self, update_fields, field):
|
||||
if field not in update_fields:
|
||||
update_fields.append(field)
|
||||
|
||||
|
||||
class PrimordialModel(CreatedModifiedModel):
|
||||
'''
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -156,7 +156,7 @@ class JobOptions(BaseModel):
|
||||
|
||||
def clean_credential(self):
|
||||
cred = self.credential
|
||||
if cred and cred.kind != 'ssh':
|
||||
if cred and cred.kind != 'machine':
|
||||
raise ValidationError(
|
||||
_('You must provide a machine / SSH credential.'),
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from awx.main.models import (
|
||||
JobTemplate,
|
||||
Job,
|
||||
NotificationTemplate,
|
||||
CredentialType,
|
||||
Credential,
|
||||
Inventory,
|
||||
Label,
|
||||
@@ -84,8 +85,14 @@ def mk_project(name, organization=None, description=None, persisted=True):
|
||||
return project
|
||||
|
||||
|
||||
def mk_credential(name, cloud=False, kind='ssh', persisted=True):
|
||||
cred = Credential(name=name, cloud=cloud, kind=kind)
|
||||
def mk_credential(name, credential_type='ssh', persisted=True):
|
||||
type_ = CredentialType.defaults[credential_type]()
|
||||
if persisted:
|
||||
type_.save()
|
||||
cred = Credential(
|
||||
credential_type=type_,
|
||||
name=name
|
||||
)
|
||||
if persisted:
|
||||
cred.save()
|
||||
return cred
|
||||
|
||||
@@ -213,12 +213,12 @@ def create_job_template(name, roles=None, persisted=True, **kwargs):
|
||||
if 'cloud_credential' in kwargs:
|
||||
cloud_cred = kwargs['cloud_credential']
|
||||
if type(cloud_cred) is not Credential:
|
||||
cloud_cred = mk_credential(cloud_cred, kind='aws', persisted=persisted)
|
||||
cloud_cred = mk_credential(cloud_cred, credential_type='aws', persisted=persisted)
|
||||
|
||||
if 'network_credential' in kwargs:
|
||||
net_cred = kwargs['network_credential']
|
||||
if type(net_cred) is not Credential:
|
||||
net_cred = mk_credential(net_cred, kind='net', persisted=persisted)
|
||||
net_cred = mk_credential(net_cred, credential_type='net', persisted=persisted)
|
||||
|
||||
if 'project' in kwargs:
|
||||
proj = kwargs['project']
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
205
awx/main/tests/functional/api/test_credential_type.py
Normal file
205
awx/main/tests/functional/api/test_credential_type.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from awx.main.models.credential import CredentialType
|
||||
from awx.api.versioning import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_as_unauthorized_xfail(get):
|
||||
response = get(reverse('api:credential_type_list'))
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_as_normal_user(get, alice):
|
||||
response = get(reverse('api:credential_type_list'), alice)
|
||||
assert response.status_code == 200
|
||||
assert response.data['count'] == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_as_admin(get, admin):
|
||||
response = get(reverse('api:credential_type_list'), admin)
|
||||
assert response.status_code == 200
|
||||
assert response.data['count'] == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_as_unauthorized_xfail(get, post):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'name': 'Custom Credential Type',
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_as_unauthorized_xfail(patch):
|
||||
ssh = CredentialType.defaults['ssh']()
|
||||
ssh.save()
|
||||
response = patch(
|
||||
reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}),
|
||||
{
|
||||
'name': 'Some Other Name'
|
||||
}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_as_unauthorized_xfail(delete):
|
||||
ssh = CredentialType.defaults['ssh']()
|
||||
ssh.save()
|
||||
response = delete(
|
||||
reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}),
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_as_normal_user_xfail(get, post, alice):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'name': 'Custom Credential Type',
|
||||
}, alice)
|
||||
assert response.status_code == 403
|
||||
assert get(reverse('api:credential_type_list'), alice).data['count'] == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_as_admin(get, post, admin):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'kind': 'cloud',
|
||||
'name': 'Custom Credential Type',
|
||||
'inputs': {},
|
||||
'injectors': {}
|
||||
}, admin)
|
||||
assert response.status_code == 201
|
||||
|
||||
response = get(reverse('api:credential_type_list'), admin)
|
||||
assert response.data['count'] == 1
|
||||
assert response.data['results'][0]['name'] == 'Custom Credential Type'
|
||||
assert response.data['results'][0]['inputs'] == {}
|
||||
assert response.data['results'][0]['injectors'] == {}
|
||||
assert response.data['results'][0]['managed_by_tower'] is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_managed_by_tower_readonly(get, post, admin):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'kind': 'cloud',
|
||||
'name': 'Custom Credential Type',
|
||||
'inputs': {},
|
||||
'injectors': {},
|
||||
'managed_by_tower': True
|
||||
}, admin)
|
||||
assert response.status_code == 201
|
||||
|
||||
response = get(reverse('api:credential_type_list'), admin)
|
||||
assert response.data['count'] == 1
|
||||
assert response.data['results'][0]['managed_by_tower'] is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_with_valid_inputs(get, post, admin):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'kind': 'cloud',
|
||||
'name': 'MyCloud',
|
||||
'inputs': {
|
||||
'fields': [{
|
||||
'id': 'api_token',
|
||||
'label': 'API Token',
|
||||
'type': 'string',
|
||||
'secret': True,
|
||||
'ask_at_runtime': True
|
||||
}]
|
||||
},
|
||||
'injectors': {}
|
||||
}, admin)
|
||||
assert response.status_code == 201
|
||||
|
||||
response = get(reverse('api:credential_type_list'), admin)
|
||||
assert response.data['count'] == 1
|
||||
fields = response.data['results'][0]['inputs']['fields']
|
||||
assert len(fields) == 1
|
||||
assert fields[0]['id'] == 'api_token'
|
||||
assert fields[0]['label'] == 'API Token'
|
||||
assert fields[0]['ask_at_runtime'] is True
|
||||
assert fields[0]['secret'] is True
|
||||
assert fields[0]['type'] == 'string'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_with_invalid_inputs_xfail(post, admin):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'kind': 'cloud',
|
||||
'name': 'MyCloud',
|
||||
'inputs': {'feeelds': {},},
|
||||
'injectors': {}
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
assert "'feeelds' was unexpected" in json.dumps(response.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_with_valid_injectors(get, post, admin):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'kind': 'cloud',
|
||||
'name': 'MyCloud',
|
||||
'inputs': {
|
||||
'fields': [{
|
||||
'id': 'api_token',
|
||||
'label': 'API Token',
|
||||
'type': 'string',
|
||||
'secret': True,
|
||||
'ask_at_runtime': True
|
||||
}]
|
||||
},
|
||||
'injectors': {
|
||||
'env': {
|
||||
'ANSIBLE_MY_CLOUD_TOKEN': '{{api_token}}'
|
||||
}
|
||||
}
|
||||
}, admin)
|
||||
assert response.status_code == 201
|
||||
|
||||
response = get(reverse('api:credential_type_list'), admin)
|
||||
assert response.data['count'] == 1
|
||||
injectors = response.data['results'][0]['injectors']
|
||||
assert len(injectors) == 1
|
||||
assert injectors['env'] == {
|
||||
'ANSIBLE_MY_CLOUD_TOKEN': '{{api_token}}'
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_with_invalid_injectors_xfail(post, admin):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'kind': 'cloud',
|
||||
'name': 'MyCloud',
|
||||
'inputs': {},
|
||||
'injectors': {'nonsense': 123}
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_with_undefined_template_variable_xfail(post, admin):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'kind': 'cloud',
|
||||
'name': 'MyCloud',
|
||||
'inputs': {
|
||||
'fields': [{
|
||||
'id': 'api_token',
|
||||
'label': 'API Token',
|
||||
'type': 'string',
|
||||
'secret': True,
|
||||
'ask_at_runtime': True
|
||||
}]
|
||||
},
|
||||
'injectors': {
|
||||
'env': {'ANSIBLE_MY_CLOUD_TOKEN': '{{api_tolkien}}'}
|
||||
}
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
assert "'api_tolkien' is undefined" in json.dumps(response.data)
|
||||
@@ -10,8 +10,15 @@ from awx.api.versioning import reverse
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runtime_data(organization):
|
||||
cred_obj = Credential.objects.create(name='runtime-cred', kind='ssh', username='test_user2', password='pas4word2')
|
||||
def runtime_data(organization, credentialtype_ssh):
|
||||
cred_obj = Credential.objects.create(
|
||||
name='runtime-cred',
|
||||
credential_type=credentialtype_ssh,
|
||||
inputs={
|
||||
'username': 'test_user2',
|
||||
'password': 'pas4word2'
|
||||
}
|
||||
)
|
||||
inv_obj = organization.inventories.create(name="runtime-inv")
|
||||
return dict(
|
||||
extra_vars='{"job_launch_var": 4}',
|
||||
|
||||
@@ -28,7 +28,7 @@ from rest_framework.test import (
|
||||
force_authenticate,
|
||||
)
|
||||
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.models.credential import CredentialType, Credential
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models.inventory import (
|
||||
Group,
|
||||
@@ -191,18 +191,43 @@ def organization(instance):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credential():
|
||||
return Credential.objects.create(kind='aws', name='test-cred', username='something', password='secret')
|
||||
def credentialtype_ssh():
|
||||
ssh = CredentialType.defaults['ssh']()
|
||||
ssh.save()
|
||||
return ssh
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def machine_credential():
|
||||
return Credential.objects.create(name='machine-cred', kind='ssh', username='test_user', password='pas4word')
|
||||
def credentialtype_aws():
|
||||
aws = CredentialType.defaults['aws']()
|
||||
aws.save()
|
||||
return aws
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def org_credential(organization):
|
||||
return Credential.objects.create(kind='aws', name='test-cred', username='something', password='secret', organization=organization)
|
||||
def credentialtype_net():
|
||||
net = CredentialType.defaults['net']()
|
||||
net.save()
|
||||
return net
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credential(credentialtype_aws):
|
||||
return Credential.objects.create(credential_type=credentialtype_aws, name='test-cred',
|
||||
inputs={'username': 'something', 'password': 'secret'})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def machine_credential(credentialtype_ssh):
|
||||
return Credential.objects.create(credential_type=credentialtype_ssh, name='machine-cred',
|
||||
inputs={'username': 'test_user', 'password': 'pas4word'})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def org_credential(organization, credentialtype_aws):
|
||||
return Credential.objects.create(credential_type=credentialtype_aws, name='test-cred',
|
||||
inputs={'username': 'something', 'password': 'secret'},
|
||||
organization=organization)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
289
awx/main/tests/functional/test_credential.py
Normal file
289
awx/main/tests/functional/test_credential.py
Normal file
@@ -0,0 +1,289 @@
|
||||
# Copyright (c) 2017 Ansible by Red Hat
|
||||
# All Rights Reserved.
|
||||
|
||||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from awx.main.utils.common import decrypt_field
|
||||
from awx.main.models import Credential, CredentialType
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_default_cred_types():
|
||||
assert sorted(CredentialType.defaults.keys()) == [
|
||||
'aws',
|
||||
'azure',
|
||||
'azure_rm',
|
||||
'cloudforms',
|
||||
'gce',
|
||||
'net',
|
||||
'openstack',
|
||||
'rackspace',
|
||||
'satellite6',
|
||||
'scm',
|
||||
'ssh',
|
||||
'vault',
|
||||
'vmware',
|
||||
]
|
||||
for type_ in CredentialType.defaults.values():
|
||||
assert type_().managed_by_tower is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('kind', ['net', 'scm', 'ssh', 'vault'])
|
||||
def test_cred_type_kind_uniqueness(kind):
|
||||
"""
|
||||
non-cloud credential types are exclusive_on_kind (you can only use *one* of
|
||||
them at a time)
|
||||
"""
|
||||
assert CredentialType.defaults[kind]().unique_by_kind is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cloud_kind_uniqueness():
|
||||
"""
|
||||
you can specify more than one cloud credential type (as long as they have
|
||||
different names so you don't e.g., use ec2 twice")
|
||||
"""
|
||||
assert CredentialType.defaults['aws']().unique_by_kind is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('input_, valid', [
|
||||
({}, True),
|
||||
({'fields': []}, True),
|
||||
({'fields': {}}, False),
|
||||
({'fields': 123}, False),
|
||||
({'fields': [{'id': 'username', 'label': 'Username', 'foo': 'bar'}]}, False),
|
||||
({'fields': [{'id': 'username', 'label': 'Username', 'type': 'string'}]}, True),
|
||||
({'fields': [{'id': 'username', 'label': 'Username', 'help_text': 1}]}, False),
|
||||
({'fields': [{'id': 'username', 'label': 'Username', 'help_text': 'Help Text'}]}, True), # noqa
|
||||
({'fields': [{'id': 'password', 'label': 'Password', 'type': 'number'}]}, True),
|
||||
({'fields': [{'id': 'ssh_key', 'label': 'SSH Key', 'type': 'ssh_private_key'}]}, True), # noqa
|
||||
({'fields': [{'id': 'other', 'label': 'Other', 'type': 'boolean'}]}, False),
|
||||
({'fields': [{'id': 'certificate', 'label': 'Cert', 'multiline': True}]}, True),
|
||||
({'fields': [{'id': 'certificate', 'label': 'Cert', 'multiline': 'bad'}]}, False), # noqa
|
||||
({'fields': [{'id': 'token', 'label': 'Token', 'secret': True}]}, True),
|
||||
({'fields': [{'id': 'token', 'label': 'Token', 'secret': 'bad'}]}, False),
|
||||
({'fields': [{'id': 'token', 'label': 'Token', 'ask_at_runtime': True}]}, True),
|
||||
({'fields': [{'id': 'token', 'label': 'Token', 'ask_at_runtime': 'bad'}]}, False), # noqa
|
||||
({'fields': [{'id': 'become_method', 'label': 'Become', 'choices': 'not-a-list'}]}, False), # noqa
|
||||
({'fields': [{'id': 'become_method', 'label': 'Become', 'choices': []}]}, False),
|
||||
({'fields': [{'id': 'become_method', 'label': 'Become', 'choices': ['su', 'sudo']}]}, True), # noqa
|
||||
({'fields': [{'id': 'become_method', 'label': 'Become', 'choices': ['dup', 'dup']}]}, False), # noqa
|
||||
])
|
||||
def test_cred_type_input_schema_validity(input_, valid):
|
||||
type_ = CredentialType(
|
||||
kind='cloud',
|
||||
name='SomeCloud',
|
||||
managed_by_tower=True,
|
||||
inputs=input_
|
||||
)
|
||||
if valid is False:
|
||||
with pytest.raises(ValidationError):
|
||||
type_.full_clean()
|
||||
else:
|
||||
type_.full_clean()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('injectors, valid', [
|
||||
({}, True),
|
||||
({'invalid-injector': {}}, False),
|
||||
({'file': 123}, False),
|
||||
({'file': {}}, False),
|
||||
({'file': {'template': '{{username}}'}}, True),
|
||||
({'file': {'foo': 'bar'}}, False),
|
||||
({'ssh': 123}, False),
|
||||
({'ssh': {}}, False),
|
||||
({'ssh': {'public': 'PUB'}}, False),
|
||||
({'ssh': {'private': 'PRIV'}}, False),
|
||||
({'ssh': {'public': 'PUB', 'private': 'PRIV'}}, True),
|
||||
({'ssh': {'public': 'PUB', 'private': 'PRIV', 'a': 'b'}}, False),
|
||||
({'password': {}}, False),
|
||||
({'password': {'key': 'Password:'}}, False),
|
||||
({'password': {'value': '{{pass}}'}}, False),
|
||||
({'password': {'key': 'Password:', 'value': '{{pass}}'}}, True),
|
||||
({'password': {'key': 'Password:', 'value': '{{pass}}', 'a': 'b'}}, False),
|
||||
({'env': 123}, False),
|
||||
({'env': {}}, True),
|
||||
({'env': {'AWX_SECRET': '{{awx_secret}}'}}, True),
|
||||
({'env': {'AWX_SECRET_99': '{{awx_secret}}'}}, True),
|
||||
({'env': {'99': '{{awx_secret}}'}}, False),
|
||||
({'env': {'AWX_SECRET=': '{{awx_secret}}'}}, False),
|
||||
({'extra_vars': 123}, False),
|
||||
({'extra_vars': {}}, True),
|
||||
({'extra_vars': {'hostname': '{{host}}'}}, True),
|
||||
({'extra_vars': {'hostname_99': '{{host}}'}}, True),
|
||||
({'extra_vars': {'99': '{{host}}'}}, False),
|
||||
({'extra_vars': {'99=': '{{host}}'}}, False),
|
||||
])
|
||||
def test_cred_type_injectors_schema(injectors, valid):
|
||||
type_ = CredentialType(
|
||||
kind='cloud',
|
||||
name='SomeCloud',
|
||||
managed_by_tower=True,
|
||||
inputs={
|
||||
'fields': [
|
||||
{'id': 'username', 'type': 'string', 'label': '_'},
|
||||
{'id': 'pass', 'type': 'string', 'label': '_'},
|
||||
{'id': 'awx_secret', 'type': 'string', 'label': '_'},
|
||||
{'id': 'host', 'type': 'string', 'label': '_'},
|
||||
]
|
||||
},
|
||||
injectors=injectors
|
||||
)
|
||||
if valid is False:
|
||||
with pytest.raises(ValidationError):
|
||||
type_.full_clean()
|
||||
else:
|
||||
type_.full_clean()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_creation(organization_factory):
|
||||
org = organization_factory('test').organization
|
||||
type_ = CredentialType(
|
||||
kind='cloud',
|
||||
name='SomeCloud',
|
||||
managed_by_tower=True,
|
||||
inputs={
|
||||
'fields': [{
|
||||
'id': 'username',
|
||||
'label': 'Username for SomeCloud',
|
||||
'type': 'string'
|
||||
}]
|
||||
}
|
||||
)
|
||||
type_.save()
|
||||
|
||||
cred = Credential(credential_type=type_, name="Bob's Credential",
|
||||
inputs={'username': 'bob'}, organization=org)
|
||||
cred.save()
|
||||
cred.full_clean()
|
||||
assert isinstance(cred, Credential)
|
||||
assert cred.name == "Bob's Credential"
|
||||
assert cred.inputs['username'] == cred.username == 'bob'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_creation_validation_failure(organization_factory):
|
||||
org = organization_factory('test').organization
|
||||
type_ = CredentialType(
|
||||
kind='cloud',
|
||||
name='SomeCloud',
|
||||
managed_by_tower=True,
|
||||
inputs={
|
||||
'fields': [{
|
||||
'id': 'username',
|
||||
'label': 'Username for SomeCloud',
|
||||
'type': 'string'
|
||||
}]
|
||||
}
|
||||
)
|
||||
type_.save()
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
cred = Credential(credential_type=type_, name="Bob's Credential",
|
||||
inputs={'user': 'wrong-key'}, organization=org)
|
||||
cred.save()
|
||||
cred.full_clean()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_encryption(organization_factory, credentialtype_ssh):
|
||||
org = organization_factory('test').organization
|
||||
cred = Credential(
|
||||
credential_type=credentialtype_ssh,
|
||||
name="Bob's Credential",
|
||||
inputs={'password': 'testing123'},
|
||||
organization=org
|
||||
)
|
||||
cred.save()
|
||||
|
||||
assert Credential.objects.count() == 1
|
||||
cred = Credential.objects.all()[:1].get()
|
||||
assert cred.inputs['password'].startswith('$encrypted$')
|
||||
assert decrypt_field(cred, 'password') == 'testing123'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_encryption_with_ask(organization_factory, credentialtype_ssh):
|
||||
org = organization_factory('test').organization
|
||||
cred = Credential(
|
||||
credential_type=credentialtype_ssh,
|
||||
name="Bob's Credential",
|
||||
inputs={'password': 'ASK'},
|
||||
organization=org
|
||||
)
|
||||
cred.save()
|
||||
|
||||
assert Credential.objects.count() == 1
|
||||
cred = Credential.objects.all()[:1].get()
|
||||
assert cred.inputs['password'] == 'ASK'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_with_multiple_secrets(organization_factory, credentialtype_ssh):
|
||||
org = organization_factory('test').organization
|
||||
cred = Credential(
|
||||
credential_type=credentialtype_ssh,
|
||||
name="Bob's Credential",
|
||||
inputs={'ssh_key_data': 'SOMEKEY', 'ssh_key_unlock': 'testing123'},
|
||||
organization=org
|
||||
)
|
||||
cred.save()
|
||||
|
||||
assert Credential.objects.count() == 1
|
||||
cred = Credential.objects.all()[:1].get()
|
||||
|
||||
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
|
||||
assert decrypt_field(cred, 'ssh_key_data') == 'SOMEKEY'
|
||||
assert cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
|
||||
assert decrypt_field(cred, 'ssh_key_unlock') == 'testing123'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_update(organization_factory, credentialtype_ssh):
|
||||
org = organization_factory('test').organization
|
||||
cred = Credential(
|
||||
credential_type=credentialtype_ssh,
|
||||
name="Bob's Credential",
|
||||
inputs={'password': 'testing123'},
|
||||
organization=org
|
||||
)
|
||||
cred.save()
|
||||
|
||||
assert Credential.objects.count() == 1
|
||||
cred = Credential.objects.all()[:1].get()
|
||||
cred.inputs['password'] = 'newpassword'
|
||||
cred.save()
|
||||
|
||||
assert Credential.objects.count() == 1
|
||||
cred = Credential.objects.all()[:1].get()
|
||||
assert cred.inputs['password'].startswith('$encrypted$')
|
||||
assert decrypt_field(cred, 'password') == 'newpassword'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_update_with_prior(organization_factory, credentialtype_ssh):
|
||||
org = organization_factory('test').organization
|
||||
cred = Credential(
|
||||
credential_type=credentialtype_ssh,
|
||||
name="Bob's Credential",
|
||||
inputs={'password': 'testing123'},
|
||||
organization=org
|
||||
)
|
||||
cred.save()
|
||||
|
||||
assert Credential.objects.count() == 1
|
||||
cred = Credential.objects.all()[:1].get()
|
||||
cred.inputs['username'] = 'joe'
|
||||
cred.inputs['password'] = '$encrypted$'
|
||||
cred.save()
|
||||
|
||||
assert Credential.objects.count() == 1
|
||||
cred = Credential.objects.all()[:1].get()
|
||||
assert cred.inputs['username'] == 'joe'
|
||||
assert cred.inputs['password'].startswith('$encrypted$')
|
||||
assert decrypt_field(cred, 'password') == 'testing123'
|
||||
@@ -5,12 +5,12 @@ from awx.main.models import Credential
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_unique_org_name_kind(organization_factory):
|
||||
def test_cred_unique_org_name_kind(organization_factory, credentialtype_ssh):
|
||||
objects = organization_factory("test")
|
||||
|
||||
cred = Credential(name="test", kind="net", organization=objects.organization)
|
||||
cred = Credential(name="test", credential_type=credentialtype_ssh, organization=objects.organization)
|
||||
cred.save()
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
cred = Credential(name="test", kind="net", organization=objects.organization)
|
||||
cred = Credential(name="test", credential_type=credentialtype_ssh, organization=objects.organization)
|
||||
cred.save()
|
||||
|
||||
@@ -21,12 +21,12 @@ def test_credential_migration_user(credential, user, permissions):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_two_teams_same_cred_name(organization_factory):
|
||||
def test_two_teams_same_cred_name(organization_factory, credentialtype_net):
|
||||
objects = organization_factory("test",
|
||||
teams=["team1", "team2"])
|
||||
|
||||
cred1 = Credential.objects.create(name="test", kind="net", deprecated_team=objects.teams.team1)
|
||||
cred2 = Credential.objects.create(name="test", kind="net", deprecated_team=objects.teams.team2)
|
||||
cred1 = Credential.objects.create(name="test", credential_type=credentialtype_net, deprecated_team=objects.teams.team1)
|
||||
cred2 = Credential.objects.create(name="test", credential_type=credentialtype_net, deprecated_team=objects.teams.team2)
|
||||
|
||||
rbac.migrate_credential(apps, None)
|
||||
|
||||
@@ -119,7 +119,7 @@ def test_credential_access_auditor(credential, organization_factory):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_access_admin(user, team, credential):
|
||||
def test_credential_access_admin(user, team, credential, credentialtype_aws):
|
||||
u = user('org-admin', False)
|
||||
team.organization.admin_role.members.add(u)
|
||||
|
||||
@@ -137,7 +137,7 @@ def test_credential_access_admin(user, team, credential):
|
||||
credential.admin_role.parents.add(team.admin_role)
|
||||
credential.save()
|
||||
|
||||
cred = Credential.objects.create(kind='aws', name='test-cred')
|
||||
cred = Credential.objects.create(credential_type=credentialtype_aws, name='test-cred')
|
||||
cred.deprecated_team = team
|
||||
cred.save()
|
||||
|
||||
|
||||
@@ -51,13 +51,20 @@ class TestJobRelaunchAccess:
|
||||
return jt.create_unified_job()
|
||||
|
||||
@pytest.fixture
|
||||
def job_with_prompts(self, machine_credential, inventory, organization):
|
||||
def job_with_prompts(self, machine_credential, inventory, organization, credentialtype_ssh):
|
||||
jt = JobTemplate.objects.create(
|
||||
name='test-job-template-prompts', credential=machine_credential, inventory=inventory,
|
||||
ask_tags_on_launch=True, ask_variables_on_launch=True, ask_skip_tags_on_launch=True,
|
||||
ask_limit_on_launch=True, ask_job_type_on_launch=True, ask_inventory_on_launch=True,
|
||||
ask_credential_on_launch=True)
|
||||
new_cred = Credential.objects.create(name='new-cred', kind='ssh', username='test_user', password='pas4word')
|
||||
new_cred = Credential.objects.create(
|
||||
name='new-cred',
|
||||
credential_type=credentialtype_ssh,
|
||||
inputs={
|
||||
'username': 'test_user',
|
||||
'password': 'pas4word'
|
||||
}
|
||||
)
|
||||
new_inv = Inventory.objects.create(name='new-inv', organization=organization)
|
||||
return jt.create_unified_job(credential=new_cred, inventory=new_inv)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from awx.main.migrations import _old_access as old_access
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_migration():
|
||||
def test_project_migration(credentialtype_ssh):
|
||||
'''
|
||||
|
||||
o1 o2 o3 with o1 -- i1 o2 -- i2
|
||||
@@ -59,7 +59,7 @@ def test_project_migration():
|
||||
o2 = Organization.objects.create(name='o2')
|
||||
o3 = Organization.objects.create(name='o3')
|
||||
|
||||
c1 = Credential.objects.create(name='c1')
|
||||
c1 = Credential.objects.create(name='c1', credential_type=credentialtype_ssh)
|
||||
|
||||
project_name = unicode("\xc3\xb4", "utf-8")
|
||||
p1 = Project.objects.create(name=project_name, credential=c1)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models import Inventory, Credential, Project
|
||||
from awx.main.models import Inventory, CredentialType, Credential, Project
|
||||
from awx.main.models.workflow import (
|
||||
WorkflowJobTemplate, WorkflowJobTemplateNode, WorkflowJobOptions,
|
||||
WorkflowJob, WorkflowJobNode
|
||||
@@ -125,7 +125,12 @@ def job_node_no_prompts(workflow_job_unit, jt_ask):
|
||||
def job_node_with_prompts(job_node_no_prompts):
|
||||
job_node_no_prompts.char_prompts = example_prompts
|
||||
job_node_no_prompts.inventory = Inventory(name='example-inv')
|
||||
job_node_no_prompts.credential = Credential(name='example-inv', kind='ssh', username='asdf', password='asdf')
|
||||
ssh_type = CredentialType.defaults['ssh']()
|
||||
job_node_no_prompts.credential = Credential(
|
||||
name='example-inv',
|
||||
credential_type=ssh_type,
|
||||
inputs={'username': 'asdf', 'password': 'asdf'}
|
||||
)
|
||||
return job_node_no_prompts
|
||||
|
||||
|
||||
@@ -138,7 +143,12 @@ def wfjt_node_no_prompts(workflow_job_template_unit, jt_ask):
|
||||
def wfjt_node_with_prompts(wfjt_node_no_prompts):
|
||||
wfjt_node_no_prompts.char_prompts = example_prompts
|
||||
wfjt_node_no_prompts.inventory = Inventory(name='example-inv')
|
||||
wfjt_node_no_prompts.credential = Credential(name='example-inv', kind='ssh', username='asdf', password='asdf')
|
||||
ssh_type = CredentialType.defaults['ssh']()
|
||||
wfjt_node_no_prompts.credential = Credential(
|
||||
name='example-inv',
|
||||
credential_type=ssh_type,
|
||||
inputs={'username': 'asdf', 'password': 'asdf'}
|
||||
)
|
||||
return wfjt_node_no_prompts
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.models.credential import CredentialType, Credential
|
||||
from awx.main.models.jobs import Job
|
||||
from awx.main.models.inventory import Inventory
|
||||
from awx.main.tasks import RunJob
|
||||
@@ -10,12 +10,15 @@ def test_aws_cred_parse(mocker):
|
||||
with mocker.patch('django.db.ConnectionRouter.db_for_write'):
|
||||
job = Job(id=1)
|
||||
job.inventory = mocker.MagicMock(spec=Inventory, id=2)
|
||||
aws = CredentialType.defaults['aws']()
|
||||
|
||||
options = {
|
||||
'kind': 'aws',
|
||||
'username': 'aws_user',
|
||||
'password': 'aws_passwd',
|
||||
'security_token': 'token',
|
||||
'credential_type': aws,
|
||||
'inputs': {
|
||||
'username': 'aws_user',
|
||||
'password': 'aws_passwd',
|
||||
'security_token': 'token',
|
||||
}
|
||||
}
|
||||
job.cloud_credential = Credential(**options)
|
||||
|
||||
@@ -23,22 +26,26 @@ def test_aws_cred_parse(mocker):
|
||||
mocker.patch.object(run_job, 'should_use_proot', return_value=False)
|
||||
|
||||
env = run_job.build_env(job, private_data_dir='/tmp')
|
||||
assert env['AWS_ACCESS_KEY'] == options['username']
|
||||
assert env['AWS_SECRET_KEY'] == options['password']
|
||||
assert env['AWS_SECURITY_TOKEN'] == options['security_token']
|
||||
assert env['AWS_ACCESS_KEY'] == options['inputs']['username']
|
||||
assert env['AWS_SECRET_KEY'] == options['inputs']['password']
|
||||
assert env['AWS_SECURITY_TOKEN'] == options['inputs']['security_token']
|
||||
|
||||
|
||||
def test_net_cred_parse(mocker):
|
||||
with mocker.patch('django.db.ConnectionRouter.db_for_write'):
|
||||
job = Job(id=1)
|
||||
job.inventory = mocker.MagicMock(spec=Inventory, id=2)
|
||||
net = CredentialType.defaults['aws']()
|
||||
|
||||
options = {
|
||||
'username':'test',
|
||||
'password':'test',
|
||||
'authorize': True,
|
||||
'authorize_password': 'passwd',
|
||||
'ssh_key_data': """-----BEGIN PRIVATE KEY-----\nstuff==\n-----END PRIVATE KEY-----""",
|
||||
'credential_type': net,
|
||||
'inputs': {
|
||||
'username':'test',
|
||||
'password':'test',
|
||||
'authorize': True,
|
||||
'authorize_password': 'passwd',
|
||||
'ssh_key_data': """-----BEGIN PRIVATE KEY-----\nstuff==\n-----END PRIVATE KEY-----""",
|
||||
}
|
||||
}
|
||||
private_data_files = {
|
||||
'network_credential': '/tmp/this_file_does_not_exist_during_test_but_the_path_is_real',
|
||||
@@ -49,21 +56,25 @@ def test_net_cred_parse(mocker):
|
||||
mocker.patch.object(run_job, 'should_use_proot', return_value=False)
|
||||
|
||||
env = run_job.build_env(job, private_data_dir='/tmp', private_data_files=private_data_files)
|
||||
assert env['ANSIBLE_NET_USERNAME'] == options['username']
|
||||
assert env['ANSIBLE_NET_PASSWORD'] == options['password']
|
||||
assert env['ANSIBLE_NET_USERNAME'] == options['inputs']['username']
|
||||
assert env['ANSIBLE_NET_PASSWORD'] == options['inputs']['password']
|
||||
assert env['ANSIBLE_NET_AUTHORIZE'] == '1'
|
||||
assert env['ANSIBLE_NET_AUTH_PASS'] == options['authorize_password']
|
||||
assert env['ANSIBLE_NET_AUTH_PASS'] == options['inputs']['authorize_password']
|
||||
assert env['ANSIBLE_NET_SSH_KEYFILE'] == private_data_files['network_credential']
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_job(mocker):
|
||||
ssh = CredentialType.defaults['ssh']()
|
||||
options = {
|
||||
'username':'test',
|
||||
'password':'test',
|
||||
'ssh_key_data': """-----BEGIN PRIVATE KEY-----\nstuff==\n-----END PRIVATE KEY-----""",
|
||||
'authorize': True,
|
||||
'authorize_password': 'passwd',
|
||||
'credential_type': ssh,
|
||||
'inputs': {
|
||||
'username':'test',
|
||||
'password':'test',
|
||||
'ssh_key_data': """-----BEGIN PRIVATE KEY-----\nstuff==\n-----END PRIVATE KEY-----""",
|
||||
'authorize': True,
|
||||
'authorize_password': 'passwd',
|
||||
}
|
||||
}
|
||||
|
||||
mock_job_attrs = {'forks': False, 'id': 1, 'cancel_flag': False, 'status': 'running', 'job_type': 'normal',
|
||||
|
||||
Reference in New Issue
Block a user