mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
feat: Two-Factor Authentication (TOTP, Email OTP) and OIDC/SSO – full implementation with admin UI (#933)
feat: Two-Factor Authentication (TOTP, Email OTP) and OIDC/SSO – full implementation with admin UI (#933)
This commit is contained in:
@@ -61,7 +61,7 @@ class TestAuthSetupAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "testadmin",
|
||||
"admin_password": "testpassword123",
|
||||
"admin_password": "TestPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -96,14 +96,14 @@ class TestAuthLoginAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "logintest",
|
||||
"admin_password": "loginpassword123",
|
||||
"admin_password": "LoginPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
# Now login
|
||||
response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "logintest", "password": "loginpassword123"},
|
||||
json={"username": "logintest", "password": "LoginPass1!"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -123,7 +123,7 @@ class TestAuthLoginAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "invalidtest",
|
||||
"admin_password": "correctpassword",
|
||||
"admin_password": "CorrectPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -158,13 +158,13 @@ class TestAuthMeAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "metest",
|
||||
"admin_password": "mepassword123",
|
||||
"admin_password": "MePass1!",
|
||||
},
|
||||
)
|
||||
|
||||
login_response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "metest", "password": "mepassword123"},
|
||||
json={"username": "metest", "password": "MePass1!"},
|
||||
)
|
||||
token = login_response.json()["access_token"]
|
||||
|
||||
@@ -254,13 +254,13 @@ class TestUsersAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "usersadmin",
|
||||
"admin_password": "adminpassword123",
|
||||
"admin_password": "AdminPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
login_response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "usersadmin", "password": "adminpassword123"},
|
||||
json={"username": "usersadmin", "password": "AdminPass1!"},
|
||||
)
|
||||
return login_response.json()["access_token"]
|
||||
|
||||
@@ -274,7 +274,7 @@ class TestUsersAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "authreqadmin",
|
||||
"admin_password": "adminpassword123",
|
||||
"admin_password": "AdminPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -306,7 +306,7 @@ class TestUsersAPI:
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"username": "newuser",
|
||||
"password": "newuserpassword",
|
||||
"password": "Newuserpass1!",
|
||||
"role": "user",
|
||||
},
|
||||
)
|
||||
@@ -327,7 +327,7 @@ class TestUsersAPI:
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"username": "duplicateuser",
|
||||
"password": "password123",
|
||||
"password": "Password123!",
|
||||
"role": "user",
|
||||
},
|
||||
)
|
||||
@@ -338,7 +338,7 @@ class TestUsersAPI:
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"username": "duplicateuser",
|
||||
"password": "password456",
|
||||
"password": "Password456!",
|
||||
"role": "user",
|
||||
},
|
||||
)
|
||||
@@ -356,7 +356,7 @@ class TestUsersAPI:
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"username": "updateuser",
|
||||
"password": "password123",
|
||||
"password": "Password123!",
|
||||
"role": "user",
|
||||
},
|
||||
)
|
||||
@@ -382,7 +382,7 @@ class TestUsersAPI:
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"username": "deleteuser",
|
||||
"password": "password123",
|
||||
"password": "Password123!",
|
||||
"role": "user",
|
||||
},
|
||||
)
|
||||
@@ -410,14 +410,14 @@ class TestAuthDisableAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "disableadmin",
|
||||
"admin_password": "adminpassword123",
|
||||
"admin_password": "AdminPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
# Login to get token
|
||||
login_response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "disableadmin", "password": "adminpassword123"},
|
||||
json={"username": "disableadmin", "password": "AdminPass1!"},
|
||||
)
|
||||
token = login_response.json()["access_token"]
|
||||
|
||||
@@ -446,13 +446,13 @@ class TestGroupsAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "groupsadmin",
|
||||
"admin_password": "adminpassword123",
|
||||
"admin_password": "AdminPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
login_response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "groupsadmin", "password": "adminpassword123"},
|
||||
json={"username": "groupsadmin", "password": "AdminPass1!"},
|
||||
)
|
||||
return login_response.json()["access_token"]
|
||||
|
||||
@@ -592,13 +592,13 @@ class TestUserGroupsAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "usergroupadmin",
|
||||
"admin_password": "adminpassword123",
|
||||
"admin_password": "AdminPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
login_response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "usergroupadmin", "password": "adminpassword123"},
|
||||
json={"username": "usergroupadmin", "password": "AdminPass1!"},
|
||||
)
|
||||
return login_response.json()["access_token"]
|
||||
|
||||
@@ -619,7 +619,7 @@ class TestUserGroupsAPI:
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"username": "groupuser",
|
||||
"password": "password123",
|
||||
"password": "Password123!",
|
||||
"group_ids": [operators_group["id"]],
|
||||
},
|
||||
)
|
||||
@@ -636,7 +636,7 @@ class TestUserGroupsAPI:
|
||||
user_response = await async_client.post(
|
||||
"/api/v1/users/",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={"username": "addtogroup", "password": "password123"},
|
||||
json={"username": "addtogroup", "password": "Password123!"},
|
||||
)
|
||||
user_id = user_response.json()["id"]
|
||||
|
||||
@@ -675,13 +675,13 @@ class TestChangePasswordAPI:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "pwchangeadmin",
|
||||
"admin_password": "adminpassword123",
|
||||
"admin_password": "AdminPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
admin_login = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "pwchangeadmin", "password": "adminpassword123"},
|
||||
json={"username": "pwchangeadmin", "password": "AdminPass1!"},
|
||||
)
|
||||
admin_token = admin_login.json()["access_token"]
|
||||
|
||||
@@ -689,13 +689,13 @@ class TestChangePasswordAPI:
|
||||
await async_client.post(
|
||||
"/api/v1/users/",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"username": "pwchangeuser", "password": "oldpassword123"},
|
||||
json={"username": "pwchangeuser", "password": "Oldpassword123!"},
|
||||
)
|
||||
|
||||
# Login as regular user
|
||||
user_login = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "pwchangeuser", "password": "oldpassword123"},
|
||||
json={"username": "pwchangeuser", "password": "Oldpassword123!"},
|
||||
)
|
||||
return user_login.json()["access_token"]
|
||||
|
||||
@@ -707,8 +707,8 @@ class TestChangePasswordAPI:
|
||||
"/api/v1/users/me/change-password",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
json={
|
||||
"current_password": "oldpassword123",
|
||||
"new_password": "newpassword456",
|
||||
"current_password": "Oldpassword123!",
|
||||
"new_password": "Newpassword456!",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -718,7 +718,7 @@ class TestChangePasswordAPI:
|
||||
# Verify can login with new password
|
||||
login_response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "pwchangeuser", "password": "newpassword456"},
|
||||
json={"username": "pwchangeuser", "password": "Newpassword456!"},
|
||||
)
|
||||
assert login_response.status_code == 200
|
||||
|
||||
@@ -731,7 +731,7 @@ class TestChangePasswordAPI:
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
json={
|
||||
"current_password": "wrongpassword",
|
||||
"new_password": "newpassword456",
|
||||
"new_password": "Newpassword456!",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -746,7 +746,7 @@ class TestChangePasswordAPI:
|
||||
"/api/v1/users/me/change-password",
|
||||
json={
|
||||
"current_password": "oldpassword",
|
||||
"new_password": "newpassword",
|
||||
"new_password": "Strongpass456!",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -768,7 +768,7 @@ class TestAuthMiddlewarePublicRoutes:
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "middlewareadmin",
|
||||
"admin_password": "adminpassword123",
|
||||
"admin_password": "AdminPass1!",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -786,7 +786,7 @@ class TestAuthMiddlewarePublicRoutes:
|
||||
"""Verify /api/v1/auth/login is accessible without auth."""
|
||||
response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "middlewareadmin", "password": "adminpassword123"},
|
||||
json={"username": "middlewareadmin", "password": "AdminPass1!"},
|
||||
)
|
||||
# Should not return 401 (unauthorized) - it should either succeed or return
|
||||
# a different error (like 400 for wrong credentials)
|
||||
@@ -826,7 +826,7 @@ class TestAuthMiddlewarePublicRoutes:
|
||||
# Login to get token
|
||||
login_response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "middlewareadmin", "password": "adminpassword123"},
|
||||
json={"username": "middlewareadmin", "password": "AdminPass1!"},
|
||||
)
|
||||
token = login_response.json()["access_token"]
|
||||
|
||||
@@ -863,3 +863,57 @@ class TestAuthMiddlewarePublicRoutes:
|
||||
# Will likely be 400 (advanced auth not enabled) but that's okay -
|
||||
# the important thing is it's not blocked by auth middleware
|
||||
assert response.status_code in [200, 400]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# H-1: Input length validation
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestInputLengthValidation:
|
||||
"""LoginRequest and SetupRequest must reject oversized inputs (H-1)."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_login_password_too_long_rejected(self, async_client: AsyncClient):
|
||||
"""Password exceeding 256 characters must be rejected with 422."""
|
||||
response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "admin", "password": "x" * 257},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_login_username_too_long_rejected(self, async_client: AsyncClient):
|
||||
"""Username exceeding 150 characters must be rejected with 422."""
|
||||
response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "u" * 151, "password": "password"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_setup_password_too_long_rejected(self, async_client: AsyncClient):
|
||||
"""SetupRequest admin_password exceeding 256 characters must be rejected with 422."""
|
||||
response = await async_client.post(
|
||||
"/api/v1/auth/setup",
|
||||
json={
|
||||
"auth_enabled": True,
|
||||
"admin_username": "admin",
|
||||
"admin_password": "x" * 257,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_login_password_at_limit_accepted(self, async_client: AsyncClient):
|
||||
"""Password of exactly 256 characters must pass schema validation (may fail auth)."""
|
||||
response = await async_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "admin", "password": "x" * 256},
|
||||
)
|
||||
# Schema accepts it; auth may reject with 401 (auth disabled) or 400
|
||||
assert response.status_code != 422
|
||||
|
||||
Reference in New Issue
Block a user