Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-5430] ENABLE_GV feature flipping for get_addon #10610

Draft
wants to merge 16 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions addons/base/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import requests
import markupsafe
from os.path import basename
from website.settings import MFR_SERVER_URL
Expand Down Expand Up @@ -54,3 +55,50 @@ def format_last_known_metadata(auth, node, file, error_type):
]
return ''.join(parts)
return msg


class GravyValetAddonAppConfig:
class MockNodeSetting:
def __init__(self, resource, auth, legacy_config):
resp = requests.get(
settings.GV_RESOURCE_DOMAIN.format(owner_uri=resource.absolute_url),
auth=auth
)

resp.raise_for_status()
data = resp.json()['data']

requests.get(data['relationships'], auth=auth)

class MockUserSetting:
def __init__(self, resource, auth, legacy_config):
requests.get(
settings.GV_USER_DOMAIN.format(owner_uri=resource.absolute_url),
auth=auth
)

def __init__(self, data, external_storage_service_data, resource, auth):
self.data = data

self.external_storage_service_data = external_storage_service_data
# TODO: Names in GV must be exact matches?
self.name = self.external_storage_service_data['data']['attributes']['name']
self.legacy_config = settings.ADDONS_AVAILABLE_DICT[self.name]
self.resource = resource
self.auth = auth
self.FOLDER_SELECTED = self.legacy_config.FOLDER_SELECTED
self.NODE_AUTHORIZED = self.legacy_config.NODE_DEAUTHORIZED
self.NODE_DEAUTHORIZED = self.legacy_config.NODE_DEAUTHORIZED
self.actions = self.legacy_config.actions

@property
def node_settings(self):
return self.MockNodeSetting(self.resource, self.auth, self.legacy_config)

@property
def user_settings(self):
return self.MockUserSetting(self.resource, self.auth, self.legacy_config)

@property
def configured(self):
return True
151 changes: 151 additions & 0 deletions api_tests/providers/test_files_with_gv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import pytest

from osf import features
from api_tests import utils as api_utils
from osf_tests.factories import (
AuthUserFactory,
ProjectFactory,
)
from waffle.testutils import override_flag
from django.shortcuts import reverse


@pytest.mark.django_db
class TestWaffleFilesProviderView:

@pytest.fixture(autouse=True)
def override_flag(self):
with override_flag(features.ENABLE_GV, active=True):
yield

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

@pytest.fixture()
def provider_gv_id(self):
return 1337

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

@pytest.fixture()
def file(self, user, node):
return api_utils.create_test_file(
node,
user,
create_guid=False
)
Johnetordoff marked this conversation as resolved.
Show resolved Hide resolved

@pytest.fixture()
def addon_files_url(self, node, provider_gv_id):
return reverse(
'nodes:node-storage-provider-detail',
kwargs={
'version': 'v2',
'node_id': node._id,
'provider': provider_gv_id
}
)

def test_must_have_auth(self, app, addon_files_url):
res = app.get(
addon_files_url,
expect_errors=True
)
assert res.status_code == 401

def test_must_be_contributor(self, app, addon_files_url):
res = app.get(
addon_files_url,
auth=AuthUserFactory().auth,
expect_errors=True
)
assert res.status_code == 403

def test_get_file_provider(self, app, user, addon_files_url, file, provider_gv_id):
res = app.get(
addon_files_url,
auth=user.auth
)

assert res.status_code == 200
attributes = res.json['data']['attributes']
assert attributes['provider'] == str(provider_gv_id)
assert attributes['name'] == str(provider_gv_id)
Comment on lines +79 to +80
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brianjgeiger would we like either of these (probably name?) to be the display_name of the addon?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm using primarily gv information for this on the front end, but yeaaaahhh, let's try name to be the display_name.

assert res.json['data']['id'] == f'{file.target._id}:{str(provider_gv_id)}'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want this to just be the provider_gv_id. @brianjgeiger?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for everything but (I believe) osfstorage, if there's a gv provider id, then that should be the whole id. Yay for unique provider ids.



@pytest.mark.django_db
class TestWaffleFilesView:

@pytest.fixture(autouse=True)
def override_flag(self):
with override_flag(features.ENABLE_GV, active=True):
yield

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

@pytest.fixture()
def provider_gv_id(self):
return 1

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

@pytest.fixture()
def file(self, user, node):
return api_utils.create_test_file(
node,
user,
create_guid=False
)

@pytest.fixture()
def file_url(self, node, provider_gv_id):
return reverse(
'nodes:node-files',
kwargs={
'version': 'v2',
'node_id': node._id,
'provider': str(provider_gv_id) + '/',
'path': 'test_path/'
Johnetordoff marked this conversation as resolved.
Show resolved Hide resolved
}
)

def test_must_have_auth(self, app, file_url):
res = app.get(
file_url,
expect_errors=True
)
assert res.status_code == 401

def test_must_be_contributor(self, app, file_url):
res = app.get(
file_url,
auth=AuthUserFactory().auth,
expect_errors=True
)
assert res.status_code == 403

def test_get_file_provider(self, app, user, file_url, file, provider_gv_id):
res = app.get(
file_url,
auth=user.auth
)

assert res.status_code == 200
attributes = res.json['data']['attributes']
assert attributes['provider'] == str(provider_gv_id)
assert attributes['name'] == str(provider_gv_id)
assert res.json['data']['id'] == f'{file.target._id}:{str(provider_gv_id)}'
28 changes: 27 additions & 1 deletion osf/models/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import itertools
import logging
import re
import waffle
import requests
from future.moves.urllib.parse import urljoin
import warnings
from rest_framework import status as http_status
Expand Down Expand Up @@ -54,6 +56,7 @@
from .user import OSFUser
from .validators import validate_title, validate_doi
from framework.auth.core import Auth
from osf import features
from osf.utils.datetime_aware_jsonfield import DateTimeAwareJSONField
from osf.utils.fields import NonNaiveDateTimeField, ensure_str
from osf.utils.requests import get_request_and_user_id, string_type_request_headers
Expand Down Expand Up @@ -83,7 +86,7 @@
from api.caching import settings as cache_settings
from api.caching.utils import storage_usage_cache
from api.share.utils import update_share

from addons.base.utils import GravyValetAddonAppConfig

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -2430,6 +2433,29 @@ def _remove_from_associated_collections(self, auth=None, force=False):
force=True
)

def get_addon(self, name, is_deleted=False):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My instinct is that, instead of having the subclasses completely override get_addon, the superclass should start with a check for the GravyValet flag and then invoke a _get_addon_from_gravyvalet or similar method that is defined on the subclass. That gets rid of the need to call super() and better delegates responsibility to the subclass

request, user_id = get_request_and_user_id()

if hasattr(request, 'user') and not isinstance(request.user, AnonymousUser) and waffle.flag_is_active(request, features.ENABLE_GV) and name not in ['osfstorage', 'wiki']:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to guard this against the AnonymousUser. Unauthenticated users should still be able to see the addons for a public node.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is still true (this is also a delta between node and user behavior)

resp = requests.get(
settings.GV_NODE_ADDON_ENDPOINT.format(addon_id=name),
auth=(request.user.username, request.user.password)
)
configured_storage_addon = resp.json()
resp = requests.get(
configured_storage_addon['data']['relationships']['external_storage_service']['links']['related'],
auth=(request.user.username, request.user.password)
)
external_storage_service_data = resp.json()
return GravyValetAddonAppConfig(
configured_storage_addon,
external_storage_service_data,
self,
auth=(request.user.username, request.user.password)
)
Johnetordoff marked this conversation as resolved.
Show resolved Hide resolved
else:
return super().get_addon(name, is_deleted)


class NodeUserObjectPermission(UserObjectPermissionBase):
"""
Expand Down
27 changes: 27 additions & 0 deletions osf/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
from future.moves.urllib.parse import urljoin, urlencode
import uuid
import waffle
from copy import deepcopy

from flask import Request as FlaskRequest
Expand Down Expand Up @@ -59,6 +60,10 @@
from website.util.metrics import OsfSourceTags
from importlib import import_module
from osf.utils.requests import get_headers_from_request
import requests
from osf.utils.requests import get_request_and_user_id
from osf import features
from addons.base.utils import GravyValetAddonAppConfig

SessionStore = import_module(settings.SESSION_ENGINE).SessionStore

Expand Down Expand Up @@ -1220,6 +1225,28 @@ def create_unregistered(cls, fullname, email=None):

return user

def get_addon(self, name, is_deleted=False):
request, user_id = get_request_and_user_id()

if hasattr(request, 'user') and waffle.flag_is_active(request, features.ENABLE_GV) and name not in ['osfstorage', 'wiki']:
resp = requests.get(
settings.GV_NODE_ADDON_ENDPOINT.format(addon_id=name),
auth=(request.user.username, request.user.password)
)
configured_storage_addon = resp.json()
resp = requests.get(
configured_storage_addon['data']['relationships']['external_storage_service']['links']['related'],
auth=(request.user.username, request.user.password)
)
external_storage_service_data = resp.json()
return GravyValetAddonAppConfig(
configured_storage_addon,
external_storage_service_data,
self,
auth=(request.user.username, request.user.password)
)
else:
return super().get_addon(name, is_deleted)
def update_guessed_names(self):
"""Updates the CSL name fields inferred from the the full name.
"""
Expand Down
9 changes: 9 additions & 0 deletions website/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -2142,3 +2142,12 @@ def from_node_usage(cls, usage_bytes, private_limit=None, public_limit=None):

WAFFLE_VALUES_YAML = 'osf/features.yaml'
DEFAULT_DRAFT_NODE_TITLE = 'Untitled'
GV_RESOURCE_DOMAIN = 'http://192.168.168.167:8004/v1/resource-references/?filter[resource_uri]={owner_uri}'
GV_USER_DOMAIN = 'http://192.168.168.167:8004/v1/user-references/?filter[user_uri]={owner_uri}'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
GV_RESOURCE_DOMAIN = 'http://192.168.168.167:8004/v1/resource-references/?filter[resource_uri]={owner_uri}'
GV_USER_DOMAIN = 'http://192.168.168.167:8004/v1/user-references/?filter[user_uri]={owner_uri}'
GV_API_ROOT = 'http://192.168.168.167:8004/v1
GV_RESOURCE_ENDPOINT = GV_DOMAIN + 'resource-references/?filter[resource_uri]={resource_uri}'
GV_USER_ENDPOINT = GV_DOMAIN + 'user-references/?filter[user_uri]={owner_uri}'
# These two are for `get_addon` vs `get_addons`
GV_USER_ADDON_ENDPOINT = GV_DOMAIN + 'v1/authorized-storage-accounts/{account_id}'
GV_NODE_ADDON_ENDPOINT = GV_DOMAIN + 'v1/configured-storage-addons/{addon_id}'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could optionally append &include=authorized_storage_accounts|configured_storage_addons to the user/resource endpoints

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
GV_RESOURCE_DOMAIN = 'http://192.168.168.167:8004/v1/resource-references/?filter[resource_uri]={owner_uri}'
GV_USER_DOMAIN = 'http://192.168.168.167:8004/v1/user-references/?filter[user_uri]={owner_uri}'

GV_API_ROOT = 'http://192.168.168.167:8004/v1'
Johnetordoff marked this conversation as resolved.
Show resolved Hide resolved
GV_RESOURCE_ENDPOINT = GV_API_ROOT + 'resource-references/?filter[resource_uri]={resource_uri}'
GV_USER_ENDPOINT = GV_API_ROOT + 'user-references/?filter[user_uri]={owner_uri}'
# These two are for `get_addon` vs `get_addons`
GV_USER_ADDON_ENDPOINT = 'http://192.168.168.167:8004/v1/authorized-storage-accounts/{account_id}'
GV_NODE_ADDON_ENDPOINT = 'http://192.168.168.167:8004/v1/configured-storage-addons/{addon_id}/base_account/'
GV_EXTERNAL_STORAGE_ENDPOINT = 'http://192.168.168.167:8004/v1/external-storage-services/{service_id}'
Loading