Skip to content

Commit e85e1fc

Browse files
authored
Add feature flip for build endpoints (#3205)
* Add feature flipping models to Projects, plus example feature flip Also, use a new pattern for API instance objects * Refactoring the make_api_* calls into the API models * Add migrations for proxy models * Add some admin features * Bump setuptools version. * Add feature flip for setuptools latest version Also, add a convenience method on the Project model for feature flag dependent values. * Add feature flip for build endpoints Returns 403 if the project doesn't have the build endpoint enabled. * Rework feature relationship, add default true value based on date This allows for features on deprecated code, where we can say the feature is true historically. * Fix some issues, more tests * Add data migration adding feature to database * Fix migration * Fix tests * Rename Feature.feature -> Feature.feature_id, other cleanup * Drop setuptools pinning * Update field name * Fix migration as well * Use semver for setuptools version * Missed merge conflict * Rename function, also go private function
1 parent 4417322 commit e85e1fc

File tree

4 files changed

+117
-2
lines changed

4 files changed

+117
-2
lines changed

readthedocs/core/views/hooks.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from readthedocs.core.utils import trigger_build
1212
from readthedocs.builds.constants import LATEST
1313
from readthedocs.projects import constants
14-
from readthedocs.projects.models import Project
14+
from readthedocs.projects.models import Project, Feature
1515
from readthedocs.projects.tasks import update_imported_docs
1616

1717
import logging
@@ -23,6 +23,10 @@ class NoProjectException(Exception):
2323
pass
2424

2525

26+
def _allow_deprecated_webhook(project):
27+
return project.has_feature(Feature.ALLOW_DEPRECATED_WEBHOOKS)
28+
29+
2630
def _build_version(project, slug, already_built=()):
2731
"""
2832
Where we actually trigger builds for a project and slug.
@@ -108,6 +112,13 @@ def _build_url(url, projects, branches):
108112
ret = ""
109113
all_built = {}
110114
all_not_building = {}
115+
116+
# This endpoint doesn't require authorization, we shouldn't allow builds to
117+
# be triggered from this any longer. Deprecation plan is to selectively
118+
# allow access to this endpoint for now.
119+
if not any(_allow_deprecated_webhook(project) for project in projects):
120+
return HttpResponse('This API endpoint is deprecated', status=403)
121+
111122
for project in projects:
112123
(built, not_building) = build_branches(project, branches)
113124
if not built:
@@ -314,6 +325,11 @@ def generic_build(request, project_id_or_slug=None):
314325
project_id_or_slug)
315326
return HttpResponseNotFound(
316327
'Repo not found: %s' % project_id_or_slug)
328+
# This endpoint doesn't require authorization, we shouldn't allow builds to
329+
# be triggered from this any longer. Deprecation plan is to selectively
330+
# allow access to this endpoint for now.
331+
if not _allow_deprecated_webhook(project):
332+
return HttpResponse('This API endpoint is deprecated', status=403)
317333
if request.method == 'POST':
318334
slug = request.POST.get('version_slug', project.default_version)
319335
log.info(
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""Add feature for allowing access to deprecated webhook endpoints"""
4+
5+
from __future__ import unicode_literals
6+
7+
from django.db import migrations
8+
9+
10+
FEATURE_ID = 'allow_deprecated_webhooks'
11+
12+
13+
def forward_add_feature(apps, schema_editor):
14+
Feature = apps.get_model('projects', 'Feature')
15+
Feature.objects.create(
16+
feature_id=FEATURE_ID,
17+
default_true=True,
18+
)
19+
20+
21+
def reverse_add_feature(apps, schema_editor):
22+
Feature = apps.get_model('projects', 'Feature')
23+
Feature.objects.filter(feature_id=FEATURE_ID).delete()
24+
25+
26+
class Migration(migrations.Migration):
27+
28+
dependencies = [
29+
('projects', '0020_add-api-project-proxy'),
30+
]
31+
32+
operations = [
33+
migrations.RunPython(forward_add_feature, reverse_add_feature)
34+
]

readthedocs/projects/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,11 +1003,13 @@ def add_features(sender, **kwargs):
10031003
# may be added by other packages
10041004
USE_SPHINX_LATEST = 'use_sphinx_latest'
10051005
USE_SETUPTOOLS_LATEST = 'use_setuptools_latest'
1006+
ALLOW_DEPRECATED_WEBHOOKS = 'allow_deprecated_webhooks'
10061007
PIP_ALWAYS_UPGRADE = 'pip_always_upgrade'
10071008

10081009
FEATURES = (
10091010
(USE_SPHINX_LATEST, _('Use latest version of Sphinx')),
10101011
(USE_SETUPTOOLS_LATEST, _('Use latest version of setuptools')),
1012+
(ALLOW_DEPRECATED_WEBHOOKS, _('Allow deprecated webhook views')),
10111013
(PIP_ALWAYS_UPGRADE, _('Always run pip install --upgrade')),
10121014
)
10131015

readthedocs/rtd_tests/tests/test_post_commit_hooks.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from future.backports.urllib.parse import urlencode
1010

1111
from readthedocs.builds.models import Version
12-
from readthedocs.projects.models import Project
12+
from readthedocs.projects.models import Project, Feature
1313

1414

1515
log = logging.getLogger(__name__)
@@ -32,6 +32,11 @@ def _setup(self):
3232
self.mocks = [mock.patch('readthedocs.core.views.hooks.trigger_build')]
3333
self.patches = [m.start() for m in self.mocks]
3434

35+
self.feature = Feature.objects.get(feature_id=Feature.ALLOW_DEPRECATED_WEBHOOKS)
36+
self.feature.projects.add(self.pip)
37+
self.feature.projects.add(self.rtfd)
38+
self.feature.projects.add(self.sphinx)
39+
3540
self.client.login(username='eric', password='test')
3641

3742

@@ -141,6 +146,23 @@ def test_gitlab_post_commit_knows_default_branches(self):
141146
rtd.default_branch = old_default
142147
rtd.save()
143148

149+
def test_gitlab_webhook_is_deprecated(self):
150+
# Project is created after feature, not included in historical allowance
151+
url = 'https://github.com/rtfd/readthedocs-build'
152+
payload = self.payload.copy()
153+
payload['project']['http_url'] = url
154+
project = get(
155+
Project,
156+
main_language_project=None,
157+
repo=url,
158+
)
159+
r = self.client.post(
160+
'/gitlab/',
161+
data=json.dumps(payload),
162+
content_type='application/json'
163+
)
164+
self.assertEqual(r.status_code, 403)
165+
144166

145167
class GitHubPostCommitTest(BasePostCommitTest):
146168
fixtures = ["eric"]
@@ -297,6 +319,23 @@ def test_github_post_commit_knows_default_branches(self):
297319
rtd.default_branch = old_default
298320
rtd.save()
299321

322+
def test_github_webhook_is_deprecated(self):
323+
# Project is created after feature, not included in historical allowance
324+
url = 'https://github.com/rtfd/readthedocs-build'
325+
payload = self.payload.copy()
326+
payload['repository']['url'] = url
327+
project = get(
328+
Project,
329+
main_language_project=None,
330+
repo=url,
331+
)
332+
r = self.client.post(
333+
'/github/',
334+
data=json.dumps(payload),
335+
content_type='application/json'
336+
)
337+
self.assertEqual(r.status_code, 403)
338+
300339

301340
class CorePostCommitTest(BasePostCommitTest):
302341
fixtures = ["eric"]
@@ -320,6 +359,12 @@ def test_hook_state_tracking(self):
320359
# Need to re-query to get updated DB entry
321360
self.assertEqual(Project.objects.get(slug='read-the-docs').has_valid_webhook, True)
322361

362+
def test_generic_webhook_is_deprecated(self):
363+
# Project is created after feature, not included in historical allowance
364+
project = get(Project, main_language_project=None)
365+
r = self.client.post('/build/%s' % project.pk, {'version_slug': 'master'})
366+
self.assertEqual(r.status_code, 403)
367+
323368

324369
class BitBucketHookTests(BasePostCommitTest):
325370

@@ -476,6 +521,7 @@ def test_bitbucket_default_branch(self):
476521
Project, repo='HTTPS://bitbucket.org/test/project', slug='test-project',
477522
default_branch='integration', repo_type='git',
478523
)
524+
self.feature.projects.add(self.test_project)
479525

480526
self.git_payload['commits'] = [{
481527
"branch": "integration",
@@ -487,3 +533,20 @@ def test_bitbucket_default_branch(self):
487533
r = self.client.post('/bitbucket/', data=json.dumps(self.git_payload),
488534
content_type='application/json')
489535
self.assertContains(r, '(URL Build) Build Started: bitbucket.org/test/project [latest]')
536+
537+
def test_bitbucket_webhook_is_deprecated(self):
538+
# Project is created after feature, not included in historical allowance
539+
url = 'https://bitbucket.org/rtfd/readthedocs-build'
540+
payload = self.git_payload.copy()
541+
payload['repository']['absolute_url'] = '/rtfd/readthedocs-build'
542+
project = get(
543+
Project,
544+
main_language_project=None,
545+
repo=url,
546+
)
547+
r = self.client.post(
548+
'/bitbucket/',
549+
data=json.dumps(payload),
550+
content_type='application/json'
551+
)
552+
self.assertEqual(r.status_code, 403)

0 commit comments

Comments
 (0)