feat(auth): add OIDC_ALLOW_REGISTRATION toggle

Gate automatic account creation on OIDC login behind a new OIDC_ALLOW_REGISTRATION environment variable. Defaults to true, preserving the current auto-provisioning behavior; set it to false to run OIDC in an "existing users only" mode, where a login from an email without an existing RomM account is rejected with a 403 instead of silently creating one. Existing users are unaffected either way.

Adds the config constant, env.template and docs entries, and tests covering the enabled/disabled and existing-user paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Spinnich
2026-06-20 19:03:15 +00:00
parent 28e2ebe780
commit 11a81c756f
5 changed files with 105 additions and 0 deletions

View File

@@ -23,6 +23,16 @@ def mock_oidc_enabled(mocker):
mocker.patch("handler.auth.base_handler.OIDC_ENABLED", True)
@pytest.fixture
def mock_oidc_allow_registration_enabled(mocker):
mocker.patch("handler.auth.base_handler.OIDC_ALLOW_REGISTRATION", True)
@pytest.fixture
def mock_oidc_allow_registration_disabled(mocker):
mocker.patch("handler.auth.base_handler.OIDC_ALLOW_REGISTRATION", False)
@pytest.fixture
def mock_token():
return {
@@ -171,6 +181,7 @@ async def test_oidc_valid_token_decoding(
async def test_oidc_valid_add_user(
mocker,
mock_oidc_enabled,
mock_oidc_allow_registration_enabled,
mock_token,
mock_openid_configuration,
config_override,
@@ -218,6 +229,85 @@ async def test_oidc_valid_add_user(
assert mock_add_user.call_args.args[0].role == romm_role
async def test_oidc_registration_disabled_rejects_unknown_user(
mocker,
mock_oidc_enabled,
mock_oidc_allow_registration_disabled,
mock_token,
mock_openid_configuration,
):
"""Test that a login from an unknown email is rejected when registration is disabled."""
mocker.patch(
"handler.database.db_user_handler.get_user_by_email", return_value=None
)
mock_add_user = mocker.patch("handler.database.db_user_handler.add_user")
mocker.patch.object(
StarletteOAuth2App,
"load_server_metadata",
return_value=mock_openid_configuration,
)
oidc_handler = OpenIDHandler()
with pytest.raises(HTTPException, match="registration is disabled"):
await oidc_handler.get_current_active_user_from_openid_token(mock_token)
mock_add_user.assert_not_called()
async def test_oidc_registration_enabled_creates_unknown_user(
mocker,
mock_oidc_enabled,
mock_oidc_allow_registration_enabled,
mock_token,
mock_openid_configuration,
):
"""Test that a login from an unknown email creates a new user when registration is enabled."""
mocker.patch(
"handler.database.db_user_handler.get_user_by_email", return_value=None
)
mock_user = MagicMock(enabled=True, role=Role.VIEWER)
mock_add_user = mocker.patch(
"handler.database.db_user_handler.add_user", return_value=mock_user
)
mocker.patch.object(
StarletteOAuth2App,
"load_server_metadata",
return_value=mock_openid_configuration,
)
oidc_handler = OpenIDHandler()
user, _ = await oidc_handler.get_current_active_user_from_openid_token(mock_token)
mock_add_user.assert_called_once()
assert user == mock_user
async def test_oidc_registration_disabled_allows_existing_user(
mocker,
mock_oidc_enabled,
mock_oidc_allow_registration_disabled,
mock_token,
mock_openid_configuration,
):
"""Test that an existing user can still log in when registration is disabled."""
mock_user = MagicMock(enabled=True, role=Role.VIEWER)
mocker.patch(
"handler.database.db_user_handler.get_user_by_email", return_value=mock_user
)
mock_add_user = mocker.patch("handler.database.db_user_handler.add_user")
mocker.patch.object(
StarletteOAuth2App,
"load_server_metadata",
return_value=mock_openid_configuration,
)
oidc_handler = OpenIDHandler()
user, _ = await oidc_handler.get_current_active_user_from_openid_token(mock_token)
assert user == mock_user
mock_add_user.assert_not_called()
async def test_oidc_valid_edit_user_role(
mocker,
mock_oidc_enabled,