-
Notifications
You must be signed in to change notification settings - Fork 0
Use private attrs for sensitive config credentials #8
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| model_validator, | ||
| PlainSerializer, | ||
| PrivateAttr, | ||
| TypeAdapter, | ||
| ) | ||
| from strenum import KebabCaseStrEnum, LowercaseStrEnum | ||
|
|
||
|
|
@@ -86,6 +87,10 @@ | |
| ZstdCompressionLevel = Annotated[int, Field(ge=1, le=19)] | ||
|
|
||
|
|
||
| _optional_non_empty_str_validator = TypeAdapter(Optional[NonEmptyStr]) | ||
| _optional_str_validator = TypeAdapter(Optional[str]) | ||
|
|
||
|
|
||
| class DeploymentType(KebabCaseStrEnum): | ||
| BASE = auto() | ||
| FULL = auto() | ||
|
|
@@ -159,6 +164,8 @@ def validate_query_engine_package_compatibility(self): | |
|
|
||
|
|
||
| class Database(BaseModel): | ||
| model_config = ConfigDict(extra="allow") | ||
|
|
||
| DEFAULT_PORT: ClassVar[int] = 3306 | ||
|
|
||
| type: DatabaseEngineStr = DatabaseEngine.MARIADB | ||
|
|
@@ -169,8 +176,43 @@ class Database(BaseModel): | |
| auto_commit: bool = False | ||
| compress: bool = True | ||
|
|
||
| username: Optional[NonEmptyStr] = None | ||
| password: Optional[NonEmptyStr] = None | ||
| _username: Optional[NonEmptyStr] = PrivateAttr(default=None) | ||
| _password: Optional[NonEmptyStr] = PrivateAttr(default=None) | ||
|
|
||
| @property | ||
| def username(self) -> Optional[NonEmptyStr]: | ||
| return self._username | ||
|
|
||
| @username.setter | ||
| def username(self, value: Optional[NonEmptyStr]) -> None: | ||
| self._username = _optional_non_empty_str_validator.validate_python(value) | ||
|
|
||
| @property | ||
| def password(self) -> Optional[NonEmptyStr]: | ||
| return self._password | ||
|
|
||
| @password.setter | ||
| def password(self, value: Optional[NonEmptyStr]) -> None: | ||
| self._password = _optional_non_empty_str_validator.validate_python(value) | ||
|
|
||
| def model_post_init(self, __context: Any) -> None: | ||
| super().model_post_init(__context) | ||
| extra = getattr(self, "__pydantic_extra__", None) | ||
| if not extra: | ||
| return | ||
|
|
||
| if "username" in extra: | ||
| self.username = extra.pop("username") | ||
| if "password" in extra: | ||
| self.password = extra.pop("password") | ||
|
|
||
| # Maintain previous "extra='ignore'" semantics for any other unexpected keys. | ||
| for key in list(extra.keys()): | ||
| if key not in {"username", "password"}: | ||
| extra.pop(key) | ||
|
|
||
| if not extra: | ||
| self.__pydantic_extra__ = None | ||
|
|
||
| def ensure_credentials_loaded(self): | ||
| if self.username is None or self.password is None: | ||
|
|
@@ -220,10 +262,6 @@ def get_clp_connection_params_and_type(self, disable_localhost_socket_connection | |
| connection_params_and_type["ssl_cert"] = self.ssl_cert | ||
| return connection_params_and_type | ||
|
|
||
| def dump_to_primitive_dict(self): | ||
| d = self.model_dump(exclude={"username", "password"}) | ||
| return d | ||
|
|
||
| def load_credentials_from_file(self, credentials_file_path: pathlib.Path): | ||
| config = read_yaml_config_file(credentials_file_path) | ||
| if config is None: | ||
|
|
@@ -276,17 +314,40 @@ class QueryWorker(BaseModel): | |
|
|
||
|
|
||
| class Redis(BaseModel): | ||
| model_config = ConfigDict(extra="allow") | ||
|
|
||
| DEFAULT_PORT: ClassVar[int] = 6379 | ||
|
|
||
| host: DomainStr = "localhost" | ||
| port: Port = DEFAULT_PORT | ||
| query_backend_database: int = 0 | ||
| compression_backend_database: int = 1 | ||
| # redis can perform authentication without a username | ||
| password: Optional[str] = None | ||
| _password: Optional[str] = PrivateAttr(default=None) | ||
|
|
||
| @property | ||
| def password(self) -> Optional[str]: | ||
| return self._password | ||
|
|
||
| def dump_to_primitive_dict(self): | ||
| return self.model_dump(exclude={"password"}) | ||
| @password.setter | ||
| def password(self, value: Optional[str]) -> None: | ||
| self._password = _optional_str_validator.validate_python(value) | ||
|
|
||
| def model_post_init(self, __context: Any) -> None: | ||
| super().model_post_init(__context) | ||
| extra = getattr(self, "__pydantic_extra__", None) | ||
| if not extra: | ||
| return | ||
|
|
||
| if "password" in extra: | ||
| self.password = extra.pop("password") | ||
|
|
||
| for key in list(extra.keys()): | ||
| if key != "password": | ||
| extra.pop(key) | ||
|
|
||
| if not extra: | ||
| self.__pydantic_extra__ = None | ||
|
|
||
| def load_credentials_from_file(self, credentials_file_path: pathlib.Path): | ||
| config = read_yaml_config_file(credentials_file_path) | ||
|
|
@@ -341,16 +402,49 @@ def transform_for_container(self): | |
|
|
||
|
|
||
| class Queue(BaseModel): | ||
| model_config = ConfigDict(extra="allow") | ||
|
|
||
| DEFAULT_PORT: ClassVar[int] = 5672 | ||
|
|
||
| host: DomainStr = "localhost" | ||
| port: Port = DEFAULT_PORT | ||
|
|
||
| username: Optional[NonEmptyStr] = None | ||
| password: Optional[str] = None | ||
| _username: Optional[NonEmptyStr] = PrivateAttr(default=None) | ||
| _password: Optional[str] = PrivateAttr(default=None) | ||
|
|
||
| @property | ||
| def username(self) -> Optional[NonEmptyStr]: | ||
| return self._username | ||
|
|
||
| @username.setter | ||
| def username(self, value: Optional[NonEmptyStr]) -> None: | ||
| self._username = _optional_non_empty_str_validator.validate_python(value) | ||
|
|
||
| @property | ||
| def password(self) -> Optional[str]: | ||
| return self._password | ||
|
|
||
| @password.setter | ||
| def password(self, value: Optional[str]) -> None: | ||
| self._password = _optional_str_validator.validate_python(value) | ||
|
|
||
| def dump_to_primitive_dict(self): | ||
| return self.model_dump(exclude={"username", "password"}) | ||
| def model_post_init(self, __context: Any) -> None: | ||
| super().model_post_init(__context) | ||
| extra = getattr(self, "__pydantic_extra__", None) | ||
| if not extra: | ||
| return | ||
|
|
||
| if "username" in extra: | ||
| self.username = extra.pop("username") | ||
| if "password" in extra: | ||
| self.password = extra.pop("password") | ||
|
|
||
| for key in list(extra.keys()): | ||
| if key not in {"username", "password"}: | ||
| extra.pop(key) | ||
|
|
||
| if not extra: | ||
| self.__pydantic_extra__ = None | ||
|
|
||
|
Comment on lines
404
to
448
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainVerify Queue password validation and ensure consistency. The Queue class uses Verify whether Queue passwords should be allowed to be empty:
The same concerns about 🏁 Script executed: #!/bin/bash
# Description: Check Queue password validation requirements
# Find Queue password usage and validation
echo "=== Queue password usage ==="
rg -n -C3 'queue\.(password|_password)' --type py
# Check credentials file generation for queue password
echo -e "\n=== Credentials file generation ==="
rg -n -A5 -B5 'QUEUE_COMPONENT_NAME.*password' --type pyLength of output: 4524 🏁 Script executed: #!/bin/bash
# Check get_config_value implementation and Database class for comparison
echo "=== get_config_value implementation ==="
rg -n -A10 'def get_config_value' --type py
echo -e "\n=== Database class password definition ==="
rg -n -B5 -A15 'class Database' --type py | head -60
echo -e "\n=== Check for password fallbacks or defaults ==="
rg -n 'password.*=.*get_config_value|password.*default' --type py | head -20Length of output: 5328 🏁 Script executed: #!/bin/bash
# Check for any special handling of empty passwords and Queue auth configuration
echo "=== Check Queue authentication mode/behavior ==="
rg -n 'Queue|queue.*auth' --type py components/clp-py-utils/clp_py_utils/clp_config.py | head -20
echo -e "\n=== Check Redis password usage and comparison ==="
rg -n -B3 -A3 'class Redis' --type py components/clp-py-utils/clp_py_utils/clp_config.py
echo -e "\n=== Check if empty passwords are tested or validated ==="
rg -n 'password.*""|\\.password.*None' --type py | grep -i 'test\|default' | head -10
echo -e "\n=== Check credentials file schema/documentation ==="
rg -n 'QUEUE_COMPONENT_NAME|queue.*user.*password' --type py | head -20Length of output: 1784 🏁 Script executed: #!/bin/bash
# Final checks: Look for any AMQP/RabbitMQ setup that might explain Optional[str]
echo "=== Check for RabbitMQ guest/no-auth mode or defaults ==="
rg -n -i 'rabbitmq|guest|noauth|auth.*disable' --type py
echo -e "\n=== Check Redis auth configuration or mode ==="
rg -n -B5 -A5 'class Redis' --type py
echo -e "\n=== Check if Queue/Redis passwords are ever None or empty in practice ==="
rg -n 'password\s*=\s*None|password\s*==\s*""' --type py
echo -e "\n=== Verify if Database pattern is intentional by checking comments ==="
rg -n -B3 'NonEmptyStr.*password|password.*NonEmptyStr' --type pyLength of output: 2155 Align Queue password validation with Database for consistency and correctness. The Queue class uses
Change line 413 from 🤖 Prompt for AI Agents |
||
| def load_credentials_from_file(self, credentials_file_path: pathlib.Path): | ||
| config = read_yaml_config_file(credentials_file_path) | ||
|
|
@@ -426,9 +520,6 @@ class S3IngestionConfig(BaseModel): | |
| type: Literal[StorageType.S3.value] = StorageType.S3.value | ||
| aws_authentication: AwsAuthentication | ||
|
|
||
| def dump_to_primitive_dict(self): | ||
| return self.model_dump() | ||
|
|
||
| def transform_for_container(self): | ||
| pass | ||
|
|
||
|
|
@@ -773,18 +864,6 @@ def get_deployment_type(self) -> DeploymentType: | |
| else: | ||
| return DeploymentType.FULL | ||
|
|
||
| def dump_to_primitive_dict(self): | ||
| custom_serialized_fields = { | ||
| "database", | ||
| "queue", | ||
| "redis", | ||
| } | ||
| d = self.model_dump(exclude=custom_serialized_fields) | ||
| for key in custom_serialized_fields: | ||
| d[key] = getattr(self, key).dump_to_primitive_dict() | ||
|
|
||
| return d | ||
|
|
||
| @model_validator(mode="after") | ||
| def validate_presto_config(self): | ||
| query_engine = self.package.query_engine | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify Redis password validation requirements.
The Redis class follows the same pattern as Database but uses
Optional[str]for password (line 326) instead ofOptional[NonEmptyStr]. This allows empty passwords.Verify whether Redis should accept empty passwords in your deployment:
Consider using
Optional[NonEmptyStr]if empty passwords should not be allowed, or add documentation explaining why empty passwords are permitted.The same concerns about
__pydantic_extra__manipulation from the Database class apply here. Consider the refactoring suggestions from the Database review.🏁 Script executed:
Length of output: 2173
🏁 Script executed:
Length of output: 329
🏁 Script executed:
Length of output: 3398
Use
Optional[NonEmptyStr]for Redis password to match Database class validation.The Redis class uses
Optional[str]for password validation (line 326), while the Database class usesOptional[NonEmptyStr](line 181). This inconsistency allows Redis to accept empty passwords.Based on usage at line 735 in
general.py(Celery RESULT_BACKEND connection string), empty passwords create configuration ambiguity. Either:Optional[NonEmptyStr]with_optional_non_empty_str_validatorfor consistency with Database🤖 Prompt for AI Agents