fairagro-middleware-shared 12.2.2.dev37__tar.gz → 12.2.2.dev40__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/PKG-INFO +1 -1
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/security/__init__.py +2 -1
- fairagro_middleware_shared-12.2.2.dev40/src/middleware/shared/security/url_str.py +83 -0
- fairagro_middleware_shared-12.2.2.dev40/tests/unit/test_url_str.py +51 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/.gitignore +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/README.md +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/pyproject.toml +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/common/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/common/models.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/common/rocrate.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/py.typed +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v1/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v1/models.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v2/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v2/models.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v3/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v3/models.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/config/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/config/config_base.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/config/config_wrapper.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/config/logging.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/json_types.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/py.typed +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/formats/__init__.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/formats/base.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/formats/jsonld.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/model.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/security/url_redact.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/tracing.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_api_models.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_config_base.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_config_wrapper.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_harvest_report.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_logging.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_rocrate.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_tracing.py +0 -0
- {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_url_redact.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Security helpers shared across middleware components."""
|
|
2
2
|
|
|
3
3
|
from middleware.shared.security.url_redact import redact_url_userinfo
|
|
4
|
+
from middleware.shared.security.url_str import UrlStr
|
|
4
5
|
|
|
5
|
-
__all__ = ["redact_url_userinfo"]
|
|
6
|
+
__all__ = ["UrlStr", "redact_url_userinfo"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""URL value type that redacts HTTP(S) userinfo on ordinary stringification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import GetCoreSchemaHandler
|
|
8
|
+
from pydantic_core import PydanticCustomError, core_schema
|
|
9
|
+
|
|
10
|
+
from middleware.shared.security.url_redact import redact_url_userinfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UrlStr:
|
|
14
|
+
"""Credential-bearing URL: ``str()`` redacts userinfo; ``unredacted()`` for Git.
|
|
15
|
+
|
|
16
|
+
Use this instead of plain ``str`` (or fully opaque ``SecretStr``) for remotes
|
|
17
|
+
that may embed oauth2 tokens in the URL. Host and path remain visible after
|
|
18
|
+
redaction for ops diagnostics.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("_url",)
|
|
22
|
+
|
|
23
|
+
def __init__(self, url: str) -> None:
|
|
24
|
+
"""Wrap *url*; may contain userinfo credentials."""
|
|
25
|
+
self._url = url
|
|
26
|
+
|
|
27
|
+
def unredacted(self) -> str:
|
|
28
|
+
"""Return the full URL including credentials (Git CLI / GitPython only)."""
|
|
29
|
+
return self._url
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
"""Redacted form safe for logs, traces, and diagnostics."""
|
|
33
|
+
return redact_url_userinfo(self._url)
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
"""Repr uses the redacted form so tokens never appear in debug output."""
|
|
37
|
+
return f"UrlStr({str(self)!r})"
|
|
38
|
+
|
|
39
|
+
def __eq__(self, other: object) -> bool:
|
|
40
|
+
"""Compare on the raw URL; also accepts an equal plain ``str``."""
|
|
41
|
+
if isinstance(other, UrlStr):
|
|
42
|
+
return self._url == other._url
|
|
43
|
+
if isinstance(other, str):
|
|
44
|
+
return self._url == other
|
|
45
|
+
return NotImplemented
|
|
46
|
+
|
|
47
|
+
def __hash__(self) -> int:
|
|
48
|
+
"""Hash the raw URL so equal ``UrlStr`` values collide correctly."""
|
|
49
|
+
return hash(self._url)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def __get_pydantic_core_schema__(
|
|
53
|
+
cls,
|
|
54
|
+
_source: type[Any],
|
|
55
|
+
_handler: GetCoreSchemaHandler,
|
|
56
|
+
) -> core_schema.CoreSchema:
|
|
57
|
+
"""Accept ``str`` or ``UrlStr``; serialize as the redacted string."""
|
|
58
|
+
|
|
59
|
+
def _validate(value: object) -> UrlStr:
|
|
60
|
+
if isinstance(value, UrlStr):
|
|
61
|
+
return value
|
|
62
|
+
if isinstance(value, str):
|
|
63
|
+
return UrlStr(value)
|
|
64
|
+
raise PydanticCustomError(
|
|
65
|
+
"url_str_type",
|
|
66
|
+
"UrlStr input should be a str or UrlStr",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
python_schema = core_schema.no_info_plain_validator_function(_validate)
|
|
70
|
+
return core_schema.json_or_python_schema(
|
|
71
|
+
python_schema=core_schema.union_schema([
|
|
72
|
+
core_schema.is_instance_schema(cls),
|
|
73
|
+
python_schema,
|
|
74
|
+
]),
|
|
75
|
+
json_schema=core_schema.no_info_after_validator_function(
|
|
76
|
+
_validate,
|
|
77
|
+
core_schema.str_schema(),
|
|
78
|
+
),
|
|
79
|
+
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
80
|
+
str,
|
|
81
|
+
when_used="always",
|
|
82
|
+
),
|
|
83
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Tests for UrlStr redacting URL type."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import BaseModel, ValidationError
|
|
5
|
+
|
|
6
|
+
from middleware.shared.security import UrlStr
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_url_str_str_and_repr_redact_userinfo() -> None:
|
|
10
|
+
"""Ordinary string conversion hides oauth2 userinfo; host/path remain."""
|
|
11
|
+
url = UrlStr("https://oauth2:secret-token@gitlab.example.com/group/repo.git")
|
|
12
|
+
assert str(url) == "https://***@gitlab.example.com/group/repo.git"
|
|
13
|
+
assert "secret-token" not in str(url)
|
|
14
|
+
assert "secret-token" not in repr(url)
|
|
15
|
+
assert "gitlab.example.com/group/repo.git" in str(url)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_url_str_unredacted_returns_raw() -> None:
|
|
19
|
+
"""Explicit accessor returns the full credential-bearing URL."""
|
|
20
|
+
raw = "https://oauth2:secret-token@gitlab.example.com/group/repo.git"
|
|
21
|
+
assert UrlStr(raw).unredacted() == raw
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_url_str_equality_and_hash() -> None:
|
|
25
|
+
"""Equality and hashing use the raw URL (including vs plain str)."""
|
|
26
|
+
raw = "https://oauth2:tok@host/r.git"
|
|
27
|
+
a = UrlStr(raw)
|
|
28
|
+
b = UrlStr(raw)
|
|
29
|
+
assert a == b
|
|
30
|
+
assert a == raw
|
|
31
|
+
assert hash(a) == hash(b)
|
|
32
|
+
assert a != UrlStr("https://other.example/r.git")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_url_str_pydantic_field_accepts_str_and_url_str() -> None:
|
|
36
|
+
"""Model fields typed as UrlStr accept str or UrlStr; dump is redacted."""
|
|
37
|
+
|
|
38
|
+
class Model(BaseModel):
|
|
39
|
+
repo: UrlStr
|
|
40
|
+
|
|
41
|
+
# model_validate exercises str→UrlStr coercion (constructor is typed as UrlStr only).
|
|
42
|
+
from_str = Model.model_validate({"repo": "https://oauth2:secret@host/r.git"})
|
|
43
|
+
assert from_str.repo.unredacted() == "https://oauth2:secret@host/r.git"
|
|
44
|
+
assert from_str.model_dump()["repo"] == "https://***@host/r.git"
|
|
45
|
+
|
|
46
|
+
wrapped = UrlStr("https://oauth2:secret@host/r.git")
|
|
47
|
+
from_url = Model(repo=wrapped)
|
|
48
|
+
assert from_url.repo is wrapped or from_url.repo == wrapped
|
|
49
|
+
|
|
50
|
+
with pytest.raises(ValidationError):
|
|
51
|
+
Model.model_validate({"repo": 123})
|
{fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/.gitignore
RENAMED
|
File without changes
|
{fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/README.md
RENAMED
|
File without changes
|
{fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|