Skip to content

Feature/multiple profiles #12

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

Merged
merged 68 commits into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
618d899
Rename section name
Mar 9, 2020
65ea9cd
Working
Mar 10, 2020
85e3ceb
Make showing profile work
Mar 10, 2020
a3e5961
List
Mar 10, 2020
8af84ed
Format
Mar 10, 2020
f25b0c8
Make work
Mar 10, 2020
d13ad88
Test profile
Mar 10, 2020
0b12a7e
Tests pas
Mar 10, 2020
2baf042
Update doc
Mar 10, 2020
25d88ee
Update doc
Mar 10, 2020
0b20d04
Format?
Mar 10, 2020
e2cdfb4
Use use
Mar 10, 2020
aec835e
Update readme:
Mar 10, 2020
f6ebfa7
I forgot what was this commit is for
Mar 11, 2020
e90bd27
Oh ya
Mar 11, 2020
695124d
Refactor
Mar 11, 2020
2b9ad29
Fix a test
Mar 11, 2020
e496129
All tests pass exclude config tests
Mar 11, 2020
98703f4
Write some config tests that I actually like
Mar 11, 2020
b0242cc
Fix show
Mar 11, 2020
a31b07e
Made switch actually work
Mar 11, 2020
2880d75
Fix merge flicts
Mar 11, 2020
be1c501
Prevent saving config file during tests
Mar 11, 2020
ef04e58
Remove flicts from cl
Mar 11, 2020
396d454
Update cl
Mar 11, 2020
a3a4bf0
Fix readme
Mar 11, 2020
fe78da1
Prevent db from being created in tests
Mar 11, 2020
3fd91a2
Support list search filters
Mar 11, 2020
ad44063
Remove unused const
Mar 11, 2020
331e7a2
Take a profile for reset pw
Mar 11, 2020
0d1cbf9
Add docstrings
Mar 11, 2020
54b9824
More tests
Mar 11, 2020
af9e43f
Fix advanced query
Mar 11, 2020
2015cea
Mention wildcards in readme
Mar 11, 2020
298d047
Remove autouse
Mar 11, 2020
d5964b4
Reset mock
Mar 11, 2020
cf0effa
Just check that save is called
Mar 11, 2020
0a1598d
Fix cursor store so it supports multiple rows
Mar 11, 2020
5cd88c8
Update docstrs
Mar 11, 2020
478c4d2
Update docstrs
Mar 11, 2020
7983961
Missing u
Mar 11, 2020
6f32070
Change method name
Mar 11, 2020
da8c602
Update changelog
Mar 12, 2020
dcbc30b
Made internal internal
Mar 12, 2020
1b9a709
Fix docstr
Mar 12, 2020
6744fd1
Fix docstrs
Mar 12, 2020
3f44734
Fix docstrs
Mar 12, 2020
7bf30b7
Add test
Mar 12, 2020
e9e3413
Fix cl
Mar 12, 2020
5f3e658
Update readme
Mar 12, 2020
979c918
Update readme
Mar 12, 2020
1704072
Update readme
Mar 12, 2020
5058bfb
Merge master
Mar 12, 2020
ed873a3
Strip and format
Mar 12, 2020
f3adb3c
mention format in readme
Mar 12, 2020
451df3b
Mention dev unrecommended ignore of ssl errors
Mar 12, 2020
3be7c91
Strip at the right spot
Mar 12, 2020
886e2e9
missing us
Mar 12, 2020
4bb9c7e
Delete space
Mar 12, 2020
b7c0bc1
Optimize imports
Mar 12, 2020
3236748
Update CHANGELOG.md
alanag13 Mar 12, 2020
857a651
Fix missing values exit
Mar 12, 2020
600a8b3
Merge branch 'feature/multiple-profiles' of https://github.com/code42…
Mar 12, 2020
bde152e
Fix issue where clearing checkpoint deleted everyones checkpoint
Mar 12, 2020
f34ad14
Create filters before sdk to validate args sooner
Mar 12, 2020
a2f001c
Reorganize extraction
Mar 12, 2020
351f5df
Bumps
Mar 12, 2020
44e049f
Print help messagE
Mar 12, 2020
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta

## Unreleased

### Added

- Support for multiple profiles:
- Optional `--profile` flag for `securitydata`, `profile show`, `profile set`.
- `code42 profile use` for changing the default profile.
- `code42 profile list` for listing all the available profiles.

### Fixed

- Fixed bug where port attached to `securitydata send-to` command was not properly applied.
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ $ python setup.py install

First, set your profile:
```bash
code42 profile set -s https://example.authority.com -u [email protected]
code42 profile set --profile MY_FIRST_PROFILE -s https://example.authority.com -u [email protected]
```
The `--profile` flag is required the first time and it takes a name.
On subsequent uses of `set`, not specifying the profile will set the default profile.

Your profile contains the necessary properties for logging into Code42 servers.
After running this `code42 profile set`, you will be prompted about storing a password.
After running `code42 profile set`, you will be prompted about storing a password.
If you agree, you will be securely prompted to input your password.
Your password is not stored in plain-text, and is not shown when you do `code42 profile show`.
However, `code42 profile show` will confirm that there is a password set for your profile.
Expand All @@ -40,6 +43,16 @@ To re-enable SSL errors, do:
code42 profile set --enable-ssl-errors
```

You can add multiple profiles with different names and the change the default profile with the `use` command:
```bash
code42 profile use MY_SECOND_PROFILE
```

To see all your profiles, do:
```bash
code42 profile list
```

Next, you can query for events and send them to three possible destination types
* stdout
* A file
Expand Down
25 changes: 25 additions & 0 deletions src/code42cli/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
PROFILE_NAME_KEY = u"profile_name"
PROFILE_HELP_MESSAGE = (
u"The name of the profile containing your Code42 username and authority host address."
)


def add_arguments_to_parser(parser):
add_debug_arg(parser)
add_profile_name_arg(parser)


def add_debug_arg(parser):
parser.add_argument(
u"-d",
u"--debug",
dest=u"is_debug_mode",
action=u"store_true",
help=u"Turn on Debug logging.",
)


def add_profile_name_arg(parser):
parser.add_argument(
u"--profile", action=u"store", dest=PROFILE_NAME_KEY, help=PROFILE_HELP_MESSAGE
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mirrors azure -Profile arg

)
4 changes: 4 additions & 0 deletions src/code42cli/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
from urlparse import urljoin, urlparse

str = unicode

import repr as reprlib
else:
from urllib.parse import urljoin, urlparse

str = str

import reprlib
244 changes: 117 additions & 127 deletions src/code42cli/profile/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,136 +6,126 @@
import code42cli.util as util
from code42cli.compat import str

_DEFAULT_VALUE = u"__DEFAULT__"
_PROFILE_ENV_VAR_NAME = u"CODE42CLI_PROFILE"


class ConfigurationKeys(object):
USER_SECTION = u"Code42"
class ConfigAccessor(object):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This object was the refactor I mentioned that made things much more straightforward and testable.

DEFAULT_VALUE = u"__DEFAULT__"
AUTHORITY_KEY = u"c42_authority_url"
USERNAME_KEY = u"c42_username"
IGNORE_SSL_ERRORS_KEY = u"ignore-ssl-errors"
INTERNAL_SECTION = u"Internal"
HAS_SET_PROFILE_KEY = u"has_set_profile"


def get_config_profile():
"""Get your config file profile."""
parser = ConfigParser()
if not profile_has_been_set():
util.print_error(u"Profile has not completed setup.")
print(u"")
print(u"To set, use: ")
util.print_bold(u"\tcode42 profile set -s <authority-URL> -u <username>")
print(u"")
exit(1)

return _get_config_profile_from_parser(parser)


def profile_has_been_set():
"""Whether you have, at one point in time, set your username and authority server URL."""
parser = ConfigParser()
config_file_path = _get_config_file_path()
parser.read(config_file_path)
settings = parser[ConfigurationKeys.INTERNAL_SECTION]
return settings.getboolean(ConfigurationKeys.HAS_SET_PROFILE_KEY)


def mark_as_set_if_complete():
if not _profile_can_be_set():
return
parser = ConfigParser()
config_file_path = _get_config_file_path()
parser.read(config_file_path)
settings = parser[ConfigurationKeys.INTERNAL_SECTION]
settings[ConfigurationKeys.HAS_SET_PROFILE_KEY] = u"True"
_save(parser, ConfigurationKeys.HAS_SET_PROFILE_KEY)


def set_username(new_username):
parser = ConfigParser()
profile = _get_config_profile_from_parser(parser)
profile[ConfigurationKeys.USERNAME_KEY] = new_username
_save(parser, ConfigurationKeys.USERNAME_KEY)


def set_authority_url(new_url):
parser = ConfigParser()
profile = _get_config_profile_from_parser(parser)
profile[ConfigurationKeys.AUTHORITY_KEY] = new_url
_save(parser, ConfigurationKeys.AUTHORITY_KEY)


def set_ignore_ssl_errors(new_value):
parser = ConfigParser()
profile = _get_config_profile_from_parser(parser)
profile[ConfigurationKeys.IGNORE_SSL_ERRORS_KEY] = str(new_value)
_save(parser, ConfigurationKeys.IGNORE_SSL_ERRORS_KEY)


def _profile_can_be_set():
"""Whether your current username and authority URL are set,
but your profile has not been marked as set.
"""
parser = ConfigParser()
profile = _get_config_profile_from_parser(parser)
username = profile[ConfigurationKeys.USERNAME_KEY]
authority = profile[ConfigurationKeys.AUTHORITY_KEY]
return username != _DEFAULT_VALUE and authority != _DEFAULT_VALUE and not profile_has_been_set()


def _get_config_profile_from_parser(parser):
config_file_path = _get_config_file_path()
parser.read(config_file_path)
config = parser[ConfigurationKeys.USER_SECTION]
return config


def _get_config_file_path():
path = u"{}config.cfg".format(util.get_user_project_path())
if not os.path.exists(path) or not _verify_config_file(path):
_create_new_config_file(path)
return path


def _create_new_config_file(path):
config_parser = ConfigParser()
config_parser = _create_user_section(config_parser)
config_parser = _create_internal_section(config_parser)
_save(config_parser, None, path)


def _create_user_section(parser):
keys = ConfigurationKeys
parser.add_section(keys.USER_SECTION)
parser[keys.USER_SECTION] = {}
parser[keys.USER_SECTION][keys.AUTHORITY_KEY] = _DEFAULT_VALUE
parser[keys.USER_SECTION][keys.USERNAME_KEY] = _DEFAULT_VALUE
parser[keys.USER_SECTION][keys.IGNORE_SSL_ERRORS_KEY] = u"False"
return parser


def _create_internal_section(parser):
keys = ConfigurationKeys
parser.add_section(keys.INTERNAL_SECTION)
parser[keys.INTERNAL_SECTION] = {}
parser[keys.INTERNAL_SECTION][keys.HAS_SET_PROFILE_KEY] = u"False"
return parser


def _save(parser, key=None, path=None):
path = _get_config_file_path() if path is None else path
util.open_file(path, u"w+", lambda f: parser.write(f))
if key is not None:
if key == ConfigurationKeys.HAS_SET_PROFILE_KEY:
print(u"You have completed setting up your profile!")
DEFAULT_PROFILE_IS_COMPLETE = u"default_profile_is_complete"
DEFAULT_PROFILE = u"default_profile"
_INTERNAL_SECTION = u"Internal"

def __init__(self, parser):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It takes a ConfigParser

self.parser = parser
self.path = u"{}config.cfg".format(util.get_user_project_path())
if not os.path.exists(self.path):
self._create_internal_section()
self._save()
else:
print(u"'{}' has been successfully updated".format(key))


def _verify_config_file(path):
keys = ConfigurationKeys
config_parser = ConfigParser()
config_parser.read(path)
sections = config_parser.sections()
return keys.USER_SECTION in sections and keys.INTERNAL_SECTION in sections
self.parser.read(self.path)

@property
def internal(self):
return self.parser[self._INTERNAL_SECTION]

def has_setup_default_profile(self):
return not len(self._get_profile_names())

def get_profile(self, name=None):
name = name or self._default_profile_name
if name not in self.parser.sections() or name == self.DEFAULT_VALUE:
raise Exception("Profile does not exist.")
return self.parser[name]

def get_all_profiles(self):
profiles = []
names = self._get_profile_names()
for name in names:
profiles.append(self.get_profile(name))
return profiles

def create_profile_if_not_exists(self, name):
try:
self.get_profile(name)
except Exception as ex:
if name is not None and name != self.DEFAULT_VALUE:
self._create_profile_section(name)
else:
raise ex

def switch_default_profile(self, new_default_name):
if self.get_profile(new_default_name) is None:
raise Exception("Profile does not exist.")
self.internal[self.DEFAULT_PROFILE] = new_default_name
self._save()

def set_authority_url(self, new_value, profile_name=None):
profile = self.get_profile(profile_name)
profile[self.AUTHORITY_KEY] = new_value
self._save()
self._try_complete_setup(profile)

def set_username(self, new_value, profile_name=None):
profile = self.get_profile(profile_name)
profile[self.USERNAME_KEY] = new_value
self._save()
self._try_complete_setup(profile)

def set_ignore_ssl_errors(self, new_value, profile_name=None):
profile = self.get_profile(profile_name)
profile[self.IGNORE_SSL_ERRORS_KEY] = str(new_value)
self._save()

@property
def _default_profile_name(self):
return self.internal[self.DEFAULT_PROFILE]

def _get_profile_names(self):
names = list(self.parser.sections())
names.remove(self._INTERNAL_SECTION)
return names

def _create_internal_section(self):
self.parser.add_section(self._INTERNAL_SECTION)
self.parser[self._INTERNAL_SECTION] = {}
self.parser[self._INTERNAL_SECTION][self.DEFAULT_PROFILE_IS_COMPLETE] = str(False)
self.parser[self._INTERNAL_SECTION][self.DEFAULT_PROFILE] = self.DEFAULT_VALUE

def _create_profile_section(self, name):
self.parser.add_section(name)
self.parser[name] = {}
self.parser[name][self.AUTHORITY_KEY] = self.DEFAULT_VALUE
self.parser[name][self.USERNAME_KEY] = self.DEFAULT_VALUE
self.parser[name][self.IGNORE_SSL_ERRORS_KEY] = str(False)
default_profile = self.internal.get(self.DEFAULT_PROFILE)
if default_profile is None or default_profile is self.DEFAULT_VALUE:
self.internal[self.DEFAULT_PROFILE] = name

def _save(self):
util.open_file(self.path, u"w+", lambda f: self.parser.write(f))

def _try_complete_setup(self, profile):
if self.internal.getboolean(self.DEFAULT_PROFILE_IS_COMPLETE):
return

authority = profile.get(self.AUTHORITY_KEY)
username = profile.get(self.USERNAME_KEY)

authority_valid = authority is not None and authority != self.DEFAULT_VALUE
username_valid = username is not None and username != self.DEFAULT_VALUE

if not authority_valid or not username_valid:
return

self.internal[self.DEFAULT_PROFILE_IS_COMPLETE] = str(True)
if self.internal[self.DEFAULT_PROFILE] == self.DEFAULT_VALUE:
self.internal[self.DEFAULT_PROFILE] = profile.name

self._save()


def get_config_accessor():
return ConfigAccessor(ConfigParser())
28 changes: 14 additions & 14 deletions src/code42cli/profile/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,27 @@

import keyring

import code42cli.profile.config as config
from code42cli.profile.config import ConfigurationKeys
from code42cli.profile.config import get_config_accessor, ConfigAccessor

_ROOT_SERVICE_NAME = u"code42cli"


def get_password():
"""Gets your currently stored password for your username / authority URL combo."""
profile = config.get_config_profile()
service_name = _get_service_name(profile)
def get_password(profile_name):
"""Gets your currently stored password for your profile."""
accessor = get_config_accessor()
profile = accessor.get_profile(profile_name)
service_name = _get_service_name(profile_name)
username = _get_username(profile)
password = keyring.get_password(service_name, username)
return password


def set_password_from_prompt():
"""Prompts and sets your password for your username / authority URL combo."""
def set_password_from_prompt(profile_name):
"""Prompts and sets your password for your profile."""
password = getpass()
profile = config.get_config_profile()
service_name = _get_service_name(profile)
accessor = get_config_accessor()
profile = accessor.get_profile(profile_name)
service_name = _get_service_name(profile_name)
username = _get_username(profile)
keyring.set_password(service_name, username, password)
print(u"'Code42 Password' updated.")
Expand All @@ -34,10 +35,9 @@ def get_password_from_prompt():
return getpass()


def _get_service_name(profile):
authority_url = profile[ConfigurationKeys.AUTHORITY_KEY]
return u"{}::{}".format(_ROOT_SERVICE_NAME, authority_url)
def _get_service_name(profile_name):
return u"{}::{}".format(_ROOT_SERVICE_NAME, profile_name)


def _get_username(profile):
return profile[ConfigurationKeys.USERNAME_KEY]
return profile[ConfigAccessor.USERNAME_KEY]
Loading