move code linting to a stricter pep8-esque auto-formatting tool, black

This commit is contained in:
Ryan Petrello
2021-03-19 12:44:51 -04:00
parent 9b702e46fe
commit c2ef0a6500
671 changed files with 20538 additions and 21924 deletions

View File

@@ -18,7 +18,6 @@ logger = logging.getLogger('awx.api.authentication')
class LoggedBasicAuthentication(authentication.BasicAuthentication):
def authenticate(self, request):
if not settings.AUTH_BASIC_ENABLED:
return
@@ -35,22 +34,18 @@ class LoggedBasicAuthentication(authentication.BasicAuthentication):
class SessionAuthentication(authentication.SessionAuthentication):
def authenticate_header(self, request):
return 'Session'
class LoggedOAuth2Authentication(OAuth2Authentication):
def authenticate(self, request):
ret = super(LoggedOAuth2Authentication, self).authenticate(request)
if ret:
user, token = ret
username = user.username if user else '<none>'
logger.info(smart_text(
u"User {} performed a {} to {} through the API using OAuth 2 token {}.".format(
username, request.method, request.path, token.pk
)
))
logger.info(
smart_text(u"User {} performed a {} to {} through the API using OAuth 2 token {}.".format(username, request.method, request.path, token.pk))
)
setattr(user, 'oauth_scopes', [x for x in token.scope.split() if x])
return ret

View File

@@ -38,16 +38,20 @@ register(
register(
'OAUTH2_PROVIDER',
field_class=OAuth2ProviderField,
default={'ACCESS_TOKEN_EXPIRE_SECONDS': oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
'AUTHORIZATION_CODE_EXPIRE_SECONDS': oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS,
'REFRESH_TOKEN_EXPIRE_SECONDS': oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS},
default={
'ACCESS_TOKEN_EXPIRE_SECONDS': oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
'AUTHORIZATION_CODE_EXPIRE_SECONDS': oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS,
'REFRESH_TOKEN_EXPIRE_SECONDS': oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
},
label=_('OAuth 2 Timeout Settings'),
help_text=_('Dictionary for customizing OAuth 2 timeouts, available items are '
'`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number '
'of seconds, `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of '
'authorization codes in the number of seconds, and `REFRESH_TOKEN_EXPIRE_SECONDS`, '
'the duration of refresh tokens, after expired access tokens, '
'in the number of seconds.'),
help_text=_(
'Dictionary for customizing OAuth 2 timeouts, available items are '
'`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number '
'of seconds, `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of '
'authorization codes in the number of seconds, and `REFRESH_TOKEN_EXPIRE_SECONDS`, '
'the duration of refresh tokens, after expired access tokens, '
'in the number of seconds.'
),
category=_('Authentication'),
category_slug='authentication',
unit=_('seconds'),
@@ -57,10 +61,12 @@ register(
field_class=fields.BooleanField,
default=False,
label=_('Allow External Users to Create OAuth2 Tokens'),
help_text=_('For security reasons, users from external auth providers (LDAP, SAML, '
'SSO, Radius, and others) are not allowed to create OAuth2 tokens. '
'To change this behavior, enable this setting. Existing tokens will '
'not be deleted when this setting is toggled off.'),
help_text=_(
'For security reasons, users from external auth providers (LDAP, SAML, '
'SSO, Radius, and others) are not allowed to create OAuth2 tokens. '
'To change this behavior, enable this setting. Existing tokens will '
'not be deleted when this setting is toggled off.'
),
category=_('Authentication'),
category_slug='authentication',
)
@@ -71,8 +77,7 @@ register(
required=False,
default='',
label=_('Login redirect override URL'),
help_text=_('URL to which unauthorized users will be redirected to log in. '
'If blank, users will be sent to the Tower login page.'),
help_text=_('URL to which unauthorized users will be redirected to log in. If blank, users will be sent to the Tower login page.'),
category=_('Authentication'),
category_slug='authentication',
)

View File

@@ -16,7 +16,4 @@ class ActiveJobConflict(ValidationError):
# turn everything in self.detail into string by using force_text.
# Declare detail afterwards circumvent this behavior.
super(ActiveJobConflict, self).__init__()
self.detail = {
"error": _("Resource is being used by running jobs."),
"active_jobs": active_jobs
}
self.detail = {"error": _("Resource is being used by running jobs."), "active_jobs": active_jobs}

View File

@@ -16,10 +16,10 @@ __all__ = ['BooleanNullField', 'CharNullField', 'ChoiceNullField', 'VerbatimFiel
class NullFieldMixin(object):
'''
"""
Mixin to prevent shortcutting validation when we want to allow null input,
but coerce the resulting value to another type.
'''
"""
def validate_empty_values(self, data):
(is_empty_value, data) = super(NullFieldMixin, self).validate_empty_values(data)
@@ -29,18 +29,18 @@ class NullFieldMixin(object):
class BooleanNullField(NullFieldMixin, serializers.NullBooleanField):
'''
"""
Custom boolean field that allows null and empty string as False values.
'''
"""
def to_internal_value(self, data):
return bool(super(BooleanNullField, self).to_internal_value(data))
class CharNullField(NullFieldMixin, serializers.CharField):
'''
"""
Custom char field that allows null as input and coerces to an empty string.
'''
"""
def __init__(self, **kwargs):
kwargs['allow_null'] = True
@@ -51,9 +51,9 @@ class CharNullField(NullFieldMixin, serializers.CharField):
class ChoiceNullField(NullFieldMixin, serializers.ChoiceField):
'''
"""
Custom choice field that allows null as input and coerces to an empty string.
'''
"""
def __init__(self, **kwargs):
kwargs['allow_null'] = True
@@ -64,9 +64,9 @@ class ChoiceNullField(NullFieldMixin, serializers.ChoiceField):
class VerbatimField(serializers.Field):
'''
"""
Custom field that passes the value through without changes.
'''
"""
def to_internal_value(self, data):
return data
@@ -77,22 +77,19 @@ class VerbatimField(serializers.Field):
class OAuth2ProviderField(fields.DictField):
default_error_messages = {
'invalid_key_names': _('Invalid key names: {invalid_key_names}'),
}
default_error_messages = {'invalid_key_names': _('Invalid key names: {invalid_key_names}')}
valid_key_names = {'ACCESS_TOKEN_EXPIRE_SECONDS', 'AUTHORIZATION_CODE_EXPIRE_SECONDS', 'REFRESH_TOKEN_EXPIRE_SECONDS'}
child = fields.IntegerField(min_value=1)
def to_internal_value(self, data):
data = super(OAuth2ProviderField, self).to_internal_value(data)
invalid_flags = (set(data.keys()) - self.valid_key_names)
invalid_flags = set(data.keys()) - self.valid_key_names
if invalid_flags:
self.fail('invalid_key_names', invalid_key_names=', '.join(list(invalid_flags)))
return data
class DeprecatedCredentialField(serializers.IntegerField):
def __init__(self, **kwargs):
kwargs['allow_null'] = True
kwargs['default'] = None

View File

@@ -27,9 +27,9 @@ from awx.main.utils.db import get_all_field_names
class TypeFilterBackend(BaseFilterBackend):
'''
"""
Filter on type field now returned with all objects.
'''
"""
def filter_queryset(self, request, queryset, view):
try:
@@ -64,7 +64,7 @@ class TypeFilterBackend(BaseFilterBackend):
def get_fields_from_path(model, path):
'''
"""
Given a Django ORM lookup path (possibly over multiple models)
Returns the fields in the line, and also the revised lookup path
ex., given
@@ -73,7 +73,7 @@ def get_fields_from_path(model, path):
returns tuple of fields traversed as well and a corrected path,
for special cases we do substitutions
([<IntegerField for timeout>], 'project__timeout')
'''
"""
# Store of all the fields used to detect repeats
field_list = []
new_parts = []
@@ -82,12 +82,9 @@ def get_fields_from_path(model, path):
raise ParseError(_('No related model for field {}.').format(name))
# HACK: Make project and inventory source filtering by old field names work for backwards compatibility.
if model._meta.object_name in ('Project', 'InventorySource'):
name = {
'current_update': 'current_job',
'last_update': 'last_job',
'last_update_failed': 'last_job_failed',
'last_updated': 'last_job_run',
}.get(name, name)
name = {'current_update': 'current_job', 'last_update': 'last_job', 'last_update_failed': 'last_job_failed', 'last_updated': 'last_job_run'}.get(
name, name
)
if name == 'type' and 'polymorphic_ctype' in get_all_field_names(model):
name = 'polymorphic_ctype'
@@ -121,28 +118,42 @@ def get_fields_from_path(model, path):
def get_field_from_path(model, path):
'''
"""
Given a Django ORM lookup path (possibly over multiple models)
Returns the last field in the line, and the revised lookup path
ex.
(<IntegerField for timeout>, 'project__timeout')
'''
"""
field_list, new_path = get_fields_from_path(model, path)
return (field_list[-1], new_path)
class FieldLookupBackend(BaseFilterBackend):
'''
"""
Filter using field lookups provided via query string parameters.
'''
"""
RESERVED_NAMES = ('page', 'page_size', 'format', 'order', 'order_by',
'search', 'type', 'host_filter', 'count_disabled', 'no_truncate')
RESERVED_NAMES = ('page', 'page_size', 'format', 'order', 'order_by', 'search', 'type', 'host_filter', 'count_disabled', 'no_truncate')
SUPPORTED_LOOKUPS = ('exact', 'iexact', 'contains', 'icontains',
'startswith', 'istartswith', 'endswith', 'iendswith',
'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'in',
'isnull', 'search')
SUPPORTED_LOOKUPS = (
'exact',
'iexact',
'contains',
'icontains',
'startswith',
'istartswith',
'endswith',
'iendswith',
'regex',
'iregex',
'gt',
'gte',
'lt',
'lte',
'in',
'isnull',
'search',
)
# A list of fields that we know can be filtered on without the possiblity
# of introducing duplicates
@@ -189,10 +200,7 @@ class FieldLookupBackend(BaseFilterBackend):
try:
return self.to_python_related(value)
except ValueError:
raise ParseError(_('Invalid {field_name} id: {field_id}').format(
field_name=getattr(field, 'name', 'related field'),
field_id=value)
)
raise ParseError(_('Invalid {field_name} id: {field_id}').format(field_name=getattr(field, 'name', 'related field'), field_id=value))
else:
return field.to_python(value)
@@ -205,13 +213,13 @@ class FieldLookupBackend(BaseFilterBackend):
field_list, new_lookup = self.get_fields_from_lookup(model, lookup)
field = field_list[-1]
needs_distinct = (not all(isinstance(f, self.NO_DUPLICATES_ALLOW_LIST) for f in field_list))
needs_distinct = not all(isinstance(f, self.NO_DUPLICATES_ALLOW_LIST) for f in field_list)
# Type names are stored without underscores internally, but are presented and
# and serialized over the API containing underscores so we remove `_`
# for polymorphic_ctype__model lookups.
if new_lookup.startswith('polymorphic_ctype__model'):
value = value.replace('_','')
value = value.replace('_', '')
elif new_lookup.endswith('__isnull'):
value = to_python_boolean(value)
elif new_lookup.endswith('__in'):
@@ -329,24 +337,20 @@ class FieldLookupBackend(BaseFilterBackend):
args = []
for n, k, v in and_filters:
if n:
args.append(~Q(**{k:v}))
args.append(~Q(**{k: v}))
else:
args.append(Q(**{k:v}))
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))
)
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:
for n, k, v in or_filters:
if n:
q |= ~Q(**{k:v})
q |= ~Q(**{k: v})
else:
q |= Q(**{k:v})
q |= Q(**{k: v})
args.append(q)
if search_filters and search_filter_relation == 'OR':
q = Q()
@@ -360,11 +364,11 @@ class FieldLookupBackend(BaseFilterBackend):
for constrain in constrains:
q_chain |= Q(**{constrain: term})
queryset = queryset.filter(q_chain)
for n,k,v in chain_filters:
for n, k, v in chain_filters:
if n:
q = ~Q(**{k:v})
q = ~Q(**{k: v})
else:
q = Q(**{k:v})
q = Q(**{k: v})
queryset = queryset.filter(q)
queryset = queryset.filter(*args)
if needs_distinct:
@@ -377,9 +381,9 @@ class FieldLookupBackend(BaseFilterBackend):
class OrderByBackend(BaseFilterBackend):
'''
"""
Filter to apply ordering based on query string parameters.
'''
"""
def filter_queryset(self, request, queryset, view):
try:

View File

@@ -35,55 +35,50 @@ from rest_framework.negotiation import DefaultContentNegotiation
# AWX
from awx.api.filters import FieldLookupBackend
from awx.main.models import (
UnifiedJob, UnifiedJobTemplate, User, Role, Credential,
WorkflowJobTemplateNode, WorkflowApprovalTemplate
)
from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate
from awx.main.access import access_registry
from awx.main.utils import (
camelcase_to_underscore,
get_search_fields,
getattrd,
get_object_or_400,
decrypt_field,
get_awx_version,
)
from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version
from awx.main.utils.db import get_all_field_names
from awx.main.views import ApiErrorView
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
from awx.api.versioning import URLPathVersioning
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
'SubListDestroyAPIView',
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
'RetrieveUpdateDestroyAPIView',
'SubDetailAPIView',
'ResourceAccessList',
'ParentMixin',
'DeleteLastUnattachLabelMixin',
'SubListAttachDetachAPIView',
'CopyAPIView', 'BaseUsersList',]
__all__ = [
'APIView',
'GenericAPIView',
'ListAPIView',
'SimpleListAPIView',
'ListCreateAPIView',
'SubListAPIView',
'SubListCreateAPIView',
'SubListDestroyAPIView',
'SubListCreateAttachDetachAPIView',
'RetrieveAPIView',
'RetrieveUpdateAPIView',
'RetrieveDestroyAPIView',
'RetrieveUpdateDestroyAPIView',
'SubDetailAPIView',
'ResourceAccessList',
'ParentMixin',
'DeleteLastUnattachLabelMixin',
'SubListAttachDetachAPIView',
'CopyAPIView',
'BaseUsersList',
]
logger = logging.getLogger('awx.api.generics')
analytics_logger = logging.getLogger('awx.analytics.performance')
class LoggedLoginView(auth_views.LoginView):
def get(self, request, *args, **kwargs):
# The django.auth.contrib login form doesn't perform the content
# negotiation we've come to expect from DRF; add in code to catch
# situations where Accept != text/html (or */*) and reply with
# an HTTP 406
try:
DefaultContentNegotiation().select_renderer(
request,
[StaticHTMLRenderer],
'html'
)
DefaultContentNegotiation().select_renderer(request, [StaticHTMLRenderer], 'html')
except NotAcceptable:
resp = Response(status=status.HTTP_406_NOT_ACCEPTABLE)
resp.accepted_renderer = StaticHTMLRenderer()
@@ -96,7 +91,7 @@ class LoggedLoginView(auth_views.LoginView):
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
current_user = getattr(request, 'user', None)
if request.user.is_authenticated:
logger.info(smart_text(u"User {} logged in from {}".format(self.request.user.username,request.META.get('REMOTE_ADDR', None))))
logger.info(smart_text(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None))))
ret.set_cookie('userLoggedIn', 'true')
current_user = UserSerializer(self.request.user)
current_user = smart_text(JSONRenderer().render(current_user.data))
@@ -106,29 +101,27 @@ class LoggedLoginView(auth_views.LoginView):
return ret
else:
if 'username' in self.request.POST:
logger.warn(smart_text(u"Login failed for user {} from {}".format(self.request.POST.get('username'),request.META.get('REMOTE_ADDR', None))))
logger.warn(smart_text(u"Login failed for user {} from {}".format(self.request.POST.get('username'), request.META.get('REMOTE_ADDR', None))))
ret.status_code = 401
return ret
class LoggedLogoutView(auth_views.LogoutView):
def dispatch(self, request, *args, **kwargs):
original_user = getattr(request, 'user', None)
ret = super(LoggedLogoutView, self).dispatch(request, *args, **kwargs)
current_user = getattr(request, 'user', None)
ret.set_cookie('userLoggedIn', 'false')
if (not current_user or not getattr(current_user, 'pk', True)) \
and current_user != original_user:
if (not current_user or not getattr(current_user, 'pk', True)) and current_user != original_user:
logger.info("User {} logged out.".format(original_user.username))
return ret
def get_view_description(view, html=False):
'''Wrapper around REST framework get_view_description() to continue
"""Wrapper around REST framework get_view_description() to continue
to support our historical div.
'''
"""
desc = views.get_view_description(view, html=html)
if html:
desc = '<div class="description">%s</div>' % desc
@@ -138,6 +131,7 @@ def get_view_description(view, html=False):
def get_default_schema():
if settings.SETTINGS_MODULE == 'awx.settings.development':
from awx.api.swagger import AutoSchema
return AutoSchema()
else:
return views.APIView.schema
@@ -149,21 +143,23 @@ class APIView(views.APIView):
versioning_class = URLPathVersioning
def initialize_request(self, request, *args, **kwargs):
'''
"""
Store the Django REST Framework Request object as an attribute on the
normal Django request, store time the request started.
'''
"""
self.time_started = time.time()
if getattr(settings, 'SQL_DEBUG', False):
self.queries_before = len(connection.queries)
# If there are any custom headers in REMOTE_HOST_HEADERS, make sure
# they respect the allowed proxy list
if all([
settings.PROXY_IP_ALLOWED_LIST,
request.environ.get('REMOTE_ADDR') not in settings.PROXY_IP_ALLOWED_LIST,
request.environ.get('REMOTE_HOST') not in settings.PROXY_IP_ALLOWED_LIST
]):
if all(
[
settings.PROXY_IP_ALLOWED_LIST,
request.environ.get('REMOTE_ADDR') not in settings.PROXY_IP_ALLOWED_LIST,
request.environ.get('REMOTE_HOST') not in settings.PROXY_IP_ALLOWED_LIST,
]
):
for custom_header in settings.REMOTE_HOST_HEADERS:
if custom_header.startswith('HTTP_'):
request.environ.pop(custom_header, None)
@@ -178,17 +174,19 @@ class APIView(views.APIView):
request.drf_request_user = None
self.__init_request_error__ = exc
except UnsupportedMediaType as exc:
exc.detail = _('You did not use correct Content-Type in your HTTP request. '
'If you are using our REST API, the Content-Type must be application/json')
exc.detail = _(
'You did not use correct Content-Type in your HTTP request. ' 'If you are using our REST API, the Content-Type must be application/json'
)
self.__init_request_error__ = exc
return drf_request
def finalize_response(self, request, response, *args, **kwargs):
'''
"""
Log warning for 400 requests. Add header with elapsed time.
'''
"""
from awx.main.utils import get_licenser
from awx.main.utils.licensing import OpenLicense
#
# If the URL was rewritten, and we get a 404, we should entirely
# replace the view in the request context with an ApiErrorView()
@@ -212,8 +210,12 @@ class APIView(views.APIView):
return response
if response.status_code >= 400:
status_msg = "status %s received by user %s attempting to access %s from %s" % \
(response.status_code, request.user, request.path, request.META.get('REMOTE_ADDR', None))
status_msg = "status %s received by user %s attempting to access %s from %s" % (
response.status_code,
request.user,
request.path,
request.META.get('REMOTE_ADDR', None),
)
if hasattr(self, '__init_request_error__'):
response = self.handle_exception(self.__init_request_error__)
if response.status_code == 401:
@@ -225,7 +227,7 @@ class APIView(views.APIView):
time_started = getattr(self, 'time_started', None)
response['X-API-Product-Version'] = get_awx_version()
response['X-API-Product-Name'] = 'AWX' if isinstance(get_licenser(), OpenLicense) else 'Red Hat Ansible Tower'
response['X-API-Node'] = settings.CLUSTER_HOST_ID
if time_started:
time_elapsed = time.time() - self.time_started
@@ -311,18 +313,12 @@ class APIView(views.APIView):
return data
def determine_version(self, request, *args, **kwargs):
return (
getattr(request, 'version', None),
getattr(request, 'versioning_scheme', None),
)
return (getattr(request, 'version', None), getattr(request, 'versioning_scheme', None))
def dispatch(self, request, *args, **kwargs):
if self.versioning_class is not None:
scheme = self.versioning_class()
request.version, request.versioning_scheme = (
scheme.determine_version(request, *args, **kwargs),
scheme
)
request.version, request.versioning_scheme = (scheme.determine_version(request, *args, **kwargs), scheme)
if 'version' in kwargs:
kwargs.pop('version')
return super(APIView, self).dispatch(request, *args, **kwargs)
@@ -378,25 +374,22 @@ class GenericAPIView(generics.GenericAPIView, APIView):
d = super(GenericAPIView, self).get_description_context()
if hasattr(self.model, "_meta"):
if hasattr(self.model._meta, "verbose_name"):
d.update({
'model_verbose_name': smart_text(self.model._meta.verbose_name),
'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
})
d.update(
{
'model_verbose_name': smart_text(self.model._meta.verbose_name),
'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
}
)
serializer = self.get_serializer()
metadata = self.metadata_class()
metadata.request = self.request
for method, key in [
('GET', 'serializer_fields'),
('POST', 'serializer_create_fields'),
('PUT', 'serializer_update_fields')
]:
for method, key in [('GET', 'serializer_fields'), ('POST', 'serializer_create_fields'), ('PUT', 'serializer_update_fields')]:
d[key] = metadata.get_serializer_info(serializer, method=method)
d['settings'] = settings
return d
class SimpleListAPIView(generics.ListAPIView, GenericAPIView):
def get_queryset(self):
return self.request.user.get_queryset(self.model)
@@ -413,9 +406,7 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
else:
order_field = 'name'
d = super(ListAPIView, self).get_description_context()
d.update({
'order_field': order_field,
})
d.update({'order_field': order_field})
return d
@property
@@ -426,9 +417,13 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
def related_search_fields(self):
def skip_related_name(name):
return (
name is None or name.endswith('_role') or name.startswith('_') or
name.startswith('deprecated_') or name.endswith('_set') or
name == 'polymorphic_ctype')
name is None
or name.endswith('_role')
or name.startswith('_')
or name.startswith('deprecated_')
or name.endswith('_set')
or name == 'polymorphic_ctype'
)
fields = set([])
for field in self.model._meta.fields:
@@ -482,9 +477,7 @@ class ParentMixin(object):
def get_parent_object(self):
if self.parent_object is not None:
return self.parent_object
parent_filter = {
self.lookup_field: self.kwargs.get(self.lookup_field, None),
}
parent_filter = {self.lookup_field: self.kwargs.get(self.lookup_field, None)}
self.parent_object = get_object_or_404(self.parent_model, **parent_filter)
return self.parent_object
@@ -513,10 +506,12 @@ class SubListAPIView(ParentMixin, ListAPIView):
def get_description_context(self):
d = super(SubListAPIView, self).get_description_context()
d.update({
'parent_model_verbose_name': smart_text(self.parent_model._meta.verbose_name),
'parent_model_verbose_name_plural': smart_text(self.parent_model._meta.verbose_name_plural),
})
d.update(
{
'parent_model_verbose_name': smart_text(self.parent_model._meta.verbose_name),
'parent_model_verbose_name_plural': smart_text(self.parent_model._meta.verbose_name_plural),
}
)
return d
def get_queryset(self):
@@ -531,7 +526,6 @@ class SubListAPIView(ParentMixin, ListAPIView):
class DestroyAPIView(generics.DestroyAPIView):
def has_delete_permission(self, obj):
return self.request.user.can_access(self.model, 'delete', obj)
@@ -545,12 +539,12 @@ class SubListDestroyAPIView(DestroyAPIView, SubListAPIView):
"""
Concrete view for deleting everything related by `relationship`.
"""
check_sub_obj_permission = True
def destroy(self, request, *args, **kwargs):
instance_list = self.get_queryset()
if (not self.check_sub_obj_permission and
not request.user.can_access(self.parent_model, 'delete', self.get_parent_object())):
if not self.check_sub_obj_permission and not request.user.can_access(self.parent_model, 'delete', self.get_parent_object()):
raise PermissionDenied()
self.perform_list_destroy(instance_list)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -574,9 +568,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
def get_description_context(self):
d = super(SubListCreateAPIView, self).get_description_context()
d.update({
'parent_key': getattr(self, 'parent_key', None),
})
d.update({'parent_key': getattr(self, 'parent_key', None)})
return d
def get_queryset(self):
@@ -610,8 +602,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
# attempt to deserialize the object
serializer = self.get_serializer(data=data)
if not serializer.is_valid():
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Verify we have permission to add the object as given.
if not request.user.can_access(self.model, 'add', serializer.validated_data):
@@ -635,9 +626,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
def get_description_context(self):
d = super(SubListCreateAttachDetachAPIView, self).get_description_context()
d.update({
"has_attach": True,
})
d.update({"has_attach": True})
return d
def attach_validate(self, request):
@@ -675,9 +664,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
sub = get_object_or_400(self.model, pk=sub_id)
# Verify we have permission to attach.
if not request.user.can_access(self.parent_model, 'attach', parent, sub,
self.relationship, data,
skip_sub_obj_read_check=created):
if not request.user.can_access(self.parent_model, 'attach', parent, sub, self.relationship, data, skip_sub_obj_read_check=created):
raise PermissionDenied()
# Verify that the relationship to be added is valid.
@@ -716,8 +703,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
relationship = getattrd(parent, self.relationship)
sub = get_object_or_400(self.model, pk=sub_id)
if not request.user.can_access(self.parent_model, 'unattach', parent,
sub, self.relationship, request.data):
if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship, request.data):
raise PermissionDenied()
if parent_key:
@@ -735,28 +721,24 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
def post(self, request, *args, **kwargs):
if not isinstance(request.data, dict):
return Response('invalid type for post data',
status=status.HTTP_400_BAD_REQUEST)
return Response('invalid type for post data', status=status.HTTP_400_BAD_REQUEST)
if 'disassociate' in request.data:
return self.unattach(request, *args, **kwargs)
else:
return self.attach(request, *args, **kwargs)
class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView):
'''
"""
Derived version of SubListCreateAttachDetachAPIView that prohibits creation
'''
"""
metadata_class = SublistAttachDetatchMetadata
def post(self, request, *args, **kwargs):
sub_id = request.data.get('id', None)
if not sub_id:
return Response(
dict(msg=_("{} 'id' field is missing.".format(
self.model._meta.verbose_name.title()))),
status=status.HTTP_400_BAD_REQUEST)
return Response(dict(msg=_("{} 'id' field is missing.".format(self.model._meta.verbose_name.title()))), status=status.HTTP_400_BAD_REQUEST)
return super(SubListAttachDetachAPIView, self).post(request, *args, **kwargs)
def update_raw_data(self, data):
@@ -768,11 +750,11 @@ class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView):
class DeleteLastUnattachLabelMixin(object):
'''
"""
Models for which you want the last instance to be deleted from the database
when the last disassociate is called should inherit from this class. Further,
the model should implement is_detached()
'''
"""
def unattach(self, request, *args, **kwargs):
(sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request)
@@ -798,7 +780,6 @@ class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
def update(self, request, *args, **kwargs):
self.update_filter(request, *args, **kwargs)
return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs)
@@ -839,6 +820,7 @@ class ResourceAccessList(ParentMixin, ListAPIView):
def trigger_delayed_deep_copy(*args, **kwargs):
from awx.main.tasks import deep_copy_model_obj
connection.on_commit(lambda: deep_copy_model_obj.delay(*args, **kwargs))
@@ -869,8 +851,7 @@ class CopyAPIView(GenericAPIView):
field_val[secret] = decrypt_field(obj, secret)
elif isinstance(field_val, dict):
for sub_field in field_val:
if isinstance(sub_field, str) \
and isinstance(field_val[sub_field], str):
if isinstance(sub_field, str) and isinstance(field_val[sub_field], str):
field_val[sub_field] = decrypt_field(obj, field_name, sub_field)
elif isinstance(field_val, str):
try:
@@ -882,15 +863,11 @@ class CopyAPIView(GenericAPIView):
def _build_create_dict(self, obj):
ret = {}
if self.copy_return_serializer_class:
all_fields = Metadata().get_serializer_info(
self._get_copy_return_serializer(), method='POST'
)
all_fields = Metadata().get_serializer_info(self._get_copy_return_serializer(), method='POST')
for field_name, field_info in all_fields.items():
if not hasattr(obj, field_name) or field_info.get('read_only', True):
continue
ret[field_name] = CopyAPIView._decrypt_model_field_if_needed(
obj, field_name, getattr(obj, field_name)
)
ret[field_name] = CopyAPIView._decrypt_model_field_if_needed(obj, field_name, getattr(obj, field_name))
return ret
@staticmethod
@@ -908,9 +885,11 @@ class CopyAPIView(GenericAPIView):
except AttributeError:
continue
# Adjust copy blocked fields here.
if field.name in fields_to_discard or field.name in [
'id', 'pk', 'polymorphic_ctype', 'unifiedjobtemplate_ptr', 'created_by', 'modified_by'
] or field.name.endswith('_role'):
if (
field.name in fields_to_discard
or field.name in ['id', 'pk', 'polymorphic_ctype', 'unifiedjobtemplate_ptr', 'created_by', 'modified_by']
or field.name.endswith('_role')
):
create_kwargs.pop(field.name, None)
continue
if field.one_to_many:
@@ -926,33 +905,24 @@ class CopyAPIView(GenericAPIView):
elif field.name == 'name' and not old_parent:
create_kwargs[field.name] = copy_name or field_val + ' copy'
elif field.name in fields_to_preserve:
create_kwargs[field.name] = CopyAPIView._decrypt_model_field_if_needed(
obj, field.name, field_val
)
create_kwargs[field.name] = CopyAPIView._decrypt_model_field_if_needed(obj, field.name, field_val)
# WorkflowJobTemplateNodes that represent an approval are *special*;
# when we copy them, we actually want to *copy* the UJT they point at
# rather than share the template reference between nodes in disparate
# workflows
if (
isinstance(obj, WorkflowJobTemplateNode) and
isinstance(getattr(obj, 'unified_job_template'), WorkflowApprovalTemplate)
):
new_approval_template, sub_objs = CopyAPIView.copy_model_obj(
None, None, WorkflowApprovalTemplate,
obj.unified_job_template, creater
)
if isinstance(obj, WorkflowJobTemplateNode) and isinstance(getattr(obj, 'unified_job_template'), WorkflowApprovalTemplate):
new_approval_template, sub_objs = CopyAPIView.copy_model_obj(None, None, WorkflowApprovalTemplate, obj.unified_job_template, creater)
create_kwargs['unified_job_template'] = new_approval_template
new_obj = model.objects.create(**create_kwargs)
logger.debug('Deep copy: Created new object {}({})'.format(
new_obj, model
))
logger.debug('Deep copy: Created new object {}({})'.format(new_obj, model))
# Need to save separatedly because Djang-crum get_current_user would
# not work properly in non-request-response-cycle context.
new_obj.created_by = creater
new_obj.save()
from awx.main.signals import disable_activity_stream
with disable_activity_stream():
for m2m in m2m_to_preserve:
for related_obj in m2m_to_preserve[m2m].all():
@@ -978,8 +948,7 @@ class CopyAPIView(GenericAPIView):
for key in create_kwargs:
create_kwargs[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key]
try:
can_copy = request.user.can_access(self.model, 'add', create_kwargs) and \
request.user.can_access(self.model, 'copy_related', obj)
can_copy = request.user.can_access(self.model, 'add', create_kwargs) and request.user.can_access(self.model, 'copy_related', obj)
except PermissionDenied:
return Response({'can_copy': False})
return Response({'can_copy': can_copy})
@@ -998,8 +967,7 @@ class CopyAPIView(GenericAPIView):
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
new_obj, sub_objs = CopyAPIView.copy_model_obj(
None, None, self.model, obj, request.user, create_kwargs=create_kwargs,
copy_name=serializer.validated_data.get('name', '')
None, None, self.model, obj, request.user, create_kwargs=create_kwargs, copy_name=serializer.validated_data.get('name', '')
)
if hasattr(new_obj, 'admin_role') and request.user not in new_obj.admin_role.members.all():
new_obj.admin_role.members.add(request.user)
@@ -1011,13 +979,9 @@ class CopyAPIView(GenericAPIView):
cache.set(key, sub_objs, timeout=3600)
permission_check_func = None
if hasattr(type(self), 'deep_copy_permission_check_func'):
permission_check_func = (
type(self).__module__, type(self).__name__, 'deep_copy_permission_check_func'
)
permission_check_func = (type(self).__module__, type(self).__name__, 'deep_copy_permission_check_func')
trigger_delayed_deep_copy(
self.model.__module__, self.model.__name__,
obj.pk, new_obj.pk, request.user.pk, key,
permission_check_func=permission_check_func
self.model.__module__, self.model.__name__, obj.pk, new_obj.pk, request.user.pk, key, permission_check_func=permission_check_func
)
serializer = self._get_copy_return_serializer(new_obj)
headers = {'Location': new_obj.get_absolute_url(request=request)}
@@ -1026,7 +990,7 @@ class CopyAPIView(GenericAPIView):
class BaseUsersList(SubListCreateAttachDetachAPIView):
def post(self, request, *args, **kwargs):
ret = super(BaseUsersList, self).post( request, *args, **kwargs)
ret = super(BaseUsersList, self).post(request, *args, **kwargs)
if ret.status_code != 201:
return ret
try:

View File

@@ -28,18 +28,23 @@ from awx.main.tasks import AWXReceptorJob
class Metadata(metadata.SimpleMetadata):
def get_field_info(self, field):
field_info = OrderedDict()
field_info['type'] = self.label_lookup[field]
field_info['required'] = getattr(field, 'required', False)
text_attrs = [
'read_only', 'label', 'help_text',
'min_length', 'max_length',
'min_value', 'max_value',
'category', 'category_slug',
'defined_in_file', 'unit',
'read_only',
'label',
'help_text',
'min_length',
'max_length',
'min_value',
'max_value',
'category',
'category_slug',
'defined_in_file',
'unit',
]
for attr in text_attrs:
@@ -61,8 +66,9 @@ class Metadata(metadata.SimpleMetadata):
'type': _('Data type for this {}.'),
'url': _('URL for this {}.'),
'related': _('Data structure with URLs of related resources.'),
'summary_fields': _('Data structure with name/description for related resources. '
'The output for some objects may be limited for performance reasons.'),
'summary_fields': _(
'Data structure with name/description for related resources. ' 'The output for some objects may be limited for performance reasons.'
),
'created': _('Timestamp when this {} was created.'),
'modified': _('Timestamp when this {} was last modified.'),
}
@@ -101,9 +107,7 @@ class Metadata(metadata.SimpleMetadata):
field_info['children'] = self.get_serializer_info(field)
if not isinstance(field, (RelatedField, ManyRelatedField)) and hasattr(field, 'choices'):
choices = [
(choice_value, choice_name) for choice_value, choice_name in field.choices.items()
]
choices = [(choice_value, choice_name) for choice_value, choice_name in field.choices.items()]
if not any(choice in ('', None) for choice, _ in choices):
if field.allow_blank:
choices = [("", "---------")] + choices
@@ -131,7 +135,6 @@ class Metadata(metadata.SimpleMetadata):
for (notification_type_name, notification_tr_name, notification_type_class) in NotificationTemplate.NOTIFICATION_TYPES:
field_info[notification_type_name] = notification_type_class.default_messages
# Update type of fields returned...
model_field = None
if serializer and hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'model'):
@@ -149,22 +152,19 @@ class Metadata(metadata.SimpleMetadata):
field_info['type'] = 'integer'
elif field.field_name in ('created', 'modified'):
field_info['type'] = 'datetime'
elif (
RelatedField in field.__class__.__bases__ or
isinstance(model_field, ForeignKey)
):
elif RelatedField in field.__class__.__bases__ or isinstance(model_field, ForeignKey):
field_info['type'] = 'id'
elif (
isinstance(field, JSONField) or
isinstance(model_field, JSONField) or
isinstance(field, DRFJSONField) or
isinstance(getattr(field, 'model_field', None), JSONField) or
field.field_name == 'credential_passwords'
isinstance(field, JSONField)
or isinstance(model_field, JSONField)
or isinstance(field, DRFJSONField)
or isinstance(getattr(field, 'model_field', None), JSONField)
or field.field_name == 'credential_passwords'
):
field_info['type'] = 'json'
elif (
isinstance(field, ManyRelatedField) and
field.field_name == 'credentials'
isinstance(field, ManyRelatedField)
and field.field_name == 'credentials'
# launch-time credentials
):
field_info['type'] = 'list_of_ids'
@@ -175,10 +175,7 @@ class Metadata(metadata.SimpleMetadata):
def get_serializer_info(self, serializer, method=None):
filterer = getattr(serializer, 'filter_field_metadata', lambda fields, method: fields)
return filterer(
super(Metadata, self).get_serializer_info(serializer),
method
)
return filterer(super(Metadata, self).get_serializer_info(serializer), method)
def determine_actions(self, request, view):
# Add field information for GET requests (so field names/labels are
@@ -274,6 +271,7 @@ class Metadata(metadata.SimpleMetadata):
metadata['object_roles'] = roles
from rest_framework import generics
if isinstance(view, generics.ListAPIView) and hasattr(view, 'paginator'):
metadata['max_page_size'] = view.paginator.max_page_size
@@ -293,7 +291,6 @@ class RoleMetadata(Metadata):
class SublistAttachDetatchMetadata(Metadata):
def determine_actions(self, request, view):
actions = super(SublistAttachDetatchMetadata, self).determine_actions(request, view)
method = 'POST'

View File

@@ -3,13 +3,9 @@
from django.conf.urls import url
from awx.api.views import (
MetricsView
)
from awx.api.views import MetricsView
urls = [
url(r'^$', MetricsView.as_view(), name='metrics_view'),
]
urls = [url(r'^$', MetricsView.as_view(), name='metrics_view')]
__all__ = ['urls']

View File

@@ -10,7 +10,6 @@ from rest_framework.utils.urls import replace_query_param
class DisabledPaginator(DjangoPaginator):
@property
def num_pages(self):
return 1
@@ -49,8 +48,7 @@ class Pagination(pagination.PageNumberPagination):
def get_html_context(self):
context = super().get_html_context()
context['page_links'] = [pl._replace(url=self.cap_page_size(pl.url))
for pl in context['page_links']]
context['page_links'] = [pl._replace(url=self.cap_page_size(pl.url)) for pl in context['page_links']]
return context

View File

@@ -15,16 +15,25 @@ from awx.main.utils import get_object_or_400
logger = logging.getLogger('awx.api.permissions')
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', 'VariableDataPermission',
'TaskPermission', 'ProjectUpdatePermission', 'InventoryInventorySourcesUpdatePermission',
'UserPermission', 'IsSuperUser', 'InstanceGroupTowerPermission', 'WorkflowApprovalPermission']
__all__ = [
'ModelAccessPermission',
'JobTemplateCallbackPermission',
'VariableDataPermission',
'TaskPermission',
'ProjectUpdatePermission',
'InventoryInventorySourcesUpdatePermission',
'UserPermission',
'IsSuperUser',
'InstanceGroupTowerPermission',
'WorkflowApprovalPermission',
]
class ModelAccessPermission(permissions.BasePermission):
'''
"""
Default permissions class to check user access based on the model and
request method, optionally verifying the request data.
'''
"""
def check_options_permissions(self, request, view, obj=None):
return self.check_get_permissions(request, view, obj)
@@ -35,8 +44,7 @@ class ModelAccessPermission(permissions.BasePermission):
def check_get_permissions(self, request, view, obj=None):
if hasattr(view, 'parent_model'):
parent_obj = view.get_parent_object()
if not check_user_access(request.user, view.parent_model, 'read',
parent_obj):
if not check_user_access(request.user, view.parent_model, 'read', parent_obj):
return False
if not obj:
return True
@@ -45,8 +53,7 @@ class ModelAccessPermission(permissions.BasePermission):
def check_post_permissions(self, request, view, obj=None):
if hasattr(view, 'parent_model'):
parent_obj = view.get_parent_object()
if not check_user_access(request.user, view.parent_model, 'read',
parent_obj):
if not check_user_access(request.user, view.parent_model, 'read', parent_obj):
return False
if hasattr(view, 'parent_key'):
if not check_user_access(request.user, view.model, 'add', {view.parent_key: parent_obj}):
@@ -60,10 +67,7 @@ class ModelAccessPermission(permissions.BasePermission):
extra_kwargs = {}
if view.obj_permission_type == 'admin':
extra_kwargs['data'] = {}
return check_user_access(
request.user, view.model, view.obj_permission_type, obj,
**extra_kwargs
)
return check_user_access(request.user, view.model, view.obj_permission_type, obj, **extra_kwargs)
else:
if obj:
return True
@@ -74,8 +78,7 @@ class ModelAccessPermission(permissions.BasePermission):
# FIXME: For some reason this needs to return True
# because it is first called with obj=None?
return True
return check_user_access(request.user, view.model, 'change', obj,
request.data)
return check_user_access(request.user, view.model, 'change', obj, request.data)
def check_patch_permissions(self, request, view, obj=None):
return self.check_put_permissions(request, view, obj)
@@ -89,10 +92,10 @@ class ModelAccessPermission(permissions.BasePermission):
return check_user_access(request.user, view.model, 'delete', obj)
def check_permissions(self, request, view, obj=None):
'''
"""
Perform basic permissions checking before delegating to the appropriate
method based on the request method.
'''
"""
# Don't allow anonymous users. 401, not 403, hence no raised exception.
if not request.user or request.user.is_anonymous:
@@ -117,9 +120,7 @@ class ModelAccessPermission(permissions.BasePermission):
return result
def has_permission(self, request, view, obj=None):
logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)',
request.user, request.method, request.data,
view.__class__.__name__, obj)
logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)', request.user, request.method, request.data, view.__class__.__name__, obj)
try:
response = self.check_permissions(request, view, obj)
except Exception as e:
@@ -134,10 +135,10 @@ class ModelAccessPermission(permissions.BasePermission):
class JobTemplateCallbackPermission(ModelAccessPermission):
'''
"""
Permission check used by job template callback view for requests from
empheral hosts.
'''
"""
def has_permission(self, request, view, obj=None):
# If another authentication method was used and it's not a POST, return
@@ -160,18 +161,16 @@ class JobTemplateCallbackPermission(ModelAccessPermission):
class VariableDataPermission(ModelAccessPermission):
def check_put_permissions(self, request, view, obj=None):
if not obj:
return True
return check_user_access(request.user, view.model, 'change', obj,
dict(variables=request.data))
return check_user_access(request.user, view.model, 'change', obj, dict(variables=request.data))
class TaskPermission(ModelAccessPermission):
'''
"""
Permission checks used for API callbacks from running a task.
'''
"""
def has_permission(self, request, view, obj=None):
# If another authentication method was used other than the one for
@@ -182,8 +181,7 @@ class TaskPermission(ModelAccessPermission):
# Verify that the ID present in the auth token is for a valid, active
# unified job.
try:
unified_job = UnifiedJob.objects.get(status='running',
pk=int(request.auth.split('-')[0]))
unified_job = UnifiedJob.objects.get(status='running', pk=int(request.auth.split('-')[0]))
except (UnifiedJob.DoesNotExist, TypeError):
return False
@@ -197,10 +195,10 @@ class TaskPermission(ModelAccessPermission):
class WorkflowApprovalPermission(ModelAccessPermission):
'''
"""
Permission check used by workflow `approval` and `deny` views to determine
who has access to approve and deny paused workflow nodes
'''
"""
def check_post_permissions(self, request, view, obj=None):
approval = get_object_or_400(view.model, pk=view.kwargs['pk'])
@@ -208,9 +206,10 @@ class WorkflowApprovalPermission(ModelAccessPermission):
class ProjectUpdatePermission(ModelAccessPermission):
'''
"""
Permission check used by ProjectUpdateView to determine who can update projects
'''
"""
def check_get_permissions(self, request, view, obj=None):
project = get_object_or_400(view.model, pk=view.kwargs['pk'])
return check_user_access(request.user, view.model, 'read', project)

View File

@@ -11,7 +11,6 @@ from rest_framework.utils import encoders
class SurrogateEncoder(encoders.JSONEncoder):
def encode(self, obj):
ret = super(SurrogateEncoder, self).encode(obj)
try:
@@ -28,9 +27,9 @@ class DefaultJSONRenderer(renderers.JSONRenderer):
class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
'''
"""
Customizations to the default browsable API renderer.
'''
"""
def get_default_renderer(self, view):
renderer = super(BrowsableAPIRenderer, self).get_default_renderer(view)
@@ -48,9 +47,7 @@ class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
# see: https://github.com/ansible/awx/issues/3108
# https://code.djangoproject.com/ticket/28121
return data
return super(BrowsableAPIRenderer, self).get_content(renderer, data,
accepted_media_type,
renderer_context)
return super(BrowsableAPIRenderer, self).get_content(renderer, data, accepted_media_type, renderer_context)
def get_context(self, data, accepted_media_type, renderer_context):
# Store the associated response status to know how to populate the raw
@@ -125,18 +122,13 @@ class AnsiDownloadRenderer(PlainTextRenderer):
class PrometheusJSONRenderer(renderers.JSONRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
if isinstance(data, dict):
# HTTP errors are {'detail': ErrorDetail(string='...', code=...)}
return super(PrometheusJSONRenderer, self).render(
data, accepted_media_type, renderer_context
)
return super(PrometheusJSONRenderer, self).render(data, accepted_media_type, renderer_context)
parsed_metrics = text_string_to_metric_families(data)
data = {}
for family in parsed_metrics:
for sample in family.samples:
data[sample[0]] = {"labels": sample[1], "value": sample[2]}
return super(PrometheusJSONRenderer, self).render(
data, accepted_media_type, renderer_context
)
return super(PrometheusJSONRenderer, self).render(data, accepted_media_type, renderer_context)

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,6 @@ from rest_framework_swagger import renderers
class SuperUserSchemaGenerator(SchemaGenerator):
def has_view_permissions(self, path, method, view):
#
# Generate the Swagger schema as if you were a superuser and
@@ -25,17 +24,17 @@ class SuperUserSchemaGenerator(SchemaGenerator):
class AutoSchema(DRFAuthSchema):
def get_link(self, path, method, base_url):
link = super(AutoSchema, self).get_link(path, method, base_url)
try:
serializer = self.view.get_serializer()
except Exception:
serializer = None
warnings.warn('{}.get_serializer() raised an exception during '
'schema generation. Serializer fields will not be '
'generated for {} {}.'
.format(self.view.__class__.__name__, method, path))
warnings.warn(
'{}.get_serializer() raised an exception during '
'schema generation. Serializer fields will not be '
'generated for {} {}.'.format(self.view.__class__.__name__, method, path)
)
link.__dict__['deprecated'] = getattr(self.view, 'deprecated', False)
@@ -43,9 +42,7 @@ class AutoSchema(DRFAuthSchema):
if hasattr(self.view, 'swagger_topic'):
link.__dict__['topic'] = str(self.view.swagger_topic).title()
elif serializer and hasattr(serializer, 'Meta'):
link.__dict__['topic'] = str(
serializer.Meta.model._meta.verbose_name_plural
).title()
link.__dict__['topic'] = str(serializer.Meta.model._meta.verbose_name_plural).title()
elif hasattr(self.view, 'model'):
link.__dict__['topic'] = str(self.view.model._meta.verbose_name_plural).title()
else:
@@ -62,18 +59,10 @@ class SwaggerSchemaView(APIView):
_ignore_model_permissions = True
exclude_from_schema = True
permission_classes = [AllowAny]
renderer_classes = [
CoreJSONRenderer,
renderers.OpenAPIRenderer,
renderers.SwaggerUIRenderer
]
renderer_classes = [CoreJSONRenderer, renderers.OpenAPIRenderer, renderers.SwaggerUIRenderer]
def get(self, request):
generator = SuperUserSchemaGenerator(
title='Ansible Tower API',
patterns=None,
urlconf=None
)
generator = SuperUserSchemaGenerator(title='Ansible Tower API', patterns=None, urlconf=None)
schema = generator.get_schema(request=request)
# python core-api doesn't support the deprecation yet, so track it
# ourselves and return it in a response header
@@ -103,11 +92,6 @@ class SwaggerSchemaView(APIView):
schema._data[topic]._data[path] = node
if not schema:
raise exceptions.ValidationError(
'The schema generator did not return a schema Document'
)
raise exceptions.ValidationError('The schema generator did not return a schema Document')
return Response(
schema,
headers={'X-Deprecated-Paths': json.dumps(_deprecated)}
)
return Response(schema, headers={'X-Deprecated-Paths': json.dumps(_deprecated)})

View File

@@ -3,10 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
ActivityStreamList,
ActivityStreamDetail,
)
from awx.api.views import ActivityStreamList, ActivityStreamDetail
urls = [

View File

@@ -3,10 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
AdHocCommandEventList,
AdHocCommandEventDetail,
)
from awx.api.views import AdHocCommandEventList, AdHocCommandEventDetail
urls = [

View File

@@ -3,10 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
CredentialInputSourceDetail,
CredentialInputSourceList,
)
from awx.api.views import CredentialInputSourceDetail, CredentialInputSourceList
urls = [

View File

@@ -3,13 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
CredentialTypeList,
CredentialTypeDetail,
CredentialTypeCredentialList,
CredentialTypeActivityStreamList,
CredentialTypeExternalTest,
)
from awx.api.views import CredentialTypeList, CredentialTypeDetail, CredentialTypeCredentialList, CredentialTypeActivityStreamList, CredentialTypeExternalTest
urls = [

View File

@@ -3,20 +3,14 @@
from django.conf.urls import url
from awx.api.views import (
InstanceList,
InstanceDetail,
InstanceUnifiedJobsList,
InstanceInstanceGroupsList,
)
from awx.api.views import InstanceList, InstanceDetail, InstanceUnifiedJobsList, InstanceInstanceGroupsList
urls = [
url(r'^$', InstanceList.as_view(), name='instance_list'),
url(r'^(?P<pk>[0-9]+)/$', InstanceDetail.as_view(), name='instance_detail'),
url(r'^(?P<pk>[0-9]+)/jobs/$', InstanceUnifiedJobsList.as_view(), name='instance_unified_jobs_list'),
url(r'^(?P<pk>[0-9]+)/instance_groups/$', InstanceInstanceGroupsList.as_view(),
name='instance_instance_groups_list'),
url(r'^(?P<pk>[0-9]+)/instance_groups/$', InstanceInstanceGroupsList.as_view(), name='instance_instance_groups_list'),
]
__all__ = ['urls']

View File

@@ -3,12 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
InstanceGroupList,
InstanceGroupDetail,
InstanceGroupUnifiedJobsList,
InstanceGroupInstanceList,
)
from awx.api.views import InstanceGroupList, InstanceGroupDetail, InstanceGroupUnifiedJobsList, InstanceGroupInstanceList
urls = [

View File

@@ -3,12 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
InventoryScriptList,
InventoryScriptDetail,
InventoryScriptObjectRolesList,
InventoryScriptCopy,
)
from awx.api.views import InventoryScriptList, InventoryScriptDetail, InventoryScriptObjectRolesList, InventoryScriptCopy
urls = [

View File

@@ -29,12 +29,21 @@ urls = [
url(r'^(?P<pk>[0-9]+)/credentials/$', InventorySourceCredentialsList.as_view(), name='inventory_source_credentials_list'),
url(r'^(?P<pk>[0-9]+)/groups/$', InventorySourceGroupsList.as_view(), name='inventory_source_groups_list'),
url(r'^(?P<pk>[0-9]+)/hosts/$', InventorySourceHostsList.as_view(), name='inventory_source_hosts_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', InventorySourceNotificationTemplatesStartedList.as_view(),
name='inventory_source_notification_templates_started_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', InventorySourceNotificationTemplatesErrorList.as_view(),
name='inventory_source_notification_templates_error_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', InventorySourceNotificationTemplatesSuccessList.as_view(),
name='inventory_source_notification_templates_success_list'),
url(
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
InventorySourceNotificationTemplatesStartedList.as_view(),
name='inventory_source_notification_templates_started_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
InventorySourceNotificationTemplatesErrorList.as_view(),
name='inventory_source_notification_templates_error_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
InventorySourceNotificationTemplatesSuccessList.as_view(),
name='inventory_source_notification_templates_success_list',
),
]
__all__ = ['urls']

View File

@@ -3,12 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
JobEventList,
JobEventDetail,
JobEventChildrenList,
JobEventHostsList,
)
from awx.api.views import JobEventList, JobEventDetail, JobEventChildrenList, JobEventHostsList
urls = [

View File

@@ -3,13 +3,9 @@
from django.conf.urls import url
from awx.api.views import (
JobHostSummaryDetail,
)
from awx.api.views import JobHostSummaryDetail
urls = [
url(r'^(?P<pk>[0-9]+)/$', JobHostSummaryDetail.as_view(), name='job_host_summary_detail'),
]
urls = [url(r'^(?P<pk>[0-9]+)/$', JobHostSummaryDetail.as_view(), name='job_host_summary_detail')]
__all__ = ['urls']

View File

@@ -34,12 +34,21 @@ urls = [
url(r'^(?P<pk>[0-9]+)/schedules/$', JobTemplateSchedulesList.as_view(), name='job_template_schedules_list'),
url(r'^(?P<pk>[0-9]+)/survey_spec/$', JobTemplateSurveySpec.as_view(), name='job_template_survey_spec'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', JobTemplateActivityStreamList.as_view(), name='job_template_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', JobTemplateNotificationTemplatesStartedList.as_view(),
name='job_template_notification_templates_started_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', JobTemplateNotificationTemplatesErrorList.as_view(),
name='job_template_notification_templates_error_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', JobTemplateNotificationTemplatesSuccessList.as_view(),
name='job_template_notification_templates_success_list'),
url(
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
JobTemplateNotificationTemplatesStartedList.as_view(),
name='job_template_notification_templates_started_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
JobTemplateNotificationTemplatesErrorList.as_view(),
name='job_template_notification_templates_error_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
JobTemplateNotificationTemplatesSuccessList.as_view(),
name='job_template_notification_templates_success_list',
),
url(r'^(?P<pk>[0-9]+)/instance_groups/$', JobTemplateInstanceGroupsList.as_view(), name='job_template_instance_groups_list'),
url(r'^(?P<pk>[0-9]+)/access_list/$', JobTemplateAccessList.as_view(), name='job_template_access_list'),
url(r'^(?P<pk>[0-9]+)/object_roles/$', JobTemplateObjectRolesList.as_view(), name='job_template_object_roles_list'),

View File

@@ -3,15 +3,9 @@
from django.conf.urls import url
from awx.api.views import (
LabelList,
LabelDetail,
)
from awx.api.views import LabelList, LabelDetail
urls = [
url(r'^$', LabelList.as_view(), name='label_list'),
url(r'^(?P<pk>[0-9]+)/$', LabelDetail.as_view(), name='label_detail'),
]
urls = [url(r'^$', LabelList.as_view(), name='label_list'), url(r'^(?P<pk>[0-9]+)/$', LabelDetail.as_view(), name='label_detail')]
__all__ = ['urls']

View File

@@ -3,15 +3,9 @@
from django.conf.urls import url
from awx.api.views import (
NotificationList,
NotificationDetail,
)
from awx.api.views import NotificationList, NotificationDetail
urls = [
url(r'^$', NotificationList.as_view(), name='notification_list'),
url(r'^(?P<pk>[0-9]+)/$', NotificationDetail.as_view(), name='notification_detail'),
]
urls = [url(r'^$', NotificationList.as_view(), name='notification_list'), url(r'^(?P<pk>[0-9]+)/$', NotificationDetail.as_view(), name='notification_detail')]
__all__ = ['urls']

View File

@@ -16,32 +16,12 @@ from awx.api.views import (
urls = [
url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
url(
r'^applications/(?P<pk>[0-9]+)/$',
OAuth2ApplicationDetail.as_view(),
name='o_auth2_application_detail'
),
url(
r'^applications/(?P<pk>[0-9]+)/tokens/$',
ApplicationOAuth2TokenList.as_view(),
name='o_auth2_application_token_list'
),
url(
r'^applications/(?P<pk>[0-9]+)/activity_stream/$',
OAuth2ApplicationActivityStreamList.as_view(),
name='o_auth2_application_activity_stream_list'
),
url(r'^applications/(?P<pk>[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'),
url(r'^applications/(?P<pk>[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='o_auth2_application_token_list'),
url(r'^applications/(?P<pk>[0-9]+)/activity_stream/$', OAuth2ApplicationActivityStreamList.as_view(), name='o_auth2_application_activity_stream_list'),
url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
url(
r'^tokens/(?P<pk>[0-9]+)/$',
OAuth2TokenDetail.as_view(),
name='o_auth2_token_detail'
),
url(
r'^tokens/(?P<pk>[0-9]+)/activity_stream/$',
OAuth2TokenActivityStreamList.as_view(),
name='o_auth2_token_activity_stream_list'
),
url(r'^tokens/(?P<pk>[0-9]+)/$', OAuth2TokenDetail.as_view(), name='o_auth2_token_detail'),
url(r'^tokens/(?P<pk>[0-9]+)/activity_stream/$', OAuth2TokenActivityStreamList.as_view(), name='o_auth2_token_activity_stream_list'),
]
__all__ = ['urls']

View File

@@ -10,13 +10,10 @@ from oauthlib import oauth2
from oauth2_provider import views
from awx.main.models import RefreshToken
from awx.api.views import (
ApiOAuthAuthorizationRootView,
)
from awx.api.views import ApiOAuthAuthorizationRootView
class TokenView(views.TokenView):
def create_token_response(self, request):
# Django OAuth2 Toolkit has a bug whereby refresh tokens are *never*
# properly expired (ugh):
@@ -26,9 +23,7 @@ class TokenView(views.TokenView):
# This code detects and auto-expires them on refresh grant
# requests.
if request.POST.get('grant_type') == 'refresh_token' and 'refresh_token' in request.POST:
refresh_token = RefreshToken.objects.filter(
token=request.POST['refresh_token']
).first()
refresh_token = RefreshToken.objects.filter(token=request.POST['refresh_token']).first()
if refresh_token:
expire_seconds = settings.OAUTH2_PROVIDER.get('REFRESH_TOKEN_EXPIRE_SECONDS', 0)
if refresh_token.created + timedelta(seconds=expire_seconds) < now():

View File

@@ -43,14 +43,26 @@ urls = [
url(r'^(?P<pk>[0-9]+)/credentials/$', OrganizationCredentialList.as_view(), name='organization_credential_list'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', OrganizationActivityStreamList.as_view(), name='organization_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates/$', OrganizationNotificationTemplatesList.as_view(), name='organization_notification_templates_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', OrganizationNotificationTemplatesStartedList.as_view(),
name='organization_notification_templates_started_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', OrganizationNotificationTemplatesErrorList.as_view(),
name='organization_notification_templates_error_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', OrganizationNotificationTemplatesSuccessList.as_view(),
name='organization_notification_templates_success_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_approvals/$', OrganizationNotificationTemplatesApprovalList.as_view(),
name='organization_notification_templates_approvals_list'),
url(
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
OrganizationNotificationTemplatesStartedList.as_view(),
name='organization_notification_templates_started_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
OrganizationNotificationTemplatesErrorList.as_view(),
name='organization_notification_templates_error_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
OrganizationNotificationTemplatesSuccessList.as_view(),
name='organization_notification_templates_success_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_approvals/$',
OrganizationNotificationTemplatesApprovalList.as_view(),
name='organization_notification_templates_approvals_list',
),
url(r'^(?P<pk>[0-9]+)/instance_groups/$', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'),
url(r'^(?P<pk>[0-9]+)/galaxy_credentials/$', OrganizationGalaxyCredentialsList.as_view(), name='organization_galaxy_credentials_list'),
url(r'^(?P<pk>[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'),

View File

@@ -35,10 +35,16 @@ urls = [
url(r'^(?P<pk>[0-9]+)/activity_stream/$', ProjectActivityStreamList.as_view(), name='project_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/schedules/$', ProjectSchedulesList.as_view(), name='project_schedules_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', ProjectNotificationTemplatesErrorList.as_view(), name='project_notification_templates_error_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', ProjectNotificationTemplatesSuccessList.as_view(),
name='project_notification_templates_success_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', ProjectNotificationTemplatesStartedList.as_view(),
name='project_notification_templates_started_list'),
url(
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
ProjectNotificationTemplatesSuccessList.as_view(),
name='project_notification_templates_success_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
ProjectNotificationTemplatesStartedList.as_view(),
name='project_notification_templates_started_list',
),
url(r'^(?P<pk>[0-9]+)/object_roles/$', ProjectObjectRolesList.as_view(), name='project_object_roles_list'),
url(r'^(?P<pk>[0-9]+)/access_list/$', ProjectAccessList.as_view(), name='project_access_list'),
url(r'^(?P<pk>[0-9]+)/copy/$', ProjectCopy.as_view(), name='project_copy'),

View File

@@ -3,14 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
RoleList,
RoleDetail,
RoleUsersList,
RoleTeamsList,
RoleParentsList,
RoleChildrenList,
)
from awx.api.views import RoleList, RoleDetail, RoleUsersList, RoleTeamsList, RoleParentsList, RoleChildrenList
urls = [

View File

@@ -3,12 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
ScheduleList,
ScheduleDetail,
ScheduleUnifiedJobsList,
ScheduleCredentialsList,
)
from awx.api.views import ScheduleList, ScheduleDetail, ScheduleUnifiedJobsList, ScheduleCredentialsList
urls = [

View File

@@ -3,13 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
SystemJobList,
SystemJobDetail,
SystemJobCancel,
SystemJobNotificationsList,
SystemJobEventsList
)
from awx.api.views import SystemJobList, SystemJobDetail, SystemJobCancel, SystemJobNotificationsList, SystemJobEventsList
urls = [

View File

@@ -21,12 +21,21 @@ urls = [
url(r'^(?P<pk>[0-9]+)/launch/$', SystemJobTemplateLaunch.as_view(), name='system_job_template_launch'),
url(r'^(?P<pk>[0-9]+)/jobs/$', SystemJobTemplateJobsList.as_view(), name='system_job_template_jobs_list'),
url(r'^(?P<pk>[0-9]+)/schedules/$', SystemJobTemplateSchedulesList.as_view(), name='system_job_template_schedules_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', SystemJobTemplateNotificationTemplatesStartedList.as_view(),
name='system_job_template_notification_templates_started_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', SystemJobTemplateNotificationTemplatesErrorList.as_view(),
name='system_job_template_notification_templates_error_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', SystemJobTemplateNotificationTemplatesSuccessList.as_view(),
name='system_job_template_notification_templates_success_list'),
url(
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
SystemJobTemplateNotificationTemplatesStartedList.as_view(),
name='system_job_template_notification_templates_started_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
SystemJobTemplateNotificationTemplatesErrorList.as_view(),
name='system_job_template_notification_templates_error_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
SystemJobTemplateNotificationTemplatesSuccessList.as_view(),
name='system_job_template_notification_templates_success_list',
),
]
__all__ = ['urls']

View File

@@ -5,10 +5,7 @@ from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.conf.urls import include, url
from awx.api.generics import (
LoggedLoginView,
LoggedLogoutView,
)
from awx.api.generics import LoggedLoginView, LoggedLogoutView
from awx.api.views import (
ApiRootView,
ApiV2RootView,
@@ -33,9 +30,7 @@ from awx.api.views import (
OAuth2ApplicationDetail,
)
from awx.api.views.metrics import (
MetricsView,
)
from awx.api.views.metrics import MetricsView
from .organization import urls as organization_urls
from .user import urls as user_urls
@@ -146,17 +141,11 @@ app_name = 'api'
urlpatterns = [
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
url(r'^(?P<version>(v2))/', include(v2_urls)),
url(r'^login/$', LoggedLoginView.as_view(
template_name='rest_framework/login.html',
extra_context={'inside_login_context': True}
), name='login'),
url(r'^logout/$', LoggedLogoutView.as_view(
next_page='/api/', redirect_field_name='next'
), name='logout'),
url(r'^login/$', LoggedLoginView.as_view(template_name='rest_framework/login.html', extra_context={'inside_login_context': True}), name='login'),
url(r'^logout/$', LoggedLogoutView.as_view(next_page='/api/', redirect_field_name='next'), name='logout'),
url(r'^o/', include(oauth2_root_urls)),
]
if settings.SETTINGS_MODULE == 'awx.settings.development':
from awx.api.swagger import SwaggerSchemaView
urlpatterns += [
url(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view'),
]
urlpatterns += [url(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view')]

View File

@@ -20,7 +20,7 @@ from awx.api.views import (
UserAuthorizedTokenList,
)
urls = [
urls = [
url(r'^$', UserList.as_view(), name='user_list'),
url(r'^(?P<pk>[0-9]+)/$', UserDetail.as_view(), name='user_detail'),
url(r'^(?P<pk>[0-9]+)/teams/$', UserTeamsList.as_view(), name='user_teams_list'),
@@ -35,7 +35,6 @@ urls = [
url(r'^(?P<pk>[0-9]+)/tokens/$', OAuth2UserTokenList.as_view(), name='o_auth2_token_list'),
url(r'^(?P<pk>[0-9]+)/authorized_tokens/$', UserAuthorizedTokenList.as_view(), name='user_authorized_token_list'),
url(r'^(?P<pk>[0-9]+)/personal_tokens/$', UserPersonalTokenList.as_view(), name='user_personal_token_list'),
]
]
__all__ = ['urls']

View File

@@ -1,10 +1,6 @@
from django.conf.urls import url
from awx.api.views import (
WebhookKeyView,
GithubWebhookReceiver,
GitlabWebhookReceiver,
)
from awx.api.views import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver
urlpatterns = [

View File

@@ -3,12 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
WorkflowApprovalList,
WorkflowApprovalDetail,
WorkflowApprovalApprove,
WorkflowApprovalDeny,
)
from awx.api.views import WorkflowApprovalList, WorkflowApprovalDetail, WorkflowApprovalApprove, WorkflowApprovalDeny
urls = [

View File

@@ -3,10 +3,7 @@
from django.conf.urls import url
from awx.api.views import (
WorkflowApprovalTemplateDetail,
WorkflowApprovalTemplateJobsList,
)
from awx.api.views import WorkflowApprovalTemplateDetail, WorkflowApprovalTemplateJobsList
urls = [

View File

@@ -33,14 +33,26 @@ urls = [
url(r'^(?P<pk>[0-9]+)/survey_spec/$', WorkflowJobTemplateSurveySpec.as_view(), name='workflow_job_template_survey_spec'),
url(r'^(?P<pk>[0-9]+)/workflow_nodes/$', WorkflowJobTemplateWorkflowNodesList.as_view(), name='workflow_job_template_workflow_nodes_list'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', WorkflowJobTemplateActivityStreamList.as_view(), name='workflow_job_template_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', WorkflowJobTemplateNotificationTemplatesStartedList.as_view(),
name='workflow_job_template_notification_templates_started_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', WorkflowJobTemplateNotificationTemplatesErrorList.as_view(),
name='workflow_job_template_notification_templates_error_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', WorkflowJobTemplateNotificationTemplatesSuccessList.as_view(),
name='workflow_job_template_notification_templates_success_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_approvals/$', WorkflowJobTemplateNotificationTemplatesApprovalList.as_view(),
name='workflow_job_template_notification_templates_approvals_list'),
url(
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
WorkflowJobTemplateNotificationTemplatesStartedList.as_view(),
name='workflow_job_template_notification_templates_started_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
WorkflowJobTemplateNotificationTemplatesErrorList.as_view(),
name='workflow_job_template_notification_templates_error_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
WorkflowJobTemplateNotificationTemplatesSuccessList.as_view(),
name='workflow_job_template_notification_templates_success_list',
),
url(
r'^(?P<pk>[0-9]+)/notification_templates_approvals/$',
WorkflowJobTemplateNotificationTemplatesApprovalList.as_view(),
name='workflow_job_template_notification_templates_approvals_list',
),
url(r'^(?P<pk>[0-9]+)/access_list/$', WorkflowJobTemplateAccessList.as_view(), name='workflow_job_template_access_list'),
url(r'^(?P<pk>[0-9]+)/object_roles/$', WorkflowJobTemplateObjectRolesList.as_view(), name='workflow_job_template_object_roles_list'),
url(r'^(?P<pk>[0-9]+)/labels/$', WorkflowJobTemplateLabelList.as_view(), name='workflow_job_template_label_list'),

View File

@@ -40,13 +40,10 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
class URLPathVersioning(BaseVersioning):
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
if request.version is not None:
kwargs = {} if (kwargs is None) else kwargs
kwargs[self.version_param] = request.version
request = None
return super(BaseVersioning, self).reverse(
viewname, args, kwargs, request, format, **extra
)
return super(BaseVersioning, self).reverse(viewname, args, kwargs, request, format, **extra)

File diff suppressed because it is too large Load Diff

View File

@@ -28,14 +28,7 @@ from awx.main.models import (
InventorySource,
CustomInventoryScript,
)
from awx.api.generics import (
ListCreateAPIView,
RetrieveUpdateDestroyAPIView,
SubListAPIView,
SubListAttachDetachAPIView,
ResourceAccessList,
CopyAPIView,
)
from awx.api.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView, SubListAPIView, SubListAttachDetachAPIView, ResourceAccessList, CopyAPIView
from awx.api.serializers import (
InventorySerializer,
@@ -46,10 +39,7 @@ from awx.api.serializers import (
CustomInventoryScriptSerializer,
JobTemplateSerializer,
)
from awx.api.views.mixin import (
RelatedJobsPreventDeleteMixin,
ControlledByScmMixin,
)
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, ControlledByScmMixin
logger = logging.getLogger('awx.api.views.organization')
@@ -101,7 +91,7 @@ class InventoryScriptObjectRolesList(SubListAPIView):
model = Role
serializer_class = RoleSerializer
parent_model = CustomInventoryScript
search_fields = ('role_field', 'content_type__model',)
search_fields = ('role_field', 'content_type__model')
def get_queryset(self):
po = self.get_parent_object()
@@ -134,8 +124,7 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, Retri
# Do not allow changes to an Inventory kind.
if kind is not None and obj.kind != kind:
return Response(dict(error=_('You cannot turn a regular inventory into a "smart" inventory.')),
status=status.HTTP_405_METHOD_NOT_ALLOWED)
return Response(dict(error=_('You cannot turn a regular inventory into a "smart" inventory.')), status=status.HTTP_405_METHOD_NOT_ALLOWED)
return super(InventoryDetail, self).update(request, *args, **kwargs)
def destroy(self, request, *args, **kwargs):
@@ -175,7 +164,7 @@ class InventoryInstanceGroupsList(SubListAttachDetachAPIView):
class InventoryAccessList(ResourceAccessList):
model = User # needs to be User for AccessLists's
model = User # needs to be User for AccessLists's
parent_model = Inventory
@@ -184,7 +173,7 @@ class InventoryObjectRolesList(SubListAPIView):
model = Role
serializer_class = RoleSerializer
parent_model = Inventory
search_fields = ('role_field', 'content_type__model',)
search_fields = ('role_field', 'content_type__model')
def get_queryset(self):
po = self.get_parent_object()

View File

@@ -17,9 +17,7 @@ from rest_framework.exceptions import PermissionDenied
from awx.main.analytics.metrics import metrics
from awx.api import renderers
from awx.api.generics import (
APIView,
)
from awx.api.generics import APIView
logger = logging.getLogger('awx.analytics')
@@ -30,13 +28,10 @@ class MetricsView(APIView):
name = _('Metrics')
swagger_topic = 'Metrics'
renderer_classes = [renderers.PlainTextRenderer,
renderers.PrometheusJSONRenderer,
renderers.BrowsableAPIRenderer,]
renderer_classes = [renderers.PlainTextRenderer, renderers.PrometheusJSONRenderer, renderers.BrowsableAPIRenderer]
def get(self, request):
''' Show Metrics Details '''
if (request.user.is_superuser or request.user.is_system_auditor):
if request.user.is_superuser or request.user.is_system_auditor:
return Response(metrics().decode('UTF-8'))
raise PermissionDenied()

View File

@@ -16,14 +16,8 @@ from rest_framework.response import Response
from rest_framework import status
from awx.main.constants import ACTIVE_STATES
from awx.main.utils import (
get_object_or_400,
parse_yaml_or_json,
)
from awx.main.models.ha import (
Instance,
InstanceGroup,
)
from awx.main.utils import get_object_or_400, parse_yaml_or_json
from awx.main.models.ha import Instance, InstanceGroup
from awx.main.models.organization import Team
from awx.main.models.projects import Project
from awx.main.models.inventory import Inventory
@@ -34,9 +28,10 @@ logger = logging.getLogger('awx.api.views.mixin')
class UnifiedJobDeletionMixin(object):
'''
"""
Special handling when deleting a running unified job object.
'''
"""
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
if not request.user.can_access(self.model, 'delete', obj):
@@ -53,22 +48,21 @@ class UnifiedJobDeletionMixin(object):
# Prohibit deletion if job events are still coming in
if obj.finished and now() < obj.finished + dateutil.relativedelta.relativedelta(minutes=1):
# less than 1 minute has passed since job finished and events are not in
return Response({"error": _("Job has not finished processing events.")},
status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Job has not finished processing events.")}, status=status.HTTP_400_BAD_REQUEST)
else:
# if it has been > 1 minute, events are probably lost
logger.warning('Allowing deletion of {} through the API without all events '
'processed.'.format(obj.log_format))
logger.warning('Allowing deletion of {} through the API without all events ' 'processed.'.format(obj.log_format))
obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class InstanceGroupMembershipMixin(object):
'''
"""
This mixin overloads attach/detach so that it calls InstanceGroup.save(),
triggering a background recalculation of policy-based instance group
membership.
'''
"""
def attach(self, request, *args, **kwargs):
response = super(InstanceGroupMembershipMixin, self).attach(request, *args, **kwargs)
sub_id, res = self.attach_validate(request)
@@ -84,9 +78,7 @@ class InstanceGroupMembershipMixin(object):
ig_obj = get_object_or_400(ig_qs, pk=sub_id)
else:
# similar to get_parent_object, but selected for update
parent_filter = {
self.lookup_field: self.kwargs.get(self.lookup_field, None),
}
parent_filter = {self.lookup_field: self.kwargs.get(self.lookup_field, None)}
ig_obj = get_object_or_404(ig_qs, **parent_filter)
if inst_name not in ig_obj.policy_instance_list:
ig_obj.policy_instance_list.append(inst_name)
@@ -126,9 +118,7 @@ class InstanceGroupMembershipMixin(object):
ig_obj = get_object_or_400(ig_qs, pk=sub_id)
else:
# similar to get_parent_object, but selected for update
parent_filter = {
self.lookup_field: self.kwargs.get(self.lookup_field, None),
}
parent_filter = {self.lookup_field: self.kwargs.get(self.lookup_field, None)}
ig_obj = get_object_or_404(ig_qs, **parent_filter)
if inst_name in ig_obj.policy_instance_list:
ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name))
@@ -146,16 +136,13 @@ class RelatedJobsPreventDeleteMixin(object):
if len(active_jobs) > 0:
raise ActiveJobConflict(active_jobs)
time_cutoff = now() - dateutil.relativedelta.relativedelta(minutes=1)
recent_jobs = obj._get_related_jobs().filter(finished__gte = time_cutoff)
recent_jobs = obj._get_related_jobs().filter(finished__gte=time_cutoff)
for unified_job in recent_jobs.get_real_instances():
if not unified_job.event_processing_finished:
raise PermissionDenied(_(
'Related job {} is still processing events.'
).format(unified_job.log_format))
raise PermissionDenied(_('Related job {} is still processing events.').format(unified_job.log_format))
class OrganizationCountsMixin(object):
def get_serializer_context(self, *args, **kwargs):
full_context = super(OrganizationCountsMixin, self).get_serializer_context(*args, **kwargs)
@@ -177,26 +164,23 @@ class OrganizationCountsMixin(object):
# Produce counts of Foreign Key relationships
db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization')
db_results['teams'] = Team.accessible_objects(
self.request.user, 'read_role').values('organization').annotate(
Count('organization')).order_by('organization')
db_results['teams'] = (
Team.accessible_objects(self.request.user, 'read_role').values('organization').annotate(Count('organization')).order_by('organization')
)
db_results['job_templates'] = jt_qs.values('organization').annotate(Count('organization')).order_by('organization')
db_results['projects'] = project_qs.values('organization').annotate(Count('organization')).order_by('organization')
# Other members and admins of organization are always viewable
db_results['users'] = org_qs.annotate(
users=Count('member_role__members', distinct=True),
admins=Count('admin_role__members', distinct=True)
).values('id', 'users', 'admins')
db_results['users'] = org_qs.annotate(users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True)).values(
'id', 'users', 'admins'
)
count_context = {}
for org in org_id_list:
org_id = org['id']
count_context[org_id] = {
'inventories': 0, 'teams': 0, 'users': 0, 'job_templates': 0,
'admins': 0, 'projects': 0}
count_context[org_id] = {'inventories': 0, 'teams': 0, 'users': 0, 'job_templates': 0, 'admins': 0, 'projects': 0}
for res, count_qs in db_results.items():
if res == 'users':
@@ -218,21 +202,20 @@ class OrganizationCountsMixin(object):
class ControlledByScmMixin(object):
'''
"""
Special method to reset SCM inventory commit hash
if anything that it manages changes.
'''
"""
def _reset_inv_src_rev(self, obj):
if self.request.method in SAFE_METHODS or not obj:
return
project_following_sources = obj.inventory_sources.filter(
update_on_project_update=True, source='scm')
project_following_sources = obj.inventory_sources.filter(update_on_project_update=True, source='scm')
if project_following_sources:
# Allow inventory changes unrelated to variables
if self.model == Inventory and (
not self.request or not self.request.data or
parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables)):
not self.request or not self.request.data or parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables)
):
return
project_following_sources.update(scm_last_revision='')

View File

@@ -24,7 +24,7 @@ from awx.main.models import (
User,
Team,
InstanceGroup,
Credential
Credential,
)
from awx.api.generics import (
ListCreateAPIView,
@@ -47,13 +47,12 @@ from awx.api.serializers import (
NotificationTemplateSerializer,
InstanceGroupSerializer,
ExecutionEnvironmentSerializer,
ProjectSerializer, JobTemplateSerializer, WorkflowJobTemplateSerializer,
CredentialSerializer
)
from awx.api.views.mixin import (
RelatedJobsPreventDeleteMixin,
OrganizationCountsMixin,
ProjectSerializer,
JobTemplateSerializer,
WorkflowJobTemplateSerializer,
CredentialSerializer,
)
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, OrganizationCountsMixin
logger = logging.getLogger('awx.api.views.organization')
@@ -84,23 +83,20 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
org_counts = {}
access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'}
direct_counts = Organization.objects.filter(id=org_id).annotate(
users=Count('member_role__members', distinct=True),
admins=Count('admin_role__members', distinct=True)
).values('users', 'admins')
direct_counts = (
Organization.objects.filter(id=org_id)
.annotate(users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True))
.values('users', 'admins')
)
if not direct_counts:
return full_context
org_counts = direct_counts[0]
org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count()
org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count()
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count()
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count()
org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
org_counts['hosts'] = Host.objects.org_active_count(org_id)
full_context['related_field_counts'] = {}
@@ -240,14 +236,12 @@ class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView):
def is_valid_relation(self, parent, sub, created=False):
if sub.kind != 'galaxy_api_token':
return {'msg': _(
f"Credential must be a Galaxy credential, not {sub.credential_type.name}."
)}
return {'msg': _(f"Credential must be a Galaxy credential, not {sub.credential_type.name}.")}
class OrganizationAccessList(ResourceAccessList):
model = User # needs to be User for AccessLists's
model = User # needs to be User for AccessLists's
parent_model = Organization
@@ -256,7 +250,7 @@ class OrganizationObjectRolesList(SubListAPIView):
model = Role
serializer_class = RoleSerializer
parent_model = Organization
search_fields = ('role_field', 'content_type__model',)
search_fields = ('role_field', 'content_type__model')
def get_queryset(self):
po = self.get_parent_object()

View File

@@ -24,22 +24,11 @@ from awx.api.generics import APIView
from awx.conf.registry import settings_registry
from awx.main.analytics import all_collectors
from awx.main.ha import is_ha_environment
from awx.main.utils import (
get_awx_version,
get_ansible_version,
get_custom_venv_choices,
to_python_boolean,
)
from awx.main.utils import get_awx_version, get_ansible_version, get_custom_venv_choices, to_python_boolean
from awx.main.utils.licensing import validate_entitlement_manifest
from awx.api.versioning import reverse, drf_reverse
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
from awx.main.models import (
Project,
Organization,
Instance,
InstanceGroup,
JobTemplate,
)
from awx.main.models import Project, Organization, Instance, InstanceGroup, JobTemplate
from awx.main.utils import set_environ
logger = logging.getLogger('awx.api.views.root')
@@ -60,7 +49,7 @@ class ApiRootView(APIView):
data = OrderedDict()
data['description'] = _('AWX REST API')
data['current_version'] = v2
data['available_versions'] = dict(v2 = v2)
data['available_versions'] = dict(v2=v2)
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
data['custom_logo'] = settings.CUSTOM_LOGO
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
@@ -146,6 +135,7 @@ class ApiV2PingView(APIView):
"""A simple view that reports very basic information about this
instance, which is acceptable to be public information.
"""
permission_classes = (AllowAny,)
authentication_classes = ()
name = _('Ping')
@@ -157,23 +147,19 @@ class ApiV2PingView(APIView):
Everything returned here should be considered public / insecure, as
this requires no auth and is intended for use by the installer process.
"""
response = {
'ha': is_ha_environment(),
'version': get_awx_version(),
'active_node': settings.CLUSTER_HOST_ID,
'install_uuid': settings.INSTALL_UUID,
}
response = {'ha': is_ha_environment(), 'version': get_awx_version(), 'active_node': settings.CLUSTER_HOST_ID, 'install_uuid': settings.INSTALL_UUID}
response['instances'] = []
for instance in Instance.objects.all():
response['instances'].append(dict(node=instance.hostname, uuid=instance.uuid, heartbeat=instance.modified,
capacity=instance.capacity, version=instance.version))
response['instances'].append(
dict(node=instance.hostname, uuid=instance.uuid, heartbeat=instance.modified, capacity=instance.capacity, version=instance.version)
)
sorted(response['instances'], key=operator.itemgetter('node'))
response['instance_groups'] = []
for instance_group in InstanceGroup.objects.prefetch_related('instances'):
response['instance_groups'].append(dict(name=instance_group.name,
capacity=instance_group.capacity,
instances=[x.hostname for x in instance_group.instances.all()]))
response['instance_groups'].append(
dict(name=instance_group.name, capacity=instance_group.capacity, instances=[x.hostname for x in instance_group.instances.all()])
)
return Response(response)
@@ -190,6 +176,7 @@ class ApiV2SubscriptionView(APIView):
def post(self, request):
from awx.main.utils.common import get_licenser
data = request.data.copy()
if data.get('subscriptions_password') == '$encrypted$':
data['subscriptions_password'] = settings.SUBSCRIPTIONS_PASSWORD
@@ -203,10 +190,7 @@ class ApiV2SubscriptionView(APIView):
settings.SUBSCRIPTIONS_PASSWORD = data['subscriptions_password']
except Exception as exc:
msg = _("Invalid Subscription")
if (
isinstance(exc, requests.exceptions.HTTPError) and
getattr(getattr(exc, 'response', None), 'status_code', None) == 401
):
if isinstance(exc, requests.exceptions.HTTPError) and getattr(getattr(exc, 'response', None), 'status_code', None) == 401:
msg = _("The provided credentials are invalid (HTTP 401).")
elif isinstance(exc, requests.exceptions.ProxyError):
msg = _("Unable to connect to proxy server.")
@@ -215,8 +199,7 @@ class ApiV2SubscriptionView(APIView):
elif isinstance(exc, (ValueError, OSError)) and exc.args:
msg = exc.args[0]
else:
logger.exception(smart_text(u"Invalid subscription submitted."),
extra=dict(actor=request.user.username))
logger.exception(smart_text(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
return Response(validated)
@@ -242,16 +225,14 @@ class ApiV2AttachView(APIView):
pw = getattr(settings, 'SUBSCRIPTIONS_PASSWORD', None)
if pool_id and user and pw:
from awx.main.utils.common import get_licenser
data = request.data.copy()
try:
with set_environ(**settings.AWX_TASK_ENV):
validated = get_licenser().validate_rh(user, pw)
except Exception as exc:
msg = _("Invalid Subscription")
if (
isinstance(exc, requests.exceptions.HTTPError) and
getattr(getattr(exc, 'response', None), 'status_code', None) == 401
):
if isinstance(exc, requests.exceptions.HTTPError) and getattr(getattr(exc, 'response', None), 'status_code', None) == 401:
msg = _("The provided credentials are invalid (HTTP 401).")
elif isinstance(exc, requests.exceptions.ProxyError):
msg = _("Unable to connect to proxy server.")
@@ -260,8 +241,7 @@ class ApiV2AttachView(APIView):
elif isinstance(exc, (ValueError, OSError)) and exc.args:
msg = exc.args[0]
else:
logger.exception(smart_text(u"Invalid subscription submitted."),
extra=dict(actor=request.user.username))
logger.exception(smart_text(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
for sub in validated:
if sub['pool_id'] == pool_id:
@@ -287,6 +267,7 @@ class ApiV2ConfigView(APIView):
'''Return various sitewide configuration settings'''
from awx.main.utils.common import get_licenser
license_data = get_licenser().validate()
if not license_data.get('valid_key', False):
@@ -314,22 +295,23 @@ class ApiV2ConfigView(APIView):
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
data['user_ldap_fields'] = user_ldap_fields
if request.user.is_superuser \
or request.user.is_system_auditor \
or Organization.accessible_objects(request.user, 'admin_role').exists() \
or Organization.accessible_objects(request.user, 'auditor_role').exists() \
or Organization.accessible_objects(request.user, 'project_admin_role').exists():
data.update(dict(
project_base_dir = settings.PROJECTS_ROOT,
project_local_paths = Project.get_local_path_choices(),
custom_virtualenvs = get_custom_venv_choices()
))
if (
request.user.is_superuser
or request.user.is_system_auditor
or Organization.accessible_objects(request.user, 'admin_role').exists()
or Organization.accessible_objects(request.user, 'auditor_role').exists()
or Organization.accessible_objects(request.user, 'project_admin_role').exists()
):
data.update(
dict(
project_base_dir=settings.PROJECTS_ROOT, project_local_paths=Project.get_local_path_choices(), custom_virtualenvs=get_custom_venv_choices()
)
)
elif JobTemplate.accessible_objects(request.user, 'admin_role').exists():
data['custom_virtualenvs'] = get_custom_venv_choices()
return Response(data)
def post(self, request):
if not isinstance(request.data, dict):
return Response({"error": _("Invalid subscription data")}, status=status.HTTP_400_BAD_REQUEST)
@@ -346,11 +328,11 @@ class ApiV2ConfigView(APIView):
try:
data_actual = json.dumps(request.data)
except Exception:
logger.info(smart_text(u"Invalid JSON submitted for license."),
extra=dict(actor=request.user.username))
logger.info(smart_text(u"Invalid JSON submitted for license."), extra=dict(actor=request.user.username))
return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST)
from awx.main.utils.common import get_licenser
license_data = json.loads(data_actual)
if 'license_key' in license_data:
return Response({"error": _('Legacy license submitted. A subscription manifest is now required.')}, status=status.HTTP_400_BAD_REQUEST)
@@ -358,10 +340,7 @@ class ApiV2ConfigView(APIView):
try:
json_actual = json.loads(base64.b64decode(license_data['manifest']))
if 'license_key' in json_actual:
return Response(
{"error": _('Legacy license submitted. A subscription manifest is now required.')},
status=status.HTTP_400_BAD_REQUEST
)
return Response({"error": _('Legacy license submitted. A subscription manifest is now required.')}, status=status.HTTP_400_BAD_REQUEST)
except Exception:
pass
try:
@@ -375,8 +354,7 @@ class ApiV2ConfigView(APIView):
try:
license_data_validated = get_licenser().license_from_manifest(license_data)
except Exception:
logger.warning(smart_text(u"Invalid subscription submitted."),
extra=dict(actor=request.user.username))
logger.warning(smart_text(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
else:
license_data_validated = get_licenser().validate()
@@ -387,8 +365,7 @@ class ApiV2ConfigView(APIView):
settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host())
return Response(license_data_validated)
logger.warning(smart_text(u"Invalid subscription submitted."),
extra=dict(actor=request.user.username))
logger.warning(smart_text(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": _("Invalid subscription")}, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request):

View File

@@ -26,10 +26,7 @@ class WebhookKeyView(GenericAPIView):
permission_classes = (WebhookKeyPermission,)
def get_queryset(self):
qs_models = {
'job_templates': JobTemplate,
'workflow_job_templates': WorkflowJobTemplate,
}
qs_models = {'job_templates': JobTemplate, 'workflow_job_templates': WorkflowJobTemplate}
self.model = qs_models.get(self.kwargs['model_kwarg'])
return super().get_queryset()
@@ -57,10 +54,7 @@ class WebhookReceiverBase(APIView):
ref_keys = {}
def get_queryset(self):
qs_models = {
'job_templates': JobTemplate,
'workflow_job_templates': WorkflowJobTemplate,
}
qs_models = {'job_templates': JobTemplate, 'workflow_job_templates': WorkflowJobTemplate}
model = qs_models.get(self.kwargs['model_kwarg'])
if model is None:
raise PermissionDenied
@@ -120,10 +114,7 @@ class WebhookReceiverBase(APIView):
# Ensure that the full contents of the request are captured for multiple uses.
request.body
logger.debug(
"headers: {}\n"
"data: {}\n".format(request.headers, request.data)
)
logger.debug("headers: {}\n" "data: {}\n".format(request.headers, request.data))
obj = self.get_object()
self.check_signature(obj)
@@ -132,16 +123,11 @@ class WebhookReceiverBase(APIView):
event_ref = self.get_event_ref()
status_api = self.get_event_status_api()
kwargs = {
'unified_job_template_id': obj.id,
'webhook_service': obj.webhook_service,
'webhook_guid': event_guid,
}
kwargs = {'unified_job_template_id': obj.id, 'webhook_service': obj.webhook_service, 'webhook_guid': event_guid}
if WorkflowJob.objects.filter(**kwargs).exists() or Job.objects.filter(**kwargs).exists():
# Short circuit if this webhook has already been received and acted upon.
logger.debug("Webhook previously received, returning without action.")
return Response({'message': _("Webhook previously received, aborting.")},
status=status.HTTP_202_ACCEPTED)
return Response({'message': _("Webhook previously received, aborting.")}, status=status.HTTP_202_ACCEPTED)
kwargs = {
'_eager_fields': {
@@ -156,7 +142,7 @@ class WebhookReceiverBase(APIView):
'tower_webhook_event_ref': event_ref,
'tower_webhook_status_api': status_api,
'tower_webhook_payload': request.data,
}
},
}
new_job = obj.create_unified_job(**kwargs)
@@ -205,11 +191,7 @@ class GithubWebhookReceiver(WebhookReceiverBase):
class GitlabWebhookReceiver(WebhookReceiverBase):
service = 'gitlab'
ref_keys = {
'Push Hook': 'checkout_sha',
'Tag Push Hook': 'checkout_sha',
'Merge Request Hook': 'object_attributes.last_commit.id',
}
ref_keys = {'Push Hook': 'checkout_sha', 'Tag Push Hook': 'checkout_sha', 'Merge Request Hook': 'object_attributes.last_commit.id'}
def get_event_type(self):
return self.request.META.get('HTTP_X_GITLAB_EVENT')
@@ -229,8 +211,7 @@ class GitlabWebhookReceiver(WebhookReceiverBase):
return
parsed = urllib.parse.urlparse(repo_url)
return "{}://{}/api/v4/projects/{}/statuses/{}".format(
parsed.scheme, parsed.netloc, project['id'], self.get_event_ref())
return "{}://{}/api/v4/projects/{}/statuses/{}".format(parsed.scheme, parsed.netloc, project['id'], self.get_event_ref())
def get_signature(self):
return force_bytes(self.request.META.get('HTTP_X_GITLAB_TOKEN') or '')