Use scoped tokens to load <audio> urls instead of JWT

This commit is contained in:
Agate 2020-05-11 10:06:35 +02:00
parent 13d28f7b0c
commit ec8dfdb740
17 changed files with 265 additions and 39 deletions

View file

@ -15,6 +15,7 @@ from funkwhale_api.federation import api_serializers as federation_api_serialize
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.federation import tasks as federation_tasks
from funkwhale_api.music import licenses, models, serializers, tasks, views
from funkwhale_api.users import authentication as users_authentication
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
@ -1488,3 +1489,15 @@ def test_other_user_cannot_delete_track(factories, logged_in_api_client):
assert response.status_code == 404
track.refresh_from_db()
def test_listen_to_track_with_scoped_token(factories, api_client):
user = factories["users.User"]()
token = users_authentication.generate_scoped_token(
user_id=user.pk, user_secret=user.secret_key, scopes=["read:libraries"]
)
upload = factories["music.Upload"](playable=True)
url = reverse("api:v1:listen-detail", kwargs={"uuid": upload.track.uuid})
response = api_client.get(url, {"token": token})
assert response.status_code == 200

View file

@ -62,7 +62,7 @@ def test_scope_permission_anonymous_policy(
view = mocker.Mock(
required_scope="libraries", anonymous_policy=policy, anonymous_scopes=set()
)
request = mocker.Mock(method="GET", user=anonymous_user, actor=None)
request = mocker.Mock(method="GET", user=anonymous_user, actor=None, scopes=None)
p = permissions.ScopePermission()
@ -76,7 +76,7 @@ def test_scope_permission_dict_no_required(mocker, anonymous_user):
action="read",
anonymous_scopes=set(),
)
request = mocker.Mock(method="GET", user=anonymous_user, actor=None)
request = mocker.Mock(method="GET", user=anonymous_user, actor=None, scopes=None)
p = permissions.ScopePermission()
@ -97,7 +97,7 @@ def test_scope_permission_user(
):
user = factories["users.User"]()
should_allow = mocker.patch.object(permissions, "should_allow")
request = mocker.Mock(method=method, user=user, actor=None)
request = mocker.Mock(method=method, user=user, actor=None, scopes=None)
view = mocker.Mock(
required_scope=required_scope, anonymous_policy=False, action=action
)
@ -131,10 +131,27 @@ def test_scope_permission_token(mocker, factories):
)
def test_scope_permission_request_scopes(mocker, factories):
should_allow = mocker.patch.object(permissions, "should_allow")
request = mocker.Mock(method="POST", scopes=["write:profile", "read:playlists"])
view = mocker.Mock(required_scope="profile", anonymous_policy=False)
p = permissions.ScopePermission()
assert p.has_permission(request, view) == should_allow.return_value
should_allow.assert_called_once_with(
required_scope="write:profile",
request_scopes={"write:profile", "read:playlists"},
)
def test_scope_permission_actor(mocker, factories, anonymous_user):
should_allow = mocker.patch.object(permissions, "should_allow")
request = mocker.Mock(
method="POST", actor=factories["federation.Actor"](), user=anonymous_user
method="POST",
actor=factories["federation.Actor"](),
user=anonymous_user,
scopes=None,
)
view = mocker.Mock(required_scope="profile", anonymous_policy=False)
p = permissions.ScopePermission()
@ -151,7 +168,7 @@ def test_scope_permission_token_anonymous_user_auth_required(
):
preferences["common__api_authentication_required"] = True
should_allow = mocker.patch.object(permissions, "should_allow")
request = mocker.Mock(method="POST", user=anonymous_user, actor=None)
request = mocker.Mock(method="POST", user=anonymous_user, actor=None, scopes=None)
view = mocker.Mock(required_scope="profile", anonymous_policy=False)
p = permissions.ScopePermission()
@ -166,7 +183,7 @@ def test_scope_permission_token_anonymous_user_auth_not_required(
):
preferences["common__api_authentication_required"] = False
should_allow = mocker.patch.object(permissions, "should_allow")
request = mocker.Mock(method="POST", user=anonymous_user, actor=None)
request = mocker.Mock(method="POST", user=anonymous_user, actor=None, scopes=None)
view = mocker.Mock(
required_scope="profile", anonymous_policy="setting", anonymous_scopes=set()
)

View file

@ -0,0 +1,83 @@
import pytest
from django.core import signing
from funkwhale_api.users import authentication
def test_generate_scoped_token(mocker):
dumps = mocker.patch("django.core.signing.dumps")
result = authentication.generate_scoped_token(
user_id=42, user_secret="hello", scopes=["read"],
)
assert result == dumps.return_value
dumps.assert_called_once_with(
{"scopes": ["read"], "user_secret": "hello", "user_id": 42},
salt="scoped_tokens",
)
def test_authenticate_scoped_token(mocker, factories, settings):
loads = mocker.spy(signing, "loads")
user = factories["users.User"]()
token = signing.dumps(
{"user_id": user.pk, "user_secret": str(user.secret_key), "scopes": ["read"]},
salt="scoped_tokens",
)
logged_user, scopes = authentication.authenticate_scoped_token(token)
assert scopes == ["read"]
assert logged_user == user
loads.assert_called_once_with(
token, salt="scoped_tokens", max_age=settings.SCOPED_TOKENS_MAX_AGE
)
def test_authenticate_scoped_token_bad_signature():
with pytest.raises(authentication.exceptions.AuthenticationFailed):
authentication.authenticate_scoped_token("hello")
def test_authenticate_scoped_token_bad_secret_key(factories):
user = factories["users.User"]()
token = authentication.generate_scoped_token(
user_id=user.pk, user_secret="invalid", scopes=["read"]
)
with pytest.raises(authentication.exceptions.AuthenticationFailed):
authentication.authenticate_scoped_token(token)
def test_scope_token_authentication(fake_request, factories, mocker):
user = factories["users.User"]()
actor = user.create_actor()
authenticate_scoped_token = mocker.spy(authentication, "authenticate_scoped_token")
token = authentication.generate_scoped_token(
user_id=user.pk, user_secret=user.secret_key, scopes=["read"]
)
request = fake_request.get("/", {"token": token})
auth = authentication.ScopedTokenAuthentication()
assert auth.authenticate(request) == (user, None)
assert request.scopes == ["read"]
assert request.actor == actor
authenticate_scoped_token.assert_called_once_with(token)
def test_scope_token_invalid(fake_request, factories):
token = "test"
request = fake_request.get("/", {"token": token})
auth = authentication.ScopedTokenAuthentication()
with pytest.raises(authentication.exceptions.AuthenticationFailed):
auth.authenticate(request)
def test_scope_token_missing(fake_request, factories):
request = fake_request.get("/")
auth = authentication.ScopedTokenAuthentication()
assert auth.authenticate(request) is None

View file

@ -42,3 +42,18 @@ def test_registration_serializer_validates_password_properly(data, expected_erro
with pytest.raises(serializers.serializers.ValidationError, match=expected_error):
serializer.is_valid(raise_exception=True)
def test_me_serializer_includes_tokens(factories, mocker):
user = factories["users.User"]()
generate_scoped_token = mocker.patch(
"funkwhale_api.users.authentication.generate_scoped_token"
)
expected = {"listen": generate_scoped_token.return_value}
serializer = serializers.MeSerializer(user)
assert serializer.data["tokens"] == expected
generate_scoped_token.assert_called_once_with(
user_id=user.pk, user_secret=user.secret_key, scopes=["read:libraries"]
)