add dcos oauth support (#560)
This commit is contained in:
@@ -160,7 +160,6 @@ fi
|
||||
|
||||
ENV_SETUP="$VIRTUAL_ENV_PATH/bin/env-setup"
|
||||
source "$ENV_SETUP"
|
||||
dcos config set core.email anonymous-optout
|
||||
dcos config set core.reporting false
|
||||
dcos config set core.dcos_url $DCOS_URL
|
||||
dcos config set core.ssl_verify false
|
||||
|
||||
@@ -45,30 +45,12 @@ def _segment_track(event, conf, properties):
|
||||
"""
|
||||
|
||||
data = {'event': event,
|
||||
'properties': properties}
|
||||
|
||||
if 'core.email' in conf:
|
||||
data['userId'] = conf['core.email']
|
||||
else:
|
||||
data['anonymousId'] = session_id
|
||||
'properties': properties,
|
||||
'anonymousId': session_id}
|
||||
|
||||
_segment_request('track', data)
|
||||
|
||||
|
||||
def segment_identify(conf):
|
||||
"""
|
||||
Send a segment.io 'identify' event
|
||||
|
||||
:param conf: dcos config file
|
||||
:type conf: Toml
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
if 'core.email' in conf:
|
||||
data = {'userId': conf.get('core.email')}
|
||||
_segment_request('identify', data)
|
||||
|
||||
|
||||
def _segment_request(path, data):
|
||||
"""
|
||||
Send a segment.io HTTP request
|
||||
|
||||
0
cli/dcoscli/auth/__init__.py
Normal file
0
cli/dcoscli/auth/__init__.py
Normal file
108
cli/dcoscli/auth/main.py
Normal file
108
cli/dcoscli/auth/main.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import dcoscli
|
||||
import docopt
|
||||
from dcos import cmds, config, emitting, http, util
|
||||
from dcos.errors import DCOSAuthorizationException, DCOSException
|
||||
from dcoscli.subcommand import default_command_info, default_doc
|
||||
from dcoscli.util import decorate_docopt_usage
|
||||
|
||||
from six.moves import urllib
|
||||
|
||||
emitter = emitting.FlatEmitter()
|
||||
logger = util.get_logger(__name__)
|
||||
|
||||
|
||||
def main(argv):
|
||||
try:
|
||||
return _main(argv)
|
||||
except DCOSException as e:
|
||||
emitter.publish(e)
|
||||
return 1
|
||||
|
||||
|
||||
@decorate_docopt_usage
|
||||
def _main(argv):
|
||||
args = docopt.docopt(
|
||||
default_doc("auth"),
|
||||
argv=argv,
|
||||
version='dcos-auth version {}'.format(dcoscli.version))
|
||||
|
||||
http.silence_requests_warnings()
|
||||
|
||||
return cmds.execute(_cmds(), args)
|
||||
|
||||
|
||||
def _cmds():
|
||||
"""
|
||||
:returns: all the supported commands
|
||||
:rtype: list of dcos.cmds.Command
|
||||
"""
|
||||
|
||||
return [
|
||||
cmds.Command(
|
||||
hierarchy=['auth', 'login'],
|
||||
arg_keys=[],
|
||||
function=_login),
|
||||
|
||||
cmds.Command(
|
||||
hierarchy=['auth', 'logout'],
|
||||
arg_keys=[],
|
||||
function=_logout),
|
||||
|
||||
cmds.Command(
|
||||
hierarchy=['auth'],
|
||||
arg_keys=['--info'],
|
||||
function=_info),
|
||||
]
|
||||
|
||||
|
||||
def _info(info):
|
||||
"""
|
||||
:param info: Whether to output a description of this subcommand
|
||||
:type info: boolean
|
||||
:returns: process status
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
emitter.publish(default_command_info("auth"))
|
||||
return 0
|
||||
|
||||
|
||||
def _login():
|
||||
"""
|
||||
:returns: process status
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
# every call to login will generate a new token if applicable
|
||||
_logout()
|
||||
conf = util.get_config()
|
||||
dcos_url = conf.get("core.dcos_url")
|
||||
if dcos_url is None:
|
||||
msg = ("Please provide the url to your DCOS cluster: "
|
||||
"`dcos config set core.dcos_url`")
|
||||
raise DCOSException(msg)
|
||||
|
||||
# hit protected endpoint which will prompt for auth if cluster has auth
|
||||
try:
|
||||
url = urllib.parse.urljoin(dcos_url, 'exhibitor')
|
||||
http.get(url)
|
||||
# if the user is authenticated, they have effectively "logged in" even if
|
||||
# they are not authorized for this endpoint
|
||||
except DCOSAuthorizationException:
|
||||
pass
|
||||
|
||||
emitter.publish("Login successful!")
|
||||
return 0
|
||||
|
||||
|
||||
def _logout():
|
||||
"""
|
||||
Logout the user from dcos acs auth or oauth
|
||||
|
||||
:returns: process status
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
if util.get_config().get("core.dcos_acs_token") is not None:
|
||||
config.unset("core.dcos_acs_token")
|
||||
return 0
|
||||
@@ -4,7 +4,6 @@ import dcoscli
|
||||
import docopt
|
||||
from dcos import cmds, config, emitting, http, util
|
||||
from dcos.errors import DCOSException
|
||||
from dcoscli import analytics
|
||||
from dcoscli.subcommand import default_command_info, default_doc
|
||||
from dcoscli.util import decorate_docopt_usage
|
||||
|
||||
@@ -88,9 +87,11 @@ def _set(name, value):
|
||||
notice = ("This config property has been deprecated. "
|
||||
"Please add your repositories with `dcos package repo add`")
|
||||
return DCOSException(notice)
|
||||
toml_config = config.set_val(name, value)
|
||||
if (name == 'core.reporting' is True) or (name == 'core.email'):
|
||||
analytics.segment_identify(toml_config)
|
||||
if name == "core.email":
|
||||
notice = "This config property has been deprecated."
|
||||
return DCOSException(notice)
|
||||
|
||||
config.set_val(name, value)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
21
cli/dcoscli/data/help/auth.txt
Normal file
21
cli/dcoscli/data/help/auth.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
Description:
|
||||
Authenticate to DCOS cluster
|
||||
|
||||
Usage:
|
||||
dcos auth --info
|
||||
dcos auth login
|
||||
dcos auth logout
|
||||
|
||||
Commands:
|
||||
login
|
||||
Login to your DCOS Cluster.
|
||||
logout
|
||||
Logout of your DCOS Cluster.
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print usage.
|
||||
--info
|
||||
Print a short description of this subcommand.
|
||||
--version
|
||||
Print version information.
|
||||
@@ -5,8 +5,7 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import dcoscli
|
||||
import docopt
|
||||
from dcos import (auth, constants, emitting, errors, http, mesos, subcommand,
|
||||
util)
|
||||
from dcos import constants, emitting, errors, http, mesos, subcommand, util
|
||||
from dcos.errors import DCOSAuthenticationException, DCOSException
|
||||
from dcoscli import analytics
|
||||
from dcoscli.subcommand import SubcommandMain, default_doc
|
||||
@@ -40,10 +39,6 @@ def _main():
|
||||
|
||||
util.configure_process_from_environ()
|
||||
|
||||
if args['<command>'] != 'config' and \
|
||||
not auth.check_if_user_authenticated():
|
||||
auth.force_auth()
|
||||
|
||||
config = util.get_config()
|
||||
set_ssl_info_env_vars(config)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ def _default_modules():
|
||||
"""
|
||||
|
||||
# avoid circular imports
|
||||
from dcoscli.auth import main as auth_main
|
||||
from dcoscli.config import main as config_main
|
||||
from dcoscli.help import main as help_main
|
||||
from dcoscli.marathon import main as marathon_main
|
||||
@@ -19,7 +20,8 @@ def _default_modules():
|
||||
from dcoscli.service import main as service_main
|
||||
from dcoscli.task import main as task_main
|
||||
|
||||
return {'config': config_main,
|
||||
return {'auth': auth_main,
|
||||
'config': config_main,
|
||||
'help': help_main,
|
||||
'marathon': marathon_main,
|
||||
'node': node_main,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
[core]
|
||||
reporting = false
|
||||
email = "test@mail.com"
|
||||
dcos_url = "http://dcos.snakeoil.mesosphere.com"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[core]
|
||||
dcos_acs_token = "foobar"
|
||||
dcos_url = "https://dcos.snakeoil.mesosphere.com"
|
||||
email = "test@mail.com"
|
||||
reporting = true
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
[core]
|
||||
email = "test@mail.com"
|
||||
reporting = true
|
||||
dcos_url = "http://dcos.snakeoil.mesosphere.com"
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
[core]
|
||||
reporting = false
|
||||
email = "test@mail.com"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[core]
|
||||
reporting = false
|
||||
email = "test@mail.com"
|
||||
[package]
|
||||
cosmos_url = "http://localhost:7070"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[core]
|
||||
reporting = false
|
||||
email = "test@mail.com"
|
||||
timeout = 5
|
||||
dcos_url = "http://dcos.snakeoil.mesosphere.com"
|
||||
ssl_verify = "false"
|
||||
|
||||
21
cli/tests/data/help/auth.txt
Normal file
21
cli/tests/data/help/auth.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
Description:
|
||||
Authenticate to DCOS cluster
|
||||
|
||||
Usage:
|
||||
dcos auth --info
|
||||
dcos auth login
|
||||
dcos auth logout
|
||||
|
||||
Commands:
|
||||
login
|
||||
Login to your DCOS Cluster.
|
||||
logout
|
||||
Logout of your DCOS Cluster.
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print usage.
|
||||
--info
|
||||
Print a short description of this subcommand.
|
||||
--version
|
||||
Print version information.
|
||||
@@ -5,6 +5,7 @@ for easy management of a DCOS installation.
|
||||
|
||||
Available DCOS commands:
|
||||
|
||||
auth Authenticate to DCOS cluster
|
||||
config Manage the DCOS configuration file
|
||||
help Display help information about DCOS
|
||||
marathon Deploy and manage applications to DCOS
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[core]
|
||||
reporting = false
|
||||
email = "test@mail.com"
|
||||
[marathon]
|
||||
[package]
|
||||
cosmos_url = "http://localhost:7070"
|
||||
|
||||
@@ -3,5 +3,4 @@ cosmos_url = "http://localhost:7070"
|
||||
[core]
|
||||
timeout = 5
|
||||
dcos_url = "https://dcos.snakeoil.mesosphere.com"
|
||||
email = "test@mail.com"
|
||||
reporting = false
|
||||
|
||||
@@ -510,4 +510,4 @@ def config_unset(key, env=None):
|
||||
returncode, stdout, stderr = exec_command(cmd, env=env)
|
||||
|
||||
assert returncode == 0
|
||||
assert stderr == b''
|
||||
assert stdout == b''
|
||||
|
||||
53
cli/tests/integrations/test_auth.py
Normal file
53
cli/tests/integrations/test_auth.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import os
|
||||
|
||||
from dcos import constants
|
||||
|
||||
import pytest
|
||||
|
||||
from .common import assert_command, config_set, exec_command
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
r = os.environ.copy()
|
||||
r.update({
|
||||
constants.PATH_ENV: os.environ[constants.PATH_ENV],
|
||||
constants.DCOS_CONFIG_ENV: os.path.join("tests", "data", "dcos.toml"),
|
||||
})
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def test_info():
|
||||
stdout = b'Authenticate to DCOS cluster\n'
|
||||
assert_command(['dcos', 'auth', '--info'],
|
||||
stdout=stdout)
|
||||
|
||||
|
||||
def test_version():
|
||||
stdout = b'dcos-auth version SNAPSHOT\n'
|
||||
assert_command(['dcos', 'auth', '--version'],
|
||||
stdout=stdout)
|
||||
|
||||
|
||||
def test_logout_no_token(env):
|
||||
exec_command(['dcos', 'config', 'unset', 'core.dcos_acs_token'], env=env)
|
||||
|
||||
returncode, _, stderr = exec_command(
|
||||
['dcos', 'config', 'show', 'core.dcos_acs_token'], env=env)
|
||||
assert returncode == 1
|
||||
assert stderr == b"Property 'core.dcos_acs_token' doesn't exist\n"
|
||||
|
||||
|
||||
def test_logout_with_token(env):
|
||||
config_set('core.dcos_acs_token', "foobar", env=env)
|
||||
stderr = b"[core.dcos_acs_token]: changed\n"
|
||||
assert_command(
|
||||
['dcos', 'config', 'set', 'core.dcos_acs_token', 'faketoken'],
|
||||
stderr=stderr,
|
||||
env=env)
|
||||
|
||||
stderr = b'Removed [core.dcos_acs_token]\n'
|
||||
assert_command(['dcos', 'auth', 'logout'],
|
||||
stderr=stderr,
|
||||
env=env)
|
||||
@@ -51,7 +51,6 @@ def test_version():
|
||||
|
||||
def _test_list_property(env):
|
||||
stdout = b"""core.dcos_url=http://dcos.snakeoil.mesosphere.com
|
||||
core.email=test@mail.com
|
||||
core.reporting=False
|
||||
core.ssl_verify=false
|
||||
core.timeout=5
|
||||
@@ -89,7 +88,6 @@ def test_get_top_property(env):
|
||||
b"Property 'core' doesn't fully specify a value - "
|
||||
b"possible properties are:\n"
|
||||
b"core.dcos_url\n"
|
||||
b"core.email\n"
|
||||
b"core.reporting\n"
|
||||
b"core.ssl_verify\n"
|
||||
b"core.timeout\n"
|
||||
@@ -108,6 +106,13 @@ def test_set_package_sources_property(env):
|
||||
returncode=1)
|
||||
|
||||
|
||||
def test_set_core_email_property(env):
|
||||
notice = (b"This config property has been deprecated.\n")
|
||||
assert_command(['dcos', 'config', 'set', 'core.email', 'foo@bar.com'],
|
||||
stderr=notice,
|
||||
returncode=1)
|
||||
|
||||
|
||||
def test_set_existing_string_property(env):
|
||||
config_set('core.dcos_url',
|
||||
'http://dcos.snakeoil.mesosphere.com:5081', env)
|
||||
@@ -175,7 +180,7 @@ def test_unset_missing_property(env):
|
||||
|
||||
def test_unset_output(env):
|
||||
assert_command(['dcos', 'config', 'unset', 'core.reporting'],
|
||||
stdout=b'Removed [core.reporting]\n',
|
||||
stderr=b'Removed [core.reporting]\n',
|
||||
env=env)
|
||||
config_set('core.reporting', 'false', env)
|
||||
|
||||
@@ -185,7 +190,6 @@ def test_unset_top_property(env):
|
||||
b"Property 'core' doesn't fully specify a value - "
|
||||
b"possible properties are:\n"
|
||||
b"core.dcos_url\n"
|
||||
b"core.email\n"
|
||||
b"core.reporting\n"
|
||||
b"core.ssl_verify\n"
|
||||
b"core.timeout\n"
|
||||
|
||||
@@ -26,6 +26,7 @@ for easy management of a DCOS installation.
|
||||
|
||||
Available DCOS commands:
|
||||
|
||||
\tauth \tAuthenticate to DCOS cluster
|
||||
\tconfig \tManage the DCOS configuration file
|
||||
\thelp \tDisplay help information about DCOS
|
||||
\tmarathon \tDeploy and manage applications to DCOS
|
||||
@@ -75,3 +76,9 @@ def test_help_task():
|
||||
with open('tests/data/help/task.txt') as content:
|
||||
assert_command(['dcos', 'help', 'task'],
|
||||
stdout=content.read().encode('utf-8'))
|
||||
|
||||
|
||||
def test_help_auth():
|
||||
with open('tests/data/help/auth.txt') as content:
|
||||
assert_command(['dcos', 'help', 'auth'],
|
||||
stdout=content.read().encode('utf-8'))
|
||||
|
||||
@@ -4,14 +4,11 @@ from functools import wraps
|
||||
|
||||
import dcoscli.analytics
|
||||
import rollbar
|
||||
from dcos import constants, http
|
||||
from dcoscli.constants import SEGMENT_URL
|
||||
from dcos import constants
|
||||
from dcoscli.main import main
|
||||
from dcoscli.subcommand import SubcommandMain
|
||||
|
||||
from mock import patch
|
||||
|
||||
from .common import mock_called_some_args
|
||||
|
||||
ANON_ID = 0
|
||||
USER_ID = 'test@mail.com'
|
||||
@@ -32,22 +29,6 @@ def _mock(fn):
|
||||
return wrapper
|
||||
|
||||
|
||||
@_mock
|
||||
def test_config_set():
|
||||
argv = ['set', 'core.email', 'test@mail.com']
|
||||
|
||||
config_thread = SubcommandMain("config", argv)
|
||||
exitcode, err = config_thread.run_and_capture()
|
||||
assert exitcode == 0
|
||||
assert err is None
|
||||
|
||||
# segment.io
|
||||
assert mock_called_some_args(http.post,
|
||||
'{}/identify'.format(SEGMENT_URL),
|
||||
json={'userId': 'test@mail.com'},
|
||||
timeout=(1, 1))
|
||||
|
||||
|
||||
@_mock
|
||||
def test_cluster_id_not_sent_on_config_call():
|
||||
"""Tests that cluster_id is not sent to segment.io on call to config
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
from dcos 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_anonymous_login():
|
||||
with patch('sys.stdin.readline', return_value='\n'), \
|
||||
patch('uuid.uuid1', return_value='anonymous@email'):
|
||||
|
||||
assert _mock_dcos_run(['dcos', 'help'], False) == 0
|
||||
assert _mock_dcos_run(['dcos',
|
||||
'config', 'show', 'core.email'], False) == 0
|
||||
assert _mock_dcos_run(['dcos',
|
||||
'config', 'unset', 'core.email'], False) == 0
|
||||
|
||||
|
||||
def _mock_dcos_run(args, authenticated=True):
|
||||
if authenticated:
|
||||
env = _config_with_credentials()
|
||||
else:
|
||||
env = _config_without_credentials()
|
||||
|
||||
with patch('sys.argv', args), patch.dict(os.environ, env):
|
||||
return main()
|
||||
|
||||
|
||||
def test_when_authenticated():
|
||||
with patch('dcos.auth.force_auth'):
|
||||
|
||||
_mock_dcos_run(['dcos'], True)
|
||||
assert auth.force_auth.call_count == 0
|
||||
|
||||
|
||||
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')
|
||||
}
|
||||
@@ -25,6 +25,14 @@ def test_get_auth_scheme_acs():
|
||||
assert realm == "acsjwt"
|
||||
|
||||
|
||||
def test_get_auth_scheme_oauth():
|
||||
with patch('requests.Response') as mock:
|
||||
mock.headers = {'www-authenticate': 'oauthjwt'}
|
||||
auth_scheme, realm = http.get_auth_scheme(mock)
|
||||
assert auth_scheme == "oauthjwt"
|
||||
assert realm == "oauthjwt"
|
||||
|
||||
|
||||
def test_get_auth_scheme_bad_request():
|
||||
with patch('requests.Response') as mock:
|
||||
mock.headers = {'www-authenticate': ''}
|
||||
@@ -116,6 +124,23 @@ def test_request_with_bad_auth_acl(mock, req_mock, auth_mock):
|
||||
assert e.exconly().split(':')[1].strip() == "Authentication failed"
|
||||
|
||||
|
||||
@patch('requests.Response')
|
||||
@patch('dcos.http._request')
|
||||
@patch('dcos.http._get_http_auth')
|
||||
def test_request_with_bad_oauth(mock, req_mock, auth_mock):
|
||||
mock.url = 'http://domain.com'
|
||||
mock.headers = {'www-authenticate': 'oauthjwt'}
|
||||
mock.status_code = 401
|
||||
|
||||
auth_mock.return_value = http.DCOSAcsAuth("token")
|
||||
|
||||
req_mock.return_value = mock
|
||||
|
||||
with pytest.raises(DCOSException) as e:
|
||||
http._request_with_auth(mock, "method", mock.url)
|
||||
assert e.exconly().split(':')[1].strip() == "Authentication failed"
|
||||
|
||||
|
||||
@patch('requests.Response')
|
||||
@patch('dcos.http._request')
|
||||
@patch('dcos.http._get_http_auth')
|
||||
@@ -152,3 +177,22 @@ def test_request_with_auth_acl(mock, req_mock, auth_mock):
|
||||
|
||||
response = http._request_with_auth(mock, "method", mock.url)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@patch('requests.Response')
|
||||
@patch('dcos.http._request')
|
||||
@patch('dcos.http._get_http_auth')
|
||||
def test_request_with_auth_oauth(mock, req_mock, auth_mock):
|
||||
mock.url = 'http://domain.com'
|
||||
mock.headers = {'www-authenticate': 'oauthjwt'}
|
||||
mock.status_code = 401
|
||||
|
||||
auth = http.DCOSAcsAuth("token")
|
||||
auth_mock.return_value = auth
|
||||
|
||||
mock2 = copy.deepcopy(mock)
|
||||
mock2.status_code = 200
|
||||
req_mock.return_value = mock2
|
||||
|
||||
response = http._request_with_auth(mock, "method", mock.url)
|
||||
assert response.status_code == 200
|
||||
|
||||
138
dcos/auth.py
138
dcos/auth.py
@@ -1,138 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import pkg_resources
|
||||
from dcos import config, emitting, errors, http, jsonitem, util
|
||||
from dcos.errors import DCOSException
|
||||
from six import iteritems
|
||||
|
||||
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()
|
||||
logger = util.get_logger(__name__)
|
||||
|
||||
|
||||
def _authorize():
|
||||
"""Create OAuth flow and authorize user
|
||||
|
||||
:return: credentials dict
|
||||
:rtype: dict
|
||||
"""
|
||||
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'
|
||||
)
|
||||
return _run(flow)
|
||||
except:
|
||||
logger.exception('Error during OAuth web flow')
|
||||
raise DCOSException('There was a problem with '
|
||||
'web authentication.')
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
emitter.publish(
|
||||
errors.DefaultError(
|
||||
'\n\n\n{}\n\n {}\n\n'.format(
|
||||
'Go to the following link in your browser:',
|
||||
flow.step1_get_authorize_url())))
|
||||
|
||||
sys.stderr.write('Enter verification code: ')
|
||||
code = sys.stdin.readline().strip()
|
||||
if not code:
|
||||
sys.stderr.write('Skipping authentication.\nEnter email address: ')
|
||||
|
||||
email = sys.stdin.readline().strip()
|
||||
if not email:
|
||||
emitter.publish(
|
||||
errors.DefaultError(
|
||||
'Skipping email input.'))
|
||||
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 = util.get_config()
|
||||
return dcos_config.get('core.email', '') != ''
|
||||
|
||||
|
||||
def force_auth():
|
||||
"""Make user authentication process
|
||||
|
||||
:returns authentication process status
|
||||
:rtype: boolean
|
||||
"""
|
||||
|
||||
credentials = _authorize()
|
||||
_save_auth_keys(credentials)
|
||||
|
||||
|
||||
def _save_auth_keys(key_dict):
|
||||
"""
|
||||
:param key_dict: auth parameters dict
|
||||
:type key_dict: dict
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
toml_config = util.get_config(True)
|
||||
|
||||
section = 'core'
|
||||
config_schema = json.loads(
|
||||
pkg_resources.resource_string(
|
||||
'dcos',
|
||||
'data/config-schema/core.json').decode('utf-8'))
|
||||
for k, v in iteritems(key_dict):
|
||||
python_value = jsonitem.parse_json_value(k, v, config_schema)
|
||||
name = '{}.{}'.format(section, k)
|
||||
toml_config[name] = python_value
|
||||
|
||||
config.save(toml_config)
|
||||
return None
|
||||
@@ -130,7 +130,7 @@ def unset(name):
|
||||
elif isinstance(value, collections.Mapping):
|
||||
raise DCOSException(_generate_choice_msg(name, value))
|
||||
else:
|
||||
emitter.publish("Removed [{}]".format(name))
|
||||
emitter.publish(DefaultError("Removed [{}]".format(name)))
|
||||
save(toml_config)
|
||||
return
|
||||
|
||||
|
||||
@@ -13,22 +13,12 @@
|
||||
"title": "DCOS ACS token",
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"description": "Your email address",
|
||||
"title": "Your email address",
|
||||
"type": "string"
|
||||
},
|
||||
"mesos_master_url": {
|
||||
"description": "Mesos master URL. Must be set in format: \"http://host:port\"",
|
||||
"format": "uri",
|
||||
"title": "Mesos Master URL",
|
||||
"type": "string"
|
||||
},
|
||||
"refresh_token": {
|
||||
"description": "Your OAuth refresh token",
|
||||
"title": "The OAuth refresh token",
|
||||
"type": "string"
|
||||
},
|
||||
"reporting": {
|
||||
"default": true,
|
||||
"description": "Whether to report usage events to Mesosphere",
|
||||
@@ -42,11 +32,6 @@
|
||||
"title": "Request timeout in seconds",
|
||||
"type": "integer"
|
||||
},
|
||||
"token": {
|
||||
"description": "Your OAuth access token",
|
||||
"title": "Your OAuth access token",
|
||||
"type": "string"
|
||||
},
|
||||
"ssl_verify": {
|
||||
"type": "string",
|
||||
"default": "false",
|
||||
|
||||
72
dcos/http.py
72
dcos/http.py
@@ -161,9 +161,13 @@ def _request_with_auth(response,
|
||||
if creds not in AUTH_CREDS and response.status_code == 200:
|
||||
AUTH_CREDS[creds] = auth
|
||||
# acs invalid token
|
||||
elif response.status_code == 401 and auth_scheme == "acsjwt":
|
||||
elif response.status_code == 401 and \
|
||||
auth_scheme in ["acsjwt", "oauthjwt"]:
|
||||
|
||||
if util.get_config().get("core.dcos_acs_token") is not None:
|
||||
config.unset("core.dcos_acs_token")
|
||||
msg = ("Your core.dcos_acs_token is invalid. "
|
||||
"Please run: `dcos auth login`")
|
||||
raise DCOSException(msg)
|
||||
|
||||
i += 1
|
||||
|
||||
@@ -342,7 +346,7 @@ def _get_auth_credentials(username, hostname):
|
||||
|
||||
def get_auth_scheme(response):
|
||||
"""Return authentication scheme and realm requested by server for 'Basic'
|
||||
or 'acsjwt' (DCOS acs auth) type or None
|
||||
or 'acsjwt' (DCOS acs auth) or 'oauthjwt' (DCOS acs oauth) type or None
|
||||
|
||||
:param response: requests.response
|
||||
:type response: requests.Response
|
||||
@@ -354,7 +358,8 @@ def get_auth_scheme(response):
|
||||
auths = response.headers['www-authenticate'].split(',')
|
||||
scheme = next((auth_type.rstrip().lower() for auth_type in auths
|
||||
if auth_type.rstrip().lower().startswith("basic") or
|
||||
auth_type.rstrip().lower().startswith("acsjwt")),
|
||||
auth_type.rstrip().lower().startswith("acsjwt") or
|
||||
auth_type.rstrip().lower().startswith("oauthjwt")),
|
||||
None)
|
||||
if scheme:
|
||||
scheme_info = scheme.split("=")
|
||||
@@ -385,7 +390,7 @@ def _get_http_auth(response, url, auth_scheme):
|
||||
password = url.password
|
||||
|
||||
if 'www-authenticate' in response.headers:
|
||||
if auth_scheme not in ['basic', 'acsjwt']:
|
||||
if auth_scheme not in ['basic', 'acsjwt', 'oauthjwt']:
|
||||
msg = ("Server responded with an HTTP 'www-authenticate' field of "
|
||||
"'{}', DCOS only supports 'Basic'".format(
|
||||
response.headers['www-authenticate']))
|
||||
@@ -396,17 +401,59 @@ def _get_http_auth(response, url, auth_scheme):
|
||||
# we'd already be authed by python requests module
|
||||
username, password = _get_auth_credentials(username, hostname)
|
||||
return HTTPBasicAuth(username, password)
|
||||
# dcos auth (acs or oauth)
|
||||
else:
|
||||
return _get_dcos_acs_auth(username, password, hostname)
|
||||
return _get_dcos_auth(auth_scheme, username, password, hostname)
|
||||
else:
|
||||
msg = ("Invalid HTTP response: server returned an HTTP 401 response "
|
||||
"with no 'www-authenticate' field")
|
||||
raise DCOSException(msg)
|
||||
|
||||
|
||||
def _get_dcos_acs_auth(username, password, hostname):
|
||||
"""Get authentication flow for dcos acs auth
|
||||
def _get_dcos_oauth_creds(dcos_url):
|
||||
"""Get token credential for dcos oath
|
||||
|
||||
:param dcos_url: dcos cluster url
|
||||
:type dcos_url: str
|
||||
:returns: token from browser for oauth flow
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
oauth_login = 'login?redirect_uri=urn:ietf:wg:oauth:2.0:oob'
|
||||
url = urllib.parse.urljoin(dcos_url, oauth_login)
|
||||
msg = "\n{}\n\n {}\n\n{} ".format(
|
||||
"Please go to the following link in your browser:",
|
||||
url,
|
||||
"Enter authentication token:")
|
||||
sys.stderr.write(msg)
|
||||
sys.stderr.flush()
|
||||
token = sys.stdin.readline().strip()
|
||||
return {"token": token}
|
||||
|
||||
|
||||
def _get_dcos_acs_auth_creds(username, password, hostname):
|
||||
"""Get credentials for dcos acs auth
|
||||
|
||||
:param username: username user for authentication
|
||||
:type username: str
|
||||
:param password: password for authentication
|
||||
:type password: str
|
||||
:param hostname: hostname for credentials
|
||||
:type hostname: str
|
||||
:returns: username/password credentials
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
if password is None:
|
||||
username, password = _get_auth_credentials(username, hostname)
|
||||
return {"uid": username, "password": password}
|
||||
|
||||
|
||||
def _get_dcos_auth(auth_scheme, username, password, hostname):
|
||||
"""Get authentication flow for dcos acs auth and dcos oauth
|
||||
|
||||
:param auth_scheme: authentication_scheme
|
||||
:type auth_scheme: str
|
||||
:param username: username user for authentication
|
||||
:type username: str
|
||||
:param password: password for authentication
|
||||
@@ -421,16 +468,17 @@ def _get_dcos_acs_auth(username, password, hostname):
|
||||
token = toml_config.get("core.dcos_acs_token")
|
||||
if token is None:
|
||||
dcos_url = toml_config.get("core.dcos_url")
|
||||
url = urllib.parse.urljoin(dcos_url, 'acs/api/v1/auth/login')
|
||||
if password is None:
|
||||
username, password = _get_auth_credentials(username, hostname)
|
||||
creds = {"uid": username, "password": password}
|
||||
if auth_scheme == "acsjwt":
|
||||
creds = _get_dcos_acs_auth_creds(username, password, hostname)
|
||||
else:
|
||||
creds = _get_dcos_oauth_creds(dcos_url)
|
||||
|
||||
verify = _verify_ssl()
|
||||
# Silence 'Unverified HTTPS request' and 'SecurityWarning' for bad cert
|
||||
if verify is not None:
|
||||
silence_requests_warnings()
|
||||
|
||||
url = urllib.parse.urljoin(dcos_url, 'acs/api/v1/auth/login')
|
||||
# using private method here, so we don't retry on this request
|
||||
# error here will be bubbled up to _request_with_auth
|
||||
response = _request('post', url, json=creds, verify=verify)
|
||||
|
||||
@@ -134,8 +134,8 @@ def default_subcommands():
|
||||
:returns: list of all the default dcos cli subcommands
|
||||
:rtype: [str]
|
||||
"""
|
||||
|
||||
return ["config", "help", "marathon", "node", "package", "service", "task"]
|
||||
return ["auth", "config", "help", "marathon",
|
||||
"node", "package", "service", "task"]
|
||||
|
||||
|
||||
def documentation(executable_path):
|
||||
|
||||
Reference in New Issue
Block a user