mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-01 15:22:37 +00:00
chore: modernize security module typing (#3469)
- Disable E501, apply Ruff formatting - Update typing (Self, BeforeValidator), remove dead code - Convert Fingerprint to Pydantic dataclass and fix serialization/copy behavior - Add TODO for dynamic namespace config
This commit is contained in:
15
src/crewai/security/constants.py
Normal file
15
src/crewai/security/constants.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""Security constants for CrewAI.
|
||||||
|
|
||||||
|
This module contains security-related constants used throughout the security module.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- TODO: Determine if CREW_AI_NAMESPACE should be made dynamic or configurable
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
CREW_AI_NAMESPACE: Annotated[
|
||||||
|
UUID,
|
||||||
|
"Create a deterministic UUID using v5 (SHA-1). Custom namespace for CrewAI to enhance security.",
|
||||||
|
] = UUID("f47ac10b-58cc-4372-a567-0e02b2c3d479")
|
||||||
@@ -1,130 +1,123 @@
|
|||||||
"""
|
"""Fingerprint Module
|
||||||
Fingerprint Module
|
|
||||||
|
|
||||||
This module provides functionality for generating and validating unique identifiers
|
This module provides functionality for generating and validating unique identifiers
|
||||||
for CrewAI agents. These identifiers are used for tracking, auditing, and security.
|
for CrewAI agents. These identifiers are used for tracking, auditing, and security.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, Optional
|
from typing import Annotated, Any
|
||||||
|
from uuid import UUID, uuid4, uuid5
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import BaseModel, BeforeValidator, Field, PrivateAttr
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from crewai.security.constants import CREW_AI_NAMESPACE
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_metadata(v: Any) -> dict[str, Any]:
|
||||||
|
"""Validate that metadata is a dictionary with string keys and valid values."""
|
||||||
|
if not isinstance(v, dict):
|
||||||
|
raise ValueError("Metadata must be a dictionary")
|
||||||
|
|
||||||
|
# Validate that all keys are strings
|
||||||
|
for key, value in v.items():
|
||||||
|
if not isinstance(key, str):
|
||||||
|
raise ValueError(f"Metadata keys must be strings, got {type(key)}")
|
||||||
|
|
||||||
|
# Validate nested dictionaries (prevent deeply nested structures)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
# Check for nested dictionaries (limit depth to 1)
|
||||||
|
for nested_key, nested_value in value.items():
|
||||||
|
if not isinstance(nested_key, str):
|
||||||
|
raise ValueError(
|
||||||
|
f"Nested metadata keys must be strings, got {type(nested_key)}"
|
||||||
|
)
|
||||||
|
if isinstance(nested_value, dict):
|
||||||
|
raise ValueError("Metadata can only be nested one level deep")
|
||||||
|
|
||||||
|
# Check for maximum metadata size (prevent DoS)
|
||||||
|
if len(str(v)) > 10_000: # Limit metadata size to 10KB
|
||||||
|
raise ValueError("Metadata size exceeds maximum allowed (10KB)")
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
class Fingerprint(BaseModel):
|
class Fingerprint(BaseModel):
|
||||||
"""
|
"""A class for generating and managing unique identifiers for agents.
|
||||||
A class for generating and managing unique identifiers for agents.
|
|
||||||
|
|
||||||
Each agent has dual identifiers:
|
Each agent has dual identifiers:
|
||||||
- Human-readable ID: For debugging and reference (derived from role if not specified)
|
- Human-readable ID: For debugging and reference (derived from role if not specified)
|
||||||
- Fingerprint UUID: Unique runtime identifier for tracking and auditing
|
- Fingerprint UUID: Unique runtime identifier for tracking and auditing
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
uuid_str (str): String representation of the UUID for this fingerprint, auto-generated
|
uuid_str: String representation of the UUID for this fingerprint, auto-generated
|
||||||
created_at (datetime): When this fingerprint was created, auto-generated
|
created_at: When this fingerprint was created, auto-generated
|
||||||
metadata (Dict[str, Any]): Additional metadata associated with this fingerprint
|
metadata: Additional metadata associated with this fingerprint
|
||||||
"""
|
"""
|
||||||
|
|
||||||
uuid_str: str = Field(default_factory=lambda: str(uuid.uuid4()), description="String representation of the UUID")
|
_uuid_str: str = PrivateAttr(default_factory=lambda: str(uuid4()))
|
||||||
created_at: datetime = Field(default_factory=datetime.now, description="When this fingerprint was created")
|
_created_at: datetime = PrivateAttr(default_factory=datetime.now)
|
||||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata for this fingerprint")
|
metadata: Annotated[dict[str, Any], BeforeValidator(_validate_metadata)] = Field(
|
||||||
|
default_factory=dict
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
)
|
||||||
|
|
||||||
@field_validator('metadata')
|
|
||||||
@classmethod
|
|
||||||
def validate_metadata(cls, v):
|
|
||||||
"""Validate that metadata is a dictionary with string keys and valid values."""
|
|
||||||
if not isinstance(v, dict):
|
|
||||||
raise ValueError("Metadata must be a dictionary")
|
|
||||||
|
|
||||||
# Validate that all keys are strings
|
|
||||||
for key, value in v.items():
|
|
||||||
if not isinstance(key, str):
|
|
||||||
raise ValueError(f"Metadata keys must be strings, got {type(key)}")
|
|
||||||
|
|
||||||
# Validate nested dictionaries (prevent deeply nested structures)
|
|
||||||
if isinstance(value, dict):
|
|
||||||
# Check for nested dictionaries (limit depth to 1)
|
|
||||||
for nested_key, nested_value in value.items():
|
|
||||||
if not isinstance(nested_key, str):
|
|
||||||
raise ValueError(f"Nested metadata keys must be strings, got {type(nested_key)}")
|
|
||||||
if isinstance(nested_value, dict):
|
|
||||||
raise ValueError("Metadata can only be nested one level deep")
|
|
||||||
|
|
||||||
# Check for maximum metadata size (prevent DoS)
|
|
||||||
if len(str(v)) > 10000: # Limit metadata size to 10KB
|
|
||||||
raise ValueError("Metadata size exceeds maximum allowed (10KB)")
|
|
||||||
|
|
||||||
return v
|
|
||||||
|
|
||||||
def __init__(self, **data):
|
|
||||||
"""Initialize a Fingerprint with auto-generated uuid_str and created_at."""
|
|
||||||
# Remove uuid_str and created_at from data to ensure they're auto-generated
|
|
||||||
if 'uuid_str' in data:
|
|
||||||
data.pop('uuid_str')
|
|
||||||
if 'created_at' in data:
|
|
||||||
data.pop('created_at')
|
|
||||||
|
|
||||||
# Call the parent constructor with the modified data
|
|
||||||
super().__init__(**data)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self) -> uuid.UUID:
|
def uuid_str(self) -> str:
|
||||||
|
"""Get the string representation of the UUID for this fingerprint."""
|
||||||
|
return self._uuid_str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def created_at(self) -> datetime:
|
||||||
|
"""Get the creation timestamp for this fingerprint."""
|
||||||
|
return self._created_at
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uuid(self) -> UUID:
|
||||||
"""Get the UUID object for this fingerprint."""
|
"""Get the UUID object for this fingerprint."""
|
||||||
return uuid.UUID(self.uuid_str)
|
return UUID(self.uuid_str)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _generate_uuid(cls, seed: str) -> str:
|
def _generate_uuid(cls, seed: str) -> str:
|
||||||
"""
|
"""Generate a deterministic UUID based on a seed string.
|
||||||
Generate a deterministic UUID based on a seed string.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
seed (str): The seed string to use for UUID generation
|
seed: The seed string to use for UUID generation
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: A string representation of the UUID consistently generated from the seed
|
A string representation of the UUID consistently generated from the seed
|
||||||
"""
|
"""
|
||||||
if not isinstance(seed, str):
|
|
||||||
raise ValueError("Seed must be a string")
|
|
||||||
|
|
||||||
if not seed.strip():
|
if not seed.strip():
|
||||||
raise ValueError("Seed cannot be empty or whitespace")
|
raise ValueError("Seed cannot be empty or whitespace")
|
||||||
|
|
||||||
# Create a deterministic UUID using v5 (SHA-1)
|
|
||||||
# Custom namespace for CrewAI to enhance security
|
|
||||||
|
|
||||||
# Using a unique namespace specific to CrewAI to reduce collision risks
|
return str(uuid5(CREW_AI_NAMESPACE, seed))
|
||||||
CREW_AI_NAMESPACE = uuid.UUID('f47ac10b-58cc-4372-a567-0e02b2c3d479')
|
|
||||||
return str(uuid.uuid5(CREW_AI_NAMESPACE, seed))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate(cls, seed: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> 'Fingerprint':
|
def generate(
|
||||||
"""
|
cls, seed: str | None = None, metadata: dict[str, Any] | None = None
|
||||||
Static factory method to create a new Fingerprint.
|
) -> Self:
|
||||||
|
"""Static factory method to create a new Fingerprint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
seed (Optional[str]): A string to use as seed for the UUID generation.
|
seed: A string to use as seed for the UUID generation.
|
||||||
If None, a random UUID is generated.
|
If None, a random UUID is generated.
|
||||||
metadata (Optional[Dict[str, Any]]): Additional metadata to store with the fingerprint.
|
metadata: Additional metadata to store with the fingerprint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Fingerprint: A new Fingerprint instance
|
A new Fingerprint instance
|
||||||
"""
|
"""
|
||||||
fingerprint = cls(metadata=metadata or {})
|
fingerprint = cls(metadata=metadata or {})
|
||||||
if seed:
|
if seed:
|
||||||
# For seed-based generation, we need to manually set the uuid_str after creation
|
# For seed-based generation, we need to manually set the _uuid_str after creation
|
||||||
object.__setattr__(fingerprint, 'uuid_str', cls._generate_uuid(seed))
|
fingerprint.__dict__["_uuid_str"] = cls._generate_uuid(seed)
|
||||||
return fingerprint
|
return fingerprint
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""String representation of the fingerprint (the UUID)."""
|
"""String representation of the fingerprint (the UUID)."""
|
||||||
return self.uuid_str
|
return self.uuid_str
|
||||||
|
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
"""Compare fingerprints by their UUID."""
|
"""Compare fingerprints by their UUID."""
|
||||||
if isinstance(other, Fingerprint):
|
if type(other) is Fingerprint:
|
||||||
return self.uuid_str == other.uuid_str
|
return self.uuid_str == other.uuid_str
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -132,29 +125,27 @@ class Fingerprint(BaseModel):
|
|||||||
"""Hash of the fingerprint (based on UUID)."""
|
"""Hash of the fingerprint (based on UUID)."""
|
||||||
return hash(self.uuid_str)
|
return hash(self.uuid_str)
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
"""
|
"""Convert the fingerprint to a dictionary representation.
|
||||||
Convert the fingerprint to a dictionary representation.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: Dictionary representation of the fingerprint
|
Dictionary representation of the fingerprint
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"uuid_str": self.uuid_str,
|
"uuid_str": self.uuid_str,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"metadata": self.metadata
|
"metadata": self.metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, Any]) -> 'Fingerprint':
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
||||||
"""
|
"""Create a Fingerprint from a dictionary representation.
|
||||||
Create a Fingerprint from a dictionary representation.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (Dict[str, Any]): Dictionary representation of a fingerprint
|
data: Dictionary representation of a fingerprint
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Fingerprint: A new Fingerprint instance
|
A new Fingerprint instance
|
||||||
"""
|
"""
|
||||||
if not data:
|
if not data:
|
||||||
return cls()
|
return cls()
|
||||||
@@ -163,8 +154,10 @@ class Fingerprint(BaseModel):
|
|||||||
|
|
||||||
# For consistency with existing stored fingerprints, we need to manually set these
|
# For consistency with existing stored fingerprints, we need to manually set these
|
||||||
if "uuid_str" in data:
|
if "uuid_str" in data:
|
||||||
object.__setattr__(fingerprint, 'uuid_str', data["uuid_str"])
|
fingerprint.__dict__["_uuid_str"] = data["uuid_str"]
|
||||||
if "created_at" in data and isinstance(data["created_at"], str):
|
if "created_at" in data and isinstance(data["created_at"], str):
|
||||||
object.__setattr__(fingerprint, 'created_at', datetime.fromisoformat(data["created_at"]))
|
fingerprint.__dict__["_created_at"] = datetime.fromisoformat(
|
||||||
|
data["created_at"]
|
||||||
|
)
|
||||||
|
|
||||||
return fingerprint
|
return fingerprint
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""
|
"""Security Configuration Module
|
||||||
Security Configuration Module
|
|
||||||
|
|
||||||
This module provides configuration for CrewAI security features, including:
|
This module provides configuration for CrewAI security features, including:
|
||||||
- Authentication settings
|
- Authentication settings
|
||||||
@@ -10,9 +9,10 @@ The SecurityConfig class is the primary interface for managing security settings
|
|||||||
in CrewAI applications.
|
in CrewAI applications.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
from crewai.security.fingerprint import Fingerprint
|
from crewai.security.fingerprint import Fingerprint
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@ class SecurityConfig(BaseModel):
|
|||||||
- Impersonation/delegation tokens *TODO*
|
- Impersonation/delegation tokens *TODO*
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
version (str): Version of the security configuration
|
|
||||||
fingerprint (Fingerprint): The unique fingerprint automatically generated for the component
|
fingerprint (Fingerprint): The unique fingerprint automatically generated for the component
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -37,80 +36,52 @@ class SecurityConfig(BaseModel):
|
|||||||
# Note: Cannot use frozen=True as existing tests modify the fingerprint property
|
# Note: Cannot use frozen=True as existing tests modify the fingerprint property
|
||||||
)
|
)
|
||||||
|
|
||||||
version: str = Field(
|
|
||||||
default="1.0.0",
|
|
||||||
description="Version of the security configuration"
|
|
||||||
)
|
|
||||||
|
|
||||||
fingerprint: Fingerprint = Field(
|
fingerprint: Fingerprint = Field(
|
||||||
default_factory=Fingerprint,
|
default_factory=Fingerprint, description="Unique identifier for the component"
|
||||||
description="Unique identifier for the component"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_compatible(self, min_version: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if this security configuration is compatible with the minimum required version.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
min_version (str): Minimum required version in semver format (e.g., "1.0.0")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if this configuration is compatible, False otherwise
|
|
||||||
"""
|
|
||||||
# Simple version comparison (can be enhanced with packaging.version if needed)
|
|
||||||
current = [int(x) for x in self.version.split(".")]
|
|
||||||
minimum = [int(x) for x in min_version.split(".")]
|
|
||||||
|
|
||||||
# Compare major, minor, patch versions
|
|
||||||
for c, m in zip(current, minimum):
|
|
||||||
if c > m:
|
|
||||||
return True
|
|
||||||
if c < m:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@model_validator(mode='before')
|
@field_validator("fingerprint", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_fingerprint(cls, values):
|
def validate_fingerprint(cls, v: Any) -> Fingerprint:
|
||||||
"""Ensure fingerprint is properly initialized."""
|
"""Ensure fingerprint is properly initialized."""
|
||||||
if isinstance(values, dict):
|
if v is None:
|
||||||
# Handle case where fingerprint is not provided or is None
|
return Fingerprint()
|
||||||
if 'fingerprint' not in values or values['fingerprint'] is None:
|
if isinstance(v, str):
|
||||||
values['fingerprint'] = Fingerprint()
|
if not v.strip():
|
||||||
# Handle case where fingerprint is a string (seed)
|
raise ValueError("Fingerprint seed cannot be empty")
|
||||||
elif isinstance(values['fingerprint'], str):
|
return Fingerprint.generate(seed=v)
|
||||||
if not values['fingerprint'].strip():
|
if isinstance(v, dict):
|
||||||
raise ValueError("Fingerprint seed cannot be empty")
|
return Fingerprint.from_dict(v)
|
||||||
values['fingerprint'] = Fingerprint.generate(seed=values['fingerprint'])
|
if isinstance(v, Fingerprint):
|
||||||
return values
|
return v
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
raise ValueError(f"Invalid fingerprint type: {type(v)}")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Convert the security config to a dictionary.
|
Convert the security config to a dictionary.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: Dictionary representation of the security config
|
Dictionary representation of the security config
|
||||||
"""
|
"""
|
||||||
result = {
|
return {"fingerprint": self.fingerprint.to_dict()}
|
||||||
"fingerprint": self.fingerprint.to_dict()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, Any]) -> 'SecurityConfig':
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
||||||
"""
|
"""
|
||||||
Create a SecurityConfig from a dictionary.
|
Create a SecurityConfig from a dictionary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (Dict[str, Any]): Dictionary representation of a security config
|
data: Dictionary representation of a security config
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
SecurityConfig: A new SecurityConfig instance
|
A new SecurityConfig instance
|
||||||
"""
|
"""
|
||||||
# Make a copy to avoid modifying the original
|
fingerprint_data = data.get("fingerprint")
|
||||||
data_copy = data.copy()
|
fingerprint = (
|
||||||
|
Fingerprint.from_dict(fingerprint_data)
|
||||||
fingerprint_data = data_copy.pop("fingerprint", None)
|
if fingerprint_data
|
||||||
fingerprint = Fingerprint.from_dict(fingerprint_data) if fingerprint_data else Fingerprint()
|
else Fingerprint()
|
||||||
|
)
|
||||||
|
|
||||||
return cls(fingerprint=fingerprint)
|
return cls(fingerprint=fingerprint)
|
||||||
|
|||||||
Reference in New Issue
Block a user