Skip to content

Commit 1486de2

Browse files
authored
API: validate RemoteRepository when creating a Project (#8983)
Extend `ProjectCreateSerializer` to validate if there is a `RemoteRepository` match for the project's repository created. If it's not found (matching `ssh_url`, `clone_url` and `html_url`) we call a validation function that will act depending on the platform: - community: won't raise any exception and just won't connect the `Project` to a `RemoteRepository`. This is the current behavior and this PR keeps it the same - commercial: if the organization has VCS SSO enabled and we didn't find a `RemoteRepository` it will raise a `ValidationError` communicating the user that we weren't able to import the `Project` This is the first step on making SSO UX a little nicer and avoid ending up with disconnected `Project` -> `RemoteRepository` and loosing access to them.
1 parent bd0f3dc commit 1486de2

File tree

2 files changed

+74
-0
lines changed

2 files changed

+74
-0
lines changed

readthedocs/api/v3/serializers.py

+44
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.conf import settings
55
from django.contrib.auth.models import User
6+
from django.db.models import Q
67
from django.urls import reverse
78
from django.utils.translation import gettext as _
89
from rest_flex_fields import FlexFieldsModelSerializer
@@ -473,6 +474,19 @@ class Meta:
473474
'homepage',
474475
)
475476

477+
def _validate_remote_repository(self, data):
478+
"""
479+
Validate connection between `Project` and `RemoteRepository`.
480+
481+
We don't do anything in community, but we do ensure this relationship
482+
is posible before creating the `Project` on commercial when the
483+
organization has VCS SSO enabled.
484+
485+
If we cannot ensure the relationship here, this method should raise a
486+
`ValidationError`.
487+
"""
488+
pass
489+
476490
def validate_name(self, value):
477491
potential_slug = slugify(value)
478492
if not potential_slug:
@@ -485,6 +499,36 @@ def validate_name(self, value):
485499
)
486500
return value
487501

502+
def validate(self, data): # pylint: disable=arguments-differ
503+
repo = data.get('repo')
504+
try:
505+
# We are looking for an exact match of the repository URL entered
506+
# by the user and any of the known URLs (ssh, clone, html) we have
507+
# in our database for this remote repository.
508+
#
509+
# If the `RemoteRepository` is found, we save it to link with
510+
# `Project` object after performing its creating.
511+
query = Q(ssh_url=repo) | Q(clone_url=repo) | Q(html_url=repo)
512+
remote_repository = RemoteRepository.objects.get(query)
513+
data.update({
514+
'remote_repository': remote_repository,
515+
})
516+
except RemoteRepository.DoesNotExist:
517+
self._validate_remote_repository(data)
518+
519+
return data
520+
521+
def create(self, validated_data):
522+
remote_repository = validated_data.pop('remote_repository', None)
523+
project = super().create(validated_data)
524+
525+
# Link the Project with the RemoteRepository if we found it.
526+
if remote_repository:
527+
project.remote_repository = remote_repository
528+
project.save()
529+
530+
return project
531+
488532

489533
class ProjectCreateSerializer(SettingsOverrideObject):
490534
_default_class = ProjectCreateSerializerBase

readthedocs/api/v3/tests/test_projects.py

+30
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.urls import reverse
55

66
from readthedocs.projects.models import Project
7+
from readthedocs.oauth.models import RemoteRepository
78

89
from .mixins import APIEndpointMixin
910

@@ -129,6 +130,7 @@ def test_import_project(self):
129130
self.assertTrue(query.exists())
130131

131132
project = query.first()
133+
self.assertIsNone(project.remote_repository)
132134
self.assertEqual(project.name, 'Test Project')
133135
self.assertEqual(project.slug, 'test-project')
134136
self.assertEqual(project.repo, 'https://github.com/rtfd/template')
@@ -206,6 +208,34 @@ def test_import_project_with_extra_fields(self):
206208
self.assertNotEqual(project.default_version, 'v1.0')
207209
self.assertIn(self.me, project.users.all())
208210

211+
def test_import_project_with_remote_repository(self):
212+
remote_repository = fixture.get(
213+
RemoteRepository,
214+
full_name='rtfd/template',
215+
clone_url='https://github.com/rtfd/template',
216+
html_url='https://github.com/rtfd/template',
217+
ssh_url='[email protected]:rtfd/template.git',
218+
)
219+
220+
data = {
221+
'name': 'Test Project',
222+
'repository': {
223+
'url': 'https://github.com/rtfd/template',
224+
'type': 'git',
225+
},
226+
}
227+
228+
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}')
229+
response = self.client.post(reverse('projects-list'), data)
230+
self.assertEqual(response.status_code, 201)
231+
232+
query = Project.objects.filter(slug='test-project')
233+
self.assertTrue(query.exists())
234+
235+
project = query.first()
236+
self.assertIsNotNone(project.remote_repository)
237+
self.assertEqual(project.remote_repository, remote_repository)
238+
209239
def test_update_project(self):
210240
data = {
211241
'name': 'Updated name',

0 commit comments

Comments
 (0)