Coverage for src/app/secure_system.py: 100%
76 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 15:01 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 15:01 +0000
1"""
2Secure System Module.
4This module demonstrates a secure implementation of a user management service
5following strict typing and security guidelines.
6"""
8import threading
9from abc import ABC, abstractmethod
10from uuid import UUID, uuid4
12from pydantic import EmailStr, Field, SecretStr
13from pydantic.networks import IPvAnyAddress
15from taipanstack.core.result import Err, Ok, Result
16from taipanstack.security import SecureBaseModel, hash_password
17from taipanstack.utils.logging import get_logger
19# Configure logger
20logger = get_logger(__name__)
23class UserNotFoundError(Exception):
24 """Exception raised when a user is not found."""
26 def __init__(self, user_id: UUID) -> None:
27 """Initialize the exception with the user ID."""
28 self.user_id = user_id
29 super().__init__(f"User with ID {user_id} not found.")
32class UserAlreadyExistsError(Exception):
33 """Exception raised when a user already exists."""
35 def __init__(self, message: str) -> None:
36 """Initialize the exception with a message."""
37 self.message = message
38 super().__init__(message)
41class UserCreationError(Exception):
42 """Exception class for user creation errors."""
44 def __init__(self, message: str = "Failed to create user") -> None:
45 """Initialize the exception with a message."""
46 self.message = message
47 super().__init__(message)
50class UserCreate(SecureBaseModel):
51 """
52 Model for creating a new user.
54 Attributes:
55 username: The username of the user.
56 email: The email address of the user.
57 password: The password of the user (will be treated as a secret).
58 ip_address: The IP address from which the user is registering.
60 """
62 username: str = Field(..., min_length=3, max_length=50, pattern=r"^[a-zA-Z0-9_]+$")
63 email: EmailStr
64 password: SecretStr
65 ip_address: IPvAnyAddress | None = None
68class User(SecureBaseModel):
69 """
70 Model representing a registered user.
72 Attributes:
73 id: Unique identifier for the user.
74 username: The username of the user.
75 email: The email address of the user.
76 is_active: Whether the user account is active.
78 """
80 id: UUID
81 username: str
82 email: EmailStr
83 is_active: bool = True
86class UserInDB(User):
87 """
88 Model representing a registered user in the database.
90 Attributes:
91 password_hash: The hashed password of the user.
93 """
95 password_hash: str
98class UserRepository(ABC):
99 """Abstract base class for user data access."""
101 @abstractmethod
102 def save(self, user: UserInDB) -> Result[None, UserAlreadyExistsError]:
103 """
104 Save a user to the repository.
106 Args:
107 user: The user to save.
109 """
111 @abstractmethod
112 def get_by_id(self, user_id: UUID) -> UserInDB | None:
113 """
114 Retrieve a user by their ID.
116 Args:
117 user_id: The UUID of the user.
119 Returns:
120 The UserInDB object if found, otherwise None.
122 """
125class InMemoryUserRepository(UserRepository):
126 """In-memory implementation of UserRepository."""
128 def __init__(self) -> None:
129 """Initialize the in-memory repository."""
130 self._storage: dict[UUID, UserInDB] = {}
131 self._lock = threading.Lock()
133 def save(self, user: UserInDB) -> Result[None, UserAlreadyExistsError]:
134 """
135 Save a user to the in-memory storage.
137 Args:
138 user: The user to save.
140 """
141 with self._lock:
142 if any(
143 (u.username == user.username or u.email == user.email)
144 and u.id != user.id
145 for u in self._storage.values()
146 ):
147 return Err(
148 UserAlreadyExistsError(f"User {user.username} already exists.")
149 )
150 self._storage[user.id] = user
151 return Ok(None)
153 def get_by_id(self, user_id: UUID) -> UserInDB | None:
154 """
155 Retrieve a user from the in-memory storage.
157 Args:
158 user_id: The UUID of the user.
160 Returns:
161 The UserInDB object if found, otherwise None.
163 """
164 with self._lock:
165 return self._storage.get(user_id)
168class UserService:
169 """Service for managing users securely."""
171 def __init__(self, user_repository: UserRepository) -> None:
172 """
173 Initialize the UserService with a repository.
175 Args:
176 user_repository: The repository to use for data access.
178 """
179 self._user_repository = user_repository
181 def create_user(self, user_create: UserCreate) -> Result[User, UserCreationError]:
182 """
183 Create a new user.
185 Args:
186 user_create: The user creation data.
188 Returns:
189 Ok(User) on success, Err(UserCreationError) on failure.
191 """
192 # Hash the password securely using the security module
193 try:
194 pwd_hash = hash_password(user_create.password)
195 except ValueError as e:
196 logger.warning(
197 "Failed to create user (invalid password)",
198 username=user_create.username,
199 )
200 return Err(UserCreationError(message=str(e)))
202 user_id = uuid4()
203 user_in_db = UserInDB(
204 id=user_id,
205 username=user_create.username,
206 email=user_create.email,
207 password_hash=pwd_hash,
208 )
209 save_result = self._user_repository.save(user_in_db)
210 match save_result:
211 case Ok():
212 logger.info("User created successfully", user_id=user_in_db.id)
213 # Return the public User model, excluding the password hash
214 public_user = User(
215 id=user_in_db.id,
216 username=user_in_db.username,
217 email=user_in_db.email,
218 is_active=user_in_db.is_active,
219 )
220 return Ok(public_user)
221 case Err(error):
222 logger.warning("Failed to create user", user_id=user_in_db.id)
223 return Err(UserCreationError(message=str(error)))
224 case _:
225 return Err(UserCreationError(message="Unknown save error")) # type: ignore[unreachable]
227 def get_user(self, user_id: UUID) -> Result[User, UserNotFoundError]:
228 """
229 Retrieve a user by ID using Result pattern.
231 Args:
232 user_id: The UUID of the user.
234 Returns:
235 Ok(User) if found, Err(UserNotFoundError) if not found.
237 Example:
238 >>> result = service.get_user(some_id)
239 >>> if isinstance(result, Ok):
240 ... print(f"Found: {result.unwrap().username}")
241 ... else:
242 ... print(f"Not found: {result.unwrap_err().user_id}")
244 """
245 user_in_db = self._user_repository.get_by_id(user_id)
246 if user_in_db is None:
247 logger.warning("User lookup failed", user_id=user_id)
248 return Err(UserNotFoundError(user_id))
250 public_user = User(
251 id=user_in_db.id,
252 username=user_in_db.username,
253 email=user_in_db.email,
254 is_active=user_in_db.is_active,
255 )
256 return Ok(public_user)