├── .gitignore ├── CHANGELOG ├── MANIFEST.in ├── README.txt ├── scripts └── ssh-authorizer ├── setup.cfg ├── setup.py └── ssh_authorizer ├── __init__.py ├── __main__.py ├── commands.py └── helpers.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.3 - 2013-03-20 2 | 3 | * Now works under python 2 4 | * Refactoring around argparse 5 | 6 | 1.2 - 2012-12-21 7 | 8 | * Make beautiful output in the console 9 | * Some refactoring 10 | 11 | 1.1 - 2012-12-21 12 | 13 | * First public release 14 | * It's just work 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include scripts/ssh-authorizer 2 | include README.txt 3 | include setup.cfg 4 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Manager for remote ~/.ssh/authorized_keys. 2 | 3 | Usage: ssh-authorizer {help,get,add,del,test} [--raw] ssh_string ... 4 | 5 | command: 6 | help: Print this help. 7 | 8 | get: Display remote authorized_keys. 9 | get --raw: Display without formating. 10 | 11 | add: Add keys to remote authorized_keys. 12 | del: Delete keys from remote authorized_keys. 13 | test: Test keys exist in remote authorized_keys. 14 | 15 | ssh_string: String with connect info: [user@]host[:port]. 16 | By default user is current system user, port=22. 17 | 18 | keys: For commands "add" and "test" this is list of files with keys. 19 | If empty -- "~/ssh/id_rsa.pub" used. 20 | 21 | keys: For commad "del" this is key indeces for delete. 22 | See "get" without "--raw". 23 | 24 | Examples: 25 | 26 | ssh-authorizer get username@hostname 27 | Get authorized_keys in host hostname for user username. 28 | 29 | ssh-authorizer add user@host 30 | Add your local "~/ssh/id_rsa.pub" to remote "~/ssh/authorized_keys". 31 | 32 | ssh-authorizer add user@host key.pub key2.pub 33 | Add "key.pub" "key2.pub" to remote "~/ssh/authorized_keys". 34 | 35 | ssh-authorizer del user@host 1 3 36 | Delete fist and third keys from remote "~/ssh/authorized_keys". 37 | 38 | ssh-authorizer test user@host key.pub key2.pub 39 | "key.pub" "key2.pub" already in remote "~/ssh/authorized_keys"? Check it. 40 | 41 | TODO: 42 | 43 | ssh-authorizer del user@host 44 | Delete your "~/ssh/id_rsa.pub" from remote "~/ssh/authorized_keys". 45 | 46 | ssh-authorizer del user@host zzz@macbook 47 | Delete key "zzz@macbook" from remote "~/ssh/authorized_keys". 48 | 49 | get --short: Like "get", but without key hashes. 50 | 51 | Human readable errors. 52 | -------------------------------------------------------------------------------- /scripts/ssh-authorizer: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from ssh_authorizer.__main__ import main 4 | 5 | main() 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [sdist] 2 | force-manifest = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='ssh_authorizer', 5 | version='1.3', 6 | description='Manager for remote ~/.ssh/authorized_keys.', 7 | author='Alexander Zelenyak aka ZZZ', 8 | author_email='zzz.sochi@gmail.com', 9 | url='https://github.com/zzzsochi/ssh-authorizer/', 10 | packages=['ssh_authorizer'], 11 | scripts=['scripts/ssh-authorizer'], 12 | install_requires=['sh'], 13 | license='BSD', 14 | platforms='Unix', 15 | classifiers=[ 16 | 'Operating System :: Unix', 17 | 'Programming Language :: Python :: 2', 18 | 'Programming Language :: Python :: 2.7', 19 | 'Programming Language :: Python :: 3', 20 | 'Programming Language :: Python :: 3.3', 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /ssh_authorizer/__init__.py: -------------------------------------------------------------------------------- 1 | """Manager for remote ~/.ssh/authorized_keys. 2 | 3 | Usage: ssh-authorizer {help,get,add,del,test} [--raw] ssh_string ... 4 | 5 | command: 6 | help: Print this help. 7 | 8 | get: Display remote authorized_keys. 9 | get --raw: Display without formating. 10 | 11 | add: Add keys to remote authorized_keys. 12 | del: Delete keys from remote authorized_keys. 13 | test: Test keys exist in remote authorized_keys. 14 | 15 | ssh_string: String with connect info: [user@]host[:port]. 16 | By default user is current system user, port=22. 17 | 18 | keys: For commands "add" and "test" this is list of files with keys. 19 | If empty -- "~/ssh/id_rsa.pub" used. 20 | 21 | keys: For commad "del" this is key indeces for delete. 22 | See "get" without "--raw". 23 | 24 | Examples: 25 | 26 | ssh-authorizer get username@hostname 27 | Get authorized_keys in host hostname for user username. 28 | 29 | ssh-authorizer add user@host 30 | Add your local "~/ssh/id_rsa.pub" to remote "~/ssh/authorized_keys". 31 | 32 | ssh-authorizer add user@host key.pub key2.pub 33 | Add "key.pub" "key2.pub" to remote "~/ssh/authorized_keys". 34 | 35 | ssh-authorizer del user@host 1 3 36 | Delete fist and third keys from remote "~/ssh/authorized_keys". 37 | 38 | ssh-authorizer test user@host key.pub key2.pub 39 | "key.pub" "key2.pub" already in remote "~/ssh/authorized_keys"? Check it. 40 | 41 | TODO: 42 | 43 | ssh-authorizer del user@host 44 | Delete your "~/ssh/id_rsa.pub" from remote "~/ssh/authorized_keys". 45 | 46 | ssh-authorizer del user@host zzz@macbook 47 | Delete key "zzz@macbook" from remote "~/ssh/authorized_keys". 48 | 49 | get --short: Like "get", but without key hashes. 50 | 51 | Human readable errors. 52 | """ 53 | -------------------------------------------------------------------------------- /ssh_authorizer/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | ssh-authorizer help 4 | ssh-authorizer get zzz@dev.durakov.net 5 | ssh-authorizer add zzz@dev.durakov.net key1 key2... 6 | ssh-authorizer del zzz@dev.durakov.net id1 id2... 7 | ssh-authorizer test zzz@dev.durakov.net key1 key2... 8 | """ 9 | 10 | # import sys 11 | import logging 12 | import argparse 13 | 14 | import ssh_authorizer.commands as commands 15 | from ssh_authorizer.helpers import parse_ssh_string 16 | 17 | 18 | logging.basicConfig(format='%(message)s', level=logging.INFO) 19 | 20 | 21 | def help(args): 22 | commands.help() 23 | 24 | 25 | def get(args): 26 | user, host, port = parse_ssh_string(args.ssh_string[0]) 27 | commands.get(user, host, port, args.raw) 28 | 29 | 30 | def add(args): 31 | user, host, port = parse_ssh_string(args.ssh_string[0]) 32 | commands.add(user, host, port, args.keys) 33 | 34 | 35 | def delete(args): 36 | user, host, port = parse_ssh_string(args.ssh_string[0]) 37 | commands.delete(user, host, port, args.keys) 38 | 39 | 40 | def test(args): 41 | user, host, port = parse_ssh_string(args.ssh_string[0]) 42 | commands.test(user, host, port, args.keys) 43 | 44 | 45 | def main(): 46 | parser = argparse.ArgumentParser(description='Manager for remote ~/.ssh/authorized_keys.') 47 | subparsers = parser.add_subparsers(dest='cmd', help='Commands') 48 | # parser_help = subparsers.add_parser('help', help='') 49 | 50 | parser_get = subparsers.add_parser('help', help='Display help information') 51 | parser_get.set_defaults(func=help) 52 | 53 | parser_get = subparsers.add_parser('get', help='Display remote authorized_keys') 54 | parser_get.add_argument('--raw', action='store_true', help='Display as is.') 55 | parser_get.add_argument('ssh_string', nargs=1, help='Remote host') 56 | parser_get.set_defaults(func=get) 57 | 58 | parser_add = subparsers.add_parser('add', help='Add keys to remote authorized_keys') 59 | parser_add.add_argument('ssh_string', nargs=1, help='Remote host') 60 | parser_add.add_argument('keys', nargs='*', help='Keys for add') 61 | parser_add.set_defaults(func=add) 62 | 63 | parser_del = subparsers.add_parser('del', help='Delete keys from remote authorized_keys') 64 | parser_del.add_argument('ssh_string', nargs=1, help='Remote host') 65 | parser_del.add_argument('keys', nargs='+', help='Keys indexes for remote') 66 | parser_del.set_defaults(func=delete) 67 | 68 | parser_test = subparsers.add_parser('test', help='Test keys exist in remote authorized_keys') 69 | parser_test.add_argument('ssh_string', nargs=1, help='Remote host') 70 | parser_test.add_argument('keys', nargs='*', help='Keys indexes for remote') 71 | parser_test.set_defaults(func=test) 72 | 73 | args = parser.parse_args() 74 | args.func(args) 75 | 76 | 77 | if __name__ == '__main__': 78 | main() 79 | -------------------------------------------------------------------------------- /ssh_authorizer/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | 4 | import sh 5 | 6 | from ssh_authorizer.helpers import (SSHController, 7 | SCPController, 8 | NoSuchFileError, 9 | get_authorized_keys, 10 | set_authorized_keys, 11 | create_authorized_keys_file, 12 | load_local_keys) 13 | 14 | 15 | def help(): 16 | from ssh_authorizer import __doc__ as doc 17 | print(doc) 18 | 19 | 20 | def get(user, host, port, raw): 21 | if not raw: 22 | ssh_controller = SSHController(user, host, port) 23 | 24 | try: 25 | keys = [k for k in get_authorized_keys(controller=ssh_controller) if k] 26 | except sh.ErrorReturnCode_1: 27 | sys.exit(1) 28 | except NoSuchFileError: 29 | keys = [] 30 | 31 | if len(keys) == 1: 32 | print("{c.user}@{c.host}:{c.port} - found one key:\n".format(c=ssh_controller)) 33 | elif keys: 34 | print("{c.user}@{c.host}:{c.port} - found {0} keys:\n".format(len(keys), c=ssh_controller)) 35 | else: 36 | print("{c.user}@{c.host}:{c.port} - not found keys".format(c=ssh_controller)) 37 | 38 | for n, key in enumerate(keys): 39 | if key: 40 | print('{0}: {1}'.format(n + 1, key)) 41 | 42 | else: 43 | ssh_controller = SSHController(user, host, port) 44 | 45 | try: 46 | keys = get_authorized_keys(controller=ssh_controller) 47 | except sh.ErrorReturnCode_1: 48 | sys.exit(1) 49 | except NoSuchFileError: 50 | keys = [] 51 | 52 | print('\n'.join(keys)) 53 | 54 | return 55 | 56 | 57 | def add(user, host, port, key_files): 58 | local_keys = load_local_keys(key_files) 59 | 60 | ssh_controller = SSHController(user, host, port) 61 | 62 | try: 63 | remote_keys = get_authorized_keys(ssh_controller) 64 | except sh.ErrorReturnCode_1: 65 | sys.exit(1) 66 | except NoSuchFileError: 67 | create_authorized_keys_file(ssh_controller) 68 | remote_keys = [] 69 | 70 | new_keys = [] 71 | already_keys = [] 72 | 73 | for key_file in key_files: 74 | key = local_keys[key_file] 75 | 76 | if key not in remote_keys: 77 | new_keys.append(key) 78 | else: 79 | already_keys.append(key_file) 80 | 81 | if already_keys: 82 | logging.info('{c.user}@{c.host}:{c.port} - already in authorized_keys: "{0}"' 83 | .format('", "'.join(already_keys), c=ssh_controller)) 84 | 85 | if new_keys: 86 | keys = remote_keys + new_keys 87 | 88 | scp_controller = SCPController(user, host, port) 89 | scp_controller.password = ssh_controller.password 90 | 91 | try: 92 | set_authorized_keys(scp_controller, keys) 93 | except sh.ErrorReturnCode_1: 94 | sys.exit(1) 95 | 96 | 97 | def delete(user, host, port, key_ids): 98 | ssh_controller = SSHController(user, host, port) 99 | 100 | try: 101 | remote_keys = [k for k in get_authorized_keys(ssh_controller) if k] 102 | except NoSuchFileError: 103 | logging.critical('{c.user}@{c.host}:{c.port} - error: not found authorized_keys' 104 | .format(c=ssh_controller)) 105 | sys.exit(1) 106 | 107 | try: 108 | for key_id in [int(i) for i in sorted(key_ids, reverse=True)]: 109 | del remote_keys[key_id - 1] 110 | except IndexError: 111 | logging.critical('{c.user}@{c.host}:{c.port} - error: not found key indexes' 112 | .format(c=ssh_controller)) 113 | sys.exit(1) 114 | 115 | scp_controller = SCPController(user, host, port) 116 | scp_controller.password = ssh_controller.password 117 | set_authorized_keys(scp_controller, remote_keys) 118 | 119 | 120 | def test(user, host, port, key_files): 121 | local_keys = load_local_keys(key_files) 122 | 123 | ssh_controller = SSHController(user, host, port) 124 | 125 | try: 126 | remote_keys = [k for k in get_authorized_keys(ssh_controller) if k] 127 | except NoSuchFileError: 128 | logging.info('{c.user}@{c.host}:{c.port} - not found authorized_keys' 129 | .format(c=ssh_controller)) 130 | remote_keys = [] 131 | 132 | oks = [] 133 | 134 | for key_file in key_files: 135 | ok = local_keys[key_file] in remote_keys 136 | oks.append(ok) 137 | print('{0}: {1}'.format(key_file, 'ok' if ok else 'fail')) 138 | 139 | return not int(all(oks)) 140 | -------------------------------------------------------------------------------- /ssh_authorizer/helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logging 4 | from getpass import getpass 5 | from tempfile import NamedTemporaryFile 6 | 7 | from sh import ssh, scp, ErrorReturnCode_1 8 | 9 | 10 | class NoSuchFileError(Exception): 11 | pass 12 | 13 | 14 | def parse_ssh_string(ssh_string): 15 | """ 16 | >>> parse_ssh_string('user@host:13') 17 | ('user', 'host', 13) 18 | >>> parse_ssh_string('user@host') 19 | ('user', 'host', 22) 20 | >>> parse_ssh_string('host:13')[1:] 21 | ('host', 13) 22 | >>> parse_ssh_string('host')[1:] 23 | ('host', 22) 24 | >>> import os 25 | >>> os_user = os.environ.get('USER') 26 | >>> parse_ssh_string('host:13')[0] == os_user 27 | True 28 | >>> parse_ssh_string('host')[0] == os_user 29 | True 30 | """ 31 | 32 | user = os.environ.get('USER') 33 | port = 22 34 | 35 | if '@' in ssh_string: 36 | user, ssh_string = ssh_string.split('@') 37 | 38 | if ':' in ssh_string: 39 | host, port = ssh_string.split(':') 40 | port = int(port) 41 | else: 42 | host = ssh_string 43 | 44 | return user, host, port 45 | 46 | 47 | def load_local_keys(key_files): 48 | if not key_files: 49 | key_files.append(os.path.expanduser('~/.ssh/id_rsa.pub')) 50 | logging.info('Loading local id_rsa.pub') 51 | else: 52 | logging.info('Loading keys: {0}'.format(', '.join(key_files))) 53 | 54 | local_keys = {} 55 | 56 | for key_file in key_files: 57 | with open(os.path.expanduser(key_file), 'rt') as f: 58 | key_data = f.read().strip() 59 | local_keys[key_file] = key_data 60 | 61 | return local_keys 62 | 63 | 64 | class Controller(object): 65 | out = b'' 66 | user = None 67 | host = None 68 | port = 22 69 | password = None 70 | 71 | def __init__(self, user, host, port=22): 72 | self.user = user 73 | self.host = host 74 | self.port = port or 22 75 | 76 | def __call__(self, *args, **kwargs): 77 | logging.debug('run command: "{0}"'.format(self.process.ran)) 78 | 79 | def out_iteract(self, char, stdin, process): 80 | if isinstance(char, str): 81 | self.out += char.encode('utf8') 82 | else: 83 | self.out += char 84 | 85 | out = self.out.decode('utf-8', errors='ignore') 86 | 87 | if out.endswith('password: '): 88 | self.clear() 89 | stdin.put(self.get_password() + '\n') 90 | 91 | def get_password(self): 92 | logging.debug('request password') 93 | 94 | if not self.password: 95 | prompt = '{c.user}@{c.host}:{c.port} - need password: '.format(c=self) 96 | self.password = getpass(prompt) 97 | 98 | return self.password 99 | 100 | def clear(self): 101 | self.out = b'' 102 | self.error = b'' 103 | 104 | def wait(self): 105 | return self.process.wait() 106 | 107 | 108 | class SSHController(Controller): 109 | no_such_file_error = False 110 | 111 | def __call__(self, *args, **kwargs): 112 | self.process = ssh( 113 | '-o UserKnownHostsFile=/dev/null', 114 | '-o StrictHostKeyChecking=no', 115 | '-o LogLevel=quiet', 116 | '{0}@{1}'.format(self.user, self.host), 117 | '-p', self.port, 118 | 'LANG=C', *args, 119 | _out=self.out_iteract, _out_bufsize=0, _tty_in=True, 120 | **kwargs) 121 | 122 | super(SSHController, self).__call__(*args, **kwargs) 123 | 124 | def out_iteract(self, char, stdin, process): 125 | super(SSHController, self).out_iteract(char, stdin, process) 126 | 127 | out = self.out.decode('utf-8', errors='ignore') 128 | 129 | if out.endswith('No such file or directory'): 130 | self.no_such_file_error = True 131 | process.kill() 132 | 133 | 134 | class SCPController(Controller): 135 | def __call__(self, local_file, remote_file, **kwargs): 136 | self.process = scp( 137 | '-o UserKnownHostsFile=/dev/null', 138 | '-o StrictHostKeyChecking=no', 139 | '-o LogLevel=quiet', 140 | '-P', self.port, 141 | local_file, 142 | '{0}@{1}:{2}'.format(self.user, self.host, remote_file), 143 | _out=self.out_iteract, _out_bufsize=0, _tty_in=True, 144 | **kwargs) 145 | 146 | super(SCPController, self).__call__(local_file, remote_file, **kwargs) 147 | 148 | 149 | def get_authorized_keys(controller): 150 | logging.info('{c.user}@{c.host}:{c.port} - getting authorized_keys'.format(c=controller)) 151 | 152 | try: 153 | controller.clear() 154 | controller('cat ~/.ssh/authorized_keys') 155 | controller.wait() 156 | 157 | except ErrorReturnCode_1: 158 | if controller.no_such_file_error: 159 | raise NoSuchFileError() 160 | else: 161 | logging.critical(controller.out.decode('utf8', errors='ignore')) 162 | raise 163 | 164 | except Exception: 165 | logging.critical(controller.out.decode('utf8', errors='ignore')) 166 | raise 167 | 168 | out = controller.out.decode('utf8', errors='ignore') 169 | return [line.strip() for line in out.split('\n')] 170 | 171 | 172 | def create_authorized_keys_file(controller): 173 | logging.info('{c.user}@{c.host}:{c.port} - creating ~/.ssh'.format(c=controller)) 174 | 175 | try: 176 | controller.clear() 177 | controller('mkdir -p ~/.ssh') 178 | controller.wait() 179 | 180 | except ErrorReturnCode_1: 181 | if controller.no_such_file_error: 182 | raise NoSuchFileError() 183 | else: 184 | logging.critical(controller.out.decode('utf8', errors='ignore')) 185 | raise 186 | 187 | except Exception: 188 | logging.critical(controller.out.decode('utf8', errors='ignore')) 189 | raise 190 | 191 | 192 | def set_authorized_keys(controller, keys): 193 | logging.info('{c.user}@{c.host}:{c.port} - writing authorized_keys'.format(c=controller)) 194 | 195 | if sys.version_info.major >= 3: 196 | buffering = 'buffering' 197 | else: 198 | buffering = 'bufsize' 199 | 200 | with NamedTemporaryFile('w+b', **{buffering: 0}) as tmp: 201 | data = '\n'.join(keys) 202 | tmp.write(data.encode('utf8')) 203 | 204 | if data and data[-1] != '\n': 205 | tmp.write(b'\n') 206 | 207 | try: 208 | controller.clear() 209 | controller(tmp.name, '~/.ssh/authorized_keys') 210 | controller.wait() 211 | except Exception: 212 | logging.critical(controller.out.decode('utf8', errors='ignore')) 213 | raise 214 | --------------------------------------------------------------------------------