mirror of
https://github.com/ansible/awx.git
synced 2026-04-09 12:09:20 -02:30
move code linting to a stricter pep8-esque auto-formatting tool, black
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)})
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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')]
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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='')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 '')
|
||||
|
||||
Reference in New Issue
Block a user