Cleanup this and add refactoring around large constructors (add a parse method). Handle error cases better...
This commit is contained in:
@@ -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
|
||||||
|
Reference in New Issue
Block a user