Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add sensitive metadata #365

Merged
merged 17 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cuenca_validations/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
'digits',
'get_state_name',
'uuid_field',
'LogConfig',
]

from .card import StrictPaymentCardNumber
Expand Down Expand Up @@ -152,6 +153,7 @@
from .files import BatchFileMetadata
from .general import (
JSONEncoder,
LogConfig,
SantizedDict,
StrictPositiveInt,
digits,
Expand Down
7 changes: 7 additions & 0 deletions cuenca_validations/types/general.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from dataclasses import dataclass
from typing import Annotated, Any, Optional

from pydantic import AnyUrl, Field, HttpUrl, PlainSerializer, StringConstraints
Expand Down Expand Up @@ -82,3 +83,9 @@ def digits(

def get_state_name(state: State):
return names_state[state]


@dataclass
class LogConfig:
masked: bool = False
unmasked_chars_length: int = 0
7 changes: 7 additions & 0 deletions cuenca_validations/types/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
from base64 import urlsafe_b64encode
from typing import Callable

from .general import LogConfig


def uuid_field(prefix: str = '') -> Callable[[], str]:
def base64_uuid_func() -> str:
return prefix + urlsafe_b64encode(uuid.uuid4().bytes).decode()[:-2]

return base64_uuid_func


def get_log_config(field) -> LogConfig:
"""Helper function to find LogConfig in field metadata"""
return next(m for m in field.metadata if isinstance(m, LogConfig))
11 changes: 9 additions & 2 deletions cuenca_validations/types/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@
PaymentCardNumber,
StrictPaymentCardNumber,
)
from .general import SerializableAnyUrl, SerializableHttpUrl, StrictPositiveInt
from .general import (
LogConfig,
SerializableAnyUrl,
SerializableHttpUrl,
StrictPositiveInt,
)
from .identities import (
Address,
Beneficiary,
Expand Down Expand Up @@ -490,7 +495,9 @@ def beneficiary_percentage(


class UserLoginRequest(BaseRequest):
password: str # Set password field to str for backward compatibility.
password: Annotated[
str, LogConfig(masked=True)
] # Set password field to str for backward compatibility.
user_id: Optional[str] = Field(None, description='Deprecated field')
model_config = ConfigDict(
json_schema_extra={'example': {'password': 'supersecret'}},
Expand Down
2 changes: 1 addition & 1 deletion cuenca_validations/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.0.4'
__version__ = '2.0.5.dev4'
53 changes: 51 additions & 2 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import json
from dataclasses import dataclass
from enum import Enum
from typing import Annotated

import pytest
from freezegun import freeze_time
from pydantic import BaseModel, SecretStr, ValidationError
from pydantic import AfterValidator, BaseModel, SecretStr, ValidationError

from cuenca_validations.types import (
Address,
Expand All @@ -24,7 +25,8 @@
SessionType,
State,
)
from cuenca_validations.types.general import StrictPositiveInt
from cuenca_validations.types.general import LogConfig, StrictPositiveInt
from cuenca_validations.types.helpers import get_log_config
from cuenca_validations.types.requests import (
ApiKeyUpdateRequest,
BankAccountValidationRequest,
Expand Down Expand Up @@ -586,3 +588,50 @@ class IntModel(BaseModel):
def test_strict_positive_int_invalid(value, expected_error, expected_message):
with pytest.raises(expected_error, match=expected_message):
IntModel(value=value)


def validate_password(password: str) -> str:
"""
Example of a custom validator
Check if the password contains repeated numbers
"""
import re

if re.search(r'(\d).*\1', password):
raise ValueError("Password cannot contain repeated digits")
return password


class MetadataModel(BaseModel):
password: Annotated[
str, AfterValidator(validate_password), LogConfig(masked=True)
]
secret: Annotated[str, LogConfig(masked=True)]
partial_secret: Annotated[
str, LogConfig(masked=True, unmasked_chars_length=4)
]


def test_metadata():
model = MetadataModel(
password="Mypass123",
secret="super-secret",
partial_secret="1234567890",
)

password_field = MetadataModel.model_fields["password"]
secret_field = MetadataModel.model_fields["secret"]
partial_field = MetadataModel.model_fields["partial_secret"]

assert get_log_config(password_field).masked is True
assert get_log_config(password_field).unmasked_chars_length == 0

assert get_log_config(secret_field).masked is True
assert get_log_config(secret_field).unmasked_chars_length == 0

assert get_log_config(partial_field).masked is True
assert get_log_config(partial_field).unmasked_chars_length == 4

assert model.password == "Mypass123"
assert model.secret == "super-secret"
assert model.partial_secret == "1234567890"
Loading