mirror of
https://github.com/ansible/awx.git
synced 2026-03-07 03:31:10 -03:30
Merge pull request #7836 from ryanpetrello/downstream-merge
merge in a few downstream fixes Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/<version>`.
|
This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/<version>`.
|
||||||
|
|
||||||
|
## 14.0.0 (Aug TBD, 2020)
|
||||||
|
- Fixed https://access.redhat.com/security/cve/cve-2020-14327 - Server-side request forgery on credentials
|
||||||
|
- Fixed https://access.redhat.com/security/cve/cve-2020-14328 - Server-side request forgery on webhooks
|
||||||
|
- Fixed https://access.redhat.com/security/cve/cve-2020-14329 - Sensitive data exposure on labels
|
||||||
|
- Fixed https://access.redhat.com/security/cve/cve-2020-14337 - Named URLs allow for testing the presence or absence of objects
|
||||||
|
|
||||||
## 13.0.0 (Jun 23, 2020)
|
## 13.0.0 (Jun 23, 2020)
|
||||||
- Added import and export commands to the official AWX CLI, replacing send and receive from the old tower-cli (https://github.com/ansible/awx/pull/6125).
|
- Added import and export commands to the official AWX CLI, replacing send and receive from the old tower-cli (https://github.com/ansible/awx/pull/6125).
|
||||||
- Removed scripts as a means of running inventory updates of built-in types (https://github.com/ansible/awx/pull/6911)
|
- Removed scripts as a means of running inventory updates of built-in types (https://github.com/ansible/awx/pull/6911)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from awx.main.utils import (
|
|||||||
StubLicense
|
StubLicense
|
||||||
)
|
)
|
||||||
from awx.main.utils.db import get_all_field_names
|
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.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
||||||
from awx.api.versioning import URLPathVersioning
|
from awx.api.versioning import URLPathVersioning
|
||||||
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
||||||
@@ -188,6 +189,29 @@ class APIView(views.APIView):
|
|||||||
'''
|
'''
|
||||||
Log warning for 400 requests. Add header with elapsed time.
|
Log warning for 400 requests. Add header with elapsed time.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
#
|
||||||
|
# If the URL was rewritten, and we get a 404, we should entirely
|
||||||
|
# replace the view in the request context with an ApiErrorView()
|
||||||
|
# Without this change, there will be subtle differences in the BrowseableAPIRenderer
|
||||||
|
#
|
||||||
|
# These differences could provide contextual clues which would allow
|
||||||
|
# anonymous users to determine if usernames were valid or not
|
||||||
|
# (e.g., if an anonymous user visited `/api/v2/users/valid/`, and got a 404,
|
||||||
|
# but also saw that the page heading said "User Detail", they might notice
|
||||||
|
# that's a difference in behavior from a request to `/api/v2/users/not-valid/`, which
|
||||||
|
# would show a page header of "Not Found"). Changing the view here
|
||||||
|
# guarantees that the rendered response will look exactly like the response
|
||||||
|
# when you visit a URL that has no matching URL paths in `awx.api.urls`.
|
||||||
|
#
|
||||||
|
if response.status_code == 404 and 'awx.named_url_rewritten' in request.environ:
|
||||||
|
self.headers.pop('Allow', None)
|
||||||
|
response = super(APIView, self).finalize_response(request, response, *args, **kwargs)
|
||||||
|
view = ApiErrorView()
|
||||||
|
setattr(view, 'request', request)
|
||||||
|
response.renderer_context['view'] = view
|
||||||
|
return response
|
||||||
|
|
||||||
if response.status_code >= 400:
|
if response.status_code >= 400:
|
||||||
status_msg = "status %s received by user %s attempting to access %s from %s" % \
|
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))
|
(response.status_code, request.user, request.path, request.META.get('REMOTE_ADDR', None))
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import time
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from urllib3.exceptions import ConnectTimeoutError
|
||||||
|
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -171,6 +173,15 @@ def api_exception_handler(exc, context):
|
|||||||
exc = ParseError(exc.args[0])
|
exc = ParseError(exc.args[0])
|
||||||
if isinstance(context['view'], UnifiedJobStdout):
|
if isinstance(context['view'], UnifiedJobStdout):
|
||||||
context['view'].renderer_classes = [renderers.BrowsableAPIRenderer, JSONRenderer]
|
context['view'].renderer_classes = [renderers.BrowsableAPIRenderer, JSONRenderer]
|
||||||
|
if isinstance(exc, APIException):
|
||||||
|
req = context['request']._request
|
||||||
|
if 'awx.named_url_rewritten' in req.environ and not str(getattr(exc, 'status_code', 0)).startswith('2'):
|
||||||
|
# if the URL was rewritten, and it's not a 2xx level status code,
|
||||||
|
# revert the request.path to its original value to avoid leaking
|
||||||
|
# any context about the existance of resources
|
||||||
|
req.path = req.environ['awx.named_url_rewritten']
|
||||||
|
if exc.status_code == 403:
|
||||||
|
exc = NotFound(detail=_('Not found.'))
|
||||||
return exception_handler(exc, context)
|
return exception_handler(exc, context)
|
||||||
|
|
||||||
|
|
||||||
@@ -1397,10 +1408,18 @@ class CredentialExternalTest(SubDetailAPIView):
|
|||||||
obj.credential_type.plugin.backend(**backend_kwargs)
|
obj.credential_type.plugin.backend(**backend_kwargs)
|
||||||
return Response({}, status=status.HTTP_202_ACCEPTED)
|
return Response({}, status=status.HTTP_202_ACCEPTED)
|
||||||
except requests.exceptions.HTTPError as exc:
|
except requests.exceptions.HTTPError as exc:
|
||||||
message = 'HTTP {}\n{}'.format(exc.response.status_code, exc.response.text)
|
message = 'HTTP {}'.format(exc.response.status_code)
|
||||||
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
message = exc.__class__.__name__
|
||||||
|
args = getattr(exc, 'args', [])
|
||||||
|
for a in args:
|
||||||
|
if isinstance(
|
||||||
|
getattr(a, 'reason', None),
|
||||||
|
ConnectTimeoutError
|
||||||
|
):
|
||||||
|
message = str(a.reason)
|
||||||
|
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
|
class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
|
||||||
@@ -1449,10 +1468,18 @@ class CredentialTypeExternalTest(SubDetailAPIView):
|
|||||||
obj.plugin.backend(**backend_kwargs)
|
obj.plugin.backend(**backend_kwargs)
|
||||||
return Response({}, status=status.HTTP_202_ACCEPTED)
|
return Response({}, status=status.HTTP_202_ACCEPTED)
|
||||||
except requests.exceptions.HTTPError as exc:
|
except requests.exceptions.HTTPError as exc:
|
||||||
message = 'HTTP {}\n{}'.format(exc.response.status_code, exc.response.text)
|
message = 'HTTP {}'.format(exc.response.status_code)
|
||||||
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
message = exc.__class__.__name__
|
||||||
|
args = getattr(exc, 'args', [])
|
||||||
|
for a in args:
|
||||||
|
if isinstance(
|
||||||
|
getattr(a, 'reason', None),
|
||||||
|
ConnectTimeoutError
|
||||||
|
):
|
||||||
|
message = str(a.reason)
|
||||||
|
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class HostRelatedSearchMixin(object):
|
class HostRelatedSearchMixin(object):
|
||||||
|
|||||||
@@ -2479,13 +2479,16 @@ class NotificationAccess(BaseAccess):
|
|||||||
|
|
||||||
class LabelAccess(BaseAccess):
|
class LabelAccess(BaseAccess):
|
||||||
'''
|
'''
|
||||||
I can see/use a Label if I have permission to associated organization
|
I can see/use a Label if I have permission to associated organization, or to a JT that the label is on
|
||||||
'''
|
'''
|
||||||
model = Label
|
model = Label
|
||||||
prefetch_related = ('modified_by', 'created_by', 'organization',)
|
prefetch_related = ('modified_by', 'created_by', 'organization',)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.all()
|
return self.model.objects.filter(
|
||||||
|
Q(organization__in=Organization.accessible_pk_qs(self.user, 'read_role')) |
|
||||||
|
Q(unifiedjobtemplate_labels__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
|
||||||
|
)
|
||||||
|
|
||||||
@check_superuser
|
@check_superuser
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .plugin import CredentialPlugin, CertFiles
|
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
||||||
|
|
||||||
from urllib.parse import quote, urlencode, urljoin
|
from urllib.parse import quote, urlencode, urljoin
|
||||||
|
|
||||||
@@ -82,8 +82,9 @@ def aim_backend(**kwargs):
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
cert=cert,
|
cert=cert,
|
||||||
verify=verify,
|
verify=verify,
|
||||||
|
allow_redirects=False,
|
||||||
)
|
)
|
||||||
res.raise_for_status()
|
raise_for_status(res)
|
||||||
return res.json()['Content']
|
return res.json()['Content']
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .plugin import CredentialPlugin, CertFiles
|
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
from urllib.parse import urljoin, quote
|
from urllib.parse import urljoin, quote
|
||||||
@@ -58,7 +58,8 @@ def conjur_backend(**kwargs):
|
|||||||
|
|
||||||
auth_kwargs = {
|
auth_kwargs = {
|
||||||
'headers': {'Content-Type': 'text/plain'},
|
'headers': {'Content-Type': 'text/plain'},
|
||||||
'data': api_key
|
'data': api_key,
|
||||||
|
'allow_redirects': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
@@ -68,11 +69,12 @@ def conjur_backend(**kwargs):
|
|||||||
urljoin(url, '/'.join(['authn', account, username, 'authenticate'])),
|
urljoin(url, '/'.join(['authn', account, username, 'authenticate'])),
|
||||||
**auth_kwargs
|
**auth_kwargs
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
raise_for_status(resp)
|
||||||
token = base64.b64encode(resp.content).decode('utf-8')
|
token = base64.b64encode(resp.content).decode('utf-8')
|
||||||
|
|
||||||
lookup_kwargs = {
|
lookup_kwargs = {
|
||||||
'headers': {'Authorization': 'Token token="{}"'.format(token)},
|
'headers': {'Authorization': 'Token token="{}"'.format(token)},
|
||||||
|
'allow_redirects': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://www.conjur.org/api.html#secrets-retrieve-a-secret-get
|
# https://www.conjur.org/api.html#secrets-retrieve-a-secret-get
|
||||||
@@ -88,7 +90,7 @@ def conjur_backend(**kwargs):
|
|||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
lookup_kwargs['verify'] = cert
|
lookup_kwargs['verify'] = cert
|
||||||
resp = requests.get(path, timeout=30, **lookup_kwargs)
|
resp = requests.get(path, timeout=30, **lookup_kwargs)
|
||||||
resp.raise_for_status()
|
raise_for_status(resp)
|
||||||
return resp.text
|
return resp.text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import pathlib
|
import pathlib
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from .plugin import CredentialPlugin, CertFiles
|
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -145,7 +145,10 @@ def kv_backend(**kwargs):
|
|||||||
cacert = kwargs.get('cacert', None)
|
cacert = kwargs.get('cacert', None)
|
||||||
api_version = kwargs['api_version']
|
api_version = kwargs['api_version']
|
||||||
|
|
||||||
request_kwargs = {'timeout': 30}
|
request_kwargs = {
|
||||||
|
'timeout': 30,
|
||||||
|
'allow_redirects': False,
|
||||||
|
}
|
||||||
|
|
||||||
sess = requests.Session()
|
sess = requests.Session()
|
||||||
sess.headers['Authorization'] = 'Bearer {}'.format(token)
|
sess.headers['Authorization'] = 'Bearer {}'.format(token)
|
||||||
@@ -175,7 +178,7 @@ def kv_backend(**kwargs):
|
|||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
request_kwargs['verify'] = cert
|
request_kwargs['verify'] = cert
|
||||||
response = sess.get(request_url, **request_kwargs)
|
response = sess.get(request_url, **request_kwargs)
|
||||||
response.raise_for_status()
|
raise_for_status(response)
|
||||||
|
|
||||||
json = response.json()
|
json = response.json()
|
||||||
if api_version == 'v2':
|
if api_version == 'v2':
|
||||||
@@ -198,7 +201,10 @@ def ssh_backend(**kwargs):
|
|||||||
role = kwargs['role']
|
role = kwargs['role']
|
||||||
cacert = kwargs.get('cacert', None)
|
cacert = kwargs.get('cacert', None)
|
||||||
|
|
||||||
request_kwargs = {'timeout': 30}
|
request_kwargs = {
|
||||||
|
'timeout': 30,
|
||||||
|
'allow_redirects': False,
|
||||||
|
}
|
||||||
|
|
||||||
request_kwargs['json'] = {'public_key': kwargs['public_key']}
|
request_kwargs['json'] = {'public_key': kwargs['public_key']}
|
||||||
if kwargs.get('valid_principals'):
|
if kwargs.get('valid_principals'):
|
||||||
@@ -215,7 +221,7 @@ def ssh_backend(**kwargs):
|
|||||||
request_kwargs['verify'] = cert
|
request_kwargs['verify'] = cert
|
||||||
resp = sess.post(request_url, **request_kwargs)
|
resp = sess.post(request_url, **request_kwargs)
|
||||||
|
|
||||||
resp.raise_for_status()
|
raise_for_status(resp)
|
||||||
return resp.json()['data']['signed_key']
|
return resp.json()['data']['signed_key']
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,19 @@ import tempfile
|
|||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
CredentialPlugin = namedtuple('CredentialPlugin', ['name', 'inputs', 'backend'])
|
CredentialPlugin = namedtuple('CredentialPlugin', ['name', 'inputs', 'backend'])
|
||||||
|
|
||||||
|
|
||||||
|
def raise_for_status(resp):
|
||||||
|
resp.raise_for_status()
|
||||||
|
if resp.status_code >= 300:
|
||||||
|
exc = HTTPError()
|
||||||
|
setattr(exc, 'response', resp)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
class CertFiles():
|
class CertFiles():
|
||||||
"""
|
"""
|
||||||
A context manager used for writing a certificate and (optional) key
|
A context manager used for writing a certificate and (optional) key
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.migrations.executor import MigrationExecutor
|
from django.db.migrations.executor import MigrationExecutor
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import redirect
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -148,7 +148,21 @@ class URLModificationMiddleware(MiddlewareMixin):
|
|||||||
def _named_url_to_pk(cls, node, resource, named_url):
|
def _named_url_to_pk(cls, node, resource, named_url):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if node.populate_named_url_query_kwargs(kwargs, named_url):
|
if node.populate_named_url_query_kwargs(kwargs, named_url):
|
||||||
return str(get_object_or_404(node.model, **kwargs).pk)
|
match = node.model.objects.filter(**kwargs).first()
|
||||||
|
if match:
|
||||||
|
return str(match.pk)
|
||||||
|
else:
|
||||||
|
# if the name does *not* resolve to any actual resource,
|
||||||
|
# we should still attempt to route it through so that 401s are
|
||||||
|
# respected
|
||||||
|
# using "zero" here will cause the URL regex to match e.g.,
|
||||||
|
# /api/v2/users/<integer>/, but it also means that anonymous
|
||||||
|
# users will go down the path of having their credentials
|
||||||
|
# verified; in this way, *anonymous* users will that visit
|
||||||
|
# /api/v2/users/invalid-username/ *won't* see a 404, they'll
|
||||||
|
# see a 401 as if they'd gone to /api/v2/users/0/
|
||||||
|
#
|
||||||
|
return '0'
|
||||||
if resource == 'job_templates' and '++' not in named_url:
|
if resource == 'job_templates' and '++' not in named_url:
|
||||||
# special case for deprecated job template case
|
# special case for deprecated job template case
|
||||||
# will not raise a 404 on its own
|
# will not raise a 404 on its own
|
||||||
@@ -178,6 +192,7 @@ class URLModificationMiddleware(MiddlewareMixin):
|
|||||||
old_path = request.path_info
|
old_path = request.path_info
|
||||||
new_path = self._convert_named_url(old_path)
|
new_path = self._convert_named_url(old_path)
|
||||||
if request.path_info != new_path:
|
if request.path_info != new_path:
|
||||||
|
request.environ['awx.named_url_rewritten'] = request.path
|
||||||
request.path = request.path.replace(request.path_info, new_path)
|
request.path = request.path.replace(request.path_info, new_path)
|
||||||
request.path_info = new_path
|
request.path_info = new_path
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ class GrafanaBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
|||||||
headers=grafana_headers,
|
headers=grafana_headers,
|
||||||
verify=(not self.grafana_no_verify_ssl))
|
verify=(not self.grafana_no_verify_ssl))
|
||||||
if r.status_code >= 400:
|
if r.status_code >= 400:
|
||||||
logger.error(smart_text(_("Error sending notification grafana: {}").format(r.text)))
|
logger.error(smart_text(_("Error sending notification grafana: {}").format(r.status_code)))
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise Exception(smart_text(_("Error sending notification grafana: {}").format(r.text)))
|
raise Exception(smart_text(_("Error sending notification grafana: {}").format(r.status_code)))
|
||||||
sent_messages += 1
|
sent_messages += 1
|
||||||
return sent_messages
|
return sent_messages
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ class MattermostBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
|||||||
r = requests.post("{}".format(m.recipients()[0]),
|
r = requests.post("{}".format(m.recipients()[0]),
|
||||||
json=payload, verify=(not self.mattermost_no_verify_ssl))
|
json=payload, verify=(not self.mattermost_no_verify_ssl))
|
||||||
if r.status_code >= 400:
|
if r.status_code >= 400:
|
||||||
logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text)))
|
logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.status_code)))
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise Exception(smart_text(_("Error sending notification mattermost: {}").format(r.text)))
|
raise Exception(smart_text(_("Error sending notification mattermost: {}").format(r.status_code)))
|
||||||
sent_messages += 1
|
sent_messages += 1
|
||||||
return sent_messages
|
return sent_messages
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ class RocketChatBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
|||||||
|
|
||||||
if r.status_code >= 400:
|
if r.status_code >= 400:
|
||||||
logger.error(smart_text(
|
logger.error(smart_text(
|
||||||
_("Error sending notification rocket.chat: {}").format(r.text)))
|
_("Error sending notification rocket.chat: {}").format(r.status_code)))
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise Exception(smart_text(
|
raise Exception(smart_text(
|
||||||
_("Error sending notification rocket.chat: {}").format(r.text)))
|
_("Error sending notification rocket.chat: {}").format(r.status_code)))
|
||||||
sent_messages += 1
|
sent_messages += 1
|
||||||
return sent_messages
|
return sent_messages
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ class WebhookBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
|||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
verify=(not self.disable_ssl_verification))
|
verify=(not self.disable_ssl_verification))
|
||||||
if r.status_code >= 400:
|
if r.status_code >= 400:
|
||||||
logger.error(smart_text(_("Error sending notification webhook: {}").format(r.text)))
|
logger.error(smart_text(_("Error sending notification webhook: {}").format(r.status_code)))
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.text)))
|
raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.status_code)))
|
||||||
sent_messages += 1
|
sent_messages += 1
|
||||||
return sent_messages
|
return sent_messages
|
||||||
|
|||||||
@@ -219,3 +219,27 @@ def test_credential(get, admin_user, credentialtype_ssh):
|
|||||||
url = reverse('api:credential_detail', kwargs={'pk': test_cred.pk})
|
url = reverse('api:credential_detail', kwargs={'pk': test_cred.pk})
|
||||||
response = get(url, user=admin_user, expect=200)
|
response = get(url, user=admin_user, expect=200)
|
||||||
assert response.data['related']['named_url'].endswith('/test_cred++Machine+ssh++/')
|
assert response.data['related']['named_url'].endswith('/test_cred++Machine+ssh++/')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_403_vs_404(get):
|
||||||
|
cindy = User.objects.create(
|
||||||
|
username='cindy',
|
||||||
|
password='test_user',
|
||||||
|
is_superuser=False
|
||||||
|
)
|
||||||
|
bob = User.objects.create(
|
||||||
|
username='bob',
|
||||||
|
password='test_user',
|
||||||
|
is_superuser=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# bob cannot see cindy, pk lookup should be a 403
|
||||||
|
url = reverse('api:user_detail', kwargs={'pk': cindy.pk})
|
||||||
|
get(url, user=bob, expect=403)
|
||||||
|
|
||||||
|
# bob cannot see cindy, username lookup should be a 404
|
||||||
|
get('/api/v2/users/cindy/', user=bob, expect=404)
|
||||||
|
|
||||||
|
get(f'/api/v2/users/{cindy.pk}/', expect=401)
|
||||||
|
get('/api/v2/users/cindy/', expect=404)
|
||||||
|
|||||||
@@ -20,8 +20,19 @@ def test_label_get_queryset_su(label, user):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_label_access(label, user):
|
def test_label_read_access(label, user):
|
||||||
access = LabelAccess(user('user', False))
|
access = LabelAccess(user('user', False))
|
||||||
|
assert not access.can_read(label)
|
||||||
|
label.organization.member_role.members.add(user('user', False))
|
||||||
|
assert access.can_read(label)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_label_jt_read_access(label, user, job_template):
|
||||||
|
access = LabelAccess(user('user', False))
|
||||||
|
assert not access.can_read(label)
|
||||||
|
job_template.read_role.members.add(user('user', False))
|
||||||
|
job_template.labels.add(label)
|
||||||
assert access.can_read(label)
|
assert access.can_read(label)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user