Skip to content

Commit

Permalink
Make owncloud configurable via api v2
Browse files Browse the repository at this point in the history
  • Loading branch information
John Tordoff committed Dec 1, 2023
1 parent 3d3bfdc commit 0256fca
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 1 deletion.
12 changes: 12 additions & 0 deletions addons/owncloud/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import logging
import owncloud

from addons.base.models import (BaseOAuthNodeSettings, BaseOAuthUserSettings,
BaseStorageAddon)
Expand Down Expand Up @@ -68,6 +69,10 @@ class NodeSettings(BaseOAuthNodeSettings, BaseStorageAddon):

_api = None

@property
def has_auth(self):
return bool(self.user_settings and self.user_settings.has_auth and self.external_account)

@property
def api(self):
if self._api is None:
Expand All @@ -83,6 +88,13 @@ def folder_name(self):
return self.folder_id

def set_folder(self, folder, auth=None):
c = OwnCloudClient(self.external_account.oauth_secret, verify_certs=settings.USE_SSL)
c.login(self.external_account.display_name, self.external_account.oauth_key)
try:
assert c.list(folder)
except owncloud.owncloud.HTTPResponseError:
raise exceptions.InvalidFolderError(f'path `{folder}` is not found')

if folder == '/ (Full ownCloud)':
folder = '/'
self.folder_id = folder
Expand Down
54 changes: 54 additions & 0 deletions api/nodes/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from django.db import connection
from distutils.version import StrictVersion
import owncloud
import requests
from owncloud import Client as OwnCloudClient

from api.base.exceptions import (
Conflict, EndpointNotImplementedError,
Expand Down Expand Up @@ -43,6 +46,7 @@
from website.project import new_private_link
from website.project.model import NodeUpdateError
from osf.utils import permissions as osf_permissions
from addons.owncloud.settings import USE_SSL


class RegistrationProviderRelationshipField(RelationshipField):
Expand Down Expand Up @@ -2038,3 +2042,53 @@ def update(self, obj, validated_data):
# permission is in writeable_method_fields, so validation happens on OSF Group model
raise exceptions.ValidationError(detail=str(e))
return obj


class OwncloudNodeAddonSettingsSerializer(NodeAddonSettingsSerializer):
"""
This serializer adds specific behavior for the OwnCloud addon. It enables a user to add and update their owncloud
credentials to their node settings via the API.
"""

host = ser.CharField(required=False, allow_null=True, write_only=True)
username = ser.CharField(required=False, allow_null=True, write_only=True)
password = ser.CharField(required=False, allow_null=True, write_only=True)

def update(self, instance, validated_data):
instance = super().update(instance, validated_data)
username = validated_data.get('username')
password = validated_data.get('password')
host = validated_data.get('host')

if username and password and host:
# Validate credentials before adding them to NodeSettings
try:
client = OwnCloudClient(host, verify_certs=USE_SSL)
except requests.exceptions.ConnectionError:
raise exceptions.ValidationError('Invalid ownCloud server.')

try:
client.login(username, password)
client.logout()
except owncloud.owncloud.HTTPResponseError:
raise exceptions.PermissionDenied('ownCloud Login failed.')

# Add credentials to user settings if user makes them via the v2 API,
account, created = ExternalAccount.objects.get_or_create(
provider='owncloud',
provider_id=f'{host}:{username}'.lower()
)
account.display_name = username
account.oauth_key = password
account.oauth_secret = host
account.save()

instance.external_account = account
instance.save()

user = self.context['request'].user
if not user.external_accounts.filter(id=account.id).exists():
user.external_accounts.add(account)
user.save()

return instance
6 changes: 5 additions & 1 deletion api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
NodeGroupsSerializer,
NodeGroupsCreateSerializer,
NodeGroupsDetailSerializer,
OwncloudNodeAddonSettingsSerializer,
)
from api.nodes.utils import NodeOptimizationMixin, enforce_no_children
from api.osf_groups.views import OSFGroupMixin
Expand Down Expand Up @@ -1419,8 +1420,11 @@ def get_serializer_class(self):
"""
Use NodeDetailSerializer which requires 'id'
"""
if 'provider' in self.kwargs and self.kwargs['provider'] == 'forward':
provider = self.kwargs.get('provider')
if provider == 'forward':
return ForwardNodeAddonSettingsSerializer
elif provider == 'owncloud':
return OwncloudNodeAddonSettingsSerializer
else:
return NodeAddonSettingsSerializer

Expand Down
92 changes: 92 additions & 0 deletions api_tests/addons_tests/owncloud/test_configure_owncloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import mock
import pytest
from framework.auth.core import Auth
from api.base.settings.defaults import API_BASE
from osf_tests.factories import ProjectFactory, AuthUserFactory, ExternalAccountFactory

_mock = lambda attributes: type('MockObject', (mock.Mock,), attributes)

def mock_owncloud_client():
return _mock({
'login': lambda username, password: None,
'logout': lambda: None,
'list': lambda folder: [folder]
})


@pytest.mark.django_db
class TestOwnCloudConfig:
"""
This class tests for new Owncloud Addon behavior created by the POSE grant. This new behavior allows a user access
two additional features via the API.
1. Adds ability to add credentials via v2 API tested in `test_addon_credentials_PATCH`
2. Adds ability to set owncloud base folders via v2 API tested in `test_addon_credentials_PATCH`
This also adds validation for setting owncloud folder_id's checking them againest the owncloud server.
"""

@pytest.fixture()
def user(self):
return AuthUserFactory()

@pytest.fixture()
def node(self, user):
return ProjectFactory(creator=user)

@pytest.fixture()
def enabled_addon(self, node, user):
addon = node.get_or_add_addon('owncloud', auth=Auth(user))
addon.save()
return addon

@pytest.fixture()
def node_with_authorized_addon(self, user, node, enabled_addon):
external_account = ExternalAccountFactory(
provider='owncloud',
display_name='test_username',
oauth_key='test_password',
oauth_secret='http://test_host.com',
)
enabled_addon.external_account = external_account
user_settings = user.get_or_add_addon('owncloud')
enabled_addon.user_settings = user_settings
user.external_accounts.add(external_account)
user.save()
user_settings.save()
enabled_addon.save()
return node

@mock.patch('addons.owncloud.models.OwnCloudClient', return_value=mock_owncloud_client())
def test_addon_folders_PATCH(self, mock_client, app, node_with_authorized_addon, user):
resp = app.patch_json_api(
f'/{API_BASE}nodes/{node_with_authorized_addon._id}/addons/owncloud/',
{
'data': {
'attributes': {
'folder_id': '/'}
}
},
auth=user.auth
)
assert resp.status_code == 200
assert resp.json['data']['attributes']['folder_id'] == '/'
assert resp.json['data']['attributes']['folder_path'] == '/'

@mock.patch('api.nodes.serializers.OwnCloudClient', return_value=mock_owncloud_client())
def test_addon_credentials_PATCH(self, mock_client, app, node, user, enabled_addon):
resp = app.patch_json_api(
f'/{API_BASE}nodes/{node._id}/addons/owncloud/',
{
'data': {
'attributes': {
'username': 'FastBatman',
'password': 'Quez_Watkins',
'host': 'https://sirianni@eagles.bird',
}
}
},
auth=user.auth
)
assert resp.status_code == 200
assert resp.json['data']['attributes']['external_account_id']
5 changes: 5 additions & 0 deletions api_tests/nodes/views/test_node_addons.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from api.base.settings.defaults import API_BASE
from osf_tests.factories import AuthUserFactory
from tests.base import ApiAddonTestCase
from api_tests.addons_tests.owncloud.test_configure_owncloud import mock_owncloud_client

from addons.mendeley.tests.factories import (
MendeleyAccountFactory, MendeleyNodeSettingsFactory
Expand Down Expand Up @@ -997,6 +998,10 @@ def _mock_folder_result(self):
'id': '/'
}

def test_settings_detail_PUT_all_sets_settings(self):
with mock.patch('addons.owncloud.models.OwnCloudClient', return_value=mock_owncloud_client()):
return super().test_settings_detail_PUT_all_sets_settings()


class TestNodeS3Addon(NodeConfigurableAddonTestSuiteMixin, ApiAddonTestCase):
short_name = 's3'
Expand Down

0 comments on commit 0256fca

Please sign in to comment.