add user auth flow

[ADD] DCOS-831 Add login feature
This commit is contained in:
Yura
2015-04-24 13:37:40 -07:00
committed by yura
parent b6efc7a605
commit 4d9e9cdbbd
10 changed files with 228 additions and 9 deletions

View File

@@ -33,7 +33,7 @@ from subprocess import PIPE, Popen
import dcoscli
import docopt
from dcos.api import constants, emitting, errors, http, subcommand, util
from dcos.api import auth, constants, emitting, errors, http, subcommand, util
from dcoscli import analytics
emitter = emitting.FlatEmitter()
@@ -45,6 +45,11 @@ def main():
if not _is_valid_configuration():
return 1
if not auth.check_if_user_authenticated():
auth_status = auth.force_auth()
if not auth_status:
return 1
args = docopt.docopt(
__doc__,
version='dcos version {}'.format(dcoscli.version),

View File

@@ -68,7 +68,8 @@ setup(
'toml',
'virtualenv',
'rollbar',
'futures'
'futures',
'oauth2client'
],
# If there are data files included in your packages that need to be

View File

@@ -1,2 +1,3 @@
[core]
reporting = false
email = "test@mail.com"

View File

@@ -1,2 +1,3 @@
[core]
reporting = true
email = "test@mail.com"

View File

@@ -0,0 +1,3 @@
[core]
reporting = false
email = "test@mail.com"

View File

@@ -0,0 +1,2 @@
[core]
reporting = false

View File

@@ -1,10 +1,11 @@
[subcommand]
pip_find_links = "../dist"
[marathon]
port = 8080
host = "localhost"
[package]
cache = "tmp/cache"
sources = [ "git://github.com/mesosphere/universe.git", "https://github.com/mesosphere/universe/archive/master.zip",]
[core]
email = "test@mail.com"
reporting = false
[marathon]
host = "localhost"
port = 8080
[package]
sources = [ "git://github.com/mesosphere/universe.git", "https://github.com/mesosphere/universe/archive/master.zip",]
cache = "tmp/cache"

View File

@@ -0,0 +1,52 @@
import os
import webbrowser
from dcos.api import auth, constants, util
from dcoscli.main import main
from mock import Mock, patch
def test_no_browser_auth():
webbrowser.get = Mock(side_effect=webbrowser.Error())
with patch('webbrowser.open') as op:
_mock_dcos_run([util.which('dcos')], False)
assert op.call_count == 0
def test_when_authenticated():
with patch('dcos.api.auth.force_auth'):
_mock_dcos_run([util.which('dcos')], True)
assert auth.force_auth.call_count == 0
def test_anonymous_login():
with patch('six.moves.input',
return_value=''), patch('uuid.uuid1',
return_value='anonymous@email'):
assert _mock_dcos_run([util.which('dcos'),
'config', 'show'], False) == 0
assert _mock_dcos_run([util.which('dcos'), 'config',
'show', 'core.email'], False) == 0
assert _mock_dcos_run([util.which('dcos'), 'config',
'unset', 'core.email'], False) == 0
def _mock_dcos_run(args, authenticated=True):
env = _config_with_credentials() if authenticated else \
_config_without_credentials()
with patch('sys.argv', args), patch.dict(os.environ, env):
return main()
def _config_with_credentials():
return {constants.DCOS_CONFIG_ENV: os.path.join(
'tests', 'data', 'auth', 'dcos_with_credentials.toml')}
def _config_without_credentials():
return {constants.DCOS_CONFIG_ENV: os.path.join(
'tests', 'data', 'auth', 'dcos_without_credentials.toml')}

View File

@@ -69,7 +69,8 @@ def test_list_property(env):
env)
assert returncode == 0
assert stdout == b"""core.reporting=False
assert stdout == b"""core.email=test@mail.com
core.reporting=False
marathon.host=localhost
marathon.port=8080
package.cache=tmp/cache

152
dcos/api/auth.py Normal file
View File

@@ -0,0 +1,152 @@
import json
import os
import uuid
import pkg_resources
import toml
from dcos.api import config, constants, emitting, errors, http, jsonitem
from six import iteritems, moves
from oauth2client import client
CLIENT_ID = '6a552732-ab9b-410d-9b7d-d8c6523b09a1'
CLIENT_SECRET = 'f56c1e2b-8599-40ca-b6a0-3aba3e702eae'
AUTH_URL = 'https://accounts.mesosphere.com/oauth/authorize'
TOKEN_URL = 'https://accounts.mesosphere.com/oauth/token'
USER_INFO_URL = 'https://accounts.mesosphere.com/api/v1/user.json'
CORE_TOKEN_KEY = 'token'
CORE_EMAIL_KEY = 'email'
emitter = emitting.FlatEmitter()
def _authorize():
"""Create OAuth flow and authorize user
:return: Tuple of credentials dict end Error
:rtype: (dict, dcos.api.errors.Error)
"""
try:
flow = client.OAuth2WebServerFlow(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
scope='',
auth_uri=AUTH_URL,
token_uri=TOKEN_URL,
redirect_uri=client.OOB_CALLBACK_URN,
response_type='code'
)
res = _run(flow)
return res, None
except:
err = errors.DefaultError('There was a problem with '
'web authentication.')
return None, err
def make_oauth_request(code, flow):
"""Make request to auth server using auth_code.
:param: code: auth_code read from cli
:param: flow: OAuth2 web server flow
:return: dict with the keys token and email
:rtype: dict
"""
credential = flow.step2_exchange(code)
token = credential.access_token
headers = {'Authorization': str('Bearer ' + token)}
data = http.requests.get(USER_INFO_URL, headers=headers).json()
mail = data['email']
credentials = {CORE_TOKEN_KEY: credential.access_token,
CORE_EMAIL_KEY: mail}
return credentials
def _run(flow):
"""Make authorization and retrieve access token and user email.
:param flow: OAuth2 web server flow
:param launch_browser: if possible to run browser
:return: dict with the keys token and email
:rtype: dict
"""
auth_url = flow.step1_get_authorize_url()
message = """Thank you for installing the Mesosphere DCOS CLI.
Please log in with your Mesosphere Account by pasting
the following URL into your browser to continue."""
emitter.publish(errors.DefaultError(
'{message}\n\n {url}\n\n'.format(message=message,
url=auth_url,)))
code = moves.input('Please enter Mesosphere verification code: ').strip()
if not code:
email = moves.input('Skipping authentication.'
' Please enter email address:').strip()
if not email:
emitter.publish(errors.DefaultError('Skipping email input,'
' using anonymous id:'))
email = str(uuid.uuid1())
return {CORE_EMAIL_KEY: email}
return make_oauth_request(code, flow)
def check_if_user_authenticated():
""" check if user is authenticated already
:returns user auth status
:rtype: boolean
"""
dcos_config = config.load_from_path(
os.environ[constants.DCOS_CONFIG_ENV])
return dcos_config.get('core.email', '') != ''
def force_auth():
""" Make user authentication process
:returns authentication process status
:rtype: boolean
"""
credentials, error = _authorize()
if error is not None:
emitter.publish(error)
return False
else:
error = _save_auth_keys(credentials)
if error is not None:
emitter.publish(error)
return False
return True
def _save_auth_keys(key_dict):
"""
:param key_dict: auth parameters dict
:type key_dict: dict
:returns: Error value
:rtype: Error
"""
config_path = os.environ[constants.DCOS_CONFIG_ENV]
toml_config = config.mutable_load_from_path(config_path)
section = 'core'
config_schema = json.loads(
pkg_resources.resource_string(
'dcoscli',
'data/config-schema/core.json').decode('utf-8'))
for k, v in iteritems(key_dict):
python_value, err = jsonitem.parse_json_value(k, v, config_schema)
if err is not None:
return err
name = '{}.{}'.format(section, k)
toml_config[name] = python_value
serial = toml.dumps(toml_config._dictionary)
with open(config_path, 'w') as config_file:
config_file.write(serial)
return None