Merge pull request #166 from facetoe/credentials

Credentials
This commit is contained in:
enkore 2015-02-23 20:55:45 +01:00
commit eafbb60ecf
10 changed files with 163 additions and 8 deletions

View File

@ -122,3 +122,23 @@ Also change your i3wm config to the following:
position top
workspace_buttons yes
}
Settings that require credentials can utilize the keyring module to keep sensitive information out of config files.
To take advantage of this feature, simply use the setting_util.py script to set the credentials for a module. Once this
is done you can add the module to your config without specifying the credentials, eg:
::
# Use the default keyring to retrieve credentials. To determine which backend is the default on your system, run
# python -c 'import keyring; print(keyring.get_keyring())'
status.register('github')
If you don't want to use the default you can set a specific keyring like so:
::
from keyring.backends.file import PlaintextKeyring
status.register('github', keyring_backend=PlaintextKeyring())
i3pystatus will locate and set the credentials during the module loading process. Currently supported credentals are "password", "email" and "username".

View File

@ -14,6 +14,9 @@ tools for this which make this even easier:
periodically.
- Settings (already built into above classes) allow you to easily
specify user-modifiable attributes of your class for configuration.
- For modules that require credentials, it is recommended to add a
keyring_backend setting to allow users to specify their own backends
for retrieving sensitive credentials.
Required settings and default values are also handled.

View File

@ -2,10 +2,10 @@ from i3pystatus.core.util import KeyConstraintDict
from i3pystatus.core.exceptions import ConfigKeyError, ConfigMissingError
import inspect
import logging
import getpass
class SettingsBase:
"""
Support class for providing a nice and flexible settings interface
@ -18,6 +18,8 @@ class SettingsBase:
Settings are stored as attributes of self.
"""
__PROTECTED_SETTINGS = ["password", "email", "username"]
settings = (
("log_level", "Set to true to log error to .i3pystatus-<pid> file"),
)
@ -44,21 +46,25 @@ class SettingsBase:
return kwargs
def merge_with_parents_settings():
settings = tuple()
# getmro returns base classes according to Method Resolution Order
for cls in inspect.getmro(self.__class__):
if hasattr(cls, "settings"):
settings = settings + cls.settings
return settings
self.__name__ = "{}.{}".format(
self.__module__, self.__class__.__name__)
settings = merge_with_parents_settings()
settings = self.flatten_settings(settings)
sm = KeyConstraintDict(settings, self.required)
settings_source = get_argument_dict(args, kwargs)
protected = self.get_protected_settings(settings_source)
settings_source.update(protected)
try:
sm.update(settings_source)
except KeyError as exc:
@ -70,13 +76,48 @@ class SettingsBase:
raise ConfigMissingError(
type(self).__name__, missing=exc.keys) from exc
self.__name__ = "{}.{}".format(
self.__module__, self.__class__.__name__)
self.logger = logging.getLogger(self.__name__)
self.logger.setLevel(self.log_level)
self.init()
def get_protected_settings(self, settings_source):
"""
Attempt to retrieve protected settings from keyring if they are not already set.
"""
user_backend = settings_source.get('keyring_backend')
found_settings = dict()
for setting_name in self.__PROTECTED_SETTINGS:
# Nothing to do if the setting is already defined.
if settings_source.get(setting_name):
continue
setting = None
identifier = "%s.%s" % (self.__name__, setting_name)
if hasattr(self, 'required') and setting_name in getattr(self, 'required'):
setting = self.get_setting_from_keyring(identifier, user_backend)
elif hasattr(self, setting_name):
setting = self.get_setting_from_keyring(identifier, user_backend)
if setting:
found_settings.update({setting_name: setting})
return found_settings
def get_setting_from_keyring(self, setting_identifier, keyring_backend=None):
"""
Retrieves a protected setting from keyring
:param setting_identifier: must be in the format package.module.Class.setting
"""
# If a custom keyring backend has been defined, use it.
if keyring_backend:
return keyring_backend.get_password(setting_identifier, getpass.getuser())
# Otherwise try and use default keyring.
try:
import keyring
except ImportError:
pass
else:
return keyring.get_password(setting_identifier, getpass.getuser())
def init(self):
"""Convenience method which is called after all settings are set

View File

@ -23,11 +23,13 @@ class Github(IntervalModule):
password = ''
format = '{unread}'
interval = 600
keyring_backend = None
on_leftclick = 'open_github'
settings = (
('format', 'format string'),
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
('unread_marker', 'sets the string that the "unread" formatter shows when there are pending notifications'),
("username", ""),
("password", ""),

View File

@ -16,10 +16,12 @@ class IMAP(Backend):
settings = (
"host", "port",
"username", "password",
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
"ssl",
"mailbox",
)
required = ("host", "username", "password")
keyring_backend = None
port = 993
ssl = True

View File

@ -20,10 +20,12 @@ class ModsDeChecker(IntervalModule):
settings = (
("format",
"""Use {unread} as the formatter for number of unread posts"""),
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
("offset", """subtract number of posts before output"""),
"color", "username", "password"
)
required = ("username", "password")
keyring_backend = None
color = "#7181fe"
offset = 0

View File

@ -29,9 +29,11 @@ class pyLoad(IntervalModule):
"format",
"captcha_true", "captcha_false",
"download_true", "download_false",
"username", "password"
"username", "password",
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
)
required = ("username", "password")
keyring_backend = None
address = "http://127.0.0.1:8000"
format = "{captcha} {progress_all:.1f}% {speed:.1f} kb/s"

View File

@ -35,6 +35,7 @@ class Reddit(IntervalModule):
("format", "Format string used for output."),
("username", "Reddit username."),
("password", "Reddit password."),
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
("subreddit", "Subreddit to monitor. Uses frontpage if unspecified."),
("sort_by", "'hot', 'new', 'rising', 'controversial', or 'top'."),
("color", "Standard color."),
@ -48,6 +49,7 @@ class Reddit(IntervalModule):
format = "[{submission_subreddit}] {submission_title} ({submission_domain})"
username = ""
password = ""
keyring_backend = None
subreddit = ""
sort_by = "hot"
color = "#FFFFFF"

View File

@ -5,7 +5,6 @@ from bs4 import BeautifulSoup
class WhosOnLocation():
email = None
password = None
session = None
@ -68,9 +67,11 @@ class WOL(IntervalModule):
password = None
settings = (
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
'email',
'password'
)
keyring_backend = None
color_on_site = '#00FF00'
color_off_site = '#ff0000'

80
setting_util.py Executable file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env python
import glob
import inspect
import os
import keyring
import getpass
import sys
import signal
from i3pystatus import Module, SettingsBase
from i3pystatus.core import ClassFinder
from collections import defaultdict, OrderedDict
def signal_handler(signal, frame):
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
def get_int_in_range(prompt, _range):
while True:
answer = input(prompt)
try:
n = int(answer.strip())
if n in _range:
return n
else:
print("Value out of range!")
except ValueError:
print("Invalid input!")
modules = [os.path.basename(m.replace('.py', ''))
for m in glob.glob(os.path.join(os.path.dirname(__file__), "i3pystatus", "*.py"))
if not os.path.basename(m).startswith('_')]
protected_settings = SettingsBase._SettingsBase__PROTECTED_SETTINGS
class_finder = ClassFinder(Module)
credential_modules = defaultdict(dict)
for module_name in modules:
try:
module = class_finder.get_module(module_name)
clazz = class_finder.get_class(module)
members = [m[0] for m in inspect.getmembers(clazz) if not m[0].startswith('_')]
if any([hasattr(clazz, setting) for setting in protected_settings]):
credential_modules[clazz.__name__]['credentials'] = list(set(protected_settings) & set(members))
credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__)
elif hasattr(clazz, 'required'):
protected = []
required = getattr(clazz, 'required')
for setting in protected_settings:
if setting in required:
protected.append(setting)
if protected:
credential_modules[clazz.__name__]['credentials'] = protected
credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__)
except ImportError:
continue
choices = [k for k in credential_modules.keys()]
for idx, module in enumerate(choices, start=1):
print("%s - %s" % (idx, module))
index = get_int_in_range("Choose module:\n> ", range(1, len(choices) + 1))
module_name = choices[index - 1]
module = credential_modules[module_name]
for idx, setting in enumerate(module['credentials'], start=1):
print("%s - %s" % (idx, setting))
choices = module['credentials']
index = get_int_in_range("Choose setting for %s:\n> " % module_name, range(1, len(choices) + 1))
setting = choices[index - 1]
answer = getpass.getpass("Enter value for %s:\n> " % setting)
answer2 = getpass.getpass("Re-enter value\n> ")
if answer == answer2:
key = "%s.%s" % (module['key'], setting)
keyring.set_password(key, getpass.getuser(), answer)
print("%s set!" % setting)
else:
print("Values don't match - nothing set.")