Cleanup this and add refactoring around large constructors (add a parse method). Handle error cases better...

This commit is contained in:
Joshua Harlow
2012-06-09 12:35:39 -07:00
parent 28af2a6a9a
commit ff2f2f5842

View File

@@ -20,42 +20,70 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
import os.path import pwd
import cloudinit.util as util
from cloudinit import log as logging
from cloudinit import util
LOG = logging.getLogger(__name__)
class AuthKeyEntry(): class AuthKeyEntry(object):
# lines are options, keytype, base64-encoded key, comment """
# man page says the following which I did not understand: AUTHORIZED_KEYS FILE FORMAT
# The options field is optional; its presence is determined by whether AuthorizedKeysFile specifies the file containing public keys for public
# the line starts with a number or not (the options field never starts key authentication; if none is specified, the default is
# with a number) ~/.ssh/authorized_keys. Each line of the file contains one key (empty
options = None (because of the size of the public key encoding) up to a limit of 8 kilo-
keytype = None bytes, which permits DSA keys up to 8 kilobits and RSA keys up to 16
base64 = None kilobits. You don't want to type them in; instead, copy the
comment = None identity.pub, id_dsa.pub, or the id_rsa.pub file and edit it.
is_comment = False
line_in = "" sshd enforces a minimum RSA key modulus size for protocol 1 and protocol
2 keys of 768 bits.
The options (if present) consist of comma-separated option specifica-
tions. No spaces are permitted, except within double quotes. The fol-
lowing option specifications are supported (note that option keywords are
case-insensitive):
"""
def __init__(self, line, def_opt=None): def __init__(self, line, def_opt=None):
line = line.rstrip("\n\r") self.line = str(line)
self.line_in = line (self.value, self.components) = self._parse(self.line, def_opt)
if line.startswith("#") or line.strip() == "":
self.is_comment = True def _form_components(self, toks):
components = {}
if len(toks) == 1:
components['base64'] = toks[0]
elif len(toks) == 2:
components['base64'] = toks[0]
components['comment'] = toks[1]
elif len(toks) == 3:
components['keytype'] = toks[0]
components['base64'] = toks[1]
components['comment'] = toks[2]
return components
def get(self, piece):
return self.components.get(piece)
def _parse(self, in_line, def_opt):
line = in_line.rstrip("\r\n")
if line.startswith("#") or line.strip() == '':
return (False, {})
else: else:
ent = line.strip() ent = line.strip()
toks = ent.split(None, 3) toks = ent.split(None, 3)
if len(toks) == 1: tmp_components = {}
self.base64 = toks[0] if def_opt:
elif len(toks) == 2: tmp_components['options'] = def_opt
(self.base64, self.comment) = toks if len(toks) < 4:
elif len(toks) == 3: tmp_components.update(self._form_components(toks))
(self.keytype, self.base64, self.comment) = toks else:
elif len(toks) == 4:
i = 0
ent = line.strip()
quoted = False
# taken from auth_rsa_key_allowed in auth-rsa.c # taken from auth_rsa_key_allowed in auth-rsa.c
i = 0
quoted = False
try: try:
while (i < len(ent) and while (i < len(ent) and
((quoted) or (ent[i] not in (" ", "\t")))): ((quoted) or (ent[i] not in (" ", "\t")))):
@@ -67,124 +95,129 @@ class AuthKeyEntry():
quoted = not quoted quoted = not quoted
i = i + 1 i = i + 1
except IndexError: except IndexError:
self.is_comment = True return (False, {})
return
try: try:
self.options = ent[0:i] options = ent[0:i]
(self.keytype, self.base64, self.comment) = \ toks = ent[i + 1:].split(None, 3)
ent[i + 1:].split(None, 3) if options:
except ValueError: tmp_components['options'] = options
# we did not understand this line tmp_components.update(self._form_components(toks))
self.is_comment = True except (IndexError, ValueError):
return (False, {})
# We got some useful value!
return (True, tmp_components)
if self.options == None and def_opt: def __str__(self):
self.options = def_opt if not self.value:
return self.line
return
def debug(self):
print("line_in=%s\ncomment: %s\noptions=%s\nkeytype=%s\nbase64=%s\n"
"comment=%s\n" % (self.line_in, self.is_comment, self.options,
self.keytype, self.base64, self.comment)),
def __repr__(self):
if self.is_comment:
return(self.line_in)
else: else:
toks = [] toks = []
for e in (self.options, self.keytype, self.base64, self.comment): if 'options' in self.components:
if e: toks.append(self.components['options'])
toks.append(e) if 'keytype' in self.components:
toks.append(self.components['keytype'])
return(' '.join(toks)) if 'base64' in self.components:
toks.append(self.components['base64'])
if 'comment' in self.components:
toks.append(self.components['comment'])
if not toks:
return ''
return ' '.join(toks)
def update_authorized_keys(fname, keys): def update_authorized_keys(fname, keys):
# keys is a list of AuthKeyEntries lines = []
# key_prefix is the prefix (options) to prepend
try: try:
fp = open(fname, "r") if os.path.isfile(fname):
lines = fp.readlines() # lines have carriage return lines = util.load_file(fname).splitlines()
fp.close() except (IOError, OSError):
except IOError: LOG.exception("Error reading lines from %s", fname)
lines = [] lines = []
ka_stats = {} # keys_added status to_add = list(keys)
for k in keys:
ka_stats[k] = False
to_add = []
for key in keys:
to_add.append(key)
for i in range(0, len(lines)): for i in range(0, len(lines)):
ent = AuthKeyEntry(lines[i]) ent = AuthKeyEntry(lines[i])
if not ent.value:
continue
# Replace those with the same base64
for k in keys: for k in keys:
if k.base64 == ent.base64 and not k.is_comment: if not k.value:
continue
if k.get('base64') == ent.get('base64'):
# Replace it with our better one
ent = k ent = k
try: # Don't add it later
to_add.remove(k) to_add.remove(k)
except ValueError:
pass
lines[i] = str(ent) lines[i] = str(ent)
# now append any entries we did not match above # Now append any entries we did not match above
for key in to_add: for key in to_add:
lines.append(str(key)) lines.append(str(key))
if len(lines) == 0: # Ensure it ends with a newline
return("") lines.append('')
else: return '\n'.join(lines)
return('\n'.join(lines) + "\n")
def setup_user_keys(keys, user, key_prefix, log=None): def setup_user_keys(keys, user, key_prefix, sshd_config_fn="/etc/ssh/sshd_config"):
import pwd
saved_umask = os.umask(077)
pwent = pwd.getpwnam(user) pwent = pwd.getpwnam(user)
ssh_dir = '%s/.ssh' % pwent.pw_dir ssh_dir = os.path.join(pwent.pw_dir, '.ssh')
if not os.path.exists(ssh_dir): if not os.path.exists(ssh_dir):
os.mkdir(ssh_dir) util.ensure_dir(ssh_dir, mode=0700)
os.chown(ssh_dir, pwent.pw_uid, pwent.pw_gid) util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)
try:
ssh_cfg = parse_ssh_config()
akeys = ssh_cfg.get("AuthorizedKeysFile", "%h/.ssh/authorized_keys")
akeys = akeys.replace("%h", pwent.pw_dir)
akeys = akeys.replace("%u", user)
if not akeys.startswith('/'):
akeys = os.path.join(pwent.pw_dir, akeys)
authorized_keys = akeys
except Exception:
authorized_keys = '%s/.ssh/authorized_keys' % pwent.pw_dir
if log:
util.logexc(log)
key_entries = [] key_entries = []
for k in keys: for k in keys:
ke = AuthKeyEntry(k, def_opt=key_prefix) key_entries.append(AuthKeyEntry(k, def_opt=key_prefix))
key_entries.append(ke)
content = update_authorized_keys(authorized_keys, key_entries) with util.SeLinuxGuard(ssh_dir, recursive=True):
util.write_file(authorized_keys, content, 0600) try:
"""
AuthorizedKeysFile may contain tokens
of the form %T which are substituted during connection set-up.
The following tokens are defined: %% is replaced by a literal
'%', %h is replaced by the home directory of the user being
authenticated and %u is replaced by the username of that user.
"""
ssh_cfg = parse_ssh_config(sshd_config_fn)
akeys = ssh_cfg.get("authorizedkeysfile", '')
akeys = akeys.strip()
if not akeys:
akeys = "%h/.ssh/authorized_keys"
akeys = akeys.replace("%h", pwent.pw_dir)
akeys = akeys.replace("%u", user)
akeys = akeys.replace("%%", '%')
if not akeys.startswith('/'):
akeys = os.path.join(pwent.pw_dir, akeys)
authorized_keys = akeys
except (IOError, OSError):
authorized_keys = os.path.join(ssh_dir, 'authorized_keys')
LOG.exception(("Failed extracting 'AuthorizedKeysFile' in ssh config"
" from %s, using 'AuthorizedKeysFile' file %s instead."),
sshd_config_fn, authorized_keys)
os.chown(authorized_keys, pwent.pw_uid, pwent.pw_gid) content = update_authorized_keys(authorized_keys, key_entries)
util.restorecon_if_possible(ssh_dir, recursive=True) util.ensure_dir(os.path.dirname(authorized_keys), mode=0700)
util.write_file(authorized_keys, content, mode=0600)
os.umask(saved_umask) util.chownbyid(authorized_keys, pwent.pw_uid, pwent.pw_gid)
def parse_ssh_config(fname="/etc/ssh/sshd_config"): def parse_ssh_config(fname):
"""
The file contains keyword-argu-ment pairs, one per line.
Lines starting with '#' and empty lines are interpreted as comments.
Note: key-words are case-insensitive and arguments are case-sensitive
"""
ret = {} ret = {}
fp = open(fname) if not os.path.isfile(fname):
for l in fp.readlines(): return ret
l = l.strip() for line in util.load_file(fname).splitlines():
if not l or l.startswith("#"): line = line.strip()
if not line or line.startswith("#"):
continue continue
key, val = l.split(None, 1) (key, val) = line.split(None, 1)
ret[key] = val key = key.strip().lower()
fp.close() if key:
return(ret) ret[key] = val
return ret