Skip to content

Commit f497cb8

Browse files
authored
feat: add validate for source repo url (#2318)
1 parent 1e5f320 commit f497cb8

7 files changed

Lines changed: 161 additions & 4 deletions

File tree

apiserver/paasng/conf.yaml.tpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,10 @@ BK_AUDIT_ENDPOINT = ""
939939
# - example-auth-token
940940
# host: http://bkmonitor-query.example.com
941941

942+
## ------------------------------------ 安全配置 ------------------------------------
943+
944+
# FORBIDDEN_REPO_PORTS 包含与代码/镜像仓库相关的敏感端口,配置后,平台将不允许用户填写或注册相关的代码/镜像仓库
945+
FORBIDDEN_REPO_PORTS = [21, 22, 23]
942946

943947
## ------------------------------------ internal 配置,仅开发项目与特殊环境下使用 ------------------------------------
944948

apiserver/paasng/paasng/platform/modules/serializers.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,14 @@
4444
from paasng.platform.templates.models import Template
4545
from paasng.utils.i18n.serializers import TranslatedCharField
4646
from paasng.utils.serializers import SourceControlField, UserNameField
47-
from paasng.utils.validators import RE_APP_CODE, DnsSafeNameValidator, ReservedWordValidator, validate_procfile
47+
from paasng.utils.validators import (
48+
RE_APP_CODE,
49+
DnsSafeNameValidator,
50+
ReservedWordValidator,
51+
validate_image_repo,
52+
validate_procfile,
53+
validate_repo_url,
54+
)
4855

4956

5057
def validate_build_method(build_method: RuntimeType, source_origin: SourceOrigin):
@@ -272,6 +279,22 @@ def validate_source_init_template(self, tmpl_name):
272279
raise ValidationError(_("模板 {} 不可用").format(tmpl_name))
273280
return tmpl_name
274281

282+
def validate(self, attrs):
283+
source_repo_url = attrs.get("source_repo_url")
284+
if source_repo_url:
285+
self._validate_source_repo_url(source_repo_url, attrs["source_origin"])
286+
return attrs
287+
288+
@staticmethod
289+
def _validate_source_repo_url(source_repo_url, source_origin):
290+
try:
291+
if source_origin == SourceOrigin.CNATIVE_IMAGE:
292+
validate_image_repo(source_repo_url)
293+
else:
294+
validate_repo_url(source_repo_url)
295+
except ValueError as e:
296+
raise ValidationError({"source_repo_url": str(e)})
297+
275298

276299
class ModuleBuildConfigSLZ(serializers.Serializer):
277300
"""模块镜像构建信息"""

apiserver/paasng/paasng/platform/sourcectl/serializers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import logging
1919

2020
from rest_framework import serializers
21+
from rest_framework.exceptions import ValidationError
2122
from rest_framework.validators import UniqueTogetherValidator
2223

2324
from paasng.platform.applications.serializers.fields import SourceDirField
@@ -26,6 +27,7 @@
2627
from paasng.platform.sourcectl.source_types import get_sourcectl_type
2728
from paasng.platform.sourcectl.type_specs import BkSvnSourceTypeSpec
2829
from paasng.utils.serializers import SourceControlField, UserNameField, VerificationCodeField
30+
from paasng.utils.validators import validate_image_repo, validate_repo_url
2931

3032
logger = logging.getLogger(__name__)
3133

@@ -170,6 +172,22 @@ class RepoBackendModifySLZ(serializers.Serializer):
170172
source_repo_auth_info = serializers.JSONField(required=False, default={})
171173
source_dir = SourceDirField(help_text="Procfile 所在目录, 如果是根目录可不填.")
172174

175+
def validate_source_repo_url(self, value: str) -> str:
176+
if not value:
177+
return value
178+
179+
is_image_repo = self.context["is_image_repo"]
180+
181+
try:
182+
if is_image_repo:
183+
validate_image_repo(value)
184+
else:
185+
validate_repo_url(value)
186+
except ValueError as e:
187+
raise ValidationError(str(e))
188+
else:
189+
return value
190+
173191

174192
##########################
175193
# 源码包相关的 serializers #

apiserver/paasng/paasng/platform/sourcectl/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ def modify(self, request, code, module_name):
358358
if module.get_source_origin() == SourceOrigin.IMAGE_REGISTRY:
359359
return self._modify_image(request, code, module_name)
360360

361-
slz = slzs.RepoBackendModifySLZ(data=self.request.data)
361+
slz = slzs.RepoBackendModifySLZ(data=self.request.data, context={"is_image_repo": False})
362362
slz.is_valid(raise_exception=True)
363363
data = slz.data
364364
repo_type = data["source_control_type"]
@@ -408,7 +408,7 @@ def modify(self, request, code, module_name):
408408

409409
def _modify_image(self, request, code, module_name):
410410
module = self.get_module_via_path()
411-
slz = slzs.RepoBackendModifySLZ(data=request.data)
411+
slz = slzs.RepoBackendModifySLZ(data=request.data, context={"is_image_repo": True})
412412
slz.is_valid(raise_exception=True)
413413
data = slz.data
414414
repo_url = data["source_repo_url"]

apiserver/paasng/paasng/settings/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,3 +1538,6 @@ def _build_file_handler(log_path: Path, filename: str, format: str) -> Dict:
15381538
FE_FEATURE_SETTINGS_CNATIVE_MGRLEGACY = settings.get("FE_FEATURE_SETTINGS_CNATIVE_MGRLEGACY", False)
15391539
# 应用令牌,用于 APP 调用用户态的云 API
15401540
FE_FEATURE_SETTINGS_APP_ACCESS_TOKEN = settings.get("FE_FEATURE_SETTINGS_APP_ACCESS_TOKEN", False)
1541+
1542+
# FORBIDDEN_REPO_PORTS 包含与代码/镜像仓库相关的敏感端口,配置后,平台将不允许用户填写或注册相关的代码/镜像仓库
1543+
FORBIDDEN_REPO_PORTS = settings.get("FORBIDDEN_REPO_PORTS", [])

apiserver/paasng/paasng/utils/validators.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
import base64
1919
import re
2020
from typing import Dict
21+
from urllib.parse import urlparse
2122

23+
from django.conf import settings
2224
from django.core.exceptions import ValidationError
2325
from django.core.validators import RegexValidator
2426
from django.utils.deconstruct import deconstructible
2527
from django.utils.encoding import force_str
2628
from past.builtins import basestring
29+
from moby_distribution.registry.utils import parse_image
2730

2831
from paasng.core.region.models import Region, RegionList, filter_region_by_name
2932

@@ -158,3 +161,48 @@ def validate_procfile(procfile: Dict[str, str]) -> Dict[str, str]:
158161

159162
# Formalize procfile data and return
160163
return {k.lower(): v for k, v in procfile.items()}
164+
165+
166+
def validate_image_repo(image_repo: str):
167+
"""Validate image repo port security.
168+
169+
:param image_repo: image repo
170+
:raise: ValueError if image repo is invalid
171+
"""
172+
repo_domain = parse_image(image_repo, default_registry="docker.io").domain
173+
174+
if ":" not in repo_domain:
175+
return
176+
177+
repo_port = repo_domain.rsplit(":")[-1]
178+
179+
try:
180+
port = int(repo_port)
181+
except ValueError:
182+
raise ValueError(f"Invalid image repo: the port {repo_port} is not an integer")
183+
184+
if port in [int(p) for p in settings.FORBIDDEN_REPO_PORTS]:
185+
raise ValueError(f"Invalid image repo: the port number {port} is forbidden")
186+
187+
188+
def validate_repo_url(repo_url: str):
189+
"""Validate repo url format, protocol, and port security.
190+
191+
:param repo_url: repo url
192+
:raise: ValueError if repo url is invalid
193+
"""
194+
try:
195+
parsed_url = urlparse(repo_url)
196+
except Exception:
197+
raise ValueError("Invalid url")
198+
199+
if not parsed_url.netloc:
200+
parsed_url = urlparse(f"https://{repo_url}")
201+
if not parsed_url.netloc:
202+
raise ValueError("Invalid url")
203+
204+
if parsed_url.scheme not in ["http", "https", "git", "svn"]:
205+
raise ValueError("Invalid url: only support http/https/git/svn scheme")
206+
207+
if parsed_url.port and parsed_url.port in [int(p) for p in settings.FORBIDDEN_REPO_PORTS]:
208+
raise ValueError(f"Invalid url: the port number {parsed_url.port} is forbidden")

apiserver/paasng/tests/paasng/test_utils/test_validators.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import pytest
1919
from django.core.exceptions import ValidationError
2020

21-
from paasng.utils.validators import DnsSafeNameValidator, ReservedWordValidator
21+
from paasng.utils.validators import DnsSafeNameValidator, ReservedWordValidator, validate_image_repo, validate_repo_url
2222

2323

2424
class TestReservedWordValidator:
@@ -47,3 +47,64 @@ def test_negative_sample(self, input_str):
4747
with pytest.raises(ValidationError) as exec_info:
4848
validator(input_str)
4949
assert exec_info.value.message == validator.message
50+
51+
52+
class Test__validate_repo_url:
53+
@pytest.mark.parametrize(
54+
"repo_url",
55+
["mysql://127.0.0.1", "postgres://127.0.0.1"],
56+
)
57+
def test_invalid_protocol(self, repo_url):
58+
with pytest.raises(ValueError, match="Invalid url: only support http/https/git/svn scheme"):
59+
validate_repo_url(repo_url)
60+
61+
@pytest.mark.parametrize(
62+
"repo_url",
63+
["http://127.0.0.1:22/bkapps.git", "https://127.0.0.1:23/bkapps.git"],
64+
)
65+
def test_invalid_port(self, repo_url, settings):
66+
settings.FORBIDDEN_REPO_PORTS = [22, 23]
67+
with pytest.raises(ValueError, match=r"Invalid url: the port number \d+ is forbidden"):
68+
validate_repo_url(repo_url)
69+
70+
@pytest.mark.parametrize(
71+
"repo_url",
72+
["/www.example.com", "//127.0.0.1"],
73+
)
74+
def test_invalid_url(self, repo_url):
75+
with pytest.raises(ValueError, match="Invalid url"):
76+
validate_repo_url(repo_url)
77+
78+
@pytest.mark.parametrize(
79+
"repo_url",
80+
[
81+
"http://127.0.0.1:80/bkapps.git",
82+
"https://127.0.0.1/bkapps.git",
83+
"git://127.0.0.1",
84+
"svn://127.0.0.1",
85+
],
86+
)
87+
def test_valid_url(self, repo_url):
88+
validate_repo_url(repo_url)
89+
90+
91+
class Test__validate_image_repo:
92+
@pytest.mark.parametrize(
93+
"image_repo",
94+
["mirror.tencent.com:22/bkpaas", "mirror.tencent.com:23/bkapps"],
95+
)
96+
def test_invalid_port(self, image_repo, settings):
97+
settings.FORBIDDEN_REPO_PORTS = [22, 23]
98+
with pytest.raises(ValueError, match=r"Invalid image repo: the port number \d+ is forbidden"):
99+
validate_image_repo(image_repo)
100+
101+
@pytest.mark.parametrize(
102+
"image_repo",
103+
[
104+
"mirror.tencent.com/bkapps",
105+
"mirror.tencent.com:443/bkapps",
106+
"nginx",
107+
],
108+
)
109+
def test_valid_repo(self, image_repo):
110+
validate_image_repo(image_repo)

0 commit comments

Comments
 (0)