Skip to content

Commit d6a7cf0

Browse files
Copilotlstein
andcommitted
feat: add strict_password_checking config option to relax password requirements
- Add `strict_password_checking: bool = Field(default=False)` to InvokeAIAppConfig - Add `get_password_strength()` function to password_utils.py (returns weak/moderate/strong) - Add `strict_password_checking` field to SetupStatusResponse API endpoint - Update users_base.py and users_default.py to accept `strict_password_checking` param - Update auth.py router to pass config.strict_password_checking to all user service calls - Create shared frontend utility passwordUtils.ts for password strength validation - Update AdministratorSetup, UserProfile, UserManagement components to: - Fetch strict_password_checking from setup status endpoint - Show colored strength indicators (red/yellow/blue) in non-strict mode - Allow any non-empty password in non-strict mode - Maintain strict validation behavior when strict_password_checking=True - Update SetupStatusResponse type in auth.ts endpoint - Add passwordStrength and passwordHelperRelaxed translation keys to en.json - Add tests for new get_password_strength() function Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
1 parent 6fbb87b commit d6a7cf0

12 files changed

Lines changed: 3414 additions & 3192 deletions

File tree

invokeai/app/api/routers/auth.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class SetupStatusResponse(BaseModel):
7979

8080
setup_required: bool = Field(description="Whether initial setup is required")
8181
multiuser_enabled: bool = Field(description="Whether multiuser mode is enabled")
82+
strict_password_checking: bool = Field(description="Whether strict password requirements are enforced")
8283

8384

8485
@auth_router.get("/status", response_model=SetupStatusResponse)
@@ -92,13 +93,13 @@ async def get_setup_status() -> SetupStatusResponse:
9293

9394
# If multiuser is disabled, setup is never required
9495
if not config.multiuser:
95-
return SetupStatusResponse(setup_required=False, multiuser_enabled=False)
96+
return SetupStatusResponse(setup_required=False, multiuser_enabled=False, strict_password_checking=config.strict_password_checking)
9697

9798
# In multiuser mode, check if an admin exists
9899
user_service = ApiDependencies.invoker.services.users
99100
setup_required = not user_service.has_admin()
100101

101-
return SetupStatusResponse(setup_required=setup_required, multiuser_enabled=True)
102+
return SetupStatusResponse(setup_required=setup_required, multiuser_enabled=True, strict_password_checking=config.strict_password_checking)
102103

103104

104105
@auth_router.post("/login", response_model=LoginResponse)
@@ -248,7 +249,7 @@ async def setup_admin(
248249
password=request.password,
249250
is_admin=True,
250251
)
251-
user = user_service.create_admin(user_data)
252+
user = user_service.create_admin(user_data, strict_password_checking=config.strict_password_checking)
252253
except ValueError as e:
253254
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
254255

@@ -359,14 +360,15 @@ async def create_user(
359360
HTTPException: 400 if email already exists or password is weak
360361
"""
361362
user_service = ApiDependencies.invoker.services.users
363+
config = ApiDependencies.invoker.services.configuration
362364
try:
363365
user_data = UserCreateRequest(
364366
email=request.email,
365367
display_name=request.display_name,
366368
password=request.password,
367369
is_admin=request.is_admin,
368370
)
369-
return user_service.create(user_data)
371+
return user_service.create(user_data, strict_password_checking=config.strict_password_checking)
370372
except ValueError as e:
371373
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
372374

@@ -414,14 +416,15 @@ async def update_user(
414416
HTTPException: 404 if user not found
415417
"""
416418
user_service = ApiDependencies.invoker.services.users
419+
config = ApiDependencies.invoker.services.configuration
417420
try:
418421
changes = UserUpdateRequest(
419422
display_name=request.display_name,
420423
password=request.password,
421424
is_admin=request.is_admin,
422425
is_active=request.is_active,
423426
)
424-
return user_service.update(user_id, changes)
427+
return user_service.update(user_id, changes, strict_password_checking=config.strict_password_checking)
425428
except ValueError as e:
426429
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
427430

@@ -483,6 +486,7 @@ async def update_current_user(
483486
HTTPException: 404 if user not found
484487
"""
485488
user_service = ApiDependencies.invoker.services.users
489+
config = ApiDependencies.invoker.services.configuration
486490

487491
# Verify current password when attempting a password change
488492
if request.new_password is not None:
@@ -509,6 +513,6 @@ async def update_current_user(
509513
display_name=request.display_name,
510514
password=request.new_password,
511515
)
512-
return user_service.update(current_user.user_id, changes)
516+
return user_service.update(current_user.user_id, changes, strict_password_checking=config.strict_password_checking)
513517
except ValueError as e:
514518
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e

invokeai/app/services/auth/password_utils.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Password hashing and validation utilities."""
22

3-
from typing import cast
3+
from typing import Literal, cast
44

55
from passlib.context import CryptContext
66

@@ -84,3 +84,30 @@ def validate_password_strength(password: str) -> tuple[bool, str]:
8484
return False, "Password must contain uppercase, lowercase, and numbers"
8585

8686
return True, ""
87+
88+
89+
def get_password_strength(password: str) -> Literal["weak", "moderate", "strong"]:
90+
"""Determine the strength of a password.
91+
92+
Strength levels:
93+
- weak: less than 8 characters
94+
- moderate: 8+ characters but missing at least one of uppercase, lowercase, or digit
95+
- strong: 8+ characters with uppercase, lowercase, and digit
96+
97+
Args:
98+
password: The password to evaluate
99+
100+
Returns:
101+
One of "weak", "moderate", or "strong"
102+
"""
103+
if len(password) < 8:
104+
return "weak"
105+
106+
has_upper = any(c.isupper() for c in password)
107+
has_lower = any(c.islower() for c in password)
108+
has_digit = any(c.isdigit() for c in password)
109+
110+
if not (has_upper and has_lower and has_digit):
111+
return "moderate"
112+
113+
return "strong"

invokeai/app/services/config/config_default.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class InvokeAIAppConfig(BaseSettings):
111111
unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.
112112
allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.
113113
multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.
114+
strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength is reported to the user.
114115
"""
115116

116117
_root: Optional[Path] = PrivateAttr(default=None)
@@ -206,6 +207,7 @@ class InvokeAIAppConfig(BaseSettings):
206207

207208
# MULTIUSER
208209
multiuser: bool = Field(default=False, description="Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.")
210+
strict_password_checking: bool = Field(default=False, description="Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.")
209211

210212
# fmt: on
211213

invokeai/app/services/users/users_base.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ class UserServiceBase(ABC):
99
"""High-level service for user management."""
1010

1111
@abstractmethod
12-
def create(self, user_data: UserCreateRequest) -> UserDTO:
12+
def create(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
1313
"""Create a new user.
1414
1515
Args:
1616
user_data: User creation data
17+
strict_password_checking: If True (default), passwords must meet strength requirements.
18+
If False, any non-empty password is accepted.
1719
1820
Returns:
1921
The created user
2022
2123
Raises:
22-
ValueError: If email already exists or password is weak
24+
ValueError: If email already exists or (when strict) password is weak
2325
"""
2426
pass
2527

@@ -48,18 +50,20 @@ def get_by_email(self, email: str) -> UserDTO | None:
4850
pass
4951

5052
@abstractmethod
51-
def update(self, user_id: str, changes: UserUpdateRequest) -> UserDTO:
53+
def update(self, user_id: str, changes: UserUpdateRequest, strict_password_checking: bool = True) -> UserDTO:
5254
"""Update user.
5355
5456
Args:
5557
user_id: The user ID
5658
changes: Fields to update
59+
strict_password_checking: If True (default), passwords must meet strength requirements.
60+
If False, any non-empty password is accepted.
5761
5862
Returns:
5963
The updated user
6064
6165
Raises:
62-
ValueError: If user not found or password is weak
66+
ValueError: If user not found or (when strict) password is weak
6367
"""
6468
pass
6569

@@ -98,17 +102,19 @@ def has_admin(self) -> bool:
98102
pass
99103

100104
@abstractmethod
101-
def create_admin(self, user_data: UserCreateRequest) -> UserDTO:
105+
def create_admin(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
102106
"""Create an admin user (for initial setup).
103107
104108
Args:
105109
user_data: User creation data
110+
strict_password_checking: If True (default), passwords must meet strength requirements.
111+
If False, any non-empty password is accepted.
106112
107113
Returns:
108114
The created admin user
109115
110116
Raises:
111-
ValueError: If admin already exists or password is weak
117+
ValueError: If admin already exists or (when strict) password is weak
112118
"""
113119
pass
114120

invokeai/app/services/users/users_default.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ def __init__(self, db: SqliteDatabase):
2121
"""
2222
self._db = db
2323

24-
def create(self, user_data: UserCreateRequest) -> UserDTO:
24+
def create(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
2525
"""Create a new user."""
2626
# Validate password strength
27-
is_valid, error_msg = validate_password_strength(user_data.password)
28-
if not is_valid:
29-
raise ValueError(error_msg)
27+
if strict_password_checking:
28+
is_valid, error_msg = validate_password_strength(user_data.password)
29+
if not is_valid:
30+
raise ValueError(error_msg)
31+
elif not user_data.password:
32+
raise ValueError("Password cannot be empty")
3033

3134
# Check if email already exists
3235
if self.get_by_email(user_data.email) is not None:
@@ -106,7 +109,7 @@ def get_by_email(self, email: str) -> UserDTO | None:
106109
last_login_at=datetime.fromisoformat(row[7]) if row[7] else None,
107110
)
108111

109-
def update(self, user_id: str, changes: UserUpdateRequest) -> UserDTO:
112+
def update(self, user_id: str, changes: UserUpdateRequest, strict_password_checking: bool = True) -> UserDTO:
110113
"""Update user."""
111114
# Check if user exists
112115
user = self.get(user_id)
@@ -115,9 +118,12 @@ def update(self, user_id: str, changes: UserUpdateRequest) -> UserDTO:
115118

116119
# Validate password if provided
117120
if changes.password is not None:
118-
is_valid, error_msg = validate_password_strength(changes.password)
119-
if not is_valid:
120-
raise ValueError(error_msg)
121+
if strict_password_checking:
122+
is_valid, error_msg = validate_password_strength(changes.password)
123+
if not is_valid:
124+
raise ValueError(error_msg)
125+
elif not changes.password:
126+
raise ValueError("Password cannot be empty")
121127

122128
# Build update query dynamically based on provided fields
123129
updates: list[str] = []
@@ -208,7 +214,7 @@ def has_admin(self) -> bool:
208214
count = row[0] if row else 0
209215
return bool(count > 0)
210216

211-
def create_admin(self, user_data: UserCreateRequest) -> UserDTO:
217+
def create_admin(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
212218
"""Create an admin user (for initial setup)."""
213219
if self.has_admin():
214220
raise ValueError("Admin user already exists")
@@ -220,7 +226,7 @@ def create_admin(self, user_data: UserCreateRequest) -> UserDTO:
220226
password=user_data.password,
221227
is_admin=True,
222228
)
223-
return self.create(admin_data)
229+
return self.create(admin_data, strict_password_checking=strict_password_checking)
224230

225231
def list_users(self, limit: int = 100, offset: int = 0) -> list[UserDTO]:
226232
"""List all users."""

0 commit comments

Comments
 (0)