From 914e586150901b0fd44f5ca5098cd2a81a2d45a4 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 2 Jul 2013 11:50:03 -0400 Subject: [PATCH] Work in progress on empheral host callback. --- awx/main/access.py | 1 + awx/main/authentication.py | 6 +++--- awx/main/models/__init__.py | 14 +++++++++++-- awx/main/rbac.py | 19 ++++++++++++++++-- awx/main/serializers.py | 2 ++ awx/main/tasks.py | 2 +- awx/main/urls.py | 1 + awx/main/views.py | 39 ++++++++++++++++++++++++++++++++----- 8 files changed, 71 insertions(+), 13 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index b4d171996a..6d9d99b5f3 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import User from rest_framework.exceptions import PermissionDenied +from awx import MODE from awx.main.models import * from awx.main.licenses import LicenseReader diff --git a/awx/main/authentication.py b/awx/main/authentication.py index 79b2b4ee62..1c5af775aa 100644 --- a/awx/main/authentication.py +++ b/awx/main/authentication.py @@ -5,7 +5,7 @@ from rest_framework import exceptions # AWX from awx.main.models import Job -class JobCallbackAuthentication(authentication.BaseAuthentication): +class JobTaskAuthentication(authentication.BaseAuthentication): ''' Custom authentication used for views accessed by the inventory and callback scripts when running a job. @@ -20,9 +20,9 @@ class JobCallbackAuthentication(authentication.BaseAuthentication): job = Job.objects.get(pk=job_id, status='running') except Job.DoesNotExist: return None - token = job.callback_auth_token + token = job.task_auth_token if auth[1] != token: - raise exceptions.AuthenticationFailed('Invalid job callback token') + raise exceptions.AuthenticationFailed('Invalid job task token') return (None, token) def authenticate_header(self, request): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index f9338c8554..72ca7e7670 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -645,6 +645,16 @@ class JobTemplate(CommonModel): def get_absolute_url(self): return reverse('main:job_template_detail', args=(self.pk,)) + def can_start_without_user_input(self): + '''Return whether job template can be used to start a new job without + requiring any user input.''' + if not self.credential: + return False + for field in ('ssh_password', 'sudo_password', 'ssh_key_unlock'): + if getattr(self.credential, 'needs_%s' % field): + return False + return True + class Job(CommonModel): ''' A job applies a project (with playbook) to an inventory source with a given @@ -802,8 +812,8 @@ class Job(CommonModel): pass @property - def callback_auth_token(self): - '''Return temporary auth token used for task callbacks via API.''' + def task_auth_token(self): + '''Return temporary auth token used for task requests via API.''' if self.status == 'running': h = hmac.new(settings.SECRET_KEY, self.created.isoformat()) return '%d-%s' % (self.pk, h.hexdigest()) diff --git a/awx/main/rbac.py b/awx/main/rbac.py index 064d4faba6..bc6ddc8de0 100644 --- a/awx/main/rbac.py +++ b/awx/main/rbac.py @@ -122,14 +122,29 @@ class CustomRbac(permissions.BasePermission): def has_object_permission(self, request, view, obj): return self.has_permission(request, view, obj) -class JobCallbackPermission(CustomRbac): +class JobTemplateCallbackPermission(CustomRbac): + + def has_permission(self, request, view, obj=None): + # If another authentication method was used and it's not a POST, return + # True to fall through to the next permission class. + if request.user or request.auth and request.method.lower() != 'post': + return super(JobTemplateCallbackPermission, self).has_permission(request, view, obj) + + return False + # FIXME + #try: + # job_template = JobTemplate.objects.get(active=True, pk=int(request.auth.split('-')[0])) + #except Job.DoesNotExist: + # return False + +class JobTaskPermission(CustomRbac): def has_permission(self, request, view, obj=None): # If another authentication method was used other than the one for job # callbacks, return True to fall through to the next permission class. if request.user or not request.auth: - return super(JobCallbackPermission, self).has_permission(request, view, obj) + return super(JobTaskPermission, self).has_permission(request, view, obj) # FIXME: Verify that inventory or job event requested are for the same # job ID present in the auth token, etc. diff --git a/awx/main/serializers.py b/awx/main/serializers.py index dbb399ed1f..8fdd0f0ecc 100644 --- a/awx/main/serializers.py +++ b/awx/main/serializers.py @@ -355,6 +355,8 @@ class JobTemplateSerializer(BaseSerializer): )) if obj.credential: res['credential'] = reverse('main:credential_detail', args=(obj.credential.pk,)) + if obj.host_config_key: + res['callback'] = reverse('main:job_template_callback', args=(obj.pk,)) return res def validate_playbook(self, attrs, source): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 285fc356e0..5804c9bb0b 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -88,7 +88,7 @@ class RunJob(Task): if hasattr(settings, 'ANSIBLE_TRANSPORT'): env['ANSIBLE_TRANSPORT'] = getattr(settings, 'ANSIBLE_TRANSPORT') env['REST_API_URL'] = settings.INTERNAL_API_URL - env['REST_API_TOKEN'] = job.callback_auth_token or '' + env['REST_API_TOKEN'] = job.task_auth_token or '' env['ANSIBLE_NOCOLOR'] = '1' # Prevent output of escape sequences. return env diff --git a/awx/main/urls.py b/awx/main/urls.py index 8797c312b9..9d064e37ca 100644 --- a/awx/main/urls.py +++ b/awx/main/urls.py @@ -91,6 +91,7 @@ job_template_urls = patterns('awx.main.views', url(r'^$', 'job_template_list'), url(r'^(?P[0-9]+)/$', 'job_template_detail'), url(r'^(?P[0-9]+)/jobs/$', 'job_template_jobs_list'), + url(r'^(?P[0-9]+)/callback/$', 'job_template_callback'), ) job_urls = patterns('awx.main.views', diff --git a/awx/main/views.py b/awx/main/views.py index bc1e409cd5..86af6addf5 100644 --- a/awx/main/views.py +++ b/awx/main/views.py @@ -26,7 +26,7 @@ from rest_framework.views import APIView # AWX from awx.main.access import * -from awx.main.authentication import JobCallbackAuthentication +from awx.main.authentication import JobTaskAuthentication from awx.main.licenses import LicenseReader from awx.main.base_views import * from awx.main.models import * @@ -1014,9 +1014,9 @@ class InventoryScriptView(generics.RetrieveAPIView): ''' model = Inventory - authentication_classes = [JobCallbackAuthentication] + \ + authentication_classes = [JobTaskAuthentication] + \ api_settings.DEFAULT_AUTHENTICATION_CLASSES - permission_classes = (JobCallbackPermission,) + permission_classes = (JobTaskPermission,) filter_backends = () def retrieve(self, request, *args, **kwargs): @@ -1066,6 +1066,35 @@ class JobTemplateDetail(BaseDetail): serializer_class = JobTemplateSerializer permission_classes = (CustomRbac,) +class JobTemplateCallback(generics.RetrieveAPIView): + ''' + Configure a host to POST to this resource using the `host_config_key`. + ''' + + model = JobTemplate + permission_classes = (JobTemplateCallbackPermission,) + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + host_config_key=obj.host_config_key, + ) + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + # Permission class should have already validated host_config_key. + # FIXME: Find host from request. + limit = obj.limit + # FIXME: Update limit based on host. + job = obj.create_job(limit=limit) + result = job.start() + if not result: + data = dict(passwords_needed_to_start=job.get_passwords_needed_to_start()) + return Response(data, status=400) + else: + return Response(status=202) + class JobTemplateJobsList(BaseSubList): model = Job @@ -1258,9 +1287,9 @@ class GroupJobEventsList(BaseJobEventsList): class JobJobEventsList(BaseJobEventsList): parent_model = Job - authentication_classes = [JobCallbackAuthentication] + \ + authentication_classes = [JobTaskAuthentication] + \ api_settings.DEFAULT_AUTHENTICATION_CLASSES - permission_classes = (JobCallbackPermission,) + permission_classes = (JobTaskPermission,) # Post allowed for job event callback only. def post(self, request, *args, **kwargs):