refactor: change OAuth scope separator from colon to dot for IDP compatibility

Many identity providers (AWS Cognito, Okta, Azure AD) reject or mishandle
colons in OAuth scope names. This migrates all custom scopes from
`resource:action` to `resource.action` format (e.g., `notes:read` →
`notes.read`), which is universally accepted and aligns with industry
conventions (Microsoft, Google).

Includes Alembic migration 004 for stored scope strings and ADR-024
documenting the rationale and RFC references.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-07 10:07:02 +02:00
co-authored by Claude Opus 4.6
parent 899b9c7191
commit 29fd0486c9
44 changed files with 724 additions and 520 deletions
+8 -8
View File
@@ -12,24 +12,24 @@ from nextcloud_mcp_server.auth.scope_authorization import (
def test_scope_decorator_stores_metadata():
"""Test that @require_scopes decorator stores scope requirements as function metadata."""
@require_scopes("notes:read", "notes:write")
@require_scopes("notes.read", "notes.write")
async def example_function():
pass
# Verify metadata is stored
assert hasattr(example_function, "_required_scopes")
assert example_function._required_scopes == ["notes:read", "notes:write"]
assert example_function._required_scopes == ["notes.read", "notes.write"]
@pytest.mark.unit
def test_scope_decorator_with_single_scope():
"""Test decorator with a single scope requirement."""
@require_scopes("calendar:read")
@require_scopes("calendar.read")
async def example_function():
pass
assert example_function._required_scopes == ["calendar:read"]
assert example_function._required_scopes == ["calendar.read"]
@pytest.mark.unit
@@ -46,18 +46,18 @@ def test_scope_decorator_with_no_scopes():
@pytest.mark.unit
def test_insufficient_scope_error():
"""Test InsufficientScopeError exception structure."""
missing = ["notes:write", "calendar:write"]
missing = ["notes.write", "calendar.write"]
error = InsufficientScopeError(missing)
assert error.missing_scopes == missing
assert "notes:write" in str(error)
assert "calendar:write" in str(error)
assert "notes.write" in str(error)
assert "calendar.write" in str(error)
@pytest.mark.unit
def test_insufficient_scope_error_with_custom_message():
"""Test InsufficientScopeError with custom message."""
missing = ["files:write"]
missing = ["files.write"]
custom_msg = "You need more permissions"
error = InsufficientScopeError(missing, custom_msg)