change_password
Change a user's password on a Webmin server. Provide username and new password. Blocked in safe mode to prevent unauthorized modifications.
Instructions
Change a user's password. This is a dangerous operation and is blocked in safe mode.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| server | No | Server alias (e.g., 'pi1', 'web-server'). Uses default server if not specified. | |
| username | Yes | Username of the user | |
| new_password | Yes | New password |
Implementation Reference
- src/tools/users.py:428-487 (handler)Main handler function for the change_password tool. Implements password change logic: checks safe mode, validates password, finds user via passwd::find_user, and changes password via passwd::change_password.
async def change_password( client: WebminClient, username: str, new_password: str, safe_mode: bool = True, ) -> ToolResult: """Change a user's password. This is a dangerous operation and is blocked in safe mode. Args: client: Authenticated WebminClient instance. username: Username of the user. new_password: New password. safe_mode: Whether safe mode is enabled. Returns: ToolResult with status. """ # Safety check if safe_mode: return ToolResult.fail( code="SAFETY_BLOCKED", message="Changing passwords is blocked in safe mode. " "Disable safe mode (WEBMIN_SAFE_MODE=false) to allow password changes.", ) # Validate password if not new_password or len(new_password) < 1: return ToolResult.fail( code="INVALID_PASSWORD", message="Password cannot be empty", ) try: # Use passwd::find_user which searches both local and LDAP backends user = await client.call("passwd", "find_user", username) if not user: return ToolResult.fail( code="USER_NOT_FOUND", message=f"User '{username}' not found", ) # Use passwd::change_password which properly encrypts the password, # updates shadow timestamps, handles file locking, runs pre/post hooks, # and propagates to other modules (Samba, MySQL, etc.) await client.call("passwd", "change_password", user, new_password, 1) return ToolResult.ok({ "action": "change_password", "success": True, "username": username, }) except Exception as e: return ToolResult.fail( code="CHANGE_PASSWORD_ERROR", message=f"Failed to change password for user '{username}': {e}", ) - src/server.py:557-578 (registration)MCP Tool registration for 'change_password'. Defines the input schema with username and new_password required fields.
Tool( name="change_password", description=( "Change a user's password. This is a dangerous operation and is " "blocked in safe mode." ), inputSchema={ "type": "object", "properties": { **SERVER_PARAM, "username": { "type": "string", "description": "Username of the user", }, "new_password": { "type": "string", "description": "New password", }, }, "required": ["username", "new_password"], }, ), - src/server.py:1549-1567 (registration)Dispatch logic in call_tool that maps the 'change_password' tool name to the users.change_password handler function.
if name == "change_password": username = arguments.get("username") new_password = arguments.get("new_password") if not username: return ToolResult.fail( code="MISSING_ARGUMENT", message="Missing required argument: username", ) if not new_password: return ToolResult.fail( code="MISSING_ARGUMENT", message="Missing required argument: new_password", ) return await users.change_password( client, username=username, new_password=new_password, safe_mode=config.safe_mode, ) - src/models.py:35-67 (helper)ToolResult model - the return type used by change_password to return success/failure responses.
class ToolResult(BaseModel): """Generic result wrapper for MCP tool responses.""" success: bool = Field( description="Whether the operation succeeded", ) data: dict[str, Any] | None = Field( default=None, description="Result data on success", ) error: WebminError | None = Field( default=None, description="Error details on failure", ) @classmethod def ok(cls, data: dict[str, Any]) -> "ToolResult": """Create a successful result.""" return cls(success=True, data=data) @classmethod def fail( cls, code: str, message: str, details: dict[str, Any] | None = None, ) -> "ToolResult": """Create a failure result.""" return cls( success=False, error=WebminError(code=code, message=message, details=details), ) - src/tools/users.py:14-37 (helper)Helper function _validate_username used by other user tools but not directly by change_password (which skips username validation).
def _validate_username(username: str) -> str | None: """Validate a username. Args: username: The username to validate. Returns: Error message if invalid, None if valid. """ if not username: return "Username cannot be empty" if len(username) > 32: return "Username must be 32 characters or less" # Linux username rules: lowercase letters, digits, underscore, hyphen # Must start with a letter or underscore if not re.match(r"^[a-z_][a-z0-9_-]*$", username): return ( "Username must start with a letter or underscore, and contain only " "lowercase letters, digits, underscores, and hyphens" ) return None