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

Mutiple oauth app urls #29

Merged
merged 9 commits into from
Mar 26, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2018-03-23
### Added
- `PlotlyAuth` now supports multiple URLs. Supply a localhost URL and a remote
URL in order to test your Plotly login on your local machine while keeping
the login screen available in your deployed app. Usage:
```
dash_auth.PlotlyAuth(app, 'my-app', 'private', [
'https://my-deployed-dash-app.com',
'http://localhost:8050'
])
```

## [0.0.11] - 2018-02-01
### Added
- Added logging on request failure for the `PlotlyAuth` handler
Expand Down
29 changes: 25 additions & 4 deletions dash_auth/plotly_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ class PlotlyAuth(OAuthBase):
TOKEN_COOKIE_NAME = 'plotly_oauth_token'

def __init__(self, app, app_name, sharing, app_url):
"""
Provides Plotly Authentication login screen to a Dash app.

Args:
app: A `dash.Dash` app
app_name: The name of your Dash app. This name will be registered
on the Plotly server
sharing: 'private' or 'public'
app_url: String or list of strings. The URL(s) of the Dash app.
This is used to register your app with Plotly's OAuth system.
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, the "main" URL goes first.

For example, to test locally, supply a list of URLs with
the first URL being your remote server and the second URL
being e.g. http://localhost:8050
Returns:
None
"""
super(PlotlyAuth, self).__init__(app, app_url)

self._fid = create_or_overwrite_dash_app(
Expand Down Expand Up @@ -98,12 +114,11 @@ def create_or_overwrite_dash_app(filename, sharing, app_url):
'filename': filename,
'share_key_enabled': True if sharing == 'secret' else False,
'world_readable': True if sharing == 'public' else False,
'app_url': app_url
'app_url': app_url if isinstance(app_url, str) else app_url[0]
})

res_lookup = api_requests.get('/v2/files/lookup?path={}'.format(filename))
if res_lookup.status_code == 404:
# TODO - Better request handling
res_create = api_requests.post('/v2/dash-apps', data=payload)
try:
res_create.raise_for_status()
Expand Down Expand Up @@ -132,13 +147,19 @@ def create_or_overwrite_dash_app(filename, sharing, app_url):


def create_or_overwrite_oauth_app(app_url, name):
redirect_uri = '{}/_oauth-redirect'.format(app_url.strip('/'))
if isinstance(app_url, str):
redirect_uris = '{}/_oauth-redirect'.format(app_url.strip('/'))
else:
redirect_uris = ' '.join([
'{}/_oauth-redirect'.format(url.strip('/'))
for url in app_url
])
request_data = {
'data': json.dumps({
'name': name,
'client_type': 'public',
'authorization_grant_type': 'implicit',
'redirect_uris': redirect_uri,
'redirect_uris': redirect_uris,
})
}

Expand Down
2 changes: 1 addition & 1 deletion dash_auth/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.0.11'
__version__ = '0.1.0'
49 changes: 25 additions & 24 deletions tests/IntegrationTests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,32 @@
import sys
import os

from .utils import assert_clean_console, invincible, switch_windows, wait_for
from .utils import assert_clean_console, switch_windows

TIMEOUT = 60

class IntegrationTests(unittest.TestCase):
def wait_for_element_by_css_selector(self, selector):
start_time = time.time()
while time.time() < start_time + TIMEOUT:
try:
return self.driver.find_element_by_css_selector(selector)
except Exception as e:
pass
time.sleep(0.25)
raise e

def wait_for_text_to_equal(self, selector, assertion_text):
start_time = time.time()
while time.time() < start_time + TIMEOUT:
el = self.wait_for_element_by_css_selector(selector)
try:
return self.assertEqual(el.text, assertion_text)
except Exception as e:
pass
time.sleep(0.25)
raise e

def percy_snapshot(cls, name):
if ('PERCY_PROJECT' in os.environ and
os.environ['PERCY_PROJECT'] == 'plotly/dash-auth'):
Expand Down Expand Up @@ -55,28 +77,14 @@ def setUp(self):
super(IntegrationTests, self).setUp()
self.driver = webdriver.Chrome()

def wait_for_element_by_id(id):
wait_for(lambda: None is not invincible(
lambda: self.driver.find_element_by_id(id)
))
return self.driver.find_element_by_id(id)
self.wait_for_element_by_id = wait_for_element_by_id

def wait_for_element_by_css_selector(css_selector):
wait_for(lambda: None is not invincible(
lambda: self.driver.find_element_by_css_selector(css_selector)
))
return self.driver.find_element_by_css_selector(css_selector)
self.wait_for_element_by_css_selector = wait_for_element_by_css_selector

def tearDown(self):
super(IntegrationTests, self).tearDown()
time.sleep(5)
self.server_process.terminate()
time.sleep(5)
self.driver.quit()

def startServer(self, app):
def startServer(self, app, skip_visit=False):
def run():
app.scripts.config.serve_locally = True
app.run_server(
Expand All @@ -91,17 +99,10 @@ def run():
time.sleep(15)

# Visit the dash page
try:
if not skip_visit:
self.driver.get('http://localhost:8050{}'.format(
app.config['routes_pathname_prefix'])
)
except:
print('Failed attempt to load page, trying again')
print(self.server_process)
print(self.server_process.is_alive())
time.sleep(5)
print(requests.get('http://localhost:8050'))
self.driver.get('http://localhost:8050')

time.sleep(0.5)

Expand Down
12 changes: 5 additions & 7 deletions tests/test_basic_auth_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from selenium.common.exceptions import NoSuchElementException

from .IntegrationTests import IntegrationTests
from .utils import assert_clean_console, invincible, switch_windows, wait_for
from .utils import assert_clean_console, switch_windows
from dash_auth import basic_auth


Expand Down Expand Up @@ -46,13 +46,12 @@ def update_output(new_value):
TEST_USERS['valid']
)

self.startServer(app)
self.startServer(app, skip_visit=True)

self.assertEqual(
requests.get('http://localhost:8050').status_code,
401
)
with self.assertRaises(NoSuchElementException):
self.driver.find_element_by_id('output')

# login using the URL instead of the alert popup
# selenium has no way of accessing the alert popup
Expand All @@ -62,6 +61,5 @@ def update_output(new_value):
# but it saves the credentials as part of the browser.
# visiting the page again will use the saved credentials
self.driver.get('http://localhost:8050')
time.sleep(5)
el = self.wait_for_element_by_id('output')
self.assertEqual(el.text, 'initial value')
el = self.wait_for_element_by_css_selector('#output')
self.wait_for_text_to_equal('#output', 'initial value')
67 changes: 36 additions & 31 deletions tests/test_plotly_auth_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
import plotly.plotly as py

from .IntegrationTests import IntegrationTests
from .utils import assert_clean_console, invincible, switch_windows, wait_for
from .utils import assert_clean_console, switch_windows
from .users import users
from dash_auth import plotly_auth


class Tests(IntegrationTests):
def plotly_auth_login_flow(self, username, pw, url_base_pathname):
def plotly_auth_login_flow(self, username, pw,
url_base_pathname=None, oauth_urls=None):
os.environ['PLOTLY_USERNAME'] = users['creator']['username']
os.environ['PLOTLY_API_KEY'] = users['creator']['api_key']
app = dash.Dash(__name__, url_base_pathname=url_base_pathname)
Expand All @@ -35,50 +36,46 @@ def update_output(new_value):
app,
'integration-test',
'private',
'http://localhost:8050{}'.format(url_base_pathname)
(
'http://localhost:8050{}'.format(url_base_pathname)
if url_base_pathname else oauth_urls
)
)

self.startServer(app)

time.sleep(10)
self.percy_snapshot('login screen - {} {} {}'.format(
username, pw, url_base_pathname))
try:
el = self.wait_for_element_by_id('dash-auth--login__container')
el = self.wait_for_element_by_css_selector('#dash-auth--login__container')
except Exception as e:
print(self.wait_for_element_by_tag_name('body').html)
raise e

self.driver.find_element_by_id('dash-auth--login__button').click()
time.sleep(5)
self.wait_for_element_by_css_selector(
'#dash-auth--login__button').click()
switch_windows(self.driver)
time.sleep(20)
self.wait_for_element_by_id(
'js-auth-modal-signin-username'
self.wait_for_element_by_css_selector(
'#js-auth-modal-signin-username'
).send_keys(username)

self.driver.find_element_by_id(
'js-auth-modal-signin-password'
self.wait_for_element_by_css_selector(
'#js-auth-modal-signin-password'
).send_keys(pw)

self.driver.find_element_by_id('js-auth-modal-signin-submit').click()
self.wait_for_element_by_css_selector(
'#js-auth-modal-signin-submit').click()

# wait for oauth screen
time.sleep(5)
self.percy_snapshot('oauth screen - {} {} {}'.format(
username, pw, url_base_pathname))
self.wait_for_element_by_css_selector('input[name="allow"]').click()

def private_app_unauthorized(self, url_base_pathname):
def private_app_unauthorized(self, url_base_pathname=None, oauth_urls=None):
self.plotly_auth_login_flow(
users['viewer']['username'],
users['viewer']['pw'],
url_base_pathname
url_base_pathname=url_base_pathname,
oauth_urls=oauth_urls
)
time.sleep(5)
self.percy_snapshot('private_app_unauthorized 1 - {}'.format(
url_base_pathname))
el = self.wait_for_element_by_id('dash-auth--authorization__denied')
el = self.wait_for_element_by_css_selector(
'#dash-auth--authorization__denied')
self.assertEqual(el.text, 'You are not authorized to view this app')
switch_windows(self.driver)
self.percy_snapshot('private_app_unauthorized 2 - {}'.format(
Expand All @@ -87,20 +84,18 @@ def private_app_unauthorized(self, url_base_pathname):
# login screen should still be there
self.percy_snapshot('private_app_unauthorized 3 - {}'.format(
url_base_pathname))
self.wait_for_element_by_id('dash-auth--login__container')
self.wait_for_element_by_css_selector(
'#dash-auth--login__container')

def private_app_authorized(self, url_base_pathname):
def private_app_authorized(self, url_base_pathname=None, oauth_urls=None):
self.plotly_auth_login_flow(
users['creator']['username'],
users['creator']['pw'],
url_base_pathname
url_base_pathname,
)
switch_windows(self.driver)
time.sleep(5)
self.percy_snapshot('private_app_authorized - {}'.format(
url_base_pathname))
try:
el = self.wait_for_element_by_id('output')
el = self.wait_for_element_by_css_selector('#output')
except:
print((self.driver.find_element_by_tag_name('body').html))
self.assertEqual(el.text, 'initial value')
Expand All @@ -116,3 +111,13 @@ def test_private_app_unauthorized_index(self):

def test_private_app_unauthorized_route(self):
self.private_app_unauthorized('/my-app/')


def test_private_app_authorized_index_multiple_oauth_urls(self):
self.private_app_authorized(
'/',
oauth_urls=[
'http://test-domain.plotly.systems:8050/',
'http://localhost:8050/'
]
)
55 changes: 0 additions & 55 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
import time


TIMEOUT = 20 # Seconds


def clean_history(driver, domains):
temp = driver.get_location()
for domain in domains:
Expand All @@ -15,15 +12,6 @@ def clean_history(driver, domains):
driver.open(temp)


def invincible(func):
def wrap():
try:
return func()
except:
pass
return wrap


def switch_windows(driver):
new_window_handle = None
while not new_window_handle:
Expand All @@ -39,49 +27,6 @@ class WaitForTimeout(Exception):
pass


def wait_for(condition_function, get_message=lambda: '', *args, **kwargs):
"""
Waits for condition_function to return True or raises WaitForTimeout.
:param (function) condition_function: Should return True on success.
:param args: Optional args to pass to condition_function.
:param kwargs: Optional kwargs to pass to condition_function.
if `timeout` is in kwargs, it will be used to override TIMEOUT
:raises: WaitForTimeout If condition_function doesn't return True in time.
Usage:
def get_element(selector):
# some code to get some element or return a `False`-y value.
selector = '.js-plotly-plot'
try:
wait_for(get_element, selector)
except WaitForTimeout:
self.fail('element never appeared...')
plot = get_element(selector) # we know it exists.
"""
def wrapped_condition_function():
"""We wrap this to alter the call base on the closure."""
if args and kwargs:
return condition_function(*args, **kwargs)
if args:
return condition_function(*args)
if kwargs:
return condition_function(**kwargs)
return condition_function()

if 'timeout' in kwargs:
timeout = kwargs['timeout']
del kwargs['timeout']
else:
timeout = TIMEOUT

start_time = time.time()
while time.time() < start_time + timeout:
if wrapped_condition_function():
return True
time.sleep(0.5)

raise WaitForTimeout(get_message())


def assert_clean_console(TestClass):
def assert_no_console_errors(TestClass):
TestClass.assertEqual(
Expand Down