Skip to content

Commit

Permalink
Merge pull request #81 from jathanism/fix-auth_header
Browse files Browse the repository at this point in the history
Fixes #80 - Associate auth_header setting w/ auth_header method.
  • Loading branch information
dmar42 committed Mar 9, 2016
2 parents 5aed09e + b56dade commit bd3eaf8
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 103 deletions.
20 changes: 11 additions & 9 deletions pynsot/app.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

"""
Base CLI commands for all objects. Model-specific objects and argument parsers
will be defined in subclasses or by way of factory methods.
"""

__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015 Dropbox, Inc.'
Base CLI commands for all objects.
Model-specific objects and argument parsers will be defined in subclasses or by
way of factory methods.
"""

from __future__ import unicode_literals
import datetime
import logging
import os
Expand All @@ -26,6 +22,12 @@
from .vendor.slumber.exceptions import (HttpClientError, HttpServerError)


__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015 Dropbox, Inc.'


# Constants/Globals
if os.getenv('DEBUG'):
logging.basicConfig(level=logging.DEBUG)
Expand Down
29 changes: 12 additions & 17 deletions pynsot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@
AuthTokenClient(url=http://localhost:8990/api)>
"""

__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015-2016 Dropbox, Inc.'


from __future__ import unicode_literals
import getpass
import json
import logging
Expand All @@ -30,17 +25,17 @@
from .vendor.slumber.exceptions import HttpClientError

from .util import get_result
from . import dotfile
from . import constants, dotfile


# Logger
log = logging.getLogger(__name__)
__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015-2016 Dropbox, Inc.'

# Header used for passthrough authentication.
AUTH_HEADER = 'X-NSoT-Email'

# Default authentication method
DEFAULT_AUTH_METHOD = 'auth_token'
# Logger
log = logging.getLogger(__name__)


__all__ = (
Expand Down Expand Up @@ -205,7 +200,7 @@ def __init__(self, client):
super(EmailHeaderAuthentication, self).__init__(client)
email = self.kwargs.pop('email', None)
default_domain = self.kwargs.pop('default_domain', 'localhost')
auth_header = self.kwargs.pop('auth_header', AUTH_HEADER)
auth_header = self.kwargs.pop('auth_header', constants.AUTH_HEADER)

if email is None and default_domain:
log.debug('No email provided; Using default_domain: %r',
Expand Down Expand Up @@ -238,7 +233,7 @@ def __call__(self, r):
class EmailHeaderClient(BaseClient):
"""Default client using email auth header method."""
authentication_class = EmailHeaderAuthentication
required_arguments = ('email', 'default_domain')
required_arguments = ('email', 'default_domain', 'auth_header')


class AuthTokenAuthentication(BaseClientAuth):
Expand Down Expand Up @@ -314,7 +309,7 @@ class AuthTokenClient(BaseClient):
}

#: Default client class
Client = AUTH_CLIENTS[DEFAULT_AUTH_METHOD]
Client = AUTH_CLIENTS[constants.DEFAULT_AUTH_METHOD]


def get_auth_client_info(auth_method):
Expand Down Expand Up @@ -373,7 +368,7 @@ def get_api_client(auth_method=None, url=None, extra_args=None):
arg_names = client_class.required_arguments

# Allow optional arguments in arg_names
optional_args = tuple(dotfile.OPTIONAL_FIELDS)
optional_args = tuple(constants.OPTIONAL_FIELDS)
arg_names += optional_args

# Remove non-relavant args
Expand Down
51 changes: 51 additions & 0 deletions pynsot/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-

"""
Constant values used across the project.
"""

from __future__ import unicode_literals
import os


__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2016 Dropbox, Inc.'


# Header used for passthrough authentication.
AUTH_HEADER = 'X-NSoT-Email'

# Default authentication method
DEFAULT_AUTH_METHOD = 'auth_token'

# Mapping of required field names and default values we want to be in the
# dotfile.
REQUIRED_FIELDS = {
'auth_method': ['auth_token', 'auth_header'],
'url': None,
}

# Fields that map to specific auth_methods.
SPECIFIC_FIELDS = {
'auth_header': {
'default_domain': 'localhost',
'auth_header': AUTH_HEADER,
}
}

# Mapping of optional field names and default values (if any)
OPTIONAL_FIELDS = {
'default_site': None,
'api_version': None,
}

# Path stuff
USER_HOME = os.path.expanduser('~')
DOTFILE_NAME = '.pynsotrc'
DOTFILE_PATH = os.path.join(USER_HOME, DOTFILE_NAME)
DOTFILE_PERMS = 0600 # -rw-------

# Config section name
SECTION_NAME = 'pynsot'
95 changes: 49 additions & 46 deletions pynsot/dotfile.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,28 @@
# -*- coding: utf-8 -*-

"""
Handle the read, write, and generation of the .pynsotrc config file.
"""

__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015 Dropbox, Inc.'


from __future__ import unicode_literals
from ConfigParser import RawConfigParser
import copy
import logging
import os

from .vendor import click
from .vendor import rcfile
from . import constants


# Logging object
log = logging.getLogger(__name__)

# Mapping of required field names and default values we want to be in the
# dotfile.
REQUIRED_FIELDS = {
'auth_method': ['auth_token', 'auth_header'],
'url': None,
}

# Mapping of optional field names and default values (if any)
OPTIONAL_FIELDS = {
'default_site': None,
'api_version': None,
}
__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015-2016 Dropbox, Inc.'

# Path stuff
USER_HOME = os.path.expanduser('~')
DOTFILE_NAME = '.pynsotrc'
DOTFILE_PATH = os.path.join(USER_HOME, DOTFILE_NAME)
DOTFILE_PERMS = 0600 # -rw-------

# Config section name
SECTION_NAME = 'pynsot'
# Logging object
log = logging.getLogger(__name__)


__all__ = (
Expand All @@ -54,14 +36,16 @@ class DotfileError(Exception):

class Dotfile(object):
"""Create, read, and write a dotfile."""
def __init__(self, filepath=DOTFILE_PATH, **kwargs):
def __init__(self, filepath=constants.DOTFILE_PATH, **kwargs):
self.filepath = filepath

def read(self, **kwargs):
"""
Read ``~/.pynsotrc`` and return it as a dict.
"""
config = rcfile.rcfile(SECTION_NAME, args={'config': self.filepath})
config = rcfile.rcfile(
constants.SECTION_NAME, args={'config': self.filepath}
)
config.pop('config', None) # We don't need this field in here.

# If there is *no* config data so far and...
Expand Down Expand Up @@ -92,28 +76,31 @@ def validate_perms(self):
# Ownership
s = os.stat(self.filepath)
if s.st_uid != os.getuid():
msg = '%s: %s must be owned by you' % (DOTFILE_NAME, self.filepath)
raise DotfileError(msg)
raise DotfileError(
'%s: %s must be owned by you' % (
constants.DOTFILE_NAME, self.filepath
)
)

# Permissions
self.enforce_perms()

def validate_fields(self, field_names, required_fields=None):
def validate_fields(self, field_names, required_fields):
"""
Make sure all the fields are set.
:param field_names:
List of field names to validate
"""
if required_fields is None:
required_fields = REQUIRED_FIELDS
:param required_fields:
List of required field names to check against
"""
for rname in sorted(required_fields):
if rname not in field_names:
msg = '%s: Missing required field: %s' % (self.filepath, rname)
raise DotfileError(msg)

def enforce_perms(self, perms=DOTFILE_PERMS):
def enforce_perms(self, perms=constants.DOTFILE_PERMS):
"""
Enforce permissions on the dotfile.
Expand All @@ -136,7 +123,7 @@ def write(self, config_data, filepath=None):
if filepath is None:
filepath = self.filepath
config = RawConfigParser()
section = SECTION_NAME
section = constants.SECTION_NAME
config.add_section(section)

# Set the config settings
Expand All @@ -161,13 +148,20 @@ def get_required_fields(cls, auth_method, required_fields=None):
Mapping of required field names to default values
"""
if required_fields is None:
required_fields = copy.deepcopy(REQUIRED_FIELDS)
required_fields = copy.deepcopy(constants.REQUIRED_FIELDS)

from .client import AUTH_CLIENTS # To avoid circular import

# Fields that do not have default values, which are specific to an
# auth_method.
auth_fields = AUTH_CLIENTS[auth_method].required_arguments
auth_items = dict.fromkeys(auth_fields)
required_fields.update(auth_items)
non_specific_fields = dict.fromkeys(auth_fields)
required_fields.update(non_specific_fields)

# Fields that MAY have default values, which are specific to an
# auth_method, if any.
specific_fields = constants.SPECIFIC_FIELDS.get(auth_method, {})
required_fields.update(specific_fields)

return required_fields

Expand All @@ -191,10 +185,10 @@ def get_config_data(cls, required_fields=None, optional_fields=None,
Dict of config data
"""
if required_fields is None:
required_fields = REQUIRED_FIELDS
required_fields = constants.REQUIRED_FIELDS

if optional_fields is None:
optional_fields = OPTIONAL_FIELDS
optional_fields = constants.OPTIONAL_FIELDS

config_data = {}

Expand Down Expand Up @@ -234,6 +228,8 @@ def process_fields(config_data, field_items, optional=False, **kwargs):
Keyword arguments of prepared values
"""
for field, default_value in field_items.iteritems():
prompt = 'Please enter %s' % (field,)

# If it's already in the config data, move on.
if field in config_data:
continue
Expand All @@ -242,13 +238,20 @@ def process_fields(config_data, field_items, optional=False, **kwargs):
if field not in kwargs:
# If it does not have a default value prompt for it
if default_value is None:
prompt = 'Please enter %s' % (field,)
if not optional:
value = click.prompt(prompt, type=str)
else:
prompt += ' (optional)'
value = click.prompt(prompt, default='',
show_default=False)
value = click.prompt(
prompt, default='', type=str, show_default=False
)

# If the default_value is a string, prompt for it, but present
# it as a default.
elif isinstance(default_value, basestring):
value = click.prompt(
prompt, type=str, default=default_value
)

# If it's a list of options, present the choices.
elif isinstance(default_value, list):
Expand Down
2 changes: 1 addition & 1 deletion pynsot/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.19'
__version__ = '0.19.1'
26 changes: 20 additions & 6 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,16 +290,30 @@

# Dummy config data used for testing dotfile and client
CONFIG_DATA = {
'email': 'jathan@localhost',
'url': 'http://localhost:8990/api',
'auth_method': 'auth_token',
'secret_key': 'MJMOl9W7jqQK3h-quiUR-cSUeuyDRhbn2ca5E31sH_I=',
'auth_token': {
'email': 'jathan@localhost',
'url': 'http://localhost:8990/api',
'auth_method': 'auth_token',
'secret_key': 'MJMOl9W7jqQK3h-quiUR-cSUeuyDRhbn2ca5E31sH_I=',
},
'auth_header': {
'email': 'jathan@localhost',
'url': 'http://localhost:8990/api',
'auth_method': 'auth_header',
'default_domain': 'localhost',
'auth_header': 'X-NSoT-Email',
}
}


@pytest.fixture
def config():
return CONFIG_DATA
def auth_token_config():
return CONFIG_DATA['auth_token']


@pytest.fixture
def auth_header_config():
return CONFIG_DATA['auth_header']


# Payload used to create Network & Device attributes used for testing.
Expand Down

0 comments on commit bd3eaf8

Please sign in to comment.