Skip to content

Commit

Permalink
Merge branch 'security-token'
Browse files Browse the repository at this point in the history
  • Loading branch information
kumar303 committed Apr 9, 2011
2 parents 1ee70ac + f1782a1 commit c07ece6
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 29 deletions.
10 changes: 10 additions & 0 deletions common/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ def wrapper(*args, **kw):
content_type='application/json',
status=status)
return wrapper


def post_required(f):
@functools.wraps(f)
def wrapper(request, *args, **kw):
if request.method != 'POST':
return http.HttpResponseNotAllowed(['POST'])
else:
return f(request, *args, **kw)
return wrapper
13 changes: 13 additions & 0 deletions migrations/002-add-start-token.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE `system_token` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`token` longtext NOT NULL,
`test_suite_id` integer NOT NULL,
`active` bool NOT NULL,
`created` datetime,
`last_modified` datetime
)
;
ALTER TABLE `system_token` ADD CONSTRAINT `test_suite_id_refs_id_3268f678`
FOREIGN KEY (`test_suite_id`) REFERENCES `system_testsuite` (`id`);
CREATE INDEX `system_token_34d728db` ON `system_token` (`active`);
COMMIT;
13 changes: 12 additions & 1 deletion system/forms.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@

from django.forms import ModelForm

from system.models import TestSuite
from system.models import TestSuite, Token

class TestSuiteForm(ModelForm):
class Meta:
model = TestSuite

def __init__(self, data=None, instance=None, **kw):
self.creating_test_suite = instance is None
super(TestSuiteForm, self).__init__(data=data, instance=instance,
**kw)

def save(self, *args, **kw):
ts = super(TestSuiteForm, self).save(*args, **kw)
if self.creating_test_suite:
Token.create(ts)
return ts
26 changes: 26 additions & 0 deletions system/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

import uuid

from django.db import models

class TestSuite(models.Model):
Expand All @@ -13,3 +15,27 @@ class TestSuite(models.Model):
null=True)
last_modified = models.DateTimeField(auto_now=True, editable=False,
null=True)

def active_tokens(self):
for tk in self.token_set.filter(active=True):
yield tk

class Token(models.Model):
token = models.TextField()
test_suite = models.ForeignKey(TestSuite)
active = models.BooleanField(default=True, db_index=True)
created = models.DateTimeField(auto_now_add=True, editable=False,
null=True)
last_modified = models.DateTimeField(auto_now=True, editable=False,
null=True)

@classmethod
def is_valid(cls, token, test_suite):
return cls.objects.filter(token=token, test_suite=test_suite,
active=True).count()

@classmethod
def create(cls, test_suite):
t = cls.objects.create(token=uuid.uuid4(), test_suite=test_suite,
active=True)
return t.token
15 changes: 15 additions & 0 deletions system/templates/system/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<th>Slug</th>
<th>URL</th>
<th>Actions</th>
<th>Tokens</th>
</tr>
{% for ts in test_suites %}
<tr>
Expand All @@ -39,6 +40,20 @@
<th><a href="{{ ts.url }}">{{ ts.url }}</a></th>
<th><a href="{% url system.edit_test_suite ts.id %}">[edit]</a>
<a href="{% url system.delete_test_suite ts.id %}">[delete]</a></th>
<th>
<ul>
{% for tk in ts.active_tokens %}
<li>{{ tk.token }}</li>
{% endfor %}
<li>
<form method="post" action="{% url system.generate_token %}">
{% csrf_token %}
<input type="hidden" name="test_suite_id" value="{{ ts.id }}"/>
<button type="submit">Generate token</button>
</form>
</li>
</ul>
</th>
</tr>
{% endfor %}
</table>
Expand Down
7 changes: 2 additions & 5 deletions system/templates/system/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,13 @@ <h2>Test Suites</h2>
<p>Number of available test suites: {{ test_suites.count }}</p>
<ul>
{% for ts in test_suites %}
<li>{{ ts.name }}
[<a class="start-tests" href="{% url system.start_tests ts.slug %}">
{% url system.start_tests ts.slug %}</a>]
</li>
<li>{{ ts.name }}</li>
{% endfor %}
</ul>
</div>
<div id="left">
<ul class="nav">
<li><a href="{% url system.test_suites %}">Add a new test suite</a></li>
<li><a href="{% url system.test_suites %}">Manage test suites</a></li>
<li><a href="https://github.com/kumar303/jstestnet">Source code for this site</a></li>
</ul>
</div>
Expand Down
93 changes: 81 additions & 12 deletions system/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
from nose.tools import eq_, raises

from common.testutils import no_form_errors
from system.models import TestSuite
from system.models import TestSuite, Token
from system.views import get_workers, NoWorkers
from work.models import TestRun, TestRunQueue, WorkQueue, Worker
from common.stdlib import json


def create_ts():
return TestSuite.objects.create(name='Zamboni', slug='zamboni',
def create_ts(name=None):
if not name:
name = 'Zamboni'
slug = name.lower()
return TestSuite.objects.create(name=name, slug=slug,
url='http://server/qunit1.html')


Expand Down Expand Up @@ -56,39 +59,45 @@ def test_status_page_ignores_partial_worker(self):

def test_start_tests_with_no_workers(self):
ts = create_ts()
r = self.client.get(reverse('system.start_tests', args=['zamboni']),
data={'browsers': 'firefox'})
token = Token.create(ts)
r = self.client.post(reverse('system.start_tests'),
data={'browsers': 'firefox', 'token': token,
'name': ts.slug})
eq_(r.status_code, 500)
data = json.loads(r.content)
eq_(data['error'], True)
eq_(data['message'], "No workers for u'firefox' are connected")

def test_start_tests_with_partial_worker(self):
ts = create_ts()
token = Token.create(ts)
# Be sure a worker that has not fully started up doesn't get
# chosen for work:
w = Worker()
w.last_heartbeat = None
w.is_alive = True
w.save()
r = self.client.get(reverse('system.start_tests', args=['zamboni']),
data={'browsers': '*'})
r = self.client.post(reverse('system.start_tests'),
data={'browsers': '*', 'token': token,
'name': ts.slug})
eq_(r.status_code, 500)
data = json.loads(r.content)
eq_(data['error'], True)
eq_(data['message'], "No workers for u'*' are connected")

def test_start_specific_worker(self):
ts = create_ts()
token = Token.create(ts)
fx_worker = create_worker(
user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; '
'rv:2.0b10) Gecko/20100101 Firefox/4.0b10')
ch_worker = create_worker(
user_agent='Mozilla/5.0 (Windows; U; Windows NT 5.2; '
'en-US) AppleWebKit/534.17 (KHTML, like Gecko)'
' Chrome/11.0.652.0 Safari/534.17')
r = self.client.get(reverse('system.start_tests', args=['zamboni']),
data={'browsers': 'firefox=~*'})
r = self.client.post(reverse('system.start_tests'),
data={'browsers': 'firefox=~*', 'token': token,
'name': ts.slug})
eq_(r.status_code, 200)
data = json.loads(r.content)

Expand All @@ -97,12 +106,54 @@ def test_start_specific_worker(self):
data = self.query(fx_worker)
eq_(data['cmd'], 'run_test')

def test_start_tests_without_token(self):
ts = create_ts()
worker = create_worker()

r = self.client.post(reverse('system.start_tests'),
data={'browsers': 'firefox', 'name': ts.slug})
eq_(r.status_code, 500)
data = json.loads(r.content)
eq_(data['error'], True)
eq_(data['message'],
'Invalid or expired token sent to start_tests. '
'Contact an administrator.')

def test_start_tests_with_correct_token(self):
ts = create_ts()
worker = create_worker()
token = Token.create(ts)

r = self.client.post(reverse('system.start_tests'),
data={'browsers': 'firefox', 'token': token,
'name': ts.slug})
eq_(r.status_code, 200)
data = json.loads(r.content)
assert 'test_run_id' in data, ('Unexpected: %s' % data)

def test_start_tests_with_wrong_token(self):
ts = create_ts('one')
other_ts = create_ts('two')
token = Token.create(other_ts)

r = self.client.post(reverse('system.start_tests'),
data={'browsers': 'firefox', 'token': token,
'name': ts.slug})
eq_(r.status_code, 500)
data = json.loads(r.content)
eq_(data['error'], True)
eq_(data['message'],
'Invalid or expired token sent to start_tests. '
'Contact an administrator.')

def test_get_job_result(self):
ts = create_ts()
token = Token.create(ts)
worker = create_worker()

r = self.client.get(reverse('system.start_tests', args=['zamboni']),
data={'browsers': 'firefox'})
r = self.client.post(reverse('system.start_tests'),
data={'browsers': 'firefox', 'token': token,
'name': ts.slug})
eq_(r.status_code, 200)
data = json.loads(r.content)
test_run_id = data['test_run_id']
Expand Down Expand Up @@ -259,6 +310,8 @@ def is_login_page(r):
is_login_page(r)
r = self.client.get(reverse('system.create_edit_test_suite'))
is_login_page(r)
r = self.client.post(reverse('system.generate_token'))
is_login_page(r)

def test_create_test_suite(self):
r = self.client.get(reverse('system.test_suites'))
Expand All @@ -278,12 +331,15 @@ def test_create_test_suite(self):
datetime.now().timetuple()[0:3])
eq_(ts.last_modified.timetuple()[0:3],
datetime.now().timetuple()[0:3])
qs = Token.objects.filter(test_suite=ts, active=True)
assert qs.count(), 'A token was not created for the new test suite'

def test_edit_test_suite(self):
ts = TestSuite(name='Zamboni', slug='zamboni',
url='http://127.0.0.1:8001/qunit/')
ts.save()
orig_ts = ts
tokens = Token.objects.count()
r = self.client.post(reverse('system.create_edit_test_suite',
args=[ts.id]), {
'name': 'Zamboni2',
Expand All @@ -299,12 +355,25 @@ def test_edit_test_suite(self):
eq_(ts.created.timetuple()[0:5],
orig_ts.created.timetuple()[0:5])
assert ts.last_modified != orig_ts.last_modified
# Make sure no new tokens were created
eq_(Token.objects.count(), tokens)

def test_delete_test_suite(self):
ts = TestSuite(name='Zamboni', slug='zamboni',
url='http://127.0.0.1:8001/qunit/')
ts.save()
r = self.client.get(reverse('system.delete_test_suite',
args=[ts.id]))
args=[ts.id]))
self.assertRedirects(r, reverse('system.test_suites'))
eq_(TestSuite.objects.filter(slug='zamboni').count(), 0)
eq_(Token.objects.filter(test_suite=ts).count(), 0)

def test_generate_token(self):
ts = TestSuite(name='Zamboni', slug='zamboni',
url='http://127.0.0.1:8001/qunit/')
ts.save()
r = self.client.post(reverse('system.generate_token'), {
'test_suite_id': ts.id
})
self.assertRedirects(r, reverse('system.test_suites'))
tk = Token.objects.get(test_suite=ts)
4 changes: 3 additions & 1 deletion system/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
name='system.restart_workers'),
url(r'^test/(?P<test_run_id>[0-9]+)/result$', views.test_result,
name='system.test_result'),
url(r'^start_tests/(?P<name>[a-zA-Z_0-9-]+)$', views.start_tests,
url(r'^start_tests/?$', views.start_tests,
name='system.start_tests'),
# Not Django admin
url(r'^admin/$', views.test_suites, name='system.test_suites'),
url(r'^admin/test_suite/([^/]+)$', views.test_suites,
name='system.edit_test_suite'),
url(r'^admin/generate_token/?$', views.generate_token,
name='system.generate_token'),
url(r'^admin/create_edit_test_suite/([^/]+)?$',
views.create_edit_test_suite, name='system.create_edit_test_suite'),
url(r'^admin/delete_test_suite/([^/]+)$', views.delete_test_suite,
Expand Down
32 changes: 27 additions & 5 deletions system/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@
from django.db.models import Q
from django.shortcuts import render_to_response, get_object_or_404
from django.template import loader, RequestContext
from django.views.decorators.csrf import csrf_view_exempt

from common.stdlib import json
from system.models import TestSuite
from system.models import TestSuite, Token
from system.forms import TestSuiteForm
from common.decorators import json_view
from common.decorators import json_view, post_required
import work.views
from work.models import Worker, WorkQueue, TestRun, TestRunQueue


class InvalidToken(Exception):
"""An invalid or expired token sent to start_tests."""


@staff_member_required
def test_suites(request, test_suite_id=None, form=None):
test_suites = TestSuite.objects.all().order_by('slug')
Expand Down Expand Up @@ -54,6 +59,13 @@ def delete_test_suite(request, pk):
return http.HttpResponseRedirect(reverse('system.test_suites'))


@staff_member_required
def generate_token(request):
ts = get_object_or_404(TestSuite, pk=request.POST['test_suite_id'])
Token.create(ts)
return http.HttpResponseRedirect(reverse('system.test_suites'))


@json_view
def test_result(request, test_run_id):
test_run = get_object_or_404(TestRun, pk=test_run_id)
Expand Down Expand Up @@ -127,16 +139,26 @@ def get_workers(qs, browser_spec):


@json_view
@post_required
@transaction.commit_on_success()
def start_tests(request, name):
ts = get_object_or_404(TestSuite, slug=name)
@csrf_view_exempt
def start_tests(request):
ts = get_object_or_404(TestSuite, slug=request.POST.get('name', None))
token_is_valid = False
if request.POST.get('token', None):
if Token.is_valid(request.POST['token'], ts):
token_is_valid = True
if not token_is_valid:
raise InvalidToken('Invalid or expired token sent to start_tests. '
'Contact an administrator.')

work.views.collect_garbage()
# TODO(kumar) don't start a test suite if it's already running.
test = TestRun(test_suite=ts)
test.save()
workers = []
qs = Worker.objects.filter(is_alive=True)
browsers = request.GET.get('browsers', None)
browsers = request.POST.get('browsers', None)
if not browsers:
raise ValueError("No browsers were specified in GET request")
for worker in get_workers(qs, browsers):
Expand Down
Loading

0 comments on commit c07ece6

Please sign in to comment.