diff --git a/assets/src/bundles/save/index.js b/assets/src/bundles/save/index.js --- a/assets/src/bundles/save/index.js +++ b/assets/src/bundles/save/index.js @@ -372,11 +372,16 @@ validUrl = isGitRepoUrl(originUrl); } + let customValidity = ''; if (validUrl) { - input.setCustomValidity(''); + if ((originUrl.password !== '' && originUrl.password !== 'anonymous')) { + customValidity = 'The origin url contains a password and cannot be accepted for security reasons'; + } } else { - input.setCustomValidity('The origin url is not valid or does not reference a code repository'); + customValidity = 'The origin url is not valid or does not reference a code repository'; } + input.setCustomValidity(customValidity); + $(input).siblings('.invalid-feedback').text(customValidity); } export function initTakeNewSnapshot() { diff --git a/cypress/integration/origin-save.spec.js b/cypress/integration/origin-save.spec.js --- a/cypress/integration/origin-save.spec.js +++ b/cypress/integration/origin-save.spec.js @@ -777,4 +777,73 @@ .should('have.class', 'active'); }); + it('should not accept origin URL with password', function() { + + makeOriginSaveRequest('git', 'https://user:password@git.example.org/user/repo'); + + cy.get('.invalid-feedback') + .should('contain', 'The origin url contains a password and cannot be accepted for security reasons'); + + }); + + it('should accept origin URL with username but without password', function() { + + cy.adminLogin(); + cy.visit(url); + + const originUrl = 'https://user@git.example.org/user/repo'; + + stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl), + saveRequestStatus: 'accepted', + originUrl: originUrl, + saveTaskStatus: 'not yet scheduled'}); + + makeOriginSaveRequest('git', originUrl); + + cy.wait('@saveRequest').then(() => { + checkAlertVisible('success', saveCodeMsg['success']); + }); + + }); + + it('should accept origin URL with anonymous credentials', function() { + + cy.adminLogin(); + cy.visit(url); + + const originUrl = 'https://anonymous:anonymous@git.example.org/user/repo'; + + stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl), + saveRequestStatus: 'accepted', + originUrl: originUrl, + saveTaskStatus: 'not yet scheduled'}); + + makeOriginSaveRequest('git', originUrl); + + cy.wait('@saveRequest').then(() => { + checkAlertVisible('success', saveCodeMsg['success']); + }); + + }); + + it('should accept origin URL with empty password', function() { + + cy.adminLogin(); + cy.visit(url); + + const originUrl = 'https://anonymous:@git.example.org/user/repo'; + + stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl), + saveRequestStatus: 'accepted', + originUrl: originUrl, + saveTaskStatus: 'not yet scheduled'}); + + makeOriginSaveRequest('git', originUrl); + + cy.wait('@saveRequest').then(() => { + checkAlertVisible('success', saveCodeMsg['success']); + }); + + }); + }); diff --git a/swh/web/common/origin_save.py b/swh/web/common/origin_save.py --- a/swh/web/common/origin_save.py +++ b/swh/web/common/origin_save.py @@ -9,6 +9,7 @@ import json import logging from typing import Any, Dict, List, Optional, Tuple +from urllib.parse import urlparse from prometheus_client import Gauge import requests @@ -218,7 +219,14 @@ _validate_url(origin_url) except ValidationError: raise BadInputExc( - "The provided origin url (%s) is not valid!" % escape(origin_url) + f"The provided origin url ({escape(origin_url)}) is not valid!" + ) + + parsed_url = urlparse(origin_url) + if parsed_url.password not in (None, "", "anonymous"): + raise BadInputExc( + "The provided origin url contains a password and cannot be " + "accepted for security reasons." ) diff --git a/swh/web/tests/api/views/test_origin_save.py b/swh/web/tests/api/views/test_origin_save.py --- a/swh/web/tests/api/views/test_origin_save.py +++ b/swh/web/tests/api/views/test_origin_save.py @@ -603,3 +603,55 @@ assert SaveOriginRequest.objects.get(user_ids__contains=f'"{regular_user.id}"') assert SaveOriginRequest.objects.get(user_ids__contains=f'"{regular_user2.id}"') + + +def test_reject_origin_url_with_password(api_client, swh_scheduler): + url = reverse( + "api-1-save-origin", + url_args={ + "visit_type": "git", + "origin_url": "https://user:password@git.example.org/user/repo", + }, + ) + resp = check_api_post_responses(api_client, url, status_code=400) + + assert resp.data == { + "exception": "BadInputExc", + "reason": ( + "The provided origin url contains a password and cannot " + "be accepted for security reasons." + ), + } + + +def test_accept_origin_url_with_username_but_without_password( + api_client, swh_scheduler +): + url = reverse( + "api-1-save-origin", + url_args={ + "visit_type": "git", + "origin_url": "https://user@git.example.org/user/repo", + }, + ) + check_api_post_responses(api_client, url, status_code=200) + + +@pytest.mark.parametrize( + "origin_url", + [ + "https://anonymous:anonymous@git.example.org/user/repo", + "https://anonymous:@git.example.org/user/repo", + ], +) +def test_accept_origin_url_with_anonymous_credentials( + api_client, swh_scheduler, origin_url +): + url = reverse( + "api-1-save-origin", + url_args={ + "visit_type": "git", + "origin_url": origin_url, + }, + ) + check_api_post_responses(api_client, url, status_code=200)